Skip to content

Commit

Permalink
Merge branch 'master' into move-remaining-emails-to-dotdigital
Browse files Browse the repository at this point in the history
  • Loading branch information
evemartin authored May 2, 2024
2 parents cafbfd5 + ba49895 commit d767de1
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 139 deletions.
47 changes: 0 additions & 47 deletions cfl_common/common/email_messages.py
Original file line number Diff line number Diff line change
@@ -1,47 +0,0 @@
from django.urls import reverse


def studentJoinRequestSentEmail(request, schoolName, accessCode):
return {
"subject": f"School or club join request sent",
"message": (
f"Your request to join the school or club '{schoolName}' in class "
f"{accessCode} has been sent to that class's teacher, who will either "
f"accept or deny your request."
),
}


def studentJoinRequestNotifyEmail(request, username, email, accessCode):
return {
"subject": f"School or club join request",
"message": (
f"There is a request waiting from student with username '{username}' and "
f"email {email} to join your class {accessCode}. "
f"Please log in to your dashboard to review the request."
),
}


def studentJoinRequestRejectedEmail(request, schoolName, accessCode):
return {
"subject": f"School or club join request rejected",
"message": (
f"Your request to join the school or club '{schoolName}' in class "
f"{accessCode} has been rejected. Speak to your teacher if you think this "
f"is an error."
),
}


