I’m in the process of upgrading an existing site that previously used an early version of Wagtail and Django 2.x, which I have now upgraded to use Django 3.1.
Previously, I repurposed functions from the PasswordResetTokenGenerator function to provide one-click email list unsubscribe functionality, which worked in Django 2.x but is failing in an odd way in Django 3.1. Since Django 3.x eliminated six I was prompted to revisit these functions and update them to work without six.
In apps that generate email to groups of users I append the body of any outgoing mail with an unsubscribe link containing a base64 encoded email address of the user and the password reset token made by the included Django password reset token function, like so…
from django.contrib.auth.tokens import default_token_generator token = default_token_generator.make_token(self.user) uid = urlsafe_base64_encode(force_bytes(self.user.email)) unsub_link = settings.BASE_URL + '/unsubscribe/' + uid + '/' + token + '/'
And then as a URL, I have…
path('unsubscribe/<uidb64>/<token>/', views.unsubscribe, name='unsubscribe'),
And as the view…
from django.shortcuts import render from django.contrib.auth import get_user_model from django.utils.decorators import method_decorator from django.utils.http import urlsafe_base64_decode from django.core.exceptions import ValidationError from django.contrib.auth.tokens import default_token_generator UserModel = get_user_model() def unsubscribe(request, uidb64, token): try: uid = urlsafe_base64_decode(uidb64).decode() user = UserModel.objects.get(email=uid) except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist, ValidationError): user = None if user is not None: token_generator = default_token_generator if token_generator.check_token(user, token): # If valid token, set is_subscribed = False and show confirmation. user.is_subscribed = False user.save() return render(request, 'account/unsubscribe_confirm.html') else: # Else, return invalid token page. return render(request, 'account/invalid_token.html')
This setup always returns an invalid token, but the funny thing is, if I step through the view function manually in the shell,
check_token comes back true!
>>> uid = 'firstname.lastname@example.org' >>> user = UserModel.objects.get(email=uid) >>> token_generator = default_token_generator >>> token_generator.check_token(user, token='an08ff-atoken123456789020f3b65c9') True