From fc57a9a69d7e3fa6cdc00b5558792787b3a68611 Mon Sep 17 00:00:00 2001
From: janmax <mail-github@jmx.io>
Date: Sat, 4 Nov 2017 21:30:47 +0100
Subject: [PATCH] Reworked the user account system and removed groups

---
 backend/.pylintrc                             |   3 +-
 backend/Makefile                              |  42 +++++
 backend/core/admin.py                         |  80 +++++++-
 backend/core/migrations/0001_initial.py       | 116 +++++++++---
 .../migrations/0002_auto_20170412_1447.py     |  27 ---
 .../migrations/0003_auto_20170412_1507.py     |  25 ---
 .../migrations/0004_auto_20170412_1704.py     |  25 ---
 .../migrations/0005_auto_20170413_0124.py     |  25 ---
 .../migrations/0006_auto_20170413_1102.py     |  20 --
 .../migrations/0007_auto_20170522_1827.py     |  25 ---
 .../migrations/0008_auto_20170522_1834.py     |  24 ---
 .../migrations/0009_auto_20170710_1308.py     |  25 ---
 .../migrations/0010_auto_20170710_1604.py     |  34 ----
 .../migrations/0011_auto_20170710_1610.py     |  25 ---
 .../migrations/0012_auto_20170711_1104.py     |  35 ----
 .../migrations/0013_auto_20170712_1643.py     |  27 ---
 .../migrations/0014_auto_20170712_1704.py     |  22 ---
 .../migrations/0015_auto_20170713_1220.py     |  19 --
 .../migrations/0016_auto_20170714_1634.py     |  41 -----
 backend/core/models.py                        | 174 ++++++++++++------
 backend/core/templates/base.html              |   5 -
 backend/core/tests.py                         |  51 ++---
 backend/grady/settings/default.py             |   5 +-
 backend/grady/settings/live.py                |   1 -
 backend/util/importer.py                      | 141 +++++++-------
 25 files changed, 434 insertions(+), 583 deletions(-)
 create mode 100644 backend/Makefile
 delete mode 100644 backend/core/migrations/0002_auto_20170412_1447.py
 delete mode 100644 backend/core/migrations/0003_auto_20170412_1507.py
 delete mode 100644 backend/core/migrations/0004_auto_20170412_1704.py
 delete mode 100644 backend/core/migrations/0005_auto_20170413_0124.py
 delete mode 100644 backend/core/migrations/0006_auto_20170413_1102.py
 delete mode 100644 backend/core/migrations/0007_auto_20170522_1827.py
 delete mode 100644 backend/core/migrations/0008_auto_20170522_1834.py
 delete mode 100644 backend/core/migrations/0009_auto_20170710_1308.py
 delete mode 100644 backend/core/migrations/0010_auto_20170710_1604.py
 delete mode 100644 backend/core/migrations/0011_auto_20170710_1610.py
 delete mode 100644 backend/core/migrations/0012_auto_20170711_1104.py
 delete mode 100644 backend/core/migrations/0013_auto_20170712_1643.py
 delete mode 100644 backend/core/migrations/0014_auto_20170712_1704.py
 delete mode 100644 backend/core/migrations/0015_auto_20170713_1220.py
 delete mode 100644 backend/core/migrations/0016_auto_20170714_1634.py

diff --git a/backend/.pylintrc b/backend/.pylintrc
index ba51efd0..5f460322 100644
--- a/backend/.pylintrc
+++ b/backend/.pylintrc
@@ -2,7 +2,7 @@
 
 # Add files or directories to the blacklist. They should be base names, not
 # paths.
-ignore=CVS, migrations, static, env, docs, manage.py
+ignore=CVS, migrations, static, env, docs, manage.py, tests
 
 # Add files or directories matching the regex patterns to the blacklist. The
 # regex matches against base names, not paths.
@@ -26,6 +26,7 @@ suggestion-mode=yes
 # active Python interpreter and may run arbitrary code.
 unsafe-load-any-extension=no
 
+load-plugins=pylint_django
 
 [MESSAGES CONTROL]
 
diff --git a/backend/Makefile b/backend/Makefile
new file mode 100644
index 00000000..1fbb4228
--- /dev/null
+++ b/backend/Makefile
@@ -0,0 +1,42 @@
+APP_LIST ?= core grady util
+DB_NAME = postgres
+
+.PHONY: collectstatic run install migrations-check isort isort-check
+
+collectstatic: # used only in production
+	python manage.py collectstatic --ignore node_modules
+	python manage.py compress --force
+
+run:
+	python manage.py runserver 0.0.0.0:8000
+
+migrations-check:
+	python manage.py makemigrations --check --dry-run
+
+isort:
+	isort -rc $(APP_LIST)
+
+isort-check:
+	isort -c -rc $(APP_LIST)
+
+loaddata:
+	python manage.py loaddata core/fixtures/testdata-groups.json
+
+loadexamples:
+	python manage.py loaddata core/fixtures/testdata-user.json
+	python manage.py loaddata core/fixtures/testdata-core.json
+
+install:
+	pip install -r requirements.txt
+	yarn
+
+test:
+	python manage.py run test
+
+coverage:
+	coverage run manage.py test
+	coverage report
+
+db:
+	docker run -rm --name $(DB_NAME) -p 5432:5432 postgres:9.5
+
diff --git a/backend/core/admin.py b/backend/core/admin.py
index 300130b8..9302bfe2 100644
--- a/backend/core/admin.py
+++ b/backend/core/admin.py
@@ -1,11 +1,85 @@
+from django import forms
 from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
 
-from .models import Feedback, Student, Submission, SubmissionType, Test
+from core.models import (Feedback, Reviewer, Student, Submission,
+                         SubmissionType, Test, Tutor, UserAccount)
 
-# Register your models here.
 
