Changeset - 3cccc3bdd90e
[Not reviewed]
0 4 1
Ben Sturmfels (bsturmfels) - 3 months ago 2024-01-26 06:49:03
ben@sturm.com.au
Email announcement about new UTS candidate
5 files changed with 81 insertions and 10 deletions:
0 comments (0 inline, 0 general)
conservancy/usethesource/admin.py
Show inline comments
 
from django.contrib import admin
 

	
 
from .emails import make_candidate_email
 
from .models import Candidate, Comment
 

	
 

	
...
 
@@ -25,3 +26,11 @@ class CandidateAdmin(admin.ModelAdmin):
 
    ]
 
    inlines = [CommentInline]
 
    prepopulated_fields = {'slug': ['name']}
 

	
 
    def save_model(self, request, obj, form, change):
 
        send_email = obj.id is None
 
        super().save_model(request, obj, form, change)
 
        if send_email:
 
            # Announce the new candidate
 
            email = make_candidate_email(obj, request.user)
 
            email.send()
conservancy/usethesource/emails.py
Show inline comments
 
from django.core.mail import EmailMessage
 
from django.shortcuts import reverse
 

	
 
SENDER = 'compliance@sfconservancy.org'
 
LIST_RECIPIENT = 'nutbush@lists.sfconservancy.org'
 

	
 

	
 
def make_candidate_email(candidate, user):
 
    """The initial email announcing the new candidate."""
 
    subject = candidate.name
 
    signature = user.get_full_name() or user.username
 
    sender = f'{signature} <{SENDER}>'
 
    to = [LIST_RECIPIENT]
 
    body = f'''\
 
We've just published the following new candidate:
 

	
 
{candidate.name}
 
Vendor: {candidate.vendor}
 
Device: {candidate.device}
 
Released: {candidate.release_date}
 

	
 
{candidate.description}
 

	
 
To download this candidate's source and binary image, visit:
 
https://sfconservancy.org{reverse('usethesource:candidate', kwargs={'slug': candidate.slug})}
 

	
 
--
 
{signature}
 
'''
 
    headers = {'Message-ID': candidate.email_message_id}
 
    return EmailMessage(subject, body, sender, to, headers=headers)
 

	
 

	
 
def make_comment_email(comment):
 
    """Email when a comment is added to a candidate."""
 
    subject = f'Re: {comment.candidate.name}'
 
    signature = comment.user.get_full_name() or comment.user.username
 
    sender = f'{signature} <compliance@sfconservancy.org>'
 
    to = ['nutbush@lists.sfconservancy.org']
 
    sender = f'{signature} <{SENDER}>'
 
    to = [LIST_RECIPIENT]
 
    body = f'{comment.message}\n\n--\n{signature}'
 
    headers = {'Message-ID': comment.email_message_id}
 
    if in_reply_to := comment.in_reply_to():
conservancy/usethesource/migrations/0005_candidate_email_message_id.py
Show inline comments
 
new file 100644
 
# Generated by Django 3.2.19 on 2024-01-26 01:36
 

	
 
import conservancy.usethesource.models
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('usethesource', '0004_auto_20240125_2352'),
 
    ]
 

	
 
    operations = [
 
        migrations.AddField(
 
            model_name='candidate',
 
            name='email_message_id',
 
            field=models.CharField(default=conservancy.usethesource.models.gen_message_id, max_length=255),
 
        ),
 
    ]
conservancy/usethesource/models.py
Show inline comments
...
 
@@ -4,6 +4,11 @@ from django.contrib.auth.models import User
 
from django.db import models
 

	
 

	
 
def gen_message_id():
 
    """Generate a time-based identifier for use in "In-Reply-To" header."""
 
    return f'<{uuid.uuid1()}@sfconservancy.org>'
 

	
 

	
 
class Candidate(models.Model):
 
    """A source/binary release we'd like to verify CCS status of."""
 

	
...
 
@@ -16,6 +21,7 @@ class Candidate(models.Model):
 
    source_url = models.URLField()
 
    binary_url = models.URLField(blank=True)
 
    ordering = models.SmallIntegerField(default=0)
 
    email_message_id = models.CharField(max_length=255, default=gen_message_id)
 

	
 
    class Meta:
 
        ordering = ['ordering', 'name']
...
 
@@ -24,11 +30,6 @@ class Candidate(models.Model):
 
        return self.name
 

	
 

	
 
def gen_message_id():
 
    """Generate a time-based identifier for use in "In-Reply-To" header."""
 
    return f'<{uuid.uuid1()}@sfconservancy.org>'
 

	
 

	
 
class Comment(models.Model):
 
    """A comment about experiences or learnings building the candidate."""
 

	
...
 
@@ -48,14 +49,14 @@ class Comment(models.Model):
 
            return None
 

	
 
    def in_reply_to(self):
 
        """Determine the message_id of the previous comment.
 
        """Determine the message_id of the previous comment or the candidate.
 

	
 
        Used for email threading.
 
        """
 
        if prev_comment := self._find_previous_comment():
 
            return prev_comment.email_message_id
 
        else:
 
            return None
 
            return self.candidate.email_message_id
 

	
 
    class Meta:
 
        ordering = ['id']
conservancy/usethesource/tests.py
Show inline comments
...
 
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
 
import pytest
 

	
 
from . import models
 
from .emails import make_comment_email
 
from .emails import make_candidate_email, make_comment_email
 
from .models import Candidate, Comment
 

	
 

	
...
 
@@ -28,6 +28,17 @@ def test_message_id():
 
    assert re.match(r'<.+@.+>', models.gen_message_id())
 

	
 

	
 
@pytest.mark.django_db
 
def test_candidate_email():
 
    user = User.objects.create(first_name='Test', last_name='User')
 
    candidate = make_candidate(name='Test Candidate', save=True)
 
    email = make_candidate_email(candidate, user)
 
    assert 'Message-ID' in email.extra_headers
 
    assert email.subject == 'Test Candidate'
 
    assert 'Test Candidate' in email.body
 
    assert 'Test User' in email.body
 

	
 

	
 
@pytest.mark.django_db
 
def test_comment_knows_comment_its_replying_to():
 
    user = User.objects.create()
0 comments (0 inline, 0 general)