diff --git a/server/api/schema.py b/server/api/schema.py index 54e08f6f..5e5f0d12 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -22,6 +22,7 @@ from rooms.mutations import RoomMutations from rooms.schema import RoomsQuery, ModuleRoomsQuery from users.schema import AllUsersQuery, UsersQuery from users.mutations import ProfileMutations +from registration.mutations_public import RegistrationMutations class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, @@ -34,7 +35,7 @@ class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQ class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, - ProfileMutations, SurveyMutations, NoteMutations, graphene.ObjectType): + ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, graphene.ObjectType): if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='__debug') diff --git a/server/core/management/commands/import_users.py b/server/core/management/commands/import_users.py index 4d3576ac..12ba6972 100644 --- a/server/core/management/commands/import_users.py +++ b/server/core/management/commands/import_users.py @@ -30,11 +30,7 @@ class Command(BaseCommand): self.stdout.write("Creating user {} {}, {}".format(first_name, last_name, email)) - user, created = User.objects.get_or_create(email=email, username=email) - user.first_name = first_name - user.last_name = last_name - user.set_password(User.objects.make_random_password()) - user.save() + user = User.objects.create_user_with_random_password(first_name, last_name, email) if row['Rolle'] == 'Lehrer': self.stdout.write("Assigning teacher role") diff --git a/server/core/settings.py b/server/core/settings.py index d6982527..5ade4f9a 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ 'statistics', 'surveys', 'notes', + 'registration', 'wagtail.contrib.forms', 'wagtail.contrib.redirects', diff --git a/server/registration/__init__.py b/server/registration/__init__.py new file mode 100644 index 00000000..49c9923c --- /dev/null +++ b/server/registration/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu diff --git a/server/registration/apps.py b/server/registration/apps.py new file mode 100644 index 00000000..a9b4ab38 --- /dev/null +++ b/server/registration/apps.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +from django.apps import AppConfig + + +class UserConfig(AppConfig): + name = 'registration' + diff --git a/server/registration/factories.py b/server/registration/factories.py new file mode 100644 index 00000000..50c493de --- /dev/null +++ b/server/registration/factories.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +import random + +import factory + +from registration.models import LicenseType, License + + +class LicenseTypeFactory(factory.django.DjangoModelFactory): + class Meta: + model = LicenseType + + name = factory.Sequence(lambda n: 'license-{}'.format(n)) + active = True + key = factory.Sequence(lambda n: "license-key-%03d" % n) + description = factory.Sequence(lambda n: "Some description %03d" % n) + + +class LicenseFactory(factory.django.DjangoModelFactory): + class Meta: + model = License + diff --git a/server/registration/migrations/0001_initial.py b/server/registration/migrations/0001_initial.py new file mode 100644 index 00000000..7fd0d43a --- /dev/null +++ b/server/registration/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 2.0.6 on 2019-10-09 09:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0009_auto_20191009_0905'), + ] + + operations = [ + migrations.CreateModel( + name='License', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='LicenseType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='License name')), + ('key', models.CharField(max_length=128)), + ('active', models.BooleanField(default=False, verbose_name='License active')), + ('description', models.TextField(default='', verbose_name='Description')), + ('for_role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Role')), + ], + ), + migrations.AddField( + model_name='license', + name='license_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registration.LicenseType'), + ), + migrations.AddField( + model_name='license', + name='licensee', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/server/registration/migrations/__init__.py b/server/registration/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/registration/models.py b/server/registration/models.py new file mode 100644 index 00000000..36c833bd --- /dev/null +++ b/server/registration/models.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +from django.utils.translation import ugettext_lazy as _ +from django.db import models + +from users.managers import RoleManager +from users.models import Role, User + + +class LicenseType(models.Model): + + name = models.CharField(_('License name'), max_length=255, blank=False, null=False) + for_role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE) + key = models.CharField(max_length=128, blank=False, null=False) + active = models.BooleanField(_('License active'), default=False) + description = models.TextField(_('Description'), default="") + + def is_teacher_license(self): + return self.for_role.key == RoleManager.TEACHER_KEY + + +class License(models.Model): + license_type = models.ForeignKey(LicenseType, blank=False, null=False, on_delete=models.CASCADE) + licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE) diff --git a/server/registration/mutations_public.py b/server/registration/mutations_public.py new file mode 100644 index 00000000..5616deb9 --- /dev/null +++ b/server/registration/mutations_public.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +import graphene +from django.conf import settings +from graphene import relay + +from registration.models import License +from registration.serializers import RegistrationSerializer +from users.models import User, Role, UserRole +from users.mutations import UpdateError, FieldError + + +class Registration(relay.ClientIDMutation): + class Input: + firstname_input = graphene.String() + lastname_input = graphene.String() + email_input = graphene.String() + license_key_input = graphene.String() + + success = graphene.Boolean() + errors = graphene.List(UpdateError) # todo: change for consistency + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + first_name = kwargs.get('firstname_input') + last_name = kwargs.get('lastname_input') + email = kwargs.get('email_input') + license_key = kwargs.get('license_key_input') + registration_data = { + 'first_name': first_name, + 'last_name': last_name, + 'email': email, + 'license_key': license_key, + } + + serializer = RegistrationSerializer(data=registration_data) + + if serializer.is_valid(): + user = User.objects.create_user_with_random_password(serializer.data['first_name'], + serializer.data['last_name'], + serializer.data['email']) + sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type']) + + if sb_license.license_type.is_teacher_license(): + teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY) + UserRole.objects.get_or_create(user=user, role=teacher_role) + # create class + else: + student_role = Role.objects.get(key=Role.objects.STUDENT_KEY) + UserRole.objects.get_or_create(user=user, role=student_role) + + return cls(success=True) + + errors = [] + for key, value in serializer.errors.items(): + error = UpdateError(field=key, errors=[]) + for field_error in serializer.errors[key]: + error.errors.append(FieldError(code=field_error.code)) + + errors.append(error) + + return cls(success=False, errors=errors) + + +class RegistrationMutations: + registration = Registration.Field() diff --git a/server/registration/serializers.py b/server/registration/serializers.py new file mode 100644 index 00000000..887fbbe1 --- /dev/null +++ b/server/registration/serializers.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +from django.contrib.auth import get_user_model +from rest_framework import serializers +from rest_framework.fields import CharField, EmailField +from django.utils.translation import ugettext_lazy as _ +from registration.models import License, LicenseType + + +class RegistrationSerializer(serializers.Serializer): + first_name = CharField(allow_blank=False) + last_name = CharField(allow_blank=False) + email = EmailField(allow_blank=False) + license_key = CharField(allow_blank=False) + skillbox_license = None + + def validate_email(self, value): + lower_email = value.lower() + # the email is used as username + if len(get_user_model().objects.filter(username=lower_email)) > 0: + raise serializers.ValidationError(_(u'Diese E-Mail ist bereits registriert')) + elif len(get_user_model().objects.filter(email=lower_email)) > 0: + raise serializers.ValidationError(_(u'Dieser E-Mail ist bereits registriert')) + else: + return lower_email + + def validate_license_key(self, value): + license_types = LicenseType.objects.filter(key=value, active=True) + if len(license_types) == 0: + raise serializers.ValidationError(_(u'Die Lizenznummer ist ungültig')) + + self.context['license_type'] = license_types[0] # Assuming there is just ONE license per key + return value diff --git a/server/registration/tests/__init__.py b/server/registration/tests/__init__.py new file mode 100644 index 00000000..779000b2 --- /dev/null +++ b/server/registration/tests/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +from django.conf import settings diff --git a/server/registration/tests/test_registration.py b/server/registration/tests/test_registration.py new file mode 100644 index 00000000..0da5dc36 --- /dev/null +++ b/server/registration/tests/test_registration.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-10-08 +# @author: chrigu +from django.contrib.sessions.middleware import SessionMiddleware +from django.test import TestCase, RequestFactory +from graphene.test import Client + +from api.schema import schema +from registration.factories import LicenseTypeFactory, LicenseFactory +from registration.models import License +from users.managers import RoleManager +from users.models import Role, User, UserRole + + +class PasswordResetTests(TestCase): + def setUp(self): + + self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role") + self.student_role = Role.objects.create(key=Role.objects.STUDENT_KEY, name="Student Role") + + self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role) + self.student_license_type = LicenseTypeFactory(for_role=self.student_role) + + self.teacher_license = LicenseFactory(license_type=self.teacher_license_type) + self.student_license = LicenseFactory(license_type=self.student_license_type) + + request = RequestFactory().post('/') + + self.email = 'sepp@skillbox.iterativ.ch' + self.first_name = 'Sepp' + self.last_name = 'Feuz' + + # adding session + middleware = SessionMiddleware() + middleware.process_request(request) + request.session.save() + self.client = Client(schema=schema, context_value=request) + + def make_register_mutation(self, first_name, last_name, email, license_key): + mutation = ''' + mutation Registration($input: RegistrationInput!){ + registration(input: $input) { + success + errors { + field + } + } + } + ''' + + return self.client.execute(mutation, variables={ + 'input': { + 'firstnameInput': first_name, + 'lastnameInput': last_name, + 'emailInput': email, + 'licenseKeyInput': license_key, + } + }) + + def _assert_user_registration(self, count, email, role_key): + users = User.objects.filter(username=self.email) + self.assertEqual(len(users), count) + user_roles = UserRole.objects.filter(user__email=email, role__key=role_key) + self.assertEqual(len(user_roles), count) + licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key) + self.assertEqual(len(licenses), count) + + def test_user_can_register_as_teacher(self): + self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY) + result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key) + self.assertTrue(result.get('data').get('registration').get('success')) + self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY) + + def test_user_can_register_as_student(self): + self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + self.assertTrue(result.get('data').get('registration').get('success')) + self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY) + + def test_existing_user_cannot_register(self): + self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + + def test_existing_user_cannot_register_with_uppercase_email(self): + self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY) + self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key) + result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key) + self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + + def test_user_cannot_register_if_firstname_is_missing(self): + result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key) + self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name') + self.assertFalse(result.get('data').get('registration').get('success')) + + def test_user_cannot_register_if_lastname_is_missing(self): + result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key) + self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name') + self.assertFalse(result.get('data').get('registration').get('success')) + + def test_user_cannot_register_if_email_is_missing(self): + result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key) + self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email') + self.assertFalse(result.get('data').get('registration').get('success')) diff --git a/server/users/managers.py b/server/users/managers.py index 8bd35110..6a36884e 100644 --- a/server/users/managers.py +++ b/server/users/managers.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from django.db import models +from django.contrib.auth.models import UserManager as DjangoUserManager class RoleManager(models.Manager): @@ -78,3 +79,14 @@ class UserRoleManager(models.Manager): user_role = self.model(user=user, role=role) user_role.save() return user_role + + +class UserManager(DjangoUserManager): + def create_user_with_random_password(self, first_name, last_name, email): + user, created = self.model.objects.get_or_create(email=email, username=email) + user.first_name = first_name + user.last_name = last_name + user.set_password(self.model.objects.make_random_password()) + user.save() + return user + diff --git a/server/users/migrations/0009_auto_20191009_0905.py b/server/users/migrations/0009_auto_20191009_0905.py new file mode 100644 index 00000000..e9b54b1a --- /dev/null +++ b/server/users/migrations/0009_auto_20191009_0905.py @@ -0,0 +1,20 @@ +# Generated by Django 2.0.6 on 2019-10-09 09:05 + +from django.db import migrations +import users.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_auto_20190904_1410'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', users.managers.UserManager()), + ], + ), + ] diff --git a/server/users/models.py b/server/users/models.py index c367e5c2..e6e93eca 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -4,7 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.translation import ugettext_lazy as _ -from users.managers import RoleManager, UserRoleManager +from users.managers import RoleManager, UserRoleManager, UserManager DEFAULT_SCHOOL_ID = 1 @@ -14,6 +14,8 @@ class User(AbstractUser): avatar_url = models.CharField(max_length=254, blank=True, default='') email = models.EmailField(_('email address'), unique=True) + objects = UserManager() + def get_role_permissions(self): perms = set() for role in Role.objects.get_roles_for_user(self): diff --git a/server/users/serializers.py b/server/users/serializers.py index ad05594c..de6037ee 100644 --- a/server/users/serializers.py +++ b/server/users/serializers.py @@ -38,7 +38,7 @@ def validate_old_new_password(value): return value -def validate_strong_email(password): +def validate_strong_password(password): has_number = re.search('\d', password) has_upper = re.search('[A-Z]', password) @@ -56,7 +56,7 @@ class PasswordSerialzer(serializers.Serializer): new_password = CharField(allow_blank=True, min_length=MIN_PASSWORD_LENGTH) def validate_new_password(self, value): - return validate_strong_email(value) + return validate_strong_password(value) def validate_old_password(self, value): return validate_old_password(value, self.context.username)