diff --git a/core/migrations/0006_auto_20180104_2001.py b/core/migrations/0006_auto_20180104_2001.py index 7a1672479bf09f7629b13877cba53a6183003e49..f790ca9e5be399441ce8ee2176a5b14a27974b82 100644 --- a/core/migrations/0006_auto_20180104_2001.py +++ b/core/migrations/0006_auto_20180104_2001.py @@ -1,8 +1,8 @@ # Generated by Django 2.0.1 on 2018-01-04 20:01 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/core/models.py b/core/models.py index f2268ed945cd64a993ee96e391ce98f01c28ac83..40af977566befbd7000bd739e75b92595482b974 100644 --- a/core/models.py +++ b/core/models.py @@ -526,6 +526,15 @@ class GeneralTaskSubscription(models.Model): subscription=self, submission=task)[0] + def reserve_all_assignments_for_a_student(self): + assert self.query_type == self.STUDENT_QUERY + + try: + while True: + self.get_or_create_work_assignment() + except SubscriptionEnded as err: + log.info(f'Loaded all subscriptions of student {self.query_key}') + def _create_new_assignment_if_subscription_empty(self): if self.assignments.filter(is_done=False).count() < 1: self.get_or_create_work_assignment() diff --git a/core/permissions.py b/core/permissions.py index e88aed697a0e5500f6f8e5f376e2d9dc8f1fae12..7c5f5cf0e427c0acf8cd12a08b3b6864f8c9e51b 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -4,7 +4,6 @@ from django.http import HttpRequest from django.views import View from rest_framework import permissions - log = logging.getLogger(__name__) diff --git a/core/serializers.py b/core/serializers.py index 18b7f14233b234bec04b853500c6c912ed39b294..ecff4a41ae11e8116f45ebc786f5d5acd57c7bbb 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,15 +1,12 @@ import logging -from django.db import IntegrityError from django.core.exceptions import ObjectDoesNotExist - from drf_dynamic_fields import DynamicFieldsMixin from rest_framework import serializers from core import models from core.models import (ExamType, Feedback, GeneralTaskSubscription, - StudentInfo, - Submission, SubmissionType, + StudentInfo, Submission, SubmissionType, TutorSubmissionAssignment) from util.factories import GradyUserFactory @@ -37,16 +34,12 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): log.debug(data) assignment_id = data.pop('assignment_id') score = data.get('score') - creator = self.context.get('request').user try: assignment = TutorSubmissionAssignment.objects.get( assignment_id=assignment_id) except ObjectDoesNotExist as err: - raise serializers.ValidationError('No assignment for id') - - if not assignment.subscription.owner == creator: - raise serializers.ValidationError('This is not your assignment') + raise serializers.ValidationError('No assignment for given id.') submission = assignment.submission if not 0 <= score <= submission.type.full_score: @@ -60,12 +53,10 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): return { **data, 'assignment': assignment, - 'of_tutor': creator, 'of_submission': submission } def create(self, validated_data) -> Feedback: - log.debug(validated_data) assignment = validated_data.pop('assignment') assignment.set_done() @@ -176,27 +167,29 @@ class SubscriptionSerializer(DynamicFieldsModelSerializer): assignments = AssignmentSerializer(read_only=True, many=True) def validate(self, data): + data['owner'] = self.context['request'].user + if 'query_key' in data != \ data['query_type'] == GeneralTaskSubscription.RANDOM: raise serializers.ValidationError( f'The {data["query_type"]} query_type does not work with the' f'provided key') - return data - - def create(self, validated_data) -> GeneralTaskSubscription: - subscription = GeneralTaskSubscription.objects.create( - owner=self.context.get("request").user, - **validated_data) try: - subscription._create_new_assignment_if_subscription_empty() - except IntegrityError as err: - log.debug(err) - raise + GeneralTaskSubscription.objects.get( + owner=data['owner'], + query_type=data['query_type'], + query_key=data.get('query_key', None)) + except ObjectDoesNotExist: + pass + else: raise serializers.ValidationError( - "Oh great, you raised an IntegrityError. I'm disappointed.") + 'The user already has the subscription') + + return data - return subscription + def create(self, validated_data) -> GeneralTaskSubscription: + return GeneralTaskSubscription.objects.create(**validated_data) class Meta: model = GeneralTaskSubscription diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py index 20f81577d32c2c3998d68cb4b03261e22e2509dc..ebdcd33d0de612dc6dd05950c46379e8595da0d7 100644 --- a/core/tests/test_subscription_assignment_service.py +++ b/core/tests/test_subscription_assignment_service.py @@ -1,12 +1,12 @@ - -from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient, APITestCase from core.models import (GeneralTaskSubscription, Submission, SubmissionType, SubscriptionEnded) -from util.factories import GradyUserFactory +from util.factories import GradyUserFactory, make_test_data -class GeneralTaskSubscriptionRandomTest(TestCase): +class GeneralTaskSubscriptionRandomTest(APITestCase): @classmethod def setUpTestData(cls): @@ -56,3 +56,120 @@ class GeneralTaskSubscriptionRandomTest(TestCase): assignment = self.subscription.get_oldest_unfinished_assignment() self.assertEqual(assignment, self.subscription.get_oldest_unfinished_assignment()) + + +class TestApiEndpoints(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.data = make_test_data(data_dict={ + 'submission_types': [ + { + 'name': '01. Sort this or that', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '02. Merge this or that or maybe even this', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '03. This one exists for the sole purpose to test', + 'full_score': 30, + 'description': 'Very complicated', + 'solution': 'Trivial!' + } + ], + 'students': [ + {'username': 'student01'}, + {'username': 'student02'} + ], + 'tutors': [ + {'username': 'tutor01'}, + {'username': 'tutor02'} + ], + 'submissions': [ + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' lorem ipsum und so\n', + 'type': '01. Sort this or that', + 'user': 'student01', + 'feedback': { + 'text': 'Not good!', + 'score': 5, + 'of_tutor': 'tutor01', + 'is_final': True + } + }, + { + 'text': 'function blabl\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '02. Merge this or that or maybe even this', + 'user': 'student01' + }, + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '03. This one exists for the sole purpose to test', + 'user': 'student01' + }, + { + 'text': 'function lorem ipsum etc\n', + 'type': '03. This one exists for the sole purpose to test', + 'user': 'student02' + }, + ]} + ) + + def test_can_create_a_subscription(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response = client.post('/api/subscription/', {'query_type': 'random'}) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_subscription_and_get_one_assignment(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response = client.post('/api/subscription/', {'query_type': 'random'}) + + self.assertEqual('tutor01', response.data['owner']) + + def test_subscription_has_next_assignment(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response_subs = client.post( + '/api/subscription/', {'query_type': 'random'}) + subscription_id = response_subs.data['subscription_id'] + assignment_id = response_subs.data['assignments'][0]['assignment_id'] + + response_current = client.get( + f'/api/subscription/{subscription_id}/assignments/current/') + + self.assertEqual(1, len(response_subs.data['assignments'])) + self.assertEqual(assignment_id, + response_current.data['assignment_id']) + + def test_subscription_can_assign_to_student(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response_subs = client.post( + '/api/subscription/', { + 'query_type': 'student', + 'query_key': 'student01' + }) + + assignments = response_subs.data['assignments'] + self.assertEqual(2, len(assignments)) diff --git a/core/tests/test_tutor_api_endpoints.py b/core/tests/test_tutor_api_endpoints.py index 908626d1a3b90829b2d1fc516b6484b4c03b801e..133b23826e0219086fe911a42f26a7fd4191017f 100644 --- a/core/tests/test_tutor_api_endpoints.py +++ b/core/tests/test_tutor_api_endpoints.py @@ -109,7 +109,6 @@ class TutorDetailViewTests(APITestCase): @classmethod def setUpTestData(cls): - cls.factory = APIClient() cls.user_factory = GradyUserFactory() def setUp(self): diff --git a/core/views.py b/core/views.py index fc584ded203c172bdbe76fd3cc84404e723f276d..6a8b9839b4f014ff2cd3c0e1f529d4498e0086ee 100644 --- a/core/views.py +++ b/core/views.py @@ -7,16 +7,16 @@ from rest_framework.decorators import api_view, detail_route from rest_framework.response import Response from core import models -from core.models import (ExamType, GeneralTaskSubscription, StudentInfo, - SubmissionType, TutorSubmissionAssignment, - Feedback) +from core.models import (ExamType, Feedback, GeneralTaskSubscription, + StudentInfo, SubmissionType, + TutorSubmissionAssignment) from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer -from core.serializers import (AssignmentSerializer, ExamSerializer, +from core.serializers import (AssignmentDetailSerializer, AssignmentSerializer, + ExamSerializer, FeedbackSerializer, StudentInfoSerializer, StudentInfoSerializerForListView, SubmissionTypeSerializer, SubscriptionSerializer, - TutorSerializer, FeedbackSerializer, - AssignmentDetailSerializer) + TutorSerializer) @api_view() @@ -57,6 +57,21 @@ class FeedbackApiView( serializer_class = FeedbackSerializer lookup_field = 'submission__submission_id' + def create(self, request, *args, **kwargs): + serializer = self.get_serializer( + data={**request.data, 'of_tutor': request.user}) + serializer.is_valid(raise_exception=True) + + if serializer.data['assignment'].subscription.owner != request.user: + return Response({'You do not have permission to edit this'}, + status=status.HTTP_403_FORBIDDEN) + + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, + status=status.HTTP_201_CREATED, + headers=headers) + class TutorApiViewSet( mixins.RetrieveModelMixin, @@ -129,6 +144,21 @@ class SubscriptionApiViewSet( instance.delete() return Response(status=status.HTTP_204_NO_CONTENT) + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + subscription = serializer.save() + + if subscription.query_type == GeneralTaskSubscription.STUDENT_QUERY: + subscription.reserve_all_assignments_for_a_student() + else: + subscription.get_oldest_unfinished_assignment() + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, + status=status.HTTP_201_CREATED, + headers=headers) + class AssignmentApiViewSet( mixins.RetrieveModelMixin, diff --git a/docs/feedback.api.json b/docs/feedback.api.json new file mode 100644 index 0000000000000000000000000000000000000000..c48beea4146bc18419f8a5b03ecf2d9de9826fe2 --- /dev/null +++ b/docs/feedback.api.json @@ -0,0 +1,43 @@ +GET /subscription/<id> + { + "subscription_id": "e313e608-7453-4053-a536-5d18fc9ec3a9", + "owner": "reviewer01", + "query_type": "random", + "query_key": "", + "assignments": [ + { + "assignment_id": "dbdde0d0-b1a6-474c-b2be-41edb5229803", + "submission_id": "1558c390-5598-482b-abd3-1f5780e75e0d", + "is_done": false + } + ] + } + +POST /subscription/ + { + "owner": "<some user>", + "query_type": "random|student|submission_type|exam", + "query_key": "<pk for query type>?" + } + +DELETE /subscription/<id> +PATCH /subscription/<id> { + "deactivate": true // or false for reactivation +} + +GET /subscription/assignments/current +GET /subscription/assignments/next +GET /subscription/assignments/past + +GET /assignment/<id> // only those belonging to the requests user + { + "assignment_id": "dbdde0d0-b1a6-474c-b2be-41edb5229803", + "submission_id": "1558c390-5598-482b-abd3-1f5780e75e0d", + "is_done": false + } + +DELETE /assignment/<id> // check done conditions + +// done conditions +// * feedback was posted +// * feedback was patched (every) diff --git a/util/factories.py b/util/factories.py index 463fb549a7a3d2926d1d7d28ceb07f8f94d3bb56..9ea00f3bd8b16329ca2680e24f7d3aa13cfdcbbf 100644 --- a/util/factories.py +++ b/util/factories.py @@ -123,9 +123,11 @@ def make_submission_types(submission_types=[], **kwargs): def make_students(students=[], **kwargs): + return [GradyUserFactory().make_student( username=student['username'], - exam=ExamType.objects.get(module_reference=student['exam']) + exam=ExamType.objects.get( + module_reference=student['exam']) if 'exam' in student else None ) for student in students]