Merged in feature/registration (pull request #39)

Feature/registration
This commit is contained in:
Christian Cueni 2019-11-11 18:59:56 +00:00
commit 5f4aefa722
60 changed files with 1264 additions and 194 deletions

2
.gitignore vendored
View File

@ -43,5 +43,3 @@ server/media/
.coverage
# Cypress screenshots
client/cypress/screenshots

View File

@ -30,6 +30,9 @@ aliases:
caches:
- pip
- node
artifacts:
- client/cypress/**/*.png
- client/cypress/**/*.mp4
services:
- postgres
script:

4
client/.gitignore vendored
View File

@ -13,3 +13,7 @@ cypress/videos
*.ntvs*
*.njsproj
*.sln
# Cypress
cypress/screenshots
cypress/videos

View File

@ -1,4 +1,4 @@
{
"baseUrl": "http://localhost:8000",
"video": false
"videoUploadOnPasses": false
}

View File

@ -1,94 +0,0 @@
describe('The Login Page', () => {
it('login and redirect to main page', () => {
const username = 'test';
const password = 'test';
cy.visit('/');
cy.login(username, password, true);
cy.get('body').contains('Neues Wissen erwerben');
});
it('user sees error message if username is omitted', () => {
const username = '';
const password = 'test';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=email-local-errors]').contains('ist ein Pflichtfeld');
});
it('user sees error message if password is omitted', () => {
const username = 'test';
const password = '';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=password-local-errors]').contains('ist ein Pflichtfeld');
});
it('user sees error message if credentials are invalid', () => {
const username = 'test';
const password = '12345';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
});
it('redirect after login', () => {
const username = 'test';
const password = 'test';
cy.visit('/book/topic/berufliche-grundbildung');
cy.login(username, password);
cy.get('body').contains('Berufliche Grundbildung');
});
// it('logs in programmatically without using the UI', () => {
// cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first
//
// const username = 'test';
// const password = 'test';
//
// cy.getCookie('csrftoken').then(token => {
// const options = {
// url: '/accounts/login/',
// method: 'POST',
// headers: {
// 'X-CSRFToken': token.value,
// 'content-type': 'multipart/form-data'
// },
// from: true,
// body: {
// username: username,
// password: password,
// // csrfmiddlewaretoken: token.value
// }
// };
//
// cy.request(options);
// cy.getCookie('sessionid').should('exist');
// cy.visit('/');
//
// cy.get('.start-page__title').should('contain', 'skillbox')
//
//
// // cy.visit('/');
// // cy.getCookie('csrftoken')
// // .then((csrftoken) => {
// // const response = cy.request({
// // method: 'POST',
// // url: '/login/',
// // form: true,
// // body: {
// // identification: username,
// // password: password,
// // csrfmiddlewaretoken: csrftoken.value
// // }
// // });
// // });
//
// });
// })
})

View File

@ -0,0 +1,47 @@
describe('The Login Page', () => {
it('login and redirect to main page', () => {
const username = 'test';
const password = 'test';
cy.visit('/');
cy.login(username, password, true);
cy.get('body').contains('Neues Wissen erwerben');
});
it('user sees error message if username is omitted', () => {
const username = '';
const password = 'test';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=email-local-errors]').contains('E-Mail ist ein Pflichtfeld');
});
it('user sees error message if password is omitted', () => {
const username = 'test';
const password = '';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=password-local-errors]').contains('Passwort ist ein Pflichtfeld');
});
it('user sees error message if credentials are invalid', () => {
const username = 'test';
const password = '12345';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
});
it('redirect after login', () => {
const username = 'test';
const password = 'test';
cy.visit('/book/topic/berufliche-grundbildung');
cy.login(username, password);
cy.get('body').contains('Berufliche Grundbildung');
});
})

View File