+class UserCreationForm(forms.ModelForm):
+    """A form for creating new users. Includes all the required
+    fields, plus a repeated password."""
+    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
+    password2 = forms.CharField(
+        label='Password confirmation', widget=forms.PasswordInput)
+
+    class Meta:
+        model = UserAccount
+        fields = ()
+
+    def clean_password2(self):
+        # Check that the two password entries match
+        password1 = self.cleaned_data.get("password1")
+        password2 = self.cleaned_data.get("password2")
+        if password1 and password2 and password1 != password2:
+            raise forms.ValidationError("Passwords don't match")
+        return password2
+
+    def save(self, commit=True):
+        # Save the provided password in hashed format
+        user = super(UserCreationForm, self).save(commit=False)
+        user.set_password(self.cleaned_data["password1"])
+        if commit:
+            user.save()
+        return user
+
+
+class UserChangeForm(forms.ModelForm):
+    """A form for updating users. Includes all the fields on
+    the user, but replaces the password field with admin's
+    password hash display field.
+    """
+    password = ReadOnlyPasswordHashField()
+
+    class Meta:
+        model = UserAccount
+        fields = ('password', 'is_active', 'is_admin')
+
+    def clean_password(self):
+        return self.initial["password"]
+
+
+class UserAdmin(BaseUserAdmin):
+    # The forms to add and change user instances
+    form = UserChangeForm
+    add_form = UserCreationForm
+
+    # The fields to be used in displaying the User model.
+    # These override the definitions on the base UserAdmin
+    # that reference specific fields on auth.User.
+    list_display = ('is_admin',)
+    list_filter = ('is_admin',)
+    fieldsets = (
+        (None, {'fields': ('password',)}),
+        ('Permissions', {'fields': ('is_admin',)}),
+    )
+    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
+    # overrides get_fieldsets to use this attribute when creating a user.
+    add_fieldsets = (
+        (None, {
+            'classes': ('wide',),
+            'fields': ('password1', 'password2')}
+         ),
+    )
+    filter_horizontal = ()
+
+
+admin.site.register(UserAccount, UserAdmin)
 admin.site.register(SubmissionType)
 admin.site.register(Feedback)
-admin.site.register(Student)
 admin.site.register(Test)
 admin.site.register(Submission)
+admin.site.register(Reviewer)
+admin.site.register(Student)
+admin.site.register(Tutor)
diff --git a/backend/core/migrations/0001_initial.py b/backend/core/migrations/0001_initial.py
index 41d82568..8f531591 100644
--- a/backend/core/migrations/0001_initial.py
+++ b/backend/core/migrations/0001_initial.py
@@ -1,12 +1,11 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.10.6 on 2017-04-05 20:11
+# Generated by Django 1.11.7 on 2017-11-04 19:10
 from __future__ import unicode_literals
 
-import django.db.models.deletion
+import core.models
 from django.conf import settings
 from django.db import migrations, models
-
-import core.models
+import django.db.models.deletion
 
 
 class Migration(migrations.Migration):
@@ -14,33 +13,72 @@ class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
     operations = [
+        migrations.CreateModel(
+            name='UserAccount',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('password', models.CharField(max_length=128, verbose_name='password')),
+                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=150, unique=True)),
+                ('fullname', models.CharField(blank=True, max_length=70, verbose_name='full name')),
+                ('is_staff', models.BooleanField(default=False, verbose_name='staff status')),
+                ('is_admin', models.BooleanField(default=False)),
+                ('is_superuser', models.BooleanField(default=False)),
+                ('is_active', models.BooleanField(default=True, verbose_name='active')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='ExamType',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('module_reference', models.CharField(max_length=50, unique=True)),
+                ('total_score', models.PositiveIntegerField()),
+                ('pass_score', models.PositiveIntegerField()),
+                ('pass_only', models.BooleanField(default=False)),
+            ],
+            options={
+                'verbose_name': 'ExamType',
+                'verbose_name_plural': 'ExamTypes',
+            },
+        ),
         migrations.CreateModel(
             name='Feedback',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('text', models.TextField()),
-                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
                 ('score', models.PositiveIntegerField(default=0)),
-                ('status', models.CharField(choices=[('I', 'editable'), ('A', 'accepted'), ('R', 'request review'), ('O', 'request reassignment')], default='I', max_length=1)),
-                ('origin', models.CharField(choices=[('E', 'was empty'), ('UT', 'passed unittests'), ('CF', 'did not compile'), ('LF', 'could not link'), ('M', 'created by a human. yak!')], default='M', max_length=2)),
-                ('of_reviewer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reviewed_submissions', to=settings.AUTH_USER_MODEL)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('modified', models.DateTimeField(auto_now=True)),
+                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
+                ('status', models.IntegerField(choices=[(0, 'editable'), (1, 'request reassignment'), (2, 'request review'), (3, 'accepted')], default=0)),
+                ('origin', models.IntegerField(choices=[(0, 'was empty'), (1, 'passed unittests'), (2, 'did not compile'), (3, 'could not link'), (4, 'created by a human. yak!')], default=4)),
             ],
             options={
                 'verbose_name': 'Feedback',
                 'verbose_name_plural': 'Feedback Set',
             },
         ),
+        migrations.CreateModel(
+            name='Reviewer',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='reviewer', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
         migrations.CreateModel(
             name='Student',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('has_logged_in', models.BooleanField(default=False)),
                 ('matrikel_no', models.CharField(default=core.models.random_matrikel_no, max_length=8, unique=True)),
-                ('name', models.CharField(default='__no_name__', max_length=50)),
-                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                ('exam', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='core.ExamType')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL)),
             ],
             options={
                 'verbose_name': 'Student',
@@ -51,16 +89,15 @@ class Migration(migrations.Migration):
             name='Submission',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
-                ('seen', models.BooleanField(default=False)),
+                ('seen_by_student', models.BooleanField(default=False)),
                 ('text', models.TextField(blank=True)),
-                ('pre_corrections', models.TextField(blank=True)),
-                ('final_feedback', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Feedback')),
-                ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Student')),
+                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
+                ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='core.Student')),
             ],
             options={
                 'verbose_name': 'Submission',
                 'verbose_name_plural': 'Submission Set',
+                'ordering': ('type__name',),
             },
         ),
         migrations.CreateModel(
@@ -68,30 +105,63 @@ class Migration(migrations.Migration):
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('name', models.CharField(max_length=50, unique=True)),
-                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
                 ('full_score', models.PositiveIntegerField(default=0)),
-                ('task_description', models.TextField()),
-                ('possible_solution', models.TextField()),
-                ('correction_guideline', models.TextField()),
+                ('description', models.TextField()),
+                ('solution', models.TextField()),
+                ('slug', models.SlugField(default=core.models.random_slug, editable=False, unique=True)),
             ],
             options={
                 'verbose_name': 'SubmissionType',
                 'verbose_name_plural': 'SubmissionType Set',
             },
         ),
