Repository: e-valuation/EvaP

Issue:
Course details should be accessible read-only for student representatives in all stages
At the moment, for courses which are in one of the states "evaluated", "reviewed" or "published", student representatives cannot see the details (like lecturer, participants, and so on) of this course.

It is true that the details of a course MUST NOT be edited in those states. However, it is desirable to have read-only access to this information e.g. to check the list of lecturers and to identify potential permission issues arising from it ("X is proxy of Y, but why can't X access Y's course results? Is Y really the main lecturer [='Dozent' flag]?)

Course details should be accessible read-only for student representatives in all stages
At the moment, for courses which are in one of the states "evaluated", "reviewed" or "published", student representatives cannot see the details (like lecturer, participants, and so on) of this course.

It is true that the details of a course MUST NOT be edited in those states. However, it is desirable to have read-only access to this information e.g. to check the list of lecturers and to identify potential permission issues arising from it ("X is proxy of Y, but why can't X access Y's course results? Is Y really the main lecturer [='Dozent' flag]?)



Discussion:
the fix for #71 will add a "courses contributing to" list to the staff_user_edit page. when fixing this issue here, please make the items in that list a link to the respective course_edit page, which should then be read only for some of the courses.

#505 adds archiving functionality. in archived semesters all courses should get a link to open them in read-only mode.

@Nef10 do you still want to do this or should i remove your assignment?

You can remove my assignment

in the meantime all courses that are not in the state `published` can already be opened. so this change is only necessary for this last state.

the fix for #71 will add a "courses contributing to" list to the staff_user_edit page. when fixing this issue here, please make the items in that list a link to the respective course_edit page, which should then be read only for some of the courses.

#505 adds archiving functionality. in archived semesters all courses should get a link to open them in read-only mode.

@Nef10 do you still want to do this or should i remove your assignment?

You can remove my assignment

in the meantime all courses that are not in the state `published` can already be opened. so this change is only necessary for this last state.


Resolving patch:
diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py
--- a/evap/evaluation/models.py
+++ b/evap/evaluation/models.py
@@ -268,7 +268,7 @@ def has_enough_questionnaires(self):
     def can_user_vote(self, user):
         """Returns whether the user is allowed to vote on this course."""
         return (self.state == "inEvaluation"
-            and self.is_in_evaluation_period
+            and self.is_in_evaluation_period()
             and user in self.participants.all()
             and user not in self.voters.all())
 
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -17,6 +17,11 @@
 logger = logging.getLogger(__name__)
 
 
+def disable_all_fields(form):
+    for field in form.fields.values():
+        field.disabled = True
+
+
 class ImportForm(forms.Form, BootstrapMixin):
     vote_start_date = forms.DateField(label=_("First day of evaluation"), localize=True)
     vote_end_date = forms.DateField(label=_("Last day of evaluation"), localize=True)
@@ -107,6 +112,10 @@ def __init__(self, *args, **kwargs):
         if self.instance.state in ['inEvaluation', 'evaluated', 'reviewed']:
             self.fields['vote_start_date'].disabled = True
 
+        if not self.instance.can_staff_edit:
+            # form is used as read-only course view
+            disable_all_fields(self)
+
     def clean(self):
         super().clean()
         vote_start_date = self.cleaned_data.get('vote_start_date')
@@ -151,6 +160,9 @@ def __init__(self, *args, **kwargs):
         if self.instance.vote_start_date:
             self.fields['event_date'].initial = self.instance.vote_start_date
 
+        if not self.instance.can_staff_edit:
+            disable_all_fields(self)
+
         if self.instance.pk:
             self.fields['responsible'].initial = self.instance.responsible_contributor
             answer_counts = dict()
@@ -222,6 +234,10 @@ def __init__(self, *args, **kwargs):
 
         self.fields['questionnaires'].queryset = Questionnaire.objects.filter(is_for_contributors=True).filter(
             Q(obsolete=False) | Q(contributions__course=self.course)).distinct()
+        
+        if not self.course.can_staff_edit:
+            # form is used as read-only course view
+            disable_all_fields(self)
 
     def save(self, *args, **kwargs):
         responsibility = self.cleaned_data['responsibility']
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -465,12 +465,6 @@ def single_result_create(request, semester_id):
 def course_edit(request, semester_id, course_id):
     semester = get_object_or_404(Semester, id=semester_id)
     course = get_object_or_404(Course, id=course_id)
-    raise_permission_denied_if_archived(course)
-
-    # check course state
-    if not course.can_staff_edit:
-        messages.warning(request, _("Editing not possible in current state."))
-        return redirect('staff:semester_view', semester_id)
 
     if course.is_single_result():
         return helper_single_result_edit(request, semester, course)
@@ -490,7 +484,11 @@ def helper_course_edit(request, semester, course):
     if form.is_valid() and formset.is_valid():
         if operation not in ('save', 'approve'):
             raise SuspiciousOperation("Invalid POST operation")
-        if course.state in ['evaluated', 'reviewed'] and course.is_in_evaluation_period:
+
+        if not course.can_staff_edit or course.is_archived:
+            raise SuspiciousOperation("Modifying this course is not allowed.")
+
+        if course.state in ['evaluated', 'reviewed'] and course.is_in_evaluation_period():
             course.reopen_evaluation()
         form.save(user=request.user)
         formset.save()
@@ -506,7 +504,7 @@ def helper_course_edit(request, semester, course):
         return custom_redirect('staff:semester_view', semester.id)
     else:
         sort_formset(request, formset)
-        template_data = dict(semester=semester, course=course, form=form, formset=formset, staff=True)
+        template_data = dict(semester=semester, course=course, form=form, formset=formset, staff=True, state=course.state)
         return render(request, "staff_course_form.html", template_data)
 
 
@@ -515,6 +513,9 @@ def helper_single_result_edit(request, semester, course):
     form = SingleResultForm(request.POST or None, instance=course)
 
     if form.is_valid():
+        if not course.can_staff_edit or course.is_archived:
+            raise SuspiciousOperation("Modifying this course is not allowed.")
+
         form.save(user=request.user)
 
         messages.success(request, _("Successfully created single result."))


Test patch:
diff --git a/evap/staff/tests/tests_general.py b/evap/staff/tests/tests_general.py
--- a/evap/staff/tests/tests_general.py
+++ b/evap/staff/tests/tests_general.py
@@ -364,7 +364,6 @@ def test_redirecting_urls(self):
             do not return 200 but redirect somewhere else.
         """
         tests = [
-            ("test_staff_semester_x_course_y_edit_fail", "/staff/semester/1/course/8/edit", "evap"),
         ]
 
         for _, url, user in tests:
@@ -815,7 +814,6 @@ def test_raise_403(self):
         self.get_assert_403(semester_url + "import", "evap")
         self.get_assert_403(semester_url + "assign", "evap")
         self.get_assert_403(semester_url + "course/create", "evap")
-        self.get_assert_403(semester_url + "course/7/edit", "evap")
         self.get_assert_403(semester_url + "courseoperation", "evap")
 
     def test_course_is_not_archived_if_participant_count_is_set(self):