From bc6fb09fc5b01952ffb738d59b8f9ca225955245 Mon Sep 17 00:00:00 2001
From: janmax <mail-github@jmx.io>
Date: Thu, 23 Nov 2017 21:58:38 +0100
Subject: [PATCH] Now able to retrieve a list of tutors from /api/tutorlist

---
 backend/core/models.py                        |  2 +
 backend/core/permissions.py                   | 32 ++++++++++++--
 backend/core/serializers.py                   | 11 ++++-
 backend/core/tests/data_factories.py          | 40 ++++++++++++-----
 .../core/tests/test_tutor_api_endpoints.py    | 43 +++++++++----------
 backend/core/urls.py                          |  1 +
 backend/core/views.py                         | 17 ++++++--
 backend/util/importer.py                      |  2 -
 8 files changed, 105 insertions(+), 43 deletions(-)

diff --git a/backend/core/models.py b/backend/core/models.py
index 1d1b23b1..c76e76e2 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 3418f6dd..d3e8b592 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 59e7587d..c6c93836 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 1d5e9b54..a7cb725a 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 88982efb..87dbe2b2 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 cde04b5a..41582a5c 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 7274d03b..2b02516e 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 fcb6f4b8..f9fed844 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
-- 
GitLab