+        migrations.CreateModel(
+            name='Test',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=30)),
+                ('label', models.CharField(max_length=50)),
+                ('annotation', models.TextField()),
+                ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tests', to='core.Submission')),
+            ],
+            options={
+                'verbose_name': 'Test',
+                'verbose_name_plural': 'Tests',
+            },
+        ),
+        migrations.CreateModel(
+            name='Tutor',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='tutor', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
         migrations.AddField(
             model_name='submission',
             name='type',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='core.SubmissionType'),
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='submissions', to='core.SubmissionType'),
+        ),
+        migrations.AddField(
+            model_name='feedback',
+            name='of_reviewer',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_submissions', to='core.Reviewer'),
         ),
         migrations.AddField(
             model_name='feedback',
             name='of_submission',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback_list', to='core.Submission'),
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='core.Submission'),
         ),
         migrations.AddField(
             model_name='feedback',
             name='of_tutor',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='corrected_submissions', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedback_list', to='core.Tutor'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='test',
+            unique_together=set([('submission', 'name')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='submission',
+            unique_together=set([('type', 'student')]),
         ),
     ]
diff --git a/backend/core/migrations/0002_auto_20170412_1447.py b/backend/core/migrations/0002_auto_20170412_1447.py
deleted file mode 100644
index f92b1853..00000000
--- a/backend/core/migrations/0002_auto_20170412_1447.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-12 14:47
-from __future__ import unicode_literals
-
-import django.utils.timezone
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='feedback',
-            name='created',
-            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
-            preserve_default=False,
-        ),
-        migrations.AddField(
-            model_name='feedback',
-            name='modified',
-            field=models.DateTimeField(auto_now=True),
-        ),
-    ]
diff --git a/backend/core/migrations/0003_auto_20170412_1507.py b/backend/core/migrations/0003_auto_20170412_1507.py
deleted file mode 100644
index be676b4c..00000000
--- a/backend/core/migrations/0003_auto_20170412_1507.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-12 15:07
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0002_auto_20170412_1447'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='feedback',
-            name='origin',
-            field=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),
-        ),
-        migrations.AlterField(
-            model_name='feedback',
-            name='status',
-            field=models.IntegerField(choices=[(0, 'editable'), (1, 'accepted'), (2, 'request review'), (3, 'request reassignment')], default=0),
-        ),
-    ]
diff --git a/backend/core/migrations/0004_auto_20170412_1704.py b/backend/core/migrations/0004_auto_20170412_1704.py
deleted file mode 100644
index fc41092b..00000000
--- a/backend/core/migrations/0004_auto_20170412_1704.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-12 17:04
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0003_auto_20170412_1507'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='student',
-            name='logged_in',
-            field=models.BooleanField(default=False),
-        ),
-        migrations.AlterField(
-            model_name='feedback',
-            name='status',
-            field=models.IntegerField(choices=[(0, 'editable'), (1, 'request reassignment'), (2, 'request review'), (3, 'accepted')], default=0),
-        ),
-    ]
diff --git a/backend/core/migrations/0005_auto_20170413_0124.py b/backend/core/migrations/0005_auto_20170413_0124.py
deleted file mode 100644
index b25f249e..00000000
--- a/backend/core/migrations/0005_auto_20170413_0124.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-13 01:24
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0004_auto_20170412_1704'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='submission',
-            name='final_feedback',
-        ),
-        migrations.AlterField(
-            model_name='feedback',
-            name='of_submission',
-            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='core.Submission'),
-        ),
-    ]
diff --git a/backend/core/migrations/0006_auto_20170413_1102.py b/backend/core/migrations/0006_auto_20170413_1102.py
deleted file mode 100644
index 38f11223..00000000
--- a/backend/core/migrations/0006_auto_20170413_1102.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-13 11:02
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0005_auto_20170413_0124'),
-    ]
-
-    operations = [
-        migrations.RenameField(
-            model_name='student',
-            old_name='logged_in',
-            new_name='has_logged_in',
-        ),
-    ]
diff --git a/backend/core/migrations/0007_auto_20170522_1827.py b/backend/core/migrations/0007_auto_20170522_1827.py
deleted file mode 100644
index fb6c0578..00000000
--- a/backend/core/migrations/0007_auto_20170522_1827.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-05-22 18:27
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0006_auto_20170413_1102'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='submission',
-            name='student',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='core.Student'),
-        ),
-        migrations.AlterUniqueTogether(
-            name='submission',
-            unique_together=set([('type', 'student')]),
-        ),
-    ]
diff --git a/backend/core/migrations/0008_auto_20170522_1834.py b/backend/core/migrations/0008_auto_20170522_1834.py
deleted file mode 100644
index 7eb42fad..00000000
--- a/backend/core/migrations/0008_auto_20170522_1834.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-05-22 18:34
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0007_auto_20170522_1827'),
-    ]
-
-    operations = [
-        migrations.RenameField(
-            model_name='submission',
-            old_name='seen',
-            new_name='seen_by_student',
-        ),
-        migrations.RemoveField(
-            model_name='submissiontype',
-            name='correction_guideline',
-        ),
-    ]
diff --git a/backend/core/migrations/0009_auto_20170710_1308.py b/backend/core/migrations/0009_auto_20170710_1308.py
deleted file mode 100644
index 0a06d82f..00000000
--- a/backend/core/migrations/0009_auto_20170710_1308.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-10 13:08
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0008_auto_20170522_1834'),
-    ]
-
-    operations = [
-        migrations.RenameField(
-            model_name='submissiontype',
-            old_name='task_description',
-            new_name='description',
-        ),
-        migrations.RenameField(
-            model_name='submissiontype',
-            old_name='possible_solution',
-            new_name='solution',
-        ),
-    ]
diff --git a/backend/core/migrations/0010_auto_20170710_1604.py b/backend/core/migrations/0010_auto_20170710_1604.py
deleted file mode 100644
index ab702ede..00000000
--- a/backend/core/migrations/0010_auto_20170710_1604.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-10 16:04
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0009_auto_20170710_1308'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='Test',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=30, unique=True)),
-                ('label', models.CharField(max_length=50, unique=True)),
-                ('annotation', models.TextField()),
-                ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tests', to='core.Submission')),
-            ],
-            options={
-                'verbose_name': 'Test',
-                'verbose_name_plural': 'Tests',
-            },
-        ),
-        migrations.AlterUniqueTogether(
-            name='test',
-            unique_together=set([('submission', 'name')]),
-        ),
-    ]
diff --git a/backend/core/migrations/0011_auto_20170710_1610.py b/backend/core/migrations/0011_auto_20170710_1610.py
deleted file mode 100644
index 7bf4689a..00000000
--- a/backend/core/migrations/0011_auto_20170710_1610.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-10 16:10
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0010_auto_20170710_1604'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='test',
-            name='label',
-            field=models.CharField(max_length=50),
-        ),
-        migrations.AlterField(
-            model_name='test',
-            name='name',
-            field=models.CharField(max_length=30),
-        ),
-    ]
diff --git a/backend/core/migrations/0012_auto_20170711_1104.py b/backend/core/migrations/0012_auto_20170711_1104.py
deleted file mode 100644
index 9cc19764..00000000
--- a/backend/core/migrations/0012_auto_20170711_1104.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-11 11:04
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0011_auto_20170710_1610'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='ExamType',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('module_reference', models.CharField(max_length=50)),
-                ('total_score', models.PositiveIntegerField()),
-                ('pass_score', models.PositiveIntegerField()),
-                ('pass_only', models.BooleanField(default=False)),
-            ],
-            options={
-                'verbose_name': 'ExamType',
-                'verbose_name_plural': 'ExamTypes',
-            },
-        ),
-        migrations.AddField(
-            model_name='student',
-            name='exam',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='students', to='core.ExamType'),
-        ),
-    ]
diff --git a/backend/core/migrations/0013_auto_20170712_1643.py b/backend/core/migrations/0013_auto_20170712_1643.py
deleted file mode 100644
index bd526207..00000000
--- a/backend/core/migrations/0013_auto_20170712_1643.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-12 16:43
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0012_auto_20170711_1104'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='examtype',
-            name='module_reference',
-            field=models.CharField(max_length=50, unique=True),
-        ),
-        migrations.AlterField(
-            model_name='student',
-            name='user',
-            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL),
-        ),
-    ]
diff --git a/backend/core/migrations/0014_auto_20170712_1704.py b/backend/core/migrations/0014_auto_20170712_1704.py
deleted file mode 100644
index 65a9c943..00000000
--- a/backend/core/migrations/0014_auto_20170712_1704.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-12 17:04
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0013_auto_20170712_1643'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='feedback',
-            name='of_tutor',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback_list', to=settings.AUTH_USER_MODEL),
-        ),
-    ]
diff --git a/backend/core/migrations/0015_auto_20170713_1220.py b/backend/core/migrations/0015_auto_20170713_1220.py
deleted file mode 100644
index eb723098..00000000
--- a/backend/core/migrations/0015_auto_20170713_1220.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-07-13 12:20
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0014_auto_20170712_1704'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='submission',
-            options={'ordering': ('type__name',), 'verbose_name': 'Submission', 'verbose_name_plural': 'Submission Set'},
-        ),
-    ]
diff --git a/backend/core/migrations/0016_auto_20170714_1634.py b/backend/core/migrations/0016_auto_20170714_1634.py
deleted file mode 100644
index 227a18a8..00000000
--- a/backend/core/migrations/0016_auto_20170714_1634.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.3 on 2017-07-14 16:34
-from __future__ import unicode_literals
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0015_auto_20170713_1220'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='submission',
-            name='pre_corrections',
-        ),
-        migrations.AlterField(
-            model_name='feedback',
-            name='of_reviewer',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_submissions', to=settings.AUTH_USER_MODEL),
-        ),
-        migrations.AlterField(
-            model_name='feedback',
-            name='of_tutor',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedback_list', to=settings.AUTH_USER_MODEL),
-        ),
-        migrations.AlterField(
-            model_name='student',
-            name='exam',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='core.ExamType'),
-        ),
-        migrations.AlterField(
-            model_name='submission',
-            name='type',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='submissions', to='core.SubmissionType'),
-        ),
-    ]
diff --git a/backend/core/models.py b/backend/core/models.py
index 8e8f10a8..f6913958 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -10,7 +10,8 @@ from collections import OrderedDict
 from random import randrange, sample
 from string import ascii_lowercase
 
