Repository: pretalx/pretalx

Issue:
Lock reviews once a submission is accepted/rejected
Nobody has any business editing a review after a decision has been made, afaik.


Discussion:

Resolving patch:
diff --git a/src/pretalx/orga/views/review.py b/src/pretalx/orga/views/review.py
--- a/src/pretalx/orga/views/review.py
+++ b/src/pretalx/orga/views/review.py
@@ -1,10 +1,12 @@
 from django.contrib import messages
 from django.db import models
+from django.shortcuts import redirect
+from django.utils.functional import cached_property
 from django.utils.translation import ugettext_lazy as _
 from django.views.generic import ListView, TemplateView
 
 from pretalx.common.permissions import PermissionRequired
-from pretalx.common.views import ActionFromUrl, CreateOrUpdateView
+from pretalx.common.views import CreateOrUpdateView
 from pretalx.orga.forms import ReviewForm
 from pretalx.person.models import EventPermission
 from pretalx.submission.models import Review
@@ -32,12 +34,12 @@ def get_context_data(self, *args, **kwargs):
         return ctx
 
 
-class ReviewSubmission(PermissionRequired, ActionFromUrl, CreateOrUpdateView):
+class ReviewSubmission(PermissionRequired, CreateOrUpdateView):
 
     form_class = ReviewForm
     model = Review
     template_name = 'orga/submission/review.html'
-    permission_required = 'submission.review_submission'
+    permission_required = 'submission.view_reviews'
 
     @property
     def submission(self):
@@ -49,27 +51,39 @@ def get_object(self):
     def get_permission_object(self):
         return self.submission
 
+    @cached_property
+    def read_only(self):
+        return not self.request.user.has_perm('submission.review_submission', self.get_object() or self.submission)
+
     def get_context_data(self, *args, **kwargs):
         ctx = super().get_context_data(*args, **kwargs)
         submission = self.request.event.submissions.get(code__iexact=self.kwargs['code'])
         ctx['submission'] = submission
         ctx['review'] = submission.reviews.filter(user=self.request.user).first()
+        ctx['read_only'] = self.read_only
         return ctx
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         kwargs['event'] = self.request.event
         kwargs['user'] = self.request.user
+        kwargs['read_only'] = self.read_only
         return kwargs
 
     def form_valid(self, form):
         form.instance.submission = self.submission
         form.instance.user = self.request.user
+        if not form.instance.pk:
+            if not self.request.user.has_perm('submission.review_submission', self.submission):
+                messages.error(self.request, _('You cannot review this submission at this time.'))
+                return redirect(self.get_success_url())
+        if form.instance.pk and not self.request.user.has_perm('submission.edit_review', form.instance):
+            messages.error(self.request, _('You cannot review this submission at this time.'))
+            return redirect(self.get_success_url())
         form.save()
         return super().form_valid(form)
 
     def get_success_url(self) -> str:
-
         if self.request.POST.get('show_next', '0').strip() == '1':
             next_submission = Review.find_missing_reviews(self.request.event, self.request.user).first()
             if next_submission:
diff --git a/src/pretalx/submission/permissions.py b/src/pretalx/submission/permissions.py
--- a/src/pretalx/submission/permissions.py
+++ b/src/pretalx/submission/permissions.py
@@ -6,6 +6,8 @@
 
 @rules.predicate
 def is_speaker(user, obj):
+    if hasattr(obj, 'submission'):
+        obj = obj.submission
     return user in obj.speakers.all()
 
 
@@ -54,6 +56,15 @@ def is_review_author(user, obj):
     return obj.user == user
 
 
+@rules.predicate
+def can_be_reviewed(user, obj):
+    if not obj:
+        return False
+    if hasattr(obj, 'submission'):
+        obj = obj.submission
+    return obj.state == SubmissionStates.SUBMITTED
+
+
 rules.add_perm('submission.withdraw_submission', can_be_withdrawn & is_speaker)
 rules.add_perm('submission.reject_submission', can_be_rejected & is_orga)
 rules.add_perm('submission.accept_submission', can_be_accepted & is_orga)
