diff --git a/hypha/apply/review/blocks.py b/hypha/apply/review/blocks.py index 35a4cb38e130b65ce3812cccaad3b3484c1501a1..64df4756dd16b5d9e55c4564e93c9c13b75d13f1 100644 --- a/hypha/apply/review/blocks.py +++ b/hypha/apply/review/blocks.py @@ -10,6 +10,7 @@ from hypha.apply.review.options import ( NA, PRIVATE, RATE_CHOICE_NA, + RATE_CHOICES, RATE_CHOICES_DICT, RECOMMENDATION_CHOICES, VISIBILILTY_HELP_TEXT, @@ -53,6 +54,49 @@ class ScoreFieldBlock(OptionalFormFieldBlock): return super().render(value, context) +class ScoreFieldWithoutTextBlock(OptionalFormFieldBlock): + """ + There are two ways score could be accepted on reviews. + + One is to use ScoreFieldBlock, where you need to put text answer along with + giving score on the review. + + Second is to use this block to just select a reasonable score with adding + any text as answer. + + This block modifies RATE_CHOICES to have empty string('') in place of NA + for text value `n/a - choose not to answer` as it helps to render this value + as default to the forms and also when this field is + required it automatically handles validation on empty string. + """ + name = 'score without text' + field_class = forms.ChoiceField + + class Meta: + icon = 'order' + + def get_field_kwargs(self, struct_value): + kwargs = super().get_field_kwargs(struct_value) + kwargs['choices'] = self.get_choices(RATE_CHOICES) + return kwargs + + def render(self, value, context=None): + data = int(context['data']) + choices = dict(self.get_choices(RATE_CHOICES)) + context['data'] = choices[data] + + return super().render(value, context) + + def get_choices(self, choices): + """ + Replace 'NA' option with an empty string choice. + """ + rate_choices = list(choices) + rate_choices.pop(-1) + rate_choices.append(('', 'n/a - choose not to answer')) + return tuple(rate_choices) + + class ReviewMustIncludeFieldBlock(MustIncludeFieldBlock): pass @@ -117,6 +161,7 @@ class ReviewCustomFormFieldsBlock(CustomFormFieldsBlock): text = TextFieldBlock(group=_('Fields')) text_markup = RichTextBlock(group=_('Fields'), label=_('Paragraph')) score = ScoreFieldBlock(group=_('Fields')) + score_without_text = ScoreFieldWithoutTextBlock(group=_('Fields')) checkbox = CheckboxFieldBlock(group=_('Fields')) dropdown = DropdownFieldBlock(group=_('Fields')) diff --git a/hypha/apply/review/forms.py b/hypha/apply/review/forms.py index 76b6368188692044bdc35f0628aed6ff7fb7c026..db2631a53e6f3b4eb54eca7c366bbdd4d932ca81 100644 --- a/hypha/apply/review/forms.py +++ b/hypha/apply/review/forms.py @@ -81,13 +81,20 @@ class ReviewModelForm(StreamBaseForm, forms.ModelForm, metaclass=MixedMetaClass) def calculate_score(self, data): scores = list() - for field in self.instance.score_fields: score = data.get(field.id)[1] # Include NA answers as 0. if score == NA: score = 0 scores.append(score) + # Check if there are score_fields_without_text and also + # append scores from them. + for field in self.instance.score_fields_without_text: + score = data.get(field.id) + # Include '' answers as 0. + if score == '': + score = 0 + scores.append(int(score)) try: return sum(scores) / len(scores) diff --git a/hypha/apply/review/migrations/0023_add_score_without_text_block.py b/hypha/apply/review/migrations/0023_add_score_without_text_block.py new file mode 100644 index 0000000000000000000000000000000000000000..d6dccfb47a06d2f2e1a8d4126e13a29b126e62d0 --- /dev/null +++ b/hypha/apply/review/migrations/0023_add_score_without_text_block.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.13 on 2020-07-20 10:55 + +from django.db import migrations +import wagtail.core.blocks +import wagtail.core.blocks.static_block +import wagtail.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0022_add_word_limit_to_text_blocks'), + ] + + operations = [ + 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('score_without_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False)), ('default_value', wagtail.core.blocks.TextBlock(label='Default value', required=False)), ('word_limit', wagtail.core.blocks.IntegerBlock(default=1000, label='Word limit'))], 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('required', wagtail.core.blocks.BooleanBlock(label='Required', required=False))], group='Fields')), ('score_without_text', wagtail.core.blocks.StructBlock([('field_label', wagtail.core.blocks.CharBlock(label='Label')), ('help_text', wagtail.core.blocks.TextBlock(label='Help text', required=False)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', 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)), ('help_link', wagtail.core.blocks.URLBlock(label='Help link', required=False)), ('info', wagtail.core.blocks.static_block.StaticBlock())], group=' Required'))]), + ), + ] diff --git a/hypha/apply/review/models.py b/hypha/apply/review/models.py index de821de348eea794515932cfb43c75898ee1a649..528db5b544b17a69e94f01d9b310bbfdfce36c09 100644 --- a/hypha/apply/review/models.py +++ b/hypha/apply/review/models.py @@ -20,6 +20,7 @@ from .blocks import ( RecommendationCommentsBlock, ReviewCustomFormFieldsBlock, ScoreFieldBlock, + ScoreFieldWithoutTextBlock, VisibilityBlock, ) from .options import ( @@ -46,6 +47,10 @@ class ReviewFormFieldsMixin(models.Model): def score_fields(self): return self._get_field_type(ScoreFieldBlock, many=True) + @property + def score_fields_without_text(self): + return self._get_field_type(ScoreFieldWithoutTextBlock, many=True) + @property def recommendation_field(self): return self._get_field_type(RecommendationBlock) diff --git a/hypha/apply/review/options.py b/hypha/apply/review/options.py index 3bcdde01898dc5f5a613c431a634938111af73d1..0d837ea1df947eae286e16afaa01b284ee740806 100644 --- a/hypha/apply/review/options.py +++ b/hypha/apply/review/options.py @@ -9,6 +9,7 @@ RATE_CHOICES = ( (5, '5. Excellent'), (NA, 'n/a - choose not to answer'), ) + RATE_CHOICES_DICT = dict(RATE_CHOICES) RATE_CHOICE_NA = RATE_CHOICES_DICT[NA] diff --git a/hypha/apply/review/tests/factories/blocks.py b/hypha/apply/review/tests/factories/blocks.py index a3b651dc29ac0ed3b2d17922f1f78d7d9a4fd649..677a3b8c23755a1e09dd88e1bab756a383419bf2 100644 --- a/hypha/apply/review/tests/factories/blocks.py +++ b/hypha/apply/review/tests/factories/blocks.py @@ -38,6 +38,15 @@ class VisibilityBlockFactory(FormFieldBlockFactory): return random.choices([PRIVATE, REVIEWER]) +class ScoreFieldWithoutTextBlockFactory(FormFieldBlockFactory): + class Meta: + model = blocks.ScoreFieldWithoutTextBlock + + @classmethod + def make_answer(cls, params=dict()): + return random.randint(1, 5) + + class ScoreFieldBlockFactory(FormFieldBlockFactory): class Meta: model = blocks.ScoreFieldBlock @@ -60,6 +69,7 @@ ReviewFormFieldsFactory = StreamFieldUUIDFactory({ 'char': CharFieldBlockFactory, 'text': RichTextFieldBlockFactory, 'score': ScoreFieldBlockFactory, + 'score_without_text': ScoreFieldWithoutTextBlockFactory, 'recommendation': RecommendationBlockFactory, 'comments': RecommendationCommentsBlockFactory, 'visibility': VisibilityBlockFactory, diff --git a/hypha/apply/review/tests/test_views.py b/hypha/apply/review/tests/test_views.py index f92f26f6ff4ced18c207b9322a28c4ec5ce9893c..ef12e0a53f198c4dca2dac2a76bb2bf5e34e10aa 100644 --- a/hypha/apply/review/tests/test_views.py +++ b/hypha/apply/review/tests/test_views.py @@ -131,23 +131,38 @@ class TestReviewScore(BaseViewTestCase): def get_kwargs(self, instance): return {'submission_pk': instance.id} - def submit_review_scores(self, *scores): + def submit_review_scores(self, scores=(), scores_without_text=()): if scores: - form = ReviewFormFactory(form_fields__multiple__score=len(scores)) + form = ReviewFormFactory( + form_fields__multiple__score=len(scores), + form_fields__multiple__score_without_text=len(scores_without_text) + ) else: - form = ReviewFormFactory(form_fields__exclude__score=True) + form = ReviewFormFactory( + form_fields__exclude__score=True, + form_fields__exclude__score_without_text=True + ) review_form = self.submission.round.review_forms.first() review_form.form = form review_form.save() - - data = ReviewFormFieldsFactory.form_response(form.form_fields, { + score_fields = { field.id: {'score': score} for field, score in zip(form.score_fields, scores) - }) + } + score_fields_without_text = { + field.id: score + for field, score in zip(form.score_fields_without_text, scores_without_text) + } + score_fields.update(score_fields_without_text) + data = ReviewFormFieldsFactory.form_response( + form.form_fields, + score_fields + ) # Make a new person for every review self.client.force_login(self.user_factory()) response = self.post_page(self.submission, data, 'form') + # import ipdb; ipdb.set_trace() self.assertIn( 'funds/applicationsubmission_admin_detail.html', response.template_name, @@ -157,11 +172,11 @@ class TestReviewScore(BaseViewTestCase): return self.submission.reviews.first() def test_score_calculated(self): - review = self.submit_review_scores(5) + review = self.submit_review_scores((5,), (5,)) self.assertEqual(review.score, 5) def test_average_score_calculated(self): - review = self.submit_review_scores(1, 5) + review = self.submit_review_scores((1, 5), (1, 5)) self.assertEqual(review.score, (1 + 5) / 2) def test_no_score_is_NA(self): @@ -169,16 +184,16 @@ class TestReviewScore(BaseViewTestCase): self.assertEqual(review.score, NA) def test_na_included_in_review_average(self): - review = self.submit_review_scores(NA, 5) + review = self.submit_review_scores((NA, 5)) self.assertEqual(review.score, 2.5) def test_na_included_reviews_average(self): - self.submit_review_scores(NA) + self.submit_review_scores((NA,)) self.assertIsNotNone(Review.objects.score()) def test_na_included_multiple_reviews_average(self): - self.submit_review_scores(NA) - self.submit_review_scores(5) + self.submit_review_scores((NA,)) + self.submit_review_scores((5,)) self.assertEqual(Review.objects.count(), 2) self.assertEqual(Review.objects.score(), 2.5)