Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move updates from staging to production #1366

Merged
merged 9 commits into from
Mar 27, 2024
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ collectstatic:
docker compose run --rm web poetry run ./manage.py collectstatic --clear --noinput

poetry:
poetry check && poetry install --sync
poetry check && poetry install --sync --no-root

lint: poetry
poetry run pre-commit run --all-files

css: poetry
poetry run ./manage.py custom_bootstrap5

makemigrations:
poetry run ./manage.py makemigrations
58 changes: 42 additions & 16 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from django.contrib.auth.models import PermissionsMixin
from django.contrib.postgres.fields.array import ArrayField
from django.core.mail.message import EmailMultiAlternatives
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
from django.db import models
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
from django.utils.html import mark_safe
from django.utils.text import slugify
from django.utils.timezone import now
Expand Down Expand Up @@ -294,6 +296,17 @@ def get_short_name(self):
def get_full_name(self):
return f"{self.given_name} {self.middle_name} {self.family_name}"

def generate_token(self):
return TimestampSigner().sign(self.username).split(":", 1)[1]

def check_token(self, token):
try:
key = f"{self.username}:{token}"
TimestampSigner().unsign(key, max_age=60 * 60 * 48) # Valid for 2 days
except (BadSignature, SignatureExpired):
return False
return True

def __str__(self):
if self.family_name:
return f"<User: {self.given_name} {self.family_name}, ID {self.id}, {self.uuid}>"
Expand Down Expand Up @@ -367,7 +380,7 @@ class Child(models.Model):
"accounts.User",
related_name="children",
related_query_name="children",
on_delete=models.CASCADE # if deleting User, also delete associated Child -
on_delete=models.CASCADE, # if deleting User, also delete associated Child -
# although may not be possible depending on Responses already associated
)

Expand Down Expand Up @@ -629,6 +642,8 @@ def send_announcement_email(cls, user: User, study, children):
"study": study,
"children": children,
"children_string": children_string,
"username": user.username,
"token": user.generate_token(),
}

text_content = get_template("emails/study_announcement.txt").render(context)
Expand All @@ -646,6 +661,7 @@ def send_announcement_email(cls, user: User, study, children):
settings.EMAIL_FROM_ADDRESS,
[user.username],
reply_to=[study.lab.contact_email],
headers=cls.email_headers(context),
)
email.attach_alternative(html_content, "text/html")
email.send()
Expand All @@ -665,25 +681,35 @@ def send_as_email(self):
lab_email = self.related_study.lab.contact_email

recipient_email_list = list(self.recipients.values_list("username", flat=True))
if len(recipient_email_list) == 1:
to_email_list = recipient_email_list
bcc_email_list = []
else:
to_email_list = [settings.EMAIL_FROM_ADDRESS]
bcc_email_list = recipient_email_list

send_mail.delay(
"custom_email",
self.subject,
to_email_list,
bcc=bcc_email_list,
from_email=lab_email,
**context,
)

for to_email in recipient_email_list:
user = User.objects.get(username=to_email)
context.update(token=user.generate_token(), username=to_email)
send_mail.delay(
"custom_email",
self.subject,
to_email,
reply_to=[lab_email],
headers=self.email_headers(context),
**context,
)

self.email_sent_timestamp = now() # will use UTC now (see USE_TZ in settings)
self.save()

@classmethod
def email_headers(cls, context):
token = context.get("token")
username = context.get("username")
base_url = settings.BASE_URL
if token and username:
return {
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"List-Unsubscribe": f"<mailto:lookit-bot@mit.edu>, <{base_url}{reverse('web:email-unsubscribe-link', kwargs={'token':token,'username':username})}>",
}
else:
return None


def create_string_listing_children(children):
child_names = [child.given_name for child in children]
Expand Down
28 changes: 13 additions & 15 deletions exp/tests/test_contact_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,19 +252,16 @@ def test_can_post_message_to_participants(self, mock_send_mail):
follow=True,
)
self.assertEqual(response.status_code, 200)
# Ensure message sent to participants 0, 1, 2
mock_send_mail.assert_called_once()
self.assertEqual(
mock_send_mail.call_args.args,
("custom_email", "test email", ["lookit.robot@some.domain"]),
)
self.assertEqual(
mock_send_mail.call_args.kwargs["bcc"],
[p.username for p in self.participants[0:3]],
)
# Ensure message sent to participants 0, 1, 2. We now mail each person
# individually to provide an appropriate unsubscribe link.
self.assertEqual(mock_send_mail.call_count, 3)

