Django 1.9.2 "reverse" error during password reset attempt - django

My app sits on Django 1.9.2 and Django REST Framework 3.3.2, with a single page app on the front-end. I must admit I'm new to Django but the docs are unmatched.
I'm trying to implement a custom PasswordResetForm view. My strategy is as follow:
User uses on a front-end form that POSTs data (email string) to an API endpoint (api/v1/password/reset) when she wants to reset her password.
If email is found in DB, send an email and return a successful response.
For part 1, here's the relevant code:
# urls.py
url(r'^api/v1/password/reset/?$', PasswordResetView.as_view(), name='password-reset-link')
-
# views.py
class PasswordResetView(GenericAPIView):
serializer_class = PasswordResetSerializer
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
# Use DRF serializer to validate data.
serializer = self.get_serializer(data=request.data)
# Ensure data is valid before proceeding.
if serializer.is_valid() == False:
return Response('error': serializer.errors, 'message': 'Error while attempting to reset password'}, status.HTTP_400_BAD_REQUEST)
else:
# Ensure the user exists.
existing_user = get_user_model().objects.filter(
email=serializer.validated_data.get('email')
)
if not existing_user:
return Response({'error': {'code': 'email_not_in_db'}}, status.HTTP_404_NOT_FOUND)
# Validated data fed to a child class of PasswordResetForm.
form = UserForgotPasswordForm(serializer.validated_data)
if form.is_valid():
path = os.path.join(os.path.dirname(os.path.abspath(__file__ + '../../')), 'templates/registration/password_reset_email.html')
try:
# Save form, effectively attempting to trigger mail sending.
# Unfortunately an exception gets thrown!
form.save(from_email='no-reply#abc.xyz', email_template_name=path, request=request)
return Response({'message': 'Password reset request sent'}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e), 'message': 'Error while attempting to reset password'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
# forms.py
class UserForgotPasswordForm(PasswordResetForm):
email = forms.EmailField(required=True, max_length=254)
class Meta:
model = CustomUser
fields = ('email',)
-
# my_app/templates/registration/password_reset_email.html
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password-reset-link' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% endautoescape %}
-
But I get the following error:
Reverse for 'password-reset-link' with arguments '()' and keyword arguments '{'uidb64': b'MTI1', 'token': '4g9-f370fd6ee48d90a40b67'}' not found. 1 pattern(s) tried: ['api/v1/password/reset/?$']
Any idea why? What am I missing? Thanks for your help.

In the url in your html you are passing a token and a uidb64:
{{ protocol }}://{{ domain }}{% url 'password-reset-link' uidb64=uid token=token %}
However, in your actual url, you do not reference the token or uidb64:
url(r'^api/v1/password/reset/?$', PasswordResetView.as_view(), name='password-reset-link')
I would recommend changing your url to:
url(r'^api/v1/password/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', PasswordResetView.as_view(), name='password-reset-link')

Your URL named password-reset-link doesn't support the keyword arguments uidb64 and token that you're sending it. Those keyword arguments aren't just extra data, they need to match somewhere in the URL pattern.
According to the error, your URL pattern is:
api/v1/password/reset/?$
In order to support what you want you'd need to put two named arguments called <uidb64> and <token>. For example:
api/v1/password/reset/(?P<uidb64>[a-zA-Z0-9]+)/(?P<token>[a-zA-Z0-9]+)/?$
You'll have to adjust your regex to support the uidb64 and token formats.

Related

django adding quotes on subsequent submits

