Repository: e-valuation/EvaP

Issue:
Staff vote preview page is accessible, but crashes when no questionnaires are assigned
We currently have this situation with http://localhost/staff/semester/21/evaluation/1668/preview. There are two contributions for this evaluation, but no questionnaires are assigned.

The staff `evaluation_preview` view calls `get_valid_form_groups_or_render_vote_page`, which in turn will check whether all form groups are valid -- there are no form groups, so this is the case. The method then asserts `not preview`, which fails.

The preview button on the staff semester view that leads to this page is enabled (it is shown if `evaluation.state < evaluation.State.IN_EVALUATION`).

@janno42 how to we want to handle evaluations without any assigned questionnaires, regarding preview as well as starting the evaluation. From what I see, starting an evaluation that has no questionnaires assigned is currently possible?


Discussion:
Approving an evaluation without questionnaires is not possible via the frontend, so an evaluation without questionnaires cannot be started.
The preview in this case should probably simply show an empty voting page.

Resolving patch:
diff --git a/evap/contributor/views.py b/evap/contributor/views.py
--- a/evap/contributor/views.py
+++ b/evap/contributor/views.py
@@ -28,7 +28,7 @@
 from evap.results.exporters import ResultsExporter
 from evap.results.tools import annotate_distributions_and_grades, get_evaluations_with_course_result_attributes
 from evap.staff.forms import ContributionFormset
-from evap.student.views import get_valid_form_groups_or_render_vote_page
+from evap.student.views import render_vote_page
 
 
 @responsible_or_contributor_or_delegate_required
@@ -155,9 +155,9 @@ def render_preview(request, formset, evaluation_form, evaluation):
             formset.save()
             request.POST = None  # this prevents errors rendered in the vote form
 
-            preview_response = get_valid_form_groups_or_render_vote_page(
+            preview_response = render_vote_page(
                 request, evaluation, preview=True, for_rendering_in_modal=True
-            )[1].content.decode()
+            ).content.decode()
             raise IntegrityError  # rollback transaction to discard the database writes
     except IntegrityError:
         pass
@@ -240,7 +240,7 @@ def evaluation_preview(request, evaluation_id):
     ):
         raise PermissionDenied
 
-    return get_valid_form_groups_or_render_vote_page(request, evaluation, preview=True)[1]
+    return render_vote_page(request, evaluation, preview=True)
 
 
 @require_POST
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -105,7 +105,7 @@
 )
 from evap.student.forms import QuestionnaireVotingForm
 from evap.student.models import TextAnswerWarning
-from evap.student.views import get_valid_form_groups_or_render_vote_page
+from evap.student.views import render_vote_page
 
 
 @manager_required
@@ -1601,7 +1601,7 @@ def evaluation_preview(request, semester_id, evaluation_id):
         raise PermissionDenied
     evaluation = get_object_or_404(Evaluation, id=evaluation_id, course__semester=semester)
 
-    return get_valid_form_groups_or_render_vote_page(request, evaluation, preview=True)[1]
+    return render_vote_page(request, evaluation, preview=True)
 
 
 @manager_required
diff --git a/evap/student/views.py b/evap/student/views.py
--- a/evap/student/views.py
+++ b/evap/student/views.py
@@ -116,7 +116,7 @@ def sorter(evaluation):
     return render(request, "student_index.html", template_data)
 
 
-def get_valid_form_groups_or_render_vote_page(request, evaluation, preview, for_rendering_in_modal=False):
+def get_vote_page_form_groups(request, evaluation, preview):
     contributions_to_vote_on = evaluation.contributions.all()
     # prevent a user from voting on themselves
     if not preview:
@@ -131,12 +131,15 @@ def get_valid_form_groups_or_render_vote_page(request, evaluation, preview, for_
             QuestionnaireVotingForm(request.POST or None, contribution=contribution, questionnaire=questionnaire)
             for questionnaire in questionnaires
         ]
+    return form_groups
 
-    if all(all(form.is_valid() for form in form_group) for form_group in form_groups.values()):
-        assert not preview
-        return form_groups, None
 
-    evaluation_form_group = form_groups.pop(evaluation.general_contribution)
+def render_vote_page(request, evaluation, preview, for_rendering_in_modal=False):
+    form_groups = get_vote_page_form_groups(request, evaluation, preview)
+
+    assert preview or not all(form.is_valid() for form_group in form_groups.values() for form in form_group)
+
+    evaluation_form_group = form_groups.pop(evaluation.general_contribution, default=[])
 
     contributor_form_groups = [
         (
@@ -175,19 +178,19 @@ def get_valid_form_groups_or_render_vote_page(request, evaluation, preview, for_
         general_contribution_textanswers_visible_to=textanswers_visible_to(evaluation.general_contribution),
         text_answer_warnings=TextAnswerWarning.objects.all(),
     )
-    return None, render(request, "student_vote.html", template_data)
+    return render(request, "student_vote.html", template_data)
 
 
 @participant_required
 def vote(request, evaluation_id):
-    # pylint: disable=too-many-locals,too-many-nested-blocks,too-many-branches
+    # pylint: disable=too-many-nested-blocks,too-many-branches
     evaluation = get_object_or_404(Evaluation, id=evaluation_id)
     if not evaluation.can_be_voted_for_by(request.user):
         raise PermissionDenied
 
-    form_groups, rendered_page = get_valid_form_groups_or_render_vote_page(request, evaluation, preview=False)
-    if rendered_page is not None:
-        return rendered_page
+    form_groups = get_vote_page_form_groups(request, evaluation, preview=False)
+    if not all(form.is_valid() for form_group in form_groups.values() for form in form_group):
+        return render_vote_page(request, evaluation, preview=False)
 
     # all forms are valid, begin vote operation
     with transaction.atomic():


Test patch:
diff --git a/evap/contributor/tests/test_views.py b/evap/contributor/tests/test_views.py
--- a/evap/contributor/tests/test_views.py
+++ b/evap/contributor/tests/test_views.py
@@ -122,6 +122,11 @@ def test_wrong_state(self):
         self.evaluation.save()
         self.app.get(self.url, user=self.responsible, status=403)
 
+    def test_without_questionnaires_assigned(self):
+        # regression test for #1747
+        self.evaluation.general_contribution.questionnaires.set([])
+        self.app.get(self.url, user=self.responsible, status=200)
+
 
 class TestContributorEvaluationEditView(WebTest):
     @classmethod
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
@@ -1980,11 +1980,17 @@ def setUpTestData(cls):
 class TestEvaluationPreviewView(WebTestStaffModeWith200Check):
     @classmethod
     def setUpTestData(cls):
-        evaluation = baker.make(Evaluation)
-        evaluation.general_contribution.questionnaires.set([baker.make(Questionnaire)])
+        cls.evaluation = baker.make(Evaluation)
+        cls.evaluation.general_contribution.questionnaires.set([baker.make(Questionnaire)])
 
-        cls.test_users = [make_manager()]
-        cls.url = f"/staff/semester/{evaluation.course.semester.pk}/evaluation/{evaluation.pk}/preview"
+        cls.manager = make_manager()
+        cls.test_users = [cls.manager]
+        cls.url = f"/staff/semester/{cls.evaluation.course.semester.pk}/evaluation/{cls.evaluation.pk}/preview"
+
+    def test_without_questionnaires_assigned(self):
+        # regression test for #1747
+        self.evaluation.general_contribution.questionnaires.set([])
+        self.app.get(self.url, user=self.manager, status=200)
 
 
 class TestEvaluationImportPersonsView(WebTestStaffMode):