diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
index 1f103389b8104f24c64220abe9ef2f6f359d9892..3624c16e6e5350129516765df98880b8b850273d 100644
--- a/core/migrations/0001_initial.py
+++ b/core/migrations/0001_initial.py
@@ -1,15 +1,15 @@
-# Generated by Django 2.0.2 on 2018-02-10 17:00
-
-import uuid
+# Generated by Django 2.1.11 on 2019-12-01 16:48
 
+import core.models.student_info
+import core.models.user_account
+from django.conf import settings
 import django.contrib.auth.models
 import django.contrib.auth.validators
+import django.core.validators
+from django.db import migrations, models
 import django.db.models.deletion
 import django.utils.timezone
-from django.conf import settings
-from django.db import migrations, models
-
-import core.models
+import uuid
 
 
 class Migration(migrations.Migration):
@@ -34,12 +34,10 @@ class Migration(migrations.Migration):
                 ('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')),
+                ('role', models.CharField(choices=[('Student', 'student'), ('Tutor', 'tutor'), ('Reviewer', 'reviewer')], max_length=50)),
                 ('user_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
                 ('fullname', models.CharField(blank=True, max_length=70, verbose_name='full name')),
                 ('is_admin', models.BooleanField(default=False)),
-                ('role', models.CharField(choices=[('Student', 'student'), ('Tutor', 'tutor'), ('Reviewer', 'reviewer')], max_length=50)),
-                ('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',
@@ -48,6 +46,7 @@ class Migration(migrations.Migration):
             },
             managers=[
                 ('objects', django.contrib.auth.models.UserManager()),
+                ('corrector', core.models.user_account.TutorReviewerManager()),
             ],
         ),
         migrations.CreateModel(
@@ -68,10 +67,10 @@ class Migration(migrations.Migration):
             name='Feedback',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('score', models.PositiveIntegerField(default=0)),
+                ('score', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('is_final', models.BooleanField(default=False)),
-                ('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)),
+                ('final_by_reviewer', models.BooleanField(default=False)),
             ],
             options={
                 'verbose_name': 'Feedback',
@@ -81,8 +80,8 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='FeedbackComment',
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('text', models.TextField()),
+                ('comment_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('text', models.TextField(blank=True)),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('modified', models.DateTimeField(auto_now=True)),
                 ('visible_to_student', models.BooleanField(default=True)),
@@ -96,13 +95,54 @@ class Migration(migrations.Migration):
                 'ordering': ('created',),
             },
         ),
+        migrations.CreateModel(
+            name='FeedbackLabel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=50, unique=True)),
+                ('description', models.TextField()),
+                ('colour', models.CharField(default='#b0b0b0', max_length=7, validators=[django.core.validators.RegexValidator(code='nomatch', message='Colour must be in format: #[0-9A-F]{7}', regex='^#[0-9A-F]{6}$')])),
+                ('feedback', models.ManyToManyField(related_name='labels', to='core.Feedback')),
+                ('feedback_comments', models.ManyToManyField(related_name='labels', to='core.FeedbackComment')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Group',
+            fields=[
+                ('group_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=120)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='MetaSubmission',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('done_assignments', models.PositiveIntegerField(default=0)),
+                ('has_active_assignment', models.BooleanField(default=False)),
+                ('has_feedback', models.BooleanField(default=False)),
+                ('has_final_feedback', models.BooleanField(default=False)),
+                ('feedback_authors', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='SolutionComment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('text', models.TextField()),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('modified', models.DateTimeField(auto_now=True)),
+                ('of_line', models.PositiveIntegerField()),
+            ],
+        ),
         migrations.CreateModel(
             name='StudentInfo',
             fields=[
                 ('student_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
                 ('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')),
+                ('matrikel_no', models.CharField(default=core.models.student_info.random_matrikel_no, max_length=30, unique=True)),
+                ('total_score', models.PositiveIntegerField(default=0)),
+                ('passes_exam', models.BooleanField(default=False)),
+                ('exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students', to='core.ExamType')),
                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL)),
             ],
             options={
@@ -116,6 +156,8 @@ class Migration(migrations.Migration):
                 ('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)),
+                ('source_code', models.TextField(blank=True, editable=False, null=True)),
+                ('source_code_available', models.BooleanField(default=False, editable=False)),
                 ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='core.StudentInfo')),
             ],
             options={
@@ -124,25 +166,15 @@ class Migration(migrations.Migration):
                 'ordering': ('type__name',),
             },
         ),
-        migrations.CreateModel(
-            name='SubmissionSubscription',
-            fields=[
-                ('deactivated', models.BooleanField(default=False)),
-                ('subscription_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
-                ('query_key', models.UUIDField(blank=True)),
-                ('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)),
-                ('feedback_stage', models.CharField(choices=[('feedback-creation', 'No feedback was ever assigned'), ('feedback-validation', 'Feedback exists but is not validated'), ('feedback-conflict-resolution', 'Previous correctors disagree')], default='feedback-creation', max_length=40)),
-                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
         migrations.CreateModel(
             name='SubmissionType',
             fields=[
                 ('submission_type_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
-                ('name', models.CharField(max_length=50, unique=True)),
+                ('name', models.CharField(max_length=100, unique=True)),
                 ('full_score', models.PositiveIntegerField(default=0)),
                 ('description', models.TextField()),
                 ('solution', models.TextField()),
+                ('programming_language', models.CharField(choices=[('c', 'C syntax highlighting'), ('java', 'Java syntax highlighting'), ('mipsasm', 'Mips syntax highlighting'), ('haskell', 'Haskell syntax highlighting'), ('python', 'Python syntax highlighting'), ('plaintext', 'No syntax highlighting')], default='c', max_length=25)),
             ],
             options={
                 'verbose_name': 'SubmissionType',
@@ -167,10 +199,11 @@ class Migration(migrations.Migration):
             name='TutorSubmissionAssignment',
             fields=[
                 ('assignment_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('stage', models.CharField(choices=[('feedback-creation', 'No feedback was ever assigned'), ('feedback-validation', 'Feedback exists but is not validated'), ('feedback-review', 'Review by exam reviewer required')], default='feedback-creation', max_length=60)),
                 ('is_done', models.BooleanField(default=False)),
                 ('created', models.DateTimeField(auto_now_add=True)),
+                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to=settings.AUTH_USER_MODEL)),
                 ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='core.Submission')),
-                ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='core.SubmissionSubscription')),
             ],
         ),
         migrations.AddField(
@@ -178,19 +211,45 @@ class Migration(migrations.Migration):
             name='type',
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='submissions', to='core.SubmissionType'),
         ),
+        migrations.AddField(
+            model_name='solutioncomment',
+            name='of_submission_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to='core.SubmissionType'),
+        ),
+        migrations.AddField(
+            model_name='solutioncomment',
+            name='of_user',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='metasubmission',
+            name='submission',
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='meta', to='core.Submission'),
+        ),
         migrations.AddField(
             model_name='feedback',
             name='of_submission',
             field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='core.Submission'),
         ),
+        migrations.AddField(
+            model_name='useraccount',
+            name='group',
+            field=models.ManyToManyField(blank=True, related_name='group', to='core.Group'),
+        ),
+        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='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.AlterUniqueTogether(
             name='test',
             unique_together={('submission', 'name')},
         ),
-        migrations.AlterUniqueTogether(
-            name='submissionsubscription',
-            unique_together={('owner', 'query_key', 'query_type', 'feedback_stage')},
-        ),
         migrations.AlterUniqueTogether(
             name='submission',
             unique_together={('type', 'student')},
diff --git a/core/migrations/0002_auto_20180210_1727.py b/core/migrations/0002_auto_20180210_1727.py
deleted file mode 100644
index 6efb9d340dba96f298a40d0beb6411e626f9ebda..0000000000000000000000000000000000000000
--- a/core/migrations/0002_auto_20180210_1727.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-10 17:27
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='submissionsubscription',
-            name='query_key',
-            field=models.UUIDField(null=True),
-        ),
-    ]
diff --git a/core/migrations/0002_auto_20191202_1018.py b/core/migrations/0002_auto_20191202_1018.py
new file mode 100644
index 0000000000000000000000000000000000000000..07cfd2d188c9282a7a55918d9aea885848028594
--- /dev/null
+++ b/core/migrations/0002_auto_20191202_1018.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.1.11 on 2019-12-02 10:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='useraccount',
+            name='group',
+        ),
+        migrations.AddField(
+            model_name='useraccount',
+            name='exercise_groups',
+            field=models.ManyToManyField(blank=True, related_name='users', to='core.Group'),
+        ),
+    ]
diff --git a/core/migrations/0003_submissiondoneassignmentscount.py b/core/migrations/0003_submissiondoneassignmentscount.py
deleted file mode 100644
index 61375c886a3e202723072949ebcd13ced0cc3e27..0000000000000000000000000000000000000000
--- a/core/migrations/0003_submissiondoneassignmentscount.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-17 12:01
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0002_auto_20180210_1727'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='SubmissionDoneAssignmentsCount',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('done_assignments', models.PositiveIntegerField(default=0)),
-                ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='counter', to='core.Submission')),
-            ],
-        ),
-    ]
diff --git a/core/migrations/0004_auto_20180217_1723.py b/core/migrations/0004_auto_20180217_1723.py
deleted file mode 100644
index b87bf36814f5d52e5261c10c90d826624f6d7aa9..0000000000000000000000000000000000000000
--- a/core/migrations/0004_auto_20180217_1723.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-17 17:23
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0003_submissiondoneassignmentscount'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='MetaSubmission',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('done_assignments', models.PositiveIntegerField(default=0)),
-                ('has_active_assignment', models.BooleanField(default=False)),
-                ('has_feedback', models.BooleanField(default=False)),
-                ('has_final_feedback', models.BooleanField(default=False)),
-                ('feedback_authors', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
-                ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='meta', to='core.Submission')),
-            ],
-        ),
-        migrations.RemoveField(
-            model_name='submissiondoneassignmentscount',
-            name='submission',
-        ),
-        migrations.AlterUniqueTogether(
-            name='tutorsubmissionassignment',
-            unique_together={('submission', 'subscription')},
-        ),
-        migrations.DeleteModel(
-            name='SubmissionDoneAssignmentsCount',
-        ),
-    ]
diff --git a/core/migrations/0005_auto_20180217_1728.py b/core/migrations/0005_auto_20180217_1728.py
deleted file mode 100644
index 8506733712e2def2dd5aa26355b5d91e36818782..0000000000000000000000000000000000000000
--- a/core/migrations/0005_auto_20180217_1728.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-17 17:28
-
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0004_auto_20180217_1723'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='metasubmission',
-            name='feedback_authors',
-            field=models.ManyToManyField(related_name='authors', to=settings.AUTH_USER_MODEL),
-        ),
-    ]
diff --git a/core/migrations/0006_auto_20180217_1729.py b/core/migrations/0006_auto_20180217_1729.py
deleted file mode 100644
index 3783adfbe216195018c3664103e64ec5aecd0e04..0000000000000000000000000000000000000000
--- a/core/migrations/0006_auto_20180217_1729.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-17 17:29
-
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0005_auto_20180217_1728'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='metasubmission',
-            name='feedback_authors',
-            field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
-        ),
-    ]
diff --git a/core/migrations/0007_auto_20180217_2049.py b/core/migrations/0007_auto_20180217_2049.py
deleted file mode 100644
index e1a6b83aa494f814205a8302518a01af9e3cbc40..0000000000000000000000000000000000000000
--- a/core/migrations/0007_auto_20180217_2049.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-17 20:49
-
-import uuid
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0006_auto_20180217_1729'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='feedbackcomment',
-            name='id',
-        ),
-        migrations.AddField(
-            model_name='feedbackcomment',
-            name='comment_id',
-            field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
-        ),
-    ]
diff --git a/core/migrations/0008_auto_20180219_1712.py b/core/migrations/0008_auto_20180219_1712.py
deleted file mode 100644
index afe81921d62cfac26894bf652477de58ba92bc7d..0000000000000000000000000000000000000000
--- a/core/migrations/0008_auto_20180219_1712.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 2.0.2 on 2018-02-19 17:12
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0007_auto_20180217_2049'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='submissiontype',
-            name='programming_language',
-            field=models.CharField(choices=[('c', 'C syntax highlighting'), ('java', 'Java syntax highlighting')], default='c', max_length=25),
-        ),
-        migrations.AlterField(
-            model_name='submissiontype',
-            name='name',
-            field=models.CharField(max_length=100, unique=True),
-        ),
-    ]
diff --git a/core/migrations/0009_auto_20180320_2335.py b/core/migrations/0009_auto_20180320_2335.py
deleted file mode 100644
index 20c7419649a0bda893e4cfd28f4b72f755c529d6..0000000000000000000000000000000000000000
--- a/core/migrations/0009_auto_20180320_2335.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 2.0.2 on 2018-03-20 23:35
-
-from django.db import migrations, models
-
-import core.models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0008_auto_20180219_1712'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='studentinfo',
-            name='passes_exam',
-            field=models.BooleanField(default=False),
-        ),
-        migrations.AddField(
-            model_name='studentinfo',
-            name='total_score',
-            field=models.PositiveIntegerField(default=0),
-        ),
-        migrations.AlterField(
-            model_name='studentinfo',
-            name='matrikel_no',
-            field=models.CharField(default=core.models.random_matrikel_no, max_length=30, unique=True),
-        ),
-    ]
diff --git a/core/migrations/0010_auto_20180805_1139.py b/core/migrations/0010_auto_20180805_1139.py
deleted file mode 100644
index 56c42fa08ff08c0ff3d65747abc1705d6c3ed654..0000000000000000000000000000000000000000
--- a/core/migrations/0010_auto_20180805_1139.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Generated by Django 2.1 on 2018-08-05 11:39
-
-import core.models
-import django.contrib.auth.models
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0009_auto_20180320_2335'),
-    ]
-
-    operations = [
-        migrations.AlterModelManagers(
-            name='useraccount',
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-                ('tutors', core.models.TutorReviewerManager()),
-            ],
-        ),
-    ]
diff --git a/core/migrations/0011_auto_20181001_1259.py b/core/migrations/0011_auto_20181001_1259.py
deleted file mode 100644
index 96582383f85820924a510a962ec6dca40a4a7745..0000000000000000000000000000000000000000
--- a/core/migrations/0011_auto_20181001_1259.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.1 on 2018-10-01 12:59
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0010_auto_20180805_1139'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='submissiontype',
-            name='programming_language',
-            field=models.CharField(choices=[('c', 'C syntax highlighting'), ('java', 'Java syntax highlighting'), ('mipsasm', 'Mips syntax highlighting'), ('haskell', 'Haskell syntax highlighting')], default='c', max_length=25),
-        ),
-    ]
diff --git a/core/migrations/0012_auto_20190306_1611.py b/core/migrations/0012_auto_20190306_1611.py
deleted file mode 100644
index 54f93ca92be48e5c48a814198dec8e77668a3eb8..0000000000000000000000000000000000000000
--- a/core/migrations/0012_auto_20190306_1611.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.1.5 on 2019-03-06 16:11
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0011_auto_20181001_1259'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='studentinfo',
-            name='exam',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students', to='core.ExamType'),
-        ),
-    ]
diff --git a/core/migrations/0013_auto_20190308_1448.py b/core/migrations/0013_auto_20190308_1448.py
deleted file mode 100644
index 3f2e76d83b47225f972a0469cdbd1062513fb200..0000000000000000000000000000000000000000
--- a/core/migrations/0013_auto_20190308_1448.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.1.4 on 2019-03-08 14:48
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0012_auto_20190306_1611'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='feedback',
-            name='score',
-            field=models.DecimalField(decimal_places=2, default=0, max_digits=5),
-        ),
-    ]
diff --git a/core/migrations/0014_feedbacklabel.py b/core/migrations/0014_feedbacklabel.py
deleted file mode 100644
index 14b9d385a957821ca66f102d1d51f87e11a78379..0000000000000000000000000000000000000000
--- a/core/migrations/0014_feedbacklabel.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 2.1.4 on 2019-04-25 15:28
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0013_auto_20190308_1448'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='FeedbackLabel',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=50)),
-                ('description', models.TextField()),
-                ('feedback', models.ManyToManyField(related_name='labels', to='core.Feedback')),
-                ('feedback_comments', models.ManyToManyField(related_name='labels', to='core.FeedbackComment')),
-            ],
-        ),
-    ]
diff --git a/core/migrations/0015_feedbacklabel_colour.py b/core/migrations/0015_feedbacklabel_colour.py
deleted file mode 100644
index 2d4715b56e28b3bed41bc4f322f4c06656a79d33..0000000000000000000000000000000000000000
--- a/core/migrations/0015_feedbacklabel_colour.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.1.4 on 2019-04-30 17:01
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0014_feedbacklabel'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='feedbacklabel',
-            name='colour',
-            field=models.CharField(default='#b0b0b0', max_length=7, validators=[django.core.validators.RegexValidator(code='nomatch', message='Colour must be in format: #[0-9A-F]{7}', regex='^#[0-9A-F]{6}$')]),
-        ),
-    ]
diff --git a/core/migrations/0016_auto_20190521_1803.py b/core/migrations/0016_auto_20190521_1803.py
deleted file mode 100644
index 93477a02517034428177421a3ee7f6b19437d0a2..0000000000000000000000000000000000000000
--- a/core/migrations/0016_auto_20190521_1803.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2 on 2019-05-21 18:03
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0015_feedbacklabel_colour'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='feedbackcomment',
-            name='text',
-            field=models.TextField(blank=True),
-        ),
-    ]
diff --git a/core/migrations/0016_solutioncomment.py b/core/migrations/0016_solutioncomment.py
deleted file mode 100644
index d76621d464405ace6b2a55f718b46dd6a56ed9b5..0000000000000000000000000000000000000000
--- a/core/migrations/0016_solutioncomment.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Generated by Django 2.1.4 on 2019-05-14 15:00
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0015_feedbacklabel_colour'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='SolutionComment',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('text', models.TextField()),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('modified', models.DateTimeField(auto_now=True)),
-                ('of_line', models.PositiveIntegerField()),
-                ('of_submission_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to='core.SubmissionType')),
-                ('of_user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-    ]
diff --git a/core/migrations/0017_auto_20190604_1631.py b/core/migrations/0017_auto_20190604_1631.py
deleted file mode 100644
index 55685cdd3d712c9f0c013fcf1949e4eff783caa4..0000000000000000000000000000000000000000
--- a/core/migrations/0017_auto_20190604_1631.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Generated by Django 2.1.4 on 2019-06-04 16:31
-
-import core.models.user_account
-import django.contrib.auth.models
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0016_auto_20190521_1803'),
-    ]
-
-    operations = [
-        migrations.AlterModelManagers(
-            name='useraccount',
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-                ('corrector', core.models.user_account.TutorReviewerManager()),
-            ],
-        ),
-    ]
diff --git a/core/migrations/0018_auto_20190709_1526.py b/core/migrations/0018_auto_20190709_1526.py
deleted file mode 100644
index 468d24424520ecc7a31f8df4c7277e6568e5f670..0000000000000000000000000000000000000000
--- a/core/migrations/0018_auto_20190709_1526.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 2.2 on 2019-07-09 15:26
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0017_auto_20190604_1631'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='feedback',
-            name='final_by_reviewer',
-            field=models.BooleanField(default=False),
-        ),
-        migrations.AlterField(
-            model_name='feedbacklabel',
-            name='name',
-            field=models.CharField(max_length=50, unique=True),
-        ),
-    ]
diff --git a/core/migrations/0018_auto_20190814_1324.py b/core/migrations/0018_auto_20190814_1324.py
deleted file mode 100644
index 68d141b6b8dffc9442e9a1e04b5c433872e3990f..0000000000000000000000000000000000000000
--- a/core/migrations/0018_auto_20190814_1324.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.1.11 on 2019-08-14 13:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0017_auto_20190604_1631'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='feedbacklabel',
-            name='name',
-            field=models.CharField(max_length=50, unique=True),
-        ),
-    ]
diff --git a/core/migrations/0019_merge_20190814_1437.py b/core/migrations/0019_merge_20190814_1437.py
deleted file mode 100644
index be3acd875f07d9910f23a48e771324e0d1b25e51..0000000000000000000000000000000000000000
--- a/core/migrations/0019_merge_20190814_1437.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 2.1.11 on 2019-08-14 14:37
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0018_auto_20190709_1526'),
-        ('core', '0018_auto_20190814_1324'),
-    ]
-
-    operations = [
-    ]
diff --git a/core/migrations/0020_auto_20190831_1417.py b/core/migrations/0020_auto_20190831_1417.py
deleted file mode 100644
index f6f6281a8b8aea11ca7efc0f66c02b373615fb99..0000000000000000000000000000000000000000
--- a/core/migrations/0020_auto_20190831_1417.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.1.11 on 2019-08-31 14:17
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0019_merge_20190814_1437'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='submissiontype',
-            name='programming_language',
-            field=models.CharField(choices=[('c', 'C syntax highlighting'), ('java', 'Java syntax highlighting'), ('mipsasm', 'Mips syntax highlighting'), ('haskell', 'Haskell syntax highlighting'), ('plaintext', 'No syntax highlighting')], default='c', max_length=25),
-        ),
-    ]
diff --git a/core/migrations/0021_merge_20190902_1246.py b/core/migrations/0021_merge_20190902_1246.py
deleted file mode 100644
index aba1adb9618428670de6e81ec9c8c324d5e4709b..0000000000000000000000000000000000000000
--- a/core/migrations/0021_merge_20190902_1246.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 2.1.11 on 2019-09-02 12:46
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0016_solutioncomment'),
-        ('core', '0020_auto_20190831_1417'),
-    ]
-
-    operations = [
-    ]
diff --git a/core/migrations/0022_auto_20191012_1539.py b/core/migrations/0022_auto_20191012_1539.py
deleted file mode 100644
index 936f4923e21c8b32d5d21a3b1e1318e54a676b5f..0000000000000000000000000000000000000000
--- a/core/migrations/0022_auto_20191012_1539.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 2.1.11 on 2019-10-12 15:39
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0021_merge_20190902_1246'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='submission',
-            name='source_code',
-            field=models.TextField(blank=True, editable=False, null=True),
-        ),
-        migrations.AddField(
-            model_name='submission',
-            name='source_code_available',
-            field=models.BooleanField(default=False, editable=False),
-        ),
-    ]
diff --git a/core/migrations/0022_remove_feedback_origin.py b/core/migrations/0022_remove_feedback_origin.py
deleted file mode 100644
index 798dccf984558b12ce29f2cf5a65decf7df0d114..0000000000000000000000000000000000000000
--- a/core/migrations/0022_remove_feedback_origin.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 2.1.13 on 2019-10-12 15:11
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0021_merge_20190902_1246'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='feedback',
-            name='origin',
-        ),
-    ]
diff --git a/core/migrations/0023_merge_20191013_1908.py b/core/migrations/0023_merge_20191013_1908.py
deleted file mode 100644
index 070a368db74e2d2a566ec852c917a207bd94b3aa..0000000000000000000000000000000000000000
--- a/core/migrations/0023_merge_20191013_1908.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 2.1.11 on 2019-10-13 19:08
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0022_remove_feedback_origin'),
-        ('core', '0022_auto_20191012_1539'),
-    ]
-
-    operations = [
-    ]
diff --git a/core/models/__init__.py b/core/models/__init__.py
index 47ab0c28db6506e4a7cd3ceadcf0c5c7b3817354..02a2341c18ae74a55059916288c9bb088562d3ae 100644
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -1,3 +1,4 @@
+from .group import Group    # noqa
 from .exam_type import ExamType  # noqa
 from .submission_type import SubmissionType, SolutionComment  # noqa
 from .user_account import UserAccount, TutorReviewerManager  # noqa