def accountDeletionEmail(request):
return {
"subject": f"We are sorry to see you go",
"title": "Your account was successfully deleted",
"message": (
f"If you have a moment before you leave us completely, please "
f"let us know the reason through our super short survey below."
f"\n\nGive feedback: https://usabi.li/do/d8e0313a31d7/5bef"
f"\n\nThank you for being part of the Code for Life community!"
),
}
4 changes: 4 additions & 0 deletions cfl_common/common/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
campaign_ids = {
"admin_given": 1569057,
"admin_revoked": 1569071,
"delete_account": 1567477,
"email_change_notification": 1551600,
"email_change_verification": 1551594,
"invite_teacher_with_account": 1569599,
"invite_teacher_without_account": 1569607,
"reset_password": 1557153,
"student_join_request_notification": 1569486,
"student_join_request_rejected": 1569470,
"student_join_request_sent": 1569477,
"teacher_released": 1569537,
"user_already_registered": 1569539,
"verify_new_user": 1551577,
Expand Down
2 changes: 1 addition & 1 deletion portal/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "6.43.1"
__version__ = "6.43.2"
5 changes: 3 additions & 2 deletions portal/tests/test_independent_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ def test_signup_under_13_sends_parent_email(self, mock_send_dotdigital_email: Mo

# Class for Selenium tests. We plan to replace these and turn them into Cypress tests
class TestIndependentStudentFrontend(BaseTest):
def test_delete_indy_account(self):
@patch("portal.views.registration.send_dotdigital_email")
def test_delete_indy_account(self, mock_send_dotdigital_email: Mock):
page = self.go_to_homepage()
page, _, _, email, password = create_independent_student(page)
page = page.independent_student_login(email, password)
Expand Down Expand Up @@ -214,7 +215,7 @@ def test_delete_indy_account(self):
assert not User.objects.get(id=user_id).is_active

# check if email has been sent
assert len(mail.outbox) == 1
mock_send_dotdigital_email.assert_called_once_with(campaign_ids["delete_account"], ANY)

def test_signup_without_newsletter(self):
page = self.go_to_homepage()
Expand Down
14 changes: 11 additions & 3 deletions portal/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,8 @@ def test_student_dashboard_view(self):
assert response.status_code == 200
assert response.context_data == EXPECTED_DATA_WITH_KURONO_GAME

def test_delete_account(self):
@patch("portal.views.registration.send_dotdigital_email")
def test_delete_account(self, mock_send_dotdigital_email: Mock):
email, password = signup_teacher_directly()
u = User.objects.get(email=email)
usrid = u.id
Expand All @@ -593,7 +594,7 @@ def test_delete_account(self):
response = c.post(url, {"password": "wrongPassword"})

assert response.status_code == 302
assert response.url == reverse("dashboard")
mock_send_dotdigital_email.assert_not_called()

# user has not been anonymised
u = User.objects.get(email=email)
Expand All @@ -604,14 +605,18 @@ def test_delete_account(self):
response = c.post(url, {"password": password, "unsubscribe_newsletter": "on"})

assert response.status_code == 302
mock_send_dotdigital_email.assert_called_once()
assert response.url == reverse("home")

# user has been anonymised
u = User.objects.get(id=usrid)
assert u.first_name == "Deleted"
assert not u.is_active

def test_delete_account_admin(self):
assert c.login(username=email, password=password) == False

@patch("portal.views.registration.send_dotdigital_email")
def test_delete_account_admin(self, mock_send_dotdigital_email: Mock):
"""test the passing of admin role after deletion of an admin account"""

email1, password1 = signup_teacher_directly()
Expand Down Expand Up @@ -666,6 +671,7 @@ def test_delete_account_admin(self):
# delete teacher1 account
url = reverse("delete_account")
c.post(url, {"password": password1})
mock_send_dotdigital_email.assert_called_once()

# user has been anonymised
u = User.objects.get(id=usrid1)
Expand Down Expand Up @@ -706,6 +712,7 @@ def test_delete_account_admin(self):
# now delete teacher3 account
url = reverse("delete_account")
c.post(url, {"password": password3})
self.assertEqual(mock_send_dotdigital_email.call_count, 2)

# 2 teachers left
teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name")
Expand Down Expand Up @@ -738,6 +745,7 @@ def test_delete_account_admin(self):

url = reverse("delete_account")
c.post(url, {"password": password2})
self.assertEqual(mock_send_dotdigital_email.call_count, 3)

# school should be anonymised
school = School._base_manager.get(id=school_id)
Expand Down
31 changes: 11 additions & 20 deletions portal/views/registration.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import ast
import re

from common.email_messages import accountDeletionEmail
from portal.views.login import has_user_lockout_expired

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

from common.helpers.emails import (
delete_contact,
NOTIFICATION_EMAIL,
PASSWORD_RESET_EMAIL,
delete_contact,
send_email,
)
from common.models import Teacher, Student, DailyActivity
from common.permissions import not_logged_in, not_fully_logged_in

from common.mail import campaign_ids, send_dotdigital_email
from common.models import DailyActivity, Student, Teacher
from common.permissions import not_fully_logged_in, not_logged_in
from django.contrib import messages as messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import user_passes_test, login_required
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.http import HttpResponseRedirect
from django.shortcuts import render
Expand All @@ -35,14 +32,15 @@
from deploy import captcha
from portal import app_settings
from portal.forms.registration import (
TeacherPasswordResetForm,
TeacherPasswordResetSetPasswordForm,
StudentPasswordResetForm,
StudentPasswordResetSetPasswordForm,
TeacherPasswordResetForm,
TeacherPasswordResetSetPasswordForm,
)
from portal.helpers.captcha import remove_captcha_from_form
from portal.helpers.ratelimit import clear_ratelimit_cache_for_user
from portal.views.api import anonymise
from portal.views.login import has_user_lockout_expired


@user_passes_test(not_logged_in, login_url=reverse_lazy("home"))
Expand Down Expand Up @@ -309,13 +307,6 @@ def delete_account(request):
delete_contact(email)

# send confirmation email
message = accountDeletionEmail(request)
send_email(
NOTIFICATION_EMAIL,
[email],
message["subject"],
message["message"],
message["title"],
)
send_dotdigital_email(campaign_ids["delete_account"], [email])

return HttpResponseRedirect(reverse_lazy("home"))
14 changes: 8 additions & 6 deletions portal/views/student/edit_account_details.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
from common.email_messages import accountDeletionEmail
from common.helpers.emails import NOTIFICATION_EMAIL, delete_contact, send_email, update_indy_email
from common.helpers.emails import (
NOTIFICATION_EMAIL,
delete_contact,
send_email,
update_indy_email,
)
from common.models import Student
from common.permissions import logged_in_as_student
from django.contrib import messages as messages
from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic.edit import FormView
from django.shortcuts import render

from portal.forms.play import StudentEditAccountForm, IndependentStudentEditAccountForm
from portal.forms.play import IndependentStudentEditAccountForm, StudentEditAccountForm
from portal.forms.registration import DeleteAccountForm

from portal.helpers.password import check_update_password
from portal.helpers.ratelimit import clear_ratelimit_cache_for_user
from portal.views.api import anonymise
from django.contrib import messages as messages


def _get_form(self, form_class):
Expand Down
77 changes: 25 additions & 52 deletions portal/views/student/play.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from common import email_messages
from common.helpers.emails import NOTIFICATION_EMAIL, send_email
from common.mail import campaign_ids, send_dotdigital_email
from common.models import Student
from common.permissions import (
logged_in_as_independent_student,
Expand All @@ -22,9 +23,7 @@
from portal.forms.play import StudentJoinOrganisationForm


class SchoolStudentDashboard(
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView
):
class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView):
template_name = "portal/play/student_dashboard.html"
login_url = reverse_lazy("student_login_access_code")

Expand All @@ -50,16 +49,10 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
custom_levels = student.new_user.shared.filter(owner=teacher)

if custom_levels:
custom_levels_data = _compute_rapid_router_scores(
student, custom_levels
)
custom_levels_data = _compute_rapid_router_scores(student, custom_levels)

context_data["total_custom_score"] = custom_levels_data[
"total_score"
]
context_data["total_custom_available_score"] = custom_levels_data[
"total_available_score"
]
context_data["total_custom_score"] = custom_levels_data["total_score"]
context_data["total_custom_available_score"] = custom_levels_data["total_available_score"]

# Get Kurono game info if the class has a game linked to it
aimmo_game = klass.active_game
Expand All @@ -72,9 +65,7 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
return context_data


class IndependentStudentDashboard(
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView
):
class IndependentStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView):
template_name = "portal/play/independent_student_dashboard.html"
login_url = reverse_lazy("independent_student_login")

Expand All @@ -91,9 +82,7 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
)


