diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 593bca48a04f9752bbb0ec6d1b44cad49289e7ab..0151cb79e8d310de298eb0a316716095d0a32fc5 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model from django.contrib.postgres.fields import JSONField from django.core.exceptions import PermissionDenied from django.db import models -from django.db.models import Count, IntegerField, OuterRef, Subquery, Sum, Q +from django.db.models import Count, IntegerField, OuterRef, Subquery, Sum, Q, Prefetch from django.db.models.expressions import RawSQL, OrderBy from django.db.models.functions import Coalesce from django.dispatch import receiver @@ -21,6 +21,7 @@ from wagtail.contrib.forms.models import AbstractFormSubmission from opentech.apply.activity.messaging import messenger, MESSAGES from opentech.apply.determinations.models import Determination +from opentech.apply.review.models import Review, ReviewOpinion from opentech.apply.review.options import AGREE from opentech.apply.stream_forms.blocks import UploadableMediaBlock from opentech.apply.stream_forms.files import StreamFieldDataEncoder @@ -143,7 +144,11 @@ class ApplicationSubmissionQueryset(JSONOrderable): ), role_icon=Subquery(roles_for_review[:1].values('role__icon')), ).prefetch_related( - 'reviews__author' + Prefetch( + 'reviews', queryset=Review.objects.select_related('author').prefetch_related( + Prefetch('opinions', queryset=ReviewOpinion.objects.select_related('author')) + ) + ) ).select_related( 'page', 'round', diff --git a/opentech/apply/funds/templates/funds/includes/review_sidebar_item.html b/opentech/apply/funds/templates/funds/includes/review_sidebar_item.html index be0bba5a1275fcdfecc6d6f717fddc1b7adf7df0..1f03ab252075094aad321f72a0758d7dd1728d92 100644 --- a/opentech/apply/funds/templates/funds/includes/review_sidebar_item.html +++ b/opentech/apply/funds/templates/funds/includes/review_sidebar_item.html @@ -9,7 +9,7 @@ <div>-</div> <div>-</div> {% else %} - {% if request.user.is_apply_staff or request.user == reviewer %} + {% if request.user == reviewer or request.user.is_reviewer and review.reviewer_visibility or request.user.is_apply_staff %} <div> <a href="{% url 'apply:submissions:reviews:review' submission_pk=review.submission.id pk=review.id %}"> <div class="reviews-sidebar__name"> diff --git a/opentech/apply/review/blocks.py b/opentech/apply/review/blocks.py index 1edebc7db987e4753a9c69ad8fac1348bf6f55f2..6528f55c23bc13afcd3ca4729c05c5785aafefb7 100644 --- a/opentech/apply/review/blocks.py +++ b/opentech/apply/review/blocks.py @@ -2,12 +2,13 @@ import json from django import forms +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from wagtail.core.blocks import RichTextBlock from opentech.apply.review.fields import ScoredAnswerField -from opentech.apply.review.options import RECOMMENDATION_CHOICES, RATE_CHOICES_DICT, RATE_CHOICE_NA +from opentech.apply.review.options import RECOMMENDATION_CHOICES, RATE_CHOICES_DICT, RATE_CHOICE_NA, VISIBILITY, VISIBILILTY_HELP_TEXT, PRIVATE from opentech.apply.stream_forms.blocks import OptionalFormFieldBlock, CharFieldBlock, TextFieldBlock, CheckboxFieldBlock, DropdownFieldBlock from opentech.apply.utils.blocks import CustomFormFieldsBlock, MustIncludeFieldBlock from opentech.apply.utils.options import RICH_TEXT_WIDGET_SHORT @@ -75,6 +76,25 @@ class RecommendationCommentsBlock(ReviewMustIncludeFieldBlock): return kwargs +class VisibilityBlock(ReviewMustIncludeFieldBlock): + name = 'visibility' + description = 'Visibility' + field_class = forms.ChoiceField + widget = forms.RadioSelect() + + class Meta: + icon = 'radio-empty' + + def get_field_kwargs(self, struct_value): + kwargs = super(VisibilityBlock, self).get_field_kwargs(struct_value) + kwargs['choices'] = VISIBILITY.items() + kwargs['initial'] = PRIVATE + kwargs['help_text'] = mark_safe('<br>'.join( + [VISIBILITY[choice] + ': ' + VISIBILILTY_HELP_TEXT[choice] for choice in VISIBILITY] + )) + return kwargs + + class ReviewCustomFormFieldsBlock(CustomFormFieldsBlock): char = CharFieldBlock(group=_('Fields')) text = TextFieldBlock(group=_('Fields')) diff --git a/opentech/apply/review/forms.py b/opentech/apply/review/forms.py index 0d29c09ea311ac510dadc7422f527ce0ad0dfc38..fba3b158d5543fadf37a76b6394d4bdcfe53fb2b 100644 --- a/opentech/apply/review/forms.py +++ b/opentech/apply/review/forms.py @@ -6,7 +6,7 @@ from opentech.apply.review.options import NA from opentech.apply.stream_forms.forms import StreamBaseForm from .models import Review, ReviewOpinion -from .options import OPINION_CHOICES +from .options import OPINION_CHOICES, PRIVATE class MixedMetaClass(type(StreamBaseForm), type(forms.ModelForm)): @@ -18,13 +18,14 @@ class ReviewModelForm(StreamBaseForm, forms.ModelForm, metaclass=MixedMetaClass) class Meta: model = Review - fields = ['recommendation', 'score', 'submission', 'author'] + fields = ['recommendation', 'visibility', 'score', 'submission', 'author'] widgets = { 'recommendation': forms.HiddenInput(), 'score': forms.HiddenInput(), 'submission': forms.HiddenInput(), 'author': forms.HiddenInput(), + 'visibility': forms.HiddenInput(), } error_messages = { @@ -65,6 +66,12 @@ class ReviewModelForm(StreamBaseForm, forms.ModelForm, metaclass=MixedMetaClass) self.instance.score = self.calculate_score(self.cleaned_data) self.instance.recommendation = int(self.cleaned_data[self.instance.recommendation_field.id]) self.instance.is_draft = self.draft_button_name in self.data + # Old review forms do not have the requred visability field. + # This will set visibility to PRIVATE by default. + try: + self.instance.visibility = self.cleaned_data[self.instance.visibility_field.id] + except AttributeError: + self.instance.visibility = PRIVATE self.instance.form_data = self.cleaned_data['form_data'] diff --git a/opentech/apply/review/migrations/0016_review_visibility.py b/opentech/apply/review/migrations/0016_review_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..3eb9e363a61c17a7a537f4540390b2808d3a9120 --- /dev/null +++ b/opentech/apply/review/migrations/0016_review_visibility.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.9 on 2018-12-12 17:48 + +from django.db import migrations, models +import wagtail.core.blocks +import wagtail.core.blocks.static_block +import wagtail.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0015_review_opinion'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='visibility', + field=models.CharField(choices=[('private', 'Private'), ('reviewers', 'Reviewers and Staff')], default='private', max_length=10, verbose_name='Visibility'), + ), + migrations.AlterField( + model_name='review', + name='form_fields', + field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('markdown_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('char', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('format', wagtail.core.blocks.ChoiceBlock(choices=[('email', 'Email'), ('url', 'URL')], label='Format', required=False)), ('default_value', wagtail.core.blocks.CharBlock(label='Default value', required=False))], group='Fields')), ('text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('text_markup', wagtail.core.blocks.RichTextBlock(group='Fields', label='Paragraph')), ('score', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('checkbox', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.BooleanBlock(required=False))], group='Fields')), ('dropdown', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('choices', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Choice')))], group='Fields')), ('recommendation', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required')), ('comments', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required')), ('visibility', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required'))]), + ), + migrations.AlterField( + model_name='reviewform', + name='form_fields', + field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('markdown_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('char', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('format', wagtail.core.blocks.ChoiceBlock(choices=[('email', 'Email'), ('url', 'URL')], label='Format', required=False)), ('default_value', wagtail.core.blocks.CharBlock(label='Default value', required=False))], group='Fields')), ('text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False))], group='Fields')), ('text_markup', wagtail.core.blocks.RichTextBlock(group='Fields', label='Paragraph')), ('score', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('checkbox', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.BooleanBlock(required=False))], group='Fields')), ('dropdown', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('choices', wagtail.core.blocks.ListBlock(wagtail.core.blocks.CharBlock(label='Choice')))], group='Fields')), ('recommendation', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required')), ('comments', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required')), ('visibility', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required'))]), + ), + ] diff --git a/opentech/apply/review/models.py b/opentech/apply/review/models.py index bd5f083d2e3f361f8e940cf242bd1baebea9be66..c8ea72569e8e5d4ef6989b5d1cdead21d899c7a0 100644 --- a/opentech/apply/review/models.py +++ b/opentech/apply/review/models.py @@ -5,13 +5,12 @@ from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.core.fields import StreamField -from opentech.apply.funds.models import AssignedReviewers from opentech.apply.funds.models.mixins import AccessFormData -from opentech.apply.review.options import YES, NO, MAYBE, RECOMMENDATION_CHOICES, OPINION_CHOICES from opentech.apply.stream_forms.models import BaseStreamForm from opentech.apply.users.models import User @@ -20,8 +19,9 @@ from .blocks import ( RecommendationBlock, RecommendationCommentsBlock, ScoreFieldBlock, + VisibilityBlock, ) -from .options import NA +from .options import NA, YES, NO, MAYBE, RECOMMENDATION_CHOICES, OPINION_CHOICES, VISIBILITY, PRIVATE, REVIEWER class ReviewFormFieldsMixin(models.Model): @@ -38,6 +38,10 @@ class ReviewFormFieldsMixin(models.Model): def recommendation_field(self): return self._get_field_type(RecommendationBlock) + @property + def visibility_field(self): + return self._get_field_type(VisibilityBlock) + @property def comment_field(self): return self._get_field_type(RecommendationCommentsBlock) @@ -129,6 +133,7 @@ class Review(ReviewFormFieldsMixin, BaseStreamForm, AccessFormData, models.Model is_draft = models.BooleanField(default=False, verbose_name=_("Draft")) created_at = models.DateTimeField(verbose_name=_("Creation time"), auto_now_add=True) updated_at = models.DateTimeField(verbose_name=_("Update time"), auto_now=True) + visibility = models.CharField(verbose_name=_("Visibility"), choices=VISIBILITY.items(), default=PRIVATE, max_length=10) # Meta: used for migration purposes only drupal_id = models.IntegerField(null=True, blank=True, editable=False) @@ -164,9 +169,14 @@ class Review(ReviewFormFieldsMixin, BaseStreamForm, AccessFormData, models.Model def get_compare_url(self): return self.revision.get_compare_url_to_latest() + @cached_property + def reviewer_visibility(self): + return self.visibility == REVIEWER + @receiver(post_save, sender=Review) def update_submission_reviewers_list(sender, **kwargs): + from opentech.apply.funds.models import AssignedReviewers review = kwargs.get('instance') # Make sure the reviewer is in the reviewers list on the submission diff --git a/opentech/apply/review/options.py b/opentech/apply/review/options.py index 84cec661736272f372f528805055a4c2fd0868fb..3bcdde01898dc5f5a613c431a634938111af73d1 100644 --- a/opentech/apply/review/options.py +++ b/opentech/apply/review/options.py @@ -29,3 +29,16 @@ OPINION_CHOICES = ( (AGREE, 'Agree'), (DISAGREE, 'Disagree'), ) + +PRIVATE = 'private' +REVIEWER = 'reviewers' + +VISIBILILTY_HELP_TEXT = { + PRIVATE: 'Visible only to staff.', + REVIEWER: 'Visible to other reviewers and staff.', +} + +VISIBILITY = { + PRIVATE: 'Private', + REVIEWER: 'Reviewers and Staff', +} diff --git a/opentech/apply/review/templates/review/review_detail.html b/opentech/apply/review/templates/review/review_detail.html index ef40757784276d254162cf1db75854d36e3bd698..fc0265dca511f9f59802668bf29c737144ca6b53 100644 --- a/opentech/apply/review/templates/review/review_detail.html +++ b/opentech/apply/review/templates/review/review_detail.html @@ -20,6 +20,10 @@ <h5>Score</h5> <p>{{ review.score }}</p> </div> + <div> + <svg class="icon icon--eye"><use xlink:href="#eye"></use></svg> + {{ review.get_visibility_display }} + </div> {% if not review.for_latest %} <div> <h5>Review was not against the latest version:</h5> diff --git a/opentech/apply/review/tests/factories/blocks.py b/opentech/apply/review/tests/factories/blocks.py index 31d6290a9dd01a88fdffd2cc908d7733fc926e0f..23ce783dc3b683457666799745928082e7828dad 100644 --- a/opentech/apply/review/tests/factories/blocks.py +++ b/opentech/apply/review/tests/factories/blocks.py @@ -3,7 +3,7 @@ import random import factory from opentech.apply.review import blocks -from opentech.apply.review.options import YES, MAYBE, NO +from opentech.apply.review.options import YES, MAYBE, NO, PRIVATE, REVIEWER from opentech.apply.stream_forms.testing.factories import FormFieldBlockFactory, CharFieldBlockFactory, \ StreamFieldUUIDFactory from opentech.apply.utils.testing.factories import RichTextFieldBlockFactory @@ -25,6 +25,15 @@ class RecommendationCommentsBlockFactory(FormFieldBlockFactory): model = blocks.RecommendationCommentsBlock +class VisibilityBlockFactory(FormFieldBlockFactory): + class Meta: + model = blocks.VisibilityBlock + + @classmethod + def make_answer(cls, params=dict()): + return random.choices([PRIVATE, REVIEWER]) + + class ScoreFieldBlockFactory(FormFieldBlockFactory): class Meta: model = blocks.ScoreFieldBlock @@ -49,4 +58,5 @@ ReviewFormFieldsFactory = StreamFieldUUIDFactory({ 'score': ScoreFieldBlockFactory, 'recommendation': RecommendationBlockFactory, 'comments': RecommendationCommentsBlockFactory, + 'visibility': VisibilityBlockFactory, }) diff --git a/opentech/apply/review/tests/factories/models.py b/opentech/apply/review/tests/factories/models.py index a6d825394a52951c3a37d9e2d409e8da67916c58..59dd321eef691091c99220f0dd02aaad495af969 100644 --- a/opentech/apply/review/tests/factories/models.py +++ b/opentech/apply/review/tests/factories/models.py @@ -4,7 +4,7 @@ from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory from opentech.apply.stream_forms.testing.factories import FormDataFactory from opentech.apply.users.tests.factories import StaffFactory -from ...options import YES, NO, MAYBE, AGREE, DISAGREE +from ...options import YES, NO, MAYBE, AGREE, DISAGREE, PRIVATE, REVIEWER from ...models import Review, ReviewForm, ReviewOpinion from . import blocks @@ -24,6 +24,8 @@ class ReviewFactory(factory.DjangoModelFactory): recommendation_yes = factory.Trait(recommendation=YES) recommendation_maybe = factory.Trait(recommendation=MAYBE) draft = factory.Trait(is_draft=True) + visibility_private = factory.Trait(visibility=PRIVATE) + visibility_reviewer = factory.Trait(visibility=REVIEWER) submission = factory.SubFactory(ApplicationSubmissionFactory) revision = factory.SelfAttribute('submission.live_revision') diff --git a/opentech/apply/review/tests/test_views.py b/opentech/apply/review/tests/test_views.py index ed32e84727f9d039324c24b26f0b757768425fe2..1a9bfbe7a23df848adff5f77ba40dd05aeb70ae9 100644 --- a/opentech/apply/review/tests/test_views.py +++ b/opentech/apply/review/tests/test_views.py @@ -2,7 +2,7 @@ from django.urls import reverse from opentech.apply.activity.models import Activity from opentech.apply.funds.tests.factories.models import ApplicationSubmissionFactory -from opentech.apply.users.tests.factories import StaffFactory, UserFactory +from opentech.apply.users.tests.factories import ReviewerFactory, StaffFactory, UserFactory from opentech.apply.utils.testing.tests import BaseViewTestCase from .factories import ReviewFactory, ReviewFormFieldsFactory, ReviewFormFactory, ReviewOpinionFactory @@ -296,3 +296,26 @@ class NonStaffReviewOpinionCase(BaseViewTestCase): review = ReviewFactory(submission=self.submission, author=staff, recommendation_yes=True) response = self.post_page(review, {'agree': AGREE}) self.assertEqual(response.status_code, 403) + + +class ReviewDetailVisibilityTestCase(BaseViewTestCase): + user_factory = ReviewerFactory + url_name = 'funds:submissions:reviews:{}' + base_view_name = 'review' + + def get_kwargs(self, instance): + return {'pk': instance.id, 'submission_pk': instance.submission.id} + + def test_review_detail_visibility_private(self): + submission = ApplicationSubmissionFactory(status='external_review', workflow_stages=2) + review = ReviewFactory(submission=submission, author=self.user, visibility_private=True) + self.client.force_login(self.user_factory()) + response = self.get_page(review) + self.assertEqual(response.status_code, 403) + + def test_review_detail_visibility_reviewer(self): + submission = ApplicationSubmissionFactory(status='external_review', workflow_stages=2) + review = ReviewFactory(submission=submission, author=self.user, visibility_reviewer=True) + self.client.force_login(self.user_factory()) + response = self.get_page(review) + self.assertEqual(response.status_code, 200) diff --git a/opentech/apply/review/views.py b/opentech/apply/review/views.py index b73244785370b8f037bbb58743b6c3aa6df1bf65..611de4fb3398ba59b499b54e073316a0b65cca47 100644 --- a/opentech/apply/review/views.py +++ b/opentech/apply/review/views.py @@ -169,9 +169,10 @@ class ReviewDisplay(DetailView): def dispatch(self, request, *args, **kwargs): review = self.get_object() + user = request.user author = review.author - if request.user != author and not request.user.is_superuser and not request.user.is_apply_staff: + if user != author and not (user.is_reviewer and review.reviewer_visibility) and not user.is_apply_staff: raise PermissionDenied if review.is_draft: diff --git a/opentech/public/forms/models.py b/opentech/public/forms/models.py index 0e9824c684d14dd7d622f46a380c2b05f42d5fac..bee7352c4805565b82b2193cdf43987aa87c8c69 100644 --- a/opentech/public/forms/models.py +++ b/opentech/public/forms/models.py @@ -6,7 +6,9 @@ from django.core.serializers.json import DjangoJSONEncoder from django.conf import settings from django.db import models from django.forms import FileField +from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import never_cache from modelcluster.fields import ParentalKey @@ -40,6 +42,7 @@ class ExtendedFormBuilder(FormBuilder): return FileField(**options) +@method_decorator(never_cache, name='serve') class FormPage(AbstractEmailForm, BasePage): form_builder = ExtendedFormBuilder subpage_types = []