@@ -6,7 +7,7 @@ from .student_info import StudentInfo, random_matrikel_no  # noqa
 from .test import Test  # noqa
 from .submission import Submission, MetaSubmission  # noqa
 from .feedback import Feedback, FeedbackComment  # noqa
-from .subscription import (NotMoreThanTwoOpenAssignmentsAllowed, SubmissionSubscription,  # noqa
-                           SubscriptionTemporarilyEnded, SubscriptionEnded)  # noqa
-from .assignment import DeletionOfDoneAssignmentsNotPermitted, TutorSubmissionAssignment  # noqa
+from .assignment import (DeletionOfDoneAssignmentsNotPermitted, TutorSubmissionAssignment,  # noqa
+                         CanOnlyCallFinishOnUnfinishedAssignments, SubmissionTypeDepleted,  # noqa
+                         NotMoreThanTwoOpenAssignmentsAllowed)  # noqa
 from .label import FeedbackLabel  # noqa
diff --git a/core/models/assignment.py b/core/models/assignment.py
index 2f9151bcbaa9416d589ad238f3a4229855715442..8936bf9ab84473704401416279d02a232f2e4765 100644
--- a/core/models/assignment.py
+++ b/core/models/assignment.py
@@ -4,7 +4,7 @@ import uuid
 import constance
 from django.db import models
 
-from core.models.submission import Submission
+from core.models import Submission, UserAccount, MetaSubmission
 
 log = logging.getLogger(__name__)
 config = constance.config
@@ -18,7 +18,61 @@ class CanOnlyCallFinishOnUnfinishedAssignments(Exception):
     pass
 
 
+class SubmissionTypeDepleted(Exception):
+    pass
+
+
+class NotMoreThanTwoOpenAssignmentsAllowed(Exception):
+    pass
+
+
+class TutorSubmissionAssignmentManager(models.Manager):
+
+    @staticmethod
+    def available_assignments(create_assignment_options):
+        stage = create_assignment_options['stage']
+        owner = create_assignment_options['owner']
+        submission_type = create_assignment_options['submission_type']
+        group = create_assignment_options.get('group')
+
+        stage = TutorSubmissionAssignment.assignment_count_on_stage[stage]
+        candidates = MetaSubmission.objects.filter(
+            submission__type__pk=submission_type,
+            done_assignments=stage,
+            has_final_feedback=False,
+            has_active_assignment=False,
+        ).exclude(
+            feedback_authors=owner
+        )
+        if group is not None:
+            candidates = candidates.filter(
+                submission__student__user__exercise_groups__pk=group
+            )
+        return candidates
+
+
 class TutorSubmissionAssignment(models.Model):
+    objects = TutorSubmissionAssignmentManager()
+
+    FEEDBACK_CREATION = 'feedback-creation'
+    FEEDBACK_VALIDATION = 'feedback-validation'
+    FEEDBACK_REVIEW = 'feedback-review'
+
+    stages = (
+        (FEEDBACK_CREATION, 'No feedback was ever assigned'),
+        (FEEDBACK_VALIDATION, 'Feedback exists but is not validated'),
+        (FEEDBACK_REVIEW, 'Review by exam reviewer required'),
+    )
+
+    assignment_count_on_stage = {
+        FEEDBACK_CREATION: 0,
+        FEEDBACK_VALIDATION: 1,
+        FEEDBACK_REVIEW: 2,
+    }
+
+    owner = models.ForeignKey(UserAccount,
+                              on_delete=models.CASCADE,
+                              related_name='assignments')
 
     assignment_id = models.UUIDField(primary_key=True,
                                      default=uuid.uuid4,
@@ -26,14 +80,16 @@ class TutorSubmissionAssignment(models.Model):
     submission = models.ForeignKey(Submission,
                                    on_delete=models.CASCADE,
                                    related_name='assignments')
-    subscription = models.ForeignKey('SubmissionSubscription',
-                                     on_delete=models.CASCADE,
-                                     related_name='assignments')
+
+    stage = models.CharField(choices=stages,
+                             max_length=60,
+                             default=FEEDBACK_CREATION)
+
     is_done = models.BooleanField(default=False)
     created = models.DateTimeField(auto_now_add=True)
 
     def __str__(self):
-        return (f'{self.subscription.owner} assigned to {self.submission}'
+        return (f'{self.owner} assigned to {self.submission}'
                 f' (done={self.is_done})')
 
     def finish(self):
@@ -42,7 +98,7 @@ class TutorSubmissionAssignment(models.Model):
             raise CanOnlyCallFinishOnUnfinishedAssignments()
 
         meta = self.submission.meta
-        meta.feedback_authors.add(self.subscription.owner)
+        meta.feedback_authors.add(self.owner)
         meta.done_assignments += 1
         meta.has_active_assignment = False
         self.is_done = True
@@ -53,6 +109,3 @@ class TutorSubmissionAssignment(models.Model):
         if self.is_done:
             raise DeletionOfDoneAssignmentsNotPermitted()
         super().delete(*args, **kwargs)
-
-    class Meta:
-        unique_together = ('submission', 'subscription')
diff --git a/core/models/group.py b/core/models/group.py
new file mode 100644
index 0000000000000000000000000000000000000000..02c40a1d45966c5b86a4aa1bf857b00fb7820a82
--- /dev/null
+++ b/core/models/group.py
@@ -0,0 +1,9 @@
+from django.db import models
+import uuid
+
+
+class Group(models.Model):
+    group_id = models.UUIDField(primary_key=True,
+                                default=uuid.uuid4,
+                                editable=False)
+    name = models.CharField(max_length=120)
diff --git a/core/models/subscription.py b/core/models/subscription.py
deleted file mode 100644
index 6788aad671723d518e0719df3338eb3d50c6e252..0000000000000000000000000000000000000000
--- a/core/models/subscription.py
+++ /dev/null
@@ -1,205 +0,0 @@
-import logging
-import secrets
-import uuid
-
-import constance
-from django.contrib.auth import get_user_model
-from django.db import models, transaction
-from django.db.models import (Q, QuerySet)
-
-from core.models.submission import MetaSubmission
-from core.models.assignment import TutorSubmissionAssignment
-
-log = logging.getLogger(__name__)
-config = constance.config
-
-
-class SubscriptionEnded(Exception):
-    pass
-
-
-class SubscriptionTemporarilyEnded(Exception):
-    pass
-
-
-class NotMoreThanTwoOpenAssignmentsAllowed(Exception):
-    pass
-
-
-def get_random_element_from_queryset(queryset):
-    qs_elements = queryset.all()
-    length = len(qs_elements)
-    index = secrets.choice(range(length))
-    return qs_elements[index]
-
-
-class SubmissionSubscription(models.Model):
-
-    RANDOM = 'random'
-    STUDENT_QUERY = 'student'
-    EXAM_TYPE_QUERY = 'exam'
-    SUBMISSION_TYPE_QUERY = 'submission_type'
-
-    type_query_mapper = {
-        RANDOM: '__any',
-        STUDENT_QUERY: 'student__pk',
-        EXAM_TYPE_QUERY: 'student__exam__pk',
-        SUBMISSION_TYPE_QUERY: 'type__pk',
-    }
-
-    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'),
-    )
-
-    FEEDBACK_CREATION = 'feedback-creation'
-    FEEDBACK_VALIDATION = 'feedback-validation'
-    FEEDBACK_CONFLICT_RESOLUTION = 'feedback-conflict-resolution'
-
-    assignment_count_on_stage = {
-        FEEDBACK_CREATION: 0,
-        FEEDBACK_VALIDATION: 1,
-        FEEDBACK_CONFLICT_RESOLUTION: 2,
-    }
-
-    stages = (
-        (FEEDBACK_CREATION, 'No feedback was ever assigned'),
-        (FEEDBACK_VALIDATION, 'Feedback exists but is not validated'),
-        (FEEDBACK_CONFLICT_RESOLUTION, 'Previous correctors disagree'),
-    )
-
-    deactivated = models.BooleanField(default=False)
-    subscription_id = models.UUIDField(primary_key=True,
-                                       default=uuid.uuid4,
-                                       editable=False)
-    owner = models.ForeignKey(get_user_model(),
-                              on_delete=models.CASCADE,
-                              related_name='subscriptions')
-    query_key = models.UUIDField(null=True)
-    query_type = models.CharField(max_length=75,
-                                  choices=QUERY_CHOICE,
-                                  default=RANDOM)
-    feedback_stage = models.CharField(choices=stages,
-                                      max_length=40,
-                                      default=FEEDBACK_CREATION)
-
-    class Meta:
-        unique_together = ('owner',
-                           'query_key',
-                           'query_type',
-                           'feedback_stage')
-
-    def _get_submission_base_query(self) -> QuerySet:
-        """ Get all submissions that are filtered by the query key and type,
-        e.g. all submissions of one student or submission type.
-        """
-        if self.query_type == self.RANDOM:
-            return MetaSubmission.objects.all()
-
-        return MetaSubmission.objects.filter(
-            **{'submission__' + self.type_query_mapper[self.query_type]:
-               self.query_key})
-
-    def _get_submissions_that_do_not_have_final_feedback(self) -> QuerySet:
-        """ There are a number of conditions to check for each submission
-
-        1. The submission does not have final feedback
-        2. The submission was not shown to this user before
-        3. The submission is not currently assigned to somebody else
-
-        Returns:
-            QuerySet -- a list of all submissions ready for consumption
-        """
-        return self._get_submission_base_query() \
-            .select_for_update(of=('self',)).exclude(
-            Q(has_final_feedback=True) |
-            Q(has_active_assignment=True) |
-            Q(feedback_authors=self.owner)
-        )
-
-    def _get_available_submissions_in_subscription_stage(self) -> QuerySet:
-        """ Another filter this time it returns all the submissions that
-        are valid in this stage. That means all previous stages have been
-        completed.
-
-        Raises:
-            SubscriptionEnded -- if the subscription will not yield
-                                 subscriptions in the future
-            SubscriptionTemporarilyEnded -- wait until new become available
-        """
-        candidates = self._get_submissions_that_do_not_have_final_feedback()
-
-        if candidates.count() == 0:
-            raise SubscriptionEnded(
-                f'The task which user {self.owner} subscribed to is done')
-
-        done_assignments_count = self.assignment_count_on_stage[self.feedback_stage]  # noqa
-        stage_candidates = candidates.filter(
-            done_assignments=done_assignments_count,
-        )
-
-        if stage_candidates.count() == 0:
-            raise SubscriptionTemporarilyEnded(
-                'Currently unavailable. Please check for more soon. '
-                'Submissions remaining: %s' % stage_candidates.count())
-
-        if (config.STOP_ON_PASS and
-                self.feedback_stage == self.FEEDBACK_CREATION):
-            stage_candidates = stage_candidates.exclude(
-                Q(submission__student__passes_exam=True) &
-                Q(submission__student__exam__pass_only=True)
-            )
-
-        return stage_candidates
-
-    @transaction.atomic
-    def get_remaining_not_final(self) -> int:
-        return self._get_submissions_that_do_not_have_final_feedback().count()
-
-    @transaction.atomic
-    def get_available_in_stage(self) -> int:
-        try:
-            return self._get_available_submissions_in_subscription_stage().count()  # noqa
-        except (SubscriptionTemporarilyEnded, SubscriptionEnded):
-            return 0
-
-    @transaction.atomic
-    def get_or_create_work_assignment(self):
-        taskqueryset = self._get_available_submissions_in_subscription_stage()
-        task = get_random_element_from_queryset(taskqueryset)
-        if self.assignments.filter(is_done=False).count() >= 2:
-            raise NotMoreThanTwoOpenAssignmentsAllowed(
-                'Not more than 2 active assignments allowed.')
-
-        log.info(f'{self.owner} is assigned to {task} ({self.feedback_stage})')
-        return TutorSubmissionAssignment.objects.create(
-            subscription=self,
-            submission=task.submission)
-
-    @transaction.atomic
-    def reserve_all_assignments_for_a_student(self):
-        assert self.query_type == self.STUDENT_QUERY
-
-        meta_submissions = self._get_submissions_that_do_not_have_final_feedback()  # noqa
-
-        for meta in meta_submissions:
-            submission = meta.submission
-            if hasattr(submission, 'assignments'):
-                submission.assignments.filter(is_done=False).delete()
-            TutorSubmissionAssignment.objects.create(
-                subscription=self,
-                submission=submission
-            )
-
-        log.info(f'Loaded all subscriptions of student {self.query_key}')
-
-    @transaction.atomic
-    def delete(self):
-        self.assignments.filter(is_done=False).delete()
-        if self.assignments.count() == 0:
-            super().delete()
-        else:
-            self.deactivated = True
-            self.save()
diff --git a/core/models/user_account.py b/core/models/user_account.py
index c2e11d96a6fb6a3a93279ef960dfc72dfaa97628..da8426bf5da6cd133cb09eaf20e5faf44140f6dc 100644
--- a/core/models/user_account.py
+++ b/core/models/user_account.py
@@ -8,6 +8,8 @@ from django.db.models import (Case, Count, IntegerField, Q,
                               Value, When)
 from django.apps import apps
 
+from core.models import Group
+
 log = logging.getLogger(__name__)
 config = constance.config
 
@@ -22,18 +24,18 @@ class TutorReviewerManager(UserManager):
         def _get_counter(stage):
             return Count(Case(
                 When(
-                    Q(subscriptions__feedback_stage=stage) &
-                    Q(subscriptions__assignments__is_done=True),
+                    Q(assignments__stage=stage) &
+                    Q(assignments__is_done=True),
                     then=Value(1))),
                 output_field=IntegerField())
 
-        submission_subscription_model = apps.get_model('core', 'SubmissionSubscription')  # noqa
+        assignment_model = apps.get_model('core', 'TutorSubmissionAssignment')  # noqa
 
         return self.get_queryset() \
             .annotate(feedback_created=_get_counter(
-                submission_subscription_model.FEEDBACK_CREATION)) \
+                assignment_model.FEEDBACK_CREATION)) \
             .annotate(feedback_validated=_get_counter(
-                submission_subscription_model.FEEDBACK_VALIDATION))
+                assignment_model.FEEDBACK_VALIDATION))
 
 
 class UserAccount(AbstractUser):
@@ -60,6 +62,10 @@ class UserAccount(AbstractUser):
                                default=uuid.uuid4,
                                editable=False)
 
+    exercise_groups = models.ManyToManyField(Group,
+                                             blank=True,
+                                             related_name='users')
+
     fullname = models.CharField('full name', max_length=70, blank=True)
     is_admin = models.BooleanField(default=False)
 
diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py
index 4643d13adfcbabc3110212933c10f1a3722c7084..f601dbf17adef8b165efea0dd05bfa866b7b4951 100644
--- a/core/serializers/__init__.py
+++ b/core/serializers/__init__.py
@@ -1,10 +1,11 @@
 from .common_serializers import *  # noqa
+from .group import GroupSerializer  # noqa
 from .submission_type import (SubmissionTypeListSerializer, SubmissionTypeSerializer,  # noqa
                               SolutionCommentSerializer)  # noqa
 from .feedback import (FeedbackSerializer,  # noqa
                        FeedbackCommentSerializer,  # noqa
                        VisibleCommentFeedbackSerializer)  # noqa
-from .subscription import *  # noqa
+from .assignment import *  # noqa
 from .student import *  # noqa
 from .submission import *  # noqa
 from .tutor import CorrectorSerializer  # noqa
