Repository: e-valuation/EvaP

Issue:
End of evaluation warning
When opening an evaluation questionnaire after the last day of evaluation (in the X hour period in which the evaluation is still possible), an information about the exact end of the evaluation should be shown on top of the page (or maybe in a modal).


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
@@ -20,7 +20,7 @@
 # see evaluation.meta for the use of Translate in this file
 from evap.evaluation.meta import LocalizeModelBase, Translate
 from evap.evaluation.tools import date_to_datetime
-from evap.settings import EVALUATION_END_OFFSET_HOURS
+from evap.settings import EVALUATION_END_OFFSET_HOURS, EVALUATION_END_WARNING_PERIOD
 
 logger = logging.getLogger(__name__)
 
@@ -262,7 +262,7 @@ def save(self, *args, **kw):
             self.contributions.create(contributor=None)
             del self.general_contribution  # invalidate cached property
 
-        assert self.vote_end_date >= self.vote_end_date
+        assert self.vote_end_date >= self.vote_start_datetime.date()
 
     @property
     def is_fully_reviewed(self):
@@ -421,6 +421,13 @@ def responsible_contributors(self):
     def days_left_for_evaluation(self):
         return (self.vote_end_date - date.today()).days
 
+    @property
+    def time_left_for_evaluation(self):
+        return self.vote_end_datetime - datetime.now()
+
+    def evaluation_ends_soon(self):
+        return self.time_left_for_evaluation.total_seconds() < EVALUATION_END_WARNING_PERIOD * 3600
+
     @property
     def days_until_evaluation(self):
         return (self.vote_start_datetime.date() - date.today()).days
diff --git a/evap/settings.py b/evap/settings.py
--- a/evap/settings.py
+++ b/evap/settings.py
@@ -272,6 +272,9 @@
 # Specify an offset that will be added to the evaluation end date (e.g. 3: If the end date is 01.01., the evaluation will end at 02.01. 03:00.).
 EVALUATION_END_OFFSET_HOURS = 3
 
+# Amount of hours in which participant will be warned
+EVALUATION_END_WARNING_PERIOD = 5
+
 ### Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/1.7/howto/static-files/
 
diff --git a/evap/student/views.py b/evap/student/views.py
--- a/evap/student/views.py
+++ b/evap/student/views.py
@@ -63,6 +63,7 @@ def vote(request, course_id):
     if not course.can_user_vote(request.user):
         raise PermissionDenied
 
+    
     # prevent a user from voting on themselves.
     contributions_to_vote_on = course.contributions.exclude(contributor=request.user).all()
     form_groups = helper_create_voting_form_groups(request, contributions_to_vote_on)
@@ -80,7 +81,11 @@ def vote(request, course_id):
             contributor_form_groups=contributor_form_groups,
             course=course,
             participants_warning=course.num_participants <= 5,
-            preview=False)
+            preview=False,
+            vote_end_datetime=course.vote_end_datetime,
+            hours_left_for_evaluation=course.time_left_for_evaluation.seconds//3600,
+            minutes_left_for_evaluation=(course.time_left_for_evaluation.seconds//60)%60,
+            evaluation_ends_soon=course.evaluation_ends_soon())
         return render(request, "student_vote.html", template_data)
 
     # all forms are valid, begin vote operation


Test patch:
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
@@ -1,6 +1,7 @@
 from datetime import datetime, timedelta, date
 from unittest.mock import patch, Mock
 
+from django.conf import settings
 from django.test import TestCase, override_settings
 from django.core.cache import cache
 from django.core import mail
@@ -10,9 +11,10 @@
 from evap.evaluation.models import (Contribution, Course, CourseType, EmailTemplate, NotArchiveable, Questionnaire,
                                     RatingAnswerCounter, Semester, UserProfile)
 from evap.results.tools import calculate_average_grades_and_deviation
+from evap.settings import EVALUATION_END_OFFSET_HOURS, EVALUATION_END_WARNING_PERIOD
 
 
-@override_settings(EVALUATION_END_OFFSET=0)
+@override_settings(EVALUATION_END_OFFSET_HOURS=0)
 class TestCourses(TestCase):
 
     def test_approved_to_in_evaluation(self):
@@ -29,7 +31,8 @@ def test_approved_to_in_evaluation(self):
         self.assertEqual(course.state, 'in_evaluation')
 
     def test_in_evaluation_to_evaluated(self):
-        course = mommy.make(Course, state='in_evaluation', vote_end_date=date.today() - timedelta(days=1))
+        course = mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2), 
+                            vote_end_date=date.today() - timedelta(days=1))
 
         with patch('evap.evaluation.models.Course.is_fully_reviewed') as mock:
             mock.__get__ = Mock(return_value=False)
@@ -40,7 +43,8 @@ def test_in_evaluation_to_evaluated(self):
 
     def test_in_evaluation_to_reviewed(self):
         # Course is "fully reviewed" as no open text_answers are present by default,
