From c99d7224138bc6d21f73103b937da845f41ef67e Mon Sep 17 00:00:00 2001 From: janmax <j.michal@stud.uni-goettingen.de> Date: Sat, 17 Feb 2018 22:25:18 +0100 Subject: [PATCH] Added the option delete feedback comments --- core/migrations/0007_auto_20180217_2049.py | 24 +++++++ core/models.py | 69 ++++++++++---------- core/serializers/feedback.py | 3 +- core/tests/test_feedback.py | 73 ++++++++++++++++++++++ core/urls.py | 4 +- core/views/__init__.py | 2 +- core/views/feedback.py | 15 +++++ docker-compose.yml | 2 + 8 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 core/migrations/0007_auto_20180217_2049.py diff --git a/core/migrations/0007_auto_20180217_2049.py b/core/migrations/0007_auto_20180217_2049.py new file mode 100644 index 00000000..e1a6b83a --- /dev/null +++ b/core/migrations/0007_auto_20180217_2049.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.2 on 2018-02-17 20:49 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20180217_1729'), + ] + + operations = [ + migrations.RemoveField( + model_name='feedbackcomment', + name='id', + ), + migrations.AddField( + model_name='feedbackcomment', + name='comment_id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/core/models.py b/core/models.py index 9bdd0c92..6572cc2b 100644 --- a/core/models.py +++ b/core/models.py @@ -425,6 +425,42 @@ class Feedback(models.Model): return self.of_submission.type.full_score +class FeedbackComment(models.Model): + """ This Class contains the Feedback for a specific line of a Submission""" + comment_id = models.UUIDField(primary_key=True, + default=uuid.uuid4, + editable=False) + text = models.TextField() + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + visible_to_student = models.BooleanField(default=True) + + of_line = models.PositiveIntegerField(default=0) + of_tutor = models.ForeignKey( + get_user_model(), + related_name="comment_list", + on_delete=models.PROTECT + ) + of_feedback = models.ForeignKey( + Feedback, + related_name="feedback_lines", + on_delete=models.CASCADE, + null=True + ) + + class Meta: + verbose_name = "Feedback Comment" + verbose_name_plural = "Feedback Comments" + ordering = ('created',) + unique_together = ('of_line', 'of_tutor', 'of_feedback') + + def __str__(self): + return 'Comment on line {} of tutor {}: "{}"'.format(self.of_line, + self.of_tutor, + self.text) + + class SubscriptionEnded(Exception): pass @@ -654,36 +690,3 @@ class TutorSubmissionAssignment(models.Model): class Meta: unique_together = ('submission', 'subscription') - - -class FeedbackComment(models.Model): - """ This Class contains the Feedback for a specific line of a Submission""" - text = models.TextField() - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - - visible_to_student = models.BooleanField(default=True) - - of_line = models.PositiveIntegerField(default=0) - of_tutor = models.ForeignKey( - get_user_model(), - related_name="comment_list", - on_delete=models.PROTECT - ) - of_feedback = models.ForeignKey( - Feedback, - related_name="feedback_lines", - on_delete=models.CASCADE, - null=True - ) - - class Meta: - verbose_name = "Feedback Comment" - verbose_name_plural = "Feedback Comments" - ordering = ('created',) - unique_together = ('of_line', 'of_tutor', 'of_feedback') - - def __str__(self): - return 'Comment on line {} of tutor {}: "{}"'.format(self.of_line, - self.of_tutor, - self.text) diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py index a0c9d43e..4fbbab3c 100644 --- a/core/serializers/feedback.py +++ b/core/serializers/feedback.py @@ -67,7 +67,8 @@ class FeedbackCommentSerializer(serializers.ModelSerializer): class Meta: model = models.FeedbackComment - fields = ('text', + fields = ('pk', + 'text', 'created', 'of_tutor', 'of_line', diff --git a/core/tests/test_feedback.py b/core/tests/test_feedback.py index 2f14e074..c219388e 100644 --- a/core/tests/test_feedback.py +++ b/core/tests/test_feedback.py @@ -387,3 +387,76 @@ class FeedbackPatchTestCase(APITestCase): response = self.client.patch(self.url, data, format='json') self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) + + +class FeedbackCommentApiEndpointTest(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.burl = '/api/feedback/' + cls.data = make_test_data({ + 'submission_types': [ + { + 'name': '01. Sort this or that', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }], + 'students': [ + {'username': 'student01'} + ], + 'tutors': [ + {'username': 'tutor01'}, + {'username': 'tutor02'}, + ], + 'reviewers': [ + {'username': 'reviewer01'}, + ], + 'submissions': [{ + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n', + 'type': '01. Sort this or that', + 'user': 'student01', + 'feedback': { + 'score': 5, + 'is_final': True, + 'feedback_lines': { + '1': [{'text': 'This is very bad!', + 'of_tutor': 'tutor01'}], + '2': [{'text': 'And this is even worse!', + 'of_tutor': 'tutor02'}], + } + } + }] + }) + + def setUp(self): + self.url = '/api/feedback-comment/%s/' + self.tutor01 = self.data['tutors'][0] + self.tutor02 = self.data['tutors'][1] + + def test_tutor_can_delete_own_comment(self): + self.client.force_authenticate(user=self.tutor01) + comment = FeedbackComment.objects.get(of_tutor=self.tutor01) + response = self.client.delete(self.url % comment.pk) + self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code) + + def test_tutor_cannot_delete_foreign_comment(self): + self.client.force_authenticate(user=self.tutor02) + comment = FeedbackComment.objects.get(of_tutor=self.tutor02) + self.client.force_authenticate(self.tutor01) + response = self.client.delete(self.url % comment.pk) + self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) + + def test_reviewer_can_delete_everything_they_want(self): + reviewer = self.data['reviewers'][0] + self.client.force_authenticate(user=reviewer) + comment01 = FeedbackComment.objects.get(of_tutor=self.tutor02) + comment02 = FeedbackComment.objects.get(of_tutor=self.tutor02) + + response = self.client.delete(self.url % comment01.pk) + self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code) + + response = self.client.delete(self.url % comment02.pk) + self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code) diff --git a/core/urls.py b/core/urls.py index a9950095..61a0cfc5 100644 --- a/core/urls.py +++ b/core/urls.py @@ -9,12 +9,14 @@ router.register('student', views.StudentReviewerApiViewSet, base_name='student') router.register('examtype', views.ExamApiViewSet) router.register('feedback', views.FeedbackApiView) +router.register('feedback-comment', views.FeedbackCommentApiView) +router.register('submission', views.SubmissionViewSet, + base_name='submission') router.register('submissiontype', views.SubmissionTypeApiView) router.register('tutor', views.TutorApiViewSet, base_name='tutor') router.register('subscription', views.SubscriptionApiViewSet, base_name='subscription') router.register('assignment', views.AssignmentApiViewSet) -router.register('submission', views.SubmissionViewSet, base_name='submission') # regular views that are not viewsets regular_views_urlpatterns = [ diff --git a/core/views/__init__.py b/core/views/__init__.py index beac6e7a..e653604f 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -1,4 +1,4 @@ -from .feedback import FeedbackApiView # noqa +from .feedback import FeedbackApiView, FeedbackCommentApiView # noqa from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet # noqa from .common_views import * # noqa from .export import StudentCSVExport # noqa diff --git a/core/views/feedback.py b/core/views/feedback.py index 3e9dc9fd..2a9700a4 100644 --- a/core/views/feedback.py +++ b/core/views/feedback.py @@ -108,3 +108,18 @@ class FeedbackApiView( serializer.save() return Response(serializer.data) + + +class FeedbackCommentApiView(viewsets.GenericViewSet): + """ Gets a list of an individual exam by Id if provided """ + permission_classes = (permissions.IsTutorOrReviewer,) + queryset = models.FeedbackComment.objects.all() + + def destroy(self, request, *args, **kwargs): + comment = self.get_object() + + user = request.user + if user.role == models.UserAccount.TUTOR and user != comment.of_tutor: + raise PermissionDenied(detail='Can only delete your own commits.') + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/docker-compose.yml b/docker-compose.yml index c48787ce..baf820d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: restart: always networks: - default + ports: + 6543:5432 grady: image: docker.gitlab.gwdg.de/j.michal/grady:master -- GitLab