diff --git a/core/admin.py b/core/admin.py index a2e9f295ca1e3cb3e5c3afd5fef9ec0cc66a6443..79001a88ee94f003a380a2964ed9369290d2b476 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from django.contrib.auth.models import Group -from core.models import (ExamType, Feedback, Reviewer, Student, Submission, - SubmissionType, Test, Tutor, UserAccount) +from core.models import (ExamType, Feedback, StudentInfo, Submission, + SubmissionType, Test, UserAccount) # Stuff we needwant admin.site.register(UserAccount) @@ -11,9 +11,7 @@ admin.site.register(Feedback) admin.site.register(Test) admin.site.register(ExamType) admin.site.register(Submission) -admin.site.register(Reviewer) -admin.site.register(Student) -admin.site.register(Tutor) +admin.site.register(StudentInfo) # ... and stuff we don't needwant admin.site.unregister(Group) diff --git a/core/management/commands/extractsubmissions.py b/core/management/commands/extractsubmissions.py index ac8db1d5ff3c6030d3b874ba0297728e8120fd1d..1e25c4ca4b01970928f13f9e9a8332c007f7e36a 100644 --- a/core/management/commands/extractsubmissions.py +++ b/core/management/commands/extractsubmissions.py @@ -1,4 +1,5 @@ from django.core.management.base import BaseCommand + from core import models diff --git a/core/management/commands/replaceusernames.py b/core/management/commands/replaceusernames.py index d496ee956be59c1baf18601f05f763d07b1069e7..cf1fe74d161ff6355e62f1a3e903de81e44b0676 100644 --- a/core/management/commands/replaceusernames.py +++ b/core/management/commands/replaceusernames.py @@ -2,10 +2,9 @@ import argparse import json import sys +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from core.models import Student - class Command(BaseCommand): help = ('replaces all usernames based on a ' @@ -21,11 +20,11 @@ class Command(BaseCommand): def _handle(self, matno2username_dict, **kwargs): matno2username = json.JSONDecoder().decode(matno2username_dict.read()) - for student in Student.objects.all(): - if student.matrikel_no in matno2username: - new_name = matno2username[student.matrikel_no] - student.user.username = new_name - student.user.save() + for student in get_user_model().get_students(): + if student.student.matrikel_no in matno2username: + new_name = matno2username[student.student.matrikel_no] + student.username = new_name + student.save() def handle(self, *args, **options): self._handle(*args, **options) diff --git a/core/management/commands/setstudentpasswords.py b/core/management/commands/setstudentpasswords.py index 0b63a275cff529e117ea1de59db82f31d659cab6..0268d6d10d4075b04b1263d0fb32e730e8ff4734 100644 --- a/core/management/commands/setstudentpasswords.py +++ b/core/management/commands/setstudentpasswords.py @@ -2,10 +2,9 @@ import csv import secrets import sys +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from core.models import Student - class Command(BaseCommand): help = ('All student passwords will be changed' @@ -26,17 +25,17 @@ class Command(BaseCommand): writer.writerow( ['Name', 'Matrikel', 'Username', 'password', 'instance']) - for student in Student.objects.all(): + for student in get_user_model().get_students(): password = ''.join(secrets.choice(choose_from) for _ in range(3)) - student.user.set_password(password) - student.user.save() + student.set_password(password) + student.save() - if not student.user.fullname: - student.user.fullname = '__no_name__' + if not student.fullname: + student.fullname = '__no_name__' - writer.writerow([student.user.fullname, student.matrikel_no, - student.user.username, password, instance]) + writer.writerow([student.fullname, student.student.matrikel_no, + student.username, password, instance]) def handle(self, *args, **options): self._handle(*args, **options) diff --git a/core/migrations/0006_auto_20180104_2001.py b/core/migrations/0006_auto_20180104_2001.py index 7a1672479bf09f7629b13877cba53a6183003e49..f790ca9e5be399441ce8ee2176a5b14a27974b82 100644 --- a/core/migrations/0006_auto_20180104_2001.py +++ b/core/migrations/0006_auto_20180104_2001.py @@ -1,8 +1,8 @@ # Generated by Django 2.0.1 on 2018-01-04 20:01 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/core/migrations/0007_auto_20180105_1136.py b/core/migrations/0007_auto_20180105_1136.py new file mode 100644 index 0000000000000000000000000000000000000000..2b3aa7ee3d0585817b5afe1a30a231f57841785d --- /dev/null +++ b/core/migrations/0007_auto_20180105_1136.py @@ -0,0 +1,41 @@ +# Generated by Django 2.0 on 2018-01-05 11:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20180104_2001'), + ] + + operations = [ + migrations.RenameModel( + old_name='Student', + new_name='StudentInfo', + ), + migrations.RemoveField( + model_name='reviewer', + name='user', + ), + migrations.RemoveField( + model_name='tutor', + name='user', + ), + migrations.RemoveField( + model_name='feedback', + name='of_reviewer', + ), + migrations.AddField( + model_name='useraccount', + name='role', + field=models.CharField(choices=[('Student', 'student'), ('Tutor', 'tutor'), ('Reviewer', 'reviewer')], default='Student', max_length=50), + preserve_default=False, + ), + migrations.DeleteModel( + name='Reviewer', + ), + migrations.DeleteModel( + name='Tutor', + ), + ] diff --git a/core/models.py b/core/models.py index 1f7d19274a567ae7864806d15039ce25c11e8054..40af977566befbd7000bd739e75b92595482b974 100644 --- a/core/models.py +++ b/core/models.py @@ -149,43 +149,48 @@ class UserAccount(AbstractUser): fullname = models.CharField('full name', max_length=70, blank=True) is_admin = models.BooleanField(default=False) - def get_associated_user(self) -> models.Model: - """ Returns the user type that is associated with this user obj """ - return \ - (hasattr(self, 'student') and self.student) or \ - (hasattr(self, 'reviewer') and self.reviewer) or \ - (hasattr(self, 'tutor') and self.tutor) + STUDENT = 'Student' + TUTOR = 'Tutor' + REVIEWER = 'Reviewer' + + ROLE_CHOICES = ( + (STUDENT, 'student'), + (TUTOR, 'tutor'), + (REVIEWER, 'reviewer') + ) - def is_reviewer(self): - return hasattr(self, 'reviewer') - - def is_tutor(self): - return hasattr(self, 'tutor') + role = models.CharField(max_length=50, choices=ROLE_CHOICES) def is_student(self): - return hasattr(self, 'student') + return self.role == 'Student' + def is_tutor(self): + return self.role == 'Tutor' -class Tutor(models.Model): - user = models.OneToOneField(get_user_model(), - on_delete=models.CASCADE, - related_name='tutor') + def is_reviewer(self): + return self.role == 'Reviewer' def get_feedback_count(self) -> int: return self.feedback_list.count() + @classmethod + def get_students(cls): + return cls.objects.filter(role=cls.STUDENT) -class Reviewer(models.Model): - user = models.OneToOneField(get_user_model(), - on_delete=models.CASCADE, - related_name='reviewer') + @classmethod + def get_tutors(cls): + return cls.objects.filter(role=cls.TUTOR) + @classmethod + def get_reviewers(cls): + return cls.objects.filter(role=cls.REVIEWER) -class Student(models.Model): + +class StudentInfo(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 - specially annotated QuerySets. + The StudentInfo 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 associated user model. @@ -331,7 +336,7 @@ class Submission(models.Model): on_delete=models.PROTECT, related_name='submissions') student = models.ForeignKey( - Student, + StudentInfo, on_delete=models.CASCADE, related_name='submissions') @@ -389,12 +394,6 @@ class Feedback(models.Model): related_name='feedback_list', blank=True, null=True) - of_reviewer = models.ForeignKey( - Reviewer, - on_delete=models.SET_NULL, - related_name='reviewed_submissions', - blank=True, - null=True) # how was this feedback created ( @@ -527,6 +526,15 @@ class GeneralTaskSubscription(models.Model): subscription=self, submission=task)[0] + def reserve_all_assignments_for_a_student(self): + assert self.query_type == self.STUDENT_QUERY + + try: + while True: + self.get_or_create_work_assignment() + except SubscriptionEnded as err: + log.info(f'Loaded all subscriptions of student {self.query_key}') + def _create_new_assignment_if_subscription_empty(self): if self.assignments.filter(is_done=False).count() < 1: self.get_or_create_work_assignment() diff --git a/core/permissions.py b/core/permissions.py index 5466fa2357f5f31a85085a172090934985e70f03..7c5f5cf0e427c0acf8cd12a08b3b6864f8c9e51b 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -4,26 +4,23 @@ 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): +class IsUserRoleGenericPermission(permissions.BasePermission): """ Generic class that encapsulates how to identify someone 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 any of the models provided in class' models attribute """ - assert self.models is not None, ( - "'%s' has to include a `models` attribute" + assert self.roles is not None, ( + "'%s' has to include a `roles` attribute" % self.__class__.__name__ ) user = request.user - is_authorized = user.is_authenticated and any(isinstance( - user.get_associated_user(), models) for models in self.models) + is_authorized = user.is_authenticated and user.role in self.roles if not is_authorized: log.warn('User "%s" has no permission to view %s', @@ -32,21 +29,21 @@ class IsUserGenericPermission(permissions.BasePermission): return is_authorized -class IsStudent(IsUserGenericPermission): +class IsStudent(IsUserRoleGenericPermission): """ Has student permissions """ - models = (Student,) + roles = ('Student', ) -class IsReviewer(IsUserGenericPermission): +class IsReviewer(IsUserRoleGenericPermission): """ Has reviewer permissions """ - models = (Reviewer,) + roles = ('Reviewer', ) -class IsTutor(IsUserGenericPermission): +class IsTutor(IsUserRoleGenericPermission): """ Has tutor permissions """ - models = (Tutor,) + roles = ('Tutor', ) -class IsTutorOrReviewer(IsUserGenericPermission): +class IsTutorOrReviewer(IsUserRoleGenericPermission): """ Has tutor or reviewer permissions """ - models = (Tutor, Reviewer,) + roles = ('Tutor', 'Reviewer') diff --git a/core/serializers.py b/core/serializers.py index c767054f769b1857cdcf08d901e2be772e1dd054..83c8bf1c60b48460630b7ddb918973c2bbc2ccf1 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,13 +1,12 @@ import logging -from django.db import IntegrityError from django.core.exceptions import ObjectDoesNotExist - from drf_dynamic_fields import DynamicFieldsMixin from rest_framework import serializers -from core.models import (ExamType, Feedback, GeneralTaskSubscription, Student, - Submission, SubmissionType, Tutor, +from core import models +from core.models import (ExamType, Feedback, GeneralTaskSubscription, + StudentInfo, Submission, SubmissionType, TutorSubmissionAssignment) from util.factories import GradyUserFactory @@ -17,19 +16,7 @@ user_factory = GradyUserFactory() class DynamicFieldsModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer): - def __init__(self, *args, **kwargs): - # Don't pass the 'fields' arg up to the superclass - fields = kwargs.pop('fields', None) - - # Instantiate the superclass normally - super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) - - if fields is not None: - # Drop any fields that are not specified in the `fields` argument. - allowed = set(fields) - existing = set(self.fields.keys()) - for field_name in existing - allowed: - self.fields.pop(field_name) + pass class ExamSerializer(DynamicFieldsModelSerializer): @@ -47,16 +34,12 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): log.debug(data) assignment_id = data.pop('assignment_id') score = data.get('score') - creator = self.context.get('request').user try: assignment = TutorSubmissionAssignment.objects.get( assignment_id=assignment_id) except ObjectDoesNotExist as err: - raise serializers.ValidationError('No assignment for id') - - if not assignment.subscription.owner == creator: - raise serializers.ValidationError('This is not your assignment') + raise serializers.ValidationError('No assignment for given id.') submission = assignment.submission if not 0 <= score <= submission.type.full_score: @@ -70,12 +53,10 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): return { **data, 'assignment': assignment, - 'of_tutor': creator, 'of_submission': submission } def create(self, validated_data) -> Feedback: - log.debug(validated_data) assignment = validated_data.pop('assignment') assignment.set_done() @@ -86,92 +67,71 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): fields = ('assignment_id', 'text', 'score') -class TestSerializer(DynamicFieldsModelSerializer): - - class Meta: - model = Test - fields = ('name', 'label', 'annotation') - - -class SubmissionTypeListSerializer(DynamicFieldsModelSerializer): - fullScore = serializers.IntegerField(source='full_score') +class SubmissionTypeSerializer(DynamicFieldsModelSerializer): + fullScore = serializers.ReadOnlyField(source='full_score') + typeId = serializers.ReadOnlyField(source='id') class Meta: model = SubmissionType - fields = ('id', 'name', 'fullScore') - - -class SubmissionTypeSerializer(SubmissionTypeListSerializer): - - class Meta: - model = SubmissionType - fields = ('id', 'name', 'fullScore', 'description', 'solution') + fields = ('typeId', 'name', 'fullScore', 'description', 'solution') class SubmissionSerializer(DynamicFieldsModelSerializer): - type = SubmissionTypeSerializer() - feedback = FeedbackSerializer() - tests = TestSerializer(many=True) - - class Meta: - model = Submission - fields = ('type', 'text', 'feedback', 'tests') - - -class SubmissionListSerializer(DynamicFieldsModelSerializer): - type = SubmissionTypeListSerializer() - # TODO change this according to new feedback model - feedback = FeedbackSerializer(fields=('score',)) + feedback = serializers.ReadOnlyField(source='feedback.text') + score = serializers.ReadOnlyField(source='feedback.score') + typeId = serializers.ReadOnlyField(source='type.id') + typeName = serializers.ReadOnlyField(source='type.name') + fullScore = serializers.ReadOnlyField(source='type.full_score') class Meta: model = Submission - fields = ('type', 'feedback') + fields = ('typeId', 'typeName', 'text', + 'feedback', 'score', 'fullScore') -class StudentSerializer(DynamicFieldsModelSerializer): +class StudentInfoSerializer(DynamicFieldsModelSerializer): name = serializers.ReadOnlyField(source='user.fullname') - matrikel_no = serializers.ReadOnlyField(source='user.matrikel_no') + user = serializers.ReadOnlyField(source='user.username') exam = ExamSerializer() - submissions = SubmissionListSerializer(many=True) + submissions = SubmissionSerializer(many=True) class Meta: - model = Student - fields = ('name', 'user', 'matrikel_no', 'exam', 'submissions') + model = StudentInfo + fields = ('name', 'user', 'exam', 'submissions') class SubmissionNoTextFieldsSerializer(DynamicFieldsModelSerializer): score = serializers.ReadOnlyField(source='feedback.score') - type = serializers.ReadOnlyField(source='type.name') - full_score = serializers.ReadOnlyField(source='type.full_score') + typeId = serializers.ReadOnlyField(source='type.id') + fullScore = serializers.ReadOnlyField(source='type.full_score') class Meta: model = Submission - fields = ('type', 'score', 'full_score') + fields = ('typeId', 'score', 'fullScore') -class StudentSerializerForListView(DynamicFieldsModelSerializer): +class StudentInfoSerializerForListView(DynamicFieldsModelSerializer): 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 + model = StudentInfo fields = ('name', 'user', 'exam', 'submissions') class TutorSerializer(DynamicFieldsModelSerializer): - username = serializers.CharField(source='user.username') feedback_count = serializers.IntegerField(source='get_feedback_count', read_only=True) - def create(self, validated_data) -> Tutor: + def create(self, validated_data) -> models.UserAccount: log.info("Crating tutor from data %s", validated_data) return user_factory.make_tutor( - username=validated_data['user']['username']) + username=validated_data['username']) class Meta: - model = Tutor + model = models.UserAccount fields = ('username', 'feedback_count') @@ -186,12 +146,12 @@ class AssignmentSerializer(DynamicFieldsModelSerializer): class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer): text = serializers.ReadOnlyField() - type_id = serializers.ReadOnlyField(source='type.id') - full_score = serializers.ReadOnlyField(source='type.full_score') + typeId = serializers.ReadOnlyField(source='type.id') + fullScore = serializers.ReadOnlyField(source='type.full_score') class Meta: model = Submission - fields = ('submission_id', 'type_id', 'text', 'full_score') + fields = ('submission_id', 'typeId', 'text', 'fullScore') class AssignmentDetailSerializer(DynamicFieldsModelSerializer): @@ -209,27 +169,29 @@ class SubscriptionSerializer(DynamicFieldsModelSerializer): assignments = AssignmentSerializer(read_only=True, many=True) def validate(self, data): + data['owner'] = self.context['request'].user + if 'query_key' in data != \ data['query_type'] == GeneralTaskSubscription.RANDOM: raise serializers.ValidationError( f'The {data["query_type"]} query_type does not work with the' f'provided key') - return data - - def create(self, validated_data) -> GeneralTaskSubscription: - subscription = GeneralTaskSubscription.objects.create( - owner=self.context.get("request").user, - **validated_data) try: - subscription._create_new_assignment_if_subscription_empty() - except IntegrityError as err: - log.debug(err) - raise + GeneralTaskSubscription.objects.get( + owner=data['owner'], + query_type=data['query_type'], + query_key=data.get('query_key', None)) + except ObjectDoesNotExist: + pass + else: raise serializers.ValidationError( - "Oh great, you raised an IntegrityError. I'm disappointed.") + 'The user already has the subscription') - return subscription + return data + + def create(self, validated_data) -> GeneralTaskSubscription: + return GeneralTaskSubscription.objects.create(**validated_data) class Meta: model = GeneralTaskSubscription diff --git a/core/tests/test_access_rights.py b/core/tests/test_access_rights.py index 9c22248b907e106d7a78448fb011655a04b5bb45..8a636dbf0191a29d4f98828310859b4d3508a49c 100644 --- a/core/tests/test_access_rights.py +++ b/core/tests/test_access_rights.py @@ -29,17 +29,17 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_tutor_has_no_access(self): - force_authenticate(self.request, user=self.tutor.user) + force_authenticate(self.request, user=self.tutor) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_reviewer_has_no_access(self): - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_student_is_authorized(self): - force_authenticate(self.request, user=self.student.user) + force_authenticate(self.request, user=self.student) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -64,17 +64,17 @@ class AccessRightsOfTutorAPIViewTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_student_has_no_access(self): - force_authenticate(self.request, user=self.student.user) + force_authenticate(self.request, user=self.student) 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) + force_authenticate(self.request, user=self.tutor) 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) + force_authenticate(self.request, user=self.reviewer) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -100,17 +100,17 @@ class AccessRightsOfStudentReviewerAPIViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_student_has_no_access(self): - force_authenticate(self.request, user=self.student.user) + force_authenticate(self.request, user=self.student) 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) + force_authenticate(self.request, user=self.tutor) 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) + force_authenticate(self.request, user=self.reviewer) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -134,16 +134,16 @@ class AccessRightsOfExamTypeAPIViewTest(APITestCase): self.view = ExamApiViewSet.as_view({'get': 'list'}) def test_student_has_no_access(self): - force_authenticate(self.request, user=self.student.user) + force_authenticate(self.request, user=self.student) 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) + force_authenticate(self.request, user=self.tutor) 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) + force_authenticate(self.request, user=self.reviewer) response = self.view(self.request) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/core/tests/test_commands.py b/core/tests/test_commands.py index 8286c8e10f1d318750fbad6a242662821105ce51..e85b8ee5f58b090755ec24e3ebd0b27801f7ed6b 100644 --- a/core/tests/test_commands.py +++ b/core/tests/test_commands.py @@ -1,13 +1,12 @@ -from django.core.management import call_command +import json +import tempfile + from django.contrib.auth import get_user_model +from django.core.management import call_command from django.test import TestCase from util.factories import GradyUserFactory -from core.models import Student -import tempfile -import json - class CommandsTestCase(TestCase): @@ -31,5 +30,5 @@ class CommandsTestCase(TestCase): args = [matno2username.name] call_command('replaceusernames', *args, **{}) - student = Student.objects.get(matrikel_no=88884444) - self.assertEqual('after', student.user.username) + student = get_user_model().objects.get(student__matrikel_no=88884444) + self.assertEqual('after', student.username) diff --git a/core/tests/test_examlist.py b/core/tests/test_examlist.py index f1e99b2d0182624576c3079149564a481317b185..b5be17ccb0f15831591d7f0619fc13973648b2d6 100644 --- a/core/tests/test_examlist.py +++ b/core/tests/test_examlist.py @@ -24,7 +24,7 @@ class ExamListTest(APITestCase): total_score=90, pass_score=45) force_authenticate(self.request, - self.user_factory.make_reviewer().user) + self.user_factory.make_reviewer()) self.view = ExamApiViewSet.as_view({'get': 'list'}) self.response = self.view(self.request) diff --git a/core/tests/test_factory_and_feedback.py b/core/tests/test_factory_and_feedback.py index 42ed4a4e2e1434cc1e4d63898c50a9ae75b153fd..5b5828901c33fcf9f2393e48c76bef4f1a884686 100644 --- a/core/tests/test_factory_and_feedback.py +++ b/core/tests/test_factory_and_feedback.py @@ -1,6 +1,7 @@ from django.test import TestCase -from core.models import Reviewer, Student, Tutor +from core import models +from core.models import StudentInfo from util.factories import GradyUserFactory @@ -10,39 +11,28 @@ class FactoryTestCase(TestCase): def test_make_student(self): - student = self.factory.make_student() + user = self.factory.make_student() - self.assertEqual(Student.objects.count(), 1) - self.assertEqual(student.exam, None) - self.assertEqual(len(str(student.matrikel_no)), 8) + self.assertEqual(StudentInfo.objects.count(), 1) + self.assertEqual(user.student.exam, None) + self.assertEqual(len(str(user.student.matrikel_no)), 8) def test_can_create_reviewer(self): - self.assertTrue(isinstance(self.factory.make_reviewer(), Reviewer)) + self.assertTrue(isinstance(self.factory.make_reviewer(), + models.UserAccount)) def test_reviewer_appears_in_query_set(self): - self.assertIn(self.factory.make_reviewer(), Reviewer.objects.all()) + self.assertIn(self.factory.make_reviewer(), + models.UserAccount.objects.all()) def test_can_create_tutor(self): - self.assertIn(self.factory.make_tutor(), Tutor.objects.all()) + self.assertIn(self.factory.make_tutor(), + models.UserAccount.objects.all()) - def test_can_create_student(self): - self.assertIn(self.factory.make_student(), Student.objects.all()) + def test_can_create_student_user(self): + self.assertIn(self.factory.make_student(), + models.UserAccount.objects.all()) - -class AccountsTestCase(TestCase): - - factory = GradyUserFactory() - - def _test_user_obj_returns_correct_type(self, maker): - model = maker() - user = model.user - self.assertEqual(user.get_associated_user(), model) - - def test_user_obj_returns_correct_type_student(self): - self._test_user_obj_returns_correct_type(self.factory.make_student) - - def test_user_obj_returns_correct_type_tutor(self): - self._test_user_obj_returns_correct_type(self.factory.make_tutor) - - def test_user_obj_returns_correct_type_reviewer(self): - self._test_user_obj_returns_correct_type(self.factory.make_reviewer) + def test_can_create_student_info(self): + self.assertIn(self.factory.make_student().student, + StudentInfo.objects.all()) diff --git a/core/tests/test_functional_views.py b/core/tests/test_functional_views.py index 5a99c8b914092f3bc786f8d710b7d9ccec7852ac..7763b5c63f990a6e727c34f512f6014c6ce400e6 100644 --- a/core/tests/test_functional_views.py +++ b/core/tests/test_functional_views.py @@ -19,16 +19,16 @@ class GetUserRoleTest(APITestCase): self.request = self.factory.get(reverse('user-role')) def test_get_user_model_returns_student(self): - force_authenticate(self.request, user=self.student.user) + force_authenticate(self.request, user=self.student) response = get_user_role(self.request) self.assertEqual(response.data['role'], 'Student') def test_get_user_model_returns_tutor(self): - force_authenticate(self.request, user=self.tutor.user) + force_authenticate(self.request, user=self.tutor) response = get_user_role(self.request) self.assertEqual(response.data['role'], 'Tutor') def test_get_user_model_returns_reviewer(self): - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) response = get_user_role(self.request) self.assertEqual(response.data['role'], 'Reviewer') diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py index 2af05f2be40a884bb09934e1155265f3f2660a82..169e794120498b0b3365bd9e55224a5c75bbc2f0 100644 --- a/core/tests/test_student_page.py +++ b/core/tests/test_student_page.py @@ -49,7 +49,7 @@ class StudentPageTests(APITestCase): }] }) - self.student = self.test_data['students'][0] + self.student = self.test_data['students'][0].student self.tutor = self.test_data['tutors'][0] self.reviewer = self.test_data['reviewers'][0] self.submission = self.test_data['submissions'][0] @@ -93,22 +93,22 @@ class StudentPageTests(APITestCase): # Tests concerning submission data def test_a_student_submissions_contains_type_name(self): self.assertEqual( - self.submission_list_first_entry['type']['name'], + self.submission_list_first_entry['typeName'], self.student.submissions.first().type.name) def test_a_student_submissions_contains_type_id(self): self.assertEqual( - self.submission_list_first_entry['type']['id'], + self.submission_list_first_entry['typeId'], self.student.submissions.first().type.id) def test_submission_data_contains_full_score(self): self.assertEqual( - self.submission_list_first_entry['type']['fullScore'], + self.submission_list_first_entry['fullScore'], self.student.submissions.first().type.full_score) def test_submission_data_contains_feedback_score(self): self.assertEqual( - self.submission_list_first_entry['feedback']['score'], + self.submission_list_first_entry['score'], self.student.submissions.first().feedback.score) # We don't want a matriculation number here @@ -148,7 +148,7 @@ class StudentSelfSubmissionsTests(APITestCase): }] }) - self.student = self.test_data['students'][0] + self.student = self.test_data['students'][0].student self.tutor = self.test_data['tutors'][0] self.submission = self.test_data['submissions'][0] self.feedback = self.submission.feedback @@ -165,29 +165,19 @@ class StudentSelfSubmissionsTests(APITestCase): # Tests concerning submission data def test_a_student_submissions_contains_type_name(self): self.assertEqual( - self.submission_list_first_entry['type']['name'], + self.submission_list_first_entry['typeName'], self.student.submissions.first().type.name) def test_a_student_submissions_contains_type_id(self): self.assertEqual( - self.submission_list_first_entry['type']['id'], + self.submission_list_first_entry['typeId'], self.student.submissions.first().type.id) def test_submission_data_contains_full_score(self): self.assertEqual( - self.submission_list_first_entry['type']['fullScore'], + self.submission_list_first_entry['fullScore'], self.student.submissions.first().type.full_score) - def test_submission_data_contains_description(self): - self.assertEqual( - self.submission_list_first_entry['type']['description'], - self.student.submissions.first().type.description) - - def test_submission_data_contains_solution(self): - self.assertEqual( - self.submission_list_first_entry['type']['solution'], - self.student.submissions.first().type.solution) - def test_submission_data_contains_text(self): self.assertEqual( self.submission_list_first_entry['text'], @@ -195,12 +185,12 @@ class StudentSelfSubmissionsTests(APITestCase): def test_submission_data_contains_feedback_score(self): self.assertEqual( - self.submission_list_first_entry['feedback']['score'], + self.submission_list_first_entry['score'], self.student.submissions.first().feedback.score) def test_submission_data_contains_feedback_text(self): self.assertEqual( - self.submission_list_first_entry['feedback']['text'], + self.submission_list_first_entry['feedback'], self.student.submissions.first().feedback.text) # We don't want a matriculation number here diff --git a/core/tests/test_student_reviewer_viewset.py b/core/tests/test_student_reviewer_viewset.py index d486641ce9bcb6a9e936988ca2b4a5bd5d10e4fd..ab39d9840366d06db063d36e6d486e72e622e203 100644 --- a/core/tests/test_student_reviewer_viewset.py +++ b/core/tests/test_student_reviewer_viewset.py @@ -48,14 +48,14 @@ class StudentPageTests(APITestCase): }] }) - self.student = self.test_data['students'][0] + self.student = self.test_data['students'][0].student self.reviewer = self.test_data['reviewers'][0] self.submission = self.test_data['submissions'][0] self.request = self.factory.get(reverse('student-list')) self.view = StudentReviewerApiViewSet.as_view({'get': 'list'}) - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) self.response = self.view(self.request) def test_can_access(self): @@ -70,4 +70,4 @@ class StudentPageTests(APITestCase): 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']) + self.response.data[0]['submissions'][0]['fullScore']) diff --git a/core/tests/test_submissiontypeview.py b/core/tests/test_submissiontypeview.py index 0bd370da38fb30272d295784593848663ac09314..5f35ffd7c2d8ceff99dc4d8f23c8a64a8d4e3e8b 100644 --- a/core/tests/test_submissiontypeview.py +++ b/core/tests/test_submissiontypeview.py @@ -23,7 +23,7 @@ class SubmissionTypeViewTestList(APITestCase): full_score=20, description='Whatever') force_authenticate(self.request, - self.user_factory.make_reviewer().user) + self.user_factory.make_reviewer()) self.view = SubmissionTypeApiView.as_view({'get': 'list'}) self.response = self.view(self.request) @@ -54,7 +54,7 @@ class SubmissionTypeViewTestRetrieve(APITestCase): description='Whatever') self.pk = SubmissionType.objects.first().pk force_authenticate(self.request, - self.user_factory.make_reviewer().user) + self.user_factory.make_reviewer()) self.view = SubmissionTypeApiView.as_view({'get': 'retrieve'}) self.response = self.view(self.request, pk=self.pk) @@ -62,7 +62,7 @@ class SubmissionTypeViewTestRetrieve(APITestCase): self.assertEqual(self.response.status_code, status.HTTP_200_OK) def test_get_id(self): - self.assertEqual(self.pk, self.response.data['id']) + self.assertEqual(self.pk, self.response.data['typeId']) def test_get_sumbission_type_name(self): self.assertEqual('Hard question', self.response.data['name']) diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py index d96aae0e56aa4edc50ee9d0e6a57b129edcb2fa9..ebdcd33d0de612dc6dd05950c46379e8595da0d7 100644 --- a/core/tests/test_subscription_assignment_service.py +++ b/core/tests/test_subscription_assignment_service.py @@ -1,12 +1,12 @@ - -from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient, APITestCase from core.models import (GeneralTaskSubscription, Submission, SubmissionType, SubscriptionEnded) -from util.factories import GradyUserFactory +from util.factories import GradyUserFactory, make_test_data -class GeneralTaskSubscriptionRandomTest(TestCase): +class GeneralTaskSubscriptionRandomTest(APITestCase): @classmethod def setUpTestData(cls): @@ -20,12 +20,14 @@ class GeneralTaskSubscriptionRandomTest(TestCase): self.submission_type = SubmissionType.objects.create( name='submission_01', full_score=14) self.submission_01 = Submission.objects.create( - type=self.submission_type, student=self.s1, text='I really failed') + type=self.submission_type, student=self.s1.student, + text='I really failed') self.submission_02 = Submission.objects.create( - type=self.submission_type, student=self.s2, text='I like apples') + type=self.submission_type, student=self.s2.student, + text='I like apples') self.subscription = GeneralTaskSubscription.objects.create( - owner=self.t.user, query_type=GeneralTaskSubscription.RANDOM) + owner=self.t, query_type=GeneralTaskSubscription.RANDOM) def test_subscription_gets_an_assignment(self): self.subscription._create_new_assignment_if_subscription_empty() @@ -54,3 +56,120 @@ class GeneralTaskSubscriptionRandomTest(TestCase): assignment = self.subscription.get_oldest_unfinished_assignment() self.assertEqual(assignment, self.subscription.get_oldest_unfinished_assignment()) + + +class TestApiEndpoints(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.data = make_test_data(data_dict={ + 'submission_types': [ + { + 'name': '01. Sort this or that', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '02. Merge this or that or maybe even this', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '03. This one exists for the sole purpose to test', + 'full_score': 30, + 'description': 'Very complicated', + 'solution': 'Trivial!' + } + ], + 'students': [ + {'username': 'student01'}, + {'username': 'student02'} + ], + 'tutors': [ + {'username': 'tutor01'}, + {'username': 'tutor02'} + ], + 'submissions': [ + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' lorem ipsum und so\n', + 'type': '01. Sort this or that', + 'user': 'student01', + 'feedback': { + 'text': 'Not good!', + 'score': 5, + 'of_tutor': 'tutor01', + 'is_final': True + } + }, + { + 'text': 'function blabl\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '02. Merge this or that or maybe even this', + 'user': 'student01' + }, + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '03. This one exists for the sole purpose to test', + 'user': 'student01' + }, + { + 'text': 'function lorem ipsum etc\n', + 'type': '03. This one exists for the sole purpose to test', + 'user': 'student02' + }, + ]} + ) + + def test_can_create_a_subscription(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response = client.post('/api/subscription/', {'query_type': 'random'}) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_subscription_and_get_one_assignment(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response = client.post('/api/subscription/', {'query_type': 'random'}) + + self.assertEqual('tutor01', response.data['owner']) + + def test_subscription_has_next_assignment(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response_subs = client.post( + '/api/subscription/', {'query_type': 'random'}) + subscription_id = response_subs.data['subscription_id'] + assignment_id = response_subs.data['assignments'][0]['assignment_id'] + + response_current = client.get( + f'/api/subscription/{subscription_id}/assignments/current/') + + self.assertEqual(1, len(response_subs.data['assignments'])) + self.assertEqual(assignment_id, + response_current.data['assignment_id']) + + def test_subscription_can_assign_to_student(self): + client = APIClient() + client.force_authenticate(user=self.data['tutors'][0]) + + response_subs = client.post( + '/api/subscription/', { + 'query_type': 'student', + 'query_key': 'student01' + }) + + assignments = response_subs.data['assignments'] + self.assertEqual(2, len(assignments)) diff --git a/core/tests/test_tutor_api_endpoints.py b/core/tests/test_tutor_api_endpoints.py index 8c8f59a5929bf73550d7db9bab80ba52168e6e1d..133b23826e0219086fe911a42f26a7fd4191017f 100644 --- a/core/tests/test_tutor_api_endpoints.py +++ b/core/tests/test_tutor_api_endpoints.py @@ -10,7 +10,7 @@ from rest_framework.reverse import reverse from rest_framework.test import (APIClient, APIRequestFactory, APITestCase, force_authenticate) -from core.models import Feedback, Tutor +from core.models import Feedback from core.views import TutorApiViewSet from util.factories import GradyUserFactory @@ -31,16 +31,16 @@ class TutorDeleteTest(APITestCase): args=['UFO'])) self.view = TutorApiViewSet.as_view({'delete': 'destroy'}) - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) 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()) + self.assertEqual(0, get_user_model().get_tutors().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()) + self.assertNotIn(self.tutor, get_user_model().objects.all()) class TutorListTests(APITestCase): @@ -57,7 +57,7 @@ class TutorListTests(APITestCase): self.request = self.factory.get(reverse('tutor-list')) self.view = TutorApiViewSet.as_view({'get': 'list'}) - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) self.response = self.view(self.request) def test_can_access(self): @@ -68,7 +68,7 @@ class TutorListTests(APITestCase): def test_feedback_count_matches_database(self): def verify_fields(tutor_obj): - t = Tutor.objects.get(user__username=tutor_obj['username']) + t = get_user_model().objects.get(username=tutor_obj['username']) return t.get_feedback_count() == tutor_obj['feedback_count'] self.assertTrue(all(map(verify_fields, self.response.data))) @@ -81,7 +81,7 @@ class TutorListTests(APITestCase): class TutorCreateTests(APITestCase): - USERNAME = 'some weird name!' + USERNAME = 'some_weird_name' @classmethod def setUpTestData(cls): @@ -94,28 +94,28 @@ class TutorCreateTests(APITestCase): {'username': self.USERNAME}) self.view = TutorApiViewSet.as_view({'post': 'create'}) - force_authenticate(self.request, user=self.reviewer.user) + force_authenticate(self.request, user=self.reviewer) self.response = self.view(self.request, username=self.USERNAME) def test_can_access(self): self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) def test_can_create(self): - self.assertEqual(Tutor.objects.first().user.username, self.USERNAME) + self.assertEqual(self.USERNAME, + get_user_model().get_tutors().first().username) class TutorDetailViewTests(APITestCase): @classmethod def setUpTestData(cls): - cls.factory = APIClient() cls.user_factory = GradyUserFactory() def setUp(self): 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) + self.client.force_authenticate(user=self.reviewer) url = reverse('tutor-detail', kwargs={'username': 'fetterotto'}) self.response = self.client.get(url, format='json') @@ -125,4 +125,4 @@ class TutorDetailViewTests(APITestCase): def test_can_view_tutor(self): self.assertEqual(self.response.data['username'], - self.tutor.user.username) + self.tutor.username) diff --git a/core/urls.py b/core/urls.py index 7898cedd9b7861057e317afdc17a9fe90ad1f406..e11beea8bfb4f019b0c240fd94676188582b127f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -6,21 +6,25 @@ from core import views # Create a router and register our viewsets with it. router = DefaultRouter() -router.register('student', views.StudentReviewerApiViewSet) +router.register('student', views.StudentReviewerApiViewSet, + base_name='student') router.register('examtype', views.ExamApiViewSet) router.register('feedback', views.FeedbackApiView) router.register('submissiontype', views.SubmissionTypeApiView) -router.register('tutor', views.TutorApiViewSet) +router.register('tutor', views.TutorApiViewSet, base_name='tutor') router.register('subscription', views.SubscriptionApiViewSet) router.register('assignment', views.AssignmentApiViewSet) # regular views that are not viewsets regular_views_urlpatterns = [ - path('student-page', + path('student-page/', views.StudentSelfApiView.as_view(), name='student-page'), - path('user-role', views.get_user_role, name='user-role'), - path('jwt-time-delta', + path('student-submissions/', + views.StudentSelfSubmissionsApiView.as_view(), + name='student-submissions'), + path('user-role/', views.get_user_role, name='user-role'), + path('jwt-time-delta/', views.get_jwt_expiration_delta, name='jwt-time-delta') ] diff --git a/core/views.py b/core/views.py index f26e69d414de17e843cdf4fea813f1228d954837..edb9840e9c1f75112696191075b0de5031b09aee 100644 --- a/core/views.py +++ b/core/views.py @@ -7,14 +7,16 @@ from rest_framework.decorators import api_view, detail_route from rest_framework.response import Response from core import models -from core.models import (ExamType, Feedback, GeneralTaskSubscription, Student, - SubmissionType, Tutor, TutorSubmissionAssignment) +from core.models import (ExamType, Feedback, GeneralTaskSubscription, + StudentInfo, SubmissionType, + TutorSubmissionAssignment) from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer from core.serializers import (AssignmentDetailSerializer, AssignmentSerializer, ExamSerializer, FeedbackSerializer, - StudentSerializer, StudentSerializerForListView, - SubmissionTypeSerializer, SubscriptionSerializer, - TutorSerializer) + StudentInfoSerializer, + StudentInfoSerializerForListView, + SubmissionSerializer, SubmissionTypeSerializer, + SubscriptionSerializer, TutorSerializer) @api_view() @@ -24,16 +26,15 @@ def get_jwt_expiration_delta(request): @api_view() def get_user_role(request): - return Response({'role': - type(request.user.get_associated_user()).__name__}) + return Response({'role': request.user.role}) class StudentSelfApiView(generics.RetrieveAPIView): """ Gets all data that belongs to one student """ permission_classes = (IsStudent,) - serializer_class = StudentSerializer + serializer_class = StudentInfoSerializer - def get_object(self) -> Student: + def get_object(self) -> StudentInfo: """ The object in question is the student associated with the requests user. Since the permission IsStudent is satisfied the member exists """ return self.request.user.student @@ -64,6 +65,21 @@ class FeedbackApiView( serializer_class = FeedbackSerializer lookup_field = 'submission__submission_id' + def create(self, request, *args, **kwargs): + serializer = self.get_serializer( + data={**request.data, 'of_tutor': request.user}) + serializer.is_valid(raise_exception=True) + + if serializer.data['assignment'].subscription.owner != request.user: + return Response({'You do not have permission to edit this'}, + status=status.HTTP_403_FORBIDDEN) + + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, + status=status.HTTP_201_CREATED, + headers=headers) + class TutorApiViewSet( mixins.RetrieveModelMixin, @@ -73,21 +89,17 @@ class TutorApiViewSet( viewsets.GenericViewSet): """ Api endpoint for creating, listing, viewing or deleteing tutors """ permission_classes = (IsReviewer,) - queryset = Tutor.objects.all() + queryset = models.UserAccount.objects.filter(role='Tutor') serializer_class = TutorSerializer - lookup_field = 'user__username' + lookup_field = 'username' lookup_url_kwarg = 'username' - def perform_destroy(self, instance): - """ deletes the tutors account and model (on delete cascade) """ - instance.user.delete() - class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): """ Gets a list of all students without individual submissions """ permission_classes = (IsReviewer,) - queryset = Student.objects.all() - serializer_class = StudentSerializerForListView + queryset = StudentInfo.objects.all() + serializer_class = StudentInfoSerializerForListView class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet): @@ -140,6 +152,21 @@ class SubscriptionApiViewSet( instance.delete() return Response(status=status.HTTP_204_NO_CONTENT) + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + subscription = serializer.save() + + if subscription.query_type == GeneralTaskSubscription.STUDENT_QUERY: + subscription.reserve_all_assignments_for_a_student() + else: + subscription.get_oldest_unfinished_assignment() + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, + status=status.HTTP_201_CREATED, + headers=headers) + class AssignmentApiViewSet( mixins.RetrieveModelMixin, diff --git a/delbert.py b/delbert.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/docs/feedback.api.json b/docs/feedback.api.json new file mode 100644 index 0000000000000000000000000000000000000000..c48beea4146bc18419f8a5b03ecf2d9de9826fe2 --- /dev/null +++ b/docs/feedback.api.json @@ -0,0 +1,43 @@ +GET /subscription/<id> + { + "subscription_id": "e313e608-7453-4053-a536-5d18fc9ec3a9", + "owner": "reviewer01", + "query_type": "random", + "query_key": "", + "assignments": [ + { + "assignment_id": "dbdde0d0-b1a6-474c-b2be-41edb5229803", + "submission_id": "1558c390-5598-482b-abd3-1f5780e75e0d", + "is_done": false + } + ] + } + +POST /subscription/ + { + "owner": "<some user>", + "query_type": "random|student|submission_type|exam", + "query_key": "<pk for query type>?" + } + +DELETE /subscription/<id> +PATCH /subscription/<id> { + "deactivate": true // or false for reactivation +} + +GET /subscription/assignments/current +GET /subscription/assignments/next +GET /subscription/assignments/past + +GET /assignment/<id> // only those belonging to the requests user + { + "assignment_id": "dbdde0d0-b1a6-474c-b2be-41edb5229803", + "submission_id": "1558c390-5598-482b-abd3-1f5780e75e0d", + "is_done": false + } + +DELETE /assignment/<id> // check done conditions + +// done conditions +// * feedback was posted +// * feedback was patched (every) diff --git a/util/factories.py b/util/factories.py index a27f20bdede0dffea2e12b956dff1afbc33d3262..9ea00f3bd8b16329ca2680e24f7d3aa13cfdcbbf 100644 --- a/util/factories.py +++ b/util/factories.py @@ -3,8 +3,8 @@ import secrets import string from core.models import UserAccount as User -from core.models import (ExamType, Feedback, Reviewer, Student, Submission, - SubmissionType, Tutor) +from core.models import (ExamType, Feedback, StudentInfo, Submission, + SubmissionType) STUDENTS = 'students' TUTORS = 'tutors' @@ -44,22 +44,35 @@ class GradyUserFactory: def _get_random_name(self, prefix='', suffix='', k=4): return ''.join((prefix, self.make_password(k), suffix)) - def _make_base_user(self, username, groupname, password=None, + def _get_group_for_user_role(self, role): + """ Returns the groupname for a role """ + return { + 'Student': 'students', + 'Tutor': 'tutors', + 'Reviewer': 'reviewers' + }[role] + + def _make_base_user(self, username, role, password=None, store_pw=False, **kwargs): """ This is a specific wrapper for the django update_or_create method of objects. - * A new user is created and password and group are set accordingly - * If the user was there before password is NOT change but group is. - * A user must only have one group. + * If now username is passed, a generic one will be generated + * A new user is created and password and role are set accordingly + * If the user was there before password IS changed + * A user must only have one role. Returns: - (User object, str): The user object that was added to the group and + (User object, str): The user object that was added to the role and the password of that user if it was created. """ + if not username: + username = self._get_random_name(prefix=role.lower() + '_') + username = username.strip() user, created = User.objects.update_or_create( username=username, + role=role, defaults=kwargs) if created: @@ -68,51 +81,33 @@ class GradyUserFactory: user.save() if created and store_pw: - self.password_storge(username, groupname, password) + self.password_storge( + username, + self._get_group_for_user_role(role), + password) return user - def _get_user_model_for_group(self, groupname): - """ Returns the model class for a usergroup """ - return { - STUDENTS: Student, - TUTORS: Tutor, - REVIEWERS: Reviewer, - }[groupname] - - def _make_user_generic(self, username, groupname, **kwargs): - """ Provides a model with a associated user but without any defaults - """ - - if not username: - username = self._get_random_name(prefix=groupname.lower() + '_') - - model = self._get_user_model_for_group(groupname) - user = self._make_base_user(username, groupname, **kwargs) - - generic_user, _ = model.objects.get_or_create(user=user) - - return generic_user - def make_student(self, username=None, matrikel_no=None, exam=None, **kwargs): """ Creates a student. Defaults can be passed via kwargs like in relation managers objects.update method. """ - user = self._make_user_generic(username, STUDENTS, **kwargs) + user = self._make_base_user(username, 'Student', **kwargs) + studentInfo = StudentInfo.objects.get_or_create(user=user)[0] if matrikel_no: - user.matrikel_no = matrikel_no + studentInfo.matrikel_no = matrikel_no if exam: - user.exam = exam - user.save() + studentInfo.exam = exam + studentInfo.save() return user def make_tutor(self, username=None, **kwargs): """ Creates or updates a tutor if needed with defaults """ - return self._make_user_generic(username, TUTORS, **kwargs) + return self._make_base_user(username, 'Tutor', **kwargs) def make_reviewer(self, username=None, **kwargs): """ Creates or updates a reviewer if needed with defaults """ - return self._make_user_generic(username, REVIEWERS, **kwargs) + return self._make_base_user(username, 'Reviewer', **kwargs) def make_exams(exams=[], **kwargs): @@ -128,9 +123,11 @@ def make_submission_types(submission_types=[], **kwargs): def make_students(students=[], **kwargs): + return [GradyUserFactory().make_student( username=student['username'], - exam=ExamType.objects.get(module_reference=student['exam']) + exam=ExamType.objects.get( + module_reference=student['exam']) if 'exam' in student else None ) for student in students] @@ -146,7 +143,7 @@ def make_reviewers(reviewers=[], **kwargs): def make_feedback(feedback, submission_object): feedback['of_tutor'] = User.objects.get( - username=feedback['of_tutor']).get_associated_user().user + username=feedback['of_tutor']) return Feedback.objects.update_or_create( of_submission=submission_object, defaults=feedback)[0] @@ -157,7 +154,7 @@ def make_submissions(submissions=[], **kwargs): for submission in submissions: submission_type, _ = SubmissionType.objects.get_or_create( name=submission.get('type', 'Auto generated type')) - student, _ = Student.objects.get_or_create(user=User.objects.get( + student, _ = StudentInfo.objects.get_or_create(user=User.objects.get( username=submission.get('user', 'default_user') )) submission_object, _ = Submission.objects.get_or_create( diff --git a/util/importer.py b/util/importer.py index d6f3080403a80c7b6f08eaa4c8e9240aa38837b9..c445545ca4f7ff13d8aa68ca69446de69028fb56 100644 --- a/util/importer.py +++ b/util/importer.py @@ -7,7 +7,7 @@ from typing import Callable import util.convert import util.processing from core.models import UserAccount as User -from core.models import (ExamType, Feedback, Student, Submission, +from core.models import (ExamType, Feedback, StudentInfo, Submission, SubmissionType, Test) from util.factories import REVIEWERS, STUDENTS, TUTORS, GradyUserFactory from util.messages import info, warn @@ -84,7 +84,7 @@ def add_user(username, group, **kwargs): def add_student(username, email, submissions, **kwargs): user = add_user(username, STUDENTS, email=email) - student, _ = Student.objects.update_or_create( + student, _ = StudentInfo.objects.update_or_create( user=user, defaults={'user': user, **kwargs} )