def _compute_rapid_router_scores(
student: Student, levels: List[Level] or QuerySet
) -> Dict[str, int]:
def _compute_rapid_router_scores(student: Student, levels: List[Level] or QuerySet) -> Dict[str, int]:
"""
Finds Rapid Router progress and score data for a specific student and a specific
set of levels. This is used to show quick score data to the student on their
Expand All @@ -112,20 +101,17 @@ def _compute_rapid_router_scores(
num_completed = num_top_scores = total_available_score = 0
total_score = 0.0
# Get a QuerySet of best attempts for each level
best_attempts = Attempt.objects.filter(
level__in=levels, student=student, is_best_attempt=True
).select_related("level")
best_attempts = Attempt.objects.filter(level__in=levels, student=student, is_best_attempt=True).select_related(
"level"
)

for level in levels:
total_available_score += _get_max_score_for_level(level)

# For each level, compare best attempt's score with level's max score and
# increment variables as needed
if best_attempts:
attempts_dict = {
best_attempt.level.id: best_attempt
for best_attempt in best_attempts
}
attempts_dict = {best_attempt.level.id: best_attempt for best_attempt in best_attempts}
for level in levels:
attempt = attempts_dict.get(level.id)

Expand Down Expand Up @@ -155,12 +141,7 @@ def _get_max_score_for_level(level: Level) -> int:
"""
return (
10
if level.id > 12
and (
level.disable_route_score
or level.disable_algorithm_score
or not level.episode
)
if level.id > 12 and (level.disable_route_score or level.disable_algorithm_score or not level.episode)
else 20
)

Expand Down Expand Up @@ -207,31 +188,23 @@ def process_join_organisation_form(request_form, request, student):
student.pending_class_request = request_form.klass
student.save()

email_message = email_messages.studentJoinRequestSentEmail(
request,
request_form.klass.teacher.school.name,
request_form.klass.access_code,
)
send_email(
NOTIFICATION_EMAIL,
send_dotdigital_email(
campaign_ids["student_join_request_sent"],
[student.new_user.email],
email_message["subject"],
email_message["message"],
email_message["subject"],
personalization_values={
"SCHOOL_CLUB_NAME": request_form.klass.teacher.school.name,
"ACCESS_CODE": request_form.klass.access_code,
},
)

email_message = email_messages.studentJoinRequestNotifyEmail(
request,
student.new_user.username,
student.new_user.email,
student.pending_class_request.access_code,
)
send_email(
NOTIFICATION_EMAIL,
send_dotdigital_email(
campaign_ids["student_join_request_notification"],
[student.pending_class_request.teacher.new_user.email],
email_message["subject"],
email_message["message"],
email_message["subject"],
personalization_values={
"USERNAME": student.new_user.username,
"EMAIL": student.new_user.email,
"ACCESS_CODE": student.pending_class_request.access_code,
},
)

messages.success(
Expand Down
Loading

0 comments on commit d767de1

Please sign in to comment.