Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 169-add-date-to-examtype
  • 233-make-exam-a-many-to-many-field-on-studentinfo-model
  • 236-improve-importer-experience
  • 243-replace-toggle-buttons-with-switches
  • 250-update-vuetify
  • 258-add-markdown-viewer
  • 265-fix-selection-changing-on-window-switching
  • 272-reviewers-should-be-able-to-assign-exercise-groups-to-tutors
  • 276-create-new-yarn-lockfile
  • 279-tutor-overview-no-scrolling
  • 282-copy-button-does-not-work-when-reviewing-corrections
  • 286-fix-misalignment-of-hide-show-sidebar-buttons
  • 287-build-test-image-constantly-failing
  • 288-add-dropdown-to-participantspage-to-set-students-groups
  • 289-fix-change-log-card
  • 291-revise-to-old-export-scheme
  • 292-update-gitlab-ci-config-for-new-runner
  • 292-update-gitlab-ci-config-for-new-runner-2
  • add-exercise-util-script
  • document-frontend-components
  • grady-exam
  • jakob.dieterle-master-patch-13835
  • master
  • parallel-test
  • test-233-branch-remove-examtype-foreign-key-on-group
  • update-export-dialogs
  • 0.0.1
  • 0.1
  • 0.2
  • 0.3
  • 0.4
  • 0.4.1
  • 0.4.2
  • 0.5.0
  • 0.5.1
  • 1.0.0
  • 1.1.0
  • 2.0.0
  • 2.0.1
  • 2.1.0
  • 2.1.1
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 4.0.0
  • 4.1.0
  • 4.2.0
  • 4.3.0
  • 4.4.0
  • 4.4.1
  • 5.0.0
  • 5.0.1
  • 5.1.0
  • 5.1.1
  • 5.1.2
  • 5.1.3
  • 5.1.4
  • 5.1.5
  • 5.1.6
  • 5.1.7
  • 5.2.0
  • 5.3.0
  • 5.3.1
  • 5.3.2
  • 5.4.0
  • 5.4.1
  • 5.4.2
  • 6.0.0
  • 6.1.0
  • legacy
70 results

Target

Select target project
  • j.michal/grady
1 result
Select Git revision
  • 169-add-date-to-examtype
  • 233-make-exam-a-many-to-many-field-on-studentinfo-model
  • 236-improve-importer-experience
  • 243-replace-toggle-buttons-with-switches
  • 250-update-vuetify
  • 258-add-markdown-viewer
  • 265-fix-selection-changing-on-window-switching
  • 272-reviewers-should-be-able-to-assign-exercise-groups-to-tutors
  • 276-create-new-yarn-lockfile
  • 279-tutor-overview-no-scrolling
  • 282-copy-button-does-not-work-when-reviewing-corrections
  • 286-fix-misalignment-of-hide-show-sidebar-buttons
  • 287-build-test-image-constantly-failing
  • 288-add-dropdown-to-participantspage-to-set-students-groups
  • 289-fix-change-log-card
  • 291-revise-to-old-export-scheme
  • 292-update-gitlab-ci-config-for-new-runner
  • 292-update-gitlab-ci-config-for-new-runner-2
  • add-exercise-util-script
  • document-frontend-components
  • grady-exam
  • jakob.dieterle-master-patch-13835
  • master
  • parallel-test
  • test-233-branch-remove-examtype-foreign-key-on-group
  • update-export-dialogs
  • 0.0.1
  • 0.1
  • 0.2
  • 0.3
  • 0.4
  • 0.4.1
  • 0.4.2
  • 0.5.0
  • 0.5.1
  • 1.0.0
  • 1.1.0
  • 2.0.0
  • 2.0.1
  • 2.1.0
  • 2.1.1
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 4.0.0
  • 4.1.0
  • 4.2.0
  • 4.3.0
  • 4.4.0
  • 4.4.1
  • 5.0.0
  • 5.0.1
  • 5.1.0
  • 5.1.1
  • 5.1.2
  • 5.1.3
  • 5.1.4
  • 5.1.5
  • 5.1.6
  • 5.1.7
  • 5.2.0
  • 5.3.0
  • 5.3.1
  • 5.3.2
  • 5.4.0
  • 5.4.1
  • 5.4.2
  • 6.0.0
  • 6.1.0
  • legacy
