Add registration backend

This commit is contained in:
Christian Cueni 2019-10-09 11:12:42 +02:00
parent 9462665826
commit 0b48607398
17 changed files with 405 additions and 9 deletions

View File

@ -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')

View File

@ -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")

View File

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'statistics',
'surveys',
'notes',
'registration',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',

View File

@ -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 <christian.cueni@iterativ.ch>

View File

@ -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 <christian.cueni@iterativ.ch>
from django.apps import AppConfig
class UserConfig(AppConfig):
name = 'registration'

View File

@ -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 <christian.cueni@iterativ.ch>
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

View File

@ -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),
),
]

View File

@ -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 <christian.cueni@iterativ.ch>
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)

View File

@ -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 <christian.cueni@iterativ.ch>
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()

View File

@ -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 <christian.cueni@iterativ.ch>
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

View File

@ -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 <christian.cueni@iterativ.ch>
from django.conf import settings

View File

@ -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 <christian.cueni@iterativ.ch>
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'))

View File

@ -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

View File

@ -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()),
],
),
]

View File

@ -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):

View File

@ -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)