Subscribers and Emails

Right, so almost everything is in place for our visitors. There is one thing left and that is the subscribe box. We have already built the form (which is in TheHeader.vue). What we have right now should look like this:

 

First we need to create a new model to save our subscribers in. Let’s create that model in our incidents folder:

import uuid
class Subscriber(models.Model):
    email = models.EmailField(unique=True)
    unique_id = models.UUIDField(primary_key=True,
                                 default=uuid.uuid4, editable=False)
    def __str__(self):
        return self.email

By default, Django creates an id for every table it creates, unless you assign the primary_key to a different field, as we are doing right now. In this case, a normal id would be quite useless as we aren’t going to connect this model to any other model and we only really care about the email addresses. Therefore, we replace the standard id primary key with the unique_id field. It will generate a UUID (Universally unique identifier) and we will use this for the unsubscribe link later on.

We could have also generated a link based on the email address, but if anyone would figure out that formula, then they could unsubscribe anyone they want. Better play it safe and create a random unique identifier for every new email.

Besides that, most generators in Django rely on the secret_key, which is fine. The only issue with that is that if you would want to change that, then all previously used unsubscribe links are not working anymore. This can only be avoided by storing those keys which is the same as the unique_id then.

Migrate the new table to our database:

python manage.py makemigrations && python manage.py migrate

Up next, we need to create a serializer for this. We will get data from the visitor and we need to process that.

We need to make sure it will match our requirements and for that, it goes through the serializer. If anything is missing or incorrect, our serializer will let us know. Very similar to Django forms. In your incidents/serializers.py add this bit:

from incidents.models import Site, Update, Incident, Uptime, Subscriber

class SubscriberSerializer(serializers.HyperlinkedModelSerializer):
    def validate_email(self, value):
        if Subscriber.objects.filter(email=value.lower()).exists(): 
            raise serializers.ValidationError("subscriber with this email already exists.") 
        return value.lower()
    
    class Meta:
        model = Subscriber
        fields = ('email',)

Make sure you add the comma after 'email'. It’s required as it will otherwise not see it as a list. Also notice the validate_email function. This will add an extra validation before we actually use the value. In this case, we will want to make sure that when someone enters [email protected] can’t also enter [email protected]. To Django, those are two completely unique entries and it would add them both to the email list.

Blame it on the visitor for entering their email twice differently, but ultimately, you shouldn’t allow that to happen. If they are desperate to get an email twice, then they could add [email protected] to send the second email too. We won’t validate that as that’s most likely intentionally done by the visitor.

Then create an APIView for this:

from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from incidents.serializers import SubscriberSerializer

class SubscriberView(APIView):
    permission_classes = (permissions.AllowAny,)
    def post(self, request):
        serializer = SubscriberSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

We explicitly let Django know that this is an URL that is accessible for everyone (later in this course, you will find out why we do this). It only has one post method, but we will add more methods later. It will put all data first through the serializer. Then it will check if it is valid. If not, it will return the error(s). If it is valid, it will save the serializer (save it in the database) and then return the data that has been saved.

Then we need to write the URL for that in our urls.py file. We will not push this request over our WebSocket. We could do this, but we could use this signup url on other places on our website when we create a normal HTTP route for this. For example: when you want to allow people to signup through your FAQ page. This would be a lot more complicated making the request through a WebSocket.

In our incidents/urls.py file (create it if it is not there), we will add this URL:

from incidents import views
from django.urls import path
urlpatterns = [
    path('subscribe', views.SubscriberView.as_view()),
]

Don’t forget to include the URL in our back/urls.py file like this:

from django.conf.urls import include
from django.urls import path
from incidents import urls as incident_urls

urlpatterns = [
    path('admin/', admin.site.urls), 
    path('api/', include(incident_urls))
]

There is one more thing that needs to be done before we can send messages to our server. We need to setup CORS (Cross-Origin Resource Sharing). When using a normal Django application, you will always send back requests from where they come from (from and to the same server), in this case, we send the information to a frontend that is not on the Django server. This is blocked by many browsers by default (you will see this message: "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.").

That’s why we need CORS to enable this. Install it with this:

pip install django-cors-headers

Then in your settings.py, you will have to do this:

Add 'corsheaders'  to your INSTALLED_APPS and also put 'corsheaders.middleware.CorsMiddleware'at the top of the MIDDLEWARE array.

Then also add this:

CORS_ORIGIN_ALLOW_ALL = True

