diff --git a/conservancy/usethesource/admin.py b/conservancy/usethesource/admin.py index bb82aa25..e1181c55 100644 --- a/conservancy/usethesource/admin.py +++ b/conservancy/usethesource/admin.py @@ -1,5 +1,6 @@ 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() diff --git a/conservancy/usethesource/emails.py b/conservancy/usethesource/emails.py index 8fde9d6f..082e10db 100644 --- a/conservancy/usethesource/emails.py +++ b/conservancy/usethesource/emails.py @@ -1,11 +1,42 @@ 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} ' - 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(): diff --git a/conservancy/usethesource/migrations/0005_candidate_email_message_id.py b/conservancy/usethesource/migrations/0005_candidate_email_message_id.py new file mode 100644 index 00000000..a9f1c295 --- /dev/null +++ b/conservancy/usethesource/migrations/0005_candidate_email_message_id.py @@ -0,0 +1,19 @@ +# 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), + ), + ] diff --git a/conservancy/usethesource/models.py b/conservancy/usethesource/models.py index 84afcf20..70b05b0c 100644 --- a/conservancy/usethesource/models.py +++ b/conservancy/usethesource/models.py @@ -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'] diff --git a/conservancy/usethesource/tests.py b/conservancy/usethesource/tests.py index e13d83ed..9e5af489 100644 --- a/conservancy/usethesource/tests.py +++ b/conservancy/usethesource/tests.py @@ -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()