Here's the goal: when the user submits the form, use one view to send the submitted data to the database, then redirect back to the form, but with the data pre-populated. This is mostly working, but something about my implementation is wrapping extra quotes around the string. For now, I'm just using a super-simple form, btw. I enter Billy, and the pre-pop is: "Billy", if I click submit again, it comes back as: "\"Billy\"", then "\"\\\"Billy\\\"\"", and so on (as far as I have tested, anyways.
relevant views are:
def editUsers(request):
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = usersForm(request.POST)
# check whether it's valid:
# process the data in form.cleaned_data as required
# redirect to a new URL:
name = json.dumps(form.data['user_name'])
request.session['editUserName'] = name
# call out to limboLogic.py to update values
test = name
return redirect('../users')
# if a GET (or any other method) we'll create a blank form
else:
return redirect('../users')
from .forms import *
def users(request):
form = None
if 'editUserName' not in request.session:
# create a blank form
form = usersForm()
else:
# form = equipmentForm(initial='jim') - used to make sure I was branching the if/else correctly
form = usersForm(initial={'user_name':request.session['editUserName']}, auto_id=False) #limboLogic.GetUserInfo(name))
return render(request, 'limboHtml/UserManagement.html', {'form': form})
form is simply:
class usersForm(forms.Form):
user_name = forms.CharField(label='New User\'s name', max_length=100)
and the template is:
{% extends "base.html" %}
{% block content %}
<div class="row">
<p>This is the user management page</p><br>
<form action="/edit/users.html" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="OK">
</form>
<br><p class="bold">This is below the form</p>
</div>
{% endblock %}
thoughts?
I can't quite say what the intracies are here, but the problem involves the fact that I was using a json class. I used this site as a guide and managed to fix the problem. note that the key aspect is inside the second if:
name = form.cleaned_data['user_name'] works fine,
name = json.dumps(form.data['user_name']) does not
the whole function as it now stands:
def editUsers(request):
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = usersForm(request.POST)
# check whether it's valid:
# process the data in form.cleaned_data as required
# redirect to a new URL:
if form.is_valid():
name = form.cleaned_data['user_name']
# name = json.dumps(form.data['user_name'])
request.session['editUserName'] = name
# call out to limboLogic.py to update values
test = name
return redirect('../users')
# if a GET (or any other method) we'll create a blank form
return redirect('../users')

Restrict access to Django template

I need to restrict access to a template in Django. This is the scenario:
A guest user uses a form
If the form is validated and fine send the user to the example.com/success/ url.
If the guest user tries to send that link example.com/success to a friend. The friend will see that page as a 404.
I have no clue how to achieve this. Any ideas?
Instead of going to a different URL (/success/), you could just show something different when the form was properly filled. For example, in your view:
def my_view(request, ...):
form = ...
show_success = False
if ... post method ...:
if form.is_valid():
... save etc. ...
show_success = True
return render(request, ..., {'show_success': show_success})
In your template:
{% if show_success %}
Success message here
{% else %}
Form here
{% endif %}

How to customize django rest auth password reset email content/template

In django rest_auth password reset, default email content look like following:-
You're receiving this email because you requested a password reset for your user account at localhost:8000.
Please go to the following page and choose a new password:
http://localhost:8000/api/reset/Kih/89a-23809182347689312b123/
Your username, in case you've forgotten: test
Thanks for using our site!
The localhost:8000 team
How to customize content of this email ?
I recently needed to implement the same thing in one of my projects and could not find a thorough answer anywhere.
So I'm leaving my solution here for anyone who needs it in the future.
Expanding on mariodev's suggestion:
1. Subclass PasswordResetSerializer and override save method.
yourproject_app/serializers.py
from django.conf import settings
from rest_auth.serializers import PasswordResetSerializer as _PasswordResetSerializer
class PasswordResetSerializer(_PasswordResetSerializer):
def save(self):
request = self.context.get('request')
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
###### USE YOUR TEXT FILE ######
'email_template_name': 'example_message.txt',
'request': request,
}
self.reset_form.save(**opts)
2. Configure AUTH_USER_MODEL
yourproject/settings.py
###### USE YOUR USER MODEL ######
AUTH_USER_MODEL = 'yourproject_app.ExampleUser'
3. Connect custom PasswordResetSerializer to override default
yourproject/settings.py
REST_AUTH_SERIALIZERS = {
'PASSWORD_RESET_SERIALIZER':
'yourproject_app.serializers.PasswordResetSerializer',
}
4. Add the path to the directory where your custom email message text file is located to TEMPLATES
yourproject/settings.py
TEMPLATES = [
{
...
'DIRS': [os.path.join(BASE_DIR, 'yourproject/templates')],
...
}
]
5. Write custom email message (default copied from Django)
yourproject/templates/example_message.txt
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset
for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}
UPDATE: This solution was written for an older version of django-rest-auth (v0.6.0). As I can tell from the comments, it seems there have been some updates made to the source package that more readily handle custom email templates out-of-box. It is always better to use methods defined in a package rather than overriding them like in my solution. Though once a necessity, it may not be so any longer.
You can inherit PasswordResetSerializer and override the get_email_options method. For example:
from rest_auth.serializers import PasswordResetSerializer
class CustomPasswordResetSerializer(PasswordResetSerializer):
def get_email_options(self):
return {
'subject_template_name': 'registration/password_reset_subject.txt',
'email_template_name': 'registration/password_reset_message.txt',
'html_email_template_name': 'registration/'
'password_reset_message.html',
'extra_email_context': {
'pass_reset_obj': self.your_extra_reset_obj
}
}
You need to hook up your own reset password serializer (PASSWORD_RESET_SERIALIZER) with customized save method.
(ref: https://github.com/Tivix/django-rest-auth/blob/v0.6.0/rest_auth/serializers.py#L123)
Unfortunately you need to override the whole save method, due to how the e-mail options are used. We we'll make it a bit more flexible in the next release (0.7.0)
A simple solution is
Create over templates directory:
-templates
-registration
password_reset_email.html
with content you want.
Django rest-auth use django.contrib.auth templates.
So for the dj-rest-auth, this is how I did it:
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.urls.base import reverse
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.utils import user_pk_to_url_str, user_username
from allauth.utils import build_absolute_uri
from dj_rest_auth.forms import AllAuthPasswordResetForm
from dj_rest_auth.serializers import PasswordResetSerializer
class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm):
def save(self, request, **kwargs):
current_site = get_current_site(request)
email = self.cleaned_data['email']
token_generator = kwargs.get('token_generator',
default_token_generator)
for user in self.users:
temp_key = token_generator.make_token(user)
# save it to the password reset model
# password_reset = PasswordReset(user=user, temp_key=temp_key)
# password_reset.save()
# send the password reset email
path = reverse(
'password_reset_confirm',
args=[user_pk_to_url_str(user), temp_key],
)
url = build_absolute_uri(None, path) # PASS NONE INSTEAD OF REQUEST
context = {
'current_site': current_site,
'user': user,
'password_reset_url': url,
'request': request,
}
if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
context['username'] = user_username(user)
get_adapter(request).send_mail('account/email/password_reset_key',
email, context)
return self.cleaned_data['email']
class CustomPasswordResetSerializer(PasswordResetSerializer):
#property
def password_reset_form_class(self):
return CustomAllAuthPasswordResetForm
# settings.py
REST_AUTH_SERIALIZERS = {
'PASSWORD_RESET_SERIALIZER':
'api.users.api.serializers.CustomPasswordResetSerializer',
}
By passing None to build_absolute_uri instead of the original request, it will take the value you have in django.contrib.sites module with SITE_ID=1. So whatever you have defined as your domain in the Django admin will now be the domain in the reset URL. This makes sense if you want to have the password reset URL point to your frontend, that might be a React application running on a different domain.
Edit:
My PR regarding this issue was merged, with the next release this will be possible to set in your settings. Checkout the docs for dj-rest-auth to see which setting you need to set.
if you want to use a html email template, an update to Brian's answer would be to add
'html_email_template_name': 'account/email/example_message.html',
just below
###### USE YOUR TEXT FILE ######
'email_template_name': 'account/email/example_message.txt',
this way you can the email with a html template
You can see why this happens by inspecting the send_mail method of the PasswordResetForm class
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
def send_mail(self, subject_template_name, email_template_name,
context, from_email, to_email, html_email_template_name=None):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
email_message.send()```
Create directory with path as following in your template folder
templates/admin/registration/
Now copy all files in django/contrib/admin/templates/registration/ into this directory you just created. You can find this directory where you have installed django. In linux, it can be find here
/usr/local/lib/python2.7/dist-packages/django/contrib/admin/templates/registration
You will need root priviliges for accessing this.
Now when you will send email, templates in you just copied in your project will be used.
This link might be helpful. With it I was able to find where the email templates were and how to customize them.
You can find the info at the bottom of the page under
Customize the email message
http://www.sarahhagstrom.com/2013/09/the-missing-django-allauth-tutorial/#Customize_the_email_message

Django Password Reset through TastyPie API causing tags in email to render incorrectly

I've built a password reset function for a django system that is exposed through a tastypie api. The rendering process looks like this
form = PasswordResetForm({'email': reset_email})
form.full_clean()
form.save({
'use_https': request.is_secure(),
'token_generator': default_token_generator,
'from_email': settings.DEFAULT_FROM_EMAIL,
'email_template_name': 'registration/password_reset_email.html',
'request': request
})
return self.create_response(request, { 'success': True })
I've used the standard Django password reset email template. When I call the endpoint, the http request gets rendered in some of the custom tags e.g
Markup:
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
Expected result:
Please go to the following page and choose a new password:
httt://www.myurl.com/password-reset
"Your username, in case you've forgotten: User Name
Actual result
Please go to the following page and choose a new password:
httt://{'request': , POST:, COOKIES:{}, META:{'CONTENT_LENGTH': '48', (...)
"Your username, in case you've forgotten: User Name
It strikes me as odd that the model based template tags render correctly, but the global ones (e.g. 'Site_name') and the reset url & token seem to break. I'm sure that's a good clue as to the cause of the problem, but I don't know where to go next.
Would be great if anyone can help.
Thanks

Authentication using Email and resulted routing problems

I'm trying to Authenticate users by their emails instead of username.
I'm using django-userena and by using its Docs,etc. I set almost anything that is needed. Something like : USERENA_WITHOUT_USERNAMES = True in its setting, etc.
But after signing up, I've faced a chain of problems. like trying to pass my username in the url for authentication, signup completion problems, etc.
I changed some view functions that need username as an argument, but this method neither solved my problem , nor is a correct (and maybe secure) way to do it.
for instance, by routing to this URL http://127.0.0.1:8000/accounts/signup/complete/ (after $ ./manage.py check_permissions ) I get this error:
global name 'username' is not defined
/userena/views.py in directto_user_template
user = get_object_or_404(User, username_iexact=username)
Is there anything that I'm missing ??
UPDATE:
Here is the output that I get:
Caught NoReverseMatch while rendering: Reverse for 'userena_activate'
with arguments '('xyz#xyz.com',
'70b60d1d97015e03ba8d57f31e4c7ff14d6ab753')' and keyword arguments
'{}' not found.
It's clear that userena tries to the email as username with URL :
userena/templates/userena/emails/activation_email_message.txt, error at line 8
1 {% load i18n %}{% autoescape off %}
2 {% if not without_usernames %}{% blocktrans with user.username as username %}Dear {{ username }},{% endblocktrans %}
3 {% endif %}
4 {% blocktrans with site.name as site %}Thank you for signing up at {{ site }}.{% endblocktrans %}
5
6 {% trans "To activate your account you should click on the link below:" %}
7
8 {{ protocol }}://{{ site.domain }}{% url userena_activate user.username activation_key %}
9
10 {% trans "Thanks for using our site!" %}
11
12 {% trans "Sincerely" %},
13 {{ site.name }}
14 {% endautoescape %}
UPDATE 2:
Alright . by reading the source code for SignupFormOnlyEmail class form, it says that a random username is generated automatically.
class SignupFormOnlyEmail(SignupForm):
"""
Form for creating a new user account but not needing a username.
This form is an adaptation of :class:`SignupForm`. It's used when
``USERENA_WITHOUT_USERNAME`` setting is set to ``True``. And thus the user
is not asked to supply an username, but one is generated for them. The user
can than keep sign in by using their email.
"""
def __init__(self, *args, **kwargs):
super(SignupFormOnlyEmail, self).__init__(*args, **kwargs)
del self.fields['username']
def save(self):
""" Generate a random username before falling back to parent signup form """
while True:
username = sha_constructor(str(random.random())).hexdigest()[:5]
try:
User.objects.get(username__iexact=username)
except User.DoesNotExist: break
self.cleaned_data['username'] = username
return super(SignupFormOnlyEmail, self).save()
UPDATE :
I finally solved the problem. I was also using django-email-as-username beside to django-userena. This was the cause of my problem. Apparently, they have some conflicts. WATCH OUT
You've defined url route userena_activate with keyword arguments (username and activation_key), but you call it just with arguments, change template to keyword arguments:
{% url userena_activate username=user.username activation_key=activation_key %}
edit due to comment:
I'm not sure if I understand your problem corectly, but I think there's a problem elsewhere. Yours error message says:
Caught NoReverseMatch while rendering: Reverse for 'userena_activate' with arguments '('xyz#xyz.com', '70b60d1d97015e03ba8d57f31e4c7ff14d6ab753')' and keyword arguments '{}' not found.
It seems you pass valid arguments to function, but you pass them wrong way. URL route in urls.py is defined in a way to expect kwargs, but you pass just args, which mismatch its definition. That is why you get this error message. Simple pass arguments as kwargs (that means each argument is passed with its name and value as showed above).
urls.py difference between argument and keyword argument:
url(r'^(?P<username>[\.\w]+)/activate/(?P<activation_key>\w+)/$', userena_views.activate, name='userena_activate'),
^ this is _keyword argument_ 'username', expects value with argument value AND name
and
url(r'^page/(.*)/$', userena_views.ProfileListView.as_view(), name='userena_profile_list_paginated'),
^ this is argument, expects only value, not argument name
A really simple alternative for using the email address as the username (effectively) is django-easy-userena - this is an upward compatible fork of Userena that adds a few nice features:
use email address as effective user ID - generates a random numeric
username that is hidden in forms and confirmation emails
fewer dependencies - doesn't require django-guardian or easy-thumbnails
terms of service agreement field is built in, displays as checkbox
I've had good results with this - installation was manual for some reason (copy userena dir to site-packages) but it worked without hassles.
I like the overall Userena approach, but easy-userena is a better fit for what I need.