@@ -62,7 +73,9 @@ def is_review_author(user, obj):
 rules.add_perm('submission.unconfirm_submission', can_be_unconfirmed & (is_speaker | is_orga))
 rules.add_perm('submission.remove_submission', can_be_removed & is_orga)
 rules.add_perm('submission.edit_submission', (can_be_edited & is_speaker) | is_orga)
-rules.add_perm('submission.review_submission', is_reviewer & ~is_speaker)
 rules.add_perm('submission.view_submission', is_speaker | is_orga | is_reviewer)
+rules.add_perm('submission.review_submission', is_reviewer & ~is_speaker & can_be_reviewed)
+rules.add_perm('submission.edit_review', can_be_reviewed & is_review_author)
+rules.add_perm('submission.view_reviews', is_reviewer & ~is_speaker)
 rules.add_perm('submission.edit_speaker_list', is_speaker | is_orga)
 rules.add_perm('submission.view_feedback', is_speaker | is_orga | is_reviewer)


Test patch:
diff --git a/src/tests/functional/orga/test_review.py b/src/tests/functional/orga/test_review.py
--- a/src/tests/functional/orga/test_review.py
+++ b/src/tests/functional/orga/test_review.py
@@ -75,6 +75,21 @@ def test_reviewer_cannot_review_own_submission(review_user, review_client, submi
     assert submission.reviews.count() == 0
 
 
+@pytest.mark.django_db
+def test_reviewer_cannot_review_accepted_submission(review_user, review_client, submission):
+    submission.accept()
+    response = review_client.post(
+        submission.orga_urls.reviews,
+        data={
+            'score': 100,
+            'text': 'LGTM',
+        },
+        follow=True,
+    )
+    assert response.status_code == 200
+    assert submission.reviews.count() == 0
+
+
 @pytest.mark.django_db
 def test_reviewer_can_edit_review(review_client, review):
     response = review_client.post(
@@ -91,6 +106,23 @@ def test_reviewer_can_edit_review(review_client, review):
     assert review.text == 'My mistake.'
 
 
+@pytest.mark.django_db
+def test_reviewer_cannot_edit_review_after_accept(review_client, review):
+    review.submission.accept()
+    response = review_client.post(
+        review.urls.base, follow=True,
+        data={
+            'score': 0,
+            'text': 'My mistake.',
+        }
+    )
+    review.refresh_from_db()
+    assert response.status_code == 200
+    assert review.submission.reviews.count() == 1
+    assert review.score != 0
+    assert review.text != 'My mistake.'
+
+
 @pytest.mark.django_db
 def test_cannot_see_other_review_before_own(other_review_client, review):
     response = other_review_client.get(review.urls.base, follow=True)
@@ -113,12 +145,41 @@ def test_cannot_see_other_review_before_own(other_review_client, review):
     assert 'My mistake' in response.content.decode()
 
 
+@pytest.mark.django_db
+def test_can_see_review(review_client, review):
+    response = review_client.get(review.urls.base, follow=True)
+    assert response.status_code == 200
+    assert review.text in response.content.decode()
+
+
+@pytest.mark.django_db
+def test_can_see_review_after_accept(review_client, review):
+    review.submission.accept()
+    response = review_client.get(review.urls.base, follow=True)
+    assert response.status_code == 200
+    assert review.text in response.content.decode()
+
+
+@pytest.mark.django_db
+def test_orga_cannot_see_review(orga_client, review):
+    response = orga_client.get(review.urls.base, follow=True)
+    assert response.status_code == 404
+
+
 @pytest.mark.django_db
 def test_reviewer_can_see_dashboard(review_client, submission, review):
     response = review_client.get(submission.event.orga_urls.reviews)
     assert response.status_code == 200
 
 
-# def test_orga_cannot_see_review
-# def test_orga_cannot_add_review
-# def test_cannot_see_own_submissions_reviews
+@pytest.mark.django_db
+def test_orga_cannot_add_review(orga_client, submission):
+    response = orga_client.post(
+        submission.orga_urls.reviews, follow=True,
+        data={
+            'score': 1,
+            'text': 'LGTM',
+        }
+    )
+    assert response.status_code == 404
+    assert submission.reviews.count() == 0