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!
(anonymized values:)
>>> uid = 'me@gmail.com'
>>> user = UserModel.objects.get(email=uid)
>>> token_generator = default_token_generator
>>> token_generator.check_token(user, token='an08ff-atoken123456789020f3b65c9')
True