70 results
Show changes
Showing
with 282 additions and 94 deletions
......@@ -70,10 +70,14 @@ class FeedbackCommentSerializer(DynamicFieldsModelSerializer):
fields = ('pk',
'text',
'created',
'modified',
'of_tutor',
'of_line',
'labels',
'visible_to_student')
read_only_fields = ('created', 'of_tutor')
# visible_to_student is kept in sync with modified, such that the latest modified
# comment is the one that is visible
read_only_fields = ('created', 'of_tutor', 'visible_to_student')
extra_kwargs = {
'of_feedback': {'write_only': True},
'of_line': {'write_only': True},
......@@ -86,6 +90,8 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
of_submission_type = serializers.ReadOnlyField(
source='of_submission.type.pk')
feedback_stage_for_user = serializers.SerializerMethodField()
labels = serializers.PrimaryKeyRelatedField(many=True, required=False,
queryset=models.FeedbackLabel.objects.all())
def get_feedback_stage_for_user(self, obj):
""" Search for the assignment of this feedback and report in which
......@@ -114,28 +120,35 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
def create(self, validated_data) -> Feedback:
submission = validated_data.pop('of_submission')
feedback_lines = validated_data.pop('feedback_lines', [])
labels = validated_data.pop('labels', [])
feedback = Feedback.objects.create(of_submission=submission,
**validated_data)
for label in labels:
feedback.labels.add(label)
submission.meta.feedback_authors.add(self.context['request'].user)
for comment in feedback_lines:
models.FeedbackComment.objects.create(
labels = comment.pop('labels', [])
comment_instance = models.FeedbackComment.objects.create(
of_feedback=feedback,
of_tutor=self.context['request'].user,
**comment
)
comment_instance.labels.set(labels)
return Feedback.objects.get(of_submission=submission)
@transaction.atomic
def update(self, feedback, validated_data):
for comment in validated_data.pop('feedback_lines', []):
models.FeedbackComment.objects.update_or_create(
labels = comment.pop('labels', [])
comment_instance, _ = models.FeedbackComment.objects.update_or_create(
of_feedback=feedback,
of_tutor=self.context['request'].user,
of_line=comment.get('of_line'),
defaults={'text': comment.get('text')})
comment_instance.labels.set(labels)
return super().update(feedback, validated_data)
......@@ -196,7 +209,7 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Feedback
fields = ('pk', 'of_submission', 'is_final', 'score', 'feedback_lines',
'created', 'of_submission_type', 'feedback_stage_for_user')
'created', 'of_submission_type', 'feedback_stage_for_user', 'labels')
class VisibleCommentFeedbackSerializer(FeedbackSerializer):
......@@ -209,7 +222,7 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer):
serializer = FeedbackCommentSerializer(
comments,
many=True,
fields=('pk', 'text', 'created', 'of_line',)
fields=('pk', 'text', 'created', 'modified', 'of_line',)
)
# this is a weird hack because, for some reason, serializer.data
# just won't contain the correct data. Instead .data returns a list
......
from rest_framework import serializers
from core.models import FeedbackLabel
class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = FeedbackLabel
fields = (
'pk',
'name',
'description',
'colour'
)
......@@ -13,7 +13,7 @@ log = logging.getLogger(__name__)
user_factory = GradyUserFactory()
class TutorSerializer(DynamicFieldsModelSerializer):
class CorrectorSerializer(DynamicFieldsModelSerializer):
feedback_created = serializers.SerializerMethodField()
feedback_validated = serializers.SerializerMethodField()
password = serializers.CharField(
......
......@@ -80,3 +80,4 @@ def set_comment_visibility_after_conflict(sender, instance, **kwargs):
of_feedback=instance.of_feedback,
)
comments_on_the_same_line.update(visible_to_student=False)
instance.visible_to_student = True
......@@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase,
force_authenticate)
from core.views import (ExamApiViewSet, StudentReviewerApiViewSet,
StudentSelfApiView, TutorApiViewSet)
StudentSelfApiView, CorrectorApiViewSet)
from util.factories import GradyUserFactory, make_exams
......@@ -66,8 +66,8 @@ class AccessRightsOfTutorAPIViewTests(APITestCase):
self.student = self.user_factory.make_student(exam=self.exam)
self.tutor = self.user_factory.make_tutor()
self.reviewer = self.user_factory.make_reviewer()
self.request = self.factory.get(reverse('tutor-list'))
self.view = TutorApiViewSet.as_view({'get': 'list'})
self.request = self.factory.get(reverse('corrector-list'))
self.view = CorrectorApiViewSet.as_view({'get': 'list'})
def test_unauthenticated_access_denied(self):
response = self.view(self.request)
......
......@@ -118,7 +118,9 @@ class ExportInstanceTest(APITestCase):
self.assertIn('pk', instance['students'][0])
self.assertIn('userPk', instance['students'][0])
self.assertIn('exam', instance['students'][0])
self.assertEqual('student01', instance['students'][1]['user'])
student_users = [s['user'] for s in instance['students']]
self.assertIn('student01', student_users)
self.assertIn('student02', student_users)
self.assertLess(0, len(instance['students'][1]['submissions']))
# students[submissions] nested
......@@ -155,7 +157,9 @@ class ExportInstanceTest(APITestCase):
# tutors fields
self.assertIn('tutors', instance)
self.assertLess(0, len(instance['tutors']))
self.assertEqual('tutor01', instance['tutors'][0]['username'])
tutor_names = [t['username'] for t in instance['tutors']]
self.assertIn('tutor01', tutor_names)
self.assertIn('reviewer', tutor_names)
class ExportJSONTest(APITestCase):
......
......@@ -4,7 +4,7 @@ from rest_framework import status
from rest_framework.test import APIRequestFactory, APITestCase
from core import models
from core.models import Feedback, FeedbackComment, Submission, SubmissionType
from core.models import Feedback, FeedbackComment, Submission, SubmissionType, FeedbackLabel
from util.factories import GradyUserFactory, make_test_data, make_exams
......@@ -113,9 +113,13 @@ class FeedbackCreateTestCase(APITestCase):
cls.sub = Submission.objects.create(student=cls.student.student,
type=cls.submission_type,
text=text)
cls.fst_label = FeedbackLabel.objects.create(name='Label1', description='Bla')
cls.snd_label = FeedbackLabel.objects.create(name='Label2', description='Bla')
def setUp(self):
self.sub.refresh_from_db()
self.fst_label.refresh_from_db()
self.snd_label.refresh_from_db()
self.client.force_authenticate(user=self.tutor)
self.subscription = models.SubmissionSubscription.objects.create(
owner=self.tutor,
......@@ -191,6 +195,28 @@ class FeedbackCreateTestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Feedback.objects.count(), 0)
def test_can_create_with_labels(self):
data = {
'score': 0,
'is_final': False,
'of_submission': self.assignment.submission.pk,
'labels': [self.fst_label.pk, self.snd_label.pk],
'feedback_lines': {
'2': {
'text': 'Why you no learn how to code, man?',
'labels': []
}
}
}
self.assertEqual(self.fst_label.feedback.count(), 0)
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.fst_label.refresh_from_db()
self.snd_label.refresh_from_db()
self.assertEqual(self.fst_label.feedback.count(), 1)
self.assertEqual(self.snd_label.feedback.count(), 1)
self.assertEqual(Feedback.objects.first().labels.count(), 2)
def test_can_create_feedback_with_half_points(self):
data = {
'score': 0.5,
......@@ -198,7 +224,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'2': {
'text': 'Why you no learn how to code, man?'
'text': 'Why you no learn how to code, man?',
'labels': []
}
}
}
......@@ -213,7 +240,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'4': {
'text': 'Why you no learn how to code, man?'
'text': 'Why you no learn how to code, man?',
'labels': []
}
}
}
......@@ -228,7 +256,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'3': {
'text': 'Nice meth!'
'text': 'Nice meth!',
'labels': []
}
}
}
......@@ -244,7 +273,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'3': {
'text': 'Nice meth!'
'text': 'Nice meth!',
'labels': []
}
}
}
......@@ -262,7 +292,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'2': {
'text': 'Well, at least you tried.'
'text': 'Well, at least you tried.',
'labels': []
},
}
}
......@@ -276,7 +307,8 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'1': {
'text': 'Well, at least you tried.'
'text': 'Well, at least you tried.',
'labels': []
},
}
}
......@@ -292,10 +324,12 @@ class FeedbackCreateTestCase(APITestCase):
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'1': {
'text': 'Nice meth!'
'text': 'Nice meth!',
'labels': []
},
'3': {
'text': 'Good one!'
'text': 'Good one!',
'labels': []
}
}
}
......@@ -353,6 +387,9 @@ class FeedbackPatchTestCase(APITestCase):
}]
})
cls.fst_label = FeedbackLabel.objects.create(name='Label1', description='Bla')
cls.snd_label = FeedbackLabel.objects.create(name='Label2', description='Bla')
def setUp(self):
self.tutor01 = self.data['tutors'][0]
self.tutor02 = self.data['tutors'][1]
......@@ -367,7 +404,10 @@ class FeedbackPatchTestCase(APITestCase):
'is_final': False,
'of_submission': self.assignment.submission.pk,
'feedback_lines': {
'2': {'text': 'Very good.'},
'2': {
'text': 'Very good.',
'labels': []
},
}
}
response = self.client.post(self.burl, data, format='json')
......@@ -375,10 +415,16 @@ class FeedbackPatchTestCase(APITestCase):
of_submission=response.data['of_submission'])
self.url = f'{self.burl}{self.feedback.of_submission.submission_id}/'
self.fst_label.refresh_from_db()
self.snd_label.refresh_from_db()
def test_can_patch_onto_the_own_feedback(self):
data = {
'feedback_lines': {
'1': {'text': 'Spam spam spam'},
'1': {
'text': 'Spam spam spam',
'labels': []
},
}
}
response = self.client.patch(self.url, data, format='json')
......@@ -395,7 +441,10 @@ class FeedbackPatchTestCase(APITestCase):
def test_can_update_a_single_line(self):
data = {
'feedback_lines': {
'2': {'text': 'Turns out this is rather bad.'},
'2': {
'text': 'Turns out this is rather bad.',
'labels': []
},
}
}
......@@ -415,7 +464,7 @@ class FeedbackPatchTestCase(APITestCase):
# Step 2 - Tutor 1 tries to patch
data = {
'feedback_lines': {
'2': {'text': 'Turns out this is rather bad.'},
'2': {'text': 'Turns out this is rather bad.', 'labels': []},
}
}
......@@ -425,7 +474,7 @@ class FeedbackPatchTestCase(APITestCase):
def test_cannot_patch_first_feedback_final(self):
data = {
'feedback_lines': {
'2': {'text': 'Turns out this is rather bad.'},
'2': {'text': 'Turns out this is rather bad.', 'labels': []},
},
'is_final': True
}
......@@ -433,6 +482,21 @@ class FeedbackPatchTestCase(APITestCase):
response = self.client.patch(self.url, data, format='json')
self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
def tutor_can_patch_labels(self):
data = {
'feedback_lines': {
'2': {
'text': 'Turns out this is rather bad.',
'labels': [self.fst_label.pk, self.snd_label.pk]
},
}
}
self.assertEqual(FeedbackComment.objects.first().labels.count(), 0)
response = self.client.patch(self.url, data, format='json')
self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertEqual(FeedbackComment.objects.first().labels.count(), 2)
class FeedbackCommentApiEndpointTest(APITestCase):
......@@ -521,19 +585,3 @@ class FeedbackCommentApiEndpointTest(APITestCase):
pass
else:
self.fail('No exception raised')
def test_reviewer_can_set_comment_visibility(self):
reviewer = self.data['reviewers'][0]
self.client.force_authenticate(user=reviewer)
comment = FeedbackComment.objects.get(of_tutor=self.tutor01)
self.assertTrue(comment.visible_to_student)
data = {
'visible_to_student': False
}
response = self.client.patch(self.url % comment.pk, data)
self.assertFalse(response.data['visible_to_student'])
comment.refresh_from_db()
self.assertFalse(comment.visible_to_student)
from rest_framework import status
from rest_framework.test import APITestCase
from core.models import FeedbackLabel
from util.factories import GradyUserFactory, make_exams
class LabelsTestCases(APITestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.factory = GradyUserFactory()
cls.exam = make_exams(exams=[{
'module_reference': 'Test Exam 01',
'total_score': 100,
'pass_score': 60,
}])[0]
cls.student = cls.factory.make_student(exam=cls.exam)
cls.tutor = cls.factory.make_tutor()
cls.reviewer = cls.factory.make_reviewer()
cls.label_post_data = {
'name': 'A label',
'description': 'with a description...'
}
cls.label_url = '/api/label/'
def test_student_can_not_read_labels(self):
self.client.force_authenticate(user=self.student)
response = self.client.get(self.label_url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(FeedbackLabel.objects.count(), 0)
def test_student_can_not_write_labels(self):
self.client.force_authenticate(user=self.student)
response = self.client.post(self.label_url, data=self.label_post_data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(FeedbackLabel.objects.count(), 0)
def test_tutor_can_create_label(self):
self.client.force_authenticate(user=self.tutor)
response = self.client.post(self.label_url, data=self.label_post_data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(FeedbackLabel.objects.count(), 1)
def test_reviewer_can_create_label(self):
self.client.force_authenticate(user=self.reviewer)
response = self.client.post(self.label_url, data=self.label_post_data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(FeedbackLabel.objects.count(), 1)
......@@ -378,9 +378,10 @@ class TestApiEndpoints(APITestCase):
"score": 23,
"of_submission": response.data['submission']['pk'],
"feedback_lines": {
1: {"text": "< some string >"},
2: {"text": "< some string >"}
}
1: {"text": "< some string >", "labels": []},
2: {"text": "< some string >", "labels": []}
},
"labels": [],
}
)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
......
......@@ -14,7 +14,7 @@ import os
from core.models import (Feedback, SubmissionSubscription,
TutorSubmissionAssignment)
from core.views import TutorApiViewSet
from core.views import CorrectorApiViewSet
from util.factories import GradyUserFactory, make_test_data
NUMBER_OF_TUTORS = 3
......@@ -30,9 +30,9 @@ class TutorDeleteTest(APITestCase):
def setUp(self):
self.tutor = self.user_factory.make_tutor(username='UFO')
self.reviewer = self.user_factory.make_reviewer()
self.request = self.factory.delete(reverse('tutor-detail',
self.request = self.factory.delete(reverse('corrector-detail',
args=[str(self.tutor.pk)]))
self.view = TutorApiViewSet.as_view({'delete': 'destroy'})
self.view = CorrectorApiViewSet.as_view({'delete': 'destroy'})
force_authenticate(self.request, user=self.reviewer)
self.response = self.view(self.request, pk=str(self.tutor.pk))
......@@ -52,8 +52,8 @@ class TutorListTests(APITestCase):
def setUpTestData(cls):
factory = APIRequestFactory()
request = factory.get(reverse('tutor-list'))
view = TutorApiViewSet.as_view({'get': 'list'})
request = factory.get(reverse('corrector-list'))
view = CorrectorApiViewSet.as_view({'get': 'list'})
data = make_test_data(data_dict={
'exams': [{
......@@ -121,8 +121,8 @@ class TutorListTests(APITestCase):
def test_can_access(self):
self.assertEqual(self.response.status_code, status.HTTP_200_OK)
def test_get_a_list_of_all_tutors(self):
self.assertEqual(2, len(self.response.data))
def test_get_a_list_of_all_correctos(self):
self.assertEqual(3, len(self.response.data))
def test_feedback_created_count_matches_database(self):
def verify_fields(tutor_obj):
......@@ -167,9 +167,9 @@ class TutorCreateTests(APITestCase):
def setUp(self):
self.reviewer = self.user_factory.make_reviewer()
self.request = self.factory.post(reverse('tutor-list'),
self.request = self.factory.post(reverse('corrector-list'),
{'username': self.USERNAME})
self.view = TutorApiViewSet.as_view({'post': 'create'})
self.view = CorrectorApiViewSet.as_view({'post': 'create'})
force_authenticate(self.request, user=self.reviewer)
self.response = self.view(self.request, username=self.USERNAME)
......@@ -194,7 +194,7 @@ class TutorDetailViewTests(APITestCase):
self.client = APIClient()
self.client.force_authenticate(user=self.reviewer)
url = reverse('tutor-detail', kwargs={'pk': str(self.tutor.pk)})
url = reverse('corrector-detail', kwargs={'pk': str(self.tutor.pk)})
self.response = self.client.get(url, format='json')
def test_can_access(self):
......@@ -218,7 +218,7 @@ class TutorRegisterTests(APITestCase):
@pytest.mark.skipif(os.environ.get('DJANGO_DEV', False),
reason="No password strengths checks in dev")
def test_password_is_strong_enough(self):
response = self.client.post('/api/tutor/register/', {
response = self.client.post('/api/corrector/register/', {
'username': 'hans',
'password': 'weak'
})
......@@ -227,7 +227,7 @@ class TutorRegisterTests(APITestCase):
self.assertIn('password', response.data)
def test_anonymous_can_request_access(self):
response = self.client.post('/api/tutor/register/', {
response = self.client.post('/api/corrector/register/', {
'username': 'hans',
'password': 'safeandsound'
})
......@@ -235,7 +235,7 @@ class TutorRegisterTests(APITestCase):
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
def test_cannot_register_active(self):
response = self.client.post('/api/tutor/register/', {
response = self.client.post('/api/corrector/register/', {
'username': 'hans',
'password': 'safeandsound',
'is_active': True
......@@ -244,7 +244,7 @@ class TutorRegisterTests(APITestCase):
self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
def test_reviewer_can_activate_tutor(self):
response = self.client.post('/api/tutor/register/', {
response = self.client.post('/api/corrector/register/', {
'username': 'hans',
'password': 'safeandsound'
})
......@@ -252,7 +252,7 @@ class TutorRegisterTests(APITestCase):
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.client.force_authenticate(self.reviewer)
response = self.client.put('/api/tutor/%s/' % response.data['pk'], {
response = self.client.put('/api/corrector/%s/' % response.data['pk'], {
'username': 'hans',
'is_active': True
})
......@@ -260,10 +260,10 @@ class TutorRegisterTests(APITestCase):
self.assertEqual(status.HTTP_200_OK, response.status_code)
def test_trottle_is_not_active_while_testing(self):
r = self.client.post('/api/tutor/register/', {'username': 'hans'})
r = self.client.post('/api/tutor/register/', {'username': 'the'})
r = self.client.post('/api/tutor/register/', {'username': 'brave'})
r = self.client.post('/api/tutor/register/', {'username': 'fears'})
r = self.client.post('/api/tutor/register/', {'username': 'spiders'})
r = self.client.post('/api/corrector/register/', {'username': 'hans'})
r = self.client.post('/api/corrector/register/', {'username': 'the'})
r = self.client.post('/api/corrector/register/', {'username': 'brave'})
r = self.client.post('/api/corrector/register/', {'username': 'fears'})
r = self.client.post('/api/corrector/register/', {'username': 'spiders'})
self.assertNotEqual(status.HTTP_429_TOO_MANY_REQUESTS, r.status_code)
......@@ -16,12 +16,14 @@ router.register('feedback-comment', views.FeedbackCommentApiView)
router.register('submission', views.SubmissionViewSet,
basename='submission')
router.register('submissiontype', views.SubmissionTypeApiView)
router.register('tutor', views.TutorApiViewSet, basename='tutor')
router.register('corrector', views.CorrectorApiViewSet, basename='corrector')
router.register('subscription', views.SubscriptionApiViewSet,
basename='subscription')
router.register('assignment', views.AssignmentApiViewSet)
router.register('statistics', views.StatisticsEndpoint, basename='statistics')
router.register('user', views.UserAccountViewSet, basename='user')
router.register('label', views.LabelApiViewSet, basename='label')
router.register('label-statistics', views.LabelStatistics, basename='label-statistics')
schema_view = get_schema_view(
openapi.Info(
......
from .feedback import FeedbackApiView, FeedbackCommentApiView # noqa
from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet # noqa
from .common_views import * # noqa
from .export import StudentJSONExport, InstanceExport # noqa
from .export import StudentJSONExport, InstanceExport # noqa
from .label import LabelApiViewSet, LabelStatistics # noqa
......@@ -23,7 +23,7 @@ from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer
from core.serializers import (ExamSerializer, StudentInfoSerializer,
StudentInfoForListViewSerializer,
SubmissionNoTypeSerializer, StudentSubmissionSerializer,
SubmissionTypeSerializer, TutorSerializer,
SubmissionTypeSerializer, CorrectorSerializer,
UserAccountSerializer)
log = logging.getLogger(__name__)
......@@ -96,7 +96,7 @@ class ExamApiViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ExamSerializer
class TutorApiViewSet(
class CorrectorApiViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.CreateModelMixin,
......@@ -105,11 +105,11 @@ class TutorApiViewSet(
viewsets.GenericViewSet):
""" Api endpoint for creating, listing, viewing or deleting tutors """
permission_classes = (IsReviewer,)
queryset = models.UserAccount.tutors \
queryset = models.UserAccount.corrector \
.with_feedback_count() \
.prefetch_related('subscriptions') \
.prefetch_related('subscriptions__assignments')
serializer_class = TutorSerializer
serializer_class = CorrectorSerializer
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
@throttle_classes([AnonRateThrottle])
......@@ -127,9 +127,11 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
""" Gets a list or a detail view of a single SubmissionType """
queryset = SubmissionType.objects.all()
serializer_class = SubmissionTypeSerializer
permission_classes = (IsTutorOrReviewer, )
class StatisticsEndpoint(viewsets.ViewSet):
permission_classes = (IsTutorOrReviewer, )
def list(self, request, *args, **kwargs):
first_sub_type = models.SubmissionType.objects.first()
......@@ -195,7 +197,7 @@ class UserAccountViewSet(viewsets.ReadOnlyModelViewSet):
and \
(old_password is None or
not check_password(old_password, user.password)):
return Response(status=status.HTTP_401_UNAUTHORIZED)
return Response(status=status.HTTP_401_UNAUTHORIZED)
new_password = request.data.get('new_password')
# validate password
......
......@@ -10,7 +10,7 @@ from core.permissions import IsReviewer
from core.serializers.common_serializers import SubmissionTypeSerializer, \
ExamSerializer, UserAccountSerializer
from core.serializers.student import StudentExportSerializer
from core.serializers.tutor import TutorSerializer
from core.serializers.tutor import CorrectorSerializer
words = xp.generate_wordlist(wordfile=xp.locate_wordfile(), min_length=5, max_length=8)
......@@ -59,7 +59,9 @@ class InstanceExport(APIView):
exam_types_serializer = ExamSerializer(ExamType.objects.all(), many=True)
submission_types_serializer = SubmissionTypeSerializer(
SubmissionType.objects.all(), many=True)
tutors_serializer = TutorSerializer(UserAccount.tutors.with_feedback_count(), many=True)
tutors_serializer = CorrectorSerializer(
UserAccount.corrector.with_feedback_count(),
many=True)
reviewer_serializer = UserAccountSerializer(UserAccount.get_reviewers(), many=True)
student_serializer = StudentExportSerializer(StudentInfo.objects.all(), many=True)
......
......@@ -134,17 +134,6 @@ class FeedbackCommentApiView(
return self.queryset
return self.queryset.filter(of_tutor=user)
def partial_update(self, request, **kwargs):
keys = self.request.data.keys()
if keys - {'visible_to_student', 'of_line', 'text'}:
raise PermissionDenied('These fields cannot be changed.')
comment = self.get_object()
serializer = self.get_serializer(comment, request.data, partial=True)
serializer.is_valid()
serializer.save()
return Response(serializer.data)
def destroy(self, request, *args, **kwargs):
with Lock():
instance = self.get_object()
......
import logging
from django.db.models import Case, When, IntegerField, Sum, Q
from rest_framework import mixins, viewsets
from rest_framework.response import Response
from core import models, permissions, serializers
from core.models import SubmissionType, FeedbackLabel
log = logging.getLogger(__name__)
class LabelApiViewSet(viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin):
permission_classes = (permissions.IsTutorOrReviewer, )
queryset = models.FeedbackLabel.objects.all()
serializer_class = serializers.LabelSerializer
class LabelStatistics(viewsets.ViewSet):
permission_classes = (permissions.IsTutorOrReviewer, )
def list(self, *args, **kwargs):
# TODO This is horribly ugly and should be killed with fire
# however, i'm unsure whether there is a better way to retrieve the
# information that hits the database less often
labels = FeedbackLabel.objects.all()
counts = list(SubmissionType.objects.annotate(
**{str(label.pk): Sum(
Case(
# if the feedback has a label or there is a visible comment with that
# label add 1 to the count
When(
Q(submissions__feedback__labels=label) |
Q(submissions__feedback__feedback_lines__labels=label) &
Q(submissions__feedback__feedback_lines__visible_to_student=True),
then=1),
output_field=IntegerField(),
default=0
)
) for label in labels}
).values('pk', *[str(label.pk) for label in labels]))
return Response(list(counts))
declare module 'v-clipboard'
declare module 'vue-color'
......@@ -19,6 +19,7 @@
"v-clipboard": "^2.0.1",
"vue": "^2.5.16",
"vue-class-component": "^6.0.0",
"vue-color": "^2.7.0",
"vue-notification": "^1.3.12",
"vue-property-decorator": "^7.3.0",
"vue-router": "^3.0.1",
......
......@@ -11,7 +11,8 @@ import {
Submission,
SubmissionNoType, SubmissionType,
Subscription,
Tutor, UserAccount
Tutor, UserAccount, LabelStatisticsForSubType,
FeedbackLabel
} from '@/models'
function getInstanceBaseUrl (): string {
......@@ -33,7 +34,7 @@ let ax: AxiosInstance = axios.create({
}
export async function registerTutor (credentials: Credentials): Promise<AxiosResponse<Tutor>> {
return ax.post<Tutor>('/api/tutor/register/', credentials)
return ax.post<Tutor>('/api/corrector/register/', credentials)
}
export async function fetchJWT (credentials: Credentials): Promise<JSONWebToken> {
......@@ -76,7 +77,7 @@ export async function fetchStudent ({ pk }:
}
export async function fetchAllTutors (): Promise<Array<Tutor>> {
const url = '/api/tutor/'
const url = '/api/corrector/'
return (await ax.get(url)).data
}
......@@ -113,6 +114,11 @@ export async function fetchStatistics (): Promise<Statistics> {
return (await ax.get(url)).data
}
export async function fetchLabelStatistics (): Promise<LabelStatisticsForSubType []> {
const url = '/api/label-statistics'
return (await ax.get(url)).data
}
interface SubscriptionCreatePayload {
queryType: Subscription.QueryTypeEnum
queryKey?: string
......@@ -183,11 +189,6 @@ export async function deleteComment (comment: FeedbackComment): Promise<AxiosRes
return ax.delete(url)
}
export async function patchComment (comment: FeedbackComment): Promise<FeedbackComment> {
const url = `/api/feedback-comment/${comment.pk}/`
return (await ax.patch(url, comment)).data
}
export async function activateAllStudentAccess (): Promise<AxiosResponse<void>> {
return ax.post('/api/student/activate/')
}
......@@ -208,6 +209,18 @@ export async function changeActiveForUser (userPk: string, active: boolean): Pro
return (await ax.patch(`/api/user/${userPk}/change_active/`, { 'is_active': active })).data
}
export async function getLabels () {
return (await ax.get('/api/label')).data
}
export async function createLabel (payload: Partial<FeedbackLabel>) {
return (await ax.post('/api/label/', payload)).data
}
export async function updateLabel (payload: FeedbackLabel) {
return (await ax.put('/api/label/' + payload.pk + '/', payload))
}
export interface StudentExportOptions { setPasswords?: boolean }
export interface StudentExportItem {
Matrikel: string,
......
......@@ -140,7 +140,6 @@ export default {
<style scoped>
.sidebar-footer {
position: absolute;
width: 100%;
bottom: 0px;
}
......