diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 4825f2d2d2..697c172bf1 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -5,13 +5,12 @@ import logging import urlparse from datetime import datetime -import pytz from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import resolve, reverse -from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import redirect from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie @@ -20,7 +19,7 @@ from django_countries import countries import third_party_auth from commerce.models import CommerceConfiguration -from edxmako.shortcuts import render_to_response, render_to_string +from edxmako.shortcuts import render_to_response from lms.djangoapps.commerce.utils import EcommerceService from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.external_auth.login_and_register import login as external_auth_login @@ -30,6 +29,11 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming.helpers import is_request_in_themed_site from openedx.core.djangoapps.user_api.accounts.api import request_password_change +from openedx.core.djangoapps.user_api.api import ( + RegistrationFormFactory, + get_login_session_form, + get_password_reset_form +) from openedx.core.djangoapps.user_api.errors import UserNotFound from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES @@ -385,43 +389,14 @@ def _get_form_descriptions(request): values are the JSON-serialized form descriptions. """ + return { - 'login': _local_server_get('/user_api/v1/account/login_session/', request.session), - 'registration': _local_server_get('/user_api/v1/account/registration/', request.session), - 'password_reset': _local_server_get('/user_api/v1/account/password_reset/', request.session) + 'password_reset': get_password_reset_form().to_json(), + 'login': get_login_session_form().to_json(), + 'registration': RegistrationFormFactory().get_registration_form(request).to_json() } -def _local_server_get(url, session): - """Simulate a server-server GET request for an in-process API. - - Arguments: - url (str): The URL of the request (excluding the protocol and domain) - session (SessionStore): The session of the original request, - used to get past the CSRF checks. - - Returns: - str: The content of the response - - """ - # Since the user API is currently run in-process, - # we simulate the server-server API call by constructing - # our own request object. We don't need to include much - # information in the request except for the session - # (to get past through CSRF validation) - request = HttpRequest() - request.method = "GET" - request.session = session - - # Call the Django view function, simulating - # the server-server API call - view, args, kwargs = resolve(url) - response = view(request, *args, **kwargs) - - # Return the content of the response - return response.content - - def _external_auth_intercept(request, mode): """Allow external auth to intercept a login/registration request. diff --git a/openedx/core/djangoapps/user_api/api.py b/openedx/core/djangoapps/user_api/api.py new file mode 100644 index 0000000000..fdfd568bf7 --- /dev/null +++ b/openedx/core/djangoapps/user_api/api.py @@ -0,0 +1,813 @@ +import copy + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from django_countries import countries + +import accounts +import third_party_auth +from edxmako.shortcuts import marketing_link +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.djangoapps.user_api.helpers import FormDescription +from openedx.features.enterprise_support.api import enterprise_customer_for_request +from student.forms import get_registration_extension_form +from student.models import UserProfile + + +def get_password_reset_form(): + """Return a description of the password reset form. + + This decouples clients from the API definition: + if the API decides to modify the form, clients won't need + to be updated. + + See `user_api.helpers.FormDescription` for examples + of the JSON-encoded form description. + + Returns: + HttpResponse + + """ + form_desc = FormDescription("post", reverse("password_change_request")) + + # Translators: This label appears above a field on the password reset + # form meant to hold the user's email address. + email_label = _(u"Email") + + # Translators: This example email address is used as a placeholder in + # a field on the password reset form meant to hold the user's email address. + email_placeholder = _(u"username@domain.com") + + # Translators: These instructions appear on the password reset form, + # immediately below a field meant to hold the user's email address. + email_instructions = _(u"The email address you used to register with {platform_name}").format( + platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) + ) + + form_desc.add_field( + "email", + field_type="email", + label=email_label, + placeholder=email_placeholder, + instructions=email_instructions, + restrictions={ + "min_length": accounts.EMAIL_MIN_LENGTH, + "max_length": accounts.EMAIL_MAX_LENGTH, + } + ) + + return form_desc + + +def get_login_session_form(): + """Return a description of the login form. + + This decouples clients from the API definition: + if the API decides to modify the form, clients won't need + to be updated. + + See `user_api.helpers.FormDescription` for examples + of the JSON-encoded form description. + + Returns: + HttpResponse + + """ + form_desc = FormDescription("post", reverse("user_api_login_session")) + + # Translators: This label appears above a field on the login form + # meant to hold the user's email address. + email_label = _(u"Email") + + # Translators: This example email address is used as a placeholder in + # a field on the login form meant to hold the user's email address. + email_placeholder = _(u"username@domain.com") + + # Translators: These instructions appear on the login form, immediately + # below a field meant to hold the user's email address. + email_instructions = _("The email address you used to register with {platform_name}").format( + platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) + ) + + form_desc.add_field( + "email", + field_type="email", + label=email_label, + placeholder=email_placeholder, + instructions=email_instructions, + restrictions={ + "min_length": accounts.EMAIL_MIN_LENGTH, + "max_length": accounts.EMAIL_MAX_LENGTH, + } + ) + + # Translators: This label appears above a field on the login form + # meant to hold the user's password. + password_label = _(u"Password") + + form_desc.add_field( + "password", + label=password_label, + field_type="password", + restrictions={ + "max_length": accounts.PASSWORD_MAX_LENGTH, + } + ) + + form_desc.add_field( + "remember", + field_type="checkbox", + label=_("Remember me"), + default=False, + required=False, + ) + + return form_desc + + +class RegistrationFormFactory(object): + """HTTP end-points for creating a new user. """ + + DEFAULT_FIELDS = ["email", "name", "username", "password"] + + EXTRA_FIELDS = [ + "confirm_email", + "first_name", + "last_name", + "city", + "state", + "country", + "gender", + "year_of_birth", + "level_of_education", + "company", + "title", + "mailing_address", + "goals", + "honor_code", + "terms_of_service", + ] + + def _is_field_visible(self, field_name): + """Check whether a field is visible based on Django settings. """ + return self._extra_fields_setting.get(field_name) in ["required", "optional"] + + def _is_field_required(self, field_name): + """Check whether a field is required based on Django settings. """ + return self._extra_fields_setting.get(field_name) == "required" + + def __init__(self): + + # Backwards compatibility: Honor code is required by default, unless + # explicitly set to "optional" in Django settings. + self._extra_fields_setting = copy.deepcopy(configuration_helpers.get_value('REGISTRATION_EXTRA_FIELDS')) + if not self._extra_fields_setting: + self._extra_fields_setting = copy.deepcopy(settings.REGISTRATION_EXTRA_FIELDS) + self._extra_fields_setting["honor_code"] = self._extra_fields_setting.get("honor_code", "required") + + # Check that the setting is configured correctly + for field_name in self.EXTRA_FIELDS: + if self._extra_fields_setting.get(field_name, "hidden") not in ["required", "optional", "hidden"]: + msg = u"Setting REGISTRATION_EXTRA_FIELDS values must be either required, optional, or hidden." + raise ImproperlyConfigured(msg) + + # Map field names to the instance method used to add the field to the form + self.field_handlers = {} + valid_fields = self.DEFAULT_FIELDS + self.EXTRA_FIELDS + for field_name in valid_fields: + handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name)) + self.field_handlers[field_name] = handler + + field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER') + if not field_order: + field_order = settings.REGISTRATION_FIELD_ORDER or valid_fields + + # Check that all of the valid_fields are in the field order and vice versa, if not set to the default order + if set(valid_fields) != set(field_order): + field_order = valid_fields + + self.field_order = field_order + + def get_registration_form(self, request): + """Return a description of the registration form. + This decouples clients from the API definition: + if the API decides to modify the form, clients won't need + to be updated. + This is especially important for the registration form, + since different edx-platform installations might + collect different demographic information. + See `user_api.helpers.FormDescription` for examples + of the JSON-encoded form description. + Arguments: + request (HttpRequest) + Returns: + HttpResponse + """ + form_desc = FormDescription("post", reverse("user_api_registration")) + self._apply_third_party_auth_overrides(request, form_desc) + + # Custom form fields can be added via the form set in settings.REGISTRATION_EXTENSION_FORM + custom_form = get_registration_extension_form() + + if custom_form: + # Default fields are always required + for field_name in self.DEFAULT_FIELDS: + self.field_handlers[field_name](form_desc, required=True) + + for field_name, field in custom_form.fields.items(): + restrictions = {} + if getattr(field, 'max_length', None): + restrictions['max_length'] = field.max_length + if getattr(field, 'min_length', None): + restrictions['min_length'] = field.min_length + field_options = getattr( + getattr(custom_form, 'Meta', None), 'serialization_options', {} + ).get(field_name, {}) + field_type = field_options.get('field_type', FormDescription.FIELD_TYPE_MAP.get(field.__class__)) + if not field_type: + raise ImproperlyConfigured( + "Field type '{}' not recognized for registration extension field '{}'.".format( + field_type, + field_name + ) + ) + form_desc.add_field( + field_name, label=field.label, + default=field_options.get('default'), + field_type=field_options.get('field_type', FormDescription.FIELD_TYPE_MAP.get(field.__class__)), + placeholder=field.initial, instructions=field.help_text, required=field.required, + restrictions=restrictions, + options=getattr(field, 'choices', None), error_messages=field.error_messages, + include_default_option=field_options.get('include_default_option'), + ) + + # Extra fields configured in Django settings + # may be required, optional, or hidden + for field_name in self.EXTRA_FIELDS: + if self._is_field_visible(field_name): + self.field_handlers[field_name]( + form_desc, + required=self._is_field_required(field_name) + ) + else: + # Go through the fields in the fields order and add them if they are required or visible + for field_name in self.field_order: + if field_name in self.DEFAULT_FIELDS: + self.field_handlers[field_name](form_desc, required=True) + elif self._is_field_visible(field_name): + self.field_handlers[field_name]( + form_desc, + required=self._is_field_required(field_name) + ) + + return form_desc + + def _add_email_field(self, form_desc, required=True): + """Add an email field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to hold the user's email address. + email_label = _(u"Email") + + # Translators: This example email address is used as a placeholder in + # a field on the registration form meant to hold the user's email address. + email_placeholder = _(u"username@domain.com") + + # Translators: These instructions appear on the registration form, immediately + # below a field meant to hold the user's email address. + email_instructions = _(u"This is what you will use to login.") + + form_desc.add_field( + "email", + field_type="email", + label=email_label, + placeholder=email_placeholder, + instructions=email_instructions, + restrictions={ + "min_length": accounts.EMAIL_MIN_LENGTH, + "max_length": accounts.EMAIL_MAX_LENGTH, + }, + required=required + ) + + def _add_confirm_email_field(self, form_desc, required=True): + """Add an email confirmation field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to confirm the user's email address. + email_label = _(u"Confirm Email") + error_msg = accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG + + form_desc.add_field( + "confirm_email", + label=email_label, + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_name_field(self, form_desc, required=True): + """Add a name field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to hold the user's full name. + name_label = _(u"Full Name") + + # Translators: This example name is used as a placeholder in + # a field on the registration form meant to hold the user's name. + name_placeholder = _(u"Jane Q. Learner") + + # Translators: These instructions appear on the registration form, immediately + # below a field meant to hold the user's full name. + name_instructions = _(u"This name will be used on any certificates that you earn.") + + form_desc.add_field( + "name", + label=name_label, + placeholder=name_placeholder, + instructions=name_instructions, + restrictions={ + "max_length": accounts.NAME_MAX_LENGTH, + }, + required=required + ) + + def _add_username_field(self, form_desc, required=True): + """Add a username field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to hold the user's public username. + username_label = _(u"Public Username") + + username_instructions = _( + # Translators: These instructions appear on the registration form, immediately + # below a field meant to hold the user's public username. + u"The name that will identify you in your courses. " + u"It cannot be changed later." + ) + + # Translators: This example username is used as a placeholder in + # a field on the registration form meant to hold the user's username. + username_placeholder = _(u"Jane_Q_Learner") + + form_desc.add_field( + "username", + label=username_label, + instructions=username_instructions, + placeholder=username_placeholder, + restrictions={ + "min_length": accounts.USERNAME_MIN_LENGTH, + "max_length": accounts.USERNAME_MAX_LENGTH, + }, + required=required + ) + + def _add_password_field(self, form_desc, required=True): + """Add a password field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to hold the user's password. + password_label = _(u"Password") + + form_desc.add_field( + "password", + label=password_label, + field_type="password", + restrictions={ + "min_length": accounts.PASSWORD_MIN_LENGTH, + "max_length": accounts.PASSWORD_MAX_LENGTH, + }, + required=required + ) + + def _add_level_of_education_field(self, form_desc, required=True): + """Add a level of education field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a dropdown menu on the registration + # form used to select the user's highest completed level of education. + education_level_label = _(u"Highest level of education completed") + error_msg = accounts.REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG + + # The labels are marked for translation in UserProfile model definition. + options = [(name, _(label)) for name, label in UserProfile.LEVEL_OF_EDUCATION_CHOICES] # pylint: disable=translation-of-non-string + form_desc.add_field( + "level_of_education", + label=education_level_label, + field_type="select", + options=options, + include_default_option=True, + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_gender_field(self, form_desc, required=True): + """Add a gender field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a dropdown menu on the registration + # form used to select the user's gender. + gender_label = _(u"Gender") + + # The labels are marked for translation in UserProfile model definition. + options = [(name, _(label)) for name, label in UserProfile.GENDER_CHOICES] # pylint: disable=translation-of-non-string + form_desc.add_field( + "gender", + label=gender_label, + field_type="select", + options=options, + include_default_option=True, + required=required + ) + + def _add_year_of_birth_field(self, form_desc, required=True): + """Add a year of birth field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a dropdown menu on the registration + # form used to select the user's year of birth. + yob_label = _(u"Year of birth") + + options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] + form_desc.add_field( + "year_of_birth", + label=yob_label, + field_type="select", + options=options, + include_default_option=True, + required=required + ) + + def _add_mailing_address_field(self, form_desc, required=True): + """Add a mailing address field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # meant to hold the user's mailing address. + mailing_address_label = _(u"Mailing address") + error_msg = accounts.REQUIRED_FIELD_MAILING_ADDRESS_MSG + + form_desc.add_field( + "mailing_address", + label=mailing_address_label, + field_type="textarea", + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_goals_field(self, form_desc, required=True): + """Add a goals field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This phrase appears above a field on the registration form + # meant to hold the user's reasons for registering with edX. + goals_label = _(u"Tell us why you're interested in {platform_name}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME) + ) + error_msg = accounts.REQUIRED_FIELD_GOALS_MSG + + form_desc.add_field( + "goals", + label=goals_label, + field_type="textarea", + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_city_field(self, form_desc, required=True): + """Add a city field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the city in which they live. + city_label = _(u"City") + error_msg = accounts.REQUIRED_FIELD_CITY_MSG + + form_desc.add_field( + "city", + label=city_label, + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_state_field(self, form_desc, required=False): + """Add a State/Province/Region field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the State/Province/Region in which they live. + state_label = _(u"State/Province/Region") + + form_desc.add_field( + "state", + label=state_label, + required=required + ) + + def _add_company_field(self, form_desc, required=False): + """Add a Company field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the Company + company_label = _(u"Company") + + form_desc.add_field( + "company", + label=company_label, + required=required + ) + + def _add_title_field(self, form_desc, required=False): + """Add a Title field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the Title + title_label = _(u"Title") + + form_desc.add_field( + "title", + label=title_label, + required=required + ) + + def _add_first_name_field(self, form_desc, required=False): + """Add a First Name field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the First Name + first_name_label = _(u"First Name") + + form_desc.add_field( + "first_name", + label=first_name_label, + required=required + ) + + def _add_last_name_field(self, form_desc, required=False): + """Add a Last Name field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to False + """ + # Translators: This label appears above a field on the registration form + # which allows the user to input the First Name + last_name_label = _(u"Last Name") + + form_desc.add_field( + "last_name", + label=last_name_label, + required=required + ) + + def _add_country_field(self, form_desc, required=True): + """Add a country field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This label appears above a dropdown menu on the registration + # form used to select the country in which the user lives. + country_label = _(u"Country") + error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG + + # If we set a country code, make sure it's uppercase for the sake of the form. + default_country = form_desc._field_overrides.get('country', {}).get('defaultValue') + if default_country: + form_desc.override_field_properties( + 'country', + default=default_country.upper() + ) + + form_desc.add_field( + "country", + label=country_label, + field_type="select", + options=list(countries), + include_default_option=True, + required=required, + error_messages={ + "required": error_msg + } + ) + + def _add_honor_code_field(self, form_desc, required=True): + """Add an honor code field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Separate terms of service and honor code checkboxes + if self._is_field_visible("terms_of_service"): + terms_label = _(u"Honor Code") + terms_link = marketing_link("HONOR") + terms_text = _(u"Review the Honor Code") + + # Combine terms of service and honor code checkboxes + else: + # Translators: This is a legal document users must agree to + # in order to register a new account. + terms_label = _(u"Terms of Service and Honor Code") + terms_link = marketing_link("HONOR") + terms_text = _(u"Review the Terms of Service and Honor Code") + + # Translators: "Terms of Service" is a legal document users must agree to + # in order to register a new account. + label = _(u"I agree to the {platform_name} {terms_of_service}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), + terms_of_service=terms_label + ) + + # Translators: "Terms of Service" is a legal document users must agree to + # in order to register a new account. + error_msg = _(u"You must agree to the {platform_name} {terms_of_service}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), + terms_of_service=terms_label + ) + + form_desc.add_field( + "honor_code", + label=label, + field_type="checkbox", + default=False, + required=required, + error_messages={ + "required": error_msg + }, + supplementalLink=terms_link, + supplementalText=terms_text + ) + + def _add_terms_of_service_field(self, form_desc, required=True): + """Add a terms of service field to a form description. + Arguments: + form_desc: A form description + Keyword Arguments: + required (bool): Whether this field is required; defaults to True + """ + # Translators: This is a legal document users must agree to + # in order to register a new account. + terms_label = _(u"Terms of Service") + terms_link = marketing_link("TOS") + terms_text = _(u"Review the Terms of Service") + + # Translators: "Terms of service" is a legal document users must agree to + # in order to register a new account. + label = _(u"I agree to the {platform_name} {terms_of_service}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), + terms_of_service=terms_label + ) + + # Translators: "Terms of service" is a legal document users must agree to + # in order to register a new account. + error_msg = _(u"You must agree to the {platform_name} {terms_of_service}").format( + platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), + terms_of_service=terms_label + ) + + form_desc.add_field( + "terms_of_service", + label=label, + field_type="checkbox", + default=False, + required=required, + error_messages={ + "required": error_msg + }, + supplementalLink=terms_link, + supplementalText=terms_text + ) + + def _apply_third_party_auth_overrides(self, request, form_desc): + """Modify the registration form if the user has authenticated with a third-party provider. + If a user has successfully authenticated with a third-party provider, + but does not yet have an account with EdX, we want to fill in + the registration form with any info that we get from the + provider. + This will also hide the password field, since we assign users a default + (random) password on the assumption that they will be using + third-party auth to log in. + Arguments: + request (HttpRequest): The request for the registration form, used + to determine if the user has successfully authenticated + with a third-party provider. + form_desc (FormDescription): The registration form description + """ + if third_party_auth.is_enabled(): + running_pipeline = third_party_auth.pipeline.get(request) + if running_pipeline: + current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) + + if current_provider: + # Override username / email / full name + field_overrides = current_provider.get_register_form_data( + running_pipeline.get('kwargs') + ) + + # When the TPA Provider is configured to skip the registration form and we are in an + # enterprise context, we need to hide all fields except for terms of service and + # ensure that the user explicitly checks that field. + hide_registration_fields_except_tos = (current_provider.skip_registration_form and + enterprise_customer_for_request(request)) + + for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: + if field_name in field_overrides: + form_desc.override_field_properties( + field_name, default=field_overrides[field_name] + ) + + if (field_name not in ['terms_of_service', 'honor_code'] + and field_overrides[field_name] + and hide_registration_fields_except_tos): + + form_desc.override_field_properties( + field_name, + field_type="hidden", + label="", + instructions="", + ) + + # Hide the password field + form_desc.override_field_properties( + "password", + default="", + field_type="hidden", + required=False, + label="", + instructions="", + restrictions={} + ) + # used to identify that request is running third party social auth + form_desc.add_field( + "social_auth_provider", + field_type="hidden", + label="", + default=current_provider.name if current_provider.name else "Third Party", + required=False, + ) diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 659eda012c..8c1d6c9b30 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -609,7 +609,7 @@ class LoginSessionViewTest(UserAPITestCase): "placeholder": "", "instructions": "", "restrictions": { - "max_length": PASSWORD_MAX_LENGTH + "max_length": PASSWORD_MAX_LENGTH, }, "errorMessages": {}, "supplementalText": "", @@ -1098,7 +1098,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): provider = self.configure_google_provider(enabled=True) with simulate_running_pipeline( - "openedx.core.djangoapps.user_api.views.third_party_auth.pipeline", "google-oauth2", + "openedx.core.djangoapps.user_api.api.third_party_auth.pipeline", "google-oauth2", email="bob@example.com", fullname="Bob", username="Bob123", @@ -1203,7 +1203,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): } ) - @mock.patch('openedx.core.djangoapps.user_api.views._') + @mock.patch('openedx.core.djangoapps.user_api.api._') def test_register_form_level_of_education_translations(self, fake_gettext): fake_gettext.side_effect = lambda text: text + ' TRANSLATED' @@ -1249,7 +1249,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase): } ) - @mock.patch('openedx.core.djangoapps.user_api.views._') + @mock.patch('openedx.core.djangoapps.user_api.api._') def test_register_form_gender_translations(self, fake_gettext): fake_gettext.side_effect = lambda text: text + ' TRANSLATED' diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py index 782139615d..cf9c85481b 100644 --- a/openedx/core/djangoapps/user_api/views.py +++ b/openedx/core/djangoapps/user_api/views.py @@ -1,16 +1,12 @@ """HTTP end-points for the User API. """ -import copy -from django.conf import settings from django.contrib.auth.models import User -from django.core.exceptions import NON_FIELD_ERRORS, ImproperlyConfigured, PermissionDenied, ValidationError -from django.core.urlresolvers import reverse +from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.http import HttpResponse, HttpResponseForbidden from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie from django.views.decorators.debug import sensitive_post_parameters -from django_countries import countries from django_filters.rest_framework import DjangoFilterBackend from opaque_keys import InvalidKeyError from opaque_keys.edx import locator @@ -19,25 +15,24 @@ from rest_framework import authentication, generics, status, viewsets from rest_framework.exceptions import ParseError from rest_framework.views import APIView -import third_party_auth +import accounts from django_comment_common.models import Role -from edxmako.shortcuts import marketing_link -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_api.accounts.api import check_account_exists +from openedx.core.djangoapps.user_api.api import ( + RegistrationFormFactory, + get_login_session_form, + get_password_reset_form +) +from openedx.core.djangoapps.user_api.helpers import require_post_params, shim_student_view +from openedx.core.djangoapps.user_api.models import UserPreference +from openedx.core.djangoapps.user_api.preferences.api import get_country_time_zones, update_email_opt_in +from openedx.core.djangoapps.user_api.serializers import CountryTimeZoneSerializer, UserPreferenceSerializer, UserSerializer from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser from openedx.core.lib.api.permissions import ApiKeyHeaderPermission -from openedx.features.enterprise_support.api import enterprise_customer_for_request from student.cookies import set_logged_in_cookies -from student.forms import get_registration_extension_form -from student.views import create_account_with_params, AccountValidationError +from student.views import AccountValidationError, create_account_with_params from util.json_request import JsonResponse -import accounts -from .helpers import FormDescription, require_post_params, shim_student_view -from .models import UserPreference, UserProfile -from .preferences.api import get_country_time_zones, update_email_opt_in -from .serializers import CountryTimeZoneSerializer, UserPreferenceSerializer, UserSerializer - class LoginSessionView(APIView): """HTTP end-points for logging in users. """ @@ -48,69 +43,7 @@ class LoginSessionView(APIView): @method_decorator(ensure_csrf_cookie) def get(self, request): - """Return a description of the login form. - - This decouples clients from the API definition: - if the API decides to modify the form, clients won't need - to be updated. - - See `user_api.helpers.FormDescription` for examples - of the JSON-encoded form description. - - Returns: - HttpResponse - - """ - form_desc = FormDescription("post", reverse("user_api_login_session")) - - # Translators: This label appears above a field on the login form - # meant to hold the user's email address. - email_label = _(u"Email") - - # Translators: This example email address is used as a placeholder in - # a field on the login form meant to hold the user's email address. - email_placeholder = _(u"username@domain.com") - - # Translators: These instructions appear on the login form, immediately - # below a field meant to hold the user's email address. - email_instructions = _("The email address you used to register with {platform_name}").format( - platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) - ) - - form_desc.add_field( - "email", - field_type="email", - label=email_label, - placeholder=email_placeholder, - instructions=email_instructions, - restrictions={ - "min_length": accounts.EMAIL_MIN_LENGTH, - "max_length": accounts.EMAIL_MAX_LENGTH, - } - ) - - # Translators: This label appears above a field on the login form - # meant to hold the user's password. - password_label = _(u"Password") - - form_desc.add_field( - "password", - label=password_label, - field_type="password", - restrictions={ - "max_length": accounts.PASSWORD_MAX_LENGTH, - } - ) - - form_desc.add_field( - "remember", - field_type="checkbox", - label=_("Remember me"), - default=False, - required=False, - ) - - return HttpResponse(form_desc.to_json(), content_type="application/json") + return HttpResponse(get_login_session_form().to_json(), content_type="application/json") @method_decorator(require_post_params(["email", "password"])) @method_decorator(csrf_protect) @@ -157,151 +90,14 @@ class LoginSessionView(APIView): class RegistrationView(APIView): """HTTP end-points for creating a new user. """ - DEFAULT_FIELDS = ["email", "name", "username", "password"] - - EXTRA_FIELDS = [ - "confirm_email", - "first_name", - "last_name", - "city", - "state", - "country", - "gender", - "year_of_birth", - "level_of_education", - "company", - "title", - "mailing_address", - "goals", - "honor_code", - "terms_of_service", - ] - # This end-point is available to anonymous users, # so do not require authentication. authentication_classes = [] - def _is_field_visible(self, field_name): - """Check whether a field is visible based on Django settings. """ - return self._extra_fields_setting.get(field_name) in ["required", "optional"] - - def _is_field_required(self, field_name): - """Check whether a field is required based on Django settings. """ - return self._extra_fields_setting.get(field_name) == "required" - - def __init__(self, *args, **kwargs): - super(RegistrationView, self).__init__(*args, **kwargs) - - # Backwards compatibility: Honor code is required by default, unless - # explicitly set to "optional" in Django settings. - self._extra_fields_setting = copy.deepcopy(configuration_helpers.get_value('REGISTRATION_EXTRA_FIELDS')) - if not self._extra_fields_setting: - self._extra_fields_setting = copy.deepcopy(settings.REGISTRATION_EXTRA_FIELDS) - self._extra_fields_setting["honor_code"] = self._extra_fields_setting.get("honor_code", "required") - - # Check that the setting is configured correctly - for field_name in self.EXTRA_FIELDS: - if self._extra_fields_setting.get(field_name, "hidden") not in ["required", "optional", "hidden"]: - msg = u"Setting REGISTRATION_EXTRA_FIELDS values must be either required, optional, or hidden." - raise ImproperlyConfigured(msg) - - # Map field names to the instance method used to add the field to the form - self.field_handlers = {} - valid_fields = self.DEFAULT_FIELDS + self.EXTRA_FIELDS - for field_name in valid_fields: - handler = getattr(self, "_add_{field_name}_field".format(field_name=field_name)) - self.field_handlers[field_name] = handler - - field_order = configuration_helpers.get_value('REGISTRATION_FIELD_ORDER') - if not field_order: - field_order = settings.REGISTRATION_FIELD_ORDER or valid_fields - - # Check that all of the valid_fields are in the field order and vice versa, if not set to the default order - if set(valid_fields) != set(field_order): - field_order = valid_fields - - self.field_order = field_order - @method_decorator(ensure_csrf_cookie) def get(self, request): - """Return a description of the registration form. - - This decouples clients from the API definition: - if the API decides to modify the form, clients won't need - to be updated. - - This is especially important for the registration form, - since different edx-platform installations might - collect different demographic information. - - See `user_api.helpers.FormDescription` for examples - of the JSON-encoded form description. - - Arguments: - request (HttpRequest) - - Returns: - HttpResponse - - """ - form_desc = FormDescription("post", reverse("user_api_registration")) - self._apply_third_party_auth_overrides(request, form_desc) - - # Custom form fields can be added via the form set in settings.REGISTRATION_EXTENSION_FORM - custom_form = get_registration_extension_form() - - if custom_form: - # Default fields are always required - for field_name in self.DEFAULT_FIELDS: - self.field_handlers[field_name](form_desc, required=True) - - for field_name, field in custom_form.fields.items(): - restrictions = {} - if getattr(field, 'max_length', None): - restrictions['max_length'] = field.max_length - if getattr(field, 'min_length', None): - restrictions['min_length'] = field.min_length - field_options = getattr( - getattr(custom_form, 'Meta', None), 'serialization_options', {} - ).get(field_name, {}) - field_type = field_options.get('field_type', FormDescription.FIELD_TYPE_MAP.get(field.__class__)) - if not field_type: - raise ImproperlyConfigured( - "Field type '{}' not recognized for registration extension field '{}'.".format( - field_type, - field_name - ) - ) - form_desc.add_field( - field_name, label=field.label, - default=field_options.get('default'), - field_type=field_options.get('field_type', FormDescription.FIELD_TYPE_MAP.get(field.__class__)), - placeholder=field.initial, instructions=field.help_text, required=field.required, - restrictions=restrictions, - options=getattr(field, 'choices', None), error_messages=field.error_messages, - include_default_option=field_options.get('include_default_option'), - ) - - # Extra fields configured in Django settings - # may be required, optional, or hidden - for field_name in self.EXTRA_FIELDS: - if self._is_field_visible(field_name): - self.field_handlers[field_name]( - form_desc, - required=self._is_field_required(field_name) - ) - else: - # Go through the fields in the fields order and add them if they are required or visible - for field_name in self.field_order: - if field_name in self.DEFAULT_FIELDS: - self.field_handlers[field_name](form_desc, required=True) - elif self._is_field_visible(field_name): - self.field_handlers[field_name]( - form_desc, - required=self._is_field_required(field_name) - ) - - return HttpResponse(form_desc.to_json(), content_type="application/json") + return HttpResponse(RegistrationFormFactory().get_registration_form(request).to_json(), + content_type="application/json") @method_decorator(csrf_exempt) def post(self, request): @@ -377,616 +173,6 @@ class RegistrationView(APIView): def dispatch(self, request, *args, **kwargs): return super(RegistrationView, self).dispatch(request, *args, **kwargs) - def _add_email_field(self, form_desc, required=True): - """Add an email field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to hold the user's email address. - email_label = _(u"Email") - - # Translators: This example email address is used as a placeholder in - # a field on the registration form meant to hold the user's email address. - email_placeholder = _(u"username@domain.com") - - # Translators: These instructions appear on the registration form, immediately - # below a field meant to hold the user's email address. - email_instructions = _(u"This is what you will use to login.") - - form_desc.add_field( - "email", - field_type="email", - label=email_label, - placeholder=email_placeholder, - instructions=email_instructions, - restrictions={ - "min_length": accounts.EMAIL_MIN_LENGTH, - "max_length": accounts.EMAIL_MAX_LENGTH, - }, - required=required - ) - - def _add_confirm_email_field(self, form_desc, required=True): - """Add an email confirmation field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to confirm the user's email address. - email_label = _(u"Confirm Email") - error_msg = accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG - - form_desc.add_field( - "confirm_email", - label=email_label, - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_name_field(self, form_desc, required=True): - """Add a name field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to hold the user's full name. - name_label = _(u"Full Name") - - # Translators: This example name is used as a placeholder in - # a field on the registration form meant to hold the user's name. - name_placeholder = _(u"Jane Q. Learner") - - # Translators: These instructions appear on the registration form, immediately - # below a field meant to hold the user's full name. - name_instructions = _(u"This name will be used on any certificates that you earn.") - - form_desc.add_field( - "name", - label=name_label, - placeholder=name_placeholder, - instructions=name_instructions, - restrictions={ - "max_length": accounts.NAME_MAX_LENGTH, - }, - required=required - ) - - def _add_username_field(self, form_desc, required=True): - """Add a username field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to hold the user's public username. - username_label = _(u"Public Username") - - username_instructions = _( - # Translators: These instructions appear on the registration form, immediately - # below a field meant to hold the user's public username. - u"The name that will identify you in your courses. " - u"It cannot be changed later." - ) - - # Translators: This example username is used as a placeholder in - # a field on the registration form meant to hold the user's username. - username_placeholder = _(u"Jane_Q_Learner") - - form_desc.add_field( - "username", - label=username_label, - instructions=username_instructions, - placeholder=username_placeholder, - restrictions={ - "min_length": accounts.USERNAME_MIN_LENGTH, - "max_length": accounts.USERNAME_MAX_LENGTH, - }, - required=required - ) - - def _add_password_field(self, form_desc, required=True): - """Add a password field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to hold the user's password. - password_label = _(u"Password") - - form_desc.add_field( - "password", - label=password_label, - field_type="password", - restrictions={ - "min_length": accounts.PASSWORD_MIN_LENGTH, - "max_length": accounts.PASSWORD_MAX_LENGTH, - }, - required=required - ) - - def _add_level_of_education_field(self, form_desc, required=True): - """Add a level of education field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a dropdown menu on the registration - # form used to select the user's highest completed level of education. - education_level_label = _(u"Highest level of education completed") - error_msg = accounts.REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG - - # The labels are marked for translation in UserProfile model definition. - options = [(name, _(label)) for name, label in UserProfile.LEVEL_OF_EDUCATION_CHOICES] # pylint: disable=translation-of-non-string - form_desc.add_field( - "level_of_education", - label=education_level_label, - field_type="select", - options=options, - include_default_option=True, - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_gender_field(self, form_desc, required=True): - """Add a gender field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a dropdown menu on the registration - # form used to select the user's gender. - gender_label = _(u"Gender") - - # The labels are marked for translation in UserProfile model definition. - options = [(name, _(label)) for name, label in UserProfile.GENDER_CHOICES] # pylint: disable=translation-of-non-string - form_desc.add_field( - "gender", - label=gender_label, - field_type="select", - options=options, - include_default_option=True, - required=required - ) - - def _add_year_of_birth_field(self, form_desc, required=True): - """Add a year of birth field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a dropdown menu on the registration - # form used to select the user's year of birth. - yob_label = _(u"Year of birth") - - options = [(unicode(year), unicode(year)) for year in UserProfile.VALID_YEARS] - form_desc.add_field( - "year_of_birth", - label=yob_label, - field_type="select", - options=options, - include_default_option=True, - required=required - ) - - def _add_mailing_address_field(self, form_desc, required=True): - """Add a mailing address field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # meant to hold the user's mailing address. - mailing_address_label = _(u"Mailing address") - error_msg = accounts.REQUIRED_FIELD_MAILING_ADDRESS_MSG - - form_desc.add_field( - "mailing_address", - label=mailing_address_label, - field_type="textarea", - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_goals_field(self, form_desc, required=True): - """Add a goals field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This phrase appears above a field on the registration form - # meant to hold the user's reasons for registering with edX. - goals_label = _(u"Tell us why you're interested in {platform_name}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME) - ) - error_msg = accounts.REQUIRED_FIELD_GOALS_MSG - - form_desc.add_field( - "goals", - label=goals_label, - field_type="textarea", - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_city_field(self, form_desc, required=True): - """Add a city field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the city in which they live. - city_label = _(u"City") - error_msg = accounts.REQUIRED_FIELD_CITY_MSG - - form_desc.add_field( - "city", - label=city_label, - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_state_field(self, form_desc, required=False): - """Add a State/Province/Region field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to False - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the State/Province/Region in which they live. - state_label = _(u"State/Province/Region") - - form_desc.add_field( - "state", - label=state_label, - required=required - ) - - def _add_company_field(self, form_desc, required=False): - """Add a Company field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to False - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the Company - company_label = _(u"Company") - - form_desc.add_field( - "company", - label=company_label, - required=required - ) - - def _add_title_field(self, form_desc, required=False): - """Add a Title field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to False - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the Title - title_label = _(u"Title") - - form_desc.add_field( - "title", - label=title_label, - required=required - ) - - def _add_first_name_field(self, form_desc, required=False): - """Add a First Name field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to False - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the First Name - first_name_label = _(u"First Name") - - form_desc.add_field( - "first_name", - label=first_name_label, - required=required - ) - - def _add_last_name_field(self, form_desc, required=False): - """Add a Last Name field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to False - - """ - # Translators: This label appears above a field on the registration form - # which allows the user to input the First Name - last_name_label = _(u"Last Name") - - form_desc.add_field( - "last_name", - label=last_name_label, - required=required - ) - - def _add_country_field(self, form_desc, required=True): - """Add a country field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This label appears above a dropdown menu on the registration - # form used to select the country in which the user lives. - country_label = _(u"Country") - error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG - - # If we set a country code, make sure it's uppercase for the sake of the form. - default_country = form_desc._field_overrides.get('country', {}).get('defaultValue') - if default_country: - form_desc.override_field_properties( - 'country', - default=default_country.upper() - ) - - form_desc.add_field( - "country", - label=country_label, - field_type="select", - options=list(countries), - include_default_option=True, - required=required, - error_messages={ - "required": error_msg - } - ) - - def _add_honor_code_field(self, form_desc, required=True): - """Add an honor code field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Separate terms of service and honor code checkboxes - if self._is_field_visible("terms_of_service"): - terms_label = _(u"Honor Code") - terms_link = marketing_link("HONOR") - terms_text = _(u"Review the Honor Code") - - # Combine terms of service and honor code checkboxes - else: - # Translators: This is a legal document users must agree to - # in order to register a new account. - terms_label = _(u"Terms of Service and Honor Code") - terms_link = marketing_link("HONOR") - terms_text = _(u"Review the Terms of Service and Honor Code") - - # Translators: "Terms of Service" is a legal document users must agree to - # in order to register a new account. - label = _(u"I agree to the {platform_name} {terms_of_service}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), - terms_of_service=terms_label - ) - - # Translators: "Terms of Service" is a legal document users must agree to - # in order to register a new account. - error_msg = _(u"You must agree to the {platform_name} {terms_of_service}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), - terms_of_service=terms_label - ) - - form_desc.add_field( - "honor_code", - label=label, - field_type="checkbox", - default=False, - required=required, - error_messages={ - "required": error_msg - }, - supplementalLink=terms_link, - supplementalText=terms_text - ) - - def _add_terms_of_service_field(self, form_desc, required=True): - """Add a terms of service field to a form description. - - Arguments: - form_desc: A form description - - Keyword Arguments: - required (bool): Whether this field is required; defaults to True - - """ - # Translators: This is a legal document users must agree to - # in order to register a new account. - terms_label = _(u"Terms of Service") - terms_link = marketing_link("TOS") - terms_text = _(u"Review the Terms of Service") - - # Translators: "Terms of service" is a legal document users must agree to - # in order to register a new account. - label = _(u"I agree to the {platform_name} {terms_of_service}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), - terms_of_service=terms_label - ) - - # Translators: "Terms of service" is a legal document users must agree to - # in order to register a new account. - error_msg = _(u"You must agree to the {platform_name} {terms_of_service}").format( - platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME), - terms_of_service=terms_label - ) - - form_desc.add_field( - "terms_of_service", - label=label, - field_type="checkbox", - default=False, - required=required, - error_messages={ - "required": error_msg - }, - supplementalLink=terms_link, - supplementalText=terms_text - ) - - def _apply_third_party_auth_overrides(self, request, form_desc): - """Modify the registration form if the user has authenticated with a third-party provider. - - If a user has successfully authenticated with a third-party provider, - but does not yet have an account with EdX, we want to fill in - the registration form with any info that we get from the - provider. - - This will also hide the password field, since we assign users a default - (random) password on the assumption that they will be using - third-party auth to log in. - - Arguments: - request (HttpRequest): The request for the registration form, used - to determine if the user has successfully authenticated - with a third-party provider. - - form_desc (FormDescription): The registration form description - - """ - if third_party_auth.is_enabled(): - running_pipeline = third_party_auth.pipeline.get(request) - if running_pipeline: - current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) - - if current_provider: - # Override username / email / full name - field_overrides = current_provider.get_register_form_data( - running_pipeline.get('kwargs') - ) - - # When the TPA Provider is configured to skip the registration form and we are in an - # enterprise context, we need to hide all fields except for terms of service and - # ensure that the user explicitly checks that field. - hide_registration_fields_except_tos = (current_provider.skip_registration_form and - enterprise_customer_for_request(request)) - - for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS: - if field_name in field_overrides: - form_desc.override_field_properties( - field_name, default=field_overrides[field_name] - ) - - if (field_name not in ['terms_of_service', 'honor_code'] - and field_overrides[field_name] - and hide_registration_fields_except_tos): - - form_desc.override_field_properties( - field_name, - field_type="hidden", - label="", - instructions="", - ) - - # Hide the password field - form_desc.override_field_properties( - "password", - default="", - field_type="hidden", - required=False, - label="", - instructions="", - restrictions={} - ) - # used to identify that request is running third party social auth - form_desc.add_field( - "social_auth_provider", - field_type="hidden", - label="", - default=current_provider.name if current_provider.name else "Third Party", - required=False, - ) - class PasswordResetView(APIView): """HTTP end-point for GETting a description of the password reset form. """ @@ -997,48 +183,7 @@ class PasswordResetView(APIView): @method_decorator(ensure_csrf_cookie) def get(self, request): - """Return a description of the password reset form. - - This decouples clients from the API definition: - if the API decides to modify the form, clients won't need - to be updated. - - See `user_api.helpers.FormDescription` for examples - of the JSON-encoded form description. - - Returns: - HttpResponse - - """ - form_desc = FormDescription("post", reverse("password_change_request")) - - # Translators: This label appears above a field on the password reset - # form meant to hold the user's email address. - email_label = _(u"Email") - - # Translators: This example email address is used as a placeholder in - # a field on the password reset form meant to hold the user's email address. - email_placeholder = _(u"username@domain.com") - - # Translators: These instructions appear on the password reset form, - # immediately below a field meant to hold the user's email address. - email_instructions = _(u"The email address you used to register with {platform_name}").format( - platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME) - ) - - form_desc.add_field( - "email", - field_type="email", - label=email_label, - placeholder=email_placeholder, - instructions=email_instructions, - restrictions={ - "min_length": accounts.EMAIL_MIN_LENGTH, - "max_length": accounts.EMAIL_MAX_LENGTH, - } - ) - - return HttpResponse(form_desc.to_json(), content_type="application/json") + return HttpResponse(get_password_reset_form().to_json(), content_type="application/json") class UserViewSet(viewsets.ReadOnlyModelViewSet):