diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 79e0ef5cb2bb5128b0833044a30414cd2a3700e2..65006d6e9ebf4d8d02d84d646297796b0ce26c3c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -40,16 +40,19 @@ test_pytest:
   services:
     - postgres:9.5
   script:
-    - DJANGO_SETTINGS_MODULE=grady.settings pytest --cov
+    - pytest --cov --ds=grady.settings core/tests
   artifacts:
     paths:
       - .coverage
+  cache:
+    paths:
+      - .coverage
 
 test_flake8:
   <<: *test_definition_virtualenv
   stage: test
   script:
-    - flake8 --exclude=migrations --ignore=N802 core
+    - flake8 --exclude=migrations --ignore=N802 core util/factories.py
 
 # ----------------------------- Frontend subsection -------------------------- #
 .test_template_frontend: &test_definition_frontend
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
index 5f840961f0accd6d68d03bb079d421e7f0322bcd..e28dd8e66f31f89a128f223316757865b760c6b9 100644
--- a/core/migrations/0001_initial.py
+++ b/core/migrations/0001_initial.py
@@ -1,10 +1,13 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.11.7 on 2017-11-04 19:10
+# Generated by Django 1.11.8 on 2017-12-22 10:51
 from __future__ import unicode_literals
 
-from typing import List, Text
+import uuid
 
+import django.contrib.auth.models
+import django.contrib.auth.validators
 import django.db.models.deletion
+import django.utils.timezone
 from django.conf import settings
 from django.db import migrations, models
 
@@ -15,7 +18,9 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies: List[Text] = []
+    dependencies = [
+        ('auth', '0008_alter_user_username_max_length'),
+    ]
 
     operations = [
         migrations.CreateModel(
@@ -24,16 +29,27 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('password', models.CharField(max_length=128, verbose_name='password')),
                 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
-                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=150, unique=True)),
+                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+                ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
+                ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
+                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                 ('fullname', models.CharField(blank=True, max_length=70, verbose_name='full name')),
-                ('is_staff', models.BooleanField(default=False, verbose_name='staff status')),
                 ('is_admin', models.BooleanField(default=False)),
-                ('is_superuser', models.BooleanField(default=False)),
-                ('is_active', models.BooleanField(default=True, verbose_name='active')),
+                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
+                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
             ],
             options={
+                'verbose_name': 'user',
+                'verbose_name_plural': 'users',
                 'abstract': False,
             },
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+            ],
         ),
         migrations.CreateModel(
             name='ExamType',
@@ -57,7 +73,6 @@ class Migration(migrations.Migration):
                 ('score', models.PositiveIntegerField(default=0)),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('modified', models.DateTimeField(auto_now=True)),
-                ('status', models.IntegerField(choices=[(0, 'editable'), (1, 'request reassignment'), (2, 'request review'), (3, 'accepted')], default=0)),
                 ('origin', models.IntegerField(choices=[(0, 'was empty'), (1, 'passed unittests'), (2, 'did not compile'), (3, 'could not link'), (4, 'created by a human. yak!')], default=4)),
             ],
             options={
@@ -65,6 +80,15 @@ class Migration(migrations.Migration):
                 'verbose_name_plural': 'Feedback Set',
             },
         ),
+        migrations.CreateModel(
+            name='GeneralTaskSubscription',
+            fields=[
+                ('subscription_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('query_key', models.CharField(blank=True, max_length=75)),
+                ('query_type', models.CharField(choices=[('random', 'Query for any submission'), ('student', 'Query for submissions of student'), ('exam', 'Query for submissions of exam type'), ('submission_type', 'Query for submissions of submissions_type')], default='random', max_length=75)),
+                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='susbscriptions', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
         migrations.CreateModel(
             name='Reviewer',
             fields=[
@@ -77,6 +101,7 @@ class Migration(migrations.Migration):
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('has_logged_in', models.BooleanField(default=False)),
+                ('matrikel_no', models.CharField(default=core.models.random_matrikel_no, max_length=8, unique=True)),
                 ('exam', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='core.ExamType')),
                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL)),
             ],
@@ -88,7 +113,7 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='Submission',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('submission_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
                 ('seen_by_student', models.BooleanField(default=False)),
                 ('text', models.TextField(blank=True)),
                 ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='core.Student')),
@@ -134,6 +159,15 @@ class Migration(migrations.Migration):
                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tutor', to=settings.AUTH_USER_MODEL)),
             ],
         ),
+        migrations.CreateModel(
+            name='TutorSubmissionAssignment',
+            fields=[
+                ('assignment_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('active', models.BooleanField(default=False)),
+                ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='assignment', to='core.Submission')),
+                ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='core.GeneralTaskSubscription')),
+            ],
+        ),
         migrations.AddField(
             model_name='submission',
             name='type',
@@ -162,4 +196,8 @@ class Migration(migrations.Migration):
             name='submission',
             unique_together=set([('type', 'student')]),
         ),
+        migrations.AlterUniqueTogether(
+            name='generaltasksubscription',
+            unique_together=set([('owner', 'query_key', 'query_type')]),
+        ),
     ]
