Opened 4 years ago

Closed 4 years ago

Last modified 4 years ago

#32349 closed Bug (invalid)

unable to obtain auth template password_reset_done when using namespaced urls

Reported by: James Miller Owned by: jhabarsingh
Component: Template system Version: 3.1
Severity: Normal Keywords: password_reset
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by James Miller)

Hi,

Whilst setting up a users app, I found that if I namespaced my urls, ie :

    <django_users_app/urls.py>
    app_name = 'django_users_app'
    urlpatterns = [
        path('accounts/', include('django.contrib.auth.urls')),
	path('login/', UserLoginView.as_view(), name='login'),
        path('dashboard/', DashboardView.as_view(), name='dashboard'),
        path('register/', RegisterView.as_view(), name='register'),
        path('profile/', ProfileView.as_view(), name='profile')
    ]

Then when I completed a password reset accessed from a link on the login page, the server crapped out and informed me that:

Reverse for 'password_reset_done' not found. 'password_reset_done' is not a valid view function or pattern name.

All my other password reset views work though.

I discovered that in django.contrib.auth.views the class PasswordResetView(PasswordContextMixin, FormView): has a success_url of:

reverse_lazy('password_reset_done')

When I edit the code and replace the above with reverse_lazy('django_users_app:password_reset_done') the correct template is returned.

If I do not edit the code, but instead remove the namespace and its reference in all my files, then the correct template is returned.

I want to know if it is possible to use a namespace with a users app that references the contrib.auth password reset templates.

There is conflicting information on the internet regarding this issue and no clear indication that I can see in the django docs.

Also, in my settings file, I have the following setting defined:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Yet when I use reset the password, I do not see any email sent. I don't know if this is a related error.

Below is the traceback information from the page when password_reset_done cannot be found:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/users/accounts/password_reset/?next=/users/login/