@ -0,0 +1,77 @@
describe('The Regstration Page', () => {
// works locally, but not in pipelines.
// it('register user', () => {
// let timestamp = Math.round((new Date()).getTime() / 1000);
// const firstname = 'pesche';
// const lastname = 'peschemann';
// const email = `skillboxtest${timestamp}@iterativ.ch`;
// const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
// cy.visit('/register');
// cy.register(firstname, lastname, email, licenseKey);
// cy.get('.reset__heading').contains('Schauen Sie in Ihr Postfach');
// });
it('user sees error message if firstname is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = '';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="firstname-local-errors"]').contains('Vorname ist ein Pflichtfeld.');
});
it('user sees error message if lastname is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = '';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="lastname-local-errors"]').contains('Nachname ist ein Pflichtfeld.');
});
it('user sees error message if email is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = ``;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="email-local-errors"]').contains('E-Mail ist ein Pflichtfeld.');
});
it('user sees error message if license is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = '';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="licenseKey-local-errors"]').contains('Lizenz ist ein Pflichtfeld.');
});
it('user sees error message if license key is wrong', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'asdsafsadfsadfasdf';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="licenseKey-remote-errors"]').contains('Die angegebenen Lizenz ist unglültig');
});
})

View File

@ -82,3 +82,23 @@ Cypress.Commands.add('changePassword', (oldPassword, newPassword) => {
}
cy.get('[data-cy=change-password-button]').click();
});
Cypress.Commands.add('register', (firstname, lastname, email, licenseKey) => {
if (firstname != '') {
cy.get('[data-cy=firstname-input]').type(firstname);
}
if (lastname != '') {
cy.get('[data-cy=lastname-input]').type(lastname);
}
if (email != '') {
cy.get('[data-cy=email-input]').type(email);
}
if (licenseKey != '') {
cy.get('[data-cy=licenseKey-input]').type(licenseKey);
}
cy.get('[data-cy=register-button]').click();
});

View File

@ -8985,8 +8985,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -9004,13 +9003,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9023,18 +9020,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@ -9137,8 +9131,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -9148,7 +9141,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9161,20 +9153,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -9191,7 +9180,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9264,8 +9252,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -9275,7 +9262,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9351,8 +9337,7 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9382,7 +9367,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9400,7 +9384,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9439,13 +9422,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@ -11449,8 +11430,7 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
@ -11475,8 +11455,7 @@
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",

View File

@ -11,6 +11,7 @@
:class="{ 'skillboxform-input__input--error': errors.has('oldPassword') }"
class="change-form__old skillbox-input skillboxform-input__input"
autocomplete="off"
data-vv-as="Altes Passwort"
data-cy="old-password">
<small v-if="errors.has('oldPassword') && submitted" class="skillboxform-input__error" data-cy="old-password-local-errors">{{ errors.first('oldPassword') }}</small>
<small v-for="error in oldPasswordErrors" :key="error" class=" skillboxform-input__error" data-cy="old-password-remote-errors">{{ error }}</small>
@ -21,6 +22,7 @@
name="newPassword"
type="text"
v-model="newPassword"
data-vv-as="Neues Passwort"
v-validate="'required|min:8|strongPassword'"
:class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
class="change-form__new skillbox-input skillboxform-input__input"

View File

@ -0,0 +1,8 @@
mutation Registration($input: RegistrationInput!){
registration(input: $input) {
success
errors {
field
}
}
}

View File