-from django.contrib.auth.models import User
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
+from django.contrib.auth import get_user_model
 from django.db import models
 from django.db.models import Value as V
 from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q,
@@ -18,7 +19,7 @@ from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q,
 from django.db.models.functions import Coalesce
 
 
-def random_slug(slug_length: int=16) -> str:
+def random_slug(slug_length: int = 16) -> str:
     """Used for all the slug fields in the application instead of relying on
     the primary keys. They are not cryptographically secure since random is
     used.
@@ -78,9 +79,9 @@ class ExamType(models.Model):
         return self.module_reference
 
     module_reference = models.CharField(max_length=50, unique=True)
-    total_score      = models.PositiveIntegerField()
-    pass_score       = models.PositiveIntegerField()
-    pass_only        = models.BooleanField(default=False)
+    total_score = models.PositiveIntegerField()
+    pass_score = models.PositiveIntegerField()
+    pass_only = models.BooleanField(default=False)
 
 
 class SubmissionType(models.Model):
@@ -104,18 +105,18 @@ class SubmissionType(models.Model):
     solution : TextField
         A sample solution or a correction guideline
     """
-    name        = models.CharField(max_length=50, unique=True)
-    full_score  = models.PositiveIntegerField(default=0)
+    name = models.CharField(max_length=50, unique=True)
+    full_score = models.PositiveIntegerField(default=0)
     description = models.TextField()
