diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0ec2cf3495f0e6cb2ed615ded22c234bd3140fab..2d62fe511bd1a7a860181ea7531b90a426d44536 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,7 @@
 stages:
         - build
         - test
+        - pages
         - staging
 
 variables:
@@ -23,21 +24,15 @@ build_backend:
         before_script:
                 - cd backend/
 
-test_coverage:
+test_pytest:
         <<: *test_definition_backend
         services:
                 - postgres:9.5
         script:
-                - coverage run manage.py test --noinput
-                - coverage report --skip-covered
+                - DJANGO_SETTINGS_MODULE=grady.settings pytest --cov
         artifacts:
                 paths:
-                - .coverage/
-
-test_pylint:
-        <<: *test_definition_backend
-        script:
-                - pylint core || exit 0
+                        - backend/.coverage
 
 test_prospector:
         <<: *test_definition_backend
@@ -56,7 +51,19 @@ test_frontend:
         script:
                 - yarn install
                 - yarn test --single-run
-        allow_failure: true
+
+# =========================== Gitlab pages section =========================== #
+test_coverage:
+        <<: *test_definition_backend
+        stage:
+                pages
+        script:
+                - coverage html -d ../public
+        dependencies:
+                - test_pytest
+        artifacts:
+                paths:
+                        - public
 
 # ============================== Staging section ============================= #
 .staging_template: &staging_definition
diff --git a/backend/.coveragerc b/backend/.coveragerc
index a7e7aac27a241c10fd195ca8bfb9b146b7cb18a3..f578fcfa0b500bed801d7aa3717a1036d669f5f2 100644
--- a/backend/.coveragerc
+++ b/backend/.coveragerc
@@ -1,13 +1,14 @@
 [run]
 branch = True
-source = core,util,grady
+source = core,util.factories
 omit =
     core/migrations/*
     core/apps.py
     core/admin.py
+    core/tests/*
 
 [report]
 ignore_errors = False
 
 [html]
-directory = coverage_html
+directory = public
diff --git a/backend/.gitignore b/backend/.gitignore
index 438bae1f35383978b491783079cc5ab16c0546a6..86ca4a4842208f80247e2f5f30648fa73444dabe 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -7,6 +7,7 @@ __pycache__
 MANIFEST
 .coverage
 cache/
+.mypy_cache/
 
 # Django specific
 dist/
@@ -23,5 +24,10 @@ static/
 env-grady/
 env/
 scripts/
+coverage_html/
+public/
 *.csv
 .importer*
+
+# node
+node_modules
diff --git a/backend/core/migrations/0001_initial.py b/backend/core/migrations/0001_initial.py
index 7fd6cdada7a80ed66d0adb09f55bb01c58a93272..5f840961f0accd6d68d03bb079d421e7f0322bcd 100644
--- a/backend/core/migrations/0001_initial.py
+++ b/backend/core/migrations/0001_initial.py
@@ -2,6 +2,8 @@
 # Generated by Django 1.11.7 on 2017-11-04 19:10
 from __future__ import unicode_literals
 
+from typing import List, Text
+
 import django.db.models.deletion
 from django.conf import settings
 from django.db import migrations, models
@@ -13,8 +15,7 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-    ]
+    dependencies: List[Text] = []
 
     operations = [
         migrations.CreateModel(
diff --git a/backend/core/migrations/0003_student_matrikel_no.py b/backend/core/migrations/0003_student_matrikel_no.py
index 0beae351c7c15a7419470e4709ed9112a2d1f2f1..82da5d1a338e2a4c3259ebdb431ee07dfe86fc03 100644
--- a/backend/core/migrations/0003_student_matrikel_no.py
+++ b/backend/core/migrations/0003_student_matrikel_no.py
@@ -2,9 +2,10 @@
 # Generated by Django 1.11.7 on 2017-11-10 21:46
 from __future__ import unicode_literals
 
-import core.models
 from django.db import migrations, models
 
+import core.models
+
 
 class Migration(migrations.Migration):
 
diff --git a/backend/core/models.py b/backend/core/models.py
index 1d1b23b19dbe7c77f4e9911035c85639578c74d3..76b024601d0fb85b549519d734f700d63d1ad2b6 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -6,6 +6,8 @@ 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
@@ -15,7 +17,7 @@ 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,
-                              Sum, When)
+                              QuerySet, Sum, When)
 from django.db.models.functions import Coalesce
 
 
@@ -28,7 +30,7 @@ def random_matrikel_no() -> str:
     return str(10_000_000 + randrange(90_000_000))
 
 
-def get_annotated_tutor_list():
+def get_annotated_tutor_list() -> QuerySet:
     """All tutor accounts are annotate with a field that includes the number of
     feedback that tutor has collaborated in.
 
@@ -105,7 +107,7 @@ class SubmissionType(models.Model):
         verbose_name_plural = "SubmissionType Set"
 
     @classmethod
-    def get_annotated_feedback_count(cls):
+    def get_annotated_feedback_count(cls) -> QuerySet:
         """ Annotates submission lists with counts
 
         The following fields are annotated:
@@ -146,7 +148,7 @@ class UserAccount(AbstractUser):
     fullname = models.CharField('full name', max_length=70, blank=True)
     is_admin = models.BooleanField(default=False)
 
-    def get_associated_user(self):
+    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 \
@@ -159,6 +161,9 @@ class Tutor(models.Model):
         get_user_model(), unique=True,
         on_delete=models.CASCADE, related_name='tutor')
 
+    def get_feedback_count(self) -> int:
+        return self.feedback_list.count()
+
 
 class Reviewer(models.Model):
     user = models.OneToOneField(
@@ -195,7 +200,7 @@ class Student(models.Model):
         get_user_model(), unique=True,
         on_delete=models.CASCADE, related_name='student')
 
-    def score_per_submission(self):
+    def score_per_submission(self) -> Dict[str, int]:
         """ TODO: get rid of it and use an annotation.
 
         Returns:
@@ -212,7 +217,7 @@ class Student(models.Model):
         })
 
     @classmethod
-    def get_overall_score_annotated_submission_list(cls):
+    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.
@@ -329,7 +334,7 @@ class Submission(models.Model):
         )
 
     @classmethod
-    def assign_tutor(cls, tutor, slug=None) -> bool:
+    def assign_tutor(cls, tutor: Tutor, slug: str=None) -> bool:
         """Assigns a tutor to a submission
 
         A submission is not assigned to the specified tutor in the case
@@ -491,14 +496,14 @@ class Feedback(models.Model):
     def __str__(self) -> str:
         return 'Feedback for {}'.format(self.of_submission)
 
-    def is_full_score(self):
+    def is_full_score(self) -> bool:
         return self.of_submission.type.full_score == self.score
 
-    def get_full_score(self):
+    def get_full_score(self) -> int:
         return self.of_submission.type.full_score
 
     @classmethod
-    def get_open_feedback(cls, user):
+    def get_open_feedback(cls, user: Union[Tutor, Reviewer]) -> QuerySet:
         """For a user, returns the feedback that is up for reassignment that
         does not belong to the user.
 
@@ -521,7 +526,7 @@ class Feedback(models.Model):
         )
 
     @classmethod
-    def tutor_unfinished_feedback(cls, user):
+    def tutor_unfinished_feedback(cls, user: Union[Tutor, Reviewer]):
         """Gets only the feedback that is assigned and not accepted. A tutor
         should have only one feedback assigned that is not accepted
 
@@ -539,7 +544,7 @@ class Feedback(models.Model):
         )
         return tutor_feedback[0] if tutor_feedback else None
 
-    def tutor_assigned_feedback(cls, user):
+    def tutor_assigned_feedback(cls, user: Union[Tutor, Reviewer]):
         """Gets all feedback that is assigned to the tutor including
         all status cases.
 
@@ -555,7 +560,7 @@ class Feedback(models.Model):
         tutor_feedback = cls.objects.filter(of_tutor=user)
         return tutor_feedback
 
-    def finalize_feedback(self, user):
+    def finalize_feedback(self, user: Union[Tutor, Reviewer]):
         """Used to mark feedback as accepted (reviewed).
 
         Parameters
@@ -567,17 +572,7 @@ class Feedback(models.Model):
         self.of_reviewer = user
         self.save()
 
-    def unfinalize_feedback(self):
-        """Used to mark feedback as accepted (reviewed)
-
-        This makes it uneditable by the tutor.
-        """
-
-        self.origin = Feedback.MANUAL
-        self.of_reviewer = None
-        self.save()
-
-    def reassign_to_tutor(self, user):
+    def reassign_to_tutor(self, user: Union[Tutor, Reviewer]):
         """When a tutor does not want to correct some task they can pass it
         along to another tutor who will accept the request.
 
diff --git a/backend/core/permissions.py b/backend/core/permissions.py
index 3418f6dd13ad69513594f22ef6612261614d72fd..29b60731037a3d58727b44826b189de4b8f6217e 100644
--- a/backend/core/permissions.py
+++ b/backend/core/permissions.py
@@ -1,9 +1,37 @@
 from rest_framework import permissions
 
-from core.models import Student
+from core.models import Reviewer, Student, Tutor
+from django.http import HttpRequest
+from django.views import View
 
 
-class IsStudent(permissions.BasePermission):
-    def has_permission(self, request, view):
+class IsUserGenericPermission(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 model"""
+        assert self.model is not None, (
+            "'%s' has to include a `model` attribute"
+            % self.__class__.__name__
+        )
+
         user = request.user
-        return user.is_authenticated() and isinstance(user.get_associated_user(), Student)
+        return user.is_authenticated() and isinstance(
+            user.get_associated_user(), self.model
+        )
+
+
+class IsStudent(IsUserGenericPermission):
+    """ Has student permissions """
+    model = Student
+
+
+class IsReviewer(IsUserGenericPermission):
+    """ Has reviewer permissions """
+    model = Reviewer
+
+
+class IsTutor(IsUserGenericPermission):
+    """ Has tutor permissions """
+    model = Tutor
diff --git a/backend/core/serializers.py b/backend/core/serializers.py
index 8178ecfae6f841340d82c7e9afada2bee9dd3c52..afccb1bb6212bc3edd35c18680e022f464fb028e 100644
--- a/backend/core/serializers.py
+++ b/backend/core/serializers.py
@@ -1,6 +1,12 @@
+import logging
+
 from rest_framework import serializers
 
-from core.models import ExamType, Feedback, Student, Submission
+from core.models import ExamType, Feedback, Student, Submission, Tutor
+from util.factories import GradyUserFactory
+
+log = logging.getLogger(__name__)
+user_factory = GradyUserFactory()
 
 
 class ExamSerializer(serializers.ModelSerializer):
@@ -38,3 +44,18 @@ class StudentSerializer(serializers.ModelSerializer):
     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',
+                                              read_only=True)
+
+    def create(self, validated_data) -> Tutor:
+        log.info("Crating tutor from data %s", validated_data)
+        return user_factory.make_tutor(
+            username=validated_data['user']['username'])
+
+    class Meta:
+        model = Tutor
+        fields = ('username', 'feedback_count')
diff --git a/backend/core/tests/__init__.py b/backend/core/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/backend/core/tests/data_factories.py b/backend/core/tests/data_factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..25e5ca71f2c33bc70e2204987b3b4c9ceb506204
--- /dev/null
+++ b/backend/core/tests/data_factories.py
@@ -0,0 +1,84 @@
+""" 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/backend/core/tests/test_access_rights.py b/backend/core/tests/test_access_rights.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff9857d688f1ebde249371bbabf497c8a0743ad2
--- /dev/null
+++ b/backend/core/tests/test_access_rights.py
@@ -0,0 +1,44 @@
+from django.urls import reverse
+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 util.factories import GradyUserFactory
+
+
+class AccessRightsOfStudentAPIViewTests(APITestCase):
+    """ All tests that enshure that only students can see what students
+    should see belong here """
+
+    @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-page'))
+        self.view = StudentApiView.as_view()
+
+    def test_unauthorized_access_denied(self):
+        response = self.view(self.request)
+        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    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_no_access(self):
+        force_authenticate(self.request, user=self.reviewer.user)
+        response = self.view(self.request)
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_student_is_authorized(self):
+        force_authenticate(self.request, user=self.student.user)
+        response = self.view(self.request)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/backend/core/tests/test_auth.py b/backend/core/tests/test_auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..85505585b10b0362b4f851f1ea16c3a88a5e3987
--- /dev/null
+++ b/backend/core/tests/test_auth.py
@@ -0,0 +1,22 @@
+from rest_framework.test import APIClient, APITestCase
+
+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.set_password(cls.credentials['password'])
+        cls.user.save()
+        cls.client = APIClient()
+
+    def test_get_token(self):
+        response = self.client.post('/api-token-auth/', self.credentials)
+        self.assertContains(response, 'token')
+
+    def test_refresh_token(self):
+        token = self.client.post('/api-token-auth/', self.credentials).data
+        response = self.client.post('/api-token-refresh/', token)
+        self.assertContains(response, 'token')
diff --git a/backend/core/tests/test_examlist.py b/backend/core/tests/test_examlist.py
new file mode 100644
index 0000000000000000000000000000000000000000..51d55a8b9e9fe2972dd881dfabbafb26035092b9
--- /dev/null
+++ b/backend/core/tests/test_examlist.py
@@ -0,0 +1,32 @@
+""" Tests that we can receive information about what exams where written """
+
+from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import (APIRequestFactory, APITestCase,
+                                 force_authenticate)
+
+from core.models import ExamType
+from core.views import ExamListView
+from util.factories import GradyUserFactory
+
+NUMBER_OF_TUTORS = 7
+
+
+class ExamListTest(APITestCase):
+
+    @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.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))
diff --git a/backend/core/tests/test_student_page.py b/backend/core/tests/test_student_page.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b15fb974d92ec1319df33e3abfa017cd0d261ea
--- /dev/null
+++ b/backend/core/tests/test_student_page.py
@@ -0,0 +1,91 @@
+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
+
+
+class StudentPageTests(APITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        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.request = self.factory.get(reverse('student-page'))
+        self.view = StudentApiView.as_view()
+
+        force_authenticate(self.request, user=self.student.user)
+        self.response = self.view(self.request)
+
+        self.exam_obj = self.response.data['exam']
+        self.submission_list = self.response.data['submissions']
+        self.submission_list_first_entry = self.submission_list[0]
+
+    def test_student_information_contains_name(self):
+        self.assertEqual(
+            self.response.data['name'], self.student.user.fullname)
+
+    def test_student_contains_associated_user(self):
+        self.assertEqual(
+            self.response.data['user'], self.student.user.username)
+
+    def test_all_student_submissions_are_loded(self):
+        self.assertEqual(len(self.submission_list),
+                         SubmissionType.objects.count())
+
+    # Tests concerning exam data
+    def test_exam_data_contains_module_reference(self):
+        self.assertEqual(
+            self.exam_obj["module_reference"],
+            self.student.exam.module_reference)
+
+    def test_exam_data_contains_total_score(self):
+        self.assertEqual(
+            self.exam_obj["total_score"], self.student.exam.total_score)
+
+    def test_exam_data_contains_pass_score(self):
+        self.assertEqual(
+            self.exam_obj["pass_score"], self.student.exam.pass_score)
+
+    def test_exam_data_contains_pass_only_field(self):
+        self.assertEqual(
+            self.exam_obj["pass_only"], self.student.exam.pass_only)
+
+    # Tests concerning submission data
+    def test_a_student_submissions_contains_type(self):
+        self.assertEqual(
+            self.submission_list_first_entry['type'],
+            self.student.submissions.first().type.name)
+
+    def test_submission_data_contains_text(self):
+        self.assertEqual(
+            self.submission_list_first_entry['text'],
+            self.student.submissions.first().text)
+
+    def test_submission_data_contains_feedback(self):
+        self.assertEqual(
+            self.submission_list_first_entry['feedback'],
+            self.student.submissions.first().feedback.text)
+
+    def test_submission_data_contains_score(self):
+        self.assertEqual(
+            self.submission_list_first_entry['score'],
+            self.student.submissions.first().feedback.score)
+
+    def test_submission_data_contains_full_score(self):
+        self.assertEqual(
+            self.submission_list_first_entry['full_score'],
+            self.student.submissions.first().type.full_score)
+
+    # We don't want a matriculation number here
+    def test_matriculation_number_is_not_senf(self):
+        self.assertNotIn('matrikel_no', self.submission_list_first_entry)
diff --git a/backend/core/tests/test_tutor_api_endpoints.py b/backend/core/tests/test_tutor_api_endpoints.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba21ab11dae10bcef8f918afbb97ed66fe46b9bd
--- /dev/null
+++ b/backend/core/tests/test_tutor_api_endpoints.py
@@ -0,0 +1,107 @@
+""" Two api endpoints are currently planned
+
+    * GET /tutor/:id to retrive information about some tutor
+    * 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 rest_framework import status
+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 util.factories import GradyUserFactory
+
+NUMBER_OF_TUTORS = 7
+
+
+@skip
+class TutorListTests(APITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.factory = APIRequestFactory()
+        cls.user_factory = GradyUserFactory()
+
+    def setUp(self):
+        self.tutor_list = [self.user_factory.make_tutor()
+                           for _ in range(NUMBER_OF_TUTORS)]
+        self.reviewer = self.user_factory.make_reviewer()
+        self.request = self.factory.get(reverse('tutor-list'))
+        self.view = TutorListApiView.as_view()
+
+        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_get_a_list_of_all_tutors(self):
+        self.assertEqual(len(self.response.data), NUMBER_OF_TUTORS)
+
+    def test_feedback_count_matches_database(self):
+        def verify_fields(tutor_obj):
+            t = Tutor.objects.get(user__username=tutor_obj['username'])
+            return t.get_feedback_count() == tutor_obj['feedback_count']
+
+        self.assertTrue(all(map(verify_fields, self.response.data)))
+
+    def test_sum_of_feedback_count(self):
+        self.assertEqual(sum(obj['feedback_count']
+                             for obj in self.response.data),
+                         Feedback.objects.count())
+
+
+class TutorCreateTests(APITestCase):
+
+    USERNAME = 'some weird name!'
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.factory = APIRequestFactory()
+        cls.user_factory = GradyUserFactory()
+
+    def setUp(self):
+        self.reviewer = self.user_factory.make_reviewer()
+        self.request = self.factory.post(reverse('tutor-create'),
+                                         {'username': self.USERNAME})
+        self.view = TutorCreateView.as_view()
+
+        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_201_CREATED)
+
+    def test_can_create(self):
+        self.assertEqual(Tutor.objects.first().user.username, self.USERNAME)
+
+# @skip("Doesn't work for dubious reasons")
+
+
+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='fetter.otto')
+        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'})
+        self.response = self.client.get(url, format='json')
+
+    def test_can_access(self):
+        self.assertEqual(self.response.status_code, status.HTTP_200_OK)
+
+    def test_can_view_tutor(self):
+        self.assertEqual(self.response.data['username'],
+                         self.tutor.user.username)
diff --git a/backend/core/tests.py b/backend/core/tests/tests.py
similarity index 98%
rename from backend/core/tests.py
rename to backend/core/tests/tests.py
index f3290fbe0132092e58ed50fa7db0e04406b9b5e3..408ebb753ada91819d0989cb32a3fdc336db9a9a 100644
--- a/backend/core/tests.py
+++ b/backend/core/tests/tests.py
@@ -2,7 +2,7 @@ from django.test import TestCase
 
 from core.models import (Feedback, Reviewer, Student, Submission,
                          SubmissionType, Tutor)
-from util.importer import GradyUserFactory
+from util.factories import GradyUserFactory
 
 
 class FeedbackTestCase(TestCase):
diff --git a/backend/core/urls.py b/backend/core/urls.py
index f9b3345c6525e001776fd15597397f103d2dcf9b..72e447a4c83e6d6b2813a552973dd7a3e36ef3de 100644
--- a/backend/core/urls.py
+++ b/backend/core/urls.py
@@ -1,15 +1,20 @@
 from django.conf.urls import url
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from rest_framework_jwt.views import obtain_jwt_token
+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()),
-    url(r'^api/student/submission/(?P<pk>[0-9]+)$', views.SubmissionApiView.as_view()),
-    url(r'^api/student/submission/(?P<pk>[0-9]+)/feedback/$', views.FeedbackApiView.as_view()),
+    url(r'^api/student/$', views.StudentApiView.as_view(), name='student-page'),
 
-    url(r'^api-token-auth/', obtain_jwt_token)
+    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'),
+
+    url(r'^api-token-auth/', obtain_jwt_token),
+    url(r'^api-token-refresh', refresh_jwt_token),
 ]
 
 urlpatterns += staticfiles_urlpatterns()
diff --git a/backend/core/views.py b/backend/core/views.py
index 2932ec09bb16aebe08c22bfdfa567ca288d737fd..4470a4ab59e360f0ef30fa6c37d1f850e4cf6aac 100644
--- a/backend/core/views.py
+++ b/backend/core/views.py
@@ -1,35 +1,51 @@
+""" 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.generics import RetrieveAPIView
+from rest_framework import generics
 
-from core.permissions import IsStudent
-from core.serializers import (FeedbackSerializer, StudentSerializer,
-                              SubmissionSerializer)
+from core.models import ExamType, Tutor, Student
+from core.permissions import IsReviewer, IsStudent
+from core.serializers import ExamSerializer, StudentSerializer, TutorSerializer
 
 log = logging.getLogger(__name__)
 
 
-
-class StudentApiView(RetrieveAPIView):
+class StudentApiView(generics.RetrieveAPIView):
+    """ Gets all data that belongs to one student """
     permission_classes = (IsStudent,)
+    serializer_class = StudentSerializer
 
-    def get_object(self):
-        log.debug("Serializing student of user '%s'", self.request.user.username)
+    def get_object(self) -> Student:
+        """ 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
-    serializer_class = StudentSerializer
 
 
-class SubmissionApiView(RetrieveAPIView):
-    permission_classes = (IsStudent,)
+class TutorListApiView(generics.ListAPIView):
+    """ A list of all tutors with information about what they corrected """
+    permission_classes = (IsReviewer,)
+    queryset = Tutor.objects.all()
+    serializer_class = TutorSerializer
 
-    def get_queryset(self):
-        return self.request.user.student.submissions
-    serializer_class = SubmissionSerializer
 
+class TutorCreateView(generics.CreateAPIView):
+    """ Creates a Tutor instance currently without a password """
+    permission_classes = (IsReviewer,)
+    serializer_class = TutorSerializer
+
+
+class ExamListView(generics.ListAPIView):
+    """ Gets a list of all exams available. List might be empty """
+    queryset = ExamType.objects.all()
+    serializer_class = ExamSerializer
 
-class FeedbackApiView(RetrieveAPIView):
-    permission_classes = (IsStudent,)
 
-    def get_queryset(self):
-        return [submission.feedback for submission in self.request.user.submissions]
-    serializer_class = FeedbackSerializer
+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()
diff --git a/backend/delbert.py b/backend/delbert.py
old mode 100644
new mode 100755
index 557260f5c0a257d7d76e6fbfca6304aae899b10c..4e05dabd289dca8aa829315cb3e21ab923eca288
--- a/backend/delbert.py
+++ b/backend/delbert.py
@@ -2,20 +2,22 @@ import argparse
 import csv
 import json
 import os
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'grady.settings')
 import secrets
 import sys
 
 import django
+django.setup()
 from django.contrib.auth.models import User
 
 import util.importer
 from core.models import Student, Submission
 
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'grady.settings')
 
-django.setup()
 
 
+unused_variable =  []
+
 def parseme():
     parser      = argparse.ArgumentParser()
     subparsers  = parser.add_subparsers(dest="command")
@@ -83,7 +85,7 @@ def parseme():
     return parser.parse_args()
 
 
-def handle_passwordlist(output=sys.stdout, instance=""):
+def handle_passwordlist(output=sys.stdout, instance="", **kwargs):
     with open('/usr/share/dict/words') as words:
         choose_from = list({word.strip().lower()
                             for word in words if 5 < len(word) < 8})
@@ -101,7 +103,7 @@ def handle_passwordlist(output=sys.stdout, instance=""):
                          student.user.username, password, instance])
 
 
-def handle_enableusers(switch, exclude, include):
+def handle_enableusers(switch, exclude, include, **kwargs):
 
     if include:
         for user in User.objects.filter(username__in=include):
@@ -113,7 +115,7 @@ def handle_enableusers(switch, exclude, include):
             user.save()
 
 
-def handle_replaceusernames(matno2username_dict):
+def handle_replaceusernames(matno2username_dict, **kwargs):
     matno2username = json.JSONDecoder().decode(matno2username_dict.read())
     for student in Student.objects.all():
         if student.matrikel_no in matno2username:
@@ -122,12 +124,12 @@ def handle_replaceusernames(matno2username_dict):
             student.user.save()
 
 
-def handle_extractsubmissions():
+def handle_extractsubmissions(output, **kwargs):
     for submission in Submission.objects.filter(feedback__isnull=False).order_by('type'):
         print(submission.feedback.score, repr(submission.text), file=open(str(submission.type).replace(' ', '_'), 'a'))
 
 
-def handle_importer():
+def handle_importer(**kwargs):
     util.importer.start()
 
 def main():
diff --git a/backend/grady/settings/default.py b/backend/grady/settings/default.py
index 86e0c94956f52ceb7b459c8d8e015d73c4825a6c..a4a5fd8ef3c1be98356143de28bf1b12723a38e2 100644
--- a/backend/grady/settings/default.py
+++ b/backend/grady/settings/default.py
@@ -154,10 +154,12 @@ REST_FRAMEWORK = {
         'rest_framework.authentication.SessionAuthentication',
         'rest_framework.authentication.BasicAuthentication',
     ),
+    'TEST_REQUEST_DEFAULT_FORMAT': 'json',
 }
 
 JWT_AUTH = {
     'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=600),
+    'JWT_ALLOW_REFRESH': True,
 }
 
 LOGGING = {
diff --git a/backend/grady/urls.py b/backend/grady/urls.py
index 9acdd9b0e47126124006bc7b2ee0445a4388a42f..95d879bbd63467ba2d213c45201a3227139e0b78 100644
--- a/backend/grady/urls.py
+++ b/backend/grady/urls.py
@@ -18,10 +18,8 @@ from django.contrib import admin
 
 urlpatterns = [
     url(r'^admin/', admin.site.urls),
-    url(r'^', include('core.urls'))
-]
+    url(r'^', include('core.urls')),
 
-urlpatterns += [
     url(r'^api-auth/', include('rest_framework.urls',
-                               namespace='rest_framework')),
+        namespace='rest_framework')),
 ]
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 9f84b378ab58611f325b862ad2fb964b3d9144cd..bd5a9cac3be9f45f6dd82e0c1ebb5032c36423c4 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -2,11 +2,10 @@ Django~=1.11.3
 django-extensions~=1.7.7
 djangorestframework~=3.6.3
 djangorestframework-jwt~=1.11.0
-django_compressor~=2.1.1
+django-cors-headers~=2.1.0
 gunicorn~=19.7.0
 psycopg2~=2.7.1
 xlrd~=1.0.0
 pytest-cov~=2.5.1
+pytest-django~=3.1.2
 prospector~=0.12.7
-django-cors-headers~=2.1.0
-pre-commit~=1.4.1
\ No newline at end of file
diff --git a/backend/util/factories.py b/backend/util/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..32c2d67fc03200556e3a44015bdf9525f7ff93b0
--- /dev/null
+++ b/backend/util/factories.py
@@ -0,0 +1,112 @@
+import configparser
+import secrets
+
+from core.models import UserAccount as User, Student, Tutor, Reviewer
+
+STUDENTS = 'students'
+TUTORS = 'tutors'
+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 store_password(username, groupname, password):
+    storage = configparser.ConfigParser()
+    storage.read(PASSWORDS)
+
+    if not groupname in storage:
+        storage[groupname] = {}
+
+    storage[groupname][username] = password
+
+    with open(PASSWORDS, 'w') as passwd_file:
+        storage.write(passwd_file)
+
+
+class GradyUserFactory:
+
+    def __init__(self,
+                 password_generator_func=get_xkcd_password,
+                 password_storge=store_password,
+                 *args, **kwargs):
+        self.password_generator_func = password_generator_func
+        self.password_storge = password_storge
+
+    @staticmethod
+    def _get_random_name(prefix='', suffix='', k=1):
+        return ''.join((prefix, get_xkcd_password(k), suffix))
+
+    def _make_base_user(self, username, groupname, 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.
+
+        Returns:
+            (User object, str): The user object that was added to the group and
+            the password of that user if it was created.
+        """
+        username = username.strip()
+
+        user, created = User.objects.update_or_create(
+            username=username,
+            defaults=kwargs)
+
+        if created:
+            password = self.password_generator_func()
+            user.set_password(password)
+            user.save()
+
+        if created and store_pw:
+            self.password_storge(username, groupname, 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)
+        if matrikel_no:
+            user.objects.update(matrikel_no=matrikel_no)
+        if exam:
+            user.objects.update(exam=exam)
+        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)
+
+    def make_reviewer(self, username=None, **kwargs):
+        """ Creates or updates a reviewer if needed with defaults """
+        return self._make_user_generic(username, REVIEWERS, **kwargs)
diff --git a/backend/util/importer.py b/backend/util/importer.py
index 299f4ae547c1b8e1a8a9dcd2568bf3ca29d0b407..f9fed844f9d36970a105c7191533ad38f9b94998 100644
--- a/backend/util/importer.py
+++ b/backend/util/importer.py
@@ -1,9 +1,7 @@
-import configparser
 import csv
 import json
 import os
 import readline
-import secrets
 from typing import Callable
 
 import util.convert
@@ -14,9 +12,7 @@ from core.models import (ExamType, Feedback, Reviewer, Student, Submission,
 from util.messages import info, warn
 from util.processing import EmptyTest
 
-STUDENTS = 'students'
-TUTORS = 'tutors'
-REVIEWERS = 'reviewers'
+from util.factories import STUDENTS, REVIEWERS, TUTORS, GradyUserFactory
 
 HISTFILE = '.importer_history'
 RECORDS = '.importer'
@@ -62,14 +58,6 @@ class chdir_context(object):
         os.chdir(self.old_dir)
 
 
-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 i(prompt: str, default: str='', is_path: bool=False, is_file: bool=False):
     if default is YES or default is NO:
         answer = valid[input(f'[Q] {prompt} ({default}): ').lower() or ('y' if YES == default else 'n')]
@@ -84,96 +72,6 @@ def i(prompt: str, default: str='', is_path: bool=False, is_file: bool=False):
 
     return answer
 
-
-def store_password(username, groupname, password):
-    storage = configparser.ConfigParser()
-    storage.read(PASSWORDS)
-
-    if not groupname in storage:
-        storage[groupname] = {}
-
-    storage[groupname][username] = password
-
-    with open(PASSWORDS, 'w') as passwd_file:
-        storage.write(passwd_file)
-
-
-class GradyUserFactory:
-
-    def __init__(self, password_generator_func=get_xkcd_password, *args, **kwargs):
-        self.password_generator_func = password_generator_func
-
-    @staticmethod
-    def _get_random_name(prefix='', suffix='', k=1):
-        return ''.join((prefix, get_xkcd_password(k), suffix))
-
-    def _make_base_user(self, username, groupname, 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.
-
-        Returns:
-            (User object, str): The user object that was added to the group and
-            the password of that user if it was created.
-        """
-        username = username.strip()
-
-        user, created = User.objects.update_or_create(
-            username=username,
-            defaults=kwargs)
-
-        if created:
-            password = self.password_generator_func()
-            user.set_password(password)
-            user.save()
-
-        if created and store_pw:
-            store_password(username, groupname, 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)
-        if matrikel_no:
-            user.objects.update(matrikel_no=matrikel_no)
-        if exam:
-            user.objects.update(exam=exam)
-        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)
-
-    def make_reviewer(self, username=None, **kwargs):
-        """ Creates or updates a reviewer if needed with defaults """
-        return self._make_user_generic(username, REVIEWERS, **kwargs)
-
 # TODO more factories
 
 def add_user(username, group, **kwargs):