diff --git a/core/serializers/assignment.py b/core/serializers/assignment.py
new file mode 100644
index 0000000000000000000000000000000000000000..faef140477373d827ab6c8de4d524691a35b8fd8
--- /dev/null
+++ b/core/serializers/assignment.py
@@ -0,0 +1,72 @@
+import secrets
+
+from rest_framework import serializers
+
+from core import models
+from core.models import (Submission, TutorSubmissionAssignment)
+from core.serializers import (DynamicFieldsModelSerializer, FeedbackSerializer,
+                              TestSerializer)
+
+
+class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
+    full_score = serializers.ReadOnlyField(source='type.full_score')
+    tests = TestSerializer(many=True, read_only=True)
+
+    class Meta:
+        model = Submission
+        fields = ('pk', 'type', 'text', 'full_score', 'tests', 'source_code_available')
+        read_only_fields = ('text', 'type')
+
+
+class AssignmentSerializer(DynamicFieldsModelSerializer):
+
+    class Meta:
+        model = TutorSubmissionAssignment
+        fields = ('pk', 'submission', 'is_done', 'owner', 'stage')
+        read_only_fields = ('is_done', 'submission', 'owner')
+
+
+class AssignmentDetailSerializer(AssignmentSerializer):
+    feedback = FeedbackSerializer(source='submission.feedback', read_only=True)
+    submission = SubmissionAssignmentSerializer(read_only=True)
+    submission_type = serializers.UUIDField(write_only=True)
+    group = serializers.UUIDField(write_only=True, required=False)
+
+    class Meta:
+        model = TutorSubmissionAssignment
+        fields = ('pk', 'submission', 'feedback', 'is_done',
+                  'owner', 'stage', 'submission_type', 'group')
+        read_only_fields = ('is_done', 'submission', 'owner')
+
+    def create(self, validated_data):
+        owner = self.context['request'].user
+
+        open_assignments = TutorSubmissionAssignment.objects.filter(
+            owner=owner,
+            is_done=False,
+        )
+
+        if len(open_assignments) > 2:
+            raise models.NotMoreThanTwoOpenAssignmentsAllowed(
+                'Not more than two active assignments allowed'
+            )
+
+        candidates = TutorSubmissionAssignment.objects.available_assignments({
+            **validated_data,
+            'owner': owner
+        })
+
+        length = len(candidates)
+
+        if length == 0:
+            raise models.SubmissionTypeDepleted(
+                'There are no submissions left for the given criteria'
+            )
+
+        index = secrets.choice(range(length))
+
+        return TutorSubmissionAssignment.objects.create(
+            submission=candidates[index].submission,
+            owner=owner,
+            stage=validated_data.get('stage')
+        )
diff --git a/core/serializers/common_serializers.py b/core/serializers/common_serializers.py
index 94e22592b3b4f93734607b21f82092f6cde5d63c..b32d937b71f206451559183c6243aef9406c6695 100644
--- a/core/serializers/common_serializers.py
+++ b/core/serializers/common_serializers.py
@@ -8,6 +8,7 @@ from rest_framework import serializers
 from rest_framework.utils import html
 
 from core import models
+from core.serializers.group import GroupSerializer
 
 from .generic import DynamicFieldsModelSerializer
 
@@ -30,6 +31,7 @@ class TestSerializer(DynamicFieldsModelSerializer):
 
 
 class UserAccountSerializer(DynamicFieldsModelSerializer):
+    exercise_groups = GroupSerializer(many=True)
 
     def validate(self, data):
         password = data.get('password')
@@ -44,8 +46,8 @@ class UserAccountSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = models.UserAccount
-        fields = ('pk', 'username', 'role', 'is_admin', 'password')
-        read_only_fields = ('pk', 'username', 'role', 'is_admin')
+        fields = ('pk', 'username', 'role', 'is_admin', 'password', 'exercise_groups')
+        read_only_fields = ('pk', 'username', 'role', 'is_admin', 'exercise_groups')
         extra_kwargs = {'password': {'write_only': True}}
 
 
diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py
index 000b0eb877d9fbb232e7016c45df75fc475ec482..1fdac79255a13d4e9a8e306a43274b49d2ecff33 100644
--- a/core/serializers/feedback.py
+++ b/core/serializers/feedback.py
@@ -1,5 +1,6 @@
 import logging
 
+import constance
 from django.db import transaction
 from rest_framework import serializers
 
@@ -10,6 +11,7 @@ from util.factories import GradyUserFactory
 
 from .generic import DynamicFieldsModelSerializer
 
+config = constance.config
 log = logging.getLogger(__name__)
 user_factory = GradyUserFactory()
 
@@ -62,13 +64,12 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
         if user.role == models.UserAccount.REVIEWER:
             return None
 
-        assignments = obj.of_submission.assignments.filter(
-            subscription__owner=user)
+        assignments = obj.of_submission.assignments.filter(owner=user)
 
         if assignments.count() == 0:
             return None
 
-        return assignments[0].subscription.feedback_stage
+        return assignments[0].stage
 
     @transaction.atomic
     def create(self, validated_data) -> Feedback:
@@ -76,9 +77,15 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
         feedback_lines = validated_data.pop('feedback_lines', [])
         labels = validated_data.pop('labels', [])
         user = self.context['request'].user
-        final_by_reviewer = validated_data.get('is_final', False) and \
+        if config.SINGLE_CORRECTION:
+            is_final = True
+            validated_data.pop('is_final')
+        else:
+            is_final = validated_data.pop('is_final', False)
+        final_by_reviewer = is_final and \
             user.role == UserAccount.REVIEWER
         feedback = Feedback.objects.create(of_submission=submission,
+                                           is_final=is_final,
                                            final_by_reviewer=final_by_reviewer,
                                            **validated_data)
         for label in labels:
diff --git a/core/serializers/group.py b/core/serializers/group.py
new file mode 100644
index 0000000000000000000000000000000000000000..b094a030baa4f9435293e094717cba50c8e1f23f
--- /dev/null
+++ b/core/serializers/group.py
@@ -0,0 +1,9 @@
+from rest_framework import serializers
+
+from core.models import Group
+
+
+class GroupSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Group
+        fields = ('pk', 'name')
diff --git a/core/serializers/subscription.py b/core/serializers/subscription.py
deleted file mode 100644
index 4f5d9dbfa77c584ccdf9dcf172142117ccec4459..0000000000000000000000000000000000000000
--- a/core/serializers/subscription.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from rest_framework import serializers
-
-from core.models import (Submission, SubmissionSubscription,
-                         TutorSubmissionAssignment)
-from core.serializers import (DynamicFieldsModelSerializer, FeedbackSerializer,
-                              TestSerializer)
-
-
-class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
-    text = serializers.ReadOnlyField()
-    type = serializers.ReadOnlyField(source='type.pk')
-    full_score = serializers.ReadOnlyField(source='type.full_score')
-    tests = TestSerializer(many=True, read_only=True)
-
-    class Meta:
-        model = Submission
-        fields = ('pk', 'type', 'text', 'full_score', 'tests', 'source_code_available')
-
-
-class AssignmentSerializer(DynamicFieldsModelSerializer):
-    owner = serializers.StringRelatedField(source='subscription.owner')
-    stage = serializers.StringRelatedField(
-        source='subscription.feedback_stage')
-
-    class Meta:
-        model = TutorSubmissionAssignment
-        fields = ('pk', 'submission', 'is_done', 'owner', 'stage')
-        read_only_fields = ('is_done', 'submission')
-
-
-class AssignmentDetailSerializer(AssignmentSerializer):
-    feedback = FeedbackSerializer(source='submission.feedback', read_only=True)
-    submission = SubmissionAssignmentSerializer(read_only=True)
-
-    class Meta:
-        model = TutorSubmissionAssignment
-        fields = ('pk', 'submission', 'feedback', 'is_done', 'subscription',
-                  'owner', 'stage')
-        read_only_fields = ('is_done', 'submission', 'feedback')
-
-    def create(self, validated_data):
-        subscription = validated_data.get('subscription')
-        return subscription.get_or_create_work_assignment()
-
-
-class SubscriptionSerializer(DynamicFieldsModelSerializer):
-    owner = serializers.ReadOnlyField(source='owner.username')
-    query_key = serializers.UUIDField(required=False)
-    assignments = serializers.SerializerMethodField()
-    remaining = serializers.SerializerMethodField()
-    available = serializers.SerializerMethodField()
-
-    def get_assignments(self, subscription):
-        queryset = subscription.assignments.filter(is_done=False)
-        serializer = AssignmentDetailSerializer(queryset, many=True)
-        return serializer.data
-
-    def get_remaining(self, subscription):
-        return subscription.get_remaining_not_final()
-
-    def get_available(self, subscription):
-        return subscription.get_available_in_stage()
-
-    def validate(self, data):
-        data['owner'] = self.context['request'].user
-
-        if 'query_key' in data != \
-                data['query_type'] == SubmissionSubscription.RANDOM:
-            raise serializers.ValidationError(
-                f'The {data["query_type"]} query_type does not work with the'
-                f'provided key')
-
-        elif 'query_key' in data:
-            query_type = data.get('query_type')
-            query_key = data.get('query_key')
-
-            select = SubmissionSubscription.type_query_mapper[query_type]
-            if Submission.objects.filter(**{select: query_key}).count() == 0:
-                raise serializers.ValidationError(
-                    'Seems no submissions exist for given query and key')
-
-        return data
-
-    def create(self, validated_data) -> SubmissionSubscription:
-        return SubmissionSubscription.objects.create(**validated_data)
-
-    def update(self, instance, validated_data):
-        instance.deactivated = False
-        instance.save()
-        return instance
-
-    class Meta:
-        model = SubmissionSubscription
-        fields = ('pk',
-                  'owner',
-                  'query_type',
-                  'query_key',
-                  'feedback_stage',
-                  'deactivated',
-                  'assignments',
-                  'remaining',
-                  'available')
-        read_only_fields = ('deactivated',)
diff --git a/core/tests/test_assignment_views.py b/core/tests/test_assignment_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c2373c0f80f508f0be08c86d5f631ba7713f316
--- /dev/null
+++ b/core/tests/test_assignment_views.py
@@ -0,0 +1,306 @@
+from rest_framework import status
+from rest_framework.test import APIClient, APITestCase
+
+from core import models
+from core.models import (TutorSubmissionAssignment)
+from util.factories import make_test_data
+
+
+class TestApiEndpoints(APITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.data = make_test_data(data_dict={
+            'exams': [{
+                'module_reference': 'Test Exam 01',
+                'total_score': 100,
+                'pass_score': 60,
+            }],
+            'submission_types': [
+                {
+                    'name': '01. Sort this or that',
+                    'full_score': 35,
+                    'description': 'Very complicated',
+                    'solution': 'Trivial!'
+                },
+                {
+                    'name': '02. Merge this or that or maybe even this',
+                    'full_score': 35,
+                    'description': 'Very complicated',
+                    'solution': 'Trivial!'
+                }
+            ],
+            'students': [
+                {
+                    'username': 'student01',
+                    'exam': 'Test Exam 01'
+                },
+                {
+                    'username': 'student02',
+                    'exam': 'Test Exam 01'
+                }
+            ],
+            'tutors': [
+                {'username': 'tutor01'},
+                {'username': 'tutor02'}
+            ],
+            'reviewers': [
+                {'username': 'reviewer'}
+            ],
+            'submissions': [
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student01',
+                    'feedback': {
+                        'score': 5,
+                        'is_final': True,
+                        'feedback_lines': {
+                            '1': [{
+                                'text': 'This is very bad!',
+                                'of_tutor': 'tutor01'
+                            }],
+                        }
+
+                    }
+                },
+                {
+                    'text': 'function blabl\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student01'
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student02'
+                },
+                {
+                    'text': 'function lorem ipsum etc\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student02'
+                },
+            ]}
+        )
+
+    def setUp(self):
+        self.client = APIClient()
+
+    def test_tutor_gets_an_assignment(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+
+    # we should simply test if any newly created assignment is unfinished
+    def test_first_work_assignment_was_created_unfinished(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertFalse(TutorSubmissionAssignment.objects.first().is_done)
+
+    def test_assignment_raises_error_when_depleted(self):
+        self.data['submissions'][0].delete()
+        self.data['submissions'][2].delete()
+
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code)
+
+    def test_assignment_delete_of_done_not_permitted(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        first = TutorSubmissionAssignment.objects.first()
+        first.is_done = True
+        first.save()
+
+        self.assertRaises(models.DeletionOfDoneAssignmentsNotPermitted,
+                          first.delete)
+
+    def test_assignment_delete_undone_permitted(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        first = TutorSubmissionAssignment.objects.first()
+        first.delete()
+
+        self.assertEqual(0, TutorSubmissionAssignment.objects.all().count())
+
+    def tutor_can_release_own_unfinished_assignments(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+        self.client.post(
+            f'/api/assignment/{response.data["pk"]}/finish/', {
+                "score": 23,
+                "of_submission": response.data['submission']['pk'],
+                "feedback_lines": {
+                    1: {"text": "< some string >", "labels": []},
+                    2: {"text": "< some string >", "labels": []}
+                },
+                "labels": [],
+            }
+        )
+        self.assertEqual(2, TutorSubmissionAssignment.objects.all().count())
+        self.assertEqual(1, TutorSubmissionAssignment.objects.filter(is_done=True).count())
+
+        self.client.force_authenticate(user=self.data['tutors'][1])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+
+        response = self.client.delete('/api/assignment/')
+        self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
+        self.assertEqual(2, TutorSubmissionAssignment.objects.all().count())
+
+    def test_two_tutors_cant_have_assignments_for_same_submission(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        assignment_fst_tutor = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][1].pk,
+            "stage": "feedback-creation",
+        }).data
+
+        self.client.force_authenticate(user=self.data['tutors'][1])
+
+        assignment_snd_tutor = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][1].pk,
+            "stage": "feedback-creation",
+        }).data
+
+        self.assertNotEqual(assignment_fst_tutor['submission']['pk'],
+                            assignment_snd_tutor['submission']['pk'])
+
+    def test_reviewer_can_get_active_assignments(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        assignment = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        }).data
+
+        # tutors shouldn't have access
+        res = self.client.get('/api/assignment/active/')
+        self.assertEqual(status.HTTP_403_FORBIDDEN, res.status_code)
+
+        self.client.force_authenticate(user=self.data['reviewers'][0])
+
+        active_assignments = self.client.get('/api/assignment/active/').data
+        self.assertIn(assignment['pk'], [assignment['pk'] for assignment in active_assignments])
+
+    def test_reviewer_can_delete_active_assignments(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        assignment = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        }).data
+
+        # tutors shouldn't have access
+        res = self.client.delete('/api/assignment/active/')
+        self.assertEqual(status.HTTP_403_FORBIDDEN, res.status_code)
+
+        self.client.force_authenticate(user=self.data['reviewers'][0])
+
+        res = self.client.delete('/api/assignment/active/')
+        self.assertEqual(status.HTTP_204_NO_CONTENT, res.status_code)
+        self.assertNotIn(
+            assignment['pk'],
+            [assignment.pk for assignment
+             in TutorSubmissionAssignment.objects.filter(is_done=False)]
+        )
+
+    def test_all_stages_of_the_subscription(self):
+        self.client.force_authenticate(user=self.data['tutors'][0])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-creation",
+        })
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+        response = self.client.post(
+            f'/api/assignment/{response.data["pk"]}/finish/', {
+                "score": 23,
+                "of_submission": response.data['submission']['pk'],
+                "feedback_lines": {
+                    1: {"text": "< some string >", "labels": []},
+                    2: {"text": "< some string >", "labels": []}
+                },
+                "labels": [],
+            }
+        )
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+
+        # some other tutor reviews it
+        self.client.force_authenticate(user=self.data['tutors'][1])
+
+        response = self.client.post('/api/assignment/', {
+            "submission_type": self.data['submission_types'][0].pk,
+            "stage": "feedback-validation",
+        })
+
+        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
+        submission_id_in_database = models.Feedback.objects.filter(
+            is_final=False).first().of_submission.submission_id
+        submission_id_in_response = response.data['submission']['pk']
+
+        self.assertEqual(
+            str(submission_id_in_database),
+            submission_id_in_response)
+
+        assignment = models.TutorSubmissionAssignment.objects.get(pk=response.data['pk'])
+        self.assertFalse(assignment.is_done)
+        response = self.client.post(
+            f'/api/assignment/{assignment.pk}/finish/', {
+                "score": 20,
+                "is_final": True,
+                "feedback_lines": {
+                    2: {"text": "< some addition by second tutor>"},
+                }
+            }
+        )
+
+        assignment.refresh_from_db()
+        meta = assignment.submission.meta
+        self.assertEqual(status.HTTP_200_OK, response.status_code)
+        self.assertEqual(2, len(response.data['feedback_lines'][2]))
+        self.assertTrue(assignment.is_done)
+        self.assertIn(self.data['tutors'][0], meta.feedback_authors.all())
+        self.assertIn(self.data['tutors'][1], meta.feedback_authors.all())
diff --git a/core/tests/test_custom_subscription_filter.py b/core/tests/test_custom_subscription_filter.py
deleted file mode 100644
index c6ac17671dc294c5d314d187a9053b197d09e880..0000000000000000000000000000000000000000
--- a/core/tests/test_custom_subscription_filter.py
+++ /dev/null
@@ -1,205 +0,0 @@
-import constance
-from rest_framework.test import APITestCase
-
-from core.models import Feedback, SubmissionSubscription
-from util.factories import make_test_data
-
-config = constance.config
-
-
-class StopOnPass(APITestCase):
-
-    @classmethod
-    def setUpTestData(cls):
-        config.STOP_ON_PASS = True
-
-    def setUp(self):
-        self.data = make_test_data(data_dict={
-            'exams': [
-                {
-                    'module_reference': 'Test Exam 01',
-                    'total_score': 50,
-                    'pass_score': 25,
-                    'pass_only': True
-                },
-                {
-                    'module_reference': 'Test Exam 02',
-                    'total_score': 50,
-                    'pass_score': 25,
-                    'pass_only': False
-                }
-            ],
-            'submission_types': [
-                {
-                    'name': '01. Sort this or that',
-                    'full_score': 20,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
-                },
-                {
-                    'name': '02. Merge this or that or maybe even this',
-                    'full_score': 15,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
-                },
-                {
-                    'name': '03. Super simple task',
-                    'full_score': 15,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
-                },
-            ],
-            'students': [
-                {'username': 'student01', 'exam': 'Test Exam 01'},
-                {'username': 'student02', 'exam': 'Test Exam 02'},
-            ],
-            'tutors': [
-                {'username': 'tutor01'},
-                {'username': 'tutor02'}
-            ],
-            'reviewers': [
-                {'username': 'reviewer'}
-            ],
-            'submissions': [
-                # Student 1
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       for blabla in bla:\n'
-                            '           lorem ipsum und so\n',
-                    'type': '01. Sort this or that',
-                    'user': 'student01',
-                },
-                {
-                    'text': 'function blabl\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '02. Merge this or that or maybe even this',
-                    'user': 'student01'
-                },
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '03. Super simple task',
-                    'user': 'student01'
-                },
-
-                # Student 2
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       for blabla in bla:\n'
-                            '           lorem ipsum und so\n',
-                    'type': '01. Sort this or that',
-                    'user': 'student02',
-                },
-                {
-                    'text': 'function blabl\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '02. Merge this or that or maybe even this',
-                    'user': 'student02'
-                },
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '03. Super simple task',
-                    'user': 'student02'
-                }
-            ]}
-        )
-        self.tutor01 = self.data['tutors'][0]
-        self.tutor02 = self.data['tutors'][1]
-        self.subscription = SubmissionSubscription.objects.create(
-            owner=self.tutor01,
-            feedback_stage=SubmissionSubscription.FEEDBACK_CREATION)
-
-    def test_all_feedback_is_available(self):
-        self.assertEqual(6, self.subscription.get_available_in_stage())
-
-    def test_all_is_available_when_score_is_too_low(self):
-        Feedback.objects.create(
-            of_submission=self.data['submissions'][0], score=20, is_final=True)
-        self.assertEqual(5, self.subscription.get_available_in_stage())
-
-    def test_default_does_not_pass_exam(self):
-        self.assertFalse(self.data['students'][0].student.passes_exam)
-
-    def test_no_more_submissions_after_pass_only_student_passed_exam(self):
-        Feedback.objects.create(
-            of_submission=self.data['submissions'][0], score=20)
-        Feedback.objects.create(
-            of_submission=self.data['submissions'][1], score=15)
-
-        self.data['students'][0].student.refresh_from_db()
-        self.assertEqual(3, self.subscription.get_available_in_stage())
-        self.assertEqual(35, self.data['students'][0].student.total_score)
-        self.assertTrue(self.data['students'][0].student.passes_exam)
-
-    # TODO why is this code commented?!?
-    # def test_submissions_left_after_not_pass_only_student_passed_exam(self):
-    #     Feedback.objects.create(
-    #         of_submission=self.data['submissions'][3], score=20)
-    #     Feedback.objects.create(
-    #         of_submission=self.data['submissions'][4], score=15)
-    #
-    #     self.data['students'][1].student.refresh_from_db()
-    #     self.assertEqual(4, self.subscription.get_available_in_stage())
-    #     self.assertEqual(35, self.data['students'][1].student.total_score)
-    #     self.assertTrue(self.data['students'][1].student.passes_exam)
-
-    def test_validation_still_allowed_when_stop_on_pass(self):
-        subscription_s1 = SubmissionSubscription.objects.create(
-            owner=self.tutor01,
-            feedback_stage=SubmissionSubscription.FEEDBACK_CREATION,
-            query_type=SubmissionSubscription.STUDENT_QUERY,
-            query_key=self.data['students'][0].student.pk
-        )
-        a1 = subscription_s1.get_or_create_work_assignment()
-        a2 = subscription_s1.get_or_create_work_assignment()
-
-        # signals recognize the open assignments
-        Feedback.objects.create(
-            of_submission=a1.submission, score=20)
-        a1.finish()
-        Feedback.objects.create(
-            of_submission=a2.submission, score=15)
-        a2.finish()
-
-        subscription_other_tutor = SubmissionSubscription.objects.create(
-            owner=self.tutor02,
-            feedback_stage=SubmissionSubscription.FEEDBACK_VALIDATION)
-
-        self.assertEqual(0, subscription_s1.get_available_in_stage())
-        self.assertEqual(2, subscription_other_tutor.get_available_in_stage())
-        self.assertEqual(6, subscription_other_tutor.get_remaining_not_final())
-
-    def test_validation_still_allowed_when_not_stop_on_pass(self):
-        subscription_s1 = SubmissionSubscription.objects.create(
-            owner=self.tutor01,
-            feedback_stage=SubmissionSubscription.FEEDBACK_CREATION,
-            query_type=SubmissionSubscription.STUDENT_QUERY,
-            query_key=self.data['students'][1].student.pk
-        )
-        a1 = subscription_s1.get_or_create_work_assignment()
-        a2 = subscription_s1.get_or_create_work_assignment()
-
-        # signals recognize the open assignments
-        Feedback.objects.create(
-            of_submission=a1.submission, score=20)
-        a1.finish()
-        Feedback.objects.create(
-            of_submission=a2.submission, score=15)
-        a2.finish()
-
-        subscription_other_tutor = SubmissionSubscription.objects.create(
-            owner=self.tutor02,
-            feedback_stage=SubmissionSubscription.FEEDBACK_VALIDATION)
-
-        self.assertEqual(1, subscription_s1.get_available_in_stage())
-        self.assertEqual(2, subscription_other_tutor.get_available_in_stage())
-        self.assertEqual(6, subscription_other_tutor.get_remaining_not_final())
diff --git a/core/tests/test_feedback.py b/core/tests/test_feedback.py
index 7217cc8630cfcfd00b4dc96ac666a839e199c235..03947c34515f3229663748031e89f167ab5fc067 100644
--- a/core/tests/test_feedback.py
+++ b/core/tests/test_feedback.py
@@ -3,8 +3,9 @@ import unittest
 from rest_framework import status
 from rest_framework.test import APIRequestFactory, APITestCase
 