This isn’t recommended for an app that is in production, we will take a look at that later. Let’s go to the front end and add some code there to make this all happen. We need a new library to be able to make requests. There are a few out there, but we will go with the one that Vue recommends, axios.

You can install it with:

npm install axios --save

Then, we need to add a baseURL for axios. Currently the baseURL is the domains url, which is not where Django lives – it’s where our front end lives. In your main.js file, go to

if (process.env.NODE_ENV === 'development') {
    axios.defaults.baseURL = 'http://localhost:8000/'
    Vue.prototype.webSocketBaseURL = 'ws://localhost:8000/ws/'
} else {
   // production settings
}

We already have part of the code above, but notice the bolder part. That’s what you need to add to change the baseURL and set it to our Django server. Don’t forget to add import axios from 'axios' at the top of the file. Axios is good to go now!

Let’s go to our TheHeader.vue file where our subscribe form is. We have added a click event that goes to the sendEmail function. This function does not exist yet, so let’s create that part in the script part of our component:

methods: {
  sendEmail () {
    axios.post('api/subscribe/', {email: this.email}).then(response=> {
      this.$store.dispatch('showNotificationErrors', {
        color: 'green',
        text: 'Cool! We will notify you of incidents!',
        time: 2000
      })
      this.email = ''
      this.showForm = !this.showForm
    }).catch(error => {
      if ('email' in error.response.data) {
         this.$store.dispatch('showNotificationErrors', {
           color: 'red',
           text: error.response.data['email'][0],
           time: 2000
        })
      }
    })
  }
},

When we click on the subscribe button, we will send a post request to our server, we will send the email address along. If this request succeeds, we will call an action from our store with the dispatch function. We will add an “error”, which is actually a simple notification that everything went well. When the request doesn’t go well, we skip the first part and go into the catch part. That’s where we will check if there is any message that is sent along with it about the email field and if that’s the case, then we will send that to our action in the store.

The store will make sure that the message is shown by our Notification component.  Don’t forget to import axios with import axios from 'axios' at the top of the script part.

We currently put our axios request in the component. This actually isn’t recommended. We do this here now since we will only have one axios request in this project. For our staff portal project, we will organize all of our axios request in one file. So more on that later!

If you want to see the emails in your admin panel, then simply register the model in admin.py with admin.site.register(subscriber). Don’t forget to import the model.

Sending notification emails

We still need to let our staff and our visitors know about issues. We will send our staff emails about downtime and issues (if they allow us) and we will send subscribers a notification about the creation/closure of an incident.

For our staff, we can simply add a few lines in our celery task. Directly under the timeout exception, we will add these lines:

from datetime import datetime, timedelta
from django.conf import settings
from django.core.mail import send_mail
from incidents.models import Uptime, Site
from users.models import User

if not Uptime.objects.filter(status='issue', site=site, date__gte=datetime.now() - timedelta(minutes=10)).exists():
    emails = [x.email for x in User.objects.all() if x.send_email_for_issues]
    send_mail("We just had a time out",
              "Please check our website, we might have issues. https://status.djangowaves.com",
              settings.DEFAULT_FROM_EMAIL, emails, fail_silently=True)

With the if condition, we will check if our staff has been notified of an issue in the last 10 minutes. We do this to not spam them with 60 messages in one hour if you would have an outage lasting an hour.

Then we get all the emails from staff that is subscribed to the issues and then we send the email.

The [x.email for x in User.objects.all() if x.send_email_for_issues] might look a bit confusing. Let’s rewrite that here, so you get an idea of how that works:

results = []
for x in User.objects.all():
    if x.send_email_for_issues:
        results.append(x.email)
return results

Now it’s not that complicated anymore, right? So, that one line replaces 5 lines. Once you understand how it works, it will really shorten the amount of code you have to write.

You also might have noticed that we have added settings.DEFAULT_FROM_EMAIL in this. We will have to define that setting in our settings.py file. This makes it easier to change if we would ever want to change the from email or from name.

We have a very similar one for the downtime exception:

if not Uptime.objects.filter(status='down', site=site, date__gte=datetime.now() - timedelta(minutes=10)).exists():
    emails = [x.email for x in User.objects.all() if x.send_email_for_downtime]
    send_mail("A site has issues.",
              "Please check our website, we might have some serious problems. https://status.djangowaves.com",
              settings.DEFAULT_FROM_EMAIL, emails)

As you can see, it is almost entirely the same, except that this one is purely for downtime.

