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:
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
Add import uuid at the top of the file. 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. It’s 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], they 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 to. We won’t validate that as that’s most likely intentionally done by the visitor. Then create an APIView for this in incidents/views.py:
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 a 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. It would be a lot more complicated to make 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
from django.contrib import admin

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 already have our axios package installed from when we installed Nuxt. Axios will be used to make the requests. It’s like the jQuery requests you used to make in javascript (if you are at least as old as me, haha). 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. We need to change that in our nuxt.conf.js. There is an axios part. In that part add this:
baseURL: 'http://localhost:8000/'
Axios is good to go now! Let’s go to our TheHeader.vue file where our subscribe form is. We need to add an event so that the visitor can click on ‘subscribe’ and it will send a request to the server. Change this line:
<form class="form">
to this:
<form class="form" @submit.prevent="sendEmail">
As you can see, we have overwritten the default submit action there. Normal forms would refresh the page, which is something that we don’t want. We have then added the sendEmail function, which will be triggered instead. This function does not exist yet, so let’s create that part in the script part of our component:
methods: {
  sendEmail () {
    this.$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 and 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 NotificationBar component. 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 requests 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 (incidents/tasks.py). Directly under the timeout exception, we will add these lines:
from django.conf import settings
from django.core.mail import EmailMessage
from incidents.models import Uptime, Site
from users.models import User
from datetime import timedelta

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]
    email = EmailMessage("We just had a time out",
                         "Please check our website, we might have issues. https://status.djangowaves.com",        
                         settings.DEFAULT_FROM_EMAIL, [User.objects.first().email], [emails], fail_silently=True)
    email.send()
As always, add the import stuff at the top of the file. 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 avoid spamming 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]
    email = EmailMessage("A site has issues.",
              "Please check our website, we might have some serious problems. https://status.djangowaves.com",
              settings.DEFAULT_FROM_EMAIL, [User.objects.first().email], [emails], fail_silently=True)
    email.send()
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.