Skip to content

Commit 94b2359

Browse files
committed
Merge branch 'unstable' into complete-icons-migration
2 parents 14cfe2b + 57debb9 commit 94b2359

File tree

138 files changed

+7396
-1578
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+7396
-1578
lines changed

contentcuration/contentcuration/forms.py

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import json
2-
from builtins import object
32

43
from django import forms
54
from django.conf import settings
65
from django.contrib.auth.forms import PasswordResetForm
76
from django.contrib.auth.forms import UserChangeForm
87
from django.contrib.auth.forms import UserCreationForm
98
from django.core import signing
9+
from django.core.exceptions import ValidationError
1010
from django.db.models import Q
1111
from django.template.loader import render_to_string
1212

@@ -16,23 +16,16 @@
1616
REGISTRATION_SALT = getattr(settings, 'REGISTRATION_SALT', 'registration')
1717

1818

19-
class ExtraFormMixin(object):
20-
21-
def check_field(self, field, error):
22-
if not self.cleaned_data.get(field):
23-
self.errors[field] = self.error_class()
24-
self.add_error(field, error)
25-
return False
26-
return self.cleaned_data.get(field)
27-
28-
2919
# LOGIN/REGISTRATION FORMS
3020
#################################################################
31-
class RegistrationForm(UserCreationForm, ExtraFormMixin):
21+
class RegistrationForm(UserCreationForm):
22+
CODE_ACCOUNT_ACTIVE = 'account_active'
23+
CODE_ACCOUNT_INACTIVE = 'account_inactive'
24+
3225
first_name = forms.CharField(required=True)
3326
last_name = forms.CharField(required=True)
34-
email = forms.CharField(required=True)
35-
password1 = forms.CharField(required=True)
27+
email = forms.EmailField(required=True)
28+
password1 = forms.CharField(required=True, min_length=8)
3629
password2 = forms.CharField(required=True)
3730
uses = forms.CharField(required=True)
3831
other_use = forms.CharField(required=False)
@@ -45,22 +38,18 @@ class RegistrationForm(UserCreationForm, ExtraFormMixin):
4538
locations = forms.CharField(required=True)
4639

4740
def clean_email(self):
48-
email = self.cleaned_data['email'].strip().lower()
49-
if User.objects.filter(Q(is_active=True) | Q(deleted=True), email__iexact=email).exists():
50-
raise UserWarning
41+
# ensure email is lower case
42+
email = self.cleaned_data["email"].strip().lower()
43+
user_qs = User.objects.filter(email__iexact=email)
44+
if user_qs.exists():
45+
if user_qs.filter(Q(is_active=True) | Q(deleted=True)).exists():
46+
raise ValidationError("Account already active", code=self.CODE_ACCOUNT_ACTIVE)
47+
else:
48+
raise ValidationError("Already registered.", code=self.CODE_ACCOUNT_INACTIVE)
5149
return email
5250

53-
def clean(self):
54-
super(RegistrationForm, self).clean()
55-
56-
# Errors should be caught on the frontend
57-
# or a warning should be thrown if the account exists
58-
self.errors.clear()
59-
return self.cleaned_data
60-
6151
def save(self, commit=True):
62-
user = super(RegistrationForm, self).save(commit=commit)
63-
user.set_password(self.cleaned_data["password1"])
52+
user = super(RegistrationForm, self).save(commit=False)
6453
user.first_name = self.cleaned_data["first_name"]
6554
user.last_name = self.cleaned_data["last_name"]
6655
user.information = {
@@ -165,7 +154,7 @@ def save(self, user):
165154
return user
166155

167156

168-
class StorageRequestForm(forms.Form, ExtraFormMixin):
157+
class StorageRequestForm(forms.Form):
169158
# Nature of content
170159
storage = forms.CharField(required=True)
171160
kind = forms.CharField(required=True)
@@ -194,7 +183,7 @@ class Meta:
194183
"audience", "import_count", "location", "uploading_for", "organization_type", "time_constraint", "message")
195184

