Commit 291004de by Greg Price

Factor create_account param validation into a form

parent ad86ef3b
...@@ -2,16 +2,23 @@ ...@@ -2,16 +2,23 @@
Utility functions for validating forms Utility functions for validating forms
""" """
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.hashers import UNUSABLE_PASSWORD from django.contrib.auth.hashers import UNUSABLE_PASSWORD
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36 from django.utils.http import int_to_base36
from django.utils.translation import ugettext_lazy as _
from django.template import loader from django.template import loader
from django.conf import settings from django.conf import settings
from microsite_configuration import microsite from microsite_configuration import microsite
from util.password_policy_validators import (
validate_password_length,
validate_password_complexity,
validate_password_dictionary,
)
class PasswordResetFormNoActive(PasswordResetForm): class PasswordResetFormNoActive(PasswordResetForm):
...@@ -70,3 +77,160 @@ class PasswordResetFormNoActive(PasswordResetForm): ...@@ -70,3 +77,160 @@ class PasswordResetFormNoActive(PasswordResetForm):
subject = subject.replace('\n', '') subject = subject.replace('\n', '')
email = loader.render_to_string(email_template_name, context) email = loader.render_to_string(email_template_name, context)
send_mail(subject, email, from_email, [user.email]) send_mail(subject, email, from_email, [user.email])
class TrueField(forms.BooleanField):
"""
A boolean field that only accepts "true" (case-insensitive) as true
"""
def to_python(self, value):
# CheckboxInput converts string to bool by case-insensitive match to "true" or "false"
if value is True:
return value
else:
return None
_USERNAME_TOO_SHORT_MSG = _("Username must be minimum of two characters long")
_EMAIL_INVALID_MSG = _("A properly formatted e-mail is required")
_PASSWORD_INVALID_MSG = _("A valid password is required")
_NAME_TOO_SHORT_MSG = _("Your legal name must be a minimum of two characters long")
class AccountCreationForm(forms.Form):
"""
A form to for account creation data. It is currently only used for
validation, not rendering.
"""
# TODO: Resolve repetition
username = forms.SlugField(
min_length=2,
max_length=30,
error_messages={
"required": _USERNAME_TOO_SHORT_MSG,
"invalid": _("Username should only consist of A-Z and 0-9, with no spaces."),
"min_length": _USERNAME_TOO_SHORT_MSG,
"max_length": _("Username cannot be more than %(limit_value)s characters long"),
}
)
email = forms.EmailField(
max_length=75, # Limit per RFCs is 254, but User's email field in django 1.4 only takes 75
error_messages={
"required": _EMAIL_INVALID_MSG,
"invalid": _EMAIL_INVALID_MSG,
"max_length": _("Email cannot be more than %(limit_value)s characters long"),
}
)
password = forms.CharField(
min_length=2,
error_messages={
"required": _PASSWORD_INVALID_MSG,
"min_length": _PASSWORD_INVALID_MSG,
}
)
name = forms.CharField(
min_length=2,
error_messages={
"required": _NAME_TOO_SHORT_MSG,
"min_length": _NAME_TOO_SHORT_MSG,
}
)
def __init__(
self,
data=None,
extra_fields=None,
extended_profile_fields=None,
enforce_username_neq_password=False,
enforce_password_policy=False,
tos_required=True
):
super(AccountCreationForm, self).__init__(data)
extra_fields = extra_fields or {}
self.extended_profile_fields = extended_profile_fields or {}
self.enforce_username_neq_password = enforce_username_neq_password
self.enforce_password_policy = enforce_password_policy
if tos_required:
self.fields["terms_of_service"] = TrueField(
error_messages={"required": _("You must accept the terms of service.")}
)
# TODO: These messages don't say anything about minimum length
error_message_dict = {
"level_of_education": _("A level of education is required"),
"gender": _("Your gender is required"),
"year_of_birth": _("Your year of birth is required"),
"mailing_address": _("Your mailing address is required"),
"goals": _("A description of your goals is required"),
"city": _("A city is required"),
"country": _("A country is required")
}
for field_name, field_value in extra_fields.items():
if field_name not in self.fields:
if field_name == "honor_code":
if field_value == "required":
self.fields[field_name] = TrueField(
error_messages={
"required": _("To enroll, you must follow the honor code.")
}
)
else:
required = field_value == "required"
min_length = 1 if field_name in ("gender", "level_of_education") else 2
error_message = error_message_dict.get(
field_name,
_("You are missing one or more required fields")
)
self.fields[field_name] = forms.CharField(
required=required,
min_length=min_length,
error_messages={
"required": error_message,
"min_length": error_message,
}
)
for field in self.extended_profile_fields:
if field not in self.fields:
self.fields[field] = forms.CharField(required=False)
def clean_password(self):
"""Enforce password policies (if applicable)"""
password = self.cleaned_data["password"]
if (
self.enforce_username_neq_password and
"username" in self.cleaned_data and
self.cleaned_data["username"] == password
):
raise ValidationError(_("Username and password fields cannot match"))
if self.enforce_password_policy:
try:
validate_password_length(password)
validate_password_complexity(password)
validate_password_dictionary(password)
except ValidationError, err:
raise ValidationError(_("Password: ") + "; ".join(err.messages))
return password
def clean_year_of_birth(self):
"""
Parse year_of_birth to an integer, but just use None instead of raising
an error if it is malformed
"""
try:
year_str = self.cleaned_data["year_of_birth"]
return int(year_str) if year_str is not None else None
except ValueError:
return None
@property
def cleaned_extended_profile(self):
"""
Return a dictionary containing the extended_profile_fields and values
"""
return {
key: value
for key, value in self.cleaned_data.items()
if key in self.extended_profile_fields and value is not None
}
...@@ -8,26 +8,30 @@ from student.models import CourseEnrollment ...@@ -8,26 +8,30 @@ from student.models import CourseEnrollment
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.forms import AccountCreationForm
from student.views import _do_create_account from student.views import _do_create_account
def get_random_post_override(): def make_random_form():
""" """
Generate unique user data for dummy users. Generate unique user data for dummy users.
""" """
identification = uuid.uuid4().hex[:8] identification = uuid.uuid4().hex[:8]
return { return AccountCreationForm(
'username': 'user_{id}'.format(id=identification), data={
'email': 'email_{id}@example.com'.format(id=identification), 'username': 'user_{id}'.format(id=identification),
'password': '12345', 'email': 'email_{id}@example.com'.format(id=identification),
'name': 'User {id}'.format(id=identification), 'password': '12345',
} 'name': 'User {id}'.format(id=identification),
},
tos_required=False
)
def create(num, course_key): def create(num, course_key):
"""Create num users, enrolling them in course_key if it's not None""" """Create num users, enrolling them in course_key if it's not None"""
for idx in range(num): for idx in range(num):
(user, user_profile, __) = _do_create_account(get_random_post_override()) (user, _, _) = _do_create_account(make_random_form())
if course_key is not None: if course_key is not None:
CourseEnrollment.enroll(user, course_key) CourseEnrollment.enroll(user, course_key)
......
...@@ -8,6 +8,7 @@ from django.utils import translation ...@@ -8,6 +8,7 @@ from django.utils import translation
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.forms import AccountCreationForm
from student.models import CourseEnrollment, Registration, create_comments_service_user from student.models import CourseEnrollment, Registration, create_comments_service_user
from student.views import _do_create_account, AccountValidationError from student.views import _do_create_account, AccountValidationError
from track.management.tracked_command import TrackedCommand from track.management.tracked_command import TrackedCommand
...@@ -80,21 +81,22 @@ class Command(TrackedCommand): ...@@ -80,21 +81,22 @@ class Command(TrackedCommand):
except InvalidKeyError: except InvalidKeyError:
course = SlashSeparatedCourseKey.from_deprecated_string(options['course']) course = SlashSeparatedCourseKey.from_deprecated_string(options['course'])
post_data = { form = AccountCreationForm(
'username': username, data={
'email': options['email'], 'username': username,
'password': options['password'], 'email': options['email'],
'name': name, 'password': options['password'],
'honor_code': u'true', 'name': name,
'terms_of_service': u'true', },
} tos_required=False
)
# django.utils.translation.get_language() will be used to set the new # django.utils.translation.get_language() will be used to set the new
# user's preferred language. This line ensures that the result will # user's preferred language. This line ensures that the result will
# match this installation's default locale. Otherwise, inside a # match this installation's default locale. Otherwise, inside a
# management command, it will always return "en-us". # management command, it will always return "en-us".
translation.activate(settings.LANGUAGE_CODE) translation.activate(settings.LANGUAGE_CODE)
try: try:
user, profile, reg = _do_create_account(post_data) user, _, reg = _do_create_account(form)
if options['staff']: if options['staff']:
user.is_staff = True user.is_staff = True
user.save() user.save()
......
...@@ -93,13 +93,19 @@ class TestCreateAccount(TestCase): ...@@ -93,13 +93,19 @@ class TestCreateAccount(TestCase):
def test_profile_saved_no_optional_fields(self): def test_profile_saved_no_optional_fields(self):
profile = self.create_account_and_fetch_profile() profile = self.create_account_and_fetch_profile()
self.assertEqual(profile.name, self.params["name"]) self.assertEqual(profile.name, self.params["name"])
self.assertIsNone(profile.level_of_education) self.assertEqual(profile.level_of_education, "")
self.assertIsNone(profile.gender) self.assertEqual(profile.gender, "")
self.assertIsNone(profile.mailing_address) self.assertEqual(profile.mailing_address, "")
self.assertIsNone(profile.city) self.assertEqual(profile.city, "")
self.assertEqual(profile.country, "") self.assertEqual(profile.country, "")
self.assertIsNone(profile.goals) self.assertEqual(profile.goals, "")
self.assertEqual(profile.meta, "") self.assertEqual(
profile.get_meta(),
{
"extra1": "",
"extra2": "",
}
)
self.assertIsNone(profile.year_of_birth) self.assertIsNone(profile.year_of_birth)
@unittest.skipUnless( @unittest.skipUnless(
...@@ -267,7 +273,7 @@ class TestCreateAccountValidation(TestCase): ...@@ -267,7 +273,7 @@ class TestCreateAccountValidation(TestCase):
# Missing # Missing
del params["username"] del params["username"]
assert_username_error("Error (401 username). E-mail us.") assert_username_error("Username must be minimum of two characters long")
# Empty, too short # Empty, too short
for username in ["", "a"]: for username in ["", "a"]:
...@@ -282,10 +288,6 @@ class TestCreateAccountValidation(TestCase): ...@@ -282,10 +288,6 @@ class TestCreateAccountValidation(TestCase):
params["username"] = "invalid username" params["username"] = "invalid username"
assert_username_error("Username should only consist of A-Z and 0-9, with no spaces.") assert_username_error("Username should only consist of A-Z and 0-9, with no spaces.")
# Matching password
params["username"] = params["password"] = "test_username_and_password"
assert_username_error("Username and password fields cannot match")
def test_email(self): def test_email(self):
params = dict(self.minimal_params) params = dict(self.minimal_params)
...@@ -298,7 +300,7 @@ class TestCreateAccountValidation(TestCase): ...@@ -298,7 +300,7 @@ class TestCreateAccountValidation(TestCase):
# Missing # Missing
del params["email"] del params["email"]
assert_email_error("Error (401 email). E-mail us.") assert_email_error("A properly formatted e-mail is required")
# Empty, too short # Empty, too short
for email in ["", "a"]: for email in ["", "a"]:
...@@ -311,7 +313,7 @@ class TestCreateAccountValidation(TestCase): ...@@ -311,7 +313,7 @@ class TestCreateAccountValidation(TestCase):
# Invalid # Invalid
params["email"] = "not_an_email_address" params["email"] = "not_an_email_address"
assert_email_error("Valid e-mail is required.") assert_email_error("A properly formatted e-mail is required")
def test_password(self): def test_password(self):
params = dict(self.minimal_params) params = dict(self.minimal_params)
...@@ -325,7 +327,7 @@ class TestCreateAccountValidation(TestCase): ...@@ -325,7 +327,7 @@ class TestCreateAccountValidation(TestCase):
# Missing # Missing
del params["password"] del params["password"]
assert_password_error("Error (401 password). E-mail us.") assert_password_error("A valid password is required")
# Empty, too short # Empty, too short
for password in ["", "a"]: for password in ["", "a"]:
...@@ -334,6 +336,10 @@ class TestCreateAccountValidation(TestCase): ...@@ -334,6 +336,10 @@ class TestCreateAccountValidation(TestCase):
# Password policy is tested elsewhere # Password policy is tested elsewhere
# Matching username
params["username"] = params["password"] = "test_username_and_password"
assert_password_error("Username and password fields cannot match")
def test_name(self): def test_name(self):
params = dict(self.minimal_params) params = dict(self.minimal_params)
...@@ -346,7 +352,7 @@ class TestCreateAccountValidation(TestCase): ...@@ -346,7 +352,7 @@ class TestCreateAccountValidation(TestCase):
# Missing # Missing
del params["name"] del params["name"]
assert_name_error("Error (401 name). E-mail us.") assert_name_error("Your legal name must be a minimum of two characters long")
# Empty, too short # Empty, too short
for name in ["", "a"]: for name in ["", "a"]:
...@@ -369,13 +375,20 @@ class TestCreateAccountValidation(TestCase): ...@@ -369,13 +375,20 @@ class TestCreateAccountValidation(TestCase):
assert_honor_code_error("To enroll, you must follow the honor code.") assert_honor_code_error("To enroll, you must follow the honor code.")
# Empty, invalid # Empty, invalid
for honor_code in ["", "false", "True"]: for honor_code in ["", "false", "not_boolean"]:
params["honor_code"] = honor_code params["honor_code"] = honor_code
assert_honor_code_error("To enroll, you must follow the honor code.") assert_honor_code_error("To enroll, you must follow the honor code.")
# True
params["honor_code"] = "tRUe"
self.assert_success(params)
with override_settings(REGISTRATION_EXTRA_FIELDS={"honor_code": "optional"}): with override_settings(REGISTRATION_EXTRA_FIELDS={"honor_code": "optional"}):
# Missing # Missing
del params["honor_code"] del params["honor_code"]
# Need to change username/email because user was created above
params["username"] = "another_test_username"
params["email"] = "another_test_email@example.com"
self.assert_success(params) self.assert_success(params)
def test_terms_of_service(self): def test_terms_of_service(self):
...@@ -393,10 +406,14 @@ class TestCreateAccountValidation(TestCase): ...@@ -393,10 +406,14 @@ class TestCreateAccountValidation(TestCase):
assert_terms_of_service_error("You must accept the terms of service.") assert_terms_of_service_error("You must accept the terms of service.")
# Empty, invalid # Empty, invalid
for terms_of_service in ["", "false", "True"]: for terms_of_service in ["", "false", "not_boolean"]:
params["terms_of_service"] = terms_of_service params["terms_of_service"] = terms_of_service
assert_terms_of_service_error("You must accept the terms of service.") assert_terms_of_service_error("You must accept the terms of service.")
# True
params["terms_of_service"] = "tRUe"
self.assert_success(params)
@ddt.data( @ddt.data(
("level_of_education", 1, "A level of education is required"), ("level_of_education", 1, "A level of education is required"),
("gender", 1, "Your gender is required"), ("gender", 1, "Your gender is required"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment