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