-        course = mommy.make(Course, state='in_evaluation', vote_end_date=date.today() - timedelta(days=1))
+        course = mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2), 
+                            vote_end_date=date.today() - timedelta(days=1))
 
         Course.update_courses()
 
@@ -49,7 +53,8 @@ def test_in_evaluation_to_reviewed(self):
 
     def test_in_evaluation_to_published(self):
         # Course is "fully reviewed" and not graded, thus gets published immediately.
-        course = mommy.make(Course, state='in_evaluation', vote_end_date=date.today() - timedelta(days=1),
+        course = mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2),
+                            vote_end_date=date.today() - timedelta(days=1),
                             is_graded=False)
 
         with patch('evap.evaluation.tools.send_publish_notifications') as mock:
@@ -60,11 +65,26 @@ def test_in_evaluation_to_published(self):
         course = Course.objects.get(pk=course.pk)
         self.assertEqual(course.state, 'published')
 
+    @override_settings(EVALUATION_END_WARNING_PERIOD=24)
+    def test_evaluation_ends_soon(self):
+        course = mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2), 
+                            vote_end_date=date.today() - timedelta(hours=24), is_graded=False)
+
+        Course.update_courses()
+        self.assertTrue(course.evaluation_ends_soon())
+
+    @override_settings(EVALUATION_END_WARNING_PERIOD=24)
+    def test_evaluation_doesnt_end_soon(self):
+        course = mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=5), 
+                            vote_end_date=date.today() - timedelta(hours=24), is_graded=False)        
+
     def test_evaluation_ended(self):
         # Course is out of evaluation period.
-        mommy.make(Course, state='in_evaluation', vote_end_date=date.today() - timedelta(days=1), is_graded=False)
+        mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2), 
+                   vote_end_date=date.today() - timedelta(days=1), is_graded=False)
         # This course is not.
-        mommy.make(Course, state='in_evaluation', vote_end_date=date.today(), is_graded=False)
+        mommy.make(Course, state='in_evaluation', vote_start_datetime=datetime.now() - timedelta(days=2), 
+                   vote_end_date=date.today(), is_graded=False)
 
         with patch('evap.evaluation.models.Course.evaluation_end') as mock:
             Course.update_courses()
diff --git a/evap/grades/tests.py b/evap/grades/tests.py
--- a/evap/grades/tests.py
+++ b/evap/grades/tests.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import date, datetime, timedelta
 
 from django.core import mail
 from django.contrib.auth.models import Group
@@ -24,14 +24,14 @@ def setUpTestData(cls):
         cls.course = mommy.make(
             Course,
             name_en="Test",
-            vote_start_datetime=datetime.now() - timedelta(10),
-            vote_end_date=datetime.now() + timedelta(10),
+            vote_start_datetime=datetime.now() - timedelta(days=10),
+            vote_end_date=date.today() + timedelta(days=10),
             participants=[cls.student, cls.student2, cls.student3],
             voters=[cls.student, cls.student2],
         )
-        contribution = Contribution(course=cls.course, contributor=responsible, responsible=True, can_edit=True,
-                                    comment_visibility=Contribution.ALL_COMMENTS)
-        contribution.save()
+        
+        contribution = mommy.make(Contribution, course=cls.course, contributor=responsible, responsible=True,
+                                  can_edit=True, comment_visibility=Contribution.ALL_COMMENTS)
         contribution.questionnaires.set([mommy.make(Questionnaire, is_for_contributors=True)])
 
         cls.course.general_contribution.questionnaires.set([mommy.make(Questionnaire)])
diff --git a/evap/staff/tests/test_importers.py b/evap/staff/tests/test_importers.py
--- a/evap/staff/tests/test_importers.py
+++ b/evap/staff/tests/test_importers.py
@@ -1,5 +1,5 @@
 import os
-from datetime import datetime
+from datetime import date, datetime
 from django.test import TestCase, override_settings
 from django.conf import settings
 from model_mommy import mommy
@@ -126,8 +126,8 @@ class TestEnrollmentImporter(TestCase):
 
     def test_valid_file_import(self):
         semester = mommy.make(Semester)
-        vote_start_date = datetime(2017, 1, 10)
-        vote_end_date = datetime(2017, 3, 10)
+        vote_start_datetime = datetime(2017, 1, 10)
+        vote_end_date = date(2017, 3, 10)
         mommy.make(CourseType, name_de="Seminar")
         mommy.make(CourseType, name_de="Vorlesung")
 
@@ -141,7 +141,7 @@ def test_valid_file_import(self):
         self.assertEqual(errors, [])
         self.assertEqual(warnings, {})
 
-        success_messages, warnings, errors = EnrollmentImporter.process(excel_content, semester, vote_start_date, vote_end_date, test_run=False)
+        success_messages, warnings, errors = EnrollmentImporter.process(excel_content, semester, vote_start_datetime, vote_end_date, test_run=False)
         self.assertIn("Successfully created 23 course(s), 6 student(s) and 17 contributor(s):", "".join(success_messages))
         self.assertIn("Ferdi Itaque (ferdi.itaque)", "".join(success_messages))
         self.assertEqual(errors, [])