Merge pull request #22233 from edx/diana/move-password-reset
Move password reset logic and code to user_authn.
This commit is contained in:
@@ -28,45 +28,12 @@ from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from openedx.core.djangoapps.user_api import accounts as accounts_settings
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
|
||||
from student.message_types import AccountRecovery as AccountRecoveryMessage
|
||||
from student.message_types import PasswordReset
|
||||
from student.models import AccountRecovery, CourseEnrollmentAllowed, email_exists_or_retired
|
||||
from util.password_policy_validators import validate_password
|
||||
|
||||
|
||||
def send_password_reset_email_for_user(user, request, preferred_email=None):
|
||||
"""
|
||||
Send out a password reset email for the given user.
|
||||
|
||||
Arguments:
|
||||
user (User): Django User object
|
||||
request (HttpRequest): Django request object
|
||||
preferred_email (str): Send email to this address if present, otherwise fallback to user's email address.
|
||||
"""
|
||||
site = get_current_site()
|
||||
message_context = get_base_template_context(site)
|
||||
message_context.update({
|
||||
'request': request, # Used by google_analytics_tracking_pixel
|
||||
# TODO: This overrides `platform_name` from `get_base_template_context` to make the tests passes
|
||||
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
'reset_link': '{protocol}://{site}{link}?track=pwreset'.format(
|
||||
protocol='https' if request.is_secure() else 'http',
|
||||
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
|
||||
link=reverse('password_reset_confirm', kwargs={
|
||||
'uidb36': int_to_base36(user.id),
|
||||
'token': default_token_generator.make_token(user),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
msg = PasswordReset().personalize(
|
||||
recipient=Recipient(user.username, preferred_email or user.email),
|
||||
language=get_user_preference(user, LANGUAGE_KEY),
|
||||
user_context=message_context,
|
||||
)
|
||||
ace.send(msg)
|
||||
|
||||
|
||||
def send_account_recovery_email_for_user(user, request, email=None):
|
||||
"""
|
||||
Send out a account recovery email for the given user.
|
||||
|
||||
@@ -7,13 +7,6 @@ from __future__ import absolute_import
|
||||
from openedx.core.djangoapps.ace_common.message import BaseMessageType
|
||||
|
||||
|
||||
class PasswordReset(BaseMessageType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PasswordReset, self).__init__(*args, **kwargs)
|
||||
|
||||
self.options['transactional'] = True
|
||||
|
||||
|
||||
class AccountRecovery(BaseMessageType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AccountRecovery, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -63,10 +63,11 @@ from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRI
|
||||
from openedx.core.djangoapps.user_api.errors import UserAPIInternalError, UserNotFound
|
||||
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.user_authn.message_types import PasswordReset
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from student.forms import AccountCreationForm, PasswordResetFormNoActive
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info, generate_activation_email_context
|
||||
from student.message_types import EmailChange, EmailChangeConfirmation, PasswordReset, RecoveryEmailCreate
|
||||
from student.message_types import EmailChange, EmailChangeConfirmation, RecoveryEmailCreate
|
||||
from student.models import (
|
||||
AccountRecovery,
|
||||
CourseEnrollment,
|
||||
|
||||
@@ -28,51 +28,6 @@ from util.password_policy_validators import (
|
||||
)
|
||||
|
||||
|
||||
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(request):
|
||||
"""Return a description of the login form.
|
||||
|
||||
|
||||
@@ -36,8 +36,3 @@ urlpatterns = [
|
||||
user_api_views.CountryTimeZoneListView.as_view(),
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
url(r'^v1/account/password_reset/$', user_api_views.PasswordResetView.as_view(),
|
||||
name="user_api_password_reset"),
|
||||
]
|
||||
|
||||
@@ -564,65 +564,6 @@ class PreferenceUsersListViewTest(UserApiTestCase):
|
||||
self.assertEqual(len(set(all_user_uris)), 2)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class PasswordResetViewTest(UserAPITestCase):
|
||||
"""Tests of the user API's password reset endpoint. """
|
||||
|
||||
def setUp(self):
|
||||
super(PasswordResetViewTest, self).setUp()
|
||||
self.url = reverse("user_api_password_reset")
|
||||
|
||||
@ddt.data("get", "post")
|
||||
def test_auth_disabled(self, method):
|
||||
self.assertAuthDisabled(method, self.url)
|
||||
|
||||
def test_allowed_methods(self):
|
||||
self.assertAllowedMethods(self.url, ["GET", "HEAD", "OPTIONS"])
|
||||
|
||||
def test_put_not_allowed(self):
|
||||
response = self.client.put(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_delete_not_allowed(self):
|
||||
response = self.client.delete(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_patch_not_allowed(self):
|
||||
response = self.client.patch(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_password_reset_form(self):
|
||||
# Retrieve the password reset form
|
||||
response = self.client.get(self.url, content_type="application/json")
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the form description matches what we expect
|
||||
form_desc = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(form_desc["method"], "post")
|
||||
self.assertEqual(form_desc["submit_url"], reverse("password_change_request"))
|
||||
self.assertEqual(form_desc["fields"], [
|
||||
{
|
||||
"name": "email",
|
||||
"defaultValue": "",
|
||||
"type": "email",
|
||||
"required": True,
|
||||
"label": "Email",
|
||||
"placeholder": "username@domain.com",
|
||||
"instructions": u"The email address you used to register with {platform_name}".format(
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
),
|
||||
"restrictions": {
|
||||
"min_length": EMAIL_MIN_LENGTH,
|
||||
"max_length": EMAIL_MAX_LENGTH
|
||||
},
|
||||
"errorMessages": {},
|
||||
"supplementalText": "",
|
||||
"supplementalLink": "",
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase):
|
||||
|
||||
@@ -25,7 +25,6 @@ from openedx.core.djangoapps.user_api import accounts
|
||||
from openedx.core.djangoapps.user_api.accounts.api import check_account_exists
|
||||
from openedx.core.djangoapps.user_api.api import (
|
||||
get_login_session_form,
|
||||
get_password_reset_form
|
||||
)
|
||||
from openedx.core.lib.api.view_utils import require_post_params
|
||||
from openedx.core.djangoapps.user_api.models import UserPreference
|
||||
@@ -40,18 +39,6 @@ from student.helpers import AccountValidationError
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
class PasswordResetView(APIView):
|
||||
"""HTTP end-point for GETting a description of the password reset form. """
|
||||
|
||||
# This end-point is available to anonymous users,
|
||||
# so do not require authentication.
|
||||
authentication_classes = []
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def get(self, request):
|
||||
return HttpResponse(get_password_reset_form().to_json(), content_type="application/json")
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
DRF class for interacting with the User ORM object
|
||||
|
||||
15
openedx/core/djangoapps/user_authn/message_types.py
Normal file
15
openedx/core/djangoapps/user_authn/message_types.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
ACE message types for user_authn-related emails.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from openedx.core.djangoapps.ace_common.message import BaseMessageType
|
||||
|
||||
|
||||
class PasswordReset(BaseMessageType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PasswordReset, self).__init__(*args, **kwargs)
|
||||
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
self.options['transactional'] = True
|
||||
@@ -11,7 +11,7 @@ from __future__ import absolute_import
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import auto_auth, login, logout, register
|
||||
from .views import auto_auth, login, logout, password_reset, register
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -19,7 +19,6 @@ urlpatterns = [
|
||||
url(r'^create_account$', register.RegistrationView.as_view(), name='create_account'),
|
||||
url(r'^user_api/v1/account/registration/$', register.RegistrationView.as_view(),
|
||||
name="user_api_registration"),
|
||||
|
||||
# Login
|
||||
url(r'^login_post$', login.login_user, name='login_post'),
|
||||
url(r'^login_ajax$', login.login_user, name="login"),
|
||||
@@ -31,6 +30,10 @@ urlpatterns = [
|
||||
url(r'^login_refresh$', login.login_refresh, name="login_refresh"),
|
||||
|
||||
url(r'^logout$', logout.LogoutView.as_view(), name='logout'),
|
||||
|
||||
url(r'^v1/account/password_reset/$', password_reset.PasswordResetView.as_view(),
|
||||
name="user_api_password_reset"),
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ from openedx.core.djangoapps.user_api.api import get_login_session_form
|
||||
from openedx.core.djangoapps.user_authn.cookies import refresh_jwt_cookies, set_logged_in_cookies
|
||||
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
|
||||
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
|
||||
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.lib.api.view_utils import require_post_params
|
||||
from student.forms import send_password_reset_email_for_user
|
||||
from student.models import LoginFailures
|
||||
from student.views import send_reactivation_email_for_user
|
||||
from third_party_auth import pipeline, provider
|
||||
|
||||
@@ -20,10 +20,10 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
|
||||
from openedx.core.djangoapps.user_api.api import (
|
||||
get_login_session_form,
|
||||
get_password_reset_form
|
||||
)
|
||||
from openedx.core.djangoapps.user_authn.cookies import are_logged_in_cookies_set
|
||||
from openedx.core.djangoapps.user_authn.views.registration_form import RegistrationFormFactory
|
||||
from openedx.core.djangoapps.user_authn.views.password_reset import get_password_reset_form
|
||||
from openedx.features.enterprise_support.api import enterprise_customer_for_request
|
||||
from openedx.features.enterprise_support.utils import (
|
||||
handle_enterprise_cookies_for_logistration,
|
||||
|
||||
114
openedx/core/djangoapps/user_authn/views/password_reset.py
Normal file
114
openedx/core/djangoapps/user_authn/views/password_reset.py
Normal file
@@ -0,0 +1,114 @@
|
||||
""" Handles password resets logic. """
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.http import HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import int_to_base36
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from edx_ace import ace
|
||||
from edx_ace.recipient import Recipient
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from openedx.core.djangoapps.user_api import accounts
|
||||
from openedx.core.djangoapps.user_api.helpers import FormDescription
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from openedx.core.djangoapps.user_authn.message_types import PasswordReset
|
||||
|
||||
|
||||
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.
|
||||
# pylint: disable=no-member
|
||||
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
|
||||
|
||||
|
||||
class PasswordResetView(APIView):
|
||||
"""HTTP end-point for GETting a description of the password reset form. """
|
||||
|
||||
# This end-point is available to anonymous users,
|
||||
# so do not require authentication.
|
||||
authentication_classes = []
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def get(self, request):
|
||||
return HttpResponse(get_password_reset_form().to_json(), content_type="application/json")
|
||||
|
||||
|
||||
def send_password_reset_email_for_user(user, request, preferred_email=None):
|
||||
"""
|
||||
Send out a password reset email for the given user.
|
||||
|
||||
Arguments:
|
||||
user (User): Django User object
|
||||
request (HttpRequest): Django request object
|
||||
preferred_email (str): Send email to this address if present, otherwise fallback to user's email address.
|
||||
"""
|
||||
site = get_current_site()
|
||||
message_context = get_base_template_context(site)
|
||||
message_context.update({
|
||||
'request': request, # Used by google_analytics_tracking_pixel
|
||||
# TODO: This overrides `platform_name` from `get_base_template_context` to make the tests passes
|
||||
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
'reset_link': '{protocol}://{site}{link}?track=pwreset'.format(
|
||||
protocol='https' if request.is_secure() else 'http',
|
||||
site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
|
||||
link=reverse('password_reset_confirm', kwargs={
|
||||
'uidb36': int_to_base36(user.id),
|
||||
'token': default_token_generator.make_token(user),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
msg = PasswordReset().personalize(
|
||||
recipient=Recipient(user.username, preferred_email or user.email),
|
||||
language=get_user_preference(user, LANGUAGE_KEY),
|
||||
user_context=message_context,
|
||||
)
|
||||
ace.send(msg)
|
||||
@@ -28,17 +28,19 @@ from six.moves import range
|
||||
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
|
||||
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
|
||||
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.test_configuration_overrides import fake_get_value
|
||||
from student.tests.test_email import mock_render_to_string
|
||||
from student.views import SETTING_CHANGE_INITIATED, password_reset, password_reset_confirm_wrapper
|
||||
from util.password_policy_validators import create_validator_config
|
||||
from util.testing import EventTestMixin
|
||||
|
||||
from .test_configuration_overrides import fake_get_value
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
settings.ROOT_URLCONF == "lms.urls",
|
||||
@@ -53,7 +55,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
|
||||
ENABLED_CACHES = ['default']
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(ResetPasswordTests, self).setUp('student.views.management.tracker')
|
||||
self.user = UserFactory.create()
|
||||
self.user.is_active = False
|
||||
@@ -219,12 +221,12 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
password_reset(req)
|
||||
_, msg, _, _ = send_email.call_args[0]
|
||||
|
||||
reset_msg = "you requested a password reset for your user account at {}"
|
||||
reset_msg = u"you requested a password reset for your user account at {}"
|
||||
reset_msg = reset_msg.format(site_name)
|
||||
|
||||
self.assertIn(reset_msg, msg)
|
||||
|
||||
sign_off = "The {} Team".format(platform_name)
|
||||
sign_off = u"The {} Team".format(platform_name)
|
||||
self.assertIn(sign_off, msg)
|
||||
|
||||
self.assert_event_emitted(
|
||||
@@ -257,7 +259,9 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
|
||||
body = bodies[body_type]
|
||||
|
||||
reset_msg = "you requested a password reset for your user account at {}".format(fake_get_value('PLATFORM_NAME'))
|
||||
reset_msg = u"you requested a password reset for your user account at {}".format(
|
||||
fake_get_value('PLATFORM_NAME')
|
||||
)
|
||||
|
||||
self.assertIn(reset_msg, body)
|
||||
|
||||
@@ -378,6 +382,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
assert not self.user.is_active
|
||||
|
||||
def test_password_reset_normalize_password(self):
|
||||
# pylint: disable=anomalous-unicode-escape-in-string
|
||||
"""
|
||||
Tests that if we provide a not properly normalized password, it is saved using our normalization
|
||||
method of NFKC.
|
||||
@@ -481,3 +486,62 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
reset_request.user = UserFactory.create()
|
||||
|
||||
self.assertRaises(Http404, password_reset_confirm_wrapper, reset_request, self.uidb36, self.token)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
class PasswordResetViewTest(UserAPITestCase):
|
||||
"""Tests of the user API's password reset endpoint. """
|
||||
|
||||
def setUp(self):
|
||||
super(PasswordResetViewTest, self).setUp()
|
||||
self.url = reverse("user_api_password_reset")
|
||||
|
||||
@ddt.data("get", "post")
|
||||
def test_auth_disabled(self, method):
|
||||
self.assertAuthDisabled(method, self.url)
|
||||
|
||||
def test_allowed_methods(self):
|
||||
self.assertAllowedMethods(self.url, ["GET", "HEAD", "OPTIONS"])
|
||||
|
||||
def test_put_not_allowed(self):
|
||||
response = self.client.put(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_delete_not_allowed(self):
|
||||
response = self.client.delete(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_patch_not_allowed(self):
|
||||
response = self.client.patch(self.url)
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_password_reset_form(self):
|
||||
# Retrieve the password reset form
|
||||
response = self.client.get(self.url, content_type="application/json")
|
||||
self.assertHttpOK(response)
|
||||
|
||||
# Verify that the form description matches what we expect
|
||||
form_desc = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(form_desc["method"], "post")
|
||||
self.assertEqual(form_desc["submit_url"], reverse("password_change_request"))
|
||||
self.assertEqual(form_desc["fields"], [
|
||||
{
|
||||
"name": "email",
|
||||
"defaultValue": "",
|
||||
"type": "email",
|
||||
"required": True,
|
||||
"label": "Email",
|
||||
"placeholder": "username@domain.com",
|
||||
"instructions": u"The email address you used to register with {platform_name}".format(
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
),
|
||||
"restrictions": {
|
||||
"min_length": EMAIL_MIN_LENGTH,
|
||||
"max_length": EMAIL_MAX_LENGTH
|
||||
},
|
||||
"errorMessages": {},
|
||||
"supplementalText": "",
|
||||
"supplementalLink": "",
|
||||
}
|
||||
])
|
||||
Reference in New Issue
Block a user