We still need to send our subscribers an email as well, but that will be done later in this course.

Right, so almost everything is in place for our visitors. There is one thing left and that is the subscribe box. We have already built the form (which is in TheHeader.vue).

First we need to create a new model to save our subscribers in. Let’s create that model in our incidents folder:

import uuid
class Subscriber(models.Model):
    email = models.EmailField(unique=True)
    unique_id = models.UUIDField(primary_key=True,
                                 default=uuid.uuid4, editable=False)
    def __str__(self):
        return self.email

By default, Django creates an id for every table it creates, unless you assign the primary_key to a different field, as we are doing right now. In this case, a normal id would be quite useless as we aren’t going to connect this model to any other model and we only really care about the email addresses. Therefore, we replace the standard id primary key with the unique_id field. It will generate a UUID (Universally unique identifier) and we will use this for the unsubscribe link later on.

We could have also generated a link based on the email address, but if anyone would figure out that formula, then they could unsubscribe anyone they want. Better play it safe and create a random unique identifier for every new email.

Besides that, most generators in Django rely on the secret_key, which is fine. The only issue with that is that if you would want to change that, then all previously used unsubscribe links are not working anymore. This can only be avoided by storing those keys which is the same as the unique_id then.

Migrate the new table to our database:

python manage.py makemigrations && python manage.py migrate

Up next, we need to create a serializer for this. We will get data from the visitor and we need to process that.

We need to make sure it will match our requirements and for that, it goes through the serializer. If anything is missing or incorrect, our serializer will let us know. Very similar to Django forms. In your incidents/serializers.py add this bit:

from incidents.models import Site, Update, Incident, Uptime, Subscriber

class SubscriberSerializer(serializers.HyperlinkedModelSerializer):
    def validate_email(self, value):
        if Subscriber.objects.filter(email=value.lower()).exists(): 
            raise serializers.ValidationError("subscriber with this email already exists.") 
        return value.lower()
    
    class Meta:
        model = Subscriber
        fields = ('email',)

Make sure you add the comma after 'email'. It’s required as it will otherwise not see it as a list. Also notice the validate_email function. This will add an extra validation before we actually use the value. In this case, we will want to make sure that when someone enters [email protected] can’t also enter [email protected]. To Django, those are two completely unique entries and it would add them both to the email list.

Blame it on the visitor for entering their email twice differently, but ultimately, you shouldn’t allow that to happen. If they are desperate to get an email twice, then they could add [email protected] to send the second email too. We won’t validate that as that’s most likely intentionally done by the visitor.

Then create an APIView for this:

from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from incidents.serializers import SubscriberSerializer

class SubscriberView(APIView):
    permission_classes = (permissions.AllowAny,)
    def post(self, request):
        serializer = SubscriberSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

We explicitly let Django know that this is an URL that is accessible for everyone (later in this course, you will find out why we do this). It only has one post method, but we will add more methods later. It will put all data first through the serializer. Then it will check if it is valid. If not, it will return the error(s). If it is valid, it will save the serializer (save it in the database) and then return the data that has been saved.

Then we need to write the URL for that in our urls.py file. We will not push this request over our WebSocket. We could do this, but we could use this signup url on other places on our website when we create a normal HTTP route for this. For example: when you want to allow people to signup through your FAQ page. This would be a lot more complicated making the request through a WebSocket.

In our incidents/urls.py file (create it if it is not there), we will add this URL:

from incidents import views
from django.urls import path
urlpatterns = [
    path('subscribe', views.SubscriberView.as_view()),
]

Don’t forget to include the URL in our back/urls.py file like this:

from django.conf.urls import include
from django.urls import path
from incidents import urls as incident_urls

urlpatterns = [
    path('admin/', admin.site.urls), 
    path('api/', include(incident_urls))
]

There is one more thing that needs to be done before we can send messages to our server. We need to setup CORS (Cross-Origin Resource Sharing). When using a normal Django application, you will always send back requests from where they come from (from and to the same server), in this case, we send the information to a frontend that is not on the Django server. This is blocked by many browsers by default (you will see this message: "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.").

That’s why we need CORS to enable this. Install it with this:

pip install django-cors-headers

Then in your settings.py, you will have to do this:

Add 'corsheaders'  to your INSTALLED_APPS and also put 'corsheaders.middleware.CorsMiddleware'at the top of the MIDDLEWARE array.

Then also add this:

CORS_ORIGIN_ALLOW_ALL = True

