Changeset - cadd69061f18
[Not reviewed]
conservancy/usethesource/emails.py
Show inline comments
 
new file 100644
 
from django.core.mail import EmailMessage
 

	
 

	
 
def make_comment_email(comment):
 
    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']
 
    body = f'{comment.message}\n\n--\n{signature}'
 
    headers = {'Message-ID': comment.email_message_id}
 
    if in_reply_to := comment.in_reply_to():
 
        # From my testing, both "In-Reply-To" and "References" headers trigger
 
        # email threading in Thunderbind. Sticking to "In-Reply-To" for now.
 
        headers['In-Reply-To'] = in_reply_to
 
    return EmailMessage(subject, body, sender, to, headers=headers)
conservancy/usethesource/migrations/0003_comment_email_message_id.py
Show inline comments
 
new file 100644
 
# Generated by Django 3.2.19 on 2024-01-25 20:59
 

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

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('usethesource', '0002_auto_20231030_1830'),
 
    ]
 

	
 
    operations = [
 
        migrations.AddField(
 
            model_name='comment',
 
            name='email_message_id',
 
            field=models.CharField(default=conservancy.usethesource.models.gen_message_id, max_length=255),
 
        ),
 
    ]
conservancy/usethesource/migrations/0004_auto_20240125_2352.py
Show inline comments
 
new file 100644
 
# Generated by Django 3.2.19 on 2024-01-25 23:52
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('usethesource', '0003_comment_email_message_id'),
 
    ]
 

	
 
    operations = [
 
        migrations.AlterField(
 
            model_name='candidate',
 
            name='description',
 
            field=models.TextField(blank=True),
 
        ),
 
        migrations.AlterField(
 
            model_name='candidate',
 
            name='release_date',
 
            field=models.DateField(blank=True, null=True),
 
        ),
 
    ]
conservancy/usethesource/models.py
Show inline comments
 
import uuid
 

	
 
from django.contrib.auth.models import User
 
from django.db import models
 

	
 

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

	
 
    name = models.CharField('Candidate name', max_length=50)
 
    slug = models.SlugField(max_length=50, unique=True)
 
    vendor = models.CharField('Vendor name', max_length=50)
 
    device = models.CharField('Device name', max_length=50)
 
    release_date = models.DateField()
 
    description = models.TextField()
 
    release_date = models.DateField(null=True, blank=True)
 
    description = models.TextField(blank=True)
 
    source_url = models.URLField()
 
    binary_url = models.URLField(blank=True)
 
    ordering = models.SmallIntegerField(default=0)
...
 