@ -16,6 +16,7 @@ import veeDe from 'vee-validate/dist/locale/de';
import {dateFilter} from './filters/date-filter';
import autoGrow from '@/directives/auto-grow'
import clickOutside from '@/directives/click-outside'
import ME_QUERY from '@/graphql/gql/meQuery.gql';
Vue.config.productionTip = false;
@ -98,13 +99,22 @@ Validator.extend('strongPassword', {
return strongRegex.test(value);
}
});
Validator.extend('email', {
getMessage: field => 'Bitte geben Sie eine gülitge E-Mail an',
validate: value => {
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
return emailRegex.test(value);
}
});
Vue.use(VeeValidate, {
locale: 'de'
});
Vue.filter('date', dateFilter);
/* logged in guard */
/* guards */
function getCookieValue(cookieName) {
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
@ -112,18 +122,32 @@ function getCookieValue(cookieName) {
return cookieValue ? cookieValue.pop() : '';
}
function redirectIfLoginRequird(to) {
function loginRequired(to) {
// public pages have the meta.public property set to true
return (!to.hasOwnProperty('meta') || !to.meta.hasOwnProperty('public') || !to.meta.public) && getCookieValue('loginStatus') !== 'true';
return !to.hasOwnProperty('meta') || !to.meta.hasOwnProperty('public') || !to.meta.public;
}
router.beforeEach((to, from, next) => {
if (redirectIfLoginRequird(to)) {
function unauthorizedAccess(to) {
return loginRequired(to) && getCookieValue('loginStatus') !== 'true';
}
function redirectStudentsWithoutClass() {
return privateApolloClient.query({
query: ME_QUERY,
}).then(({data}) => data.me.schoolClasses.edges.length === 0 && data.me.permissions.length === 0);
}
router.beforeEach(async (to, from, next) => {
if (unauthorizedAccess(to)) {
const redirectUrl = `/login?redirect=${to.path}`;
next(redirectUrl);
} else {
next();
}
if (to.name !== 'noClass' && loginRequired(to) && await redirectStudentsWithoutClass()) {
router.push({name: 'noClass'})
}
next();
});
/* eslint-disable no-new */

View File

@ -1,6 +1,6 @@
<template>
<div class="login">
<h1 class="login__title">Melden Sie sich jetzt an</h1>
<div class="login public-page">
<h1 class="login__title public-page__title">Melden Sie sich jetzt an</h1>
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="login-form__field skillboxform-input">
<label for="email" class="skillboxform-input__label">E-Mail</label>
@ -10,6 +10,7 @@
type="text"
v-model="email"
v-validate="'required'"
data-vv-as="E-Mail"
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
@ -33,6 +34,7 @@
id="pw"
name="password"
type="password"
data-vv-as="Passwort"
v-model="password"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('password') }"
@ -59,10 +61,11 @@
<button class="button button--primary button--big actions__submit" data-cy="login-button">Anmelden</button>
<a class="actions__reset text-link" href="/accounts/password_reset/">Passwort vergessen?</a>
</div>
<!--div class="registration">
<div class="registration">
<p class="registration__text">Haben Sie noch kein Konto?</p>
<a class="registration__link text-link" href="/accounts/password_reset/">Jetzt registrieren</a>
</div-->
<router-link class="registration__link text-link" :to="{name: 'registration'}">Jetzt registrieren
</router-link>
</div>
</form>
</div>
</template>
@ -92,16 +95,24 @@ export default {
store,
{
data: {
login: { success }
login
}
}
) {
try {
if (success) {
if (login.success) {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
that.$router.push(redirectUrl);
} else {
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
const firstError = login.errors[0];
switch (firstError.field) {
case 'invalid_credentials':
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
break;
case 'license_inactive':
that.loginError = 'Ihre Lizenz ist nicht mehr aktiv.';
break;
}
}
} catch (e) {
console.warn(e);
@ -137,15 +148,6 @@ export default {
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.login {
&__title {
margin-top: 48px;
font-size: 2.75rem; // 44px
margin-bottom: 24px;
font-weight: 600;
}
}
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;

View File

@ -0,0 +1,228 @@
<template>
<div class="registration public-page">
<h1 class="registration__title public-page__title">Registrieren Sie ihr persönliches Konto.</h1>
<form class="registration__form registration-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="registration-form__field skillboxform-input">
<label for="firstname" class="skillboxform-input__label">Vorname</label>
<input
id="firstname"
name="firstname"
type="text"
v-model="firstname"
data-vv-as="Vorname"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('firstname') }"
class="change-form__firstname skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="firstname-input"
/>
<small
v-if="errors.has('firstname') && submitted"
class="skillboxform-input__error"
data-cy="firstname-local-errors"
>{{ errors.first('firstname') }}</small>
<small
v-for="error in firstnameErrors"
:key="error"
class="skillboxform-input__error"
data-cy="firstname-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="lastname" class="skillboxform-input__label">Nachname</label>
<input
id="lastname"
name="lastname"
type="text"
v-model="lastname"
data-vv-as="Nachname"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('lastname') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="lastname-input"
/>
<small
v-if="errors.has('lastname') && submitted"
class="skillboxform-input__error"
data-cy="lastname-local-errors"
>{{ errors.first('lastname') }}</small>
<small
v-for="error in lastnameErrors"
:key="error"
class="skillboxform-input__error"
data-cy="lastname-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="email" class="skillboxform-input__label">E-Mail</label>
<input
id="email"
name="email"
type="text"
v-model="email"
data-vv-as="E-Mail"
v-validate="'required|email'"
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="email-input"
/>
<small
v-if="errors.has('email') && submitted"
class="skillboxform-input__error"
data-cy="email-local-errors"
>{{ errors.first('email') }}</small>
<small
v-for="error in emailErrors"
:key="error"
class="skillboxform-input__error"
data-cy="email-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="licenseKey" class="skillboxform-input__label">Lizenz</label>
<input
id="licenseKey"
name="licenseKey"
type="text"
v-model="licenseKey"
data-vv-as="Lizenz"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('licenseKey') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="licenseKey-input"
/>
<small
v-if="errors.has('licenseKey') && submitted"
class="skillboxform-input__error"
data-cy="licenseKey-local-errors"
>{{ errors.first('licenseKey') }}</small>
<small
v-for="error in licenseKeyErrors"
:key="error"
class="skillboxform-input__error"
data-cy="licenseKey-remote-errors"
>{{ error }}</small>
</div>
<div class="skillboxform-input">
<small class="skillboxform-input__error" data-cy="registration-error" v-if="registrationError">{{registrationError}}</small>
</div>
<div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="register-button">Jetzt registration</button>
</div>
</form>
</div>
</template>
<script>
import REGISTRATION_MUTATION from '@/graphql/gql/mutations/registration.gql';
export default {
components: {},
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
let that = this;
if (result) {
this.$apollo.mutate({
client: 'publicClient',
mutation: REGISTRATION_MUTATION,
variables: {
input: {
firstnameInput: this.firstname,
lastnameInput: this.lastname,
emailInput: this.email,
licenseKeyInput: this.licenseKey,
}
},
update(
store,
{
data: {
registration: { success, errors }
}
}
) {
try {
if (success) {
window.location.href = '/registration/set-password/done/';
} else {
errors.forEach(function(error) {
switch (error.field) {
case 'email':
that.emailErrors = ['Die angegebene E-Mail ist bereits registriert.'];
break;
case 'license_key':
that.licenseKeyErrors = ['Die angegebenen Lizenz ist unglültig'];
}
})
}
} catch (e) {
console.warn(e);
that.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
}
}
});
}
});
},
resetForm() {
this.email = '';
this.lastname = '';
this.firstname = '';
this.licenseKey = '';
this.firstnameErrors = '';
this.lastnameErrors = '';
this.emailErrors = '';
this.licenseKeyErrors = '';
this.registrationError = '';
this.submitted = false;
this.$validator.reset();
}
},
data() {
return {
email: '',
lastname: '',
firstname: '',
licenseKey: '',
firstnameErrors: '',
lastnameErrors: '',
emailErrors: '',
licenseKeyErrors: '',
registrationError: '',
submitted: false
};
}
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
.registration {
&__text {
font-family: $sans-serif-font-family;
margin-bottom: $small-spacing;
}
}
</style>

View File

@ -0,0 +1,6 @@
<template>
<div class="no-class public-page">
<h1 class="public-page__title">Sie sind keiner Klasse zugeteilt</h1>
<p>Sie können mySkillbox nur verwenden wenn Sie in einer Klasse zugeteilt sind. Aktuell kann Sie nur der mySkillbox-Support einer Klasse zuteilen.</p>
</div>
</template>

View File

@ -28,6 +28,8 @@ import surveyPage from '@/pages/survey'
import styleGuidePage from '@/pages/styleguide'
import moduleRoom from '@/pages/moduleRoom'
import login from '@/pages/login'
import registration from '@/pages/registration'
import waitForClass from '@/pages/waitForClass'
import store from '@/store/index';
@ -117,6 +119,21 @@ const routes = [
props: true,
meta: {layout: 'simple'}
},
{
path: '/register',
component: registration,
name: 'registration',
meta: {
public: true,
layout: 'public',
}
},
{
path: '/no-class',
component: waitForClass,
name: 'noClass',
meta: {layout: 'public'}
},
{path: '/styleguide', component: styleGuidePage},
{path: '*', component: p404}
];

View File

@ -0,0 +1,8 @@
.public-page {
&__title {
margin-top: 48px;
font-size: 2.75rem; // 44px
margin-bottom: 24px;
font-weight: 600;
}
}

View File

@ -19,3 +19,4 @@
@import "visibility";
@import "solutions";
@import "password_forms";
@import "public-page";

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

@ -3,9 +3,10 @@ from django.conf import settings
from graphene_django.debug import DjangoDebug
from users.mutations_public import UserMutations
from registration.mutations_public import RegistrationMutations
class Mutation(UserMutations, graphene.ObjectType):
class Mutation(UserMutations, RegistrationMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -9,7 +9,6 @@ from django.core.management import BaseCommand
from django.db import connection
from wagtail.core.models import Page
from assignments.factories import AssignmentFactory
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
from core.factories import UserFactory
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
@ -103,5 +102,10 @@ class Command(BaseCommand):
# ContentBlockFactory.create(parent=chapter, **self.filter_data(content_block_data, 'contents'))
ContentBlockFactory.create(parent=chapter, module=module, **content_block_data)
# now create all and rooms
management.call_command('dummy_rooms', verbosity=0)
# create license
management.call_command('create_dummy_license', verbosity=0)

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

@ -25,6 +25,8 @@ $base-font-size: 15px;
$space: 10px;
$color-brand: #17A887;
body {
font-family: $font-family;
font-size: $base-font-size;
@ -115,6 +117,15 @@ input[type=text], input[type=password], input[type=email], select {
.reset__text {
margin-bottom: 52px;
line-height: 1.5rem;
font-size: 1.125rem;
a {
color: $color-brand;
font-family: 'Montserrat', Arial, sans-serif;
font-size: 18px;
font-weight: 300;
}
}
.reset__form label {

View File

@ -7,7 +7,7 @@
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2>
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<p class="reset__text">{% trans 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten' %}</p>
<form method="post" class="mt-1 reset__form">
{% csrf_token %}
{{ form.as_p }}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Sie haben es geschafft' %}{% endblock %}
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
<p class="reset__text">{% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
<!-- templates/registration/password_reset_confirm.html -->
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<form method="post" class="mt-1 reset__form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" name="action">{% trans 'Passwort speichern' %}</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
<!-- templates/registration/password_reset_form.html -->
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Schauen Sie in Ihr Postfach' %}{% endblock %}
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
<p class="reset__text">{% trans 'Wir haben ein E-Mail mit allen weiteren Anweisungen an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf mySkillbox initial zu setzen.{% endblocktrans %}
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'set_password_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Ihr Benutzername lautet:" %} {{ user.get_username }}
{% trans "Ihr mySkillbox Team" %}
{% endautoescape %}

View File

@ -0,0 +1,21 @@
<!-- templates/registration/password_reset_form.html -->
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Willkommen bei mySkillbox' %}{% endblock %}
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Willkommen bei Myskillbox' %}</h2>
<p class="reset__text">{% trans 'Bevor Sie mySkillbox verwenden können, müssen Sie Ihre E-Mail-Adresse bestätigen und ein persönliches Passwort festlegen.' %}</p>
<form method="post" class="mt-1 reset__form">
{% csrf_token %}
<div>
<label for="id_email">{% trans 'Geben Sie als erstes hier Ihre E-Mail-Adresse ein:' %}</label>
{{ form.email }}
</div>
<button class="btn mt-1" type="submit" name="action">{% trans 'E-Mail bestätigen' %}</button>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1 @@
Myskillbox: E-Mail bestätigen und Passwort setzen

View File

@ -6,7 +6,7 @@
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
<p class="reset__text">% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
<p class="reset__text">{% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
</div>
{% endblock %}

View File

@ -7,7 +7,7 @@
{% block body %}
<div class="reset">
<h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<p class="reset__text">{% trans 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten.' %}</p>
<form method="post" class="mt-1 reset__form">
{% csrf_token %}
{{ form.as_p }}

View File

@ -1,5 +1,5 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf {{ site_name }} initial zu setzen.{% endblocktrans %}
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf mySkillbox initial zu setzen.{% endblocktrans %}
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
{% block reset_link %}

View File

@ -1,9 +1,6 @@
import json
from django.test import TestCase, Client
from django.test.utils import override_settings
from core import settings
from core.factories import UserFactory

View File

@ -8,7 +8,8 @@ from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from core import views
from core.views import SetPasswordView, SetPasswordDoneView, SetPasswordConfirmView, SetPasswordCompleteView
from core.views import LegacySetPasswordView, LegacySetPasswordDoneView, LegacySetPasswordConfirmView,\
LegacySetPasswordCompleteView, SetPasswordView, SetPasswordDoneView, SetPasswordConfirmView, SetPasswordCompleteView
urlpatterns = [
# django admin
@ -16,11 +17,20 @@ urlpatterns = [
url(r'^accounts/', include('django.contrib.auth.urls')),
url(r'^statistics/', include('statistics.urls', namespace='statistics')),
# legacy - will be removed
# set password
path('welcome/', SetPasswordView.as_view(), name='set_password'),
path('set-password/done/', SetPasswordDoneView.as_view(), name='set_password_done'),
path('set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(), name='set_password_confirm'),
path('set-password/complete/', SetPasswordCompleteView.as_view(), name='set_password_complete'),
path('welcome/', LegacySetPasswordView.as_view(), name='set_password'),
path('set-password/done/', LegacySetPasswordDoneView.as_view(), name='set_password_done'),
path('set-password/<uidb64>/<token>/', LegacySetPasswordConfirmView.as_view(), name='set_password_confirm'),
path('set-password/complete/', LegacySetPasswordCompleteView.as_view(), name='set_password_complete'),
# set password upon registration
path('registration/welcome/', SetPasswordView.as_view(), name='registration_set_password'),
path('registration/set-password/done/', SetPasswordDoneView.as_view(), name='registration_set_password_done'),
path('registration/set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(),
name='registration_set_password_confirm'),
path('registration/set-password/complete/', SetPasswordCompleteView.as_view(),
name='registration_set_password_complete'),
# wagtail
url(r'^cms/', include(wagtailadmin_urls)),

View File

@ -27,6 +27,31 @@ def home(request):
class SetPasswordView(PasswordResetView):
email_template_name = 'registration/registration_set_password_email.html'
subject_template_name = 'registration/registration_set_password_subject.txt'
success_url = reverse_lazy('registration_set_password_done')
template_name = 'registration/registration_set_password_form.html'
title = _('Password setzen')
class SetPasswordDoneView(PasswordResetDoneView):
template_name = 'registration/registration_set_password_done.html'
title = _('Password setzen versandt')
class SetPasswordConfirmView(PasswordResetConfirmView):
success_url = reverse_lazy('registration_set_password_complete')
template_name = 'registration/registration_set_password_confirm.html'
title = _('Gib ein Passwort ein')
class SetPasswordCompleteView(PasswordResetCompleteView):
template_name = 'registration/registration_set_password_complete.html'
title = _('Passwort setzen erfolgreich')
# legacy
class LegacySetPasswordView(PasswordResetView):
email_template_name = 'registration/set_password_email.html'
subject_template_name = 'registration/set_password_subject.txt'
success_url = reverse_lazy('set_password_done')
@ -34,17 +59,17 @@ class SetPasswordView(PasswordResetView):
title = _('Password setzen')
class SetPasswordDoneView(PasswordResetDoneView):
class LegacySetPasswordDoneView(PasswordResetDoneView):
template_name = 'registration/set_password_done.html'
title = _('Password setzen versandt')
class SetPasswordConfirmView(PasswordResetConfirmView):
class LegacySetPasswordConfirmView(PasswordResetConfirmView):
success_url = reverse_lazy('set_password_complete')
template_name = 'registration/set_password_confirm.html'
title = _('Gib ein Passwort ein')
class SetPasswordCompleteView(PasswordResetCompleteView):
class LegacySetPasswordCompleteView(PasswordResetCompleteView):
template_name = 'registration/set_password_complete.html'
title = _('Passwort setzen erfolgreich')

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,24 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-10
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib import admin
from registration.models import LicenseType, License
@admin.register(LicenseType)
class LicenseTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'key', 'for_role', 'active')
list_filter = ('for_role', 'active')
@admin.register(License)
class LicenseAdmin(admin.ModelAdmin):
list_display = ('license_type', 'licensee')
list_filter = ('license_type', 'licensee')

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,10 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings

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-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings

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-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings
from django.core.management import BaseCommand
from registration.models import LicenseType
from users.models import Role
class Command(BaseCommand):
def handle(self, *args, **options):
try:
role = Role.objects.get(key=Role.objects.TEACHER_KEY)
except Role.DoesNotExist:
print("LicenseType requires that a Teacher Role exsits")
LicenseType.objects.create(name='dummy_license',
for_role=role,
active=True,
key='c1fa2e2a-2e27-480d-8469-2e88414c4ad8',
description='dummy 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,18 @@
# Generated by Django 2.0.6 on 2019-10-10 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='licensetype',
name='key',
field=models.CharField(max_length=128, unique=True),
),
]

View File

@ -0,0 +1,34 @@
# -*- 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, unique=True)
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
def __str__(self):
return '%s - role: %s' % (self.name, self.for_role)
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,92 @@
# -*- 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 graphene import relay
from core.views import SetPasswordView
from registration.models import License
from registration.serializers import RegistrationSerializer
from users.models import User, Role, UserRole, SchoolClass
class PublicFieldError(graphene.ObjectType):
code = graphene.String()
class MutationError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(PublicFieldError)
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(MutationError) # 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)
default_class_name = SchoolClass.generate_default_group_name()
default_class = SchoolClass.objects.create(name=default_class_name)
user.school_classes.add(default_class)
else:
student_role = Role.objects.get(key=Role.objects.STUDENT_KEY)
UserRole.objects.get_or_create(user=user, role=student_role)
password_reset_view = SetPasswordView()
password_reset_view.request = info.context
form = password_reset_view.form_class({'email': user.email})
if not form.is_valid():
return cls(success=False, errors=form.errors)
password_reset_view.form_valid(form)
return cls(success=True)
errors = []
for key, value in serializer.errors.items():
error = MutationError(field=key, errors=[])
for field_error in serializer.errors[key]:
error.errors.append(PublicFieldError(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,120 @@
# -*- 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.core import mail
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, SchoolClass
class RegistrationTests(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)
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
self.assertEqual(len(school_classes), 0)
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)
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
self.assertEqual(len(school_classes), 1)
user = User.objects.get(email=self.email)
self.assertTrue(school_classes[0].is_user_in_schoolclass(user))
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen')
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,13 @@ 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

@ -1,10 +1,12 @@
import re
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Permission
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 +16,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):
@ -70,6 +74,25 @@ class SchoolClass(models.Model):
def __str__(self):
return 'SchoolClass {}-{}'.format(self.id, self.name)
@classmethod
def generate_default_group_name(cls):
prefix = 'Meine Klasse'
prefix_regex = r'Meine Klasse (\d+)'
initial_default_group = '{} 1'.format(prefix)
my_group_filter = cls.objects.filter(name__startswith=prefix).order_by('-pk')
if len(my_group_filter) == 0:
return initial_default_group
match = re.search(prefix_regex, my_group_filter[0].name)
if not match:
return initial_default_group
index = int(match.group(1))
return '{} {}'.format(prefix, index + 1)
def is_user_in_schoolclass(self, user):
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0

View File

@ -7,20 +7,16 @@
#
# Created on 2019-10-01
# @author: chrigu <christian.cueni@iterativ.ch>
import re
import graphene
from django.contrib.auth import authenticate, login
from graphene import relay
class FieldError(graphene.ObjectType):
code = graphene.String()
from registration.models import License
class MutationError(graphene.ObjectType):
class LoginError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(FieldError)
class Login(relay.ClientIDMutation):
@ -29,17 +25,30 @@ class Login(relay.ClientIDMutation):
password_input = graphene.String()
success = graphene.Boolean()
errors = graphene.List(MutationError) # todo: change for consistency
errors = graphene.List(LoginError) # todo: change for consistency
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
if user is not None:
login(info.context, user)
return cls(success=True, errors=[])
else:
return cls(success=False, errors=['invalid_credentials'])
if user is None:
error = LoginError(field='invalid_credentials')
return cls(success=False, errors=[error])
user_license = None
try:
user_license = License.objects.get(licensee=user)
except License.DoesNotExist:
# current users have no license, allow them to login
pass
if user_license is not None and not user_license.license_type.active:
error = LoginError(field='license_inactive')
return cls(success=False, errors=[error])
login(info.context, user)
return cls(success=True, errors=[])
class UserMutations:

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)

View File

@ -13,11 +13,15 @@ from graphene.test import Client
from api.schema_public import schema
from core.factories import UserFactory
from registration.factories import LicenseTypeFactory, LicenseFactory
from users.models import Role
class PasswordResetTests(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch')
self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role")
self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role)
request = RequestFactory().post('/')
@ -62,3 +66,25 @@ class PasswordResetTests(TestCase):
result = self.make_login_mutation(self.user.email, 'test1234')
self.assertFalse(result.get('data').get('login').get('success'))
def test_user_with_active_license_can_login(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
result = self.make_login_mutation(self.user.email, password)
self.assertTrue(result.get('data').get('login').get('success'))
def test_user_with_inactive_license_cannot_login(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
self.teacher_license_type.active = False
self.teacher_license_type.save()
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
result = self.make_login_mutation(self.user.email, password)
self.assertFalse(result.get('data').get('login').get('success'))

View File

@ -17,7 +17,7 @@ from django.contrib.auth import authenticate
from users.factories import SchoolClassFactory
class PasswordUpdate(TestCase):
class MySchoolClasses(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi')
self.another_user = UserFactory(username='pesche')

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-10
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-04-09
# @author: chrigu <christian.cueni@iterativ.ch>
from django.test import TestCase
from users.models import SchoolClass
class SchoolClasses(TestCase):
def setUp(self):
self.prefix = 'Meine Klasse'
def test_default_class_name_initial(self):
class_name = SchoolClass.generate_default_group_name()
self.assertEqual('{} 1'.format(self.prefix), class_name)
def test_default_class_name_initial_with_similar_existing(self):
SchoolClass.objects.create(name='{} abc212'.format(self.prefix))
class_name = SchoolClass.generate_default_group_name()
self.assertEqual('{} 1'.format(self.prefix), class_name)
def test_default_class_name_if_existing(self):
SchoolClass.objects.create(name='{} 1'.format(self.prefix))
SchoolClass.objects.create(name='{} 10'.format(self.prefix))
class_name = SchoolClass.generate_default_group_name()
self.assertEqual('{} 11'.format(self.prefix), class_name)