diff --git a/backend/core/admin.py b/backend/core/admin.py index 9598b953d8bcb6c9cd0045ee101dbe133c295da7..7de2ec69610f75e0a7ee00849683bac6e5fc3e2f 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -1,85 +1,19 @@ -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 django.contrib.auth.models import Group from core.models import (Feedback, Reviewer, Student, Submission, - SubmissionType, Test, Tutor, UserAccount) + SubmissionType, Test, Tutor, UserAccount, ExamType) - -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 = ('username', '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) +# Stuff we needwant +admin.site.register(UserAccount) admin.site.register(SubmissionType) admin.site.register(Feedback) admin.site.register(Test) +admin.site.register(ExamType) admin.site.register(Submission) admin.site.register(Reviewer) admin.site.register(Student) admin.site.register(Tutor) + +# ... and stuff we don't needwant +admin.site.unregister(Group) diff --git a/backend/core/migrations/0002_auto_20171110_1612.py b/backend/core/migrations/0002_auto_20171110_1612.py new file mode 100644 index 0000000000000000000000000000000000000000..51ea77b9d911e7133d6fd9f8f4d1c6d52d9b90d5 --- /dev/null +++ b/backend/core/migrations/0002_auto_20171110_1612.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-10 16:12 +from __future__ import unicode_literals + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='useraccount', + options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, + ), + migrations.AlterModelManagers( + name='useraccount', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.AddField( + model_name='useraccount', + name='date_joined', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), + ), + migrations.AddField( + model_name='useraccount', + name='email', + field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), + ), + migrations.AddField( + model_name='useraccount', + name='first_name', + field=models.CharField(blank=True, max_length=30, verbose_name='first name'), + ), + migrations.AddField( + model_name='useraccount', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + ), + migrations.AddField( + model_name='useraccount', + name='last_name', + field=models.CharField(blank=True, max_length=30, verbose_name='last name'), + ), + migrations.AddField( + model_name='useraccount', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), + ), + migrations.AlterField( + model_name='useraccount', + name='is_active', + field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'), + ), + migrations.AlterField( + model_name='useraccount', + name='is_staff', + field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'), + ), + migrations.AlterField( + model_name='useraccount', + name='is_superuser', + field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), + ), + migrations.AlterField( + model_name='useraccount', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index 3124992c15475cd0b6cbc939d66780cc26f961c3..32189b7e4f1c9f88c313e528b86583a216112b5b 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -11,7 +11,7 @@ from random import randrange, sample from string import ascii_lowercase from django.contrib.auth import get_user_model -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.contrib.auth.models import AbstractUser from django.db import models from django.db.models import Value as V from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q, @@ -150,64 +150,16 @@ 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): +class UserAccount(AbstractUser): """ 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 has_perm(self, *args): - return self.is_superuser - - def has_module_perms(self, *args): - return self.is_superuser def get_associated_user(self): """ Returns the user type that is associated with this user obj """ @@ -216,12 +168,6 @@ class UserAccount(AbstractBaseUser): (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( @@ -535,10 +481,10 @@ class Feedback(models.Model): ACCEPTED, ) = range(4) # this order matters STATUS = ( - (EDITABLE, 'editable'), - (OPEN, 'request reassignment'), - (NEEDS_REVIEW, 'request review'), - (ACCEPTED, 'accepted'), + (EDITABLE, 'editable'), + (OPEN, 'request reassignment'), + (NEEDS_REVIEW, 'request review'), + (ACCEPTED, 'accepted'), ) status = models.IntegerField( choices=STATUS, @@ -554,11 +500,11 @@ class Feedback(models.Model): MANUAL, ) = range(5) ORIGIN = ( - (WAS_EMPTY, 'was empty'), + (WAS_EMPTY, 'was empty'), (FAILED_UNIT_TESTS, 'passed unittests'), - (DID_NOT_COMPILE, 'did not compile'), - (COULD_NOT_LINK, 'could not link'), - (MANUAL, 'created by a human. yak!'), + (DID_NOT_COMPILE, 'did not compile'), + (COULD_NOT_LINK, 'could not link'), + (MANUAL, 'created by a human. yak!'), ) origin = models.IntegerField( choices=ORIGIN, diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 8178ecfae6f841340d82c7e9afada2bee9dd3c52..a5d7967f6d46e68132d215b3f577abec331c15ab 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -2,7 +2,6 @@ from rest_framework import serializers from core.models import ExamType, Feedback, Student, Submission - class ExamSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/core/views/api.py b/backend/core/views/api.py index 3f015b444b638508b0849bf517d4990ca431478c..d0d45630fc5e653657f3302e5639cda68ed0d096 100644 --- a/backend/core/views/api.py +++ b/backend/core/views/api.py @@ -1,14 +1,18 @@ +import logging + from rest_framework.generics import RetrieveAPIView from core.permissions import IsStudent from core.serializers import (FeedbackSerializer, StudentSerializer, SubmissionSerializer) +log = logging.getLogger(__name__) class StudentApiView(RetrieveAPIView): permission_classes = (IsStudent,) def get_object(self): + log.debug("Serializing student of user '%s'", self.request.user.username) return self.request.user.student serializer_class = StudentSerializer diff --git a/backend/grady/settings/default.py b/backend/grady/settings/default.py index deacf96ff165ddf6a11ac0079f4ab26d0df538bf..86e0c94956f52ceb7b459c8d8e015d73c4825a6c 100644 --- a/backend/grady/settings/default.py +++ b/backend/grady/settings/default.py @@ -159,3 +159,52 @@ REST_FRAMEWORK = { JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=600), } + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + 'django.server': { + 'datefmt': '%d/%b/%Y %H:%M:%S', + 'format': '[%(asctime)s] %(levelname)-10s %(name)-20s %(message)s', + }, + 'core': { + 'datefmt': '%d/%b/%Y %H:%M:%S', + 'format': '[%(asctime)s] %(levelname)-10s %(name)-20s "%(message)s"', + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'core' + }, + 'django': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'django.server' + }, + 'mail_admins': { # TODO: configuration + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + } + }, + 'loggers': { + 'django': { + 'handlers': ['django'], + }, + 'django.request': { + 'handlers': ['django'], + }, + 'core': { + 'handlers': ['console', 'mail_admins'], + 'level': 'DEBUG', + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 0c946c7d11266e7cf0276ac7bb2fd8dfc0c7af10..ec065b746f5b597cf8db46c922d77fd50e1d6584 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,7 @@ "start": "npm run dev", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", - "test": "npm run unit", + "test": "karma start test/unit/karma.conf.js", "lint": "eslint --ext .js,.vue src test/unit/specs" }, "dependencies": { @@ -17,6 +17,7 @@ "bootstrap": "4.0.0-beta.2", "bootstrap-vue": "^1.0.0", "vue": "^2.5.2", + "vue-beautify": "^1.1.3", "vue-router": "^3.0.1", "vuex": "^3.0.1" }, @@ -36,6 +37,7 @@ "copy-webpack-plugin": "^4.0.1", "cross-env": "^5.0.1", "css-loader": "^0.28.0", + "es6-promise": "^4.1.1", "eslint": "^3.19.0", "eslint-config-standard": "^10.2.1", "eslint-friendly-formatter": "^3.0.0", diff --git a/frontend/test/unit/index.js b/frontend/test/unit/index.js index c69f33fd8a0fb2796073222645c42c7c15188a6e..d0ddfd3846b6da1e934c141b7137655ccd6b13b2 100644 --- a/frontend/test/unit/index.js +++ b/frontend/test/unit/index.js @@ -1,4 +1,5 @@ import Vue from 'vue' +import 'es6-promise/auto' Vue.config.productionTip = false diff --git a/frontend/test/unit/karma.conf.js b/frontend/test/unit/karma.conf.js index 8e4951c9e4ecc597be347be1fd8e163cdbab13e2..b77a340f7b3796e08e7f60abee8bded5c26365df 100644 --- a/frontend/test/unit/karma.conf.js +++ b/frontend/test/unit/karma.conf.js @@ -7,10 +7,6 @@ var webpackConfig = require('../../build/webpack.test.conf') module.exports = function (config) { config.set({ - // to run in additional browsers: - // 1. install corresponding karma launcher - // http://karma-runner.github.io/0.13/config/browsers.html - // 2. add it to the `browsers` array below. browsers: ['PhantomJS'], frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], reporters: ['spec', 'coverage'], diff --git a/frontend/test/unit/specs/Hello.spec.js b/frontend/test/unit/specs/Hello.spec.js index bfaa225c95f61e61e5e03bab02e25166f063199d..4f647728ecfde3ee9cc5bfd41ac94d0e507f8b65 100644 --- a/frontend/test/unit/specs/Hello.spec.js +++ b/frontend/test/unit/specs/Hello.spec.js @@ -1,11 +1,16 @@ import Vue from 'vue' -import HelloWorld from '@/components/HelloWorld' +import Login from '@/components/Login' -describe('HelloWorld.vue', () => { - it('should render correct contents', () => { - const Constructor = Vue.extend(HelloWorld) - const vm = new Constructor().$mount() - expect(vm.$el.querySelector('.hello h1').textContent) - .to.equal('Welcome to Your Vue.js App') +describe('Login.vue', () => { + it('is doing nothing but ensure that we can test', () => { + data = {"submissions": [ + { + "type": "Aufgabe 01", + "text": "Hallo, was geht denn da?", + "feedback": null, + "score": null, + "full_score": 12 + } + ]} }) }) diff --git a/frontend/test/unit/specs/Login.spec.js b/frontend/test/unit/specs/Login.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..127bc590abbd8db0880c9bde946570d041c4011a --- /dev/null +++ b/frontend/test/unit/specs/Login.spec.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import Login from '@/components/Login' + +describe('Login.vue', () => { + it('is doing nothing but ensure that we can test', () => { + + }) +}) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9fa697bc8f22d8a02a6f90c1143b762e91d1b02a..d417ec11d84acfaefbada048d993b002dae8a9c2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1388,6 +1388,13 @@ concat-stream@1.6.0, concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +config-chain@~1.1.5: + version "1.1.11" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + connect-history-api-fallback@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.4.0.tgz#3db24f973f4b923b0e82f619ce0df02411ca623d" @@ -2016,7 +2023,7 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise@^4.0.3: +es6-promise@^4.0.3, es6-promise@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" @@ -3039,7 +3046,7 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" -ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -4039,7 +4046,7 @@ node-pre-gyp@^0.6.36: tar "^2.2.1" tar-pack "^3.4.0" -nopt@3.x: +nopt@3.x, nopt@~3.0.1: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" dependencies: @@ -4770,6 +4777,10 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + proxy-addr@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" @@ -5912,6 +5923,14 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" +vue-beautify@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/vue-beautify/-/vue-beautify-1.1.3.tgz#69d193b641db0047e26378858bb759754ee53692" + dependencies: + config-chain "~1.1.5" + mkdirp "~0.5.0" + nopt "~3.0.1" + vue-functional-data-merge@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-1.0.6.tgz#cde4f0cf3f251f7f0196341156d2c936cca63d40"