Repository: e-valuation/EvaP

Issue:
Missing import for messages
in `evaluation/models.py` the import `from django.contrib import messages` is missing, resulting in an error when `__send_to_user` tries to send a message to a user without email address.



Discussion:

Resolving patch:
diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py
--- a/evap/evaluation/models.py
+++ b/evap/evaluation/models.py
@@ -3,6 +3,7 @@
 import logging
 
 from django.conf import settings
+from django.contrib import messages
 from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group
 from django.core.exceptions import ValidationError
 from django.core.mail import EmailMessage
@@ -552,7 +553,7 @@ def update_courses(cls):
             except Exception:
                 logger.exception('An error occured when updating the state of course "{}" (id {}).'.format(course, course.id))
 
-        EmailTemplate.send_evaluation_started_notifications(courses_new_in_evaluation)
+        EmailTemplate.send_evaluation_started_notifications(courses_new_in_evaluation, None)
         send_publish_notifications(evaluation_results_courses)
         logger.info("update_courses finished.")
 
@@ -1088,7 +1089,7 @@ def __render_string(cls, text, dictionary):
         return Template(text).render(Context(dictionary, autoescape=False))
 
     @classmethod
-    def send_to_users_in_courses(cls, template, courses, recipient_groups, use_cc):
+    def send_to_users_in_courses(cls, template, courses, recipient_groups, use_cc, request):
         user_course_map = {}
         for course in courses:
             recipients = cls.recipient_list_for_course(course, recipient_groups, filter_users_in_cc=use_cc)
@@ -1098,14 +1099,15 @@ def send_to_users_in_courses(cls, template, courses, recipient_groups, use_cc):
         for user, courses in user_course_map.items():
             subject_params = {}
             body_params = {'user': user, 'courses': courses}
-            cls.__send_to_user(user, template, subject_params, body_params, use_cc=use_cc)
+            cls.__send_to_user(user, template, subject_params, body_params, use_cc=use_cc, request=request)
 
     @classmethod
-    def __send_to_user(cls, user, template, subject_params, body_params, use_cc):
+    def __send_to_user(cls, user, template, subject_params, body_params, use_cc, request=None):
         if not user.email:
             warning_message = "{} has no email address defined. Could not send email.".format(user.username)
             logger.warning(warning_message)
-            messages.warning(_(warning_message))
+            if request is not None:
+                messages.warning(request, _(warning_message))
             return
 
         if use_cc:
@@ -1168,11 +1170,11 @@ def send_publish_notifications_to_user(cls, user, courses):
         cls.__send_to_user(user, template, subject_params, body_params, use_cc=True)
 
     @classmethod
-    def send_review_notifications(cls, courses):
+    def send_review_notifications(cls, courses, request):
         template = cls.objects.get(name=cls.EDITOR_REVIEW_NOTICE)
-        cls.send_to_users_in_courses(template, courses, [cls.EDITORS], use_cc=True)
+        cls.send_to_users_in_courses(template, courses, [cls.EDITORS], use_cc=True, request=request)
 
     @classmethod
-    def send_evaluation_started_notifications(cls, courses):
+    def send_evaluation_started_notifications(cls, courses, request):
         template = cls.objects.get(name=cls.EVALUATION_STARTED)
-        cls.send_to_users_in_courses(template, courses, [cls.ALL_PARTICIPANTS], use_cc=False)
+        cls.send_to_users_in_courses(template, courses, [cls.ALL_PARTICIPANTS], use_cc=False, request=request)
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -284,10 +284,10 @@ def email_addresses(self):
         recipients = self.template.recipient_list_for_course(self.instance, self.recipient_groups, filter_users_in_cc=False)
         return set(user.email for user in recipients if user.email)
 
-    def send(self):
+    def send(self, request):
         self.template.subject = self.cleaned_data.get('subject')
         self.template.body = self.cleaned_data.get('body')