196185

197-
class IssueReportForm(forms.Form, ExtraFormMixin):
186+
class IssueReportForm(forms.Form):
198187
operating_system = forms.CharField(required=True)
199188
browser = forms.CharField(required=True)
200189
channel = forms.CharField(required=False)
@@ -204,7 +193,7 @@ class Meta:
204193
fields = ("operating_system", "browser", "channel", "description")
205194

206195

207-
class DeleteAccountForm(forms.Form, ExtraFormMixin):
196+
class DeleteAccountForm(forms.Form):
208197
email = forms.CharField(required=True)
209198

210199
def __init__(self, user, *args, **kwargs):
@@ -214,5 +203,5 @@ def __init__(self, user, *args, **kwargs):
214203
def clean_email(self):
215204
email = self.cleaned_data['email'].strip().lower()
216205
if self.user.is_admin or self.user.email.lower() != self.cleaned_data['email']:
217-
raise UserWarning
206+
raise ValidationError("Not allowed")
218207
return email

contentcuration/contentcuration/frontend/accounts/pages/Create.vue

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,40 @@
3131
v-model="form.first_name"
3232
maxlength="100"
3333
counter
34-
:label="$tr('firstNameLabel')"
3534
autofocus
35+
:label="$tr('firstNameLabel')"
36+
:error-messages="errors.first_name"
37+
@input="resetErrors('first_name')"
3638
/>
3739
<TextField
3840
v-model="form.last_name"
3941
maxlength="100"
4042
counter
4143
:label="$tr('lastNameLabel')"
44+
:error-messages="errors.last_name"
45+
@input="resetErrors('last_name')"
4246
/>
4347
<EmailField
4448
v-model="form.email"
4549
maxlength="100"
4650
counter
4751
:disabled="Boolean($route.query.email)"
48-
:error-messages="emailErrors"
49-
@input="emailErrors = []"
52+
:error-messages="errors.email"
53+
@input="resetErrors('email')"
5054
/>
5155
<PasswordField
5256
v-model="form.password1"
57+
:additionalRules="passwordValidationRules"
5358
:label="$tr('passwordLabel')"
59+
:error-messages="errors.password1"
60+
@input="resetErrors('password1')"
5461
/>
5562
<PasswordField
5663
v-model="form.password2"
5764
:additionalRules="passwordConfirmRules"
5865
:label="$tr('confirmPasswordLabel')"
66+
:error-messages="errors.password2"
67+
@input="resetErrors('password2')"
5968
/>
6069

