diff --git a/backend/core/models.py b/backend/core/models.py index 1d1b23b19dbe7c77f4e9911035c85639578c74d3..c76e76e29491540a64d478f53c7447393e507a3a 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -159,6 +159,8 @@ class Tutor(models.Model): get_user_model(), unique=True, on_delete=models.CASCADE, related_name='tutor') + def get_feedback_count(self) -> int: + return self.feedback_list.count() class Reviewer(models.Model): user = models.OneToOneField( diff --git a/backend/core/permissions.py b/backend/core/permissions.py index 3418f6dd13ad69513594f22ef6612261614d72fd..d3e8b59205c349c97cbfb9bb808d3170a4853637 100644 --- a/backend/core/permissions.py +++ b/backend/core/permissions.py @@ -1,9 +1,35 @@ from rest_framework import permissions -from core.models import Student +from core.models import Student, Reviewer, Tutor -class IsStudent(permissions.BasePermission): +class IsUserGenericPermission(permissions.BasePermission): + """ Generic class that encapsulates how to identify someone + as a member of a user Group """ + def has_permission(self, request, view): + """ required by BasePermission. Check if user is instance of model""" + assert self.model is not None, ( + "'%s' has to include a `model` attribute" + % self.__class__.__name__ + ) + user = request.user - return user.is_authenticated() and isinstance(user.get_associated_user(), Student) + return user.is_authenticated() and isinstance( + user.get_associated_user(), self.model + ) + + +class IsStudent(IsUserGenericPermission): + """ Has student permissions """ + model = Student + + +class IsReviewer(IsUserGenericPermission): + """ Has reviewer permissions """ + model = Reviewer + + +class IsTutor(IsUserGenericPermission): + """ Has tutor permissions """ + model = Tutor diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 59e7587d8023e14c9cdaea1bf57edacc25e71398..c6c93836dd2a4425c3d4efe54d3849084ef209f7 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from core.models import ExamType, Feedback, Student, Submission +from core.models import ExamType, Feedback, Student, Submission, Tutor class ExamSerializer(serializers.ModelSerializer): @@ -37,3 +37,12 @@ class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student fields = ('name', 'user', 'exam', 'submissions') + + +class TutorSerializer(serializers.ModelSerializer): + username = serializers.ReadOnlyField(source='user.username') + feedback_count = serializers.IntegerField(source='get_feedback_count') + + class Meta: + model = Tutor + fields = ('username', 'feedback_count') diff --git a/backend/core/tests/data_factories.py b/backend/core/tests/data_factories.py index 1d5e9b54171614c798b7ad54fefc1e92d038468a..a7cb725a35c8315738a3e83c460775816e8b5647 100644 --- a/backend/core/tests/data_factories.py +++ b/backend/core/tests/data_factories.py @@ -5,25 +5,38 @@ from core.models import (UserAccount, Student, Tutor, Reviewer, ExamType, SubmissionType, Submission, Feedback) -# These methods are meant to be used to provide data to insert into the test database - -def make_user(username='user01', password='p', fullname='us er01', is_admin=False): - user = UserAccount.objects.create(username=username, fullname=fullname, +# These methods are meant to be used to provide data to insert into the test +# database + +def make_user(username='user01', + password='p', + fullname='us er01', + is_admin=False): + user = UserAccount.objects.create(username=username, + fullname=fullname, is_admin=is_admin) user.set_password(password) user.save() return user -def make_exam(module_reference='TestExam B.Inf.0042', total_score=42, pass_score=21, **kwargs): - return ExamType.objects.create(module_reference=module_reference, total_score=total_score, +def make_exam(module_reference='TestExam B.Inf.0042', + total_score=42, + pass_score=21, + **kwargs): + return ExamType.objects.create(module_reference=module_reference, + total_score=total_score, pass_score=pass_score, **kwargs) -def make_submission_type(name='problem01', full_score=10, description='Very hard', +def make_submission_type(name='problem01', + full_score=10, + description='Very hard', solution='Impossible!'): - return SubmissionType.objects.create(name=name, full_score=full_score, - description=description, solution=solution) + return SubmissionType.objects.create(name=name, + full_score=full_score, + description=description, + solution=solution) def make_student(user=None, exam=None): @@ -57,11 +70,16 @@ def make_submission(type=None, student=None, text='Too hard for me ;-('): def make_feedback(of_tutor, of_submission=None, text='Very bad!', score=3): if of_submission is None: of_submission = make_submission() - return Feedback.objects.create(of_tutor=of_tutor, of_submission=of_submission, text=text, score=score) + return Feedback.objects.create(of_tutor=of_tutor, + of_submission=of_submission, + text=text, + score=score) def make_minimal_exam(): - submission = make_submission() # also creates default examType, submissionType and student + # also creates default examType, submissionType and student + submission = make_submission() + tutor = make_tutor(user=make_user(username='tutor01')) feedback = make_feedback(of_tutor=tutor, of_submission=submission) return submission, tutor, feedback diff --git a/backend/core/tests/test_tutor_api_endpoints.py b/backend/core/tests/test_tutor_api_endpoints.py index 88982efb433c80e5a2fa442e83258185499e64e2..87dbe2b2c31ccbfa57b3656cfd420dcf3eee51df 100644 --- a/backend/core/tests/test_tutor_api_endpoints.py +++ b/backend/core/tests/test_tutor_api_endpoints.py @@ -7,14 +7,16 @@ from rest_framework.test import APITestCase, APIRequestFactory, force_authenticate from rest_framework import status -from core.models import Reviewer from django.urls import reverse -from core.views import StudentApiView +from core.views import TutorListApiView +from core.models import Tutor from util.factories import GradyUserFactory +NUMBER_OF_TUTORS = 7 -class AccessRightsTests(APITestCase): + +class TutorAPIEndpoints(APITestCase): @classmethod def setUpTestData(cls): @@ -22,27 +24,24 @@ class AccessRightsTests(APITestCase): cls.user_factory = GradyUserFactory() def setUp(self): - self.student = self.user_factory.make_student() - self.tutor = self.user_factory.make_tutor() + self.tutor_list = [self.user_factory.make_tutor() + for _ in range(NUMBER_OF_TUTORS)] self.reviewer = self.user_factory.make_reviewer() - self.request = self.factory.get(reverse('student-page')) - self.view = StudentApiView.as_view() + self.request = self.factory.get(reverse('tutorlist')) + self.view = TutorListApiView.as_view() + + force_authenticate(self.request, user=self.reviewer.user) + self.response = self.view(self.request) - def test_unauthorized_access_denied(self): - response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_can_access(self): + self.assertEqual(self.response.status_code, status.HTTP_200_OK) - def test_tutor_has_no_access(self): - force_authenticate(self.request, user=self.tutor.user) - response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_get_a_list_of_all_tutors(self): + self.assertEqual(len(self.response.data), NUMBER_OF_TUTORS) - def test_reviewer_has_no_access(self): - force_authenticate(self.request, user=self.reviewer.user) - response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_sum_of_feedback_count_matches_database(self): + def verify_fields(tutor_obj): + t = Tutor.objects.get(user__username=tutor_obj['username']) + return t.get_feedback_count() == tutor_obj['feedback_count'] - def test_student_is_authorized(self): - force_authenticate(self.request, user=self.student.user) - response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(all(map(verify_fields, self.response.data))) diff --git a/backend/core/urls.py b/backend/core/urls.py index cde04b5a2439741a5c2b73f6bbf3c6493b607b8d..41582a5c2ec055c1e42221d59c596d8912196aa8 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -7,6 +7,7 @@ from core import views urlpatterns = [ url(r'^api/student/$', views.StudentApiView.as_view(), name='student-page'), + url(r'^api/tutorlist/$', views.TutorListApiView.as_view(), name='tutorlist'), url(r'^api-token-auth/', obtain_jwt_token), url(r'^api-token-refresh', refresh_jwt_token), diff --git a/backend/core/views.py b/backend/core/views.py index 7274d03baa60d0c79dd5246c0ff92b8792665fd3..2b02516e300514fa2047e714a02dc9fecc994072 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1,9 +1,10 @@ import logging -from rest_framework.generics import RetrieveAPIView +from rest_framework.generics import RetrieveAPIView, ListAPIView -from core.permissions import IsStudent -from core.serializers import StudentSerializer +from core.permissions import IsStudent, IsReviewer +from core.serializers import StudentSerializer, TutorSerializer +from core.models import Tutor log = logging.getLogger(__name__) @@ -12,6 +13,14 @@ class StudentApiView(RetrieveAPIView): permission_classes = (IsStudent,) def get_object(self): - log.debug("Serializing student of user '%s'", self.request.user.username) + log.debug("Serializing student of user '%s'", + self.request.user.username) return self.request.user.student serializer_class = StudentSerializer + + +class TutorListApiView(ListAPIView): + """ A list of all tutors with informationi about what they corrected """ + permission_classes = (IsReviewer,) + queryset = Tutor.objects.all() + serializer_class = TutorSerializer diff --git a/backend/util/importer.py b/backend/util/importer.py index fcb6f4b825a174774e14c25405219f2f35c4dd69..f9fed844f9d36970a105c7191533ad38f9b94998 100644 --- a/backend/util/importer.py +++ b/backend/util/importer.py @@ -1,9 +1,7 @@ -import configparser import csv import json import os import readline -import secrets from typing import Callable import util.convert