-        EmailTemplate.send_to_users_in_courses(self.template, [self.instance], self.recipient_groups, use_cc=True)
+        EmailTemplate.send_to_users_in_courses(self.template, [self.instance], self.recipient_groups, use_cc=True, request=request)
 
 
 class QuestionnaireForm(forms.ModelForm, BootstrapMixin):
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -224,7 +224,7 @@ def helper_semester_course_operation_prepare(request, courses, send_email):
     messages.success(request, ungettext("Successfully enabled %(courses)d course for editor review.",
         "Successfully enabled %(courses)d courses for editor review.", len(courses)) % {'courses': len(courses)})
     if send_email:
-        EmailTemplate.send_review_notifications(courses)
+        EmailTemplate.send_review_notifications(courses, request)
 
 
 def helper_semester_course_operation_approve(request, courses):
@@ -243,7 +243,7 @@ def helper_semester_course_operation_start(request, courses, send_email):
     messages.success(request, ungettext("Successfully started evaluation for %(courses)d course.",
         "Successfully started evaluation for %(courses)d courses.", len(courses)) % {'courses': len(courses)})
     if send_email:
-        EmailTemplate.send_evaluation_started_notifications(courses)
+        EmailTemplate.send_evaluation_started_notifications(courses, request)
 
 
 def helper_semester_course_operation_publish(request, courses, send_email):
@@ -612,7 +612,7 @@ def course_email(request, semester_id, course_id):
             email_addresses = '; '.join(form.email_addresses())
             messages.info(request, _('Recipients: ') + '\n' + email_addresses)
             return render(request, "staff_course_email.html", dict(semester=semester, course=course, form=form))
-        form.send()
+        form.send(request)
         messages.success(request, _("Successfully sent emails for '%s'.") % course.name)
         return custom_redirect('staff:semester_view', semester_id)
     else:


Test patch:
diff --git a/evap/evaluation/tests/test_coverage.py b/evap/evaluation/tests/test_coverage.py
--- a/evap/evaluation/tests/test_coverage.py
+++ b/evap/evaluation/tests/test_coverage.py
@@ -1,7 +1,6 @@
 from django.test.utils import override_settings
 
-from evap.evaluation.models import Questionnaire, UserProfile, Course, \
-                                   EmailTemplate, Degree, CourseType
+from evap.evaluation.models import UserProfile, Course, EmailTemplate, Degree, CourseType
 from evap.evaluation.tests.tools import WebTest
 
 
diff --git a/evap/evaluation/tests/test_models.py b/evap/evaluation/tests/test_models.py
--- a/evap/evaluation/tests/test_models.py
+++ b/evap/evaluation/tests/test_models.py
@@ -20,7 +20,7 @@ def test_approved_to_in_evaluation(self):
         with patch('evap.evaluation.models.EmailTemplate.send_evaluation_started_notifications') as mock:
             Course.update_courses()
 
-        mock.assert_called_once_with([course])
+        mock.assert_called_once_with([course], None)
 
         course = Course.objects.get(pk=course.pk)
         self.assertEqual(course.state, 'in_evaluation')
@@ -241,7 +241,7 @@ def setUpTestData(cls):
 
     def test_no_login_url_when_delegates_in_cc(self):
         self.user.delegates.add(self.other_user)
-        EmailTemplate.send_to_users_in_courses(self.template, [self.course], EmailTemplate.CONTRIBUTORS, use_cc=True)
+        EmailTemplate.send_to_users_in_courses(self.template, [self.course], EmailTemplate.CONTRIBUTORS, use_cc=True, request=None)
         self.assertEqual(len(mail.outbox), 2)
         self.assertFalse("loginkey" in mail.outbox[0].body)  # message does not contain the login url
         self.assertTrue("loginkey" in mail.outbox[1].body)  # separate email with login url was sent
@@ -250,7 +250,7 @@ def test_no_login_url_when_delegates_in_cc(self):
 
     def test_no_login_url_when_cc_users_in_cc(self):
         self.user.cc_users.add(self.other_user)