This isn’t recommended for an app that is in production, we will take a look at that later. Let’s go to the front end and add some code there to make this all happen. We need a new library to be able to make requests. There are a few out there, but we will go with the one that Vue recommends, axios.

You can install it with:

npm install axios --save

Then, we need to add a baseURL for axios. Currently the baseURL is the domains url, which is not where Django lives – it’s where our front end lives. In your main.js file, go to

if (process.env.NODE_ENV === 'development') {
    axios.defaults.baseURL = 'http://localhost:8000/'
    Vue.prototype.webSocketBaseURL = 'ws://localhost:8000/ws/'
} else {
   // production settings
}

We already have part of the code above, but notice the bolder part. That’s what you need to add to change the baseURL and set it to our Django server. Don’t forget to add import axios from 'axios' at the top of the file. Axios is good to go now!

We will then have to create a new function to send the email to the back end, so let’s create that part in the script part of our component:

methods: {
  sendEmail () {
    axios.post('api/subscribe/', {email: this.email}).then(response=> {
      this.$store.dispatch('showNotificationErrors', {
        color: 'green',
        text: 'Cool! We will notify you of incidents!',
        time: 2000
      })
      this.email = ''
      this.showForm = !this.showForm
    }).catch(error => {
      if ('email' in error.response.data) {
         this.$store.dispatch('showNotificationErrors', {
           color: 'red',
           text: error.response.data['email'][0],
           time: 2000
        })
      }
    })
  }
},

When we click on the subscribe button, we will send a post request to our server, we will send the email address along. If this request succeeds, we will call an action from our store with the dispatch function. We will add an “error”, which is actually a simple notification that everything went well. When the request doesn’t go well, we skip the first part and go into the catch part. That’s where we will check if there is any message that is sent along with it about the email field and if that’s the case, then we will send that to our action in the store.

The store will make sure that the message is shown by our Notification component.  Don’t forget to import axios with import axios from 'axios' at the top of the script part.

We currently put our axios request in the component. This actually isn’t recommended. We do this here now since we will only have one axios request in this project. For our staff portal project, we will organize all of our axios request in one file. So more on that later!

If you want to see the emails in your admin panel, then simply register the model in admin.py with admin.site.register(subscriber). Don’t forget to import the model.

Sending notification emails

We still need to let our staff and our visitors know about issues. We will send our staff emails about downtime and issues (if they allow us) and we will send subscribers a notification about the creation/closure of an incident.

For our staff, we can simply add a few lines in our celery task. Directly under the timeout exception, we will add these lines:

from datetime import datetime, timedelta
from django.conf import settings
from django.core.mail import send_mail
from incidents.models import Uptime, Site
from users.models import User

if not Uptime.objects.filter(status='issue', site=site, date__gte=datetime.now() - timedelta(minutes=10)).exists():
    emails = [x.email for x in User.objects.all() if x.send_email_for_issues]
    send_mail("We just had a time out",
              "Please check our website, we might have issues. https://status.djangowaves.com",
              settings.DEFAULT_FROM_EMAIL, emails, fail_silently=True)

With the if condition, we will check if our staff has been notified of an issue in the last 10 minutes. We do this to not spam them with 60 messages in one hour if you would have an outage lasting an hour.

Then we get all the emails from staff that is subscribed to the issues and then we send the email.

The [x.email for x in User.objects.all() if x.send_email_for_issues] might look a bit confusing. Let’s rewrite that here, so you get an idea of how that works:

results = []
for x in User.objects.all():
    if x.send_email_for_issues:
        results.append(x.email)
return results

Now it’s not that complicated anymore, right? So, that one line replaces 5 lines. Once you understand how it works, it will really shorten the amount of code you have to write.

You also might have noticed that we have added settings.DEFAULT_FROM_EMAIL in this. We will have to define that setting in our settings.py file. This makes it easier to change if we would ever want to change the from email or from name.

We have a very similar one for the downtime exception:

if not Uptime.objects.filter(status='down', site=site, date__gte=datetime.now() - timedelta(minutes=10)).exists():
    emails = [x.email for x in User.objects.all() if x.send_email_for_downtime]
    send_mail("A site has issues.",
              "Please check our website, we might have some serious problems. https://status.djangowaves.com",
              settings.DEFAULT_FROM_EMAIL, emails)

As you can see, it is almost entirely the same, except that this one is purely for downtime.

We still need to send our subscribers an email as well, but that will be done later in this course.