PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/common/djangoapps/student/forms.py

https://gitlab.com/unofficial-mirrors/edx-platform
Python | 330 lines | 320 code | 5 blank | 5 comment | 4 complexity | f6b8400d052eab3cc2888b2659466b09 MD5 | raw file
  1. """
  2. Utility functions for validating forms
  3. """
  4. import re
  5. from importlib import import_module
  6. from django import forms
  7. from django.conf import settings
  8. from django.contrib.auth.forms import PasswordResetForm
  9. from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
  10. from django.contrib.auth.models import User
  11. from django.contrib.auth.tokens import default_token_generator
  12. from django.core.exceptions import ValidationError
  13. from django.forms import widgets
  14. from django.template import loader
  15. from django.utils.http import int_to_base36
  16. from django.utils.translation import ugettext_lazy as _
  17. from django.core.validators import RegexValidator, slug_re
  18. from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
  19. from openedx.core.djangoapps.user_api import accounts as accounts_settings
  20. from student.models import CourseEnrollmentAllowed
  21. from util.password_policy_validators import validate_password_strength
  22. class PasswordResetFormNoActive(PasswordResetForm):
  23. error_messages = {
  24. 'unknown': _("That e-mail address doesn't have an associated "
  25. "user account. Are you sure you've registered?"),
  26. 'unusable': _("The user account associated with this e-mail "
  27. "address cannot reset the password."),
  28. }
  29. def clean_email(self):
  30. """
  31. This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm
  32. Except removing the requirement of active users
  33. Validates that a user exists with the given email address.
  34. """
  35. email = self.cleaned_data["email"]
  36. #The line below contains the only change, removing is_active=True
  37. self.users_cache = User.objects.filter(email__iexact=email)
  38. if not len(self.users_cache):
  39. raise forms.ValidationError(self.error_messages['unknown'])
  40. if any((user.password.startswith(UNUSABLE_PASSWORD_PREFIX))
  41. for user in self.users_cache):
  42. raise forms.ValidationError(self.error_messages['unusable'])
  43. return email
  44. def save(
  45. self,
  46. subject_template_name='emails/password_reset_subject.txt',
  47. email_template_name='registration/password_reset_email.html',
  48. use_https=False,
  49. token_generator=default_token_generator,
  50. from_email=configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL),
  51. request=None
  52. ):
  53. """
  54. Generates a one-use only link for resetting password and sends to the
  55. user.
  56. """
  57. # This import is here because we are copying and modifying the .save from Django 1.4.5's
  58. # django.contrib.auth.forms.PasswordResetForm directly, which has this import in this place.
  59. from django.core.mail import send_mail
  60. for user in self.users_cache:
  61. site_name = configuration_helpers.get_value(
  62. 'SITE_NAME',
  63. settings.SITE_NAME
  64. )
  65. context = {
  66. 'email': user.email,
  67. 'site_name': site_name,
  68. 'uid': int_to_base36(user.id),
  69. 'user': user,
  70. 'token': token_generator.make_token(user),
  71. 'protocol': 'https' if use_https else 'http',
  72. 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME)
  73. }
  74. subject = loader.render_to_string(subject_template_name, context)
  75. # Email subject *must not* contain newlines
  76. subject = subject.replace('\n', '')
  77. email = loader.render_to_string(email_template_name, context)
  78. send_mail(subject, email, from_email, [user.email])
  79. class TrueCheckbox(widgets.CheckboxInput):
  80. """
  81. A checkbox widget that only accepts "true" (case-insensitive) as true.
  82. """
  83. def value_from_datadict(self, data, files, name):
  84. value = data.get(name, '')
  85. return value.lower() == 'true'
  86. class TrueField(forms.BooleanField):
  87. """
  88. A boolean field that only accepts "true" (case-insensitive) as true
  89. """
  90. widget = TrueCheckbox
  91. def validate_username(username):
  92. """
  93. Verifies a username is valid, raises a ValidationError otherwise.
  94. Args:
  95. username (unicode): The username to validate.
  96. This function is configurable with `ENABLE_UNICODE_USERNAME` feature.
  97. """
  98. username_re = slug_re
  99. flags = None
  100. message = accounts_settings.USERNAME_INVALID_CHARS_ASCII
  101. if settings.FEATURES.get("ENABLE_UNICODE_USERNAME"):
  102. username_re = r"^{regex}$".format(regex=settings.USERNAME_REGEX_PARTIAL)
  103. flags = re.UNICODE
  104. message = accounts_settings.USERNAME_INVALID_CHARS_UNICODE
  105. validator = RegexValidator(
  106. regex=username_re,
  107. flags=flags,
  108. message=message,
  109. code='invalid',
  110. )
  111. validator(username)
  112. class UsernameField(forms.CharField):
  113. """
  114. A CharField that validates usernames based on the `ENABLE_UNICODE_USERNAME` feature.
  115. """
  116. default_validators = [validate_username]
  117. def __init__(self, *args, **kwargs):
  118. super(UsernameField, self).__init__(
  119. min_length=accounts_settings.USERNAME_MIN_LENGTH,
  120. max_length=accounts_settings.USERNAME_MAX_LENGTH,
  121. error_messages={
  122. "required": accounts_settings.USERNAME_BAD_LENGTH_MSG,
  123. "min_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
  124. "max_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
  125. }
  126. )
  127. def clean(self, value):
  128. """
  129. Strips the spaces from the username.
  130. Similar to what `django.forms.SlugField` does.
  131. """
  132. value = self.to_python(value).strip()
  133. return super(UsernameField, self).clean(value)
  134. class AccountCreationForm(forms.Form):
  135. """
  136. A form to for account creation data. It is currently only used for
  137. validation, not rendering.
  138. """
  139. _EMAIL_INVALID_MSG = _("A properly formatted e-mail is required")
  140. _PASSWORD_INVALID_MSG = _("A valid password is required")
  141. _NAME_TOO_SHORT_MSG = _("Your legal name must be a minimum of two characters long")
  142. # TODO: Resolve repetition
  143. username = UsernameField()
  144. email = forms.EmailField(
  145. max_length=accounts_settings.EMAIL_MAX_LENGTH,
  146. min_length=accounts_settings.EMAIL_MIN_LENGTH,
  147. error_messages={
  148. "required": _EMAIL_INVALID_MSG,
  149. "invalid": _EMAIL_INVALID_MSG,
  150. "max_length": _("Email cannot be more than %(limit_value)s characters long"),
  151. }
  152. )
  153. password = forms.CharField(
  154. min_length=accounts_settings.PASSWORD_MIN_LENGTH,
  155. error_messages={
  156. "required": _PASSWORD_INVALID_MSG,
  157. "min_length": _PASSWORD_INVALID_MSG,
  158. }
  159. )
  160. name = forms.CharField(
  161. min_length=accounts_settings.NAME_MIN_LENGTH,
  162. error_messages={
  163. "required": _NAME_TOO_SHORT_MSG,
  164. "min_length": _NAME_TOO_SHORT_MSG,
  165. }
  166. )
  167. def __init__(
  168. self,
  169. data=None,
  170. extra_fields=None,
  171. extended_profile_fields=None,
  172. enforce_username_neq_password=False,
  173. enforce_password_policy=False,
  174. tos_required=True
  175. ):
  176. super(AccountCreationForm, self).__init__(data)
  177. extra_fields = extra_fields or {}
  178. self.extended_profile_fields = extended_profile_fields or {}
  179. self.enforce_username_neq_password = enforce_username_neq_password
  180. self.enforce_password_policy = enforce_password_policy
  181. if tos_required:
  182. self.fields["terms_of_service"] = TrueField(
  183. error_messages={"required": _("You must accept the terms of service.")}
  184. )
  185. # TODO: These messages don't say anything about minimum length
  186. error_message_dict = {
  187. "level_of_education": _("A level of education is required"),
  188. "gender": _("Your gender is required"),
  189. "year_of_birth": _("Your year of birth is required"),
  190. "mailing_address": _("Your mailing address is required"),
  191. "goals": _("A description of your goals is required"),
  192. "city": _("A city is required"),
  193. "country": _("A country is required")
  194. }
  195. for field_name, field_value in extra_fields.items():
  196. if field_name not in self.fields:
  197. if field_name == "honor_code":
  198. if field_value == "required":
  199. self.fields[field_name] = TrueField(
  200. error_messages={
  201. "required": _("To enroll, you must follow the honor code.")
  202. }
  203. )
  204. else:
  205. required = field_value == "required"
  206. min_length = 1 if field_name in ("gender", "level_of_education") else 2
  207. error_message = error_message_dict.get(
  208. field_name,
  209. _("You are missing one or more required fields")
  210. )
  211. self.fields[field_name] = forms.CharField(
  212. required=required,
  213. min_length=min_length,
  214. error_messages={
  215. "required": error_message,
  216. "min_length": error_message,
  217. }
  218. )
  219. for field in self.extended_profile_fields:
  220. if field not in self.fields:
  221. self.fields[field] = forms.CharField(required=False)
  222. def clean_password(self):
  223. """Enforce password policies (if applicable)"""
  224. password = self.cleaned_data["password"]
  225. if (
  226. self.enforce_username_neq_password and
  227. "username" in self.cleaned_data and
  228. self.cleaned_data["username"] == password
  229. ):
  230. raise ValidationError(_("Username and password fields cannot match"))
  231. if self.enforce_password_policy:
  232. try:
  233. validate_password_strength(password)
  234. except ValidationError, err:
  235. raise ValidationError(_("Password: ") + "; ".join(err.messages))
  236. return password
  237. def clean_email(self):
  238. """ Enforce email restrictions (if applicable) """
  239. email = self.cleaned_data["email"]
  240. if settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED is not None:
  241. # This Open edX instance has restrictions on what email addresses are allowed.
  242. allowed_patterns = settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED
  243. # We append a '$' to the regexs to prevent the common mistake of using a
  244. # pattern like '.*@edx\\.org' which would match 'bob@edx.org.badguy.com'
  245. if not any(re.match(pattern + "$", email) for pattern in allowed_patterns):
  246. # This email is not on the whitelist of allowed emails. Check if
  247. # they may have been manually invited by an instructor and if not,
  248. # reject the registration.
  249. if not CourseEnrollmentAllowed.objects.filter(email=email).exists():
  250. raise ValidationError(_("Unauthorized email address."))
  251. if User.objects.filter(email__iexact=email).exists():
  252. raise ValidationError(
  253. _(
  254. "It looks like {email} belongs to an existing account. Try again with a different email address."
  255. ).format(email=email)
  256. )
  257. return email
  258. def clean_year_of_birth(self):
  259. """
  260. Parse year_of_birth to an integer, but just use None instead of raising
  261. an error if it is malformed
  262. """
  263. try:
  264. year_str = self.cleaned_data["year_of_birth"]
  265. return int(year_str) if year_str is not None else None
  266. except ValueError:
  267. return None
  268. @property
  269. def cleaned_extended_profile(self):
  270. """
  271. Return a dictionary containing the extended_profile_fields and values
  272. """
  273. return {
  274. key: value
  275. for key, value in self.cleaned_data.items()
  276. if key in self.extended_profile_fields and value is not None
  277. }
  278. def get_registration_extension_form(*args, **kwargs):
  279. """
  280. Convenience function for getting the custom form set in settings.REGISTRATION_EXTENSION_FORM.
  281. An example form app for this can be found at http://github.com/open-craft/custom-form-app
  282. """
  283. if not settings.FEATURES.get("ENABLE_COMBINED_LOGIN_REGISTRATION"):
  284. return None
  285. if not getattr(settings, 'REGISTRATION_EXTENSION_FORM', None):
  286. return None
  287. module, klass = settings.REGISTRATION_EXTENSION_FORM.rsplit('.', 1)
  288. module = import_module(module)
  289. return getattr(module, klass)(*args, **kwargs)