Fixed #17209 -- Added password reset/change class-based views
Thanks Tim Graham for the review.
This commit is contained in:
parent
20d39325ca
commit
255fb99284
|
@ -289,30 +289,30 @@ class AdminSite(object):
|
|||
Handles the "change password" task -- both form display and validation.
|
||||
"""
|
||||
from django.contrib.admin.forms import AdminPasswordChangeForm
|
||||
from django.contrib.auth.views import password_change
|
||||
from django.contrib.auth.views import PasswordChangeView
|
||||
url = reverse('admin:password_change_done', current_app=self.name)
|
||||
defaults = {
|
||||
'password_change_form': AdminPasswordChangeForm,
|
||||
'post_change_redirect': url,
|
||||
'form_class': AdminPasswordChangeForm,
|
||||
'success_url': url,
|
||||
'extra_context': dict(self.each_context(request), **(extra_context or {})),
|
||||
}
|
||||
if self.password_change_template is not None:
|
||||
defaults['template_name'] = self.password_change_template
|
||||
request.current_app = self.name
|
||||
return password_change(request, **defaults)
|
||||
return PasswordChangeView.as_view(**defaults)(request)
|
||||
|
||||
def password_change_done(self, request, extra_context=None):
|
||||
"""
|
||||
Displays the "success" page after a password change.
|
||||
"""
|
||||
from django.contrib.auth.views import password_change_done
|
||||
from django.contrib.auth.views import PasswordChangeDoneView
|
||||
defaults = {
|
||||
'extra_context': dict(self.each_context(request), **(extra_context or {})),
|
||||
}
|
||||
if self.password_change_done_template is not None:
|
||||
defaults['template_name'] = self.password_change_done_template
|
||||
request.current_app = self.name
|
||||
return password_change_done(request, **defaults)
|
||||
return PasswordChangeDoneView.as_view(**defaults)(request)
|
||||
|
||||
def i18n_javascript(self, request, extra_context=None):
|
||||
"""
|
||||
|
|
|
@ -9,11 +9,13 @@ from django.contrib.auth import views
|
|||
urlpatterns = [
|
||||
url(r'^login/$', views.LoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
|
||||
url(r'^password_change/$', views.password_change, name='password_change'),
|
||||
url(r'^password_change/done/$', views.password_change_done, name='password_change_done'),
|
||||
url(r'^password_reset/$', views.password_reset, name='password_reset'),
|
||||
url(r'^password_reset/done/$', views.password_reset_done, name='password_reset_done'),
|
||||
|
||||
url(r'^password_change/$', views.PasswordChangeView.as_view(), name='password_change'),
|
||||
url(r'^password_change/done/$', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
|
||||
|
||||
url(r'^password_reset/$', views.PasswordResetView.as_view(), name='password_reset'),
|
||||
url(r'^password_reset/done/$', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm, name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', views.password_reset_complete, name='password_reset_complete'),
|
||||
views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
]
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.contrib.sites.shortcuts import get_current_site
|
|||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.shortcuts import resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.deprecation import (
|
||||
RemovedInDjango20Warning, RemovedInDjango21Warning,
|
||||
|
@ -24,7 +24,7 @@ from django.utils.deprecation import (
|
|||
from django.utils.encoding import force_text
|
||||
from django.utils.http import is_safe_url, urlsafe_base64_decode
|
||||
from django.utils.six.moves.urllib.parse import urlparse, urlunparse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
|
@ -224,6 +224,9 @@ def password_reset(request,
|
|||
extra_context=None,
|
||||
html_email_template_name=None,
|
||||
extra_email_context=None):
|
||||
warnings.warn("The password_reset() view is superseded by the "
|
||||
"class-based PasswordResetView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
if post_reset_redirect is None:
|
||||
post_reset_redirect = reverse('password_reset_done')
|
||||
else:
|
||||
|
@ -259,6 +262,9 @@ def password_reset(request,
|
|||
def password_reset_done(request,
|
||||
template_name='registration/password_reset_done.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_reset_done() view is superseded by the "
|
||||
"class-based PasswordResetDoneView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'title': _('Password reset sent'),
|
||||
}
|
||||
|
@ -282,6 +288,9 @@ def password_reset_confirm(request, uidb64=None, token=None,
|
|||
View that checks the hash in a password reset link and presents a
|
||||
form for entering a new password.
|
||||
"""
|
||||
warnings.warn("The password_reset_confirm() view is superseded by the "
|
||||
"class-based PasswordResetConfirmView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
UserModel = get_user_model()
|
||||
assert uidb64 is not None and token is not None # checked by URLconf
|
||||
if post_reset_redirect is None:
|
||||
|
@ -324,6 +333,9 @@ def password_reset_confirm(request, uidb64=None, token=None,
|
|||
def password_reset_complete(request,
|
||||
template_name='registration/password_reset_complete.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_reset_complete() view is superseded by the "
|
||||
"class-based PasswordResetCompleteView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'login_url': resolve_url(settings.LOGIN_URL),
|
||||
'title': _('Password reset complete'),
|
||||
|
@ -334,6 +346,116 @@ def password_reset_complete(request,
|
|||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
# Class-based password reset views
|
||||
# - PasswordResetView sends the mail
|
||||
# - PasswordResetDoneView shows a success message for the above
|
||||
# - PasswordResetConfirmView checks the link the user clicked and
|
||||
# prompts for a new password
|
||||
# - PasswordResetCompleteView shows a success message for the above
|
||||
|
||||
class PasswordContextMixin(object):
|
||||
extra_context = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PasswordContextMixin, self).get_context_data(**kwargs)
|
||||
context['title'] = self.title
|
||||
if self.extra_context is not None:
|
||||
context.update(self.extra_context)
|
||||
return context
|
||||
|
||||
|
||||
class PasswordResetView(PasswordContextMixin, FormView):
|
||||
email_template_name = 'registration/password_reset_email.html'
|
||||
extra_email_context = None
|
||||
form_class = PasswordResetForm
|
||||
from_email = None
|
||||
html_email_template_name = None
|
||||
subject_template_name = 'registration/password_reset_subject.txt'
|
||||
success_url = reverse_lazy('password_reset_done')
|
||||
template_name = 'registration/password_reset_form.html'
|
||||
title = _('Password reset')
|
||||
token_generator = default_token_generator
|
||||
|
||||
@method_decorator(csrf_protect)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(PasswordResetView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
opts = {
|
||||
'use_https': self.request.is_secure(),
|
||||
'token_generator': self.token_generator,
|
||||
'from_email': self.from_email,
|
||||
'email_template_name': self.email_template_name,
|
||||
'subject_template_name': self.subject_template_name,
|
||||
'request': self.request,
|
||||
'html_email_template_name': self.html_email_template_name,
|
||||
'extra_email_context': self.extra_email_context,
|
||||
}
|
||||
form.save(**opts)
|
||||
return super(PasswordResetView, self).form_valid(form)
|
||||
|
||||
|
||||
class PasswordResetDoneView(PasswordContextMixin, TemplateView):
|
||||
template_name = 'registration/password_reset_done.html'
|
||||
title = _('Password reset sent')
|
||||
|
||||
|
||||
class PasswordResetConfirmView(PasswordContextMixin, FormView):
|
||||
form_class = SetPasswordForm
|
||||
success_url = reverse_lazy('password_reset_complete')
|
||||
template_name = 'registration/password_reset_confirm.html'
|
||||
title = _('Enter new password')
|
||||
token_generator = default_token_generator
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
assert 'uidb64' in kwargs and 'token' in kwargs
|
||||
return super(PasswordResetConfirmView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_user(self, uidb64):
|
||||
UserModel = get_user_model()
|
||||
try:
|
||||
# urlsafe_base64_decode() decodes to bytestring on Python 3
|
||||
uid = force_text(urlsafe_base64_decode(uidb64))
|
||||
user = UserModel._default_manager.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
|
||||
user = None
|
||||
return user
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(PasswordResetConfirmView, self).get_form_kwargs()
|
||||
kwargs['user'] = self.get_user(self.kwargs['uidb64'])
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
return super(PasswordResetConfirmView, self).form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PasswordResetConfirmView, self).get_context_data(**kwargs)
|
||||
user = context['form'].user
|
||||
if user is not None and self.token_generator.check_token(user, self.kwargs['token']):
|
||||
context['validlink'] = True
|
||||
else:
|
||||
context.update({
|
||||
'form': None,
|
||||
'title': _('Password reset unsuccessful'),
|
||||
'validlink': False,
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
|
||||
template_name = 'registration/password_reset_complete.html'
|
||||
title = _('Password reset complete')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PasswordResetCompleteView, self).get_context_data(**kwargs)
|
||||
context['login_url'] = resolve_url(settings.LOGIN_URL)
|
||||
return context
|
||||
|
||||
|
||||
@sensitive_post_parameters()
|
||||
@csrf_protect
|
||||
@login_required
|
||||
|
@ -343,6 +465,9 @@ def password_change(request,
|
|||
post_change_redirect=None,
|
||||
password_change_form=PasswordChangeForm,
|
||||
extra_context=None):
|
||||
warnings.warn("The password_change() view is superseded by the "
|
||||
"class-based PasswordChangeView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
if post_change_redirect is None:
|
||||
post_change_redirect = reverse('password_change_done')
|
||||
else:
|
||||
|
@ -372,6 +497,9 @@ def password_change(request,
|
|||
def password_change_done(request,
|
||||
template_name='registration/password_change_done.html',
|
||||
extra_context=None):
|
||||
warnings.warn("The password_change_done() view is superseded by the "
|
||||
"class-based PasswordChangeDoneView().",
|
||||
RemovedInDjango21Warning, stacklevel=2)
|
||||
context = {
|
||||
'title': _('Password change successful'),
|
||||
}
|
||||
|
@ -379,3 +507,37 @@ def password_change_done(request,
|
|||
context.update(extra_context)
|
||||
|
||||
return TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
class PasswordChangeView(PasswordContextMixin, FormView):
|
||||
form_class = PasswordChangeForm
|
||||
success_url = reverse_lazy('password_change_done')
|
||||
template_name = 'registration/password_change_form.html'
|
||||
title = _('Password change')
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(PasswordChangeView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(PasswordChangeView, self).get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
# Updating the password logs out all other sessions for the user
|
||||
# except the current one.
|
||||
update_session_auth_hash(self.request, form.user)
|
||||
return super(PasswordChangeView, self).form_valid(form)
|
||||
|
||||
|
||||
class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
|
||||
template_name = 'registration/password_change_done.html'
|
||||
title = _('Password change successful')
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(PasswordChangeDoneView, self).dispatch(*args, **kwargs)
|
||||
|
|
|
@ -15,7 +15,10 @@ about each item can often be found in the release notes of two versions prior.
|
|||
See the :ref:`Django 1.11 release notes<deprecated-features-1.11>` for more
|
||||
details on these changes.
|
||||
|
||||
* ``contrib.auth.views.login()`` and ``logout()`` will be removed.
|
||||
* ``contrib.auth.views.login()``, ``logout()``, ``password_change()``,
|
||||
``password_change_done()``, ``password_reset()``, ``password_reset_done()``,
|
||||
``password_reset_confirm()``, and ``password_reset_complete()`` will be
|
||||
removed.
|
||||
|
||||
.. _deprecation-removed-in-2.0:
|
||||
|
||||
|
|
|
@ -2773,10 +2773,10 @@ your URLconf. Specifically, add these four patterns::
|
|||
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
url(r'^admin/password_reset/$', auth_views.password_reset, name='admin_password_reset'),
|
||||
url(r'^admin/password_reset/done/$', auth_views.password_reset_done, name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$', auth_views.password_reset_confirm, name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'),
|
||||
url(r'^admin/password_reset/$', auth_views.PasswordResetView.as_view(), name='admin_password_reset'),
|
||||
url(r'^admin/password_reset/done/$', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
|
||||
(This assumes you've added the admin at ``admin/`` and requires that you put
|
||||
the URLs starting with ``^admin/`` before the line that includes the admin app
|
||||
|
|
|
@ -2010,7 +2010,7 @@ The secret key is used for:
|
|||
* All :doc:`messages </ref/contrib/messages>` if you are using
|
||||
:class:`~django.contrib.messages.storage.cookie.CookieStorage` or
|
||||
:class:`~django.contrib.messages.storage.fallback.FallbackStorage`.
|
||||
* All :func:`~django.contrib.auth.views.password_reset` tokens.
|
||||
* All :class:`~django.contrib.auth.views.PasswordResetView` tokens.
|
||||
* Any usage of :doc:`cryptographic signing </topics/signing>`, unless a
|
||||
different key is provided.
|
||||
|
||||
|
|
|
@ -70,6 +70,17 @@ Minor features
|
|||
:class:`~django.contrib.auth.views.LogoutView` class-based views supersede the
|
||||
deprecated ``login()`` and ``logout()`` function-based views.
|
||||
|
||||
* The :class:`~django.contrib.auth.views.PasswordChangeView`,
|
||||
:class:`~django.contrib.auth.views.PasswordChangeDoneView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetDoneView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetConfirmView`, and
|
||||
:class:`~django.contrib.auth.views.PasswordResetCompleteView` class-based
|
||||
views supersede the deprecated ``password_change()``,
|
||||
``password_change_done()``, ``password_reset()``, ``password_reset_done()``,
|
||||
``password_reset_confirm()``, and ``password_reset_complete()`` function-based
|
||||
views.
|
||||
|
||||
:mod:`django.contrib.contenttypes`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -354,3 +365,14 @@ Miscellaneous
|
|||
deprecated in favor of new class-based views
|
||||
:class:`~django.contrib.auth.views.LoginView` and
|
||||
:class:`~django.contrib.auth.views.LogoutView`.
|
||||
|
||||
* ``contrib.auth``’s ``password_change()``, ``password_change_done()``,
|
||||
``password_reset()``, ``password_reset_done()``, ``password_reset_confirm()``,
|
||||
and ``password_reset_complete()`` function-based views are deprecated in favor
|
||||
of new class-based views
|
||||
:class:`~django.contrib.auth.views.PasswordChangeView`,
|
||||
:class:`~django.contrib.auth.views.PasswordChangeDoneView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetDoneView`,
|
||||
:class:`~django.contrib.auth.views.PasswordResetConfirmView`, and
|
||||
:class:`~django.contrib.auth.views.PasswordResetCompleteView`.
|
||||
|
|
|
@ -839,7 +839,7 @@ request matches the one that's computed server-side. This allows a user to log
|
|||
out all of their sessions by changing their password.
|
||||
|
||||
The default password change views included with Django,
|
||||
:func:`django.contrib.auth.views.password_change` and the
|
||||
:class:`django.contrib.auth.views.PasswordChangeView` and the
|
||||
``user_change_password`` view in the :mod:`django.contrib.auth` admin, update
|
||||
the session with the new password hash so that a user changing their own
|
||||
password won't log themselves out. If you have a custom password change view
|
||||
|
@ -917,7 +917,7 @@ your URLconf::
|
|||
from django.contrib.auth import views as auth_views
|
||||
|
||||
urlpatterns = [
|
||||
url('^change-password/$', auth_views.password_change),
|
||||
url('^change-password/$', auth_views.PasswordChangeView.as_view()),
|
||||
]
|
||||
|
||||
The views have optional arguments you can use to alter the behavior of the
|
||||
|
@ -928,24 +928,12 @@ arguments in the URLconf, these will be passed on to the view. For example::
|
|||
urlpatterns = [
|
||||
url(
|
||||
'^change-password/$',
|
||||
auth_views.password_change,
|
||||
{'template_name': 'change-password.html'}
|
||||
auth_views.PasswordChangeView.as_view(template_name='change-password.html'),
|
||||
),
|
||||
]
|
||||
|
||||
All views return a :class:`~django.template.response.TemplateResponse`
|
||||
instance, which allows you to easily customize the response data before
|
||||
rendering. A way to do this is to wrap a view in your own view::
|
||||
|
||||
from django.contrib.auth import views
|
||||
|
||||
def change_password(request):
|
||||
template_response = views.password_change(request)
|
||||
# Do something with `template_response`
|
||||
return template_response
|
||||
|
||||
For more details, see the :doc:`TemplateResponse documentation
|
||||
</ref/template-response>`.
|
||||
All views are :doc:`class-based </topics/class-based-views/index>`, which allows
|
||||
you to easily customize them by subclassing.
|
||||
|
||||
.. _all-authentication-views:
|
||||
|
||||
|
@ -963,7 +951,7 @@ implementation details see :ref:`using-the-views`.
|
|||
:class:`LoginView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``LoginView`` optional attributes. In addition, it has:
|
||||
``LoginView`` attributes. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the
|
||||
current view. See the :ref:`namespaced URL resolution strategy
|
||||
|
@ -1111,7 +1099,7 @@ implementation details see :ref:`using-the-views`.
|
|||
class-based :class:`LogoutView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``LogoutView`` optional attributes. In addition, it has:
|
||||
``LogoutView`` attributes. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the
|
||||
current view. See the :ref:`namespaced URL resolution strategy
|
||||
|
@ -1193,65 +1181,116 @@ implementation details see :ref:`using-the-views`.
|
|||
|
||||
.. function:: password_change(request, template_name='registration/password_change_form.html', post_change_redirect=None, password_change_form=PasswordChangeForm, current_app=None, extra_context=None)
|
||||
|
||||
Allows a user to change their password.
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_change`` function-based view should be replaced by the
|
||||
class-based :class:`PasswordChangeView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordChangeView`` attributes, except the ``post_change_redirect`` and
|
||||
``password_change_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. class:: PasswordChangeView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_change``
|
||||
|
||||
**Optional arguments:**
|
||||
Allows a user to change their password.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to use for
|
||||
displaying the password change form. Defaults to
|
||||
:file:`registration/password_change_form.html` if not supplied.
|
||||
|
||||
* ``post_change_redirect``: The URL to redirect to after a successful
|
||||
password change.
|
||||
* ``success_url``: The URL to redirect to after a successful password
|
||||
change.
|
||||
|
||||
* ``password_change_form``: A custom "change password" form which must
|
||||
accept a ``user`` keyword argument. The form is responsible for
|
||||
actually changing the user's password. Defaults to
|
||||
* ``form_class``: A custom "change password" form which must accept a
|
||||
``user`` keyword argument. The form is responsible for actually changing
|
||||
the user's password. Defaults to
|
||||
:class:`~django.contrib.auth.forms.PasswordChangeForm`.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
**Template context:**
|
||||
|
||||
* ``form``: The password change form (see ``form_class`` above).
|
||||
|
||||
.. function:: password_change_done(request, template_name='registration/password_change_done.html', current_app=None, extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_change_done`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordChangeDoneView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordChangeDoneView`` attributes. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
**Template context:**
|
||||
.. class:: PasswordChangeDoneView
|
||||
|
||||
* ``form``: The password change form (see ``password_change_form`` above).
|
||||
|
||||
.. function:: password_change_done(request, template_name='registration/password_change_done.html', current_app=None, extra_context=None)
|
||||
|
||||
The page shown after a user has changed their password.
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_change_done``
|
||||
|
||||
**Optional arguments:**
|
||||
The page shown after a user has changed their password.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to use.
|
||||
Defaults to :file:`registration/password_change_done.html` if not
|
||||
supplied.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. function:: password_reset(request, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', subject_template_name='registration/password_reset_subject.txt', password_reset_form=PasswordResetForm, token_generator=default_token_generator, post_reset_redirect=None, from_email=None, current_app=None, extra_context=None, html_email_template_name=None, extra_email_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset`` function-based view should be replaced by the
|
||||
class-based :class:`PasswordResetView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetView`` attributes, except the ``post_reset_redirect`` and
|
||||
``password_reset_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. function:: password_reset(request, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', subject_template_name='registration/password_reset_subject.txt', password_reset_form=PasswordResetForm, token_generator=default_token_generator, post_reset_redirect=None, from_email=None, current_app=None, extra_context=None, html_email_template_name=None, extra_email_context=None)
|
||||
.. class:: PasswordResetView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_reset``
|
||||
|
||||
Allows a user to reset their password by generating a one-time use link
|
||||
that can be used to reset the password, and sending that link to the
|
||||
|
@ -1262,7 +1301,7 @@ implementation details see :ref:`using-the-views`.
|
|||
This prevents information leaking to potential attackers. If you want to
|
||||
provide an error message in this case, you can subclass
|
||||
:class:`~django.contrib.auth.forms.PasswordResetForm` and use the
|
||||
``password_reset_form`` argument.
|
||||
``form_class`` attribute.
|
||||
|
||||
Users flagged with an unusable password (see
|
||||
:meth:`~django.contrib.auth.models.User.set_unusable_password()` aren't
|
||||
|
@ -1271,14 +1310,16 @@ implementation details see :ref:`using-the-views`.
|
|||
error message since this would expose their account's existence but no
|
||||
mail will be sent either.
|
||||
|
||||
**URL name:** ``password_reset``
|
||||
|
||||
**Optional arguments:**
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to use for
|
||||
displaying the password reset form. Defaults to
|
||||
:file:`registration/password_reset_form.html` if not supplied.
|
||||
|
||||
* ``form_class``: Form that will be used to get the email of
|
||||
the user to reset the password for. Defaults to
|
||||
:class:`~django.contrib.auth.forms.PasswordResetForm`.
|
||||
|
||||
* ``email_template_name``: The full name of a template to use for
|
||||
generating the email with the reset password link. Defaults to
|
||||
:file:`registration/password_reset_email.html` if not supplied.
|
||||
|
@ -1287,24 +1328,16 @@ implementation details see :ref:`using-the-views`.
|
|||
the subject of the email with the reset password link. Defaults
|
||||
to :file:`registration/password_reset_subject.txt` if not supplied.
|
||||
|
||||
* ``password_reset_form``: Form that will be used to get the email of
|
||||
the user to reset the password for. Defaults to
|
||||
:class:`~django.contrib.auth.forms.PasswordResetForm`.
|
||||
|
||||
* ``token_generator``: Instance of the class to check the one time link.
|
||||
This will default to ``default_token_generator``, it's an instance of
|
||||
``django.contrib.auth.tokens.PasswordResetTokenGenerator``.
|
||||
|
||||
* ``post_reset_redirect``: The URL to redirect to after a successful
|
||||
password reset request.
|
||||
* ``success_url``: The URL to redirect to after a successful password reset
|
||||
request.
|
||||
|
||||
* ``from_email``: A valid email address. By default Django uses
|
||||
the :setting:`DEFAULT_FROM_EMAIL`.
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
|
@ -1315,15 +1348,10 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``extra_email_context``: A dictionary of context data that will available
|
||||
in the email template.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
**Template context:**
|
||||
|
||||
* ``form``: The form (see ``password_reset_form`` above) for resetting
|
||||
the user's password.
|
||||
* ``form``: The form (see ``form_class`` above) for resetting the user's
|
||||
password.
|
||||
|
||||
**Email template context:**
|
||||
|
||||
|
@ -1360,65 +1388,98 @@ implementation details see :ref:`using-the-views`.
|
|||
|
||||
.. function:: password_reset_done(request, template_name='registration/password_reset_done.html', current_app=None, extra_context=None)
|
||||
|
||||
The page shown after a user has been emailed a link to reset their
|
||||
password. This view is called by default if the :func:`password_reset` view
|
||||
doesn't have an explicit ``post_reset_redirect`` URL set.
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset_done`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordResetDoneView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetDoneView`` attributes. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. class:: PasswordResetDoneView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_reset_done``
|
||||
|
||||
The page shown after a user has been emailed a link to reset their
|
||||
password. This view is called by default if the :class:`PasswordResetView`
|
||||
doesn't have an explicit ``success_url`` URL set.
|
||||
|
||||
.. note::
|
||||
|
||||
If the email address provided does not exist in the system, the user is
|
||||
inactive, or has an unusable password, the user will still be
|
||||
redirected to this view but no email will be sent.
|
||||
|
||||
**Optional arguments:**
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to use.
|
||||
Defaults to :file:`registration/password_reset_done.html` if not
|
||||
supplied.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. function:: password_reset_confirm(request, uidb64=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None, current_app=None, extra_context=None)
|
||||
|
||||
.. deprecated:: 1.11
|
||||
|
||||
The ``password_reset_confirm`` function-based view should be replaced by
|
||||
the class-based :class:`PasswordResetConfirmView`.
|
||||
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetConfirmView`` attributes, except the ``post_reset_redirect``
|
||||
and ``set_password_form`` arguments which map to the ``success_url`` and
|
||||
``form_class`` attributes of the class-based view. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. function:: password_reset_confirm(request, uidb64=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None, current_app=None, extra_context=None)
|
||||
.. class:: PasswordResetConfirmView
|
||||
|
||||
Presents a form for entering a new password.
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_reset_confirm``
|
||||
|
||||
**Optional arguments:**
|
||||
Presents a form for entering a new password.
|
||||
|
||||
* ``uidb64``: The user's id encoded in base 64. Defaults to ``None``.
|
||||
**Keyword arguments from the URL:**
|
||||
|
||||
* ``token``: Token to check that the password is valid. Defaults to
|
||||
``None``.
|
||||
* ``uidb64``: The user's id encoded in base 64.
|
||||
|
||||
* ``token``: Token to check that the password is valid.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to display the confirm
|
||||
password view. Default value is :file:`registration/password_reset_confirm.html`.
|
||||
password view. Default value is
|
||||
:file:`registration/password_reset_confirm.html`.
|
||||
|
||||
* ``token_generator``: Instance of the class to check the password. This
|
||||
will default to ``default_token_generator``, it's an instance of
|
||||
``django.contrib.auth.tokens.PasswordResetTokenGenerator``.
|
||||
|
||||
* ``set_password_form``: Form that will be used to set the password.
|
||||
Defaults to :class:`~django.contrib.auth.forms.SetPasswordForm`
|
||||
* ``form_class``: Form that will be used to set the password. Defaults to
|
||||
:class:`~django.contrib.auth.forms.SetPasswordForm`.
|
||||
|
||||
* ``post_reset_redirect``: URL to redirect after the password reset
|
||||
done. Defaults to ``None``.
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
* ``success_url``: URL to redirect after the password reset done. Defaults
|
||||
to ``'password_reset_complete'``.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
@ -1431,35 +1492,42 @@ implementation details see :ref:`using-the-views`.
|
|||
* ``validlink``: Boolean, True if the link (combination of ``uidb64`` and
|
||||
``token``) is valid or unused yet.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. function:: password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None)
|
||||
|
||||
Presents a view which informs the user that the password has been
|
||||
successfully changed.
|
||||
.. deprecated:: 1.11
|
||||
|
||||
**URL name:** ``password_reset_complete``
|
||||
The ``password_reset_complete`` function-based view should be replaced
|
||||
by the class-based :class:`PasswordResetCompleteView`.
|
||||
|
||||
**Optional arguments:**
|
||||
|
||||
* ``template_name``: The full name of a template to display the view.
|
||||
Defaults to :file:`registration/password_reset_complete.html`.
|
||||
The optional arguments of this view are similar to the class-based
|
||||
``PasswordResetCompleteView`` attributes. In addition, it has:
|
||||
|
||||
* ``current_app``: A hint indicating which application contains the current
|
||||
view. See the :ref:`namespaced URL resolution strategy
|
||||
<topics-http-reversing-url-namespaces>` for more information.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
.. deprecated:: 1.9
|
||||
|
||||
The ``current_app`` parameter is deprecated and will be removed in
|
||||
Django 2.0. Callers should set ``request.current_app`` instead.
|
||||
|
||||
.. class:: PasswordResetCompleteView
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
**URL name:** ``password_reset_complete``
|
||||
|
||||
Presents a view which informs the user that the password has been
|
||||
successfully changed.
|
||||
|
||||
**Attributes:**
|
||||
|
||||
* ``template_name``: The full name of a template to display the view.
|
||||
Defaults to :file:`registration/password_reset_complete.html`.
|
||||
|
||||
* ``extra_context``: A dictionary of context data that will be added to the
|
||||
default context data passed to the template.
|
||||
|
||||
Helper functions
|
||||
----------------
|
||||
|
||||
|
@ -1574,8 +1642,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
|
|||
defaults to ``None``, in which case a plain text email is sent.
|
||||
|
||||
By default, ``save()`` populates the ``context`` with the
|
||||
same variables that :func:`~django.contrib.auth.views.password_reset`
|
||||
passes to its email context.
|
||||
same variables that
|
||||
:class:`~django.contrib.auth.views.PasswordResetView` passes to its
|
||||
email context.
|
||||
|
||||
.. class:: SetPasswordForm
|
||||
|
||||
|
|
|
@ -0,0 +1,446 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import SESSION_KEY
|
||||
from django.contrib.auth.forms import (
|
||||
AuthenticationForm, PasswordChangeForm, SetPasswordForm,
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.http import QueryDict
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.utils import ignore_warnings, patch_logger
|
||||
from django.utils.deprecation import RemovedInDjango21Warning
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.six.moves.urllib.parse import ParseResult, urlparse
|
||||
|
||||
from .models import CustomUser, UUIDUser
|
||||
from .settings import AUTH_TEMPLATES
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[('en', 'English')],
|
||||
LANGUAGE_CODE='en',
|
||||
TEMPLATES=AUTH_TEMPLATES,
|
||||
ROOT_URLCONF='auth_tests.urls_deprecated',
|
||||
)
|
||||
class AuthViewsTestCase(TestCase):
|
||||
"""
|
||||
Helper base class for all the follow test cases.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.u1 = User.objects.create_user(username='testclient', password='password', email='testclient@example.com')
|
||||
cls.u3 = User.objects.create_user(username='staff', password='password', email='staffmember@example.com')
|
||||
|
||||
def login(self, username='testclient', password='password'):
|
||||
response = self.client.post('/login/', {
|
||||
'username': username,
|
||||
'password': password,
|
||||
})
|
||||
self.assertIn(SESSION_KEY, self.client.session)
|
||||
return response
|
||||
|
||||
def logout(self):
|
||||
response = self.client.get('/admin/logout/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(SESSION_KEY, self.client.session)
|
||||
|
||||
def assertFormError(self, response, error):
|
||||
"""Assert that error is found in response.context['form'] errors"""
|
||||
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
|
||||
self.assertIn(force_text(error), form_errors)
|
||||
|
||||
def assertURLEqual(self, url, expected, parse_qs=False):
|
||||
"""
|
||||
Given two URLs, make sure all their components (the ones given by
|
||||
urlparse) are equal, only comparing components that are present in both
|
||||
URLs.
|
||||
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
|
||||
This is useful if you don't want the order of parameters to matter.
|
||||
Otherwise, the query strings are compared as-is.
|
||||
"""
|
||||
fields = ParseResult._fields
|
||||
|
||||
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
|
||||
if parse_qs and attr == 'query':
|
||||
x, y = QueryDict(x), QueryDict(y)
|
||||
if x and y and x != y:
|
||||
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class PasswordResetTest(AuthViewsTestCase):
|
||||
|
||||
def test_email_not_found(self):
|
||||
"""If the provided email is not registered, don't raise any error but
|
||||
also don't send any email."""
|
||||
response = self.client.get('/password_reset/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_email_found(self):
|
||||
"Email is sent if a valid email address is provided for password reset"
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn("http://", mail.outbox[0].body)
|
||||
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
|
||||
# optional multipart text/html email has been added. Make sure original,
|
||||
# default functionality is 100% the same
|
||||
self.assertFalse(mail.outbox[0].message().is_multipart())
|
||||
|
||||
def test_extra_email_context(self):
|
||||
"""
|
||||
extra_email_context should be available in the email template context.
|
||||
"""
|
||||
response = self.client.post(
|
||||
'/password_reset_extra_email_context/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn('Email email context: "Hello!"', mail.outbox[0].body)
|
||||
|
||||
def test_html_mail_template(self):
|
||||
"""
|
||||
A multipart email with text/plain and text/html is sent
|
||||
if the html_email_template parameter is passed to the view
|
||||
"""
|
||||
response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
message = mail.outbox[0].message()
|
||||
self.assertEqual(len(message.get_payload()), 2)
|
||||
self.assertTrue(message.is_multipart())
|
||||
self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
|
||||
self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
|
||||
self.assertNotIn('<html>', message.get_payload(0).get_payload())
|
||||
self.assertIn('<html>', message.get_payload(1).get_payload())
|
||||
|
||||
def test_email_found_custom_from(self):
|
||||
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
|
||||
response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
def test_poisoned_http_host(self):
|
||||
"Poisoned HTTP_HOST headers can't be used for reset emails"
|
||||
# This attack is based on the way browsers handle URLs. The colon
|
||||
# should be used to separate the port, but if the URL contains an @,
|
||||
# the colon is interpreted as part of a username for login purposes,
|
||||
# making 'evil.com' the request domain. Since HTTP_HOST is used to
|
||||
# produce a meaningful reset URL, we need to be certain that the
|
||||
# HTTP_HOST header isn't poisoned. This is done as a check when get_host()
|
||||
# is invoked, but we check here as a practical consequence.
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post(
|
||||
'/password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
def test_poisoned_http_host_admin_site(self):
|
||||
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post(
|
||||
'/admin_password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
def _read_signup_email(self, email):
|
||||
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
|
||||
self.assertIsNotNone(urlmatch, "No URL found in sent email")
|
||||
return urlmatch.group(), urlmatch.groups()[0]
|
||||
|
||||
def test_confirm_valid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
# redirect to a 'complete' page:
|
||||
self.assertContains(response, "Please enter your new password")
|
||||
|
||||
def test_confirm_invalid(self):
|
||||
url, path = self._test_confirm_start()
|
||||
# Let's munge the token in the path, but keep the same length,
|
||||
# in case the URLconf will reject a different length.
|
||||
path = path[:-5] + ("0" * 4) + path[-1]
|
||||
|
||||
response = self.client.get(path)
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_invalid_user(self):
|
||||
# Ensure that we get a 200 response for a non-existent user, not a 404
|
||||
response = self.client.get('/reset/123456/1-1/')
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_overflow_user(self):
|
||||
# Ensure that we get a 200 response for a base36 user id that overflows int
|
||||
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_invalid_post(self):
|
||||
# Same as test_confirm_invalid, but trying
|
||||
# to do a POST instead.
|
||||
url, path = self._test_confirm_start()
|
||||
path = path[:-5] + ("0" * 4) + path[-1]
|
||||
|
||||
self.client.post(path, {
|
||||
'new_password1': 'anewpassword',
|
||||
'new_password2': ' anewpassword',
|
||||
})
|
||||
# Check the password has not been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assertTrue(not u.check_password("anewpassword"))
|
||||
|
||||
def test_confirm_complete(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
# Check the password has been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assertTrue(u.check_password("anewpassword"))
|
||||
|
||||
# Check we can't use the link again
|
||||
response = self.client.get(path)
|
||||
self.assertContains(response, "The password reset link was invalid")
|
||||
|
||||
def test_confirm_different_passwords(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'x'})
|
||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
||||
|
||||
def test_reset_redirect_default(self):
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/done/')
|
||||
|
||||
def test_reset_custom_redirect(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_reset_custom_redirect_named(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/named/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
def test_confirm_redirect_default(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/reset/done/')
|
||||
|
||||
def test_confirm_redirect_custom(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_confirm_redirect_custom_named(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/named/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
def test_confirm_display_user_from_form(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
|
||||
# #16919 -- The ``password_reset_confirm`` view should pass the user
|
||||
# object to the ``SetPasswordForm``, even on GET requests.
|
||||
# For this test, we render ``{{ form.user }}`` in the template
|
||||
# ``registration/password_reset_confirm.html`` so that we can test this.
|
||||
username = User.objects.get(email='staffmember@example.com').username
|
||||
self.assertContains(response, "Hello, %s." % username)
|
||||
|
||||
# However, the view should NOT pass any user object on a form if the
|
||||
# password reset link was invalid.
|
||||
response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/')
|
||||
self.assertContains(response, "Hello, .")
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.CustomUser')
|
||||
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
||||
user_email = 'staffmember@example.com'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.u1 = CustomUser.custom_objects.create(
|
||||
email='staffmember@example.com',
|
||||
date_of_birth=datetime.date(1976, 11, 8),
|
||||
)
|
||||
cls.u1.set_password('password')
|
||||
cls.u1.save()
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
response = self.client.post('/password_reset/', {'email': self.user_email})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
def _read_signup_email(self, email):
|
||||
urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
|
||||
self.assertIsNotNone(urlmatch, "No URL found in sent email")
|
||||
return urlmatch.group(), urlmatch.groups()[0]
|
||||
|
||||
def test_confirm_valid_custom_user(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.get(path)
|
||||
# redirect to a 'complete' page:
|
||||
self.assertContains(response, "Please enter your new password")
|
||||
# then submit a new password
|
||||
response = self.client.post(path, {
|
||||
'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword',
|
||||
})
|
||||
self.assertRedirects(response, '/reset/done/')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.UUIDUser')
|
||||
class UUIDUserPasswordResetTest(CustomUserPasswordResetTest):
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# instead of fixture
|
||||
UUIDUser.objects.create_user(
|
||||
email=self.user_email,
|
||||
username='foo',
|
||||
password='foo',
|
||||
)
|
||||
return super(UUIDUserPasswordResetTest, self)._test_confirm_start()
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class ChangePasswordTest(AuthViewsTestCase):
|
||||
|
||||
def fail_login(self, password='password'):
|
||||
response = self.client.post('/login/', {
|
||||
'username': 'testclient',
|
||||
'password': password,
|
||||
})
|
||||
self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % {
|
||||
'username': User._meta.get_field('username').verbose_name
|
||||
})
|
||||
|
||||
def logout(self):
|
||||
self.client.get('/logout/')
|
||||
|
||||
def test_password_change_fails_with_invalid_old_password(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'donuts',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect'])
|
||||
|
||||
def test_password_change_fails_with_mismatched_passwords(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'donuts',
|
||||
})
|
||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
||||
|
||||
def test_password_change_succeeds(self):
|
||||
self.login()
|
||||
self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.fail_login()
|
||||
self.login(password='password1')
|
||||
|
||||
def test_password_change_done_succeeds(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
@override_settings(LOGIN_URL='/login/')
|
||||
def test_password_change_done_fails(self):
|
||||
response = self.client.get('/password_change/done/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_default(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_custom(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_password_change_redirect_custom_named(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/named/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango21Warning)
|
||||
class SessionAuthenticationTests(AuthViewsTestCase):
|
||||
def test_user_password_change_updates_session(self):
|
||||
"""
|
||||
#21649 - Ensure contrib.auth.views.password_change updates the user's
|
||||
session auth hash after a password change so the session isn't logged out.
|
||||
"""
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
# if the hash isn't updated, retrieving the redirection page will fail.
|
||||
self.assertRedirects(response, '/password_change/done/')
|
|
@ -2,8 +2,8 @@ from django.contrib.auth import authenticate
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.contrib.auth.views import (
|
||||
password_change, password_change_done, password_reset,
|
||||
password_reset_complete, password_reset_confirm, password_reset_done,
|
||||
PasswordChangeDoneView, PasswordChangeView, PasswordResetCompleteView,
|
||||
PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView,
|
||||
)
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.utils.encoding import force_bytes, force_text
|
||||
|
@ -20,35 +20,35 @@ class AuthTemplateTests(TestCase):
|
|||
request = rf.get('/somepath/')
|
||||
request.user = user
|
||||
|
||||
response = password_reset(request, post_reset_redirect='dummy/')
|
||||
response = PasswordResetView.as_view(success_url='dummy/')(request)
|
||||
self.assertContains(response, '<title>Password reset</title>')
|
||||
self.assertContains(response, '<h1>Password reset</h1>')
|
||||
|
||||
response = password_reset_done(request)
|
||||
response = PasswordResetDoneView.as_view()(request)
|
||||
self.assertContains(response, '<title>Password reset sent</title>')
|
||||
self.assertContains(response, '<h1>Password reset sent</h1>')
|
||||
|
||||
# password_reset_confirm invalid token
|
||||
response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/')
|
||||
# PasswordResetConfirmView invalid token
|
||||
response = PasswordResetConfirmView.as_view(success_url='dummy/')(request, uidb64='Bad', token='Bad')
|
||||
self.assertContains(response, '<title>Password reset unsuccessful</title>')
|
||||
self.assertContains(response, '<h1>Password reset unsuccessful</h1>')
|
||||
|
||||
# password_reset_confirm valid token
|
||||
# PasswordResetConfirmView valid token
|
||||
default_token_generator = PasswordResetTokenGenerator()
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb64 = force_text(urlsafe_base64_encode(force_bytes(user.pk)))
|
||||
response = password_reset_confirm(request, uidb64, token, post_reset_redirect='dummy/')
|
||||
response = PasswordResetConfirmView.as_view(success_url='dummy/')(request, uidb64=uidb64, token=token)
|
||||
self.assertContains(response, '<title>Enter new password</title>')
|
||||
self.assertContains(response, '<h1>Enter new password</h1>')
|
||||
|
||||
response = password_reset_complete(request)
|
||||
response = PasswordResetCompleteView.as_view()(request)
|
||||
self.assertContains(response, '<title>Password reset complete</title>')
|
||||
self.assertContains(response, '<h1>Password reset complete</h1>')
|
||||
|
||||
response = password_change(request, post_change_redirect='dummy/')
|
||||
response = PasswordChangeView.as_view(success_url='dummy/')(request)
|
||||
self.assertContains(response, '<title>Password change</title>')
|
||||
self.assertContains(response, '<h1>Password change</h1>')
|
||||
|
||||
response = password_change_done(request)
|
||||
response = PasswordChangeDoneView.as_view()(request)
|
||||
self.assertContains(response, '<title>Password change successful</title>')
|
||||
self.assertContains(response, '<h1>Password change successful</h1>')
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.contrib.messages.api import info
|
|||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.template import RequestContext, Template
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
|
||||
|
@ -67,23 +68,29 @@ urlpatterns = auth_urlpatterns + [
|
|||
url(r'^logout/next_page/$', views.LogoutView.as_view(next_page='/somewhere/')),
|
||||
url(r'^logout/next_page/named/$', views.LogoutView.as_view(next_page='password_reset')),
|
||||
url(r'^remote_user/$', remote_user_auth_view),
|
||||
url(r'^password_reset_from_email/$', views.password_reset, dict(from_email='staffmember@example.com')),
|
||||
url(r'^password_reset_extra_email_context/$', views.password_reset,
|
||||
dict(extra_email_context=dict(greeting='Hello!'))),
|
||||
url(r'^password_reset/custom_redirect/$', views.password_reset, dict(post_reset_redirect='/custom/')),
|
||||
url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')),
|
||||
url(r'^password_reset/html_email_template/$', views.password_reset,
|
||||
dict(html_email_template_name='registration/html_password_reset_email.html')),
|
||||
|
||||
url(r'^password_reset_from_email/$',
|
||||
views.PasswordResetView.as_view(from_email='staffmember@example.com')),
|
||||
url(r'^password_reset_extra_email_context/$',
|
||||
views.PasswordResetView.as_view(extra_email_context=dict(greeting='Hello!'))),
|
||||
url(r'^password_reset/custom_redirect/$',
|
||||
views.PasswordResetView.as_view(success_url='/custom/')),
|
||||
url(r'^password_reset/custom_redirect/named/$',
|
||||
views.PasswordResetView.as_view(success_url=reverse_lazy('password_reset'))),
|
||||
url(r'^password_reset/html_email_template/$',
|
||||
views.PasswordResetView.as_view(
|
||||
html_email_template_name='registration/html_password_reset_email.html'
|
||||
)),
|
||||
url(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
dict(post_reset_redirect='/custom/')),
|
||||
views.PasswordResetConfirmView.as_view(success_url='/custom/')),
|
||||
url(r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
dict(post_reset_redirect='password_reset')),
|
||||
url(r'^password_change/custom/$', views.password_change, dict(post_change_redirect='/custom/')),
|
||||
url(r'^password_change/custom/named/$', views.password_change, dict(post_change_redirect='password_reset')),
|
||||
url(r'^login_required/$', login_required(views.password_reset)),
|
||||
url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')),
|
||||
views.PasswordResetConfirmView.as_view(success_url=reverse_lazy('password_reset'))),
|
||||
url(r'^password_change/custom/$',
|
||||
views.PasswordChangeView.as_view(success_url='/custom/')),
|
||||
url(r'^password_change/custom/named/$',
|
||||
views.PasswordChangeView.as_view(success_url=reverse_lazy('password_reset'))),
|
||||
url(r'^login_required/$', login_required(views.PasswordResetView.as_view())),
|
||||
url(r'^login_required_login_url/$', login_required(views.PasswordResetView.as_view(), login_url='/somewhere/')),
|
||||
|
||||
url(r'^auth_processor_no_attr_access/$', auth_processor_no_attr_access),
|
||||
url(r'^auth_processor_attr_access/$', auth_processor_attr_access),
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
# special urls for deprecated function-based views
|
||||
urlpatterns = [
|
||||
url(r'^login/$', views.login, name='login'),
|
||||
url(r'^logout/$', views.logout, name='logout'),
|
||||
url(r'^password_change/$', views.password_change, name='password_change'),
|
||||
url(r'^password_change/done/$', views.password_change_done, name='password_change_done'),
|
||||
url(r'^password_reset/$', views.password_reset, name='password_reset'),
|
||||
url(r'^password_reset/done/$', views.password_reset_done, name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm, name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', views.password_reset_complete, name='password_reset_complete'),
|
||||
|
||||
url(r'^password_reset_from_email/$', views.password_reset, dict(from_email='staffmember@example.com')),
|
||||
url(r'^password_reset_extra_email_context/$', views.password_reset,
|
||||
dict(extra_email_context=dict(greeting='Hello!'))),
|
||||
url(r'^password_reset/custom_redirect/$', views.password_reset, dict(post_reset_redirect='/custom/')),
|
||||
url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')),
|
||||
url(r'^password_reset/html_email_template/$', views.password_reset,
|
||||
dict(html_email_template_name='registration/html_password_reset_email.html')),
|
||||
url(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
dict(post_reset_redirect='/custom/')),
|
||||
url(r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.password_reset_confirm,
|
||||
dict(post_reset_redirect='password_reset')),
|
||||
url(r'^password_change/custom/$', views.password_change, dict(post_change_redirect='/custom/')),
|
||||
url(r'^password_change/custom/named/$', views.password_change, dict(post_change_redirect='password_reset')),
|
||||
url(r'^login_required/$', login_required(views.password_reset)),
|
||||
url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')),
|
||||
|
||||
# This line is only required to render the password reset with is_admin=True
|
||||
url(r'^admin/', admin.site.urls),
|
||||
]
|
Loading…
Reference in New Issue