diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3ed4bc5becc9985223978dedac71e4fdca2c1fe5..363b86959788038c4b315423fdd2cc633b5df76a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,97 +1,122 @@ stages: - - build - - test - - pages - - staging + - build + - test + - pages + - build_image + - staging variables: - IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME -# ============================= Building section ============================= # -build_backend: - image: docker:latest - stage: build - script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - - docker build -t $IMAGE_TAG . - - docker push $IMAGE_TAG + +# ========================== Build Testing section =========================== # +build_test_env: + image: python:3.6 + stage: build + script: + - python -m venv .venv + - source .venv/bin/activate + - make install + artifacts: + paths: + - .venv/ + expire_in: 1 hour + cache: + paths: + - .venv # ============================== Testing section ============================= # # ----------------------------- Backend subsection --------------------------- # -.test_template_backend: &test_definition_backend - stage: test - image: $IMAGE_TAG - before_script: - - pip install -r requirements.dev.txt +.test_template_virtualenv: &test_definition_virtualenv + image: python:3.6 + before_script: + - source .venv/bin/activate + dependencies: + - build_test_env test_pytest: - <<: *test_definition_backend - services: - - postgres:9.5 - script: - - DJANGO_SETTINGS_MODULE=grady.settings pytest --cov - artifacts: - paths: - - .coverage + <<: *test_definition_virtualenv + stage: test + services: + - postgres:9.5 + script: + - DJANGO_SETTINGS_MODULE=grady.settings pytest --cov + artifacts: + paths: + - .coverage -test_prospector: - <<: *test_definition_backend - script: - - prospector --uses django || exit 0 +test_flake8: + <<: *test_definition_virtualenv + stage: test + script: + - flake8 --exclude=migrations --ignore=N802 core # ----------------------------- Frontend subsection -------------------------- # .test_template_frontend: &test_definition_frontend - image: node:carbon - stage: test - before_script: - - cd frontend/ + image: node:carbon + before_script: + - cd frontend/ test_frontend: - <<: *test_definition_frontend - script: - - yarn install - - yarn test --single-run + <<: *test_definition_frontend + stage: test + script: + - yarn install + - yarn test --single-run + cache: + paths: + - frontend/node_modules/ # =========================== Gitlab pages section =========================== # test_coverage: - image: $IMAGE_TAG - stage: - pages - script: - - pip install coverage - - coverage html -d public - dependencies: - - test_pytest - artifacts: - paths: - - public - only: - - master + <<: *test_definition_virtualenv + stage: + pages + script: + - coverage html -d public + dependencies: + - test_pytest + artifacts: + paths: + - public + only: + - master + +# =========================== Build Image section ============================ # +build_backend: + image: docker:latest + stage: build_image + script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + - docker build -t $IMAGE_TAG . + - docker push $IMAGE_TAG + only: + - master # ============================== Staging section ============================= # .staging_template: &staging_definition - stage: staging - image: docker:latest - only: - - master - before_script: - - apk add --update py-pip && pip install docker-compose + stage: staging + image: docker:latest + only: + - master + before_script: + - apk add --update py-pip && pip install docker-compose staging: - <<: *staging_definition - environment: - name: review/$CI_COMMIT_REF_NAME - url: https://staging.grady.janmax.org - on_stop: staging_stop - script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - - docker-compose up -d --force-recreate + <<: *staging_definition + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://staging.grady.janmax.org + on_stop: staging_stop + script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + - docker-compose up -d --force-recreate staging_stop: - <<: *staging_definition - script: - - docker-compose rm --force --stop - when: manual - environment: - name: review/$CI_COMMIT_REF_NAME - action: stop + <<: *staging_definition + script: + - docker-compose rm --force --stop + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e63378ac57329d9a0728dce6758f4d2e7585e6d0..053bd6572ce691ad5c3702e86133c104fe057666 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,10 +12,3 @@ args: - requirements.txt - requirements.dev.txt -- repo: local - hooks: - - id: prospector - name: prospector - entry: ./pre-commit-scripts/prospector.sh - language: script - types: [python] diff --git a/Dockerfile b/Dockerfile index bbb5fbb57527031bf4fdb3826b46f8638e41813f..3a33f19c679677ca9f9f92962b3d19e51a8c05f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,9 +17,6 @@ RUN apk update \ && apk add --virtual build-deps gcc python3-dev musl-dev curl \ && apk add --no-cache postgresql-dev -RUN mkdir -p /usr/share/dict -RUN curl -s https://gitlab.gwdg.de/snippets/51/raw --output /usr/share/dict/words - WORKDIR /code COPY . /code diff --git a/Makefile b/Makefile index b9dc21017bb8b37e07b0a1ee88195f1f6580b0ce..035e3be63dba808a65b7cd10049ce7e820383ce6 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ isort-check: migrate: python manage.py migrate -install: +install: pip install -r requirements.txt pip install -r requirements.dev.txt diff --git a/core/grady_speak.py b/core/grady_speak.py deleted file mode 100644 index 08b6242f7406b15b820aa11bf9ded76e07816869..0000000000000000000000000000000000000000 --- a/core/grady_speak.py +++ /dev/null @@ -1,23 +0,0 @@ -grady_says = [ - "Now let's see if we can improve this with a little water, sir.", - "Won't keep you a moment, sir.", - "Grady, sir. Delbert Grady.", - "Yes, sir.", - "That's right, sir.", - "Why no, sir. I don't believe so.", - "Ah ha, it's coming off now, sir.", - "Why no, sir. I don't believe so.", - "Yes, sir. I have a wife and two daughters, sir.", - "Oh, they're somewhere around. I'm not quite sure at the moment, sir.", - "That's strange, sir. I don't have any recollection of that at all.", - "I'm sorry to differ with you, sir, but you are the caretaker.", - "You have always been the caretaker, I should know, sir.", - "I've always been here.", - "Indeed, he is, Mr. Torrance. Avery willful boy. ", - "A rather naughty boy, if I may be so bold, sir.", - "Perhaps they need a good talking to, if you don't mind my saying so. Perhaps a bit more.", - "My girls, sir, they didn't care for the Overlook at first.", - "One of them actually stole a packet of matches and tried to burn it down.", - "But I corrected them, sir.", - "And when my wife tried to prevent me from doing my duty... I corrected her.", -] diff --git a/core/models.py b/core/models.py index 30a240f2b5521a7e049c4663098532ae6abd3f2b..966b647d3b5aad0c0a5d6759fbddc715aa703870 100644 --- a/core/models.py +++ b/core/models.py @@ -13,9 +13,8 @@ from typing import Dict, Union from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser from django.db import models -from django.db.models import Value as V from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q, - QuerySet, Sum, When) + QuerySet, Sum, Value, When) from django.db.models.functions import Coalesce @@ -125,7 +124,7 @@ class SubmissionType(models.Model): When( Q(submissions__feedback__isnull=False) & Q(submissions__feedback__status=Feedback.ACCEPTED), - then=V(1)), output_field=IntegerField(), + then=Value(1)), output_field=IntegerField(), ) ) ).annotate( @@ -230,11 +229,12 @@ class Student(models.Model): the annotated QuerySet as described above. """ return cls.objects.annotate( - overall_score=Coalesce(Sum('submissions__feedback__score'), V(0)), + overall_score=Coalesce(Sum('submissions__feedback__score'), + Value(0)), ).annotate( done=Case( - When(exam__pass_score__lt=F('overall_score'), then=V(1)), - default=V(0), + When(exam__pass_score__lt=F('overall_score'), then=Value(1)), + default=Value(0), output_field=BooleanField() ) ) @@ -362,12 +362,12 @@ class Submission(models.Model): candidates = cls.objects.filter( ( - Q(feedback__isnull=True) - | Q(feedback__origin=Feedback.DID_NOT_COMPILE) - | Q(feedback__origin=Feedback.COULD_NOT_LINK) - | Q(feedback__origin=Feedback.FAILED_UNIT_TESTS) - ) - & ~Q(feedback__of_tutor=tutor) + Q(feedback__isnull=True) | + Q(feedback__origin=Feedback.DID_NOT_COMPILE) | + Q(feedback__origin=Feedback.COULD_NOT_LINK) | + Q(feedback__origin=Feedback.FAILED_UNIT_TESTS) + ) & + ~Q(feedback__of_tutor=tutor) ) # we want a submission of a specific type @@ -542,6 +542,7 @@ class Feedback(models.Model): ) return tutor_feedback[0] if tutor_feedback else None + @classmethod def tutor_assigned_feedback(cls, user: Union[Tutor, Reviewer]): """Gets all feedback that is assigned to the tutor including all status cases. diff --git a/core/serializers.py b/core/serializers.py index f46507a58647e9e4bb5bf21705e71a84249c0725..44b91717174fe9af54cddc52ce0e668564f31b28 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -11,7 +11,8 @@ log = logging.getLogger(__name__) user_factory = GradyUserFactory() -class DynamicFieldsModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer): +class DynamicFieldsModelSerializer(DynamicFieldsMixin, + serializers.ModelSerializer): pass diff --git a/core/tests/data_factories.py b/core/tests/data_factories.py deleted file mode 100644 index f2f8b4f5a709718ec49cc04479d5d43ec172307e..0000000000000000000000000000000000000000 --- a/core/tests/data_factories.py +++ /dev/null @@ -1,85 +0,0 @@ -""" A set of factory methods that make testing easier. Each method creates all -reuired subfields if not provided by via kwargs. """ - -from core.models import (ExamType, Feedback, Reviewer, Student, Submission, - SubmissionType, Tutor, UserAccount) - -# These methods are meant to be used to provide data to insert into the test -# database - - -def make_user(username='user01', - password='p', - fullname='us er01', - is_admin=False): - user = UserAccount.objects.create(username=username, - fullname=fullname, - is_admin=is_admin) - user.set_password(password) - user.save() - return user - - -def make_exam(module_reference='TestExam B.Inf.0042', - total_score=42, - pass_score=21, - **kwargs): - return ExamType.objects.create(module_reference=module_reference, - total_score=total_score, - pass_score=pass_score, **kwargs) - - -def make_submission_type(name='problem01', - full_score=10, - description='Very hard', - solution='Impossible!'): - return SubmissionType.objects.create(name=name, - full_score=full_score, - description=description, - solution=solution) - - -def make_student(user=None, exam=None): - if user is None: - user = make_user() - if exam is None: - exam = make_exam() - return Student.objects.create(user=user, exam=exam) - - -def make_tutor(user=None): - if user is None: - user = make_user() - return Tutor.objects.create(user=user) - - -def make_reviewer(user=None): - if user is None: - user = make_user() - return Reviewer.objects.create(user=user) - - -def make_submission(type=None, student=None, text='Too hard for me ;-('): - if type is None: - type = make_submission_type() - if student is None: - student = make_student() - return Submission.objects.create(text=text, type=type, student=student) - - -def make_feedback(of_tutor, of_submission=None, text='Very bad!', score=3): - if of_submission is None: - of_submission = make_submission() - return Feedback.objects.create(of_tutor=of_tutor, - of_submission=of_submission, - text=text, - score=score) - - -def make_minimal_exam(): - # also creates default examType, submissionType and student - submission = make_submission() - - tutor = make_tutor(user=make_user(username='tutor01')) - feedback = make_feedback(of_tutor=tutor, of_submission=submission) - return submission, tutor, feedback diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py index 63ae4f0af59bfacfc935c20d00e8476fdda602e8..8bd9f349564483dea02e3c4ffc927f4ec65c8e4c 100644 --- a/core/tests/test_student_page.py +++ b/core/tests/test_student_page.py @@ -2,9 +2,9 @@ from django.urls import reverse from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) -from core.models import Reviewer, SubmissionType -from core.tests import data_factories +from core.models import SubmissionType from core.views import StudentSelfApiView +from util.factories import make_test_data class StudentPageTests(APITestCase): @@ -14,11 +14,47 @@ class StudentPageTests(APITestCase): cls.factory = APIRequestFactory() def setUp(self): - self.submission, self.tutor, self.feedback = \ - data_factories.make_minimal_exam() - self.student = self.submission.student - self.reviewer = Reviewer.objects.create( - user=data_factories.make_user(username='reviewer')) + self.test_data = make_test_data(data_dict={ + 'exams': [{ + 'module_reference': 'TestExam B.Inf.0042', + 'total_score': 42, + 'pass_score': 21 + }], + 'submission_types': [{ + 'name': 'problem01', + 'full_score': 10, + 'description': 'Very hard', + 'solution': 'Impossible!' + }], + 'students': [{ + 'username': 'user01', + 'fullname': 'us er01', + 'exam': 'TestExam B.Inf.0042' + }], + 'tutors': [{ + 'username': 'tutor01' + }], + 'reviewers': [{ + 'username': 'reviewer' + }], + 'submissions': [{ + 'user': 'user01', + 'type': 'problem01', + 'text': 'Too hard for me ;-(', + 'feedback': { + 'of_tutor': 'tutor01', + 'text': 'Very bad!', + 'score': 3 + } + }] + }) + + self.student = self.test_data['students'][0] + self.tutor = self.test_data['tutors'][0] + self.reviewer = self.test_data['reviewers'][0] + self.submission = self.test_data['submissions'][0] + self.feedback = self.submission.feedback + self.request = self.factory.get(reverse('student-page')) self.view = StudentSelfApiView.as_view() diff --git a/core/tests/test_student_reviewer_viewset.py b/core/tests/test_student_reviewer_viewset.py index cf30b662f022b649f6b33b302e737c9b5ea9cab3..d486641ce9bcb6a9e936988ca2b4a5bd5d10e4fd 100644 --- a/core/tests/test_student_reviewer_viewset.py +++ b/core/tests/test_student_reviewer_viewset.py @@ -3,9 +3,8 @@ 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 +from util.factories import make_test_data class StudentPageTests(APITestCase): @@ -13,12 +12,46 @@ 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.test_data = make_test_data(data_dict={ + 'exams': [{ + 'module_reference': 'TestExam B.Inf.0042', + 'total_score': 42, + 'pass_score': 21 + }], + 'submission_types': [{ + 'name': 'problem01', + 'full_score': 10, + 'description': 'Very hard', + 'solution': 'Impossible!' + }], + 'students': [{ + 'username': 'user01', + 'fullname': 'us er01', + 'exam': 'TestExam B.Inf.0042' + }], + 'tutors': [{ + 'username': 'tutor' + }], + 'reviewers': [{ + 'username': 'reviewer' + }], + 'submissions': [{ + 'user': 'user01', + 'type': 'problem01', + 'text': 'Too hard for me ;-(', + 'feedback': { + 'score': 3, + 'of_tutor': 'tutor' + } + }] + }) + + self.student = self.test_data['students'][0] + 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'}) diff --git a/core/urls.py b/core/urls.py index d0e251b70cbc95adbd26041e484dcc9b643c1c4e..cb66e7d2222fa0fd4a9706a00c8bac8339c93646 100644 --- a/core/urls.py +++ b/core/urls.py @@ -15,9 +15,11 @@ router.register(r'tutor', views.TutorApiViewSet) # regular views that are not viewsets regular_views_urlpatterns = [ - url(r'student-page', views.StudentSelfApiView.as_view(), name='student-page'), + url(r'student-page', views.StudentSelfApiView.as_view(), + name='student-page'), url(r'user-role', views.get_user_role, name='user-role'), - url(r'jwt-time-delta', views.get_jwt_expiration_delta, name='jwt-time-delta') + url(r'jwt-time-delta', views.get_jwt_expiration_delta, + name='jwt-time-delta') ] urlpatterns = [ diff --git a/core/views.py b/core/views.py index 32bd7f1263e3e4bedb6719e171846e7b356d1da5..6d5a9595d19d6c8451c84da2f92c6f2a674c0dcc 100644 --- a/core/views.py +++ b/core/views.py @@ -20,7 +20,8 @@ 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': + type(request.user.get_associated_user()).__name__}) class StudentSelfApiView(generics.RetrieveAPIView): diff --git a/pre-commit-scripts/prospector.sh b/pre-commit-scripts/prospector.sh deleted file mode 100755 index f6d218ff3bdf21de8df9dc93d0cbf80bcfd80452..0000000000000000000000000000000000000000 --- a/pre-commit-scripts/prospector.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -diff_files="$(git diff --cached --name-only --relative --diff-filter=AM)" -if [ -n "$diff_files" ]; then - prospector --uses django $diff_files -else - exit 0 -fi diff --git a/requirements.dev.txt b/requirements.dev.txt index 63b69b7d1ecfc80af3daa2c7027332dadfc8178a..eaa909a4ee73cc11378a4b7d2041c5d66d204e48 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,4 +1,4 @@ +flake8~=3.5.0 pre-commit~=1.4.1 -prospector~=0.12.7 pytest-cov~=2.5.1 pytest-django~=3.1.2 diff --git a/util/factories.py b/util/factories.py index a4161cfd62634c76c0e3480571b4f0f421455c6a..5b6f71575d7b426e5b8dc95048ffccb84034b3bb 100644 --- a/util/factories.py +++ b/util/factories.py @@ -1,8 +1,10 @@ import configparser import secrets +import string +from core.models import (Reviewer, Student, Tutor, ExamType, + SubmissionType, Submission, Feedback) from core.models import UserAccount as User -from core.models import Reviewer, Student, Tutor STUDENTS = 'students' TUTORS = 'tutors' @@ -11,12 +13,10 @@ REVIEWERS = 'reviewers' PASSWORDS = '.importer_passwords' -def get_xkcd_password(k=2): - with open('/usr/share/dict/words') as words: - choose_from = list({word.strip().lower() - for word in words if 5 < len(word) < 8}) - - return ''.join(secrets.choice(choose_from) for _ in range(k)) +def get_random_password(length=32): + """ Returns a cryptographically random string of specified length """ + return ''.join(secrets.choice(string.ascii_lowercase) + for _ in range(length)) def store_password(username, groupname, password): @@ -35,7 +35,7 @@ def store_password(username, groupname, password): class GradyUserFactory: def __init__(self, - password_generator_func=get_xkcd_password, + password_generator_func=get_random_password, password_storge=store_password, *args, **kwargs): self.password_generator_func = password_generator_func @@ -43,9 +43,9 @@ class GradyUserFactory: @staticmethod def _get_random_name(prefix='', suffix='', k=1): - return ''.join((prefix, get_xkcd_password(k), suffix)) + return ''.join((prefix, get_random_password(k), suffix)) - def _make_base_user(self, username, groupname, store_pw=False, **kwargs): + def _make_base_user(self, username, groupname, 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 @@ -63,7 +63,7 @@ class GradyUserFactory: defaults=kwargs) if created: - password = self.password_generator_func() + password = self.password_generator_func() if password is None else password user.set_password(password) user.save() @@ -99,9 +99,10 @@ class GradyUserFactory: relation managers objects.update method. """ user = self._make_user_generic(username, STUDENTS, **kwargs) if matrikel_no: - user.objects.update(matrikel_no=matrikel_no) + user.matrikel_no = matrikel_no if exam: - user.objects.update(exam=exam) + user.exam = exam + user.save() return user def make_tutor(self, username=None, **kwargs): @@ -111,3 +112,162 @@ class GradyUserFactory: def make_reviewer(self, username=None, **kwargs): """ Creates or updates a reviewer if needed with defaults """ return self._make_user_generic(username, REVIEWERS, **kwargs) + + +def make_exams(exams=[], **kwargs): + return [ExamType.objects.get_or_create( + module_reference=exam['module_reference'], + defaults=exam)[0] for exam in exams] + + +def make_submission_types(submission_types=[], **kwargs): + return [SubmissionType.objects.get_or_create( + name=submission_type['name'], defaults=submission_type)[0] + for submission_type in submission_types] + + +def make_students(students=[], **kwargs): + return [GradyUserFactory().make_student( + username=student['username'], + exam=ExamType.objects.get(module_reference=student['exam']) + ) for student in students] + + +def make_tutors(tutors=[], **kwargs): + return [GradyUserFactory().make_tutor(**tutor) + for tutor in tutors] + + +def make_reviewers(reviewers=[], **kwargs): + return [GradyUserFactory().make_reviewer(**reviewer) + for reviewer in reviewers] + + +def make_feedback(feedback, submission_object): + tutor = User.objects.get( + username=feedback['of_tutor']).get_associated_user() + return Feedback.objects.update_or_create( + of_submission=submission_object, + of_tutor=tutor, + defaults={ + 'text': feedback.get('text', ''), + 'score': feedback['score'] + })[0] + + +def make_submissions(submissions=[], **kwargs): + submission_objects = [] + 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( + username=submission.get('user', 'default_user') + )) + submission_object, _ = Submission.objects.get_or_create( + type=submission_type, student=student, defaults={ + 'seen_by_student': submission.get('seen_by_student', False), + 'text': submission.get('text', ''), + }) + if 'feedback' in submission: + make_feedback(submission['feedback'], submission_object) + submission_objects.append(submission_object) + return submission_objects + + +def make_test_data(data_dict): + return { + 'exams': make_exams(**data_dict), + 'submission_types': make_submission_types(**data_dict), + 'students': make_students(**data_dict), + 'tutors': make_tutors(**data_dict), + 'reviewers': make_reviewers(**data_dict), + 'submissions': make_submissions(**data_dict) + } + + +def init_test_instance(): + return make_test_data( + data_dict={ + 'exams': [{ + 'module_reference': 'Test Exam 01', + 'total_score': 100, + 'pass_score': 60, + }], + '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', + 'exam': 'Test Exam 01', + }], + 'tutors': [{ + 'username': 'tutor01' + }], + 'reviewers': [{ + 'username': 'reviewer01' + }], + 'submissions': [ + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' arrrgh\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '01. Sort this or that', + 'user': 'student01', + 'feedback': { + 'text': 'Not good!', + 'score': 5, + 'of_tutor': 'tutor01', + } + }, + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' arrrgh\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '02. Merge this or that or maybe even this', + 'user': 'student01', + 'feedback': { + 'text': 'A little bit better!', + 'score': 10, + 'of_tutor': 'tutor01', + }, + }, + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' arrrgh\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '03. This one exists for the sole purpose to test', + 'user': 'student01', + 'feedback': { + 'text': 'Awesome!', + 'score': 30, + 'of_tutor': 'tutor01', + }, + }, + ]} + )