Commit 291004de by Greg Price

Factor create_account param validation into a form

parent ad86ef3b
......@@ -2,16 +2,23 @@
Utility functions for validating forms
"""
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.hashers import UNUSABLE_PASSWORD
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from django.utils.translation import ugettext_lazy as _
from django.template import loader
from django.conf import settings
from microsite_configuration import microsite
from util.password_policy_validators import (
validate_password_length,
validate_password_complexity,
validate_password_dictionary,
)
class PasswordResetFormNoActive(PasswordResetForm):
......@@ -70,3 +77,160 @@ class PasswordResetFormNoActive(PasswordResetForm):
subject = subject.replace('\n', '')
email = loader.render_to_string(email_template_name, context)
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
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.forms import AccountCreationForm
from student.views import _do_create_account
def get_random_post_override():
def make_random_form():
"""
Generate unique user data for dummy users.
"""
identification = uuid.uuid4().hex[:8]
return {
'username': 'user_{id}'.format(id=identification),
'email': 'email_{id}@example.com'.format(id=identification),
'password': '12345',
'name': 'User {id}'.format(id=identification),
}
return AccountCreationForm(
data={
'username': 'user_{id}'.format(id=identification),
'email': 'email_{id}@example.com'.format(id=identification),
'password': '12345',
'name': 'User {id}'.format(id=identification),
},
tos_required=False
)
def create(num, course_key):
"""Create num users, enrolling them in course_key if it's not None"""
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:
CourseEnrollment.enroll(user, course_key)
......
......@@ -8,6 +8,7 @@ from django.utils import translation
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.forms import AccountCreationForm
from student.models import CourseEnrollment, Registration, create_comments_service_user
from student.views import _do_create_account, AccountValidationError
from track.management.tracked_command import TrackedCommand
......@@ -80,21 +81,22 @@ class Command(TrackedCommand):
except InvalidKeyError:
course = SlashSeparatedCourseKey.from_deprecated_string(options['course'])
post_data = {
'username': username,
'email': options['email'],
'password': options['password'],
'name': name,
'honor_code': u'true',
'terms_of_service': u'true',
}
form = AccountCreationForm(
data={
'username': username,
'email': options['email'],
'password': options['password'],
'name': name,
},
tos_required=False
)
# django.utils.translation.get_language() will be used to set the new
# user's preferred language. This line ensures that the result will
# match this installation's default locale. Otherwise, inside a
# management command, it will always return "en-us".
translation.activate(settings.LANGUAGE_CODE)
try:
user, profile, reg = _do_create_account(post_data)
user, _, reg = _do_create_account(form)
if options['staff']:
user.is_staff = True
user.save()
......
......@@ -93,13 +93,19 @@ class TestCreateAccount(TestCase):
def test_profile_saved_no_optional_fields(self):
profile = self.create_account_and_fetch_profile()
self.assertEqual(profile.name, self.params["name"])
self.assertIsNone(profile.level_of_education)
self.assertIsNone(profile.gender)
self.assertIsNone(profile.mailing_address)
self.assertIsNone(profile.city)
self.assertEqual(profile.level_of_education, "")
self.assertEqual(profile.gender, "")
self.assertEqual(profile.mailing_address, "")
self.assertEqual(profile.city, "")
self.assertEqual(profile.country, "")
self.assertIsNone(profile.goals)
self.assertEqual(profile.meta, "")
self.assertEqual(profile.goals, "")
self.assertEqual(
profile.get_meta(),
{
"extra1": "",
"extra2": "",
}
)
self.assertIsNone(profile.year_of_birth)
@unittest.skipUnless(
......@@ -267,7 +273,7 @@ class TestCreateAccountValidation(TestCase):
# Missing
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
for username in ["", "a"]:
......@@ -282,10 +288,6 @@ class TestCreateAccountValidation(TestCase):
params["username"] = "invalid username"
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):
params = dict(self.minimal_params)
......@@ -298,7 +300,7 @@ class TestCreateAccountValidation(TestCase):
# Missing
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
for email in ["", "a"]:
......@@ -311,7 +313,7 @@ class TestCreateAccountValidation(TestCase):
# Invalid
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):
params = dict(self.minimal_params)
......@@ -325,7 +327,7 @@ class TestCreateAccountValidation(TestCase):
# Missing
del params["password"]
assert_password_error("Error (401 password). E-mail us.")
assert_password_error("A valid password is required")
# Empty, too short
for password in ["", "a"]:
......@@ -334,6 +336,10 @@ class TestCreateAccountValidation(TestCase):
# 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):
params = dict(self.minimal_params)
......@@ -346,7 +352,7 @@ class TestCreateAccountValidation(TestCase):
# Missing
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
for name in ["", "a"]:
......@@ -369,13 +375,20 @@ class TestCreateAccountValidation(TestCase):
assert_honor_code_error("To enroll, you must follow the honor code.")
# Empty, invalid
for honor_code in ["", "false", "True"]:
for honor_code in ["", "false", "not_boolean"]:
params["honor_code"] = 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"}):
# Missing
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)
def test_terms_of_service(self):
......@@ -393,10 +406,14 @@ class TestCreateAccountValidation(TestCase):
assert_terms_of_service_error("You must accept the terms of service.")
# Empty, invalid
for terms_of_service in ["", "false", "True"]:
for terms_of_service in ["", "false", "not_boolean"]:
params["terms_of_service"] = 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(
("level_of_education", 1, "A level of education 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