diff --git a/core/migrations/0002_auto_20171110_1612.py b/core/migrations/0002_auto_20171110_1612.py
deleted file mode 100644
index d7eba8e70eb18ecc8e279c2dac007def36d06a48..0000000000000000000000000000000000000000
--- a/core/migrations/0002_auto_20171110_1612.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.7 on 2017-11-10 16:12
-from __future__ import unicode_literals
-
-import django.contrib.auth.models
-import django.contrib.auth.validators
-import django.utils.timezone
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('auth', '0008_alter_user_username_max_length'),
-        ('core', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='useraccount',
-            options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
-        ),
-        migrations.AlterModelManagers(
-            name='useraccount',
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-            ],
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='date_joined',
-            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'),
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='email',
-            field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='first_name',
-            field=models.CharField(blank=True, max_length=30, verbose_name='first name'),
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='groups',
-            field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='last_name',
-            field=models.CharField(blank=True, max_length=30, verbose_name='last name'),
-        ),
-        migrations.AddField(
-            model_name='useraccount',
-            name='user_permissions',
-            field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
-        ),
-        migrations.AlterField(
-            model_name='useraccount',
-            name='is_active',
-            field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'),
-        ),
-        migrations.AlterField(
-            model_name='useraccount',
-            name='is_staff',
-            field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
-        ),
-        migrations.AlterField(
-            model_name='useraccount',
-            name='is_superuser',
-            field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'),
-        ),
-        migrations.AlterField(
-            model_name='useraccount',
-            name='username',
-            field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
-        ),
-    ]
diff --git a/core/migrations/0002_auto_20171222_1116.py b/core/migrations/0002_auto_20171222_1116.py
new file mode 100644
index 0000000000000000000000000000000000000000..47abe7a7b5475c58f9a08436e87ccdb6b7248c1c
--- /dev/null
+++ b/core/migrations/0002_auto_20171222_1116.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0 on 2017-12-22 11:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='useraccount',
+            name='last_name',
+            field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
+        ),
+    ]
diff --git a/core/migrations/0003_auto_20180104_1631.py b/core/migrations/0003_auto_20180104_1631.py
new file mode 100644
index 0000000000000000000000000000000000000000..f87e50ddd8cae36e6389429e69527224d3c20df3
--- /dev/null
+++ b/core/migrations/0003_auto_20180104_1631.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.1 on 2018-01-04 16:31
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0002_auto_20171222_1116'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='tutorsubmissionassignment',
+            old_name='active',
+            new_name='is_done',
+        ),
+    ]
diff --git a/core/migrations/0003_student_matrikel_no.py b/core/migrations/0003_student_matrikel_no.py
deleted file mode 100644
index 82da5d1a338e2a4c3259ebdb431ee07dfe86fc03..0000000000000000000000000000000000000000
--- a/core/migrations/0003_student_matrikel_no.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.7 on 2017-11-10 21:46
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-import core.models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0002_auto_20171110_1612'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='student',
-            name='matrikel_no',
-            field=models.CharField(default=core.models.random_matrikel_no, max_length=8, unique=True),
-        ),
-    ]
diff --git a/core/migrations/0004_feedback_is_final.py b/core/migrations/0004_feedback_is_final.py
new file mode 100644
index 0000000000000000000000000000000000000000..7aa6f9ca74835c06330aaf25d5a583856a56df77
--- /dev/null
+++ b/core/migrations/0004_feedback_is_final.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.0.1 on 2018-01-04 16:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0003_auto_20180104_1631'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='feedback',
+            name='is_final',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/core/migrations/0005_auto_20180104_1851.py b/core/migrations/0005_auto_20180104_1851.py
new file mode 100644
index 0000000000000000000000000000000000000000..96eea1d25f454a02c8d1116615109e758b03d125
--- /dev/null
+++ b/core/migrations/0005_auto_20180104_1851.py
@@ -0,0 +1,26 @@
+# Generated by Django 2.0.1 on 2018-01-04 18:51
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0004_feedback_is_final'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='tutorsubmissionassignment',
+            name='created',
+            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='tutorsubmissionassignment',
+            name='submission',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='core.Submission'),
+        ),
+    ]
diff --git a/core/migrations/0006_auto_20180104_2001.py b/core/migrations/0006_auto_20180104_2001.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a1672479bf09f7629b13877cba53a6183003e49
--- /dev/null
+++ b/core/migrations/0006_auto_20180104_2001.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.0.1 on 2018-01-04 20:01
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0005_auto_20180104_1851'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='feedback',
+            name='of_tutor',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedback_list', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/core/models.py b/core/models.py
index 966b647d3b5aad0c0a5d6759fbddc715aa703870..1f7d19274a567ae7864806d15039ce25c11e8054 100644
--- a/core/models.py
+++ b/core/models.py
@@ -6,17 +6,21 @@ See docstring of the individual models for information on the setup of the
 database.
 '''
 
+import logging
+import uuid
 from collections import OrderedDict
 from random import randrange
-from typing import Dict, Union
+from typing import Dict
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AbstractUser
-from django.db import models
+from django.db import models, transaction
 from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q,
                               QuerySet, Sum, Value, When)
 from django.db.models.functions import Coalesce
 
+log = logging.getLogger(__name__)
+
 
 def random_matrikel_no() -> str:
     """Use as a default value for student's matriculation number.
@@ -152,20 +156,29 @@ class UserAccount(AbstractUser):
             (hasattr(self, 'reviewer') and self.reviewer) or \
             (hasattr(self, 'tutor') and self.tutor)
 
+    def is_reviewer(self):
+        return hasattr(self, 'reviewer')
+
+    def is_tutor(self):
+        return hasattr(self, 'tutor')
+
+    def is_student(self):
+        return hasattr(self, 'student')
+
 
 class Tutor(models.Model):
-    user = models.OneToOneField(
-        get_user_model(), unique=True,
-        on_delete=models.CASCADE, related_name='tutor')
+    user = models.OneToOneField(get_user_model(),
+                                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(
-        get_user_model(), unique=True,
-        on_delete=models.CASCADE, related_name='reviewer')
+    user = models.OneToOneField(get_user_model(),
+                                on_delete=models.CASCADE,
+                                related_name='reviewer')
 
 
 class Student(models.Model):
@@ -188,14 +201,16 @@ class Student(models.Model):
             The matriculation number of the student
     """
     has_logged_in = models.BooleanField(default=False)
-    matrikel_no = models.CharField(
-        unique=True, max_length=8, default=random_matrikel_no)
-    exam = models.ForeignKey(
-        'ExamType', on_delete=models.SET_NULL,
-        related_name='students', null=True)
-    user = models.OneToOneField(
-        get_user_model(), unique=True,
-        on_delete=models.CASCADE, related_name='student')
+    matrikel_no = models.CharField(unique=True,
+                                   max_length=8,
+                                   default=random_matrikel_no)
+    exam = models.ForeignKey('ExamType',
+                             on_delete=models.SET_NULL,
+                             related_name='students',
+                             null=True)
+    user = models.OneToOneField(get_user_model(),
+                                on_delete=models.CASCADE,
+                                related_name='student')
 
     def score_per_submission(self) -> Dict[str, int]:
         """ TODO: get rid of it and use an annotation.
@@ -255,7 +270,7 @@ class Student(models.Model):
 
 
 class Test(models.Model):
-    """Tests contain information that has been generated by automated tests,
+    """Tests contain information that has been unapproved by automated tests,
     and directly belongs to a submission. Often certain Feedback was already
     given by information provided by these tests.
 
@@ -268,16 +283,14 @@ class Test(models.Model):
     name : CharField
         The name of the test that was performed
     submission : ForeignKey
-        The submission the tests where generated on
+        The submission the tests where unapproved on
     """
     name = models.CharField(max_length=30)
     label = models.CharField(max_length=50)
     annotation = models.TextField()
-    submission = models.ForeignKey(
-        'submission',
-        related_name='tests',
-        on_delete=models.CASCADE,
-    )
+    submission = models.ForeignKey('submission',
+                                   related_name='tests',
+                                   on_delete=models.CASCADE,)
 
     class Meta:
         verbose_name = "Test"
@@ -308,6 +321,9 @@ class Submission(models.Model):
     type : OneToOneField
         Relation to the type containing meta information
     """
+    submission_id = models.UUIDField(primary_key=True,
+                                     default=uuid.uuid4,
+                                     editable=False)
     seen_by_student = models.BooleanField(default=False)
     text = models.TextField(blank=True)
     type = models.ForeignKey(
@@ -331,63 +347,6 @@ class Submission(models.Model):
             self.student
         )
 
-    @classmethod
-    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
-            1. the tutor already has a feedback in progress
-            2. there is no more feedback to give
-
-        Parameters
-        ----------
-        tutor : User object
-            The tutor that a submission should be assigned to.
-        slug : None, optional
-            If a slug for a submission is given the belonging Feedback is
-            assigned to the tutor. If this submission had feedback before
-            the tutor that worked on it, is unassigned.
-
-        Returns
-        -------
-        bool
-            Returns True only if feedback was actually assigned otherwise False
-
-        """
-
-        # Get a submission from the submission set
-        unfinished = Feedback.tutor_unfinished_feedback(tutor)
-        if unfinished:
-            return False
-
-        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)
-        )
-
-        # we want a submission of a specific type
-        if slug:
-            candidates = candidates.filter(type__slug=slug)
-
-        # we couldn't find any submission to correct
-        if not candidates:
-            return False
-
-        submission = candidates[0]
-        feedback = submission.feedback if hasattr(
-            submission, 'feedback') else Feedback()
-        feedback.origin = Feedback.MANUAL
-        feedback.status = Feedback.EDITABLE
-        feedback.of_tutor = tutor
-        feedback.of_submission = submission
-        feedback.save()
-        return True
-
 
 class Feedback(models.Model):
     """
@@ -404,40 +363,28 @@ class Feedback(models.Model):
         points a student receives for his submission.
     of_tutor : ForeignKey
         The tutor/reviewer how last edited the feedback
-    ORIGIN : TYPE
-        Description
     origin : IntegerField
         Of whom was this feedback originally created. She below for the choices
     score : PositiveIntegerField
         A score that has been assigned to he submission. Is final if it was
         accepted.
-    STATUS : The status determines
-        Description
-    status : PositiveIntegerField
-        The status roughly determines in which state a feedback is in. A just
-        initiated submission is editable. Based on the status feedback is
-        presented to different types of users. Students may see feedback only
-        if it has been accepted, while reviewers have access at any time.
     text : TextField
         Detailed description by the tutor about what went wrong.
         Every line in the feedback should correspond with a line in the
         students submission, maybe with additional comments appended.
-
     """
     text = models.TextField()
     score = models.PositiveIntegerField(default=0)
     created = models.DateTimeField(auto_now_add=True)
     modified = models.DateTimeField(auto_now=True)
+    is_final = models.BooleanField(default=False)
 
     of_submission = models.OneToOneField(
         Submission,
         on_delete=models.CASCADE,
-        related_name='feedback',
-        unique=True,
-        blank=False,
-        null=False)
+        related_name='feedback')
     of_tutor = models.ForeignKey(
-        Tutor,
+        get_user_model(),
         on_delete=models.SET_NULL,
         related_name='feedback_list',
         blank=True,
@@ -449,24 +396,6 @@ class Feedback(models.Model):
         blank=True,
         null=True)
 
-    # what is the current status of our feedback
-    (
-        EDITABLE,
-        OPEN,
-        NEEDS_REVIEW,
-        ACCEPTED,
-    ) = range(4)  # this order matters
-    STATUS = (
-        (EDITABLE, 'editable'),
-        (OPEN, 'request reassignment'),
-        (NEEDS_REVIEW, 'request review'),
-        (ACCEPTED, 'accepted'),
-    )
-    status = models.IntegerField(
-        choices=STATUS,
-        default=EDITABLE,
-    )
-
     # how was this feedback created
     (
         WAS_EMPTY,
@@ -500,87 +429,147 @@ class Feedback(models.Model):
     def get_full_score(self) -> int:
         return self.of_submission.type.full_score
 
-    @classmethod
-    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.
 
-        Parameters
-        ----------
-        user : User object
-            The user for which feedback should not be returned. Often the user
-            that is currently searching for a task someone else does not want
-            to do.
+class SubscriptionEnded(Exception):
+    pass
 
-        Returns
-        -------
-        QuerySet
-            All feedback objects that are open for reassignment that do not
-            belong to the user
-        """
-        return cls.objects.filter(
-            Q(status=Feedback.OPEN) &
-            ~Q(of_tutor=user)  # you shall not request your own feedback
-        )
 
-    @classmethod
-    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
+class AssignmentError(Exception):
+    pass
 
-        Parameters
-        ----------
-        user : User object
-            The tutor who formed the request
 
-        Returns
-        -------
-            The feedback or none if no feedback was assigned
-        """
-        tutor_feedback = cls.objects.filter(
-            Q(of_tutor=user), Q(status=Feedback.EDITABLE),
-        )
-        return tutor_feedback[0] if tutor_feedback else None
+class GeneralTaskSubscription(models.Model):
 
-    @classmethod
-    def tutor_assigned_feedback(cls, user: Union[Tutor, Reviewer]):
-        """Gets all feedback that is assigned to the tutor including
-        all status cases.
+    RANDOM = 'random'
+    STUDENT_QUERY = 'student'
+    EXAM_TYPE_QUERY = 'exam'
+    SUBMISSION_TYPE_QUERY = 'submission_type'
 
-        Returns
-        -------
-        a QuerySet of tasks that have been assigned to this tutor
+    type_query_mapper = {
+        RANDOM: '__any',
+        STUDENT_QUERY: 'student__user__username',
+        EXAM_TYPE_QUERY: 'student__examtype__module_reference',
+        SUBMISSION_TYPE_QUERY: 'type__title',
+    }
 
-        Parameters
-        ----------
-        user : User object
-            The user for which the feedback should be returned
-        """
-        tutor_feedback = cls.objects.filter(of_tutor=user)
-        return tutor_feedback
+    QUERY_CHOICE = (
+        (RANDOM, 'Query for any submission'),
+        (STUDENT_QUERY, 'Query for submissions of student'),
+        (EXAM_TYPE_QUERY, 'Query for submissions of exam type'),
+        (SUBMISSION_TYPE_QUERY, 'Query for submissions of submissions_type'),
+    )
 
-    def finalize_feedback(self, user: Union[Tutor, Reviewer]):
-        """Used to mark feedback as accepted (reviewed).
+    subscription_id = models.UUIDField(primary_key=True,
+                                       default=uuid.uuid4,
+                                       editable=False)
+    owner = models.ForeignKey(get_user_model(),
+                              on_delete=models.CASCADE,
+                              related_name='susbscriptions')
+    query_key = models.CharField(max_length=75, blank=True)
+    query_type = models.CharField(max_length=75,
+                                  choices=QUERY_CHOICE,
+                                  default=RANDOM)
 
-        Parameters
-        ----------
-        user : User object
-            The tutor/reviewer that marks some feedback as accepted
-        """
-        self.status = Feedback.ACCEPTED
-        self.of_reviewer = user
-        self.save()
+    class Meta:
+        unique_together = ('owner', 'query_key', 'query_type')
 
-    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.
+    def _get_submission_base_query(self) -> QuerySet:
+        if self.query_type == self.RANDOM:
+            return Submission.objects.all()
 
-        Parameters
-        ----------
-        User object
-            The user to which to feedback should be assigned to
-        """
-        assert self.status == Feedback.OPEN
-        self.of_tutor = user
-        self.status = Feedback.EDITABLE
+        return Submission.objects.filter(
+            **{self.type_query_mapper[self.query_type]: self.query_key})
+
+    def _find_unassigned_non_final_submissions(self):
+        unassigned_non_final_submissions = \
+            self._get_submission_base_query().filter(
+                Q(assignments__isnull=True),
+                Q(feedback__isnull=True)
+            )
+
+        log.debug('unassigned non final submissions %s',
+                  unassigned_non_final_submissions)
+
+        return unassigned_non_final_submissions
+
+    def _find_unassigned_unapproved_non_final_submissions(self):
+        unapproved_not_final_submissions = \
+            self._get_submission_base_query().filter(
+                Q(feedback__isnull=False),
+                Q(feedback__is_final=False),
+                ~Q(feedback__of_tutor=self.owner),
+                # TODO: prevent reassigning to the same tutor
+            )
+
+        log.debug('unapproved not final submissions %s',
+                  unapproved_not_final_submissions)
+
+        return unapproved_not_final_submissions
+
+    def _get_next_assignment_in_subscription(self):
+        assignment_priority = (
+            self._find_unassigned_non_final_submissions,
+            self._find_unassigned_unapproved_non_final_submissions
+        )
+
+        lazy_queries = (query_set() for query_set in assignment_priority)
+        for query in (q for q in lazy_queries if len(q) > 0):
+            return query.first()
+
+        raise SubscriptionEnded(
+            f'The task which user {self.owner} subscribed to is done')
+
+    @transaction.atomic
+    def get_or_create_work_assignment(self):
+        task = self._get_next_assignment_in_subscription()
+
+        return TutorSubmissionAssignment.objects.get_or_create(
+            subscription=self,
+            submission=task)[0]
+
+    def _create_new_assignment_if_subscription_empty(self):
+        if self.assignments.filter(is_done=False).count() < 1:
+            self.get_or_create_work_assignment()
+
+    def _eagerly_reserve_the_next_assignment(self):
+        if self.assignments.filter(is_done=False).count() < 2:
+            self.get_or_create_work_assignment()
+
+    def get_oldest_unfinished_assignment(self):
+        self._create_new_assignment_if_subscription_empty()
+        return self.assignments \
+            .filter(is_done=False) \
+            .order_by('created') \
+            .first()
+
+    def get_youngest_unfinished_assignment(self):
+        self._create_new_assignment_if_subscription_empty()
+        self._eagerly_reserve_the_next_assignment()
+        return self.assignments \
+            .filter(is_done=False) \
+            .order_by('-created') \
+            .first()
+
+
+class TutorSubmissionAssignment(models.Model):
+
+    assignment_id = models.UUIDField(primary_key=True,
+                                     default=uuid.uuid4,
+                                     editable=False)
+    submission = models.ForeignKey(Submission,
+                                   on_delete=models.CASCADE,
+                                   related_name='assignments')
+    subscription = models.ForeignKey(GeneralTaskSubscription,
+                                     on_delete=models.CASCADE,
+                                     related_name='assignments')
+    is_done = models.BooleanField(default=False)
+    created = models.DateTimeField(auto_now_add=True)
+
+    @transaction.atomic
+    def set_done(self):
+        self.is_done = True
         self.save()
+
+    def __str__(self):
+        return (f'{self.assignee} assigned to {self.submission}'
+                f' (active={self.active})')
diff --git a/core/permissions.py b/core/permissions.py
index 211ed2fb337a792156257d30b2fc808ace7a9f7c..5466fa2357f5f31a85085a172090934985e70f03 100644
--- a/core/permissions.py
+++ b/core/permissions.py
@@ -45,3 +45,8 @@ class IsReviewer(IsUserGenericPermission):
 class IsTutor(IsUserGenericPermission):
     """ Has tutor permissions """
     models = (Tutor,)
+
+
+class IsTutorOrReviewer(IsUserGenericPermission):
+    """ Has tutor or reviewer permissions """
+    models = (Tutor, Reviewer,)
diff --git a/core/serializers.py b/core/serializers.py
index b64a4cf14e4b913aa8bee8595a464121ffc07df1..c767054f769b1857cdcf08d901e2be772e1dd054 100644
--- a/core/serializers.py
+++ b/core/serializers.py
@@ -1,10 +1,14 @@
 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, Student, Submission,
-                         SubmissionType, Test, Tutor)
+from core.models import (ExamType, Feedback, GeneralTaskSubscription, Student,
+                         Submission, SubmissionType, Tutor,
+                         TutorSubmissionAssignment)
 from util.factories import GradyUserFactory
 
 log = logging.getLogger(__name__)
@@ -37,10 +41,49 @@ class ExamSerializer(DynamicFieldsModelSerializer):
 
 
 class FeedbackSerializer(DynamicFieldsModelSerializer):
+    assignment_id = serializers.UUIDField(write_only=True)
+
+    def validate(self, data):
+        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')
+
+        submission = assignment.submission
+        if not 0 <= score <= submission.type.full_score:
+            raise serializers.ValidationError(
+                f'Score has to be in range [0..{submission.type.full_score}].')
+
+        if hasattr(submission, 'feedback'):
+            raise serializers.ValidationError(
+                'Feedback for this submission already exists')
+
+        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()
+
+        return Feedback.objects.create(**validated_data)
 
     class Meta:
         model = Feedback
-        fields = ('text', 'score')
+        fields = ('assignment_id', 'text', 'score')
 
 
 class TestSerializer(DynamicFieldsModelSerializer):
@@ -130,3 +173,69 @@ class TutorSerializer(DynamicFieldsModelSerializer):
     class Meta:
         model = Tutor
         fields = ('username', 'feedback_count')
+
+
+class AssignmentSerializer(DynamicFieldsModelSerializer):
+    submission_id = serializers.ReadOnlyField(
+        source='submission.submission_id')
+
+    class Meta:
+        model = TutorSubmissionAssignment
+        fields = ('assignment_id', 'submission_id', 'is_done',)
+
+
+class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
+    text = serializers.ReadOnlyField()
+    type_id = serializers.ReadOnlyField(source='type.id')
+    full_score = serializers.ReadOnlyField(source='type.full_score')
+
+    class Meta:
+        model = Submission
+        fields = ('submission_id', 'type_id', 'text', 'full_score')
+
+
+class AssignmentDetailSerializer(DynamicFieldsModelSerializer):
+    submission = SubmissionAssignmentSerializer()
+    feedback = FeedbackSerializer(source='submission.feedback')
+
+    class Meta:
+        model = TutorSubmissionAssignment
+        fields = ('assignment_id', 'feedback', 'submission', 'is_done',)
+
+
+class SubscriptionSerializer(DynamicFieldsModelSerializer):
+    owner = serializers.ReadOnlyField(source='owner.username')
+    query_key = serializers.CharField(required=False)
+    assignments = AssignmentSerializer(read_only=True, many=True)
+
+    def validate(self, data):
+        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
+            raise serializers.ValidationError(
+                "Oh great, you raised an IntegrityError. I'm disappointed.")
+
+        return subscription
+
+    class Meta:
+        model = GeneralTaskSubscription
+        fields = (
+            'subscription_id',
+            'owner',
+            'query_type',
+            'query_key',
+            'assignments')
diff --git a/core/tests/test_factory_and_feedback.py b/core/tests/test_factory_and_feedback.py
index 408ebb753ada91819d0989cb32a3fdc336db9a9a..42ed4a4e2e1434cc1e4d63898c50a9ae75b153fd 100644
--- a/core/tests/test_factory_and_feedback.py
+++ b/core/tests/test_factory_and_feedback.py
@@ -1,35 +1,9 @@
 from django.test import TestCase
 
-from core.models import (Feedback, Reviewer, Student, Submission,
-                         SubmissionType, Tutor)
+from core.models import Reviewer, Student, Tutor
 from util.factories import GradyUserFactory
 
 
-class FeedbackTestCase(TestCase):
-
-    factory = GradyUserFactory()
-
-    def setUp(self):
-        self.tutor = self.factory.make_tutor()
-        self.student = self.factory.make_student()
-
-        submission_type = SubmissionType.objects.create(
-            name='Cooking some crystal with Jesse')
-        Submission.objects.create(student=self.student, type=submission_type)
-        Submission.assign_tutor(self.tutor)
-
-    def test_can_assign_tutor(self):
-        self.assertEqual(self.tutor.feedback_list.count(), 1)
-
-    def test_feedback_origin_is_manual(self):
-        feedback = self.tutor.feedback_list.all()[0]
-        self.assertEqual(feedback.origin, Feedback.MANUAL)
-
-    def test_feedback_status_is_editable(self):
-        feedback = self.tutor.feedback_list.all()[0]
-        self.assertEqual(feedback.status, Feedback.EDITABLE)
-
-
 class FactoryTestCase(TestCase):
 
     factory = GradyUserFactory()
diff --git a/core/tests/test_functional_views.py b/core/tests/test_functional_views.py
index f48645b23b3ba0f542a86ba517c1b133b08f1eda..5a99c8b914092f3bc786f8d710b7d9ccec7852ac 100644
--- a/core/tests/test_functional_views.py
+++ b/core/tests/test_functional_views.py
@@ -1,6 +1,7 @@
 from django.urls import reverse
 from rest_framework.test import (APIRequestFactory, APITestCase,
                                  force_authenticate)
+
 from core.views import get_user_role
 from util.factories import GradyUserFactory
 
diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py
new file mode 100644
index 0000000000000000000000000000000000000000..d96aae0e56aa4edc50ee9d0e6a57b129edcb2fa9
--- /dev/null
+++ b/core/tests/test_subscription_assignment_service.py
@@ -0,0 +1,56 @@
+
+from django.test import TestCase
+
+from core.models import (GeneralTaskSubscription, Submission, SubmissionType,
+                         SubscriptionEnded)
+from util.factories import GradyUserFactory
+
+
+class GeneralTaskSubscriptionRandomTest(TestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.user_factory = GradyUserFactory()
+
+    def setUp(self):
+        self.t = self.user_factory.make_tutor()
+        self.s1 = self.user_factory.make_student()
+        self.s2 = self.user_factory.make_student()
+
+        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')
+        self.submission_02 = Submission.objects.create(
+            type=self.submission_type, student=self.s2, text='I like apples')
+
+        self.subscription = GeneralTaskSubscription.objects.create(
+            owner=self.t.user, query_type=GeneralTaskSubscription.RANDOM)
+
+    def test_subscription_gets_an_assignment(self):
+        self.subscription._create_new_assignment_if_subscription_empty()
+        self.assertEqual(1, self.subscription.assignments.count())
+
+    def test_first_work_assignment_was_created_unfinished(self):
+        self.subscription._create_new_assignment_if_subscription_empty()
+        self.assertFalse(self.subscription.assignments.first().is_done)
+
+    def test_subscription_raises_error_when_depleted(self):
+        self.submission_01.delete()
+        self.submission_02.delete()
+        try:
+            self.subscription._create_new_assignment_if_subscription_empty()
+        except SubscriptionEnded as err:
+            self.assertFalse(False)
+        else:
+            self.assertTrue(False)
+
+    def test_can_prefetch(self):
+        self.subscription._create_new_assignment_if_subscription_empty()
+        self.subscription._eagerly_reserve_the_next_assignment()
+        self.assertEqual(2, self.subscription.assignments.count())
+
+    def test_oldest_assignment_is_current(self):
+        assignment = self.subscription.get_oldest_unfinished_assignment()
+        self.assertEqual(assignment,
+                         self.subscription.get_oldest_unfinished_assignment())
diff --git a/core/urls.py b/core/urls.py
index 2f2e47e682c450f8efe0bdd2a892b3424a7e5f01..7898cedd9b7861057e317afdc17a9fe90ad1f406 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,35 +1,32 @@
-from django.conf.urls import include, url
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from django.views.generic.base import TemplateView
+from django.urls import path
 from rest_framework.routers import DefaultRouter
-from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token
 
 from core import views
 
 # Create a router and register our viewsets with it.
 router = DefaultRouter()
-router.register(r'student', views.StudentReviewerApiViewSet)
-router.register(r'examtype', views.ExamApiViewSet)
-router.register(r'submissiontype', views.SubmissionTypeApiView)
-router.register(r'tutor', views.TutorApiViewSet)
+router.register('student', views.StudentReviewerApiViewSet)
+router.register('examtype', views.ExamApiViewSet)
+router.register('feedback', views.FeedbackApiView)
+router.register('submissiontype', views.SubmissionTypeApiView)
+router.register('tutor', views.TutorApiViewSet)
+router.register('subscription', views.SubscriptionApiViewSet)
+router.register('assignment', views.AssignmentApiViewSet)
 
 # regular views that are not viewsets
 regular_views_urlpatterns = [
-    url(r'student-page', views.StudentSelfApiView.as_view(),
-        name='student-page'),
-    url(r'student-submissions', views.StudentSelfSubmissionsApiView.as_view(),
-        name='student-submissions'),
-    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')
+    path('student-page',
+         views.StudentSelfApiView.as_view(),
+         name='student-page'),
+    path('user-role', views.get_user_role, name='user-role'),
+    path('jwt-time-delta',
+         views.get_jwt_expiration_delta,
+         name='jwt-time-delta')
 ]
 
 urlpatterns = [
-    url(r'^api/', include(router.urls)),
-    url(r'^api/', include(regular_views_urlpatterns)),
-    url(r'^api-token-auth/', obtain_jwt_token),
-    url(r'^api-token-refresh', refresh_jwt_token),
-    url(r'^$', TemplateView.as_view(template_name='index.html')),
+    *router.urls,
+    *regular_views_urlpatterns,
+    *staticfiles_urlpatterns()
 ]
-
-urlpatterns += staticfiles_urlpatterns()
diff --git a/core/views.py b/core/views.py
index 2b87315b923a92222eb84cffa8b04734c20245a1..f26e69d414de17e843cdf4fea813f1228d954837 100644
--- a/core/views.py
+++ b/core/views.py
@@ -2,15 +2,18 @@
 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 """
 from django.conf import settings
-from rest_framework import mixins, viewsets, generics
-from rest_framework.decorators import api_view
+from rest_framework import generics, mixins, status, viewsets
+from rest_framework.decorators import api_view, detail_route
 from rest_framework.response import Response
 
-from core.models import ExamType, Student, SubmissionType, Tutor
-from core.permissions import IsReviewer, IsStudent
-from core.serializers import (ExamSerializer, StudentSerializer,
-                              StudentSerializerForListView,
-                              SubmissionSerializer, SubmissionTypeSerializer,
+from core import models
+from core.models import (ExamType, Feedback, GeneralTaskSubscription, Student,
+                         SubmissionType, Tutor, TutorSubmissionAssignment)
+from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer
+from core.serializers import (AssignmentDetailSerializer, AssignmentSerializer,
+                              ExamSerializer, FeedbackSerializer,
+                              StudentSerializer, StudentSerializerForListView,
+                              SubmissionTypeSerializer, SubscriptionSerializer,
                               TutorSerializer)
 
 
@@ -51,11 +54,23 @@ class ExamApiViewSet(viewsets.ReadOnlyModelViewSet):
     serializer_class = ExamSerializer
 
 
-class TutorApiViewSet(mixins.RetrieveModelMixin,
-                      mixins.CreateModelMixin,
-                      mixins.DestroyModelMixin,
-                      mixins.ListModelMixin,
-                      viewsets.GenericViewSet):
+class FeedbackApiView(
+        mixins.CreateModelMixin,
+        mixins.RetrieveModelMixin,
+        viewsets.GenericViewSet):
+    """ Gets a list of an individual exam by Id if provided """
+    permission_classes = (IsTutorOrReviewer,)
+    queryset = Feedback.objects.all()
+    serializer_class = FeedbackSerializer
+    lookup_field = 'submission__submission_id'
+
+
+class TutorApiViewSet(
+        mixins.RetrieveModelMixin,
+        mixins.CreateModelMixin,
+        mixins.DestroyModelMixin,
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
     """ Api endpoint for creating, listing, viewing or deleteing tutors """
     permission_classes = (IsReviewer,)
     queryset = Tutor.objects.all()
@@ -79,3 +94,72 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
     """ Gets a list or a detail view of a single SubmissionType """
     queryset = SubmissionType.objects.all()
     serializer_class = SubmissionTypeSerializer
+
+
+class SubscriptionApiViewSet(
+        mixins.RetrieveModelMixin,
+        mixins.CreateModelMixin,
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
+    permission_classes = (IsTutorOrReviewer,)
+    queryset = GeneralTaskSubscription.objects.all()
+    serializer_class = SubscriptionSerializer
+
+    @detail_route(methods=['get'], url_path='assignments/current')
+    def current_assignment(self, request, pk=None):
+        subscription = self.get_object()
+        try:
+            assignment = subscription.get_oldest_unfinished_assignment()
+        except models.SubscriptionEnded as err:
+            return Response(
+                {'Error': 'This subscription has ended'},
+                status=status.HTTP_410_GONE)
+        serializer = AssignmentDetailSerializer(assignment)
+        return Response(serializer.data)
+
+    @detail_route(methods=['get'], url_path='assignments/next')
+    def next_assignment(self, request, pk=None):
+        subscription = self.get_object()
+        try:
+            assignment = subscription.get_youngest_unfinished_assignment()
+        except models.SubscriptionEnded as err:
+            return Response(
+                {'Error': 'Seems there is nothing left to prefetch'},
+                status=status.HTTP_410_GONE)
+        serializer = AssignmentDetailSerializer(assignment)
+        return Response(serializer.data)
+
+    def get_queryset(self):
+        return GeneralTaskSubscription.objects.filter(owner=self.request.user)
+
+    def destroy(self, request, pk=None):
+        instance = self.get_object()
+
+        # todo: prevent this via deactivation
+
+        instance.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class AssignmentApiViewSet(
+        mixins.RetrieveModelMixin,
+        mixins.ListModelMixin,
+        viewsets.GenericViewSet):
+    permission_classes = (IsTutorOrReviewer,)
+    queryset = TutorSubmissionAssignment.objects.all()
+    serializer_class = AssignmentSerializer
+
+    def get_queryset(self):
+        """ Get only assignments of that user """
+        return TutorSubmissionAssignment.objects.filter(
+            subscription__owner=self.request.user)
+
+    def destroy(self, request, pk=None):
+        """ Stop working on the assignment before it is finished """
+        instance = self.get_object()
+
+        if instance.is_done:
+            return Response(status=status.HTTP_403_FORBIDDEN)  # test
+
+        instance.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)  # test
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 066af8ec33d58aa0cdb0872630db058711d53c0c..3be7ad7365ab77bb0dcb8c5aa7095b1a0f4c4e72 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -37,7 +37,6 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
-    'django_extensions',
     'rest_framework',
     'corsheaders',
     'drf_dynamic_fields',
diff --git a/grady/urls.py b/grady/urls.py
index 5a75e52c268589248d4dca2f4b51c4dadcfe2dfd..e36863f62f8c8d3c2d7e610ed1a52e05c11e5832 100644
--- a/grady/urls.py
+++ b/grady/urls.py
@@ -1,25 +1,15 @@
-"""grady URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
-    https://docs.djangoproject.com/en/1.10/topics/http/urls/
-Examples:
-Function views
-    1. Add an import:  from my_app import views
-    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
-Class-based views
-    1. Add an import:  from other_app.views import Home
-    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
-Including another URLconf
-    1. Import the include() function: from django.conf.urls import url, include
-    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
-"""
-from django.conf.urls import include, url
 from django.contrib import admin
+from django.urls import include, path
+from django.views.generic.base import TemplateView
+from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token
 
 urlpatterns = [
-    url(r'^admin/', admin.site.urls),
-    url(r'^', include('core.urls')),
+    path('admin/', admin.site.urls),
+    path('api/', include('core.urls')),
+    path('api-auth/', include('rest_framework.urls',
+                              namespace='rest_framework')),
+    path('api-token-auth/', obtain_jwt_token),
+    path('api-token-refresh/', refresh_jwt_token),
+    path('', TemplateView.as_view(template_name='index.html'))
 
-    url(r'^api-auth/', include('rest_framework.urls',
-                               namespace='rest_framework')),
 ]
diff --git a/requirements.txt b/requirements.txt
index 8c712f888c9655e112e162df00dce2aabce7f9ec..c6db510ecba2c16b5247ce29bd6c16fb16d9a57d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,9 @@
 django-cors-headers~=2.1.0
 django-extensions~=1.7.7
 djangorestframework-jwt~=1.11.0
-djangorestframework~=3.6.3
+djangorestframework~=3.7.7
 drf-dynamic-fields~=0.2.0
-Django~=1.11.8
+Django~=2.0
 gevent~=1.2.2
 gunicorn~=19.7.0
 psycopg2~=2.7.1
diff --git a/util/factories.py b/util/factories.py
index f90e649c35bca6d74429aa48707ff27f0fdc922c..a27f20bdede0dffea2e12b956dff1afbc33d3262 100644
--- a/util/factories.py
+++ b/util/factories.py
@@ -2,9 +2,9 @@ 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 (ExamType, Feedback, Reviewer, Student, Submission,
+                         SubmissionType, Tutor)
 
 STUDENTS = 'students'
 TUTORS = 'tutors'
@@ -35,15 +35,14 @@ def store_password(username, groupname, password):
 class GradyUserFactory:
 
     def __init__(self,
-                 password_generator_func=get_random_password,
+                 make_password=get_random_password,
                  password_storge=store_password,
                  *args, **kwargs):
-        self.password_generator_func = password_generator_func
+        self.make_password = make_password
         self.password_storge = password_storge
 
-    @staticmethod
-    def _get_random_name(prefix='', suffix='', k=1):
-        return ''.join((prefix, get_random_password(k), suffix))
+    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,
                         store_pw=False, **kwargs):
@@ -63,10 +62,8 @@ class GradyUserFactory:
             username=username,
             defaults=kwargs)
 
-        if created or password is not None:
-            password = self.password_generator_func() if password is None \
-                else password
-            print(password)
+        if created:
+            password = self.make_password() if password is None else password
             user.set_password(password)
             user.save()
 
@@ -97,8 +94,7 @@ class GradyUserFactory:
 
         return generic_user
 
-    def make_student(self, username=None,
-                     matrikel_no=None,
+    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. """
@@ -134,9 +130,7 @@ 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']) if
-        'exam' in student else None,
-        password=student.get('password', None)
+        exam=ExamType.objects.get(module_reference=student['exam'])
     ) for student in students]
 
 
@@ -151,15 +145,11 @@ def make_reviewers(reviewers=[], **kwargs):
 
 
 def make_feedback(feedback, submission_object):
-    tutor = User.objects.get(
-        username=feedback['of_tutor']).get_associated_user()
+    feedback['of_tutor'] = User.objects.get(
+        username=feedback['of_tutor']).get_associated_user().user
     return Feedback.objects.update_or_create(
         of_submission=submission_object,
-        of_tutor=tutor,
-        defaults={
-            'text': feedback.get('text', ''),
-            'score': feedback['score']
-        })[0]
+        defaults=feedback)[0]
 
 
 def make_submissions(submissions=[], **kwargs):
@@ -223,13 +213,17 @@ def init_test_instance():
             'students': [{
                 'username': 'student01',
                 'exam': 'Test Exam 01',
-                'password': 'p'
+            },
+                {
+                'username': 'student02',
+                'exam': 'Test Exam 01',
             }],
             'tutors': [{
                 'username': 'tutor01'
             }],
             'reviewers': [{
-                'username': 'reviewer01'
+                'username': 'reviewer01',
+                'password': 'p'
             }],
             'submissions': [
                 {
@@ -245,6 +239,7 @@ def init_test_instance():
                         'text': 'Not good!',
                         'score': 5,
                         'of_tutor': 'tutor01',
+                        'is_final': True
                     }
                 },
                 {
@@ -255,12 +250,7 @@ def init_test_instance():
                             '       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',
-                    },
+                    'user': 'student01'
                 },
                 {
                     'text': 'function blabl\n'
@@ -270,12 +260,17 @@ def init_test_instance():
                             '       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',
-                    },
+                    'user': 'student01'
+                },
+                {
+                    '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': 'student02'
                 },
             ]}
     )