0

I am trying to create an unsubscribe link, to be included with update notification emails sent to users. Similar to this one. I'm teaching myself and do not know how to call uidb64 and token for creating the views. I cannot find any instruction in the django documentation. Could anyone help with this, or point me to where to begin? I have seen this, but it appears to be for Django 1.x and I do not know whether it is still applicable.

Escatologist
  • 23
  • 1
  • 10
  • https://docs.djangoproject.com/en/2.1/topics/signing/ Read through this and it should help you a lot :) – Shin Jan 07 '19 at 16:08
  • I have tried reading that, but my brain glazed over. Will try again. Anything more aimed at a beginner in the meantime? – Escatologist Jan 07 '19 at 23:06
  • I guess all the signing of tokens and stuff is overengineering it for your case anyway. Here is a simple approach: Simply create a link such as `path('unsubscribe/`. Inside the `view` you would then get the user by their ID and could change some of their settings. Since a basic django user doesn't have any settings like this, you could give them a profile. Create a new model (call it `Profile`) and create a `OneToOne` relation to the Django `User` model. Inside your `Profile` model you could set a status for receiving emails. – Shin Jan 08 '19 at 00:08
  • Since everyone could guess the user ids, it would also be better to set a `UUID` inside your `Profile` and use that inside your url (`path(unsubscribe/ – Shin Jan 08 '19 at 00:17
  • I've gotten as far as the path `path('unsubscribe///', views.unsubscribe, name='unsubscribe')`. I have a user extension already for gdpr acceptance and opting in to receiving emails from their profile page when logged in. I don't know how to create a link that includes uidb64 (or similar security) and token. Nor do I know how to check them in a view. – Escatologist Jan 08 '19 at 10:43
  • You were asking for a simpler approach (like the first thing you linked), so I suggested for you to drop the tokens and just do it via the profile uuid. This isn't the most secure approach, but for practicing and to get somewhere it could help. – Shin Jan 08 '19 at 10:48
  • I didin't mean to seem ungrateful. I do appreciate your comments. I'm trying to teach myself, I'd prefer to know how to do things "the proper way", but haven't found any clear instruction on how to do this. Thanks again. – Escatologist Jan 08 '19 at 14:09

1 Answers1

1

While continuing to look for solutions to this I cam across this post on simple is better than complex.

Refer to the original article for explanations of most of this, they're much better than I am. There are some differences though.

I am using Django 2, not 1.x as the url format indicates this solution is.

Also I have 2 apps, 1 (VSL) that does the thing the webapp is for, including sending notification emails; the other (users) for user management.

I have extended the default Django user model with an OptIn, for notifications and similar, in the models.py in the users app space

class OptIn(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='opt_in')
    recieve_coms = models.BooleanField(default=True)

Following the example linked above I have created a tokens.py within the VSL app space

class ComsOptTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            six.text_type(user.pk) + six.text_type(timestamp) +
            six.text_type(user.opt_in.recieve_coms)
            )

coms_opt_token = ComsOptTokenGenerator()

This is imported into views.py in the VSL app space (or wherever it needs to be used).

from .tokens import coms_opt_token

As I am using Django 2 the url is different to the example

path('unsubscribe/<uidb64>/<token>/', views.unsubscribe, name='unsubscribe')

I am not able to use views.unsubscribe.as_view(), as shown in the example, because that causes an AttributeError: type object 'Unsubscribe' has no attribute 'as_view'.

The view that does the business of unsubscribing is in views.py in the users app space

First you will need to import the following

from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode
from VSL.tokens import coms_opt_token

The view itself is

def unsubscribe(request, uidb64, token):
    try:
        uid = urlsafe_base64_decode(uidb64).decode()
        user = User.objects.get(pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None
    if user is not None and coms_opt_token.check_token(user, token):
        comsoptin = OptIn.objects.get(user=request.user)
        comsoptin.recieve_coms = False
        comsoptin.save()
        return render(request, 'users/unsubscribe.html')
    else:
        return render(request, 'registration/invalid.html')

I am not able to use unsubscribe(View), as shown in the example, because that causes a NameError: name 'View' is not defined.

And note that uid = urlsafe_base64_decode(uidb64).decode() is different to the example because of using Django 2.

That is as far as the tutorial linked above takes me.

Back to views.py in VSL I have a function for sending an email when a new event is created

First the important imports

from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from .tokens import coms_opt_token

The function

def notify_new_event(request, new_event):
    event = new_event
    users = User.objects.filter(opt_in__recieve_coms=True)
    site_name = get_current_site(request).name
    for user in users:
        if user.is_active and user != request.user and user.last_login != None:
            with open(settings.BASE_DIR + "/VSL/templates/notifications/new_event_email.txt") as t:
                ne_message = t.read()
                token = coms_opt_token.make_token(user)
                uid = urlsafe_base64_encode(force_bytes(user.pk)).decode()
                message = EmailMultiAlternatives(
                    subject = str(site_name) + str(event.title) + " event has been created.",
                    from_email = settings.EMAIL_HOST_USER,
                    to = [user.email],
                    )
                context = {'request':request, 'user':user, 'event':event, 'site_name':site_name, 'token':token, 'uid':uid}
                html_template = get_template("notifications/new_event_email.html").render(context)
                message.attach_alternative(html_template, "text/html")
                message.send()

The important bits here are token = coms_opt_token and uid = urlsafe_base64_encode(force_bytes(user.pk)).decode().

The coms_opt_token generates our secure one time token and uid hashes the user for security.

And the unsubscribe link in the email template is

href="{{ protocol }}://{{ domain }}{% url 'users:unsubscribe' uidb64=uid token=token %}"
Escatologist
  • 23
  • 1
  • 10