-        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=True)
+        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=True, request=None)
         self.assertEqual(len(mail.outbox), 2)
         self.assertFalse("loginkey" in mail.outbox[0].body)  # message does not contain the login url
         self.assertTrue("loginkey" in mail.outbox[1].body)  # separate email with login url was sent
@@ -259,18 +259,29 @@ def test_no_login_url_when_cc_users_in_cc(self):
 
     def test_login_url_when_nobody_in_cc(self):
         # message is not sent to others in cc
-        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=True)
+        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=True, request=None)
         self.assertEqual(len(mail.outbox), 1)
         self.assertTrue("loginkey" in mail.outbox[0].body)  # message does contain the login url
 
     def test_login_url_when_use_cc_is_false(self):
         # message is not sent to others in cc
         self.user.delegates.add(self.other_user)
-        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=False)
+        EmailTemplate.send_to_users_in_courses(self.template, [self.course], [EmailTemplate.CONTRIBUTORS], use_cc=False, request=None)
         self.assertEqual(len(mail.outbox), 1)
         self.assertTrue("loginkey" in mail.outbox[0].body)  # message does contain the login url
 
 
+class TestEmailTemplate(TestCase):
+    def test_missing_email_address(self):
+        """
+        Tests that __send_to_user behaves when the user has no email address.
+        Regression test to https://github.com/fsr-itse/EvaP/issues/825
+        """
+        user = mommy.make(UserProfile, email=None)
+        template = EmailTemplate.objects.get(name=EmailTemplate.STUDENT_REMINDER)
+        EmailTemplate._EmailTemplate__send_to_user(user, template, {}, {}, False, None)
+
+
 class TestEmailRecipientList(TestCase):
     def test_recipient_list(self):
         course = mommy.make(Course)
diff --git a/evap/staff/tests/test_forms.py b/evap/staff/tests/test_forms.py
--- a/evap/staff/tests/test_forms.py
+++ b/evap/staff/tests/test_forms.py
@@ -20,7 +20,7 @@ def test_course_email_form(self):
         data = {"body": "wat", "subject": "some subject", "recipients": [EmailTemplate.DUE_PARTICIPANTS]}
         form = CourseEmailForm(instance=course, data=data)
         self.assertTrue(form.is_valid())
-        form.send()
+        form.send(None)
 
         data = {"body": "wat", "subject": "some subject"}
         form = CourseEmailForm(instance=course, data=data)
diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py
--- a/evap/staff/tests/test_views.py
+++ b/evap/staff/tests/test_views.py
@@ -419,7 +419,7 @@ def setUpTestData(cls):
         semester = mommy.make(Semester, pk=1)
         participant1 = mommy.make(UserProfile, email="foo@example.com")
         participant2 = mommy.make(UserProfile, email="bar@example.com")
-        course = mommy.make(Course, pk=1, semester=semester, participants=[participant1, participant2])
+        mommy.make(Course, pk=1, semester=semester, participants=[participant1, participant2])
 
     def test_course_email(self):
         """
@@ -434,6 +434,7 @@ def test_course_email(self):
 
         self.assertEqual(len(mail.outbox), 2)
 
+
 class TestQuestionnaireDeletionView(WebTest):
     url = "/staff/questionnaire/delete"
     csrf_checks = False
@@ -442,7 +443,7 @@ class TestQuestionnaireDeletionView(WebTest):
     def setUpTestData(cls):
         mommy.make(UserProfile, username='staff', groups=[Group.objects.get(name='Staff')])
         questionnaire1 = mommy.make(Questionnaire, pk=1)
-        questionnaire2 = mommy.make(Questionnaire, pk=2)
+        mommy.make(Questionnaire, pk=2)
         mommy.make(Contribution, questionnaires=[questionnaire1])
 
     def test_questionnaire_deletion(self):