-from core import models
-from core.models import Feedback, FeedbackComment, Submission, SubmissionType, FeedbackLabel
+from core.models import (Feedback, FeedbackComment,
+                         Submission, SubmissionType,
+                         FeedbackLabel, TutorSubmissionAssignment)
 from util.factories import GradyUserFactory, make_test_data, make_exams
 
 
@@ -121,11 +122,10 @@ class FeedbackCreateTestCase(APITestCase):
         self.fst_label.refresh_from_db()
         self.snd_label.refresh_from_db()
         self.client.force_authenticate(user=self.tutor)
-        self.subscription = models.SubmissionSubscription.objects.create(
+        self.assignment = TutorSubmissionAssignment.objects.create(
+            submission=Submission.objects.first(),
             owner=self.tutor,
-            query_type='random'
         )
-        self.assignment = self.subscription.get_or_create_work_assignment()
 
     def test_cannot_create_feedback_without_feedback_lines(self):
         # TODO this test has to be adapted to test the various constraints
@@ -395,11 +395,10 @@ class FeedbackPatchTestCase(APITestCase):
         self.tutor01 = self.data['tutors'][0]
         self.tutor02 = self.data['tutors'][1]
         self.client.force_authenticate(user=self.tutor01)
-        self.subscription = models.SubmissionSubscription.objects.create(
+        self.assignment = TutorSubmissionAssignment.objects.create(
+            submission=Submission.objects.first(),
             owner=self.tutor01,
-            query_type='random',
         )
-        self.assignment = self.subscription.get_or_create_work_assignment()
         data = {
             'score': 35,
             'is_final': False,
@@ -455,12 +454,11 @@ class FeedbackPatchTestCase(APITestCase):
     @unittest.expectedFailure
     def test_tutor_can_not_update_when_there_is_a_new_assignment(self):
         # Step 1 - Create a new assignment for Tutor 2
-        second_subs = models.SubmissionSubscription.objects.create(
+        TutorSubmissionAssignment.objects.create(
+            submission=Submission.objects.last(),
             owner=self.tutor02,
-            query_type='random',
-            feedback_stage='feedback-validation'
+            stage='feedback-validation',
         )
-        second_subs.get_or_create_work_assignment()
 
         # Step 2 - Tutor 1 tries to patch
         data = {
diff --git a/core/tests/test_submissiontypeview.py b/core/tests/test_submissiontypeview.py
index 3973d7de02c9b51a3603016941fba0ed85ca6bbe..2db82648570606847d6a0a28226651782f234edc 100644
--- a/core/tests/test_submissiontypeview.py
+++ b/core/tests/test_submissiontypeview.py
@@ -10,6 +10,9 @@ from core.views import SubmissionTypeApiView
 from util.factories import GradyUserFactory
 
 
+# TODO: add tests to test the remaining counts in conjunction with the assignment logic
+# TODO: also test for pass only and stuff
+
 class SubmissionTypeViewTestList(APITestCase):
 
     @classmethod
diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py
deleted file mode 100644
index c4522609815a3e07c7610032f334ac5bcae009fd..0000000000000000000000000000000000000000
--- a/core/tests/test_subscription_assignment_service.py
+++ /dev/null
@@ -1,433 +0,0 @@
-from rest_framework import status
-from rest_framework.test import APIClient, APITestCase
-
-from core import models
-from core.models import (Submission, SubmissionSubscription, SubmissionType,
-                         SubscriptionEnded, SubscriptionTemporarilyEnded, TutorSubmissionAssignment)
-from util.factories import GradyUserFactory, make_test_data, make_exams
-
-
-class SubmissionSubscriptionRandomTest(APITestCase):
-
-    @classmethod
-    def setUpTestData(cls):
-        cls.user_factory = GradyUserFactory()
-
-    def setUp(self):
-        self.t = self.user_factory.make_tutor()
-        self.exam = make_exams(exams=[{
-                'module_reference': 'Test Exam 01',
-                'total_score': 100,
-                'pass_score': 60,
-            }])[0]
-        self.s1 = self.user_factory.make_student(exam=self.exam)
-        self.s2 = self.user_factory.make_student(exam=self.exam)
-
-        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.student,
-            text='I really failed')
-        self.submission_02 = Submission.objects.create(
-            type=self.submission_type, student=self.s2.student,
-            text='I like apples')
-
-        self.subscription = SubmissionSubscription.objects.create(
-            owner=self.t, query_type=SubmissionSubscription.RANDOM)
-
-    def test_subscription_gets_an_assignment(self):
-        self.subscription.get_or_create_work_assignment()
-        self.assertEqual(1, self.subscription.assignments.count())
-
-    def test_first_work_assignment_was_created_unfinished(self):
-        self.subscription.get_or_create_work_assignment()
-        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.get_or_create_work_assignment()
-        except SubscriptionEnded:
-            self.assertFalse(False)
-        else:
-            self.assertTrue(False)
-
-    def test_can_prefetch(self):
-        self.subscription.get_or_create_work_assignment()
-        self.subscription.get_or_create_work_assignment()
-        self.assertEqual(2, self.subscription.assignments.count())
-
-    def test_new_subscription_is_temporarily_unavailabe(self):
-        validation = SubmissionSubscription.objects.create(
-            owner=self.t, query_type=SubmissionSubscription.RANDOM,
-            feedback_stage=SubmissionSubscription.FEEDBACK_VALIDATION)
-        try:
-            validation.get_or_create_work_assignment()
-        except SubscriptionTemporarilyEnded:
-            self.assertTrue(True)
-        else:
-            self.assertTrue(False)
-
-    def test_delete_with_done_assignments_subscription_remains(self):
-        first = self.subscription.get_or_create_work_assignment()
-        self.subscription.get_or_create_work_assignment()
-        self.assertEqual(2, self.subscription.assignments.count())
-
-        first.is_done = True
-        first.save()
-        self.subscription.delete()
-        self.assertEqual(1, self.subscription.assignments.count())
-
-    def test_delete_without_done_assignments_subscription_is_deleted(self):
-        self.subscription.delete()
-        self.assertEqual(0, models.SubmissionSubscription.objects.count())
-
-    def test_assignment_delete_of_done_not_permitted(self):
-        first = self.subscription.get_or_create_work_assignment()
-        first.is_done = True
-        first.save()
-
-        self.assertRaises(models.DeletionOfDoneAssignmentsNotPermitted,
-                          first.delete)
-
-    def test_assignment_delete_undone_permitted(self):
-        first = self.subscription.get_or_create_work_assignment()
-        first.delete()
-        self.assertEqual(0, self.subscription.assignments.count())
-
-
-class TestApiEndpoints(APITestCase):
-
-    @classmethod
-    def setUpTestData(cls):
-        cls.data = make_test_data(data_dict={
-            'exams': [{
-                'module_reference': 'Test Exam 01',
-                'total_score': 100,
-                'pass_score': 60,
-            }],
-            'submission_types': [
-                {
-                    'name': '01. Sort this or that',
-                    'full_score': 35,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
-                },
-                {
-                    'name': '02. Merge this or that or maybe even this',
-                    'full_score': 35,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
-                }
-            ],
-            'students': [
-                {
-                    'username': 'student01',
-                    'exam': 'Test Exam 01'
-                },
-                {
-                    'username': 'student02',
-                    'exam': 'Test Exam 01'
-                }
-            ],
-            'tutors': [
-                {'username': 'tutor01'},
-                {'username': 'tutor02'}
-            ],
-            'reviewers': [
-                {'username': 'reviewer'}
-            ],
-            'submissions': [
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       for blabla in bla:\n'
-                            '           lorem ipsum und so\n',
-                    'type': '01. Sort this or that',
-                    'user': 'student01',
-                    'feedback': {
-                        'score': 5,
-                        'is_final': True,
-                        'feedback_lines': {
-                            '1': [{
-                                'text': 'This is very bad!',
-                                'of_tutor': 'tutor01'
-                            }],
-                        }
-
-                    }
-                },
-                {
-                    'text': 'function blabl\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '02. Merge this or that or maybe even this',
-                    'user': 'student01'
-                },
-                {
-                    'text': 'function blabl\n'
-                            '   on multi lines\n'
-                            '       asasxasx\n'
-                            '           lorem ipsum und so\n',
-                    'type': '01. Sort this or that',
-                    'user': 'student02'
-                },
-                {
-                    'text': 'function lorem ipsum etc\n',
-                    'type': '02. Merge this or that or maybe even this',
-                    'user': 'student02'
-                },
-            ]}
-        )
-
-    def setUp(self):
-        self.client = APIClient()
-
-    def test_ramaining_submissions_for_student(self):
-        self.client.force_authenticate(user=self.data['reviewers'][0])
-
-        student = self.data['students'][0]
-
-        response = self.client.post(
-            '/api/subscription/', {
-                'query_type': 'student',
-                'query_key': student.student.student_id,
-                'stage': 'feedback-creation'
-            })
-
-        # This is expected since we wanted to assign all student submissions
-        self.assertEqual(0, response.data['remaining'])
-        self.assertEqual(0, response.data['available'])
-
-    def test_remaining_submissions(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        response = self.client.post('/api/subscription/', {'query_type': 'random'})
-
-        self.assertEqual(3, response.data['remaining'])
-
-    def test_available_submissions_in_stage(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        response = self.client.post('/api/subscription/',
-                                    {'query_type': 'random',
-                                     'feedback_stage': 'feedback-validation'})
-
-        self.assertEqual(0, response.data['available'])
-
-    def test_can_create_a_subscription(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        response = self.client.post('/api/subscription/', {'query_type': 'random'})
-
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-    def test_create_subscription_and_get_one_assignment(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        response = self.client.post('/api/subscription/', {'query_type': 'random'})
-
-        self.assertEqual('tutor01', response.data['owner'])
-
-    def test_subscription_has_next_and_current_assignment(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        response_subscription_create = self.client.post(
-            '/api/subscription/', {'query_type': 'random'})
-
-        subscription_pk = response_subscription_create.data['pk']
-        response_assignment = self.client.post(
-            f'/api/assignment/', {
-                'subscription': subscription_pk
-            })
-
-        assignment_pk = response_assignment.data['pk']
-        response_subscription = self.client.get(
-            f'/api/subscription/{subscription_pk}/')
-
-        self.assertEqual(1, len(response_subscription.data['assignments']))
-        self.assertEqual(response_assignment.data['pk'],
-                         response_subscription.data['assignments'][0]['pk'])
-
-        subscription_pk = response_subscription.data['pk']
-        response_next = self.client.post(
-            f'/api/assignment/', {
-                'subscription': subscription_pk
-            })
-        response_detail_subs = \
-            self.client.get(f'/api/subscription/{subscription_pk}/')
-
-        self.assertEqual(2, len(response_detail_subs.data['assignments']))
-        self.assertNotEqual(assignment_pk, response_next.data['pk'])
-
-    def test_subscription_can_assign_to_student(self):
-        self.client.force_authenticate(user=self.data['reviewers'][0])
-
-        student = self.data['students'][0]
-
-        response = self.client.post(
-            '/api/subscription/', {
-                'query_type': 'student',
-                'query_key': student.student.student_id,
-                'stage': 'feedback-creation'
-            })
-
-        assignments = response.data['assignments']
-        self.assertEqual(1, len(assignments))
-
-    def test_two_tutors_cant_have_assignments_for_same_submission(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        subscription = self.client.post('/api/subscription/',
-                                        {'query_type': 'random'}).data
-
-        assignment_fst_tutor = self.client.post(
-            '/api/assignment/', {
-                'subscription': subscription['pk']
-            }
-        ).data
-
-        self.client.force_authenticate(user=self.data['tutors'][1])
-
-        subscription = self.client.post('/api/subscription/',
-                                        {'query_type': 'random'}).data
-
-        assignment_snd_tutor = self.client.post(
-            '/api/assignment/', {
-                'subscription': subscription['pk']
-            }
-        ).data
-
-        self.assertNotEqual(assignment_fst_tutor['submission']['pk'],
-                            assignment_snd_tutor['submission']['pk'])
-
-    def test_reviewer_can_get_active_assignments(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-        subscription = self.client.post('/api/subscription/',
-                                        {'query_type': 'random'}).data
-
-        assignment = self.client.post(
-            '/api/assignment/', {
-                'subscription': subscription['pk']
-            }
-        ).data
-
-        # tutors shouldn't have access
-        res = self.client.get(
-            '/api/assignment/active/'
-        )
-        self.assertEqual(status.HTTP_403_FORBIDDEN, res.status_code)
-
-        self.client.force_authenticate(user=self.data['reviewers'][0])
-        active_assignments = self.client.get(
-            '/api/assignment/active/'
-        ).data
-        print(assignment)
-        self.assertIn(assignment['pk'], [assignment['pk'] for assignment in active_assignments])
-
-    def test_reviewer_can_delete_active_assignments(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-        subscription = self.client.post('/api/subscription/',
-                                        {'query_type': 'random'}).data
-
-        assignment = self.client.post(
-            '/api/assignment/', {
-                'subscription': subscription['pk']
-            }
-        ).data
-
-        # tutors shouldn't have access
-        res = self.client.delete(
-            '/api/assignment/active/'
-        )
-        self.assertEqual(status.HTTP_403_FORBIDDEN, res.status_code)
-
-        self.client.force_authenticate(user=self.data['reviewers'][0])
-        res = self.client.delete(
-            '/api/assignment/active/'
-        )
-        self.assertEqual(status.HTTP_204_NO_CONTENT, res.status_code)
-        self.assertNotIn(
-            assignment['pk'],
-            [assignment.pk for assignment
-             in TutorSubmissionAssignment.objects.filter(is_done=False)]
-        )
-
-    def test_all_stages_of_the_subscription(self):
-        self.client.force_authenticate(user=self.data['tutors'][0])
-
-        # The tutor corrects something
-        response = self.client.post(
-            '/api/subscription/', {
-                'query_type': 'random',
-                'feedback_stage': 'feedback-creation'
-            })
-
-        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
-
-        subscription_pk = response.data['pk']
-        response = self.client.post(
-            f'/api/assignment/', {
-                'subscription': subscription_pk
-            })
-        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
-        response = self.client.post(
-            f'/api/assignment/{response.data["pk"]}/finish/', {
-                "score": 23,
-                "of_submission": response.data['submission']['pk'],
-                "feedback_lines": {
-                    1: {"text": "< some string >", "labels": []},
-                    2: {"text": "< some string >", "labels": []}
-                },
-                "labels": [],
-            }
-        )
-        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
-
-        # some other tutor reviews it
-        self.client.force_authenticate(user=self.data['tutors'][1])
-
-        response = self.client.post(
-            '/api/subscription/', {
-                'query_type': 'random',
-                'feedback_stage': 'feedback-validation'
-            })
-
-        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
-
-        subscription_pk = response.data['pk']
-        response = self.client.post(
-            '/api/assignment/', {
-                'subscription': subscription_pk
-            })
-
-        self.assertEqual(status.HTTP_201_CREATED, response.status_code)
-        submission_id_in_database = models.Feedback.objects.filter(
-            is_final=False).first().of_submission.submission_id
-        submission_id_in_response = response.data['submission']['pk']
-
-        self.assertEqual(
-            str(submission_id_in_database),
-            submission_id_in_response)
-
-        assignment = models.TutorSubmissionAssignment.objects.get(
-            pk=response.data['pk'])
-        self.assertFalse(assignment.is_done)
-        response = self.client.post(
-            f'/api/assignment/{assignment.pk}/finish/', {
-                "score": 20,
-                "is_final": True,
-                "feedback_lines": {
-                    2: {"text": "< some addition by second tutor>"},
-                }
-            }
-        )
-
-        assignment.refresh_from_db()
-        meta = assignment.submission.meta
-        self.assertEqual(status.HTTP_200_OK, response.status_code)
-        self.assertEqual(2, len(response.data['feedback_lines'][2]))
-        self.assertTrue(assignment.is_done)
-        self.assertIn(self.data['tutors'][0], meta.feedback_authors.all())
-        self.assertIn(self.data['tutors'][1], meta.feedback_authors.all())
diff --git a/core/tests/test_tutor_api_endpoints.py b/core/tests/test_tutor_api_endpoints.py
index 343b649dc81098ba91078b6e571939b029e9c29d..14dc86f3db4648da1414b22dad7e5f8a8667ff9f 100644
--- a/core/tests/test_tutor_api_endpoints.py
+++ b/core/tests/test_tutor_api_endpoints.py
@@ -12,8 +12,7 @@ from rest_framework.test import (APIClient, APIRequestFactory, APITestCase,
                                  force_authenticate)
 import os
 
-from core.models import (Feedback, SubmissionSubscription,
-                         TutorSubmissionAssignment)
+from core.models import Feedback, TutorSubmissionAssignment
 from core.views import CorrectorApiViewSet
 from util.factories import GradyUserFactory, make_test_data
 
@@ -100,10 +99,16 @@ class TutorListTests(APITestCase):
         )
 
         def feedback_cycle(tutor, stage):
-            subscription = SubmissionSubscription.objects.create(
+            submissions = TutorSubmissionAssignment.objects.available_assignments({
+                'owner': tutor,
+                'stage': stage,
+                'submission_type': data['submission_types'][0].pk
+            })
+            assignment = TutorSubmissionAssignment.objects.create(
                 owner=tutor,
-                feedback_stage=stage)
-            assignment = subscription.get_or_create_work_assignment()
+                stage=stage,
+                submission=submissions.first().submission
+            )
             Feedback.objects.update_or_create(
                 of_submission=assignment.submission,
                 score=35)
@@ -113,8 +118,8 @@ class TutorListTests(APITestCase):
         tutor02 = data['tutors'][1]
         reviewer = data['reviewers'][0]
 
-        feedback_cycle(tutor01, SubmissionSubscription.FEEDBACK_CREATION)
-        feedback_cycle(tutor02, SubmissionSubscription.FEEDBACK_VALIDATION)
+        feedback_cycle(tutor01, TutorSubmissionAssignment.FEEDBACK_CREATION)
+        feedback_cycle(tutor02, TutorSubmissionAssignment.FEEDBACK_VALIDATION)
 
         force_authenticate(request, user=reviewer)
         cls.response = view(request)
@@ -130,8 +135,8 @@ class TutorListTests(APITestCase):
             t = get_user_model().objects.get(username=tutor_obj['username'])
             feedback_created_count = TutorSubmissionAssignment.objects.filter(
                 is_done=True,
-                subscription__feedback_stage=SubmissionSubscription.FEEDBACK_CREATION,  # noqa
-                subscription__owner=t
+                stage=TutorSubmissionAssignment.FEEDBACK_CREATION,  # noqa
+                owner=t
             ).count()
             return feedback_created_count == tutor_obj['feedback_created']
 
@@ -142,8 +147,8 @@ class TutorListTests(APITestCase):
             t = get_user_model().objects.get(username=tutor_obj['username'])
             feedback_validated_cnt = TutorSubmissionAssignment.objects.filter(
                 is_done=True,
-                subscription__feedback_stage=SubmissionSubscription.FEEDBACK_VALIDATION,  # noqa
-                subscription__owner=t
+                stage=TutorSubmissionAssignment.FEEDBACK_VALIDATION,  # noqa
+                owner=t
             ).count()
             return feedback_validated_cnt == tutor_obj['feedback_validated']
 
diff --git a/core/urls.py b/core/urls.py
index 66dea3ff2820b65335cb146626d4ac9c7f0287ec..dc918818922b9f6d6bf6bbc178ae0d4835596c21 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -18,14 +18,13 @@ router.register('submission', views.SubmissionViewSet,
                 basename='submission')
 router.register('submissiontype', views.SubmissionTypeApiView)
 router.register('corrector', views.CorrectorApiViewSet, basename='corrector')
-router.register('subscription', views.SubscriptionApiViewSet,
-                basename='subscription')
 router.register('assignment', views.AssignmentApiViewSet)
 router.register('statistics', views.StatisticsEndpoint, basename='statistics')
 router.register('user', views.UserAccountViewSet, basename='user')
 router.register('label', views.LabelApiViewSet, basename='label')
 router.register('label-statistics', views.LabelStatistics, basename='label-statistics')
 router.register('solution-comment', views.SolutionCommentApiViewSet, basename='solution-comment')
+router.register('group', views.GroupApiViewSet, basename='group')
 
 schema_view = get_schema_view(
     openapi.Info(
diff --git a/core/views/__init__.py b/core/views/__init__.py
index c455fbd93bacba1f16ae9449167d3fda0e16b0bd..1073d2170a1a39dd559cc480b6f11e41921f8f01 100644
--- a/core/views/__init__.py
+++ b/core/views/__init__.py
@@ -1,6 +1,7 @@
 from .feedback import FeedbackApiView, FeedbackCommentApiView  # noqa
-from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet  # noqa
+from .assignment import AssignmentApiViewSet  # noqa
 from .common_views import *  # noqa
 from .export import StudentJSONExport, InstanceExport  # noqa
 from .label import LabelApiViewSet, LabelStatistics  # noqa
 from .importer import ImportApiViewSet # noqa
+from .group import GroupApiViewSet  # noqa
diff --git a/core/views/subscription.py b/core/views/assignment.py
similarity index 59%
rename from core/views/subscription.py
rename to core/views/assignment.py
index ec052529c209dfc599ead29a237deccb64b90b9f..18669cac65096358050e7904850a0c1fd3e85349 100644
--- a/core/views/subscription.py
+++ b/core/views/assignment.py
@@ -1,12 +1,11 @@
 import logging
 
-from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import mixins, status, viewsets
 from rest_framework import decorators
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 
-from core import models, permissions, serializers
+from core import models, serializers
 from core.models import TutorSubmissionAssignment
 from core.permissions import IsReviewer, IsTutorOrReviewer
 from core.serializers import AssignmentDetailSerializer, AssignmentSerializer
@@ -18,66 +17,11 @@ from core.views.util import tutor_attempts_to_patch_first_feedback_final
 log = logging.getLogger(__name__)
 
 
-class SubscriptionApiViewSet(
-        mixins.RetrieveModelMixin,
-        mixins.CreateModelMixin,
-        mixins.DestroyModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
-    permission_classes = (permissions.IsTutorOrReviewer,)
-    serializer_class = serializers.SubscriptionSerializer
-    queryset = models.SubmissionSubscription.objects\
-        .prefetch_related('assignments')\
-        .select_related('owner')
-
-    def get_queryset(self):
-        return self.queryset.filter(
-            owner=self.request.user,
-            deactivated=False
-        )
-
-    def _get_subscription_if_type_exists(self, data):
-        try:
-            return models.SubmissionSubscription.objects.get(
-                owner=self.request.user,
-                query_type=data.get('query_type', ''),
-                query_key=data.get('query_key'),
-                feedback_stage=data.get(
-                    'feedback_stage',
-                    models.SubmissionSubscription.FEEDBACK_CREATION)
-            )
-        except ObjectDoesNotExist:
-            return None
-
-    def create(self, request, *args, **kwargs):
-        subscription = self._get_subscription_if_type_exists(request.data)
-        if subscription and not subscription.deactivated:
-            return Response({'Error': 'Subscriptions have to be unique.'},
-                            status.HTTP_409_CONFLICT)
-        elif subscription and subscription.deactivated:
-            serializer = self.get_serializer(subscription,
-                                             data=request.data)
-        else:
-            serializer = self.get_serializer(data=request.data)
-
-        serializer.is_valid(raise_exception=True)
-        subscription = serializer.save()
-
-        if subscription.query_type == models.SubmissionSubscription.STUDENT_QUERY:  # noqa
-            subscription.reserve_all_assignments_for_a_student()
-
-        headers = self.get_success_headers(serializer.data)
-        return Response(serializer.data,
-                        status=status.HTTP_201_CREATED,
-                        headers=headers)
-
-
 class AssignmentApiViewSet(
         mixins.RetrieveModelMixin,
         mixins.ListModelMixin,
         viewsets.GenericViewSet):
-    queryset = TutorSubmissionAssignment.objects\
-        .select_related('subscription').all()
+    queryset = TutorSubmissionAssignment.objects.all()
     serializer_class = AssignmentSerializer
     permission_classes = (IsTutorOrReviewer, )
 
@@ -85,15 +29,12 @@ class AssignmentApiViewSet(
         if self.action in ['list', 'active', 'destroy']:
             return self.queryset.all()
         else:
-            return self.queryset.filter(subscription__owner=self.request.user)
+            return self.queryset.filter(owner=self.request.user)
 
     def _fetch_assignment(self, serializer):
         try:
             serializer.save()
-        except models.SubscriptionEnded as err:
-            return Response({'Error': str(err)},
-                            status=status.HTTP_410_GONE)
-        except models.SubscriptionTemporarilyEnded as err:
+        except models.SubmissionTypeDepleted as err:
             return Response({'Error': str(err)},
                             status=status.HTTP_404_NOT_FOUND)
         except models.NotMoreThanTwoOpenAssignmentsAllowed as err:
@@ -115,11 +56,18 @@ class AssignmentApiViewSet(
             self.get_queryset().filter(is_done=False).delete()
             return Response(status=status.HTTP_204_NO_CONTENT)
 
+    @decorators.action(detail=False, permission_classes=(IsTutorOrReviewer,), methods=['delete'])
+    def release(self):
+        self.get_queryset().filter(
+            is_done=False
+        ).delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
     @decorators.action(detail=True, methods=['post'])
     def finish(self, request, *args, **kwargs):
         context = self.get_serializer_context()
         instance = self.get_object()
-        if instance.is_done or (instance.subscription.owner != request.user):
+        if instance.is_done or (instance.owner != request.user):
             return Response(status=status.HTTP_403_FORBIDDEN)
         try:
             orig_feedback = instance.submission.feedback
@@ -144,15 +92,15 @@ class AssignmentApiViewSet(
         serializer.save()
         instance.finish()
         response_status = status.HTTP_201_CREATED if \
-            instance.subscription.feedback_stage == \
-            models.SubmissionSubscription.FEEDBACK_CREATION else status.HTTP_200_OK
+            instance.stage == \
+            models.TutorSubmissionAssignment.FEEDBACK_CREATION else status.HTTP_200_OK
         return Response(serializer.data, status=response_status)
 
     def destroy(self, request, pk=None):
         """ Stop working on the assignment before it is finished """
         instance = self.get_object()
 
-        if instance.is_done or (instance.subscription.owner != request.user and
+        if instance.is_done or (instance.owner != request.user and
                                 not request.user.is_reviewer()):
             return Response(status=status.HTTP_403_FORBIDDEN)
 
@@ -162,15 +110,17 @@ class AssignmentApiViewSet(
     def create(self, request, *args, **kwargs):
         with Lock():
             context = self.get_serializer_context()
-            serializer = AssignmentDetailSerializer(data=request.data,
+            data = request.data
+            serializer = AssignmentDetailSerializer(data=data,
                                                     context=context)
             serializer.is_valid(raise_exception=True)
             assignment = self._fetch_assignment(serializer)
+
         return assignment
 
     def retrieve(self, request, *args, **kwargs):
         assignment = self.get_object()
-        if assignment.subscription.owner != request.user:
+        if assignment.owner != request.user:
             return Response(status=status.HTTP_403_FORBIDDEN)
         serializer = AssignmentDetailSerializer(assignment)
         return Response(serializer.data)
diff --git a/core/views/common_views.py b/core/views/common_views.py
index e9431d38f0adac36152480cf03a6bda8cc3c8012..87fdff6c966acc50484b0604c399dd2260549347 100644
--- a/core/views/common_views.py
+++ b/core/views/common_views.py
@@ -19,7 +19,8 @@ from rest_framework.response import Response
 from rest_framework.throttling import AnonRateThrottle
 
 from core import models
-from core.models import ExamType, StudentInfo, SubmissionType
+from core.models import (ExamType, StudentInfo,
+                         SubmissionType, TutorSubmissionAssignment)
 from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer
 from core.serializers import (ExamSerializer, StudentInfoSerializer,
                               StudentInfoForListViewSerializer,
@@ -109,8 +110,7 @@ class CorrectorApiViewSet(
     permission_classes = (IsReviewer,)
     queryset = models.UserAccount.corrector \
         .with_feedback_count() \
-        .prefetch_related('subscriptions') \
-        .prefetch_related('subscriptions__assignments')
+        .prefetch_related('assignments')
     serializer_class = CorrectorSerializer
 
     @action(detail=False, methods=['post'], permission_classes=[AllowAny])
@@ -131,6 +131,30 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
     serializer_class = SubmissionTypeSerializer
     permission_classes = (IsTutorOrReviewer, )
 
+    @action(detail=False)
+    def available(self, request, *args, **kwargs):
+        sub_types = self.get_queryset()
+        # TODO maybe do this as a non detail view on assignment instead which is passed
+        # a create assignment dict
+        res = {}
+        for sub_type in sub_types:
+            counts = {}
+            for stage, _ in models.TutorSubmissionAssignment.stages:
+                counts_for_group = {}
+                for group in models.Group.objects.all():
+                    count_in_stage_for_group = \
+                        TutorSubmissionAssignment.objects.available_assignments({
+                            'stage': stage,
+                            'submission_type': sub_type.pk,
+                            'owner': self.request.user,
+                            'group': group.pk
+                        }).count()
+                    counts_for_group[str(group.pk)] = count_in_stage_for_group
+                counts[stage] = counts_for_group
+            res[str(sub_type.pk)] = counts
+
+        return Response(res)
+
 
 class SolutionCommentApiViewSet(
         mixins.CreateModelMixin,
@@ -210,7 +234,7 @@ class SubmissionViewSet(viewsets.ReadOnlyModelViewSet):
             )
         else:
             return self.queryset.filter(
-                assignments__subscription__owner=self.request.user
+                assignments__owner=self.request.user
             )
 
     @action(detail=True,)
diff --git a/core/views/feedback.py b/core/views/feedback.py
index a1107208f416e2c5d2bf39183c41317cec6b45f6..127f39488ed26fd8e1263a0c8932dbece670f128 100644
--- a/core/views/feedback.py
+++ b/core/views/feedback.py
@@ -48,7 +48,7 @@ class FeedbackApiView(
                 .all()
 
         return self.queryset.filter(
-            of_submission__assignments__subscription__owner=self.request.user
+            of_submission__assignments__owner=self.request.user
         )
 
     @decorators.permission_classes((permissions.IsReviewer,))
diff --git a/core/views/group.py b/core/views/group.py
new file mode 100644
index 0000000000000000000000000000000000000000..e083f26d82bd02673e4a2028c377cb7b19e716a5
--- /dev/null
+++ b/core/views/group.py
@@ -0,0 +1,14 @@
+import logging
+
+from rest_framework import mixins, viewsets
+
+from core import models, permissions, serializers
+
+log = logging.getLogger(__name__)
+
+
+class GroupApiViewSet(viewsets.GenericViewSet,
+                      mixins.ListModelMixin):
+    permission_classes = (permissions.IsTutorOrReviewer, )
+    queryset = models.Group.objects.all()
+    serializer_class = serializers.GroupSerializer
diff --git a/core/views/util.py b/core/views/util.py
index b03018738c2767f49c46cb3f0d32cf5adf61da5b..8768f46498cd1a267907ead475f49578c12fd610 100644
--- a/core/views/util.py
+++ b/core/views/util.py
@@ -15,7 +15,7 @@ def tutor_attempts_to_patch_first_feedback_final(feedback_serializer,
     if user.role == models.UserAccount.TUTOR and assignment is None:
         raise NoAssignmentForTutor()
     is_final_set = feedback_serializer.validated_data.get('is_final', False)
-    in_creation = assignment.subscription.feedback_stage == models.SubmissionSubscription.FEEDBACK_CREATION  # noqa
+    in_creation = assignment.stage == models.TutorSubmissionAssignment.FEEDBACK_CREATION  # noqa
     return is_final_set and in_creation
 
 
@@ -23,7 +23,7 @@ def get_implicit_assignment_for_user(submission, user):
     """ Check for tutor if it exists. Not relevant for reviewer """
     try:
         return models.TutorSubmissionAssignment.objects.get(
-            subscription__owner=user,
+            owner=user,
             submission=submission
         )
     except models.TutorSubmissionAssignment.DoesNotExist:
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 17ca6787bdccc5decf711801da56e8d6378d2fa6..a06de7c46f5ad7f56406a11dd36cf2aff50fd046 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -10,11 +10,13 @@ import {
   StudentInfoForListView,
   Submission,
   SubmissionNoType, SubmissionType,
-  Subscription,
   Tutor, UserAccount, LabelStatisticsForSubType,
   FeedbackLabel, SolutionComment,
-  CreateUpdateFeedback
+  CreateUpdateFeedback,
+  AvailableSubmissionCounts,
+  Group
 } from '@/models'
+import { CreateAssignment } from './models'
 
 function getInstanceBaseUrl (): string {
   if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') {
@@ -84,19 +86,6 @@ export async function fetchAllTutors (): Promise<Array<Tutor>> {
   return (await ax.get(url)).data
 }
 
-export async function fetchSubscriptions (): Promise<Array<Subscription>> {
-  return (await ax.get('/api/subscription/')).data
-}
-
-export async function deactivateSubscription ({ pk }: {pk: string}): Promise<AxiosResponse<void>> {
-  const url = `/api/subscription/${pk}/`
-  return ax.delete(url)
-}
-
-export async function fetchSubscription (subscriptionPk: string): Promise<Subscription> {
-  return (await ax.get(`/api/subscription/${subscriptionPk}/`)).data
-}
-
 export async function fetchAllFeedback (): Promise<Array<Feedback>> {
   const url = '/api/feedback/'
   return (await ax.get(url)).data
@@ -122,35 +111,8 @@ export async function fetchLabelStatistics (): Promise<LabelStatisticsForSubType
   return (await ax.get(url)).data
 }
 
-interface SubscriptionCreatePayload {
-    queryType: Subscription.QueryTypeEnum
-    queryKey?: string
-    feedbackStage?: Subscription.FeedbackStageEnum
-}
-
-export async function subscribeTo (type: Subscription.QueryTypeEnum,
-  key?: string,
-  stage?: Subscription.FeedbackStageEnum): Promise<Subscription> {
-  let data: SubscriptionCreatePayload = {
-    queryType: type
-  }
-
-  if (key) {
-    data.queryKey = key
-  }
-  if (stage) {
-    data.feedbackStage = stage
-  }
-
-  return (await ax.post('/api/subscription/', data)).data
-}
 
-export async function createAssignment (
-  { subscription = undefined, subscriptionPk = '' }:
-  {subscription?: Subscription, subscriptionPk?: string}): Promise<Assignment> {
-  const data = {
-    subscription: subscription ? subscription.pk : subscriptionPk
-  }
+export async function createAssignment (data: CreateAssignment): Promise<Assignment> {
   return (await ax.post('/api/assignment/', data)).data
 }
 
@@ -174,7 +136,17 @@ export async function fetchSubmissionTypes (): Promise<Array<SubmissionType>> {
 }
 
 export async function fetchSubmissionType (pk: string): Promise<SubmissionType> {
-  const url = `/api/submissiontype/${pk}`
+  const url = `/api/submissiontype/${pk}/`
+  return (await ax.get(url)).data
+}
+
+export async function fetchAvailableSubmissionCounts(): Promise<AvailableSubmissionCounts> {
+  const url = '/api/submissiontype/available/'
+  return (await ax.get(url)).data
+}
+
+export async function fetchGroups(): Promise<Group[]> {
+  const url = '/api/group/'
   return (await ax.get(url)).data
 }
 
@@ -250,6 +222,10 @@ export async function updateLabel (payload: FeedbackLabel) {
   return (await ax.put('/api/label/' + payload.pk + '/', payload)).data
 }
 
+export async function fetchSubmissionCounts () {
+  return (await ax.get('/api/submissiontype/available_counts/')).data
+}
+
 export interface StudentExportOptions { setPasswords?: boolean }
 export interface StudentExportItem {
   Matrikel: string,
diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
index 19601cccdce4d961a558367f79faaf2faf2e90a4..9caa968d9c6b2d1284899f7c585df033ea793e50 100644
--- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
+++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
@@ -8,7 +8,9 @@
       lg3
     >
       <v-checkbox
+        id="show-final-checkbox"
         v-model="showFinal"
+        :ripple="false"
         label="show final"
       />
     </v-flex>
diff --git a/frontend/src/components/feedback_list/FeedbackTable.vue b/frontend/src/components/feedback_list/FeedbackTable.vue
index e3030c819c261f0cccd275dbd7e811b9701a9941..0c56c50e6d81d14a18de79e8105356aa3e263d52 100644
--- a/frontend/src/components/feedback_list/FeedbackTable.vue
+++ b/frontend/src/components/feedback_list/FeedbackTable.vue
@@ -53,7 +53,7 @@ import { getObjectValueByPath } from '@/util/helpers'
 import FeedbackSearchOptions from '@/components/feedback_list/FeedbackSearchOptions.vue'
 import { FeedbackSearchOptions as OptionsModule } from '@/store/modules/feedback_list/feedback-search-options'
 import { FeedbackTable as FeedbackModule, FeedbackHistoryItem } from '@/store/modules/feedback_list/feedback-table'
-import { Subscription, Feedback } from '@/models'
+import { FeedbackStageEnum, Feedback } from '@/models'
 import { actions } from '@/store/actions'
 import { getters } from '@/store/getters'
 import { Authentication } from '../../store/modules/authentication'
@@ -112,8 +112,8 @@ export default class FeedbackTable extends Vue {
     }
     const associatedTutors = this.stageFilterString === 'all'
       ? Object.values(feedback.history).filter(histEntry => !!histEntry).map(histEntry => histEntry!.owner)
-      : feedback.history[this.stageFilterString as Subscription.FeedbackStageEnum]
-        ? [feedback.history[this.stageFilterString as Subscription.FeedbackStageEnum]!.owner] : []
+      : feedback.history[this.stageFilterString as FeedbackStageEnum]
+        ? [feedback.history[this.stageFilterString as FeedbackStageEnum]!.owner] : []
 
     return this.filterByTutors.length === 0 ||
         associatedTutors.some(tutor => this.filterByTutors.includes(tutor))
diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
index 3e9e3c967d65ed57ce747a1a5674203421849110..3ea53edba364721f917bc2930e6a9db4bc8bbbe4 100644
--- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
+++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
@@ -132,7 +132,7 @@
 <script>
 import { SubmissionNotes } from '@/store/modules/submission-notes'
 import { Authentication } from '@/store/modules/authentication'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
 
 export default {
   name: 'AnnotatedSubmissionBottomToolbar',
@@ -183,7 +183,7 @@ export default {
   },
   methods: {
     initialFinalStatus () {
-      if (this.$route.name === 'subscription') {
+      if (this.$route.name === 'correction') {
         return !SubmissionNotes.isFeedbackCreation || Authentication.isReviewer
       } else {
         if (this.feedback.hasOwnProperty('isFinal')) {
@@ -214,7 +214,7 @@ export default {
     },
     skipSubmission () {
       if (this.skippable) {
-        Subscriptions.skipAssignment().catch(() => {
+        Assignments.skipAssignment().catch(() => {
           this.$notify({
             title: 'Unable to skip submission',
             type: 'error'
diff --git a/frontend/src/components/subscriptions/SubscriptionCreation.vue b/frontend/src/components/subscriptions/SubscriptionCreation.vue
deleted file mode 100644
index 7e039d0efb5aed7c1cd05cd090c2ab6ccbf81206..0000000000000000000000000000000000000000
--- a/frontend/src/components/subscriptions/SubscriptionCreation.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<template>
-  <v-card>
-    <v-card-text>
-      <v-card-title>
-        <h3>Subscribe to {{ title }}</h3>
-      </v-card-title>
-      <v-select
-        v-if="keyItems"
-        v-model="key"
-        :items="keyItems"
-        label="Select your desired type"
-      />
-      <v-select
-        v-model="stage"
-        return-object
-        :items="possibleStages"
-        label="Select your desired feedback stage"
-      />
-      <v-card-actions>
-        <v-spacer />
-        <v-btn
-          flat
-          :loading="loading"
-          @click="subscribe"
-        >
-          Subscribe
-        </v-btn>
-      </v-card-actions>
-    </v-card-text>
-  </v-card>
-</template>
-
-<script>
-import { Authentication } from '@/store/modules/authentication'
-import { Subscriptions } from '@/store/modules/subscriptions'
-
-const stages = [
-  {
-    text: 'Initial Feedback',
-    stage: 'feedback-creation'
-  },
-  {
-    text: 'Feedback validation',
-    stage: 'feedback-validation'
-  }
-]
-
-export default {
-  name: 'SubscriptionCreation',
-  props: {
-    title: {
-      type: String,
-      required: true
-    },
-    type: {
-      type: String,
-      required: true
-    },
-    keyItems: {
-      default: () => [],
-      type: Array
-    }
-  },
-  data () {
-    return {
-      key: {
-        text: '',
-        key: ''
-      },
-      stage: stages[0],
-      loading: false
-    }
-  },
-  computed: {
-    possibleStages () {
-      let possibleStages = [...stages]
-      if (Authentication.isReviewer) {
-        possibleStages.push({
-          text: 'Conflict resolution',
-          stage: 'feedback-conflict-resolution'
-        })
-      }
-      return possibleStages
-    }
-  },
-  methods: {
-    subscribe () {
-      this.loading = true
-      Subscriptions.subscribeTo({
-        type: this.type,
-        key: this.key.key,
-        stage: this.stage.stage
-      }).catch(err => {
-        if (err.response && err.response.data['non_field_errors']) {
-          this.$notify({
-            title: 'Couldn\'t create subscription',
-            text: err.response.data['non_field_errors'].join(' '),
-            type: 'error'
-          })
-        }
-      }).finally(() => {
-        this.loading = false
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-
-</style>
diff --git a/frontend/src/components/subscriptions/SubscriptionEnded.vue b/frontend/src/components/subscriptions/SubscriptionEnded.vue
index 7890a5dcc34545de6e7b43c20d47dfe00be2d905..51f5311d1332ab848d91d3a6d0fcad034c65cf64 100644
--- a/frontend/src/components/subscriptions/SubscriptionEnded.vue
+++ b/frontend/src/components/subscriptions/SubscriptionEnded.vue
@@ -7,9 +7,9 @@
       No submissions left
     </v-card-title>
     <v-card-text>
-      All submissions for <b> {{ submissionTypeName() }} </b> in the current stage have been corrected. If you've 
-      been validating feedback or <br> 
-      resolving conflicts some submissions may become active again.
+      All submissions of this type in the current stage have been corrected. If you've
+      been validating feedback or <br>
+      reviewing, new submissions might be available in the future.
       If that is the case they will appear clickable in the sidebar again.
     </v-card-text>
     <v-card-actions class="text-xs-center">
@@ -31,11 +31,6 @@ import store from '@/store/store'
 
 @Component
 export default class SubscriptionEnded extends Vue {
-  get submissionTypes () { return store.state.submissionTypes }
-  
-  submissionTypeName () {
-    return this.submissionTypes[this.$route.params.typePk].name
-  }
 }
 
 </script>
diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue
index c38a974f818490d95d4b34773e07e93691690d00..73d5d45c1b00d4e97b3ebe664a4bb181b1841aab 100644
--- a/frontend/src/components/subscriptions/SubscriptionForList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionForList.vue
@@ -2,7 +2,7 @@
   <v-layout row>
     <v-list-tile
       exact
-      :to="subscriptionRoute"
+      :to="correctionRoute"
       style="width: 100%"
     >
       <!-- dynamically set css class depending on active -->
@@ -23,30 +23,44 @@
 import Vue from 'vue'
 import Component from 'vue-class-component'
 import { Prop } from 'vue-property-decorator'
-import { Assignment, Subscription } from '@/models'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignment , FeedbackStageEnum } from '@/models'
+import { Assignments } from '@/store/modules/assignments'
 
 @Component
 export default class SubscriptionForList extends Vue {
-  @Prop({ type: String, required: true }) pk!: string
-  @Prop({ type: String, required: true }) feedbackStage!: Subscription.FeedbackStageEnum
-  @Prop({ type: String, required: true }) queryType!: Subscription.QueryTypeEnum
-  @Prop({ type: Number, required: true }) available!: number
-  @Prop({ type: Array, required: true }) assignments!: Assignment[]
-  @Prop({ type: String, default: '' }) queryKey!: string
+  @Prop({ type: String, required: true }) name!: string
+  @Prop({ type: String, required: true }) sub_type_pk!: string
 
-  get name () {
-    return Subscriptions.resolveSubscriptionKeyToName(
-      { queryKey: this.queryKey, queryType: this.queryType })
-  }
   get active () {
-    return !!this.available || this.assignments.length > 0
+    return !!this.available
+  }
+
+  get available () {
+    const stage = Assignments.state.assignmentCreation.stage
+    const group = Assignments.state.assignmentCreation.group
+    const group_pk =  group !== undefined ? group.pk : undefined
+    if (group_pk === undefined) {
+      return 0
+    }
+    const sub_type = this.sub_type_pk
+    const forSubType = Assignments.state.submissionsLeft[sub_type]
+    const forStage = forSubType !== undefined ? forSubType[stage] : undefined
+    const forGroup = forStage !== undefined ? forStage[group_pk] : 0
+    return forGroup
   }
-  get subscriptionRoute () {
-    if (this.active) {
-      return { name: 'subscription', params: { pk: this.pk } }
+
+  get correctionRoute() {
+    const group = Assignments.state.assignmentCreation.group
+    const group_pk = group !== undefined ? group.pk : undefined
+
+    return {
+      name: 'correction',
+      params: {
+        sub_type: this.sub_type_pk,
+        stage: Assignments.state.assignmentCreation.stage,
+        group: group_pk || undefined,
+      }
     }
-    return this.$route.fullPath
   }
 }
 </script>
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index bb7e561ce37a860ad357656051a4b629b9444da9..893120b9ee7cd61664c4aa1b760b940471d56743 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -6,15 +6,21 @@
     >
       <v-toolbar-side-icon><v-icon>assignment</v-icon></v-toolbar-side-icon>
       <v-toolbar-title
-        v-if="showDetail"
+        v-if="!sidebar"
         style="min-width: fit-content;"
       >
         Tasks
       </v-toolbar-title>
       <v-spacer />
+      <v-select
+        :items="groups"
+        v-model="selectedGroup"
+        item-text="name"
+        return-object
+      />
       <v-btn
         icon
-        @click="getSubscriptions(false)"
+        @click="getAvailableSubmissionCount(false)"
       >
         <v-icon v-if="!updating">
           refresh
@@ -55,44 +61,81 @@
 <script lang="ts">
 import Vue from 'vue'
 import Component from 'vue-class-component'
-import { Prop } from 'vue-property-decorator'
+import { Prop, Watch } from 'vue-property-decorator'
 import { mapGetters, mapActions, mapState } from 'vuex'
 import { UI } from '@/store/modules/ui'
 import { actions } from '@/store/actions'
-import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation.vue'
 import SubscriptionForList from '@/components/subscriptions/SubscriptionForList.vue'
 import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage.vue'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
+import store from '../../store/store'
+import { FeedbackStageEnum, Group } from '../../models'
+import { Authentication } from '../../store/modules/authentication'
 
 @Component({
   name: 'subscription-list',
   components: {
     SubscriptionsForStage,
     SubscriptionForList,
-    SubscriptionCreation
   },
 })
 export default class SubscriptionList extends Vue {
   @Prop({type: Boolean, default: false}) sidebar!: boolean
 
-  selectedStage = null
   updating = false
   timer = 0
 
-  get subscriptions () { return Subscriptions.state.subscriptions }
-  get stages () { return Subscriptions.availableStages }
-  get stagesReadable () { return Subscriptions.availableStagesReadable }
+  get stages () { return Assignments.availableStages }
+  get stagesReadable () { return Assignments.availableStagesReadable }
   get showDetail () {
     return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed)
   }
+  get groups () {
+    return Assignments.state.groups
+  }
+
+  get selectedStage() {
+    const val = Assignments.state.assignmentCreation.stage
+    switch (val) {
+      case FeedbackStageEnum.Creation: return 0
+      case FeedbackStageEnum.Validation: return 1
+      case FeedbackStageEnum.Review: return 2
+      default:
+          throw new Error(`Illegal value ${val} in get selectedStage`)
+    }
+  }
+
+  set selectedStage(val) {
+    const map_number_to_stage = (val: number): FeedbackStageEnum => {
+      switch (val) {
+        case 0: return FeedbackStageEnum.Creation
+        case 1: return FeedbackStageEnum.Validation
+        case 2: return FeedbackStageEnum.Review
+        default:
+          throw new Error(`Illegal value ${val} in set selectedStage`)
+      }
+    }
+    Assignments.SET_CREATE_STAGE(map_number_to_stage(val))
+  }
+
+  get selectedGroup() {
+    return Assignments.state.assignmentCreation.group
+  }
 
-  async getSubscriptions (silent: boolean) {
+  set selectedGroup(val: Group | undefined) {
+    if (val === undefined) {
+      throw new Error('Setting create group to undefined is not allowed')
+    }
+    Assignments.SET_CREATE_GROUP(val)
+  }
+
+
+  async getAvailableSubmissionCount (silent: boolean) {
     if (silent === false) {
       this.updating = true
     }
-    const subscriptions = await Subscriptions.getSubscriptions()
+    await Assignments.getAvailableSubmissionCounts()
     this.updating = false
-    return subscriptions
   }
 
   beforeDestroy() {
@@ -100,18 +143,26 @@ export default class SubscriptionList extends Vue {
   }
 
   created() {
+    const ownGroup = Authentication.state.user.exerciseGroups[0]
+    this.selectedGroup = ownGroup
+
     const submissionTypes = actions.updateSubmissionTypes()
-    const subscriptions = Subscriptions.getSubscriptions()
+    const groups = Assignments.getGroups()
+    Promise.all([submissionTypes, groups]).then(() => {
+      this.getAvailableSubmissionCount(false)
+
+      this.timer = setInterval(() => {
+        this.getAvailableSubmissionCount(true)
+      }, 30 * 1e3)
+    })
 
-    this.timer = setInterval(() => {
-      this.getSubscriptions(true)
-    }, 30 * 1e3)
 
-    Promise.all([submissionTypes, subscriptions]).then(() => {
-      Subscriptions.subscribeToAll()
-      Subscriptions.cleanAssignmentsFromSubscriptions(true)
+    Promise.all([submissionTypes]).then(() => {
+      Assignments.cleanAssignments()
     })
   }
+
+
 }
 </script>
 
diff --git a/frontend/src/components/subscriptions/SubscriptionsForStage.vue b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
index 49587104479ae0cae9977fbce0f3322779a54163..6e7c7dda486ab2f4c96101b10ae49d005dea9e69 100644
--- a/frontend/src/components/subscriptions/SubscriptionsForStage.vue
+++ b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
@@ -2,11 +2,13 @@
   <v-list :dense="dense">
     <div>
       <div
-        v-for="subscription in subscriptions['submission_type']"
-        :key="subscription.pk"
+        v-for="subType in submissionTypes"
+        :key="subType.pk"
       >
         <subscription-for-list
-          v-bind="subscription"
+          :name="subType.name"
+          :sub_type_pk="subType.pk"
+          :available="1"
         />
       </div>
     </div>
@@ -15,7 +17,8 @@
 
 <script>
 import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
+import store from '../../store/store'
 export default {
   name: 'SubscriptionsForStage',
   components: {
@@ -32,9 +35,7 @@ export default {
     }
   },
   computed: {
-    subscriptions () {
-      return Subscriptions.getSubscriptionsGroupedByType[this.stage]
-    }
+      submissionTypes () { return store.state.submissionTypes }
   }
 }
 </script>
diff --git a/frontend/src/models.ts b/frontend/src/models.ts
index 359a75d6264370c8c8a2bd52a6e401a2cf7495e0..1982c228151576f7130b6f0912fb87e7b612a685 100644
--- a/frontend/src/models.ts
+++ b/frontend/src/models.ts
@@ -1,3 +1,14 @@
+export interface Group {
+  pk: string,
+  name: string
+}
+
+export interface CreateAssignment {
+  submissionType: string
+  group?: string
+  stage: FeedbackStageEnum
+}
+
 /**
  *
  * @export
@@ -33,11 +44,9 @@ export interface Assignment {
      * @type {string}
      * @memberof Assignment
      */
-    stage?: Subscription.FeedbackStageEnum
+    stage?: FeedbackStageEnum
 
     feedback?: Feedback
-
-    subscription?: string
 }
 
 export interface SubmissionAssignment {
@@ -604,6 +613,14 @@ export interface SubmissionType {
     solutionComments: {[ofLine: number]: SolutionComment[]}
 }
 
+export interface AvailableSubmissionCounts {
+  [index: string]: {
+    [index: string]: {
+      [index: string]: number
+    }
+  }
+}
+
 export interface SolutionComment {
     pk: number,
     created: string,
@@ -664,92 +681,15 @@ export interface SubmissionTypeProgress {
     submissionCount: number
 }
 
-/**
- *
- * @export
- * @interface Subscription
- */
-export interface Subscription {
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    pk: string
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    owner?: string
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    queryType?: Subscription.QueryTypeEnum
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    queryKey?: string
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    feedbackStage?: Subscription.FeedbackStageEnum
-    /**
-     *
-     * @type {boolean}
-     * @memberof Subscription
-     */
-    deactivated?: boolean
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    assignments?: Array<Assignment>
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    remaining?: string
-    /**
-     *
-     * @type {string}
-     * @memberof Subscription
-     */
-    available?: string
-}
 
 /**
  * @export
- * @namespace Subscription
+ * @enum {string}
  */
-export namespace Subscription {
-    /**
-     * @export
-     * @enum {string}
-     */
-    export enum QueryTypeEnum {
-        Random = 'random',
-        Student = 'student',
-        Exam = 'exam',
-        SubmissionType = 'submission_type'
-    }
-    /**
-     * @export
-     * @enum {string}
-     */
-    export enum FeedbackStageEnum {
-        Creation = 'feedback-creation',
-        Validation = 'feedback-validation',
-        ConflictResolution = 'feedback-conflict-resolution'
-    }
+export enum FeedbackStageEnum {
+    Creation = 'feedback-creation',
+    Validation = 'feedback-validation',
+    Review = 'feedback-review'
 }
 
 /**
@@ -864,6 +804,7 @@ export interface UserAccount {
      * @memberof UserAccount
      */
     password?: string
+    exerciseGroups: Group[]
 }
 
 /**
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 2ba14b0bce50c2029839f7acf4b4a792b6d71c8e..81cf9bff2069615050e95fcceed3a6733d4f73f4 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -9,7 +9,7 @@
       md6
     >
       <submission-correction
-        :key="subscription.pk"
+        :key="currentAssignment.pk"
         :assignment="currentAssignment"
         class="ma-4 autofocus"
         @feedbackCreated="startWorkOnNextAssignment"
@@ -44,13 +44,16 @@ import SubmissionType from '@/components/submission_type/SubmissionType.vue'
 import store from '@/store/store'
 import { SubmissionNotes } from '@/store/modules/submission-notes'
 import SubmissionTests from '@/components/SubmissionTests.vue'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
 import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation.vue'
 import { getters } from '@/store/getters'
 import { SubmissionAssignment } from '@/models'
 
 const onRouteEnterOrUpdate: NavigationGuard =  function (to, from, next) {
-  Subscriptions.changeToSubscription(to.params['pk']).then(() => {
+  Assignments.changeAssignment(to).then(() => {
+    if (from === to) {
+      return
+    }
     next()
   })
 }
@@ -68,12 +71,8 @@ export default class SubscriptionWorkPage extends Vue {
   subscriptionActive = false
   nextRoute = () => {}
 
-  get subscription () {
-    return Subscriptions.state.subscriptions[this.$route.params['pk']]
-  }
-
   get currentAssignment () {
-    return Subscriptions.state.currentAssignment
+    return Assignments.state.currentAssignment
   }
 
   get submission () {
@@ -97,22 +96,22 @@ export default class SubscriptionWorkPage extends Vue {
   }
 
   beforeRouteLeave (this: SubscriptionWorkPage, to: Route, from: Route, next: (to?: any) => void) {
-    if (to.name === 'subscription-ended') {
+    if (to.name === 'correction-ended') {
       next()
     } else {
       this.nextRoute = () => {
         next()
-        Subscriptions.deleteCurrentAssignment()
+        Assignments.deleteCurrentAssignment()
       }
     }
   }
 
   startWorkOnNextAssignment () {
-    Subscriptions.createNextAssignment().catch(() => {
-      const typePk = SubmissionNotes.state.submission.type
-        this.$router.replace(typePk + '/ended')
-        Subscriptions.SET_CURRENT_ASSIGNMENT(undefined)
-        Subscriptions.getSubscriptions()
+    Assignments.createNextAssignment().then(() => {
+      Assignments.getAvailableSubmissionCounts()
+    }).catch(() => {
+      Assignments.SET_CURRENT_ASSIGNMENT(undefined)
+      this.$router.replace({name: 'correction-ended'})
     })
   }
 }
diff --git a/frontend/src/pages/reviewer/ReviewerStartPage.vue b/frontend/src/pages/reviewer/ReviewerStartPage.vue
index 39df51b43cd8237a5b27cee009b866abe746a3ee..974ebc04997b3ec174b5ba895a009e37affea3a5 100644
--- a/frontend/src/pages/reviewer/ReviewerStartPage.vue
+++ b/frontend/src/pages/reviewer/ReviewerStartPage.vue
@@ -57,7 +57,7 @@ import ImportDialog from '@/components/ImportDialog'
 import SubscriptionList from '@/components/subscriptions/SubscriptionList'
 import SubmissionTypesOverview from '@/components/submission_type/SubmissionTypesOverview'
 import { getters } from '../../store/getters'
-import { Subscriptions } from '../../store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
 
 export default {
   name: 'ReviewerStartPage',
@@ -81,7 +81,7 @@ export default {
   methods: {
     importDone() {
       this.dataImported = true
-      Subscriptions.RESET_STATE()
+      Assignments.RESET_STATE()
     }
   }
 }
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 2d57d382fcfe4d0670e162da0eafd24732b1bb60..74182743aa302bd1ace32187f49c47727bf82d4a 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -82,14 +82,14 @@ const router = new Router({
           component: StartPageSelector
         },
         {
-          path: 'subscription/:pk',
-          name: 'subscription',
+          path: 'correction/:sub_type/:stage/:group?',
+          name: 'correction',
           beforeEnter: tutorOrReviewerOnly,
           component: SubscriptionWorkPage
         },
         {
-          path: 'subscription/:typePk/ended',
-          name: 'subscription-ended',
+          path: 'correction/ended',
+          name: 'correction-ended',
           component: SubscriptionEnded
         },
         {
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index 459424280ae43dddc4dc9b3d45171529f6a6d042..831b7fc9460e6f4627cd43d8eaeaebc910a8fa69 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -8,7 +8,7 @@ import * as api from '@/api'
 import router from '@/router/index'
 import { RootState } from '@/store/store'
 import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'
-import { Subscriptions } from '@/store/modules/subscriptions'
+import { Assignments } from '@/store/modules/assignments'
 import { TutorOverview } from './modules/tutor-overview'
 import { StudentPage } from './modules/student-page'
 
@@ -65,7 +65,7 @@ async function getStatistics () {
 
 function resetState ({ message }: {message: string}) {
   FeedbackTable.RESET_STATE()
-  Subscriptions.RESET_STATE()
+  Assignments.RESET_STATE()
   SubmissionNotes.RESET_STATE()
   StudentPage.RESET_STATE()
   Authentication.RESET_STATE()
diff --git a/frontend/src/store/modules/assignments.ts b/frontend/src/store/modules/assignments.ts
new file mode 100644
index 0000000000000000000000000000000000000000..853d8520ee685bc583220c3c007dc10e5e8c3bc0
--- /dev/null
+++ b/frontend/src/store/modules/assignments.ts
@@ -0,0 +1,194 @@
+import Vue from 'vue'
+import * as api from '@/api'
+import { cartesian, flatten, once } from '@/util/helpers'
+import { Assignment, FeedbackStageEnum, CreateAssignment, AvailableSubmissionCounts, Group} from '@/models'
+import { RootState } from '@/store/store'
+import { Authentication } from '@/store/modules/authentication'
+import { getStoreBuilder, BareActionContext } from 'vuex-typex'
+import router from '@/router'
+import { Route } from 'vue-router'
+
+export interface AssignmentsState {
+  currentAssignment?: Assignment
+  assignmentCreation: {
+    submissionType?: string
+    stage: FeedbackStageEnum
+    group?: Group
+  },
+  submissionsLeft: AvailableSubmissionCounts,
+  groups: Group[],
+  loading: boolean
+}
+
+function initialState (): AssignmentsState {
+  return {
+    currentAssignment: undefined,
+    loading: false,
+    assignmentCreation: {
+      stage: FeedbackStageEnum.Creation,
+      group: undefined,
+      submissionType: undefined
+    },
+    submissionsLeft: {},
+    groups: []
+  }
+}
+
+const mb = getStoreBuilder<RootState>().module('Assignments', initialState())
+
+const stateGetter = mb.state()
+
+
+const availableStagesGetter = mb.read(function availableStages (state, getters) {
+  let stages = [FeedbackStageEnum.Creation, FeedbackStageEnum.Validation]
+  if (Authentication.isReviewer) {
+    stages.push(FeedbackStageEnum.Review)
+  }
+  return stages
+})
+
+const availableStagesReadableGetter = mb.read(function availableStagesReadable (state, getters) {
+  let stages = ['initial', 'validate']
+  if (Authentication.isReviewer) {
+    stages.push('review')
+  }
+  return stages
+})
+
+const availableSubmissionTypeQueryKeysGetter = mb.read(function availableSubmissionTypeQueryKeys (state, getters, rootState) {
+  return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
+})
+
+const availableExamTypeQueryKeysGetter = mb.read(function availableExamTypeQueryKeys (state, getters, rootState) {
+  return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
+})
+
+
+function SET_CURRENT_ASSIGNMENT (state: AssignmentsState, assignment?: Assignment): void {
+  state.currentAssignment = assignment
+}
+
+function SET_CREATE_SUBMISSION_TYPE (state: AssignmentsState, submissionType: string): void {
+  state.assignmentCreation.submissionType = submissionType
+}
+
+function SET_CREATE_STAGE (state: AssignmentsState, stage: FeedbackStageEnum): void {
+  state.assignmentCreation.stage = stage
+}
+
+function SET_CREATE_GROUP (state: AssignmentsState, group: Group): void {
+  state.assignmentCreation.group = group
+}
+
+function SET_SUBMISSION_LEFT (state: AssignmentsState, availableSubmissions: AvailableSubmissionCounts): void {
+  state.submissionsLeft = availableSubmissions
+}
+
+function SET_GROUPS (state: AssignmentsState, groups: Group[]): void {
+  state.groups = groups
+}
+
+function UPDATE_CREATE_PARAMETERS_FROM_URL(state: AssignmentsState, route: Route) {
+  const submissionType = route.params['sub_type']
+  const stage = route.params['stage'] as FeedbackStageEnum
+  const group_par = route.params['group']
+
+  state.assignmentCreation.submissionType = submissionType
+  state.assignmentCreation.stage = stage
+  const group = state.groups.find((group) => group.pk === group_par)
+  if (group === undefined) {
+    console.log(state.groups)
+    throw new Error(`Group ${group_par} appeared in parameter but not available in ${state.groups}`)
+  }
+  state.assignmentCreation.group = group
+}
+
+function RESET_STATE (state: AssignmentsState): void {
+  Object.assign(state, initialState())
+}
+
+async function createNextAssignment({ state }: BareActionContext<AssignmentsState, RootState>) {
+  const createAssignment = state.assignmentCreation
+  if (createAssignment.submissionType === undefined ) {
+    throw new Error('SET_CREATE_SUBMISSION_TYPE needs to be called before createNextAssignment')
+  }
+
+  const data = {
+    stage: createAssignment.stage,
+    submissionType: createAssignment.submissionType!,
+    group: createAssignment.group !== undefined ? createAssignment.group.pk : undefined
+  }
+
+  Assignments.SET_CURRENT_ASSIGNMENT(await api.createAssignment(data))
+}
+
+async function cleanAssignments
+({ state }: BareActionContext<AssignmentsState, RootState>) {
+  console.log('TODO cleanAssignments')
+}
+
+async function changeAssignment
+({ state }: BareActionContext<AssignmentsState, RootState>, route: Route) {
+  Assignments.UPDATE_CREATE_PARAMETERS_FROM_URL(route)
+  if (state.currentAssignment) {
+    await Assignments.deleteCurrentAssignment()
+  }
+  await Assignments.createNextAssignment()
+}
+
+async function skipAssignment ({ state }: BareActionContext<AssignmentsState, RootState>) {
+  if (!state.currentAssignment) {
+    throw new Error('skipAssignment can only be called with active assignment')
+  }
+
+  const oldAssignment = state.currentAssignment
+  await Assignments.createNextAssignment()
+  await api.deleteAssignment({assignment: oldAssignment })
+
+}
+
+async function deleteCurrentAssignment ({ state }: BareActionContext<AssignmentsState
+  , RootState>) {
+  if (!state.currentAssignment) {
+    throw new Error('No active assignment to delete')
+  }
+  await api.deleteAssignment({assignment: state.currentAssignment})
+  Assignments.SET_CURRENT_ASSIGNMENT(undefined)
+}
+
+async function getAvailableSubmissionCounts() {
+  const counts = await api.fetchAvailableSubmissionCounts()
+  Assignments.SET_SUBMISSION_LEFT(counts)
+}
+
+async function getGroups() {
+  const groups = await api.fetchGroups()
+  Assignments.SET_GROUPS(groups)
+}
+
+
+export const Assignments = {
+  get state () { return stateGetter() },
+  get availableStages () { return availableStagesGetter() },
+  get availableStagesReadable () { return availableStagesReadableGetter() },
+  get availableSubmissionTypeQueryKeys () { return availableSubmissionTypeQueryKeysGetter() },
+  get availableExamTypeQueryKeys () { return availableExamTypeQueryKeysGetter() },
+
+  SET_CURRENT_ASSIGNMENT: mb.commit(SET_CURRENT_ASSIGNMENT),
+  SET_CREATE_SUBMISSION_TYPE: mb.commit(SET_CREATE_SUBMISSION_TYPE),
+  SET_CREATE_STAGE: mb.commit(SET_CREATE_STAGE),
+  SET_CREATE_GROUP: mb.commit(SET_CREATE_GROUP),
+  SET_SUBMISSION_LEFT: mb.commit(SET_SUBMISSION_LEFT),
+  SET_GROUPS: mb.commit(SET_GROUPS),
+  UPDATE_CREATE_PARAMETERS_FROM_URL: mb.commit(UPDATE_CREATE_PARAMETERS_FROM_URL),
+  RESET_STATE: mb.commit(RESET_STATE),
+
+
+  cleanAssignments: mb.dispatch(cleanAssignments),
+  changeAssignment: mb.dispatch(changeAssignment),
+  createNextAssignment: mb.dispatch(createNextAssignment),
+  skipAssignment: mb.dispatch(skipAssignment),
+  deleteCurrentAssignment: mb.dispatch(deleteCurrentAssignment),
+  getAvailableSubmissionCounts: mb.dispatch(getAvailableSubmissionCounts),
+  getGroups: mb.dispatch(getGroups)
+}
diff --git a/frontend/src/store/modules/authentication.ts b/frontend/src/store/modules/authentication.ts
index cdef4c0cad582b5a465760b189506be74697eea1..a87b556e5185d2541be2d7a9f11b317de1e86413 100644
--- a/frontend/src/store/modules/authentication.ts
+++ b/frontend/src/store/modules/authentication.ts
@@ -27,7 +27,8 @@ function initialState (): AuthState {
     user: {
       pk: '',
       username: '',
-      isAdmin: false
+      isAdmin: false,
+      exerciseGroups: []
     }
   }
 }
diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.ts b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
index 671301950421070aa6bbc6c42738e608c340efcd..f5e1faee38250dc22d7bdac8777ab8cc0ea44dee 100644
--- a/frontend/src/store/modules/feedback_list/feedback-search-options.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
@@ -1,7 +1,7 @@
 import { Module } from 'vuex'
 import { RootState } from '@/store/store'
 import { getStoreBuilder } from 'vuex-typex'
-import { Subscription } from '@/models'
+import { FeedbackStageEnum } from '@/models'
 import { FeedbackLabels } from '../feedback-labels'
 
 export const namespace = 'feedbackSearchOptions'
@@ -33,8 +33,8 @@ function initialState (): FeedbackSearchOptionsState {
 const mb = getStoreBuilder<RootState>().module('FeedbackSearchOptions', initialState())
 
 const mapStageDisplayToApiString: {[index: string]: string} = {
-  'Initial feedback': Subscription.FeedbackStageEnum.Creation,
-  'Validation': Subscription.FeedbackStageEnum.Validation
+  'Initial feedback': FeedbackStageEnum.Creation,
+  'Validation': FeedbackStageEnum.Validation
 }
 
 const stateGetter = mb.state()
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.ts b/frontend/src/store/modules/feedback_list/feedback-table.ts
index ef5f9da77bb157c5cafaa5ad72605e066f0ead3f..13ef429913d063b60602f76b4e2240397eeec92c 100644
--- a/frontend/src/store/modules/feedback_list/feedback-table.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-table.ts
@@ -1,6 +1,6 @@
 import { fetchAllFeedback, fetchAllAssignments } from '@/api'
 import { objectifyArray } from '@/util/helpers'
-import { Assignment, Feedback, Subscription, SubmissionType } from '@/models'
+import { Assignment, Feedback, FeedbackStageEnum, SubmissionType } from '@/models'
 import { Module } from 'vuex'
 import { RootState } from '@/store/store'
 import { getters } from '@/store/getters'
@@ -9,7 +9,7 @@ import { Authentication } from '@/store/modules/authentication'
 
 export interface FeedbackHistoryItem extends Feedback {
   history?: {
-    [key in Subscription.FeedbackStageEnum]?: {
+    [key in FeedbackStageEnum]?: {
       owner: string
       isDone: boolean
     }
diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts
index b7cffba17195f2a3c4d82897f8e5ab5983edb03d..0870424bdb3c0c4f4ea42eb8122de27d92995837 100644
--- a/frontend/src/store/modules/submission-notes.ts
+++ b/frontend/src/store/modules/submission-notes.ts
@@ -6,7 +6,7 @@ import { RootState } from '@/store/store'
 import { getStoreBuilder, BareActionContext } from 'vuex-typex'
 import { syntaxPostProcess } from '@/util/helpers'
 import { AxiosResponse } from 'axios'
-import { Subscriptions } from './subscriptions'
+import { Assignments } from './assignments'
 
 export interface SubmissionNotesState {
   submission: SubmissionNoType
@@ -203,7 +203,7 @@ Promise<AxiosResponse<void>[]> {
     throw new Error('You need to add or change a comment or a feedback label when setting a non full score.')
   }
 
-  const assignment = Subscriptions.state.currentAssignment
+  const assignment = Assignments.state.currentAssignment
   if (assignment) {
     await api.submitFeedbackForAssignment({ feedback , assignment})
   } else if (state.hasOrigFeedback) {
diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts
deleted file mode 100644
index 88a2cf712dd2dfc7a1124e5faf68569f0371b0c6..0000000000000000000000000000000000000000
--- a/frontend/src/store/modules/subscriptions.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import Vue from 'vue'
-import * as api from '@/api'
-import { cartesian, flatten, once } from '@/util/helpers'
-import { Assignment, Subscription } from '@/models'
-import { ActionContext, Module } from 'vuex'
-import { RootState } from '@/store/store'
-import { Authentication } from '@/store/modules/authentication'
-import { getStoreBuilder, BareActionContext } from 'vuex-typex'
-
-export interface SubscriptionsState {
-  subscriptions: {[pk: string]: Subscription}
-  currentAssignment?: Assignment
-  loading: boolean
-}
-
-function initialState (): SubscriptionsState {
-  return {
-    subscriptions: {},
-    currentAssignment: undefined,
-    loading: false
-  }
-}
-
-const mb = getStoreBuilder<RootState>().module('Subscriptions', initialState())
-
-const stateGetter = mb.state()
-
-const availableTypesGetter = mb.read(function availableTypes (state, getters) {
-  let types = [Subscription.QueryTypeEnum.Random, Subscription.QueryTypeEnum.SubmissionType]
-  if (Authentication.isReviewer) {
-    types.push(Subscription.QueryTypeEnum.Exam)
-  }
-  return types
-})
-
-const availableStagesGetter = mb.read(function availableStages (state, getters) {
-  let stages = [Subscription.FeedbackStageEnum.Creation, Subscription.FeedbackStageEnum.Validation]
-  if (Authentication.isReviewer) {
-    stages.push(Subscription.FeedbackStageEnum.ConflictResolution)
-  }
-  return stages
-})
-
-const availableStagesReadableGetter = mb.read(function availableStagesReadable (state, getters) {
-  let stages = ['initial', 'validate']
-  if (Authentication.isReviewer) {
-    stages.push('conflict')
-  }
-  return stages
-})
-
-const availableSubmissionTypeQueryKeysGetter = mb.read(function availableSubmissionTypeQueryKeys (state, getters, rootState) {
-  return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
-})
-
-const availableExamTypeQueryKeysGetter = mb.read(function availableExamTypeQueryKeys (state, getters, rootState) {
-  return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
-})
-
-const activeSubscriptionGetter = mb.read(function activeSubscription (state) {
-  if (state.currentAssignment && state.currentAssignment.subscription) {
-    return state.subscriptions[state.currentAssignment.subscription]
-  }
-
-  return undefined
-})
-
-const resolveSubscriptionKeyToNameGetter = mb.read(function resolveSubscriptionKeyToName (state, getters, rootState) {
-  return (subscription: {queryType: Subscription.QueryTypeEnum, queryKey: string}) => {
-    switch (subscription.queryType) {
-      case Subscription.QueryTypeEnum.Random:
-        return 'Active'
-      case Subscription.QueryTypeEnum.Exam:
-        return subscription.queryKey
-          ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam'
-      case Subscription.QueryTypeEnum.SubmissionType:
-        return subscription.queryKey
-          ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type'
-      case Subscription.QueryTypeEnum.Student:
-        return subscription.queryKey
-          ? rootState.students[subscription.queryKey].name : 'Student'
-
-      default:
-        return undefined
-    }
-  }
-})
-
-type SubscriptionsByStage = {[p in Subscription.FeedbackStageEnum]?: {[k in Subscription.QueryTypeEnum]: Subscription[]}}
-// TODO Refactor this monstrosity
-const getSubscriptionsGroupedByTypeGetter = mb.read(function getSubscriptionsGroupedByType (state, getters) {
-  const subscriptionsByType = () => {
-    return {
-      [Subscription.QueryTypeEnum.Random]: [],
-      [Subscription.QueryTypeEnum.Student]: [],
-      [Subscription.QueryTypeEnum.Exam]: [],
-      [Subscription.QueryTypeEnum.SubmissionType]: []
-    }
-  }
-  let subscriptionsByStage: SubscriptionsByStage = Subscriptions.availableStages.reduce((acc: SubscriptionsByStage,
-    curr: Subscription.FeedbackStageEnum) => {
-    acc[curr] = subscriptionsByType()
-    return acc
-  }, {})
-  Object.values(state.subscriptions).forEach((subscription: Subscription) => {
-    if (subscriptionsByStage && subscription.feedbackStage && subscription.queryType) {
-      subscriptionsByStage[subscription.feedbackStage]![subscription.queryType].push(subscription)
-    }
-  })
-  // sort the resulting arrays in subscriptions lexicographically by their query_keys
-  const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType)
-    .forEach((arr: object[]) => {
-      if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) {
-        arr.sort((subA, subB) => {
-          const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
-          const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
-          if (subALower < subBLower) {
-            return -1
-          } else if (subALower > subBLower) {
-            return 1
-          } else {
-            return 0
-          }
-        })
-      }
-    })
-  Object.values(subscriptionsByStage).forEach((subscriptionsByType: any) => {
-    sortSubscriptions(subscriptionsByType)
-  })
-  return subscriptionsByStage
-})
-
-function SET_SUBSCRIPTIONS (state: SubscriptionsState, subscriptions: Array<Subscription>): void {
-  state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => {
-    acc[curr.pk] = curr
-    return acc
-  }, {})
-}
-
-function SET_SUBSCRIPTION (state: SubscriptionsState, subscription: Subscription): void {
-  Vue.set(state.subscriptions, subscription.pk, subscription)
-}
-
-function SET_CURRENT_ASSIGNMENT (state: SubscriptionsState, assignment?: Assignment): void {
-  state.currentAssignment = assignment
-}
-
-function RESET_STATE (state: SubscriptionsState): void {
-  Object.assign(state, initialState())
-  subscribeToAll.reset()
-}
-
-async function subscribeTo (
-  context: BareActionContext<SubscriptionsState, RootState>,
-  { type, key, stage }:
-  {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}): Promise<Subscription> {
-  // don't subscribe to type, key, stage combinations if they're already present
-  let subscription = Subscriptions.getSubscriptionsGroupedByType[stage]![type].find((elem: Subscription) => {
-    if (type === Subscription.QueryTypeEnum.Random) {
-      return true
-    }
-    return elem.queryKey === key
-  })
-  subscription = subscription || await api.subscribeTo(type, key, stage)
-  Subscriptions.SET_SUBSCRIPTION(subscription)
-  return subscription
-}
-
-async function getSubscriptions () {
-  const subscriptions = await api.fetchSubscriptions()
-  Subscriptions.SET_SUBSCRIPTIONS(subscriptions)
-  return subscriptions
-}
-
-
-async function changeToSubscription({state}: BareActionContext<SubscriptionsState, RootState>, subscriptionPk: string) {
-  const currAssignment = state.currentAssignment
-  if (currAssignment && currAssignment.subscription === subscriptionPk) {
-    return
-  }
-
-  if (currAssignment) {
-    await api.deleteAssignment({assignment: currAssignment})
-  }
-
-  const newAssignment = await api.createAssignment({subscriptionPk})
-  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
-}
-
-async function createNextAssignment() {
-  const activeSubscription = Subscriptions.activeSubscription
-  if (!activeSubscription) {
-    throw new Error('There must be an active Subscription before calling createNextAssignment')
-  }
-  const newAssignment = await api.createAssignment({subscription: activeSubscription})
-  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
-}
-
-async function cleanAssignmentsFromSubscriptions
-({ state }: BareActionContext<SubscriptionsState, RootState>, excludeActive = true) {
-  Object.values(state.subscriptions).forEach(subscription => {
-    if (!excludeActive ||
-        !Subscriptions.activeSubscription ||
-        subscription.pk !== Subscriptions.activeSubscription.pk) {
-      if (subscription.assignments) {
-        subscription.assignments.forEach(assignment => {
-          api.deleteAssignment({ assignment })
-        })
-      }
-    }
-  })
-}
-
-async function skipAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) {
-  if (!state.currentAssignment || !state.currentAssignment.subscription) {
-    throw new Error('skipAssignment can only be called with active assignment')
-  }
-
-  const newAssignment = await api.createAssignment({subscriptionPk: state.currentAssignment.subscription})
-  await api.deleteAssignment({assignment: state.currentAssignment })
-
-  Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment)
-}
-
-async function deleteCurrentAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) {
-  if (!state.currentAssignment) {
-    throw new Error('No active assignment to delete')
-  }
-  await api.deleteAssignment({assignment: state.currentAssignment})
-  Subscriptions.SET_CURRENT_ASSIGNMENT(undefined)
-}
-
-async function subscribeToType
-(context: BareActionContext<SubscriptionsState, RootState>, type: Subscription.QueryTypeEnum) {
-  switch (type) {
-    case Subscription.QueryTypeEnum.Random:
-      Subscriptions.availableStages.map((stage: Subscription.FeedbackStageEnum) => {
-        Subscriptions.subscribeTo({ type, stage })
-      })
-      break
-    case Subscription.QueryTypeEnum.Exam:
-      if (Authentication.isReviewer) {
-        const stageKeyCartesian = cartesian(
-          Subscriptions.availableStages, Subscriptions.availableExamTypeQueryKeys)
-        // @ts-ignore
-        stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
-          Subscriptions.subscribeTo({ stage, type, key })
-        })
-      }
-      break
-    case Subscription.QueryTypeEnum.SubmissionType:
-      const stageKeyCartesian = cartesian(
-        Subscriptions.availableStages, Subscriptions.availableSubmissionTypeQueryKeys)
-      // @ts-ignore
-      stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
-        Subscriptions.subscribeTo({ stage, type, key })
-      })
-      break
-
-    default:
-      return
-  }
-}
-
-const subscribeToAll = once(async () => {
-  return Promise.all(flatten(Subscriptions.availableTypes.map((type) => {
-    return Subscriptions.subscribeToType(type)
-  })))
-})
-
-export const Subscriptions = {
-  get state () { return stateGetter() },
-  get availableTypes () { return availableTypesGetter() },
-  get availableStages () { return availableStagesGetter() },
-  get availableStagesReadable () { return availableStagesReadableGetter() },
-  get availableSubmissionTypeQueryKeys () { return availableSubmissionTypeQueryKeysGetter() },
-  get availableExamTypeQueryKeys () { return availableExamTypeQueryKeysGetter() },
-  get activeSubscription () { return activeSubscriptionGetter() },
-  get resolveSubscriptionKeyToName () { return resolveSubscriptionKeyToNameGetter() },
-  get getSubscriptionsGroupedByType () { return getSubscriptionsGroupedByTypeGetter() },
-
-  SET_SUBSCRIPTIONS: mb.commit(SET_SUBSCRIPTIONS),
-  SET_SUBSCRIPTION: mb.commit(SET_SUBSCRIPTION),
-  SET_CURRENT_ASSIGNMENT: mb.commit(SET_CURRENT_ASSIGNMENT),
-  RESET_STATE: mb.commit(RESET_STATE),
-
-  subscribeTo: mb.dispatch(subscribeTo),
-  getSubscriptions: mb.dispatch(getSubscriptions),
-  cleanAssignmentsFromSubscriptions: mb.dispatch(cleanAssignmentsFromSubscriptions),
-  changeToSubscription: mb.dispatch(changeToSubscription),
-  createNextAssignment: mb.dispatch(createNextAssignment),
-  skipAssignment: mb.dispatch(skipAssignment),
-  deleteCurrentAssignment: mb.dispatch(deleteCurrentAssignment),
-  subscribeToType: mb.dispatch(subscribeToType),
-  subscribeToAll: mb.dispatch(subscribeToAll, 'subscribeToAll')
-}
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 7b01426881f6baf63667a0a772b7cb8bc4f26369..439a54b8ab571c597f4c0dcb192fcc9ab90c6125 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -11,7 +11,7 @@ import './modules/feedback_list/feedback-search-options'
 // @ts-ignore
 import './modules/feedback_list/feedback-table'
 // @ts-ignore
-import './modules/subscriptions'
+import './modules/assignments'
 // @ts-ignore
 import './modules/submission-notes'
 // @ts-ignore
@@ -26,7 +26,7 @@ import './getters'
 import { UIState } from './modules/ui'
 import { AuthState } from './modules/authentication'
 import { FeedbackSearchOptionsState } from './modules/feedback_list/feedback-search-options'
-import { SubscriptionsState } from './modules/subscriptions'
+import { AssignmentsState } from './modules/assignments'
 import { FeedbackTableState } from './modules/feedback_list/feedback-table'
 import { SubmissionNotesState } from './modules/submission-notes'
 import { StudentPageState } from './modules/student-page'
@@ -63,7 +63,7 @@ export interface RootState extends RootInitialState{
   Authentication: AuthState,
   FeedbackSearchOptions: FeedbackSearchOptionsState,
   FeedbackTable: FeedbackTableState,
-  Subscriptions: SubscriptionsState,
+  Assignments: AssignmentsState,
   SubmissionNotes: SubmissionNotesState,
   StudentPage: StudentPageState,
   TutorOverview: TutorOverviewState,
diff --git a/functional_tests/test_feedback_creation.py b/functional_tests/test_feedback_creation.py
index e843de89ddbc89b5b30c6da0d00fff1d2a7dcef2..75a64e2ea52f4e2c06b82abf25984b0179f0d2a0 100644
--- a/functional_tests/test_feedback_creation.py
+++ b/functional_tests/test_feedback_creation.py
@@ -177,7 +177,7 @@ class UntestedParent:
             WebDriverWait(self.browser, 10).until(wait_until_code_changes(self, code))
             correct()
 
-            sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+            sub_url = 'correction/ended'
             WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
 
             reset_browser_after_test(self.browser, self.live_server_url)  # logs out user
@@ -198,7 +198,7 @@ class UntestedParent:
             self.browser.find_element_by_class_name('final-checkbox').click()
             self.browser.find_element_by_id('submit-feedback').click()
 
-            sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+            sub_url = 'correction/ended'
             WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
 
             reset_browser_after_test(self.browser, self.live_server_url)
@@ -209,7 +209,7 @@ class UntestedParent:
             fact.UserAccountFactory(username=user_rev, password=password, role=role)
             login(self.browser, self.live_server_url, user_rev, password)
 
-            go_to_subscription(self, 'conflict')
+            go_to_subscription(self, 'review')
             code = reconstruct_submission_code(self)
             self.assertEqual(code, code_non_final)
 
@@ -252,7 +252,7 @@ class UntestedParent:
             self.browser.find_element_by_class_name('final-checkbox').click()
             self.browser.find_element_by_id('submit-feedback').click()
 
-            sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+            sub_url = 'correction/ended'
             WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
 
             reset_browser_after_test(self.browser, self.live_server_url)  # logs out user
diff --git a/functional_tests/test_feedback_label_system.py b/functional_tests/test_feedback_label_system.py
index 3d737a4839bd280ebc22a7a618ee73cc7e7f8e71..950af8a01037fb45dd4f1eb30647e36f9bf6df3f 100644
--- a/functional_tests/test_feedback_label_system.py
+++ b/functional_tests/test_feedback_label_system.py
@@ -213,7 +213,7 @@ class FeedbackLabelSystemTest(LiveServerTestCase):
         self.assertEqual(len(added), 1)
 
         self.browser.find_element_by_id('submit-feedback').click()
-        sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+        sub_url = 'correction/ended'
         WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
         label = FeedbackLabel.objects.get(name='test')
 
@@ -257,7 +257,7 @@ class FeedbackLabelSystemTest(LiveServerTestCase):
         self.assertGreater(len(current), 1)
 
         self.browser.find_element_by_id('submit-feedback').click()
-        sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+        sub_url = 'correction/ended'
         WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
         new_label = FeedbackLabel.objects.get(name='add')
         old_label = FeedbackLabel.objects.get(name='test')
@@ -325,7 +325,7 @@ class FeedbackLabelSystemTest(LiveServerTestCase):
         self.assertEqual(len(current), 1)
 
         self.browser.find_element_by_id('submit-feedback').click()
-        sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+        sub_url = 'correction/ended'
         WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
         label = FeedbackLabel.objects.get(name='test')
 
@@ -372,7 +372,7 @@ class FeedbackLabelSystemTest(LiveServerTestCase):
         self.assertGreater(len(added), 1)
 
         self.browser.find_element_by_id('submit-feedback').click()
-        sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
+        sub_url = 'correction/ended'
         WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))
         label = FeedbackLabel.objects.get(name='add')
 
diff --git a/functional_tests/test_front_pages.py b/functional_tests/test_front_pages.py
index a213fa006e73f121cc2f2fdd5a917a2f60d1916a..710459edf2d7d29f40c0dcbd48809cc45c66bfa6 100644
--- a/functional_tests/test_front_pages.py
+++ b/functional_tests/test_front_pages.py
@@ -49,18 +49,14 @@ class UntestedParent:
             self._login()
             WebDriverWait(self.browser, 10).until(subscriptions_loaded_cond(self.browser))
             tasks = self.browser.find_element_by_name('subscription-list')
-            title = tasks.find_element_by_class_name('v-toolbar__title')
-            self.assertEqual('Tasks', title.text)
-            subscription_links = extract_hrefs_hashes(
+            submission_type_links = extract_hrefs_hashes(
                 tasks.find_elements_by_tag_name('a')
             )
-            subscriptions = models.SubmissionSubscription.objects.filter(
-                owner__username=self.username,
-                deactivated=False,
-                feedback_stage=models.SubmissionSubscription.FEEDBACK_CREATION
-            ).exclude(query_type=models.SubmissionSubscription.RANDOM)
-            for sub in subscriptions:
-                self.assertIn(f'/subscription/{sub.pk}', subscription_links)
+            sub_types = models.SubmissionType.objects.all()
+            default_group = models.Group.objects.first()
+            for sub_type in sub_types:
+                self.assertIn(f'/correction/{sub_type.pk}/feedback-creation/{default_group.pk}',
+                              submission_type_links)
 
 
 class FrontPageTestsTutor(UntestedParent.FrontPageTestsTutorReviewer):
@@ -83,8 +79,6 @@ class FrontPageTestsTutor(UntestedParent.FrontPageTestsTutorReviewer):
         links = extract_hrefs_hashes(drawer.find_elements_by_tag_name('a'))
         print(links)
         self.assertTrue(all(link in links for link in ['/home', '/feedback']))
-        task_title = drawer.find_element_by_class_name('v-toolbar__title')
-        self.assertEqual('Tasks', task_title.text)
         footer = drawer.find_element_by_class_name('sidebar-footer')
         feedback_link = footer.find_element_by_css_selector('a.feedback-link')
         self.assertEqual('Give us Feedback!', feedback_link.text)
@@ -113,8 +107,6 @@ class FrontPageTestsReviewer(UntestedParent.FrontPageTestsTutorReviewer):
         links = extract_hrefs_hashes(drawer.find_elements_by_tag_name('a'))
         self.assertTrue(all(link in links for link in
                             ['/home', '/feedback', '/participant-overview', '/tutor-overview']))
-        task_title = drawer.find_element_by_class_name('v-toolbar__title')
-        self.assertEqual('Tasks', task_title.text)
         footer = drawer.find_element_by_class_name('sidebar-footer')
         feedback_link = footer.find_element_by_css_selector('a.feedback-link')
         self.assertEqual('Give us Feedback!', feedback_link.text)
diff --git a/functional_tests/util.py b/functional_tests/util.py
index 42911a3e87d49d007d4c652c551c3cae5e99b694..c27adb42c4d34c5525a8155773a311cb077c2b35 100644
--- a/functional_tests/util.py
+++ b/functional_tests/util.py
@@ -102,14 +102,20 @@ def go_to_subscription(test_class_instance, stage='initial', sub_type=None):
     sub_type = sub_type if sub_type is not None else test_class_instance.sub_type
 
     sub_type_xpath = f'//*[contains(text(), "{sub_type.name}") ' \
-        f'and not(contains(@class, "inactive"))]'
+                     f'and not(contains(@class, "inactive-subscription")) ' \
+                     f'and contains(@class, "subscription") ' \
+                     f'and not(ancestor::div[contains(@style,"display: none;")])]'
+    print(tasks.find_elements_by_xpath(sub_type_xpath))
     WebDriverWait(test_class_instance.browser, 10).until(
         ec.element_to_be_clickable((By.XPATH, sub_type_xpath)),
         message="SubmissionType not clickable"
     )
     sub_type_el = tasks.find_element_by_xpath(sub_type_xpath)
     sub_type_el.click()
-    WebDriverWait(test_class_instance.browser, 10).until(ec.url_contains('subscription'))
+    WebDriverWait(test_class_instance.browser, 10).until(
+        ec.url_contains('correction'),
+        message='URL not change to correction URL'
+    )
 
 
 def correct_some_submission(test_class_instance):
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 36cca69c88ad14a070432e9dc835cde0d64b1fe8..482aa5c594c665557d9a7af3c590cd9c77b6dcdd 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -211,5 +211,6 @@ LOGGING = {
 CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
 CONSTANCE_CONFIG = {
     'STOP_ON_PASS': (False, "Stop correction when for pass "
-                            "only students when they reach pass score")
+                            "only students when they reach pass score"),
+    'SINGLE_CORRECTION': (False, "Set submitted feedback immediately to final and skip validation")
 }
diff --git a/util/factories.py b/util/factories.py
index eb1a0411c806c5c2e4f5e52b28990e4d493b6ccc..5f2739bfabac2ac702e231c171b21c09137b49e7 100644
--- a/util/factories.py
+++ b/util/factories.py
@@ -3,7 +3,7 @@ from xkcdpass import xkcd_password as xp
 
 from core import models
 from core.models import (ExamType, Feedback, StudentInfo, Submission,
-                         SubmissionType, UserAccount)
+                         SubmissionType, UserAccount, Group)
 
 STUDENTS = 'students'
 TUTORS = 'tutors'
@@ -53,7 +53,7 @@ class GradyUserFactory:
         }[role]
 
     def _make_base_user(self, username, role, password=None,
-                        store_pw=False, fullname='', **kwargs):
+                        store_pw=False, fullname='', exercise_groups=None, **kwargs):
         """ This is a specific wrapper for the django update_or_create method of
         objects.
             * If now username is passed, a generic one will be generated
@@ -76,6 +76,14 @@ class GradyUserFactory:
             role=role,
             defaults=kwargs)
 
+        exercise_groups = [] if exercise_groups is None else exercise_groups
+
+        groups_in_db = []
+        for group in exercise_groups:
+            groups_in_db.append(Group.objects.get_or_create(name=group)[0].pk)
+
+        user.exercise_groups.set(groups_in_db)
+
         if created or password is not None:
             password = self.make_password() if password is None else password
             user.set_password(password)
diff --git a/util/factory_boys.py b/util/factory_boys.py
index 44dcd02e17f56ff6a6e462588f363897b5c5a029..b8406ee821da3d8a8f067e88bb3b60cab8f4cf36 100644
--- a/util/factory_boys.py
+++ b/util/factory_boys.py
@@ -29,6 +29,12 @@ class SubmissionTypeFactory(DjangoModelFactory):
     programming_language = models.SubmissionType.C
 
 
+class GroupFactory(DjangoModelFactory):
+    class Meta:
+        model = models.Group
+    name = factory.Sequence(lambda n: f"Group [{n}]")
+
+
 class UserAccountFactory(DjangoModelFactory):
     class Meta:
         model = models.UserAccount
@@ -39,6 +45,11 @@ class UserAccountFactory(DjangoModelFactory):
     username = factory.Sequence(lambda n: f"{fake.user_name()}-{n}")
     password = factory.PostGenerationMethodCall('set_password', 'redrum-is-murder-reversed')
 
+    @factory.post_generation
+    def exercise_groups(self, create, extracted, **kwargs):
+        default_group, _ = models.Group.objects.get_or_create(name="Default Group")
+        self.exercise_groups.add(default_group)
+
 
 class StudentInfoFactory(DjangoModelFactory):
     class Meta:
@@ -82,16 +93,8 @@ class FeedbackCommentFactory(DjangoModelFactory):
     of_feedback = factory.SubFactory(FeedbackFactory)
 
 
-class SubmissionSubscriptionFactory(DjangoModelFactory):
-    class Meta:
-        model = models.SubmissionSubscription
-
-    owner = factory.SubFactory(UserAccountFactory)
-
-
 class TutorSubmissionAssignmentFactory(DjangoModelFactory):
     class Meta:
         model = models.TutorSubmissionAssignment
 
     submission = factory.SubFactory(SubmissionFactory)
-    subscription = factory.SubFactory(SubmissionSubscriptionFactory)