@@ -20,14 +24,38 @@ 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."""
 

	
 
    candidate = models.ForeignKey(Candidate, on_delete=models.CASCADE)
 
    user = models.ForeignKey(User, on_delete=models.PROTECT)
 
    time = models.DateTimeField(auto_now_add=True)
 
    message = models.TextField()
 
    email_message_id = models.CharField(max_length=255, default=gen_message_id)
 

	
 
    def __str__(self):
 
        return f'{self.candidate.name}, {self.user}, {self.time}'
 
        return f'{self.id}: {self.candidate.name}, {self.user}, {self.time}'
 

	
 
    def _find_previous_comment(self):
 
        try:
 
            return self.__class__.objects.filter(candidate=self.candidate, id__lt=self.id).latest('id')
 
        except self.__class__.DoesNotExist:
 
            return None
 

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

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

	
 
    class Meta:
 
        ordering = ['id']
conservancy/usethesource/templates/usethesource/comment_form.html
Show inline comments
 
<form hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:add_comment' slug=candidate.slug %}" method="post">
 
<form hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:add_comment' slug=candidate.slug %}">
 
  {% csrf_token %}
 
  {{ form.message }}
 
  <div class="mt2">
conservancy/usethesource/templates/usethesource/edit_comment_form.html
Show inline comments
 
<form class="mb3" hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:edit_comment' comment_id=comment.id %}" method="post">
 
<form class="mb3" hx-target="this" hx-swap="outerHTML" hx-post="{% url 'usethesource:edit_comment' comment_id=comment.id %}">
 
  {% csrf_token %}
 
  {{ form.message }}
 
  <div class="mt2">
conservancy/usethesource/templates/usethesource/save_button_partial.html
Show inline comments
 
<button type="submit" class="b white bg-green pv2 ph3" style="border: none">Save</button>
 
<button type="submit" class="b white bg-green pv2 ph3" style="border: none">Save and email</button>
conservancy/usethesource/tests.py
Show inline comments
 
new file 100644
 
import datetime
 
import re
 

	
 
from django.contrib.auth.models import User
 
import pytest
 

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

	
 

	
 
def make_candidate(save=False, **kwargs):
 
    defaults = {
 
        'name': 'Test Candidate',
 
        'slug': 'test',
 
        'vendor': 'test vendor',
 
        'device': 'test device',
 
        'release_date': datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
 
    }
 
    merged = defaults | kwargs
 
    obj = Candidate(**merged)
 
    if save:
 
        obj.save()
 
    return obj
 

	
 

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

	
 

	
 
@pytest.mark.django_db
 
def test_comment_knows_comment_its_replying_to():
 
    user = User.objects.create()
 
    candidate = make_candidate(name='Test Candidate', save=True)
 
    first_comment = Comment.objects.create(user=user, candidate=candidate)
 
    second_comment = Comment.objects.create(user=user, candidate=candidate)
 
    assert second_comment._find_previous_comment() == first_comment
 
    assert second_comment.in_reply_to() == first_comment.email_message_id
 

	
 

	
 
@pytest.mark.django_db
 
def test_comment_email():
 
    user = User.objects.create(first_name='Test', last_name='User')
 
    candidate = make_candidate(name='Test Candidate', save=True)
 
    models.Comment.objects.create(candidate=candidate, user=user)
 
    second_comment = models.Comment.objects.create(
 
        candidate=candidate,
 
        user=user,
 
        message='Test message',
 
    )
 
    email = make_comment_email(second_comment)
 
    assert 'Message-ID' in email.extra_headers
 
    assert 'In-Reply-To' in email.extra_headers
 
    assert email.subject == 'Re: Test Candidate'
 
    assert 'Test message' in email.body
 
    assert 'Test User' in email.body
conservancy/usethesource/views.py
Show inline comments
...
 
@@ -3,6 +3,7 @@ from django.shortcuts import get_object_or_404, redirect, render
 

	
 
from .models import Candidate, Comment
 
from .forms import CommentForm, DownloadForm
 
from .emails import make_comment_email
 

	
 

	
 
def landing_page(request):
...
 
@@ -38,11 +39,13 @@ def create_comment(request, slug):
 
    else:
 
        form = CommentForm(request.POST)
 
        if form.is_valid():
 
            instance = form.save(commit=False)
 
            instance.candidate = candidate
 
            instance.user = request.user
 
            instance.save()
 
            return redirect('usethesource:view_comment', comment_id=instance.id, show_add='true')
 
            comment = form.save(commit=False)
 
            comment.candidate = candidate
 
            comment.user = request.user
 
            comment.save()
 
            email = make_comment_email(comment)
 
            email.send()
 
            return redirect('usethesource:view_comment', comment_id=comment.id, show_add='true')
 
    return render(request, 'usethesource/comment_form.html', {'form': form, 'candidate': candidate})
 

	
 

	
...
 
@@ -54,9 +57,8 @@ def edit_comment(request, comment_id):
 
    else:
 
        form = CommentForm(request.POST, instance=comment)
 
        if form.is_valid():
 
            instance = form.save(commit=False)
 
            instance.save()
 
            return redirect('usethesource:view_comment', comment_id=instance.id, show_add='false')
 
            comment = form.save()
 
            return redirect('usethesource:view_comment', comment_id=comment.id, show_add='false')
 
    return render(request, 'usethesource/edit_comment_form.html', {'form': form, 'comment': comment})
 

	
 

	
0 comments (0 inline, 0 general)