diff --git a/core/migrations/0013_auto_20190308_1448.py b/core/migrations/0013_auto_20190308_1448.py new file mode 100644 index 0000000000000000000000000000000000000000..3f2e76d83b47225f972a0469cdbd1062513fb200 --- /dev/null +++ b/core/migrations/0013_auto_20190308_1448.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.4 on 2019-03-08 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_auto_20190306_1611'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='score', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5), + ), + ] diff --git a/core/models.py b/core/models.py index 224eca210c4242137dbbd5f905a7a336733bcfe2..77ae5c3f3a3e34905609aa1e8ffb697be2be5811 100644 --- a/core/models.py +++ b/core/models.py @@ -447,7 +447,7 @@ class Feedback(models.Model): origin : IntegerField Of whom was this feedback originally created. She below for the choices """ - score = models.PositiveIntegerField(default=0) + score = models.DecimalField(max_digits=5, decimal_places=2, default=0) created = models.DateTimeField(auto_now_add=True) is_final = models.BooleanField(default=False) @@ -456,6 +456,9 @@ class Feedback(models.Model): on_delete=models.CASCADE, related_name='feedback') + # the denominators that are allowed for the decimal score interpreted as a fraction + ALLOWED_DENOMINATORS = [1, 2] + # how was this feedback created ( WAS_EMPTY, diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py index 94949fbb48186c97d63f69151c5ba96dbe2ddc8e..808fa07d1892304e136913daec1bc40ffebd3e6f 100644 --- a/core/serializers/feedback.py +++ b/core/serializers/feedback.py @@ -163,6 +163,12 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): raise serializers.ValidationError( f'Score has to be in range [0..{submission.type.full_score}].') + if score.as_integer_ratio()[1] not in Feedback.ALLOWED_DENOMINATORS: + raise serializers.ValidationError( + f'For fractional scores, the denominator must be one of ' + f'{Feedback.ALLOWED_DENOMINATORS}' + ) + has_full_score = score == submission.type.full_score has_feedback_lines = ('feedback_lines' in data and len(data['feedback_lines']) > 0 or diff --git a/core/tests/test_feedback.py b/core/tests/test_feedback.py index d82dcca27e1c6121d4f7fd28ceb5f47fc611d119..ad541d8f7f173a8171f9637b2a3d2dab136e5c92 100644 --- a/core/tests/test_feedback.py +++ b/core/tests/test_feedback.py @@ -115,6 +115,7 @@ class FeedbackCreateTestCase(APITestCase): text=text) def setUp(self): + self.sub.refresh_from_db() self.client.force_authenticate(user=self.tutor) self.subscription = models.SubmissionSubscription.objects.create( owner=self.tutor, @@ -179,6 +180,32 @@ class FeedbackCreateTestCase(APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Feedback.objects.count(), 0) + def test_cannot_create_feedback_with_score_with_invalid_fractional_denominator(self): + data = { + 'score': 1.500000001, + 'is_final': False, + 'of_submission': self.assignment.submission.pk + } + self.assertEqual(Feedback.objects.count(), 0) + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Feedback.objects.count(), 0) + + def test_can_create_feedback_with_half_points(self): + data = { + 'score': 0.5, + 'is_final': False, + 'of_submission': self.assignment.submission.pk, + 'feedback_lines': { + '2': { + 'text': 'Why you no learn how to code, man?' + } + } + } + self.client.post(self.url, data, format='json') + object_score = self.sub.feedback.score + self.assertEqual(object_score, 0.5) + def test_check_score_is_set_accordingly(self): data = { 'score': 5, @@ -200,7 +227,7 @@ class FeedbackCreateTestCase(APITestCase): 'is_final': False, 'of_submission': self.assignment.submission.pk, 'feedback_lines': { - '4': { + '3': { 'text': 'Nice meth!' } } diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue index 891c567b15c8ce9c5d9a0e38c8520f23fe2a23e4..2987961a12e905d4f978606476629dd6424c378b 100644 --- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue @@ -12,32 +12,29 @@ <span>Skip this submission</span> </v-tooltip> <v-spacer /> - <v-alert class="score-alert ma-3" - color="error" - icon="warning" - :value="scoreError">{{ scoreError }}</v-alert> </v-flex> <v-flex> <v-layout wrap class="score-submit-container"> <v-flex xs5 class="score-flex"> <span class="mr-2">Score:</span> - <input class="score-text-field" + <input class="score-text-field" id="score-input" - type="number" - v-model="score" - @input="validateScore" + type="number" + step="0.5" + v-model="score" + @input="validateScore" @change="validateScore" /> <span> / {{fullScore}}</span> - <v-btn outline round flat + <v-btn outline round flat id="score-zero" - class="score-button" - @click="score=0" + class="score-button" + @click="score=0" color="red lighten-1">0 </v-btn> - <v-btn outline round flat + <v-btn outline round flat id="score-full" - @click="score=fullScore" - color="blue darken-3" + @click="score=fullScore" + color="blue darken-3" class="score-button">{{fullScore}} </v-btn> </v-flex> @@ -45,11 +42,11 @@ <v-layout> <v-flex xs4> <v-tooltip top v-if="showFinalCheckbox"> - <v-toolbar-items class="final-container" + <v-toolbar-items class="final-container" slot="activator"> <label>Final</label> - <v-checkbox slot="activator" - v-model="isFinal" + <v-checkbox slot="activator" + v-model="isFinal" class="final-checkbox" /> </v-toolbar-items> <span>Non final feedback will be sent to the reviewer.</span> @@ -57,10 +54,10 @@ </v-flex> <v-flex xs> <v-tooltip top> - <v-btn color="success" + <v-btn color="success" id="submit-feedback" - slot="activator" - :loading="loading" + slot="activator" + :loading="loading" @click="submit">Submit <v-icon>chevron_right</v-icon> </v-btn> @@ -71,6 +68,12 @@ </v-flex> </v-layout> </v-flex> + <v-flex v-if="scoreError"> + <v-alert class="score-alert ma-3" + color="error" + icon="warning" + :value="scoreError">{{ scoreError }}</v-alert> + </v-flex> </v-layout> </v-container> </template> diff --git a/grady/settings/default.py b/grady/settings/default.py index a147a3d85ea8b09bfbffc01eb2e9cde76fa84e1f..36cca69c88ad14a070432e9dc835cde0d64b1fe8 100644 --- a/grady/settings/default.py +++ b/grady/settings/default.py @@ -150,6 +150,7 @@ REST_FRAMEWORK = { 'DEFAULT_PARSER_CLASSES': ( 'djangorestframework_camel_case.parser.CamelCaseJSONParser', ), + 'COERCE_DECIMAL_TO_STRING': False, } JSON_CAMEL_CASE = {