6170
<!-- Usage -->
@@ -200,6 +209,7 @@
200209
import Checkbox from 'shared/views/form/Checkbox';
201210
import { policies } from 'shared/constants';
202211
import DropdownWrapper from 'shared/views/form/DropdownWrapper';
212+
import commonStrings from 'shared/translator';
203213
204214
export default {
205215
name: 'Create',
@@ -219,7 +229,6 @@
219229
return {
220230
valid: true,
221231
registrationFailed: false,
222-
emailErrors: [],
223232
form: {
224233
first_name: '',
225234
last_name: '',
@@ -237,6 +246,13 @@
237246
accepted_policy: false,
238247
accepted_tos: false,
239248
},
249+
errors: {
250+
first_name: [],
251+
last_name: [],
252+
email: [],
253+
password1: [],
254+
password2: [],
255+
},
240256
};
241257
},
242258
computed: {
@@ -247,6 +263,9 @@
247263
passwordConfirmRules() {
248264
return [value => (this.form.password1 === value ? true : this.$tr('passwordMatchMessage'))];
249265
},
266+
passwordValidationRules() {
267+
return [value => (value.length >= 8 ? true : this.$tr('passwordValidationMessage'))];
268+
},
250269
acceptedAgreement: {
251270
get() {
252271
return this.form.accepted_tos && this.form.accepted_policy;
@@ -294,10 +313,12 @@
294313
];
295314
},
296315
usageRules() {
297-
return [() => (this.form.uses.length ? true : this.$tr('fieldRequiredMessage'))];
316+
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
317+
return [() => (this.form.uses.length ? true : commonStrings.$tr('fieldRequired'))];
298318
},
299319
locationRules() {
300-
return [() => (this.form.locations.length ? true : this.$tr('fieldRequiredMessage'))];
320+
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
321+
return [() => (this.form.locations.length ? true : commonStrings.$tr('fieldRequired'))];
301322
},
302323
sources() {
303324
return sources;
@@ -359,7 +380,8 @@
359380
];
360381
},
361382
sourceRules() {
362-
return [() => (this.form.source.length ? true : this.$tr('fieldRequiredMessage'))];
383+
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
384+
return [() => (this.form.source.length ? true : commonStrings.$tr('fieldRequired'))];
363385
},
364386
clean() {
365387
return data => {
@@ -413,12 +435,11 @@
413435
},
414436
methods: {
415437
...mapActions('account', ['register']),
416-
...mapActions('policies', ['openPolicy']),
417438
showTermsOfService() {
418-
this.openPolicy(policies.TERMS_OF_SERVICE);
439+
this.$router.push({ query: { showPolicy: policies.TERMS_OF_SERVICE } });
419440
},
420441
showPrivacyPolicy() {
421-
this.openPolicy(policies.PRIVACY);
442+
this.$router.push({ query: { showPolicy: policies.PRIVACY } });
422443
},
423444
showStorageField(id) {
424445
return id === uses.STORING && this.form.uses.includes(id);
@@ -438,8 +459,27 @@
438459
.catch(error => {
439460
if (error.message === 'Network Error') {
440461
this.$refs.top.scrollIntoView({ behavior: 'smooth' });
462+
} else if (error.response.status === 400) {
463+
for (const field of error.response.data) {
464+
if (!Object.prototype.hasOwnProperty.call(this.errors, field)) {
465+
continue;
466+
}
467+
let message = '';
468+
switch (field) {
469+
case 'password1':
470+
message = this.$tr('passwordValidationMessage');
471+
break;
472+
default:
473+
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
474+
message = commonStrings.$tr('fieldHasError');
475+
break;
476+
}
477+
this.errors[field] = [message];
478+
}
479+
this.registrationFailed = true;
480+
this.valid = false;
441481
} else if (error.response.status === 403) {
442-
this.emailErrors = [this.$tr('emailExistsMessage')];
482+
this.errors.email = [this.$tr('emailExistsMessage')];
443483
} else if (error.response.status === 405) {
444484
this.$router.push({ name: 'AccountNotActivated' });
445485
} else {
@@ -452,12 +492,14 @@
452492
}
453493
return Promise.resolve();
454494
},
495+
resetErrors(field) {
496+
this.errors[field] = [];
497+
},
455498
},
456499
457500
$trs: {
458501
backToLoginButton: 'Sign in',
459502
createAnAccountTitle: 'Create an account',
460-
fieldRequiredMessage: 'Field is required',
461503
errorsMessage: 'Please fix the errors below',
462504
registrationFailed: 'There was an error registering your account. Please try again',
463505
registrationFailedOffline:
@@ -470,6 +512,7 @@
470512
passwordLabel: 'Password',
471513
confirmPasswordLabel: 'Confirm password',
472514
passwordMatchMessage: "Passwords don't match",
515+
passwordValidationMessage: 'Password should be at least 8 characters long',
473516
474517
// Usage question
475518
usageLabel: 'How do you plan on using Kolibri Studio (check all that apply)',

contentcuration/contentcuration/frontend/accounts/pages/Main.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,11 @@
153153
},
154154
methods: {
155155
...mapActions(['login']),
156-
...mapActions('policies', ['openPolicy']),
157156
showTermsOfService() {
158-
this.openPolicy(policies.TERMS_OF_SERVICE);
157+
this.$router.push({ query: { showPolicy: policies.TERMS_OF_SERVICE } });
159158
},
160159
showPrivacyPolicy() {
161-
this.openPolicy(policies.PRIVACY);
160+
this.$router.push({ query: { showPolicy: policies.PRIVACY } });
162161
},
163162
submit() {
164163
if (this.$refs.form.validate()) {

contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const defaultData = {
1717
first_name: 'Test',
1818
last_name: 'User',
1919
20-
password1: 'pass',
21-
password2: 'pass',
20+
password1: 'tester123',
21+
password2: 'tester123',
2222
uses: ['tagging'],
2323
storage: '',
2424
other_use: '',
@@ -126,6 +126,11 @@ describe('create', () => {
126126
expect(register).not.toHaveBeenCalled();
127127
});
128128
});
129+
it('should fail if password1 is too short', () => {
130+
const wrapper = makeWrapper({ password1: '123' });
131+
wrapper.vm.submit();
132+
expect(register).not.toHaveBeenCalled();
133+
});
129134
it('should fail if password1 and password2 do not match', () => {
130135
const wrapper = makeWrapper({ password1: 'some other password' });
131136
wrapper.vm.submit();
@@ -155,7 +160,7 @@ describe('create', () => {
155160
it('should say account with email already exists if register returns a 403', async () => {
156161
wrapper.setMethods({ register: makeFailedPromise(403) });
157162
await wrapper.vm.submit();
158-
expect(wrapper.vm.emailErrors).toHaveLength(1);
163+
expect(wrapper.vm.errors.email).toHaveLength(1);
159164
});
160165
it('should say account has not been activated if register returns 405', async () => {
161166
wrapper.setMethods({ register: makeFailedPromise(405) });

contentcuration/contentcuration/frontend/accounts/pages/__tests__/resetPassword.spec.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ describe('resetPassword', () => {
3939
});
4040
it('should call setPassword on submit if password data is valid', () => {
4141
wrapper.setData({ new_password1: 'pass', new_password2: 'pass' });
42-
wrapper.find({ ref: 'form' }).trigger('submit');
43-
expect(setPassword).toHaveBeenCalled();
42+
wrapper.vm.$nextTick(() => {
43+
wrapper.find({ ref: 'form' }).trigger('submit');
44+
expect(setPassword).toHaveBeenCalled();
45+
});
4446
});
4547
it('should retain data from query params so reset credentials are preserved', () => {
4648
router.replace({
@@ -50,7 +52,9 @@ describe('resetPassword', () => {
5052
},
5153
});
5254
wrapper.setData({ new_password1: 'pass', new_password2: 'pass' });
53-
wrapper.find({ ref: 'form' }).trigger('submit');
54-
expect(setPassword.mock.calls[0][0].test).toBe('testing');
55+
wrapper.vm.$nextTick(() => {
56+
wrapper.find({ ref: 'form' }).trigger('submit');
57+
expect(setPassword.mock.calls[0][0].test).toBe('testing');
58+
});
5559
});
5660
});

contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetPassword.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<PasswordField
1010
v-model="new_password1"
1111
:label="$tr('passwordLabel')"
12+
:additionalRules="passwordValidationRules"
1213
autofocus
1314
/>
1415
<PasswordField
@@ -52,6 +53,9 @@
5253
passwordConfirmRules() {
5354
return [value => (this.new_password1 === value ? true : this.$tr('passwordMatchMessage'))];
5455
},
56+
passwordValidationRules() {
57+
return [value => (value.length >= 8 ? true : this.$tr('passwordValidationMessage'))];
58+
},
5559
},
5660
methods: {
5761
...mapActions('account', ['setPassword']),
@@ -80,6 +84,7 @@
8084
resetPasswordPrompt: 'Enter and confirm your new password',
8185
passwordLabel: 'New password',
8286
passwordConfirmLabel: 'Confirm password',
87+
passwordValidationMessage: 'Password should be at least 8 characters long',
8388
passwordMatchMessage: "Passwords don't match",
8489
submitButton: 'Submit',
8590
resetPasswordFailed: 'Failed to reset password. Please try again.',

0 commit comments

Comments
 (0)