Repository: e-valuation/EvaP

Issue:
Continuous text answer review
When reviewing text answers, it should be possible to faster approve individual answers. Currently two ideas exist for the reviewing UI:
- Either approved answers are removed so that the next answer and its yes and no buttons move up to the exact same position where the old answer was. This allows to approve answers without moving the mouse every time.
- Another possibility (which would be even better) is to only show a single text answer at a time and to approve/hide it by pressing a key on the keyboard. After each choice the next answer is shown.

Additionally, after all text answers for a course have been reviewed, it should be possible to easily switch to the next course (e.g. navigation button to "next course" or another hotkey).


Discussion:
@janno42, @pixunil you guys keep in mind that #1130 needs to be integrated somehow?

Resolving patch:
diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py
--- a/evap/evaluation/models.py
+++ b/evap/evaluation/models.py
@@ -820,6 +820,10 @@ def is_private(self):
     def is_published(self):
         return self.state == self.PUBLISHED
 
+    @property
+    def is_reviewed(self):
+        return self.state != self.NOT_REVIEWED
+
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
         assert self.answer != self.original_answer
diff --git a/evap/staff/tools.py b/evap/staff/tools.py
--- a/evap/staff/tools.py
+++ b/evap/staff/tools.py
@@ -9,11 +9,12 @@
 from django.core.cache.utils import make_template_fragment_key
 from django.core.exceptions import SuspiciousOperation
 from django.db import transaction
+from django.db.models import Count
 from django.conf import settings
 from django.utils.translation import ugettext_lazy as _
 from django.utils.safestring import mark_safe
 
-from evap.evaluation.models import UserProfile, Course, Contribution
+from evap.evaluation.models import UserProfile, Course, Contribution, TextAnswer
 from evap.grades.models import GradeDocument
 from evap.results.tools import collect_results
 
@@ -183,3 +184,12 @@ def merge_users(main_user, other_user, preview=False):
     other_user.delete()
 
     return merged_user, errors, warnings
+
+
+def find_next_unreviewed_course(semester, excluded):
+    return semester.course_set.exclude(pk__in=excluded) \
+        .exclude(state='published') \
+        .exclude(can_publish_text_results=False) \
+        .filter(contributions__textanswer_set__state=TextAnswer.NOT_REVIEWED) \
+        .annotate(num_unreviewed_textanswers=Count("contributions__textanswer_set")) \
+        .order_by('vote_end_date', '-num_unreviewed_textanswers').first()
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -34,7 +34,7 @@
 from evap.staff.importers import EnrollmentImporter, UserImporter, PersonImporter
 from evap.staff.tools import (bulk_delete_users, custom_redirect, delete_import_file, delete_navbar_cache_for_users,
                               forward_messages, get_import_file_content_or_raise, import_file_exists, merge_users,
-                              save_import_file, get_parameter_from_url_or_session)
+                              save_import_file, get_parameter_from_url_or_session, find_next_unreviewed_course)
 from evap.student.forms import QuestionnaireVotingForm
 from evap.student.views import get_valid_form_groups_or_render_vote_page
 
@@ -796,7 +796,8 @@ def course_comments(request, semester_id, course_id):
     if not course.can_publish_text_results:
         raise PermissionDenied
 
-    filter_comments = get_parameter_from_url_or_session(request, "filter_comments")
+    view = request.GET.get('view', 'quick')
+    filter_comments = view == "unreviewed"
 
     CommentSection = namedtuple('CommentSection', ('questionnaire', 'contributor', 'label', 'is_responsible', 'results'))
     course_sections = []
@@ -815,9 +816,23 @@ def course_comments(request, semester_id, course_id):
             section_list = course_sections if contribution.is_general else contributor_sections
             section_list.append(CommentSection(questionnaire, contribution.contributor, contribution.label, contribution.responsible, text_results))
 
-    template_data = dict(semester=semester, course=course, course_sections=course_sections,
-            contributor_sections=contributor_sections, filter_comments=filter_comments)
-    return render(request, "staff_course_comments.html", template_data)
+    template_data = dict(semester=semester, course=course, view=view)
+
+    if view == 'quick':
+        visited = request.session.get('review-visited', set())
+        visited.add(course.pk)
+        next_course = find_next_unreviewed_course(semester, visited)
+        if not next_course and len(visited) > 1:
+            visited = {course.pk}
+            next_course = find_next_unreviewed_course(semester, visited)
+        request.session['review-visited'] = visited
+
+        sections = course_sections + contributor_sections
+        template_data.update(dict(sections=sections, next_course=next_course))
+        return render(request, "staff_course_comments_quick.html", template_data)
+    else:
+        template_data.update(dict(course_sections=course_sections, contributor_sections=contributor_sections))
+        return render(request, "staff_course_comments_full.html", template_data)
 
 
 @require_POST


Test patch:
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
@@ -1311,14 +1311,13 @@ def setUpTestData(cls):
         top_course_questionnaire = mommy.make(Questionnaire, type=Questionnaire.TOP)
         mommy.make(Question, questionnaire=top_course_questionnaire, type="L")
         cls.course.general_contribution.questionnaires.set([top_course_questionnaire])
-
-    def test_comments_showing_up(self):
         questionnaire = mommy.make(Questionnaire)
         question = mommy.make(Question, questionnaire=questionnaire, type='T')
-        contribution = mommy.make(Contribution, course=self.course, contributor=mommy.make(UserProfile), questionnaires=[questionnaire])
-        answer = 'should show up'
-        mommy.make(TextAnswer, contribution=contribution, question=question, answer=answer)
+        contribution = mommy.make(Contribution, course=cls.course, contributor=mommy.make(UserProfile), questionnaires=[questionnaire])
+        cls.answer = 'should show up'
+        mommy.make(TextAnswer, contribution=contribution, question=question, answer=cls.answer)
 
+    def test_comments_showing_up(self):
         # in a course with only one voter the view should not be available
         self.app.get(self.url, user='staff', status=403)
 
@@ -1326,8 +1325,17 @@ def test_comments_showing_up(self):
         self.let_user_vote_for_course(self.student2, self.course)
 
         # now it should work
+        self.app.get(self.url, user='staff', status=200)
+
+    def test_comments_quick_view(self):
+        self.let_user_vote_for_course(self.student2, self.course)
         page = self.app.get(self.url, user='staff', status=200)
-        self.assertContains(page, answer)
+        self.assertContains(page, self.answer)
+
+    def test_comments_full_view(self):
+        self.let_user_vote_for_course(self.student2, self.course)
+        page = self.app.get(self.url + '?view=full', user='staff', status=200)
+        self.assertContains(page, self.answer)
 
 
 class TestCourseCommentEditView(ViewTest):