Django Version: 3.1.5
Python Version: 3.9.1
Installed Applications:
['django_users_app',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django_email_verification',
 'captcha']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/views.py", line 222, in dispatch
    return super().dispatch(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/views.py", line 236, in form_valid
    return super().form_valid(form)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/edit.py", line 57, in form_valid
    return HttpResponseRedirect(self.get_success_url())
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/edit.py", line 51, in get_success_url
    if not self.success_url:
  File "/usr/local/lib/python3.9/site-packages/django/utils/functional.py", line 135, in __wrapper__
    res = func(*self.__args, **self.__kw)
  File "/usr/local/lib/python3.9/site-packages/django/urls/base.py", line 87, in reverse
    return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
  File "/usr/local/lib/python3.9/site-packages/django/urls/resolvers.py", line 685, in _reverse_with_prefix
    raise NoReverseMatch(msg)

Exception Type: NoReverseMatch at /users/accounts/password_reset/
Exception Value: Reverse for 'password_reset_done' not found. 'password_reset_done' is not a valid view function or pattern name.

Change History (12)

comment:1 by James Miller, 4 years ago

Description: modified (diff)

comment:2 by James Miller, 4 years ago

Description: modified (diff)

comment:3 by James Miller, 4 years ago

I have tried the following:

app_name = 'users_app'
urlpatterns = [
	path('accounts/', include(('django.contrib.auth.urls', 'users_app'), namespace='users_app')), 

But with no luck. In Django.contrib.auth.views.PasswordResetView the success_url should reference the app_name variable.

in

/usr/local/lib/python3.9/site-packages/django/urls/resolvers.py, line 685, in _reverse_with_prefix

                    (lookup_view_s, arg_msg, len(patterns), patterns)

                )

            else:

                msg = (

                    "Reverse for '%(view)s' not found. '%(view)s' is not "

                    "a valid view function or pattern name." % {'view': lookup_view_s}

                )

            raise NoReverseMatch(msg)

     the local vars are :

_prefix 	'/'

args       ()

kwargs 	{}

lookup_view  'password_reset_done'

lookup_view_s  'password_reset_done'

m 	None

msg  ("Reverse for 'password_reset_done' not found. 'password_reset_done' is not a valid view function or pattern name.')

n None

patterns []

possibilities 	[]

self  <URLResolver 'django_forum.urls' (None:None) '^/'>
Last edited 4 years ago by James Miller (previous) (diff)

comment:4 by jhabarsingh, 4 years ago

Owner: changed from nobody to jhabarsingh
Status: newassigned

comment:5 by James Miller, 4 years ago

So, I eventually subclassed the password_reset:

django_user_app/views.py

class UserPasswordResetView(PasswordResetView):
    html_email_template_name = 'registration/password_reset_email.html'
    success_url = reverse_lazy('django_users_app:password_reset_done')

    def __init__(self):
      super().__init__()

and used this line in my urls.py :

path('password_reset/', UserPasswordResetView.as_view(), name='password_reset'),

making sure to import the view at the top of urls.py.

Also, I discovered that the reason emails were not being sent in the console was down to the fact that the user must be active or the email won't send.

This leaves me with the conclusion that, in my opinion, the docs are not sufficiently clear. When user_app is defined, a namespace is set. But for a noob like myself, it is not at all clear that the user_app is a namespace.... surely it is an app_name?
Also, this still doesn't solve why it is that all the password reset views are working, except for the password_done_view, particularly if you don't read django's source code, and see that the success_url needs to be overridden in a subclass.

comment:6 by James Miller, 4 years ago

Now when I confirm the password change by clicking the email, I get a password confirm view, where I enter my two new passwords, and for some peculiar reason, honestly, when I click confirm, I get informed that there is no reverse match for the

"Reverse for 'password_reset_complete' not found. 'password_reset_complete' is not a valid view function or pattern name.

I guess I would have to subclass PasswordResetConfirmView but I am getting concerned that this is going to expose the possibility of hacking that view, since it no longer originates from contrib.auth.views.

I also tried the following in my urls.py

    path('password-reset-complete/',
         auth_views.PasswordResetCompleteView.as_view(
             template_name='users/password_reset_complete.html'
         ),
         name='password_reset_complete'),

and

	path('password-reset-confirm/',
         auth_views.PasswordResetConfirmView.as_view(
             success_url='registration/password_reset_complete.html'
         ),
         name='password_reset_complete'),

This issue is so frustrating and has made the Django experience change from one that was really easy and useful, to one that is not only frustrating and wasting time, but one that looks possibly insecure as well.

A comment that there is some possible good outcome to this ticket would be appreciated.

comment:7 by James Miller, 4 years ago

I can confirm that removing any namespace leaves the django.contrib.auth views etc working.

This should be a security concern, as it means that most django sites, if not all, will not have namespaced auth interaction. With individual per app namespaces, the chances of succesful hacking are reduced, as the hacker has to know the namespace name to be able to know the path to auth.

comment:8 by James Miller, 4 years ago

Basically, the reverse function at :

https://github.com/django/django/blob/75182a800a621b7a5b2c0a1f39a56e753b9a58ca/django/urls/base.py#L27

which is called by reverse_lazy('password_reset_complete') in django.contrib.auth.views.PasswordResetConfirmView and
which is called by reverse_lazy('password_reset_done') in django.contrib.auth.views.PasswordResetView

is not doing a decent job. It should find the correct namespace and then call the namespaced view, but it (the reverse function) only examines the string that is passed for a colon and returns a namespace if a colon exists.

*path, view = viewname.split(':') from https://github.com/django/django/blob/75182a800a621b7a5b2c0a1f39a56e753b9a58ca/django/urls/base.py#L39

so as the auth views mentioned above only pass in a string, no namespace is discovered.

I have tried both subclassing the auth views, and also passing in a success_url in the path function in my urls.py file, as well as a whole host of other tries, but nothing works.

I can monkeypatch the django code, to include the namespace in the call to reverse_lazy in django.contrib.auth.views, but I would prefer to see some sort of code that tries a url against the different namespaces, either in the reverse function or in the form_valid method. The form_valid method would require editing the super_class, and there is no need to edit the reverse function if it is working, so it would probably be better to amend the auth password views mentioned above, so that they set the success_url dynamically based upon available namespaces.

something like (in the two auth views django.contrib.auth.views.PasswordResetConfirmView and django.contrib.auth.views.PasswordResetView):

psuedo-code::::

dict = get_resolver().namespace_dict
for ns in dict:
    try:
          does ns:success_url path return valid
          if yes let success_url = ns:success_url
   except some_exception

I'll take a look tomorrow.

Last edited 4 years ago by James Miller (previous) (diff)

comment:9 by Mariusz Felisiak, 4 years ago

Resolution: invalid
Status: assignedclosed

Please check docs about Using the Django authentication system it clearly states URLs used by authentication views, e.g. PasswordResetView.success_url defaults to 'password_reset_done'. You left here many support questions (some with answers). If you have more questions about using authentication views with namespaced URLs you should use one of support channels. Trac is not the right place for such support questions.

Closing per TicketClosingReasons/UseSupportChannels.

comment:10 by James Miller, 4 years ago

Ok, so the first thing that I have tried is to add a 'namespace' attribute and then match the password path in my urls.py and pass in a namespace.

Although this works fine, I might as well simply pass in a modified success_url.

The problem arises when the PasswordResetConfirmView is called, as I cannot match the path to it in the urls.py, since the request is called from a link in an external email. If I try and match the path in my urls.py where the path to be matched is 'http://127.0.0.1:8000/users/accounts/reset/Mg/set-password/' I get an exception error.

If I don't match it and I have a namespace defined for my urls, then I get:

NoReverseMatch at /users/accounts/reset/Mg/set-password/

Reverse for 'password_reset_complete' not found. 'password_reset_complete' is not a valid view function or pattern name.

So, because I am unable to match the path and pass in a custom success_url, or some other attribute, I am left with the suggestion that I was correct last night, and that rather than pass in a custom success_url to handle a namespaced auth app, the code itself should cope with custom namespaces.

in reply to:  10 comment:11 by Mariusz Felisiak, 4 years ago

If you want to used namespaced URLs you need to subclass authentication views and changed URLs accordingly. Again, please don't use Trac as a support channel.

comment:12 by James Miller, 4 years ago

I have had no luck and so will remove the auth views from a namespace. It should work out of the box, yet it doesn't.

I tried the following in django/contrib/auth/views

from django.urls.base import get_resolver

def set_success_url(view_name):
    ns_dict = get_resolver().namespace_dict
    for ns in ns_dict:
        try:
            candidate = ns + ':' + view_name
            reverse_lazy(candidate)
            return reverse_lazy(candidate)
        except:
            pass
    return reverse_lazy(view_name)

and then setting the success_url class attribute to the function above, but the function above needs to be lazily evaluated, or reverse_lazy inside the function gets called long before any view classes are instantiated, and so a circular import error occurs.

Note: See TracTickets for help on using tickets.
Back to Top