-    solution    = models.TextField()
-    slug        = models.SlugField(
+    solution = models.TextField()
+    slug = models.SlugField(
         editable=False, unique=True, default=random_slug)
 
     def __str__(self) -> str:
         return self.name
 
     class Meta:
-        verbose_name        = "SubmissionType"
+        verbose_name = "SubmissionType"
         verbose_name_plural = "SubmissionType Set"
 
     @classmethod
@@ -133,7 +134,7 @@ class SubmissionType(models.Model):
             The annotated QuerySet as described above
         """
         return cls.objects\
-            .annotate( # to display only manual
+            .annotate(  # to display only manual
                 feedback_count=Count(
                     Case(
                         When(
@@ -149,8 +150,88 @@ class SubmissionType(models.Model):
             ).order_by('name')
 
 
+class UserAccountManager(BaseUserManager):
+
+    def create_user(self, password=None, **kwargs):
+        if not kwargs.get('username'):
+            raise ValueError('Users must have a valid username.')
+
+        account = self.model(
+            username=kwargs.get('username')
+        )
+
+        account.set_password(password)
+        account.save()
+
+        return account
+
+    def create_superuser(self, password, **kwargs):
+        account = self.create_user(password, **kwargs)
+
+        account.is_admin = True
+        account.is_staff = True
+        account.is_superuser = True
+        account.save()
+
+        return account
+
+
+class UserAccount(AbstractBaseUser):
+    """
+    An abstract base class implementing a fully featured User model with
+    admin-compliant permissions.
+
+    Username and password are required. Other fields are optional.
+    """
+    objects = UserAccountManager()
+
+    username = models.CharField(
+        max_length=150,
+        unique=True,
+        error_messages={
+            'unique': "A user with that username already exists.",
+        },
+    )
+
+    fullname = models.CharField('full name', max_length=70, blank=True)
+
+    is_staff = models.BooleanField('staff status', default=False)
+    is_admin = models.BooleanField(default=False)
+    is_superuser = models.BooleanField(default=False)
+
+    is_active = models.BooleanField('active', default=True)
+
+    USERNAME_FIELD = 'username'
+
+    def get_associated_user(self):
+        """ Returns the user type that is associated with this user obj """
+        return \
+            (hasattr(self, 'student') and self.student) or \
+            (hasattr(self, 'reviewer') and self.reviewer) or \
+            (hasattr(self, 'tutor') and self.tutor)
+
+    def get_short_name(self):
+        return self.username
+
+    def get_username(self):
+        return self.username
+
+
+class Tutor(models.Model):
+    user = models.OneToOneField(
+        get_user_model(), unique=True,
+        on_delete=models.CASCADE, related_name='tutor')
+
+
+class Reviewer(models.Model):
+    user = models.OneToOneField(
+        get_user_model(), unique=True,
+        on_delete=models.CASCADE, related_name='reviewer')
+
+
 class Student(models.Model):
-    """The student model includes all information of a student, that we got
+    """
+    The student model includes all information of a student, that we got
     from the E-Learning output, along with some useful classmethods that provide
     specially annotated QuerySets.
 
@@ -166,27 +247,16 @@ class Student(models.Model):
 
         matrikel_no (CharField):
             The matriculation number of the student
-
-        name (CharField):
-            The students full real name
-
-        user (UserModel):
-            The django auth user that makes a student authenticates with.
     """
-    has_logged_in   = models.BooleanField(default=False)
-    name            = models.CharField(max_length=50, default="__no_name__")
-    matrikel_no     = models.CharField(
+    has_logged_in = models.BooleanField(default=False)
+    matrikel_no = models.CharField(
         unique=True, max_length=8, default=random_matrikel_no)
-    user            = models.OneToOneField(
-        User, on_delete=models.CASCADE,
-        related_name='student',
-        limit_choices_to={'groups__name': 'Students'},
-    )
-    exam            = models.ForeignKey(
-        'ExamType',
-        on_delete=models.SET_NULL,
-        related_name='students',
-        null=True)
+    exam = models.ForeignKey(
+        'ExamType', on_delete=models.SET_NULL,
+        related_name='students', null=True)
+    user = models.OneToOneField(
+        get_user_model(), unique=True,
+        on_delete=models.CASCADE, related_name='student')
 
     def score_per_submission(self):
         """ TODO: get rid of it and use an annotation.
@@ -240,7 +310,7 @@ class Student(models.Model):
         return self.user.username
 
     class Meta:
-        verbose_name        = "Student"
+        verbose_name = "Student"
         verbose_name_plural = "Student Set"
 
 
@@ -260,8 +330,8 @@ class Test(models.Model):
     submission : ForeignKey
         The submission the tests where generated on
     """
-    name       = models.CharField(max_length=30)
-    label      = models.CharField(max_length=50)
+    name = models.CharField(max_length=30)
+    label = models.CharField(max_length=50)
     annotation = models.TextField()
     submission = models.ForeignKey(
         'submission',
@@ -270,9 +340,9 @@ class Test(models.Model):
     )
 
     class Meta:
-        verbose_name        = "Test"
+        verbose_name = "Test"
         verbose_name_plural = "Tests"
-        unique_together     = (('submission', 'name'),)
+        unique_together = (('submission', 'name'),)
 
     def __str__(self) -> str:
         return f'{self.name} {self.label}'
@@ -301,25 +371,25 @@ class Submission(models.Model):
         Relation to the type containing meta information
     """
     seen_by_student = models.BooleanField(default=False)
-    text            = models.TextField(blank=True)
-    slug            = models.SlugField(
+    text = models.TextField(blank=True)
+    slug = models.SlugField(
         editable=False,
         unique=True,
         default=random_slug)
-    type            = models.ForeignKey(
+    type = models.ForeignKey(
         SubmissionType,
         on_delete=models.PROTECT,
         related_name='submissions')
-    student         = models.ForeignKey(
+    student = models.ForeignKey(
         Student,
         on_delete=models.CASCADE,
         related_name='submissions')
 
     class Meta:
-        verbose_name        = "Submission"
+        verbose_name = "Submission"
         verbose_name_plural = "Submission Set"
-        unique_together     = (('type', 'student'),)
-        ordering            = ('type__name',)
+        unique_together = (('type', 'student'),)
+        ordering = ('type__name',)
 
     def __str__(self) -> str:
         return "Submission of type '{}' from Student '{}'".format(
@@ -422,10 +492,10 @@ class Feedback(models.Model):
         students submission, maybe with additional comments appended.
 
     """
-    text        = models.TextField()
-    score       = models.PositiveIntegerField(default=0)
-    created     = models.DateTimeField(auto_now_add=True)
-    modified    = models.DateTimeField(auto_now=True)
+    text = models.TextField()
+    score = models.PositiveIntegerField(default=0)
+    created = models.DateTimeField(auto_now_add=True)
+    modified = models.DateTimeField(auto_now=True)
 
     slug = models.SlugField(
         editable=False,
@@ -439,13 +509,13 @@ class Feedback(models.Model):
         blank=False,
         null=False)
     of_tutor = models.ForeignKey(
-        User,
+        Tutor,
         on_delete=models.SET_NULL,
         related_name='feedback_list',
         blank=True,
         null=True)
     of_reviewer = models.ForeignKey(
-        User,
+        Reviewer,
         on_delete=models.SET_NULL,
         related_name='reviewed_submissions',
         blank=True,
@@ -457,7 +527,7 @@ class Feedback(models.Model):
         OPEN,
         NEEDS_REVIEW,
         ACCEPTED,
-    ) = range(4) # this order matters
+    ) = range(4)  # this order matters
     STATUS = (
         (EDITABLE,      'editable'),
         (OPEN,          'request reassignment'),
@@ -490,7 +560,7 @@ class Feedback(models.Model):
     )
 
     class Meta:
-        verbose_name        = "Feedback"
+        verbose_name = "Feedback"
         verbose_name_plural = "Feedback Set"
 
     def __str__(self) -> str:
@@ -522,7 +592,7 @@ class Feedback(models.Model):
         """
         return cls.objects.filter(
             Q(status=Feedback.OPEN) &
-            ~Q(of_tutor=user) # you shall not request your own feedback
+            ~Q(of_tutor=user)  # you shall not request your own feedback
         )
 
     @classmethod
diff --git a/backend/core/templates/base.html b/backend/core/templates/base.html
index ad5bf093..90c2d78e 100644
--- a/backend/core/templates/base.html
+++ b/backend/core/templates/base.html
@@ -1,5 +1,4 @@
 {% load staticfiles %}
-{% load compress %}
 
 <!DOCTYPE html>
 <html lang="en">
@@ -12,13 +11,10 @@
   <title>Grady - {% block title %}Wellcome to correction hell!{% endblock %}</title>
 
   {# CSS includes #}
-  {% compress css %}
   <link rel="stylesheet" href="{% static 'datatables.net-bs4/css/dataTables.bootstrap4.css' %}">
   <link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
   <link rel="stylesheet" href="{% static 'css/custom.css' %}">
-  {% endcompress %}
 
-  {% compress js %}
   {# Importing stuff for ACE editor #}
   <script src="{% static 'ace-editor-builds/src-min/ace.js' %}"></script>
   <script src="{% static 'ace-editor-builds/src-min/mode-c_cpp.js' %}"></script>
@@ -31,7 +27,6 @@
   {# sortable table stuff #}
   <script src="{% static 'datatables.net/js/jquery.dataTables.js' %}"></script>
   <script src="{% static 'datatables.net-bs4/js/dataTables.bootstrap4.js' %}"></script>
-  {% endcompress %}
 </head>
 
 {# Navbar contaning: Brand - Title - User menu bar <---> (Username - Logout || Login form) #}
diff --git a/backend/core/tests.py b/backend/core/tests.py
index c6fcdc75..d9db4611 100644
--- a/backend/core/tests.py
+++ b/backend/core/tests.py
@@ -1,26 +1,15 @@
-from django.contrib.auth.models import Group
 from django.test import TestCase
 
-from core.models import Submission, SubmissionType, Feedback
+from core.models import Submission, SubmissionType, Feedback, Student, Tutor, Reviewer
 from util.importer import GradyUserFactory
 
 
-def ensure_groups():
-    Group.objects.get_or_create(name='Tutors')
-    Group.objects.get_or_create(name='Students')
-    Group.objects.get_or_create(name='Reviewers')
-
-
 class FeedbackTestCase(TestCase):
 
     factory = GradyUserFactory()
 
-    @classmethod
-    def setUpTestData(cls):
-        ensure_groups()
-
     def setUp(self):
-        self.tutor   = self.factory.make_tutor()
+        self.tutor = self.factory.make_tutor()
         self.student = self.factory.make_student()
 
         submission_type = SubmissionType.objects.create(
@@ -44,26 +33,40 @@ class FactoryTestCase(TestCase):
 
     factory = GradyUserFactory()
 
-    @classmethod
-    def setUpTestData(cls):
-        ensure_groups()
-
     def test_make_student(self):
 
         student = self.factory.make_student()
 
-        self.assertEqual(student.user.groups.filter(name='Students').count(), 1)
+        self.assertEqual(Student.objects.count(), 1)
         self.assertEqual(student.exam, None)
         self.assertEqual(len(str(student.matrikel_no)), 8)
 
     def test_can_create_reviewer(self):
-        self.assertTrue(Group.objects.get(name='Reviewers')
-                        in self.factory.make_reviewer().groups.all())
+        self.assertTrue(isinstance(self.factory.make_reviewer(), Reviewer))
+
+    def test_reviewer_appears_in_query_set(self):
+        self.assertIn(self.factory.make_reviewer(), Reviewer.objects.all())
 
     def test_can_create_tutor(self):
-        self.assertTrue(Group.objects.get(name='Tutors')
-                        in self.factory.make_tutor().groups.all())
+        self.assertIn(self.factory.make_tutor(), Tutor.objects.all())
 
     def test_can_create_student(self):
-        self.assertTrue(Group.objects.get(name='Students')
-                        in self.factory.make_student().user.groups.all())
+        self.assertIn(self.factory.make_student(), Student.objects.all())
+
+class AccountsTestCase(TestCase):
+
+    factory = GradyUserFactory()
+
+    def _test_user_obj_returns_correct_type(self, maker):
+        model = maker()
+        user = model.user
+        self.assertEqual(user.get_associated_user(), model)
+
+    def test_user_obj_returns_correct_type_student(self):
+        self._test_user_obj_returns_correct_type(self.factory.make_student)
+
+    def test_user_obj_returns_correct_type_tutor(self):
+        self._test_user_obj_returns_correct_type(self.factory.make_tutor)
+
+    def test_user_obj_returns_correct_type_reviewer(self):
+        self._test_user_obj_returns_correct_type(self.factory.make_reviewer)
diff --git a/backend/grady/settings/default.py b/backend/grady/settings/default.py
index 8e26f5b2..d9b710ae 100644
--- a/backend/grady/settings/default.py
+++ b/backend/grady/settings/default.py
@@ -39,7 +39,6 @@ INSTALLED_APPS = [
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django_extensions',
-    'compressor',
     'core',
 ]
 
@@ -132,5 +131,7 @@ COMPRESS_OFFLINE = True
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-    'compressor.finders.CompressorFinder',
 )
+
+AUTH_USER_MODEL = 'core.UserAccount'
+AUTH_PASSWORD_VALIDATORS = []
diff --git a/backend/grady/settings/live.py b/backend/grady/settings/live.py
index bfa9ad85..2f3bfe34 100644
--- a/backend/grady/settings/live.py
+++ b/backend/grady/settings/live.py
@@ -9,7 +9,6 @@ X_FRAME_OPTIONS = 'DENY'
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
-COMPRESS_ENABLED = not DEBUG
 
 # adjust this setting to your needs
 ALLOWED_HOSTS = ['localhost', '.grady.janmax.org']
diff --git a/backend/util/importer.py b/backend/util/importer.py
index 8e341e63..5c792deb 100644
--- a/backend/util/importer.py
+++ b/backend/util/importer.py
@@ -6,25 +6,23 @@ import readline
 import secrets
 from typing import Callable
 
-from django.contrib.auth.models import Group, User
-
 import util.convert
 import util.processing
-from core.models import (ExamType, Feedback, Student, Submission,
-                         SubmissionType, Test)
+from core.models import (ExamType, Feedback, Student, Tutor, Reviewer, Submission,
+                         SubmissionType, Test, UserAccount as User)
 from util.messages import info, warn
 from util.processing import EmptyTest
 
-STUDENTS  = 'Students'
-TUTORS    = 'Tutors'
-REVIEWERS = 'Reviewers'
+STUDENTS = 'students'
+TUTORS = 'tutors'
+REVIEWERS = 'reviewers'
 
-HISTFILE  = '.importer_history'
-RECORDS   = '.importer'
+HISTFILE = '.importer_history'
+RECORDS = '.importer'
 PASSWORDS = '.importer_passwords'
 
 YES = 'Y/n'
-NO  = 'y/N'
+NO = 'y/N'
 
 valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
 
@@ -98,40 +96,32 @@ def store_password(username, groupname, password):
     with open(PASSWORDS, 'w') as passwd_file:
         storage.write(passwd_file)
 
+
 class GradyUserFactory:
 
     def __init__(self, password_generator_func=get_xkcd_password, *args, **kwargs):
         self.password_generator_func = password_generator_func
 
     @staticmethod
-    def get_random_name(prefix='', suffix='', k=1):
+    def _get_random_name(prefix='', suffix='', k=1):
         return ''.join((prefix, get_xkcd_password(k), suffix))
 
-    def make_default_user(self, username, **kwargs):
-        return User.objects.update_or_create(username=username, defaults=kwargs)
-
-    def make_user_in_group(self, username, groupname, store_pw=False, **kwargs):
+    def _make_base_user(self, username, groupname, store_pw=False, **kwargs):
         """ This is a specific wrapper for the django update_or_create method of
         objects.
             * A new user is created and password and group are set accordingly
             * If the user was there before password is NOT change but group is. A
               user must only have one group.
 
-        Args:
-            username (str): the username is the login name
-            group (Group object): the (only) group the user should belong to
-            **kwargs: more attributes for user creation
-
         Returns:
             (User object, str): The user object that was added to the group and
             the password of that user if it was created.
         """
         username = username.strip()
 
-        user, created = self.make_default_user(
+        user, created = User.objects.update_or_create(
             username=username,
-            **kwargs
-        )
+            defaults=kwargs)
 
         if created:
             password = self.password_generator_func()
@@ -141,47 +131,52 @@ class GradyUserFactory:
         if created and store_pw:
             store_password(username, groupname, password)
 
-        group = Group.objects.get(name=groupname)
-        user.groups.clear() # remove all other groups
-        user.groups.add(group)
-        user.save()
-
         return user
 
+    def _get_user_model_for_group(self, groupname):
+        """ Returns the model class for a usergroup """
+        return {
+            STUDENTS: Student,
+            TUTORS: Tutor,
+            REVIEWERS: Reviewer,
+        }[groupname]
 
-    def make_user_in_student_group(self, username, **kwargs):
-        return self.make_user_in_group(username, STUDENTS, **kwargs)
+    def _make_user_generic(self, username, groupname, **kwargs):
+        """ Provides a model with a associated user but without any defaults
+        """
 
-    def make_student(self, username=None, name='__name', matrikel_no=None, exam=None, **kwargs):
         if not username:
-            username = self.get_random_name(prefix='student_')
+            username = self._get_random_name(prefix=groupname.lower() + '_')
 
-        user = self.make_user_in_student_group(username, **kwargs)
+        model = self._get_user_model_for_group(groupname)
+        user = self._make_base_user(username, groupname, **kwargs)
 
-        student, _ = Student.objects.update_or_create(
-            name=name,
-            defaults={
-                'user': user,
-                'exam': exam,
-                # TODO: find an elegant way to include optionals iff they exist
-            }
-        )
+        generic_user, _ = model.objects.get_or_create(user=user)
 
-        return student
+        return generic_user
+
+    def make_student(self, username=None, matrikel_no=None, exam=None, **kwargs):
+        """ Creates a student. Defaults can be passed via kwargs like in
+        relation managers objects.update method. """
+        user = self._make_user_generic(username, STUDENTS, **kwargs)
+        if matrikel_no:
+            user.objects.update(matrikel_no=matrikel_no)
+        if exam:
+            user.objects.update(exam=exam)
+        return user
 
     def make_tutor(self, username=None, **kwargs):
-        if not username:
-            username = self.get_random_name(prefix='tutor_')
-        return self.make_user_in_group(username, TUTORS, **kwargs)
+        """ Creates or updates a tutor if needed with defaults """
+        return self._make_user_generic(username, TUTORS, **kwargs)
 
     def make_reviewer(self, username=None, **kwargs):
-        if not username:
-            username = self.get_random_name(prefix='reviewer_')
-        return self.make_user_in_group(username, REVIEWERS, **kwargs)
+        """ Creates or updates a reviewer if needed with defaults """
+        return self._make_user_generic(username, REVIEWERS, **kwargs)
 
+# TODO more factories
 
 def add_user(username, group, **kwargs):
-    user = GradyUserFactory().make_user_in_group(
+    user = GradyUserFactory()._make_base_user(
         username, group, store_pw=True, **kwargs
     )
 
@@ -190,10 +185,10 @@ def add_user(username, group, **kwargs):
 
 def add_student(username, email, submissions, **kwargs):
 
-    user        = add_user(username, STUDENTS, email=email)
-    student, _  = Student.objects.update_or_create(
+    user = add_user(username, STUDENTS, email=email)
+    student, _ = Student.objects.update_or_create(
         user=user,
-        defaults={'user' : user, **kwargs}
+        defaults={'user': user, **kwargs}
     )
 
     return student
@@ -206,7 +201,7 @@ def add_submission(student_obj, code, tests, type):
     submission_obj, _ = Submission.objects.update_or_create(
         type=submission_type,
         student=student_obj,
-        defaults={'text' : code}
+        defaults={'text': code}
     )
 
     auto_correct, _ = User.objects.get_or_create(
@@ -232,11 +227,11 @@ def add_submission(student_obj, code, tests, type):
             Feedback.objects.update_or_create(
                 of_submission=submission_obj,
                 defaults={
-                    'of_tutor'  : auto_correct,
-                    'score'     : 0,
-                    'text'      : test_obj.label,
-                    'origin'    : FEEDBACK_MAPPER[test_obj.name],
-                    'status'    : Feedback.ACCEPTED if test_obj.name == EmptyTest.__name__ else Feedback.EDITABLE,
+                    'of_tutor': auto_correct,
+                    'score': 0,
+                    'text': test_obj.label,
+                    'origin': FEEDBACK_MAPPER[test_obj.name],
+                    'status': Feedback.ACCEPTED if test_obj.name == EmptyTest.__name__ else Feedback.EDITABLE,
                 }
             )
 
@@ -262,7 +257,7 @@ def call_loader(func: Callable) -> None:
                 i(f'{func.__name__} has already been processed once. Proceed anyway?', NO):
             return
 
-    func() # This executes the specified loader
+    func()  # This executes the specified loader
 
     with open(RECORDS, 'a') as records_f:
         records_f.write(func.__name__)
@@ -277,7 +272,7 @@ def do_convert_xls():
     if not ans:
         return
 
-    infile  = i('Please provide the path to the .xls file', is_file=True)
+    infile = i('Please provide the path to the .xls file', is_file=True)
     outfile = i('Where should the output go?', 'submissons.json')
 
     json_dict = util.convert.converter(infile)
@@ -287,7 +282,7 @@ def do_convert_xls():
 def do_load_submission_types():
 
     print(
-    '''For the following import you need three files:
+        '''For the following import you need three files:
 
     1) A .csv file where the columns are: id, name, score
     2) A path to a directory where I can find sample solutions named
@@ -316,9 +311,9 @@ def do_load_submission_types():
     path = i('Where are your files located?', '.', is_path=True)
 
     with chdir_context(path):
-        submission_types_csv    = i('CSV file',         'submission_types.csv')
-        lsg_dir                 = i('solution dir',     'code-lsg')
-        desc_dir                = i('descriptions dir', 'html')
+        submission_types_csv = i('CSV file', 'submission_types.csv')
+        lsg_dir = i('solution dir', 'code-lsg')
+        desc_dir = i('descriptions dir', 'html')
 
         with open(submission_types_csv, encoding='utf-8') as tfile:
             csv_rows = [row for row in csv.reader(tfile)]
@@ -328,11 +323,11 @@ def do_load_submission_types():
             with \
                     open(os.path.join(lsg_dir, tid + '-lsg.c'), encoding='utf-8') as lsg,\
                     open(os.path.join(desc_dir, tid + '.html'), encoding='utf-8') as desc:
-                data={
-                    'name'          : name,
-                    'description'   : desc.read(),
-                    'solution'      : lsg.read(),
-                    'full_score'    : int(score),
+                data = {
+                    'name': name,
+                    'description': desc.read(),
+                    'solution': lsg.read(),
+                    'full_score': int(score),
                 }
             _, created = SubmissionType.objects.update_or_create(
                 name=name,
@@ -364,7 +359,7 @@ def do_load_module_descriptions():
 
     for row in csv_rows:
         data = {
-            field : kind(data) for field, kind, data in zip(
+            field: kind(data) for field, kind, data in zip(
                 ('module_reference', 'total_score', 'pass_score', 'pass_only'),
                 (str, int, int, lambda x: x == 'yes'),
                 (col.strip() for col in row)
@@ -418,13 +413,13 @@ def do_load_submissions():
         print()
 
         exam = i('Choose wisely')
-        exam = {'exam' : exam_query_set[int(exam)]}
+        exam = {'exam': exam_query_set[int(exam)]}
 
     with open(file) as submission_file:
         submissions = json.JSONDecoder().decode(submission_file.read())
 
     for username, data in submissions.items():
-        student_obj = add_student(username, **exam, **data)
+        student_obj = make_student(username, **exam, **data)
 
         for submission_obj in data['submissions']:
             add_submission(student_obj, **submission_obj)
@@ -433,7 +428,7 @@ def do_load_submissions():
 def do_load_tutors():
 
     print('Please import tutor users by providing one name per line')
-    tutors    = i('List of tutors', 'tutors', is_file=True)
+    tutors = i('List of tutors', 'tutors', is_file=True)
 
     with open(tutors) as tutors_f:
         add_user_list(tutors_f, TUTORS)
-- 
GitLab