diff --git a/backend/core/models.py b/backend/core/models.py index 76b024601d0fb85b549519d734f700d63d1ad2b6..30a240f2b5521a7e049c4663098532ae6abd3f2b 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -6,11 +6,9 @@ See docstring of the individual models for information on the setup of the database. ''' -from typing import Union, Dict - from collections import OrderedDict -from random import randrange, sample -from string import ascii_lowercase +from random import randrange +from typing import Dict, Union from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser @@ -84,8 +82,8 @@ class SubmissionType(models.Model): Attributes ---------- description : TextField - The task description the student had to fulfill. The content may be HTML - formatted. + The task description the student had to fulfill. The content may be + HTML formatted. full_score : PositiveIntegerField Maximum score one can get on that one name : CharField @@ -173,8 +171,8 @@ class Reviewer(models.Model): class Student(models.Model): """ - The student model includes all information of a student, that we got - from the E-Learning output, along with some useful classmethods that provide + The student model includes all information of a student, that we got from + the E-Learning output, along with some useful classmethods that provide specially annotated QuerySets. Information like email (if given), and the username are stored in the @@ -218,9 +216,9 @@ class Student(models.Model): @classmethod def get_overall_score_annotated_submission_list(cls) -> QuerySet: - """Can be used to quickly annotate a user with the necessary information - on the overall score of a student and if he does not need any more - correction. + """Can be used to quickly annotate a user with the necessary + information on the overall score of a student and if he does not need + any more correction. A student is done if * module type was pass_only and student has enough points @@ -353,7 +351,7 @@ class Submission(models.Model): Returns ------- bool - Returns True only if feedback was actually assigned otherwise False. + Returns True only if feedback was actually assigned otherwise False """ @@ -421,7 +419,7 @@ class Feedback(models.Model): presented to different types of users. Students may see feedback only if it has been accepted, while reviewers have access at any time. text : TextField - Detailed description by the tutor about what went wrong or what did not. + Detailed description by the tutor about what went wrong. Every line in the feedback should correspond with a line in the students submission, maybe with additional comments appended. @@ -511,8 +509,8 @@ class Feedback(models.Model): ---------- user : User object The user for which feedback should not be returned. Often the user - that is currently searching for a task someone else does not want to - do. + that is currently searching for a task someone else does not want + to do. Returns ------- diff --git a/backend/core/permissions.py b/backend/core/permissions.py index 29b60731037a3d58727b44826b189de4b8f6217e..e9b879409c9f89bd1a918676f001e243769be3bb 100644 --- a/backend/core/permissions.py +++ b/backend/core/permissions.py @@ -1,8 +1,12 @@ -from rest_framework import permissions +import logging -from core.models import Reviewer, Student, Tutor from django.http import HttpRequest from django.views import View +from rest_framework import permissions + +from core.models import Reviewer, Student, Tutor + +log = logging.getLogger(__name__) class IsUserGenericPermission(permissions.BasePermission): @@ -10,28 +14,37 @@ class IsUserGenericPermission(permissions.BasePermission): as a member of a user Group """ def has_permission(self, request: HttpRequest, view: View) -> bool: - """ required by BasePermission. Check if user is instance of model""" - assert self.model is not None, ( - "'%s' has to include a `model` attribute" + """ required by BasePermission. Check if user is instance of any + of the models provided in class' models attribute """ + log.warn("Checking permission of request %s on view %s for user %s", + request, view, request.user) + + assert self.models is not None, ( + "'%s' has to include a `models` attribute" % self.__class__.__name__ ) user = request.user - return user.is_authenticated() and isinstance( - user.get_associated_user(), self.model - ) + is_authorized = user.is_authenticated() and any(isinstance( + user.get_associated_user(), models) for models in self.models) + + if not is_authorized: + log.warn('User %s has no permission to view %s', + user.username, view.__class__.__name__) + + return is_authorized class IsStudent(IsUserGenericPermission): """ Has student permissions """ - model = Student + models = (Student,) class IsReviewer(IsUserGenericPermission): """ Has reviewer permissions """ - model = Reviewer + models = (Reviewer,) class IsTutor(IsUserGenericPermission): """ Has tutor permissions """ - model = Tutor + models = (Tutor,) diff --git a/backend/core/serializers.py b/backend/core/serializers.py index afccb1bb6212bc3edd35c18680e022f464fb028e..56ec29e3d8f31420a9837991805950a44d6e31a1 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -2,7 +2,8 @@ import logging from rest_framework import serializers -from core.models import ExamType, Feedback, Student, Submission, Tutor +from core.models import (ExamType, Feedback, Student, Submission, + SubmissionType, Tutor) from util.factories import GradyUserFactory log = logging.getLogger(__name__) @@ -24,6 +25,13 @@ class FeedbackSerializer(serializers.ModelSerializer): fields = ('text', 'score') +class SubmissionTypeSerializer(serializers.ModelSerializer): + + class Meta: + model = SubmissionType + fields = ('name', 'full_score', 'description', 'solution') + + class SubmissionSerializer(serializers.ModelSerializer): feedback = serializers.ReadOnlyField(source='feedback.text') score = serializers.ReadOnlyField(source='feedback.score') @@ -46,6 +54,27 @@ class StudentSerializer(serializers.ModelSerializer): fields = ('name', 'user', 'exam', 'submissions') +class SubmissionNoTextFieldsSerializer(serializers.ModelSerializer): + score = serializers.ReadOnlyField(source='feedback.score') + type = serializers.ReadOnlyField(source='type.name') + full_score = serializers.ReadOnlyField(source='type.full_score') + + class Meta: + model = Submission + fields = ('type', 'score', 'full_score') + + +class StudentSerializerForListView(serializers.ModelSerializer): + name = serializers.ReadOnlyField(source='user.fullname') + user = serializers.ReadOnlyField(source='user.username') + exam = serializers.ReadOnlyField(source='exam.module_reference') + submissions = SubmissionNoTextFieldsSerializer(many=True) + + class Meta: + model = Student + fields = ('name', 'user', 'exam', 'submissions') + + class TutorSerializer(serializers.ModelSerializer): username = serializers.CharField(source='user.username') feedback_count = serializers.IntegerField(source='get_feedback_count', diff --git a/backend/core/tests/data_factories.py b/backend/core/tests/data_factories.py index 25e5ca71f2c33bc70e2204987b3b4c9ceb506204..f2f8b4f5a709718ec49cc04479d5d43ec172307e 100644 --- a/backend/core/tests/data_factories.py +++ b/backend/core/tests/data_factories.py @@ -7,6 +7,7 @@ from core.models import (ExamType, Feedback, Reviewer, Student, Submission, # 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', diff --git a/backend/core/tests/test_access_rights.py b/backend/core/tests/test_access_rights.py index ff9857d688f1ebde249371bbabf497c8a0743ad2..6e172626ff78f4374547336ebd2399e9c0461b09 100644 --- a/backend/core/tests/test_access_rights.py +++ b/backend/core/tests/test_access_rights.py @@ -3,13 +3,13 @@ from rest_framework import status from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) -from core.models import Reviewer -from core.views import StudentApiView +from core.views import (ExamApiViewSet, StudentReviewerApiViewSet, + StudentSelfApiViewSet, TutorApiViewSet) from util.factories import GradyUserFactory class AccessRightsOfStudentAPIViewTests(APITestCase): - """ All tests that enshure that only students can see what students + """ All tests that ensure that only students can see what students should see belong here """ @classmethod @@ -21,10 +21,10 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): self.student = self.user_factory.make_student() self.tutor = self.user_factory.make_tutor() 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('student_page-list')) + self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) - def test_unauthorized_access_denied(self): + def test_unauthenticated_access_denied(self): response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -42,3 +42,108 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): force_authenticate(self.request, user=self.student.user) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class AccessRightsOfTutorAPIViewTests(APITestCase): + """ Tests to ensure that only Reviewers have access to the TutorList + information """ + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.student = self.user_factory.make_student() + 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'}) + + def test_unauthenticated_access_denied(self): + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_student_has_no_access(self): + force_authenticate(self.request, user=self.student.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + 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_reviewer_has_access(self): + force_authenticate(self.request, user=self.reviewer.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class AccessRightsOfStudentReviewerAPIViewTest(APITestCase): + """ Tests to ensure that only Reviewers have access to the + StudentReviewerApi endpoint information""" + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.student = self.user_factory.make_student() + self.tutor = self.user_factory.make_tutor() + self.reviewer = self.user_factory.make_reviewer() + self.request = self.factory.get(reverse('student-list')) + self.view = StudentReviewerApiViewSet.as_view({'get': 'list'}) + + def test_unauthenticated_access_denied(self): + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_student_has_no_access(self): + force_authenticate(self.request, user=self.student.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + 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_reviewer_has_access(self): + force_authenticate(self.request, user=self.reviewer.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class AccessRightsOfExamTypeAPIViewTest(APITestCase): + """ Tests who can access the exam list. The rational here is, that this + list contains information about what number of points was necessary to pass + the exam. There is no reason why anyone should see this information except + for their own module. """ + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.student = self.user_factory.make_student() + self.tutor = self.user_factory.make_tutor() + self.reviewer = self.user_factory.make_reviewer() + self.request = self.factory.get(reverse('examtype-list')) + self.view = ExamApiViewSet.as_view({'get': 'list'}) + + def test_student_has_no_access(self): + force_authenticate(self.request, user=self.student.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + 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_reviewer_has_access(self): + force_authenticate(self.request, user=self.reviewer.user) + response = self.view(self.request) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/backend/core/tests/test_auth.py b/backend/core/tests/test_auth.py index 85505585b10b0362b4f851f1ea16c3a88a5e3987..d9af01e801c52cd9de3e061c31ea3588252b306e 100644 --- a/backend/core/tests/test_auth.py +++ b/backend/core/tests/test_auth.py @@ -4,10 +4,12 @@ from core.models import UserAccount class AuthTests(APITestCase): + @classmethod def setUpTestData(cls): cls.credentials = {'username': 'user', 'password': 'p'} - cls.user = UserAccount.objects.create(username=cls.credentials['username']) + cls.user = UserAccount.objects.create( + username=cls.credentials['username']) cls.user.set_password(cls.credentials['password']) cls.user.save() cls.client = APIClient() diff --git a/backend/core/tests/test_examlist.py b/backend/core/tests/test_examlist.py index 51d55a8b9e9fe2972dd881dfabbafb26035092b9..f1e99b2d0182624576c3079149564a481317b185 100644 --- a/backend/core/tests/test_examlist.py +++ b/backend/core/tests/test_examlist.py @@ -6,27 +6,44 @@ from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.models import ExamType -from core.views import ExamListView +from core.views import ExamApiViewSet from util.factories import GradyUserFactory -NUMBER_OF_TUTORS = 7 - class ExamListTest(APITestCase): - + """ briefly tests if we are able to retrieve data, and get correct fields + """ @classmethod def setUpTestData(cls): cls.factory = APIRequestFactory() cls.user_factory = GradyUserFactory() def setUp(self): - self.request = self.factory.get(reverse('exam-list')) - force_authenticate(self.request, self.user_factory.make_student().user) - self.view = ExamListView.as_view() + self.request = self.factory.get(reverse('examtype-list')) + self.examtype = ExamType.objects.create(module_reference='B.Inf.9000', + total_score=90, + pass_score=45) + force_authenticate(self.request, + self.user_factory.make_reviewer().user) + self.view = ExamApiViewSet.as_view({'get': 'list'}) self.response = self.view(self.request) def test_can_access_when_authenticated(self): self.assertEqual(self.response.status_code, status.HTTP_200_OK) def test_getting_all_available_exams(self): - self.assertEqual(ExamType.objects.count(), len(self.response.data)) + self.assertEqual(1, len(self.response.data)) + + # Tests concerning exam data + def test_exam_data_contains_module_reference(self): + self.assertEqual('B.Inf.9000', + self.response.data[0]["module_reference"]) + + def test_exam_data_contains_total_score(self): + self.assertEqual(90, self.response.data[0]["total_score"]) + + def test_exam_data_contains_pass_score(self): + self.assertEqual(45, self.response.data[0]["pass_score"]) + + def test_exam_data_contains_pass_only_field(self): + self.assertEqual(False, self.response.data[0]["pass_only"]) diff --git a/backend/core/tests/tests.py b/backend/core/tests/test_factory_and_feedback.py similarity index 100% rename from backend/core/tests/tests.py rename to backend/core/tests/test_factory_and_feedback.py diff --git a/backend/core/tests/test_student_page.py b/backend/core/tests/test_student_page.py index 2b15fb974d92ec1319df33e3abfa017cd0d261ea..930163ad8c94ebb906210976e06f6de8253c3b14 100644 --- a/backend/core/tests/test_student_page.py +++ b/backend/core/tests/test_student_page.py @@ -1,11 +1,10 @@ from django.urls import reverse -from rest_framework import status from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.models import Reviewer, SubmissionType from core.tests import data_factories -from core.views import StudentApiView +from core.views import StudentSelfApiViewSet class StudentPageTests(APITestCase): @@ -20,8 +19,8 @@ class StudentPageTests(APITestCase): self.student = self.submission.student self.reviewer = Reviewer.objects.create( user=data_factories.make_user(username='reviewer')) - self.request = self.factory.get(reverse('student-page')) - self.view = StudentApiView.as_view() + self.request = self.factory.get(reverse('student_page-list')) + self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) force_authenticate(self.request, user=self.student.user) self.response = self.view(self.request) @@ -87,5 +86,5 @@ class StudentPageTests(APITestCase): self.student.submissions.first().type.full_score) # We don't want a matriculation number here - def test_matriculation_number_is_not_senf(self): + def test_matriculation_number_is_not_send(self): self.assertNotIn('matrikel_no', self.submission_list_first_entry) diff --git a/backend/core/tests/test_student_reviewer_viewset.py b/backend/core/tests/test_student_reviewer_viewset.py new file mode 100644 index 0000000000000000000000000000000000000000..cf30b662f022b649f6b33b302e737c9b5ea9cab3 --- /dev/null +++ b/backend/core/tests/test_student_reviewer_viewset.py @@ -0,0 +1,40 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import (APIRequestFactory, APITestCase, + force_authenticate) + +from core.tests import data_factories +from core.views import StudentReviewerApiViewSet +from util.factories import GradyUserFactory + + +class StudentPageTests(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.submission, _, _ = data_factories.make_minimal_exam() + self.student = self.submission.student + self.reviewer = self.user_factory.make_reviewer(username='reviewer') + self.request = self.factory.get(reverse('student-list')) + self.view = StudentReviewerApiViewSet.as_view({'get': 'list'}) + + force_authenticate(self.request, user=self.reviewer.user) + self.response = self.view(self.request) + + def test_can_access(self): + self.assertEqual(self.response.status_code, status.HTTP_200_OK) + + def test_can_see_all_students(self): + self.assertEqual(1, len(self.response.data)) + + def test_submissions_score_is_included(self): + self.assertEqual(self.student.submissions.first().feedback.score, + self.response.data[0]['submissions'][0]['score']) + + def test_submissions_full_score_is_included(self): + self.assertEqual(self.student.submissions.first().type.full_score, + self.response.data[0]['submissions'][0]['full_score']) diff --git a/backend/core/tests/test_submissiontypeview.py b/backend/core/tests/test_submissiontypeview.py new file mode 100644 index 0000000000000000000000000000000000000000..c1c7f4b096e2bebee299d89abefb04fbd254816f --- /dev/null +++ b/backend/core/tests/test_submissiontypeview.py @@ -0,0 +1,46 @@ +""" Tests that we can receive information about different submission types """ + +from django.urls import reverse +from rest_framework import status +from rest_framework.test import (APIRequestFactory, APITestCase, + force_authenticate) + +from core.models import SubmissionType +from core.views import SubmissionTypeApiView +from util.factories import GradyUserFactory + + +class SubmissionTypeViewTest(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.request = self.factory.get(reverse('submissiontype-list')) + SubmissionType.objects.create(name='Hard question', + full_score=20, + description='Whatever') + force_authenticate(self.request, + self.user_factory.make_reviewer().user) + self.view = SubmissionTypeApiView.as_view({'get': 'list'}) + self.response = self.view(self.request) + + def test_can_access_when_authenticated(self): + self.assertEqual(self.response.status_code, status.HTTP_200_OK) + + def test_get_all_available_submissiontypes(self): + self.assertEqual(1, len(self.response.data)) + + def test_get_sumbission_type_name(self): + self.assertEqual('Hard question', self.response.data[0]['name']) + + def test_get_full_score(self): + self.assertEqual(20, self.response.data[0]['full_score']) + + def test_get_descritpion(self): + self.assertEqual('Whatever', self.response.data[0]['description']) + + def test_there_is_no_solution_to_nothing(self): + self.assertEqual('', self.response.data[0]['solution']) diff --git a/backend/core/tests/test_tutor_api_endpoints.py b/backend/core/tests/test_tutor_api_endpoints.py index ba21ab11dae10bcef8f918afbb97ed66fe46b9bd..8c8f59a5929bf73550d7db9bab80ba52168e6e1d 100644 --- a/backend/core/tests/test_tutor_api_endpoints.py +++ b/backend/core/tests/test_tutor_api_endpoints.py @@ -4,22 +4,45 @@ * POST /tutor/:username/:email create a new tutor and email password * GET /tutorlist list of all tutors with their scores """ -import logging as log -from unittest import skip - -from django.urls import reverse +from django.contrib.auth import get_user_model from rest_framework import status +from rest_framework.reverse import reverse from rest_framework.test import (APIClient, APIRequestFactory, APITestCase, force_authenticate) -from core.models import Feedback, Reviewer, Tutor -from core.views import TutorCreateView, TutorDetailView, TutorListApiView +from core.models import Feedback, Tutor +from core.views import TutorApiViewSet from util.factories import GradyUserFactory -NUMBER_OF_TUTORS = 7 +NUMBER_OF_TUTORS = 3 + + +class TutorDeleteTest(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + 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', + args=['UFO'])) + self.view = TutorApiViewSet.as_view({'delete': 'destroy'}) + + force_authenticate(self.request, user=self.reviewer.user) + self.response = self.view(self.request, username='UFO') + + def test_can_delete_tutor_soapbox(self): + """ see if the tutor was deleted """ + self.assertEqual(0, Tutor.objects.count()) + + def test_user_is_deleted_too(self): + """ see if the associated user was deleted (reviewer remains) """ + self.assertEqual(1, get_user_model().objects.count()) -@skip class TutorListTests(APITestCase): @classmethod @@ -32,7 +55,7 @@ class TutorListTests(APITestCase): for _ in range(NUMBER_OF_TUTORS)] self.reviewer = self.user_factory.make_reviewer() self.request = self.factory.get(reverse('tutor-list')) - self.view = TutorListApiView.as_view() + self.view = TutorApiViewSet.as_view({'get': 'list'}) force_authenticate(self.request, user=self.reviewer.user) self.response = self.view(self.request) @@ -67,12 +90,12 @@ class TutorCreateTests(APITestCase): def setUp(self): self.reviewer = self.user_factory.make_reviewer() - self.request = self.factory.post(reverse('tutor-create'), + self.request = self.factory.post(reverse('tutor-list'), {'username': self.USERNAME}) - self.view = TutorCreateView.as_view() + self.view = TutorApiViewSet.as_view({'post': 'create'}) force_authenticate(self.request, user=self.reviewer.user) - self.response = self.view(self.request) + self.response = self.view(self.request, username=self.USERNAME) def test_can_access(self): self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) @@ -80,8 +103,6 @@ class TutorCreateTests(APITestCase): def test_can_create(self): self.assertEqual(Tutor.objects.first().user.username, self.USERNAME) -# @skip("Doesn't work for dubious reasons") - class TutorDetailViewTests(APITestCase): @@ -91,12 +112,12 @@ class TutorDetailViewTests(APITestCase): cls.user_factory = GradyUserFactory() def setUp(self): - self.tutor = self.user_factory.make_tutor(username='fetter.otto') + self.tutor = self.user_factory.make_tutor(username='fetterotto') self.reviewer = self.user_factory.make_reviewer() self.client = APIClient() self.client.force_authenticate(user=self.reviewer.user) - url = reverse('tutor-detail', kwargs={'username': 'fetter.otto'}) + url = reverse('tutor-detail', kwargs={'username': 'fetterotto'}) self.response = self.client.get(url, format='json') def test_can_access(self): diff --git a/backend/core/urls.py b/backend/core/urls.py index 72e447a4c83e6d6b2813a552973dd7a3e36ef3de..e64d529790c3f805c984de185817ff3983bb6fb4 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,18 +1,21 @@ -from django.conf.urls import url +from django.conf.urls import include, url from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from rest_framework.routers import DefaultRouter from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token from core import views -urlpatterns = [ - url(r'^api/student/$', views.StudentApiView.as_view(), name='student-page'), - - url(r'^api/examlist/$', views.ExamListView.as_view(), name='exam-list'), - - url(r'^api/tutor/$', views.TutorCreateView.as_view(), name='tutor-create'), - url(r'^api/tutor/(?P<username>[\w\d\.\-@_]+)$', views.TutorDetailView.as_view(), name='tutor-detail'), - url(r'^api/tutorlist/$', views.TutorListApiView.as_view(), name='tutor-list'), +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r'student', views.StudentReviewerApiViewSet) +router.register(r'examtype', views.ExamApiViewSet) +router.register(r'submissiontype', views.SubmissionTypeApiView) +router.register(r'tutor', views.TutorApiViewSet) +router.register(r'student-page', views.StudentSelfApiViewSet, + base_name='student_page') +urlpatterns = [ + url(r'^api/', include(router.urls)), 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 4470a4ab59e360f0ef30fa6c37d1f850e4cf6aac..563331875203a5312d98bbc74e4e5e5afa15ee40 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1,20 +1,19 @@ """ All API views that are used to retrieve data from the database. They can be categorized by the permissions they require. All views require a user to be authenticated and most are only accessible by one user group """ -import logging +from rest_framework import mixins, viewsets -from rest_framework import generics - -from core.models import ExamType, Tutor, Student +from core.models import ExamType, Student, SubmissionType, Tutor from core.permissions import IsReviewer, IsStudent -from core.serializers import ExamSerializer, StudentSerializer, TutorSerializer - -log = logging.getLogger(__name__) +from core.serializers import (ExamSerializer, StudentSerializer, + StudentSerializerForListView, + SubmissionTypeSerializer, TutorSerializer) -class StudentApiView(generics.RetrieveAPIView): +class StudentSelfApiViewSet(viewsets.ReadOnlyModelViewSet): """ Gets all data that belongs to one student """ permission_classes = (IsStudent,) + queryset = Student.objects.all() serializer_class = StudentSerializer def get_object(self) -> Student: @@ -23,29 +22,38 @@ class StudentApiView(generics.RetrieveAPIView): return self.request.user.student -class TutorListApiView(generics.ListAPIView): - """ A list of all tutors with information about what they corrected """ +class ExamApiViewSet(viewsets.ReadOnlyModelViewSet): + """ Gets a list of an individual exam by Id if provided """ permission_classes = (IsReviewer,) - queryset = Tutor.objects.all() - serializer_class = TutorSerializer + queryset = ExamType.objects.all() + serializer_class = ExamSerializer -class TutorCreateView(generics.CreateAPIView): - """ Creates a Tutor instance currently without a password """ +class TutorApiViewSet(mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ Api endpoint for creating, listing, viewing or deleteing tutors """ permission_classes = (IsReviewer,) + queryset = Tutor.objects.all() serializer_class = TutorSerializer + lookup_field = 'user__username' + lookup_url_kwarg = 'username' + def perform_destroy(self, instance): + """ deletes the tutors account and model (on delete cascade) """ + instance.user.delete() -class ExamListView(generics.ListAPIView): - """ Gets a list of all exams available. List might be empty """ - queryset = ExamType.objects.all() - serializer_class = ExamSerializer +class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): + """ Gets a list of all students without individual submissions """ + permission_classes = (IsReviewer,) + queryset = Student.objects.all() + serializer_class = StudentSerializerForListView -class TutorDetailView(generics.RetrieveAPIView): - """ Gets information of a single tutor by their username """ - permissions_classes = (IsReviewer,) - serializer_class = TutorSerializer - lookup_field = 'user__username' - lookup_url_kwarg = 'username' - queryset = Tutor.objects.all() + +class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet): + """ Gets a list or a detail view of a single SubmissionType """ + queryset = SubmissionType.objects.all() + serializer_class = SubmissionTypeSerializer diff --git a/backend/grady/settings/default.py b/backend/grady/settings/default.py index a4a5fd8ef3c1be98356143de28bf1b12723a38e2..f839d3ef2724d87949af12e66706728ef32f2154 100644 --- a/backend/grady/settings/default.py +++ b/backend/grady/settings/default.py @@ -40,9 +40,9 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_extensions', - 'core', 'rest_framework', 'corsheaders', + 'core', ] MIDDLEWARE = [ @@ -119,8 +119,8 @@ GRAPH_MODELS = { 'group_models': True, } -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/' +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/' MESSAGE_TAGS = { @@ -131,9 +131,6 @@ MESSAGE_TAGS = { messages.ERROR: 'alert-danger', } -COMPRESS_ENABLED = False -COMPRESS_OFFLINE = True - STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', @@ -190,16 +187,16 @@ LOGGING = { 'django': { 'level': 'INFO', 'class': 'logging.StreamHandler', - 'formatter': 'django.server' + 'formatter': 'core' }, - 'mail_admins': { # TODO: configuration + 'mail_admins': { # TODO: configuration 'level': 'ERROR', 'class': 'django.utils.log.AdminEmailHandler', } }, 'loggers': { 'django': { - 'handlers': ['django'], + 'handlers': [], }, 'django.request': { 'handlers': ['django'], diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index 325fa0c94f1e29107e295f3098003aecb0290010..22dd0f091e66c39bb1cb8f73ac2ed657f1f7e970 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -43,7 +43,7 @@ password: this.credentials.password } this.$store.dispatch('getToken', credentials).then(response => { - this.$router.push('/student/') + this.$router.push('/reviewer/') }) } } diff --git a/frontend/src/components/reviewer/ReviewerPage.vue b/frontend/src/components/reviewer/ReviewerPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..1bcc4a334896da6ba29875885c1b38210c6c898b --- /dev/null +++ b/frontend/src/components/reviewer/ReviewerPage.vue @@ -0,0 +1,57 @@ +<template> + <div> + <v-navigation-drawer persistent stateless value="true"> + <v-toolbar flat> + <v-list class="pa-1"> + <v-list-tile avatar> + <v-list-tile-avatar> + <img src="../../assets/brand.png" /> + </v-list-tile-avatar> + <v-list-tile-content> + <v-list-tile-title class="title" >Grady Menu</v-list-tile-title> + </v-list-tile-content> + </v-list-tile> + </v-list> + </v-toolbar> + <v-divider></v-divider> + <v-list> + <v-list-tile v-for="item in items" :key="item.title" :to="item.to" @click=""> + <v-list-tile-action> + <v-icon>{{ item.icon }}</v-icon> + </v-list-tile-action> + <v-list-tile-content> + <v-list-tile-title>{{ item.title }}</v-list-tile-title> + </v-list-tile-content> + </v-list-tile> + </v-list> + </v-navigation-drawer> + + <p> + Was Geht ab? + </p> + </div> +</template> + +<script> +import ReviewerToolbar from './ReviewerToolbar.vue' + +export default { + components: { + ReviewerToolbar + }, + name: 'reviewer-page', + data () { + return { + drawer: true, + items: [ + {title: 'Student List', to: '/reviewer/student-overview'}, + {title: 'Submission List', to: '/'} + ], + right: null + } + } +} +</script> + +<style lang="css" scoped> +</style> diff --git a/frontend/src/components/reviewer/ReviewerToolbar.vue b/frontend/src/components/reviewer/ReviewerToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..8c547490d824b9aa68d6c209f221d3a2ed543ac3 --- /dev/null +++ b/frontend/src/components/reviewer/ReviewerToolbar.vue @@ -0,0 +1,20 @@ +<template> + <v-toolbar> + <v-toolbar-items> + <v-list-tile-avatar> + <img src="../../assets/brand.png"> + </v-list-tile-avatar> + </v-toolbar-items> + <v-toolbar-title>Grady</v-toolbar-title> + <v-spacer></v-spacer> + </v-toolbar> +</template> + +<script> +export default { + name: 'reviewer-toolbar' +} +</script> + +<style scoped> +</style> diff --git a/frontend/src/components/reviewer/StudentListOverview.vue b/frontend/src/components/reviewer/StudentListOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..08ae4caff8a21e2869f8c85cedc679a99ee451d4 --- /dev/null +++ b/frontend/src/components/reviewer/StudentListOverview.vue @@ -0,0 +1,21 @@ +<template> + <p> + Whack o ! + </p> +</template> + +<script> +export default { + + name: 'StudentListOverview', + + data () { + return { + + } + } +} +</script> + +<style lang="css" scoped> +</style> diff --git a/frontend/src/components/student/StudentNav.vue b/frontend/src/components/student/StudentNav.vue index 6613a097b7116f3935298a0d4db14d817fe5cabd..3b2484e9d207c8f3e87b8ff9d67eaaec93a1e4d9 100644 --- a/frontend/src/components/student/StudentNav.vue +++ b/frontend/src/components/student/StudentNav.vue @@ -1,28 +1,28 @@ <template> - <b-navbar toggleable="md" type="light" variant="light"> - <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> + <v-navbar toggleable="md" type="light" variant="light"> + <v-navbar-toggle target="nav_collapse"></v-navbar-toggle> - <b-navbar-brand> + <v-navbar-brand> <img src="../../assets/brand.png" width="30" class="d-inline-block align-top"> Grady - </b-navbar-brand> + </v-navbar-brand> - <b-collapse is-nav id="nav_collapse"> + <v-collapse is-nav id="nav_collapse"> - <b-navbar-nav id="nav-left"> - <b-nav-item class="active" href="#">Results</b-nav-item> - <b-nav-item href="#">Statistics</b-nav-item> - </b-navbar-nav> + <v-navbar-nav id="nav-left"> + <v-nav-item class="active" href="#">Results</v-nav-item> + <v-nav-item href="#">Statistics</v-nav-item> + </v-navbar-nav> <!-- Right aligned nav items --> - <b-navbar-nav class="ml-auto"> - <b-nav-item>{{ this.$store.state.username }}</b-nav-item> + <v-navbar-nav class="ml-auto"> + <v-nav-item>{{ this.$store.state.username }}</v-nav-item> <router-link to="/"> - <b-button class="btn-dark" @click="logout()" >Signout</b-button> + <v-button class="btn-dark" @click="logout()" >Signout</v-button> </router-link> - </b-navbar-nav> - </b-collapse> - </b-navbar> + </v-navbar-nav> + </v-collapse> + </v-navbar> </template> diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index c29cbc7da105a411223133581fbde9ddec64aace..efb8236906d71e2dc99b1f2f5453851413f6988a 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,6 +2,8 @@ import Vue from 'vue' import Router from 'vue-router' import Login from '@/components/Login' import StudentPage from '@/components/student/StudentPage' +import ReviewerPage from '@/components/reviewer/ReviewerPage' +import StudentListOverview from '@/components/reviewer/StudentListOverview' Vue.use(Router) @@ -16,6 +18,16 @@ export default new Router({ path: '/student/', name: 'student-page', component: StudentPage + }, + { + path: '/reviewer/', + name: 'reviewer-page', + component: ReviewerPage + }, + { + path: 'reviewer/student-overview/', + name: 'student-overview', + component: StudentListOverview } ] })