# checking that we aren't adding any users to bbc.
self.assertFalse("bbc" in mock_send_mail.call_args.kwargs)
self.assertEqual(
mock_send_mail.call_args.kwargs["from_email"], self.study.lab.contact_email
mock_send_mail.call_args.kwargs["reply_to"], [self.study.lab.contact_email]
)
self.assertFalse("from_email" in mock_send_mail.call_args)

# And that appropriate message object created
self.assertTrue(Message.objects.filter(subject="test email").exists())
Expand All @@ -289,15 +286,16 @@ def test_message_not_posted_to_non_participant(self, mock_send_mail):
)
self.assertEqual(response.status_code, 200)
# Ensure message sent only to participant 3, not participant 4 (who did not participate in this study)
mock_send_mail.assert_called_once()
mock_send_mail.assert_called()
self.assertEqual(
mock_send_mail.call_args.args,
("custom_email", "test email", [self.participants[3].username]),
("custom_email", "test email", self.participants[3].username),
)
self.assertEqual(mock_send_mail.call_args.kwargs["bcc"], [])
self.assertFalse("bbc" in mock_send_mail.call_args.kwargs)
self.assertEqual(
mock_send_mail.call_args.kwargs["from_email"], self.study.lab.contact_email
mock_send_mail.call_args.kwargs["reply_to"], [self.study.lab.contact_email]
)
self.assertFalse("from_email" in mock_send_mail.call_args)

# And that appropriate message object created
self.assertTrue(Message.objects.filter(subject="test email").exists())
Expand Down
2 changes: 1 addition & 1 deletion exp/tests/test_study_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ def test_send_study_email(self, mock_get_object: Mock, mock_send_mail: Mock):
"notify_researcher_of_study_permissions",
f"New access granted for study {mock_get_object().name}",
mock_user.username,
from_email=mock_get_object().lab.contact_email,
reply_to=[mock_get_object().lab.contact_email],
permission=mock_permission,
study_name=mock_get_object().name,
study_id=mock_get_object().id,
Expand Down
3 changes: 2 additions & 1 deletion exp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
2. Import the include() function: from django.conf.urls import url, include
3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
"""

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

Expand Down Expand Up @@ -256,7 +257,7 @@
name="preview-proxy",
),
path(
"studies/jspsych/<uuid:uuid>/<uuid:child_id>/preview/",
"studies/j/<uuid:uuid>/<uuid:child_id>/preview/",
JsPsychPreviewView.as_view(),
name="preview-jspsych",
),
Expand Down
8 changes: 4 additions & 4 deletions exp/views/lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def post(self, request, *args, **kwargs):
"notify_researcher_of_lab_permissions",
f"You are now part of the Lookit lab {lab.name}",
researcher.username,
from_email=lab.contact_email,
reply_to=[lab.contact_email],
**context,
)
if action == "make_member":
Expand All @@ -256,7 +256,7 @@ def post(self, request, *args, **kwargs):
"notify_researcher_of_lab_permissions",
f"You now have lab member permissions for the Lookit lab {lab.name}",
researcher.username,
from_email=lab.contact_email,
reply_to=[lab.contact_email],
**context,
)
if action == "make_admin":
Expand All @@ -280,7 +280,7 @@ def post(self, request, *args, **kwargs):
"notify_researcher_of_lab_permissions",
f"You are now an admin of the Lookit lab {lab.name}",
researcher.username,
from_email=lab.contact_email,
reply_to=[lab.contact_email],
**context,
)
if action == "remove_researcher":
Expand Down Expand Up @@ -312,7 +312,7 @@ def post(self, request, *args, **kwargs):
"notify_researcher_of_lab_permissions",
f"You have been removed from the Lookit lab {lab.name}",
researcher.username,
from_email=lab.contact_email,
reply_to=[lab.contact_email],
**context,
)
if action == "reset_password":
Expand Down
2 changes: 1 addition & 1 deletion exp/views/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ def send_study_email(self, user, permission):
"notify_researcher_of_study_permissions",
f"New access granted for study {self.get_object().name}",
user.username,
from_email=study.lab.contact_email,
reply_to=[study.lab.contact_email],
**context,
)

Expand Down
Loading