# Send Appointment Reminders with Python and Django

> \[!NOTE]
>
> Ahoy! We now recommend you build your appointment reminders with Twilio's built-in Message Scheduling functionality. Head on over to the [Message Scheduling documentation](/docs/messaging/features/message-scheduling) to learn more about scheduling messages.

Ready to implement SMS appointment reminders in your Django web application? We'll use the Twilio Python SDK and [Twilio SMS API](/docs/iam/api) to push out reminders to our customers when appointments are near. Here's how it works at a high level:

1. An administrator (our user) creates an appointment for a future date and time, and stores a customer's phone number in the database for that appointment
2. When that appointment is saved a background task is scheduled to send a reminder to that customer before their appointment starts
3. At a configured time in advance of the appointment, the background task sends an SMS reminder to the customer to remind them of their appointment

*[Check out how Yelp uses SMS to confirm restaurant reservations for diners.](https://customers.twilio.com/1154/yelp/?utm_source=docs\&utm_campaign=docs_to_stories)*

## Appointment Reminder Building Blocks

Here are the technologies we'll use:

* [Django](https://www.djangoproject.com/) to create a database-driven web application
* [The Messages Resource](/docs/messaging/quickstart) from Twilio's REST API to send text messages
* [Dramatiq](https://dramatiq.io/) to help us schedule and execute background tasks on a recurring basis

## How To Read This Tutorial

To implement appointment reminders, we will be working through a series of [user stories](https://en.wikipedia.org/wiki/User_story) that describe how to fully implement appointment reminders in a web application.

We'll walk through the code required to satisfy each story, and explore what we needed to add at each step.

All this can be done with the help of Twilio in under half an hour.

## Meet our Django Appointment Reminder Stack

We're building this app for Django 2.1 on Python 3.7. In addition to Dramatiq, we will use a few other Python libraries to make our task easier:

* The [twilio-python](https://github.com/twilio/twilio-python) Python SDK
* [django-timezone-field](https://github.com/mfogel/django-timezone-field) to give us a model field for storing time zones
* [django-bootstrap3](https://github.com/dyve/django-bootstrap3) and [django-forms-bootstrap](https://github.com/pinax/django-forms-bootstrap) to make our form templates prettier
* The fantastic [arrow](https://github.com/crsmithdev/arrow) library to make our datetime math foolproof

We will also use [PostgreSQL](https://www.postgresql.org/) for our database and [Redis](https://redis.io/) as our [Dramatiq message broker](https://dramatiq.io/guide.html#redis-broker).

#### Project dependencies

```txt requirements.txt
appdirs==1.4.3
appnope==0.1.0
arrow==0.15.7
asgiref==3.2.5
attrs==19.3.0
backcall==0.1.0
black==19.10b0
certifi==2019.11.28
cfgv==3.1.0
chardet==3.0.4
click==7.1.1
decorator==4.4.2
distlib==0.3.0
Django==3.0.4
django-dramatiq==0.9.1
django-bootstrap3==14.0.0
django-environ==0.4.5
django-forms-bootstrap==3.1.0
django-timezone-field==4.0
dramatiq[rabbitmq,watch]==1.9.0
entrypoints==0.3
filelock==3.0.12
flake8==3.7.9
identify==1.4.11
idna==2.9
importlib-metadata==1.5.0
ipdb==0.13.2
ipython==7.13.0
ipython-genutils==0.2.0
isort==4.3.21
jedi==0.16.0
mccabe==0.6.1
mock==4.0.2
model-mommy==2.0.0
nodeenv==1.3.5
parso==0.6.2
pathspec==0.7.0
pexpect==4.8.0
pickleshare==0.7.5
pre-commit==2.2.0
prompt-toolkit==3.0.4
ptyprocess==0.6.0
pycodestyle==2.5.0
pyflakes==2.1.1
Pygments==2.6.1
PyJWT==1.7.1
pytz==2019.3
PyYAML==5.3
redis==3.5.3
regex==2020.2.20
requests==2.23.0
selenium==3.141.0
six==1.14.0
sqlparse==0.3.1
toml==0.10.0
traitlets==4.3.3
twilio==6.36.0
typed-ast==1.4.1
urllib3==1.25.8
virtualenv==20.0.10
wcwidth==0.1.8
whitenoise==5.1.0
zipp==3.1.0
```

Now that we have all our dependencies defined, we can get started with our first user story: creating a new appointment.

## Creating an Appointment

> As a user, I want to create an appointment with a name, guest phone number, and a time in the future.

To build an automated appointment reminder app, we probably should start with an appointment. This story requires that we create a model object and a bit of the user interface to create and save a new `Appointment` in our system.

At a high level, here's what we will need to add:

* An `Appointment` model to store information we need to send the reminder
* A view to render our form and accept `POST` data from it
* An HTML form to enter details about the appointment

Alright, so we know what we need to create a new appointment. Now let's start by looking at the model, where we decide what information we want to store with the appointment.

## The Appointment Model

We only need to store four pieces of data about each appointment to send a reminder:

* The customer's name
* Their phone number
* The date and time of their appointment
* The time zone of the appointment

We also included two additional fields: `task_id` and `created`. The `task_id` field will help us keep track of the corresponding reminder task for this appointment. The `created` field is just a timestamp populated when an appointment is created.

Finally, we defined a `__str__` method to tell Django how to represent instances of our model as text. This method uses the primary key and the customer's name to create a readable representation of an appointment.

```py title="Appointment model fields" description="reminders/models.py"
# !mark(15:27)
from __future__ import unicode_literals

import redis

from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from django.urls import reverse
from six import python_2_unicode_compatible
from timezone_field import TimeZoneField

import arrow


@python_2_unicode_compatible
class Appointment(models.Model):
    name = models.CharField(max_length=150)
    phone_number = models.CharField(max_length=15)
    time = models.DateTimeField()
    time_zone = TimeZoneField(default='UTC')

    # Additional fields not visible to users
    task_id = models.CharField(max_length=50, blank=True, editable=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return 'Appointment #{0} - {1}'.format(self.pk, self.name)

    def get_absolute_url(self):
        return reverse('reminders:view_appointment', args=[str(self.id)])

    def clean(self):
        """Checks that appointments are not scheduled in the past"""

        appointment_time = arrow.get(self.time, self.time_zone.zone)

        if appointment_time < arrow.utcnow():
            raise ValidationError(
                'You cannot schedule an appointment for the past. '
                'Please check your time and time_zone')

    def schedule_reminder(self):
        """Schedule a Dramatiq task to send a reminder for this appointment"""

        # Calculate the correct time to send this reminder
        appointment_time = arrow.get(self.time, self.time_zone.zone)
        reminder_time = appointment_time.shift(minutes=-30)
        now = arrow.now(self.time_zone.zone)
        milli_to_wait = int(
            (reminder_time - now).total_seconds()) * 1000

        # Schedule the Dramatiq task
        from .tasks import send_sms_reminder
        result = send_sms_reminder.send_with_options(
            args=(self.pk,),
            delay=milli_to_wait)

        return result.options['redis_message_id']

    def save(self, *args, **kwargs):
        """Custom save method which also schedules a reminder"""

        # Check if we have scheduled a reminder for this appointment before
        if self.task_id:
            # Revoke that task in case its time has changed
            self.cancel_task()

        # Save our appointment, which populates self.pk,
        # which is used in schedule_reminder
        super(Appointment, self).save(*args, **kwargs)

        # Schedule a new reminder task for this appointment
        self.task_id = self.schedule_reminder()

        # Save our appointment again, with the new task_id
        super(Appointment, self).save(*args, **kwargs)

    def cancel_task(self):
        redis_client = redis.Redis(host=settings.REDIS_LOCAL, port=6379, db=0)
        redis_client.hdel("dramatiq:default.DQ.msgs", self.task_id)
```

Our appointment model is now setup, the next step is writing a view for it.

## New Appointment View

Django lets developers write views as [functions](https://docs.djangoproject.com/en/2.1/topics/http/views/) or [classes](https://docs.djangoproject.com/en/2.1/topics/class-based-views/).

Class-based views are great when your views need to support [CRUD-like](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) features - perfect for our appointments project.

To make a view for creating new `Appointment` objects, we'll use Django's generic [CreateView](https://docs.djangoproject.com/en/2.1/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView) class.

All we need to specify is the model it should use and what fields it should include. We don't even need to declare a form - Django will use a [ModelForm](https://docs.djangoproject.com/en/2.1/topics/forms/modelforms/#modelform) for us behind the scenes.

**Success messages**

Our view is ready to go with just those first three lines of code, but we'll make it a little better by adding the [SuccessMessageMixin](https://docs.djangoproject.com/en/2.1/ref/contrib/messages/#adding-messages-in-class-based-views).

This mixin tells our view to pass the `success_message` property of our class to the [Django messages framework](https://docs.djangoproject.com/en/2.1/ref/contrib/messages/) after a successful creation. We will display those messages to the user in our templates.

```py title="Create new Appointment" description="reminders/views.py"
# !mark(24:29)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView
from django.views.generic.list import ListView

from .models import Appointment


class AppointmentListView(ListView):
    """Shows users a list of appointments"""

    model = Appointment


class AppointmentDetailView(DetailView):
    """Shows users a single appointment"""

    model = Appointment


class AppointmentCreateView(SuccessMessageMixin, CreateView):
    """Powers a form to create a new appointment"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully created.'


class AppointmentUpdateView(SuccessMessageMixin, UpdateView):
    """Powers a form to edit existing appointments"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully updated.'


class AppointmentDeleteView(DeleteView):
    """Prompts users to confirm deletion of an appointment"""

    model = Appointment
    success_url = reverse_lazy('list_appointments')
```

Now that we have a view to create new appointments, we need to add a new URL to our URL dispatcher so users can get to it.

## Wiring up the URLs

To satisfy the appointment creation user story, we'll create a new URL at `/new` and point it to our `AppointmentCreateView`.

Because we're using a class-based view, we pass our view to our URL with the `.as_view()` method instead of just using the view's name.

```py title="Wire up URL with create Appointment view" description="reminders/urls.py"
# !mark(19)
from django.conf.urls import re_path

from .views import (
    AppointmentCreateView,
    AppointmentDeleteView,
    AppointmentDetailView,
    AppointmentListView,
    AppointmentUpdateView,
)

urlpatterns = [
    # List and detail views
    re_path(r'^$', AppointmentListView.as_view(), name='list_appointments'),
    re_path(r'^(?P<pk>[0-9]+)$',
            AppointmentDetailView.as_view(),
            name='view_appointment'),

    # Create, update, delete
    re_path(r'^new$', AppointmentCreateView.as_view(), name='new_appointment'),
    re_path(r'^(?P<pk>[0-9]+)/edit$',
            AppointmentUpdateView.as_view(),
            name='edit_appointment'),
    re_path(r'^(?P<pk>[0-9]+)/delete$',
            AppointmentDeleteView.as_view(),
            name='delete_appointment'),
]
```

With a view and a model in place, the last big piece we need to let our users create new appointments is the HTML form.

## New Appointment Form

Our form template inherits from our base template, which you can check out at `templates/base.html`.

We're using [Bootstrap](https://getbootstrap.com/) for the front end of our app, and we use the [django-forms-bootstrap](https://github.com/pinax/django-forms-bootstrap) library to help us render our form with the `|as_bootstrap_horizontal` template filter.

By naming this file `appointment_form.html`, our `AppointmentCreateView` will automatically use this template when rendering its response. If you want to name your template something else, you can specify its name by adding a `template_name` property on our view class.

```html title="New Appointment form" description="templates/reminders/appointment_form.html"
{% extends "base.html" %}

{% load bootstrap_tags %}

{% block title %}New reminder{% endblock title %}

{% block content %}
  <div class="row">
    <div class="col-lg-9">
      <div class="page-header">
        <h1>
          {% if not object.pk %}
            New appointment
          {% else %}
            Edit appointment
          {% endif %}
          </h1>
      </div>

      <form class="form-horizontal" method="post">
        {% csrf_token %}
        {{ form|as_bootstrap_horizontal }}
        <div class="form-group">
          <div class="col-sm-offset-2 col-sm-10">
            <a href="#back" class="btn btn-default">Cancel</a>
            <button type="submit" class="btn btn-primary">
              {% if not object.pk %}
                Create appointment
              {% else %}
                Update appointment
              {% endif %}
            </button>
          </div>
        </div>
      </form>
    </div>
  </div>
{% endblock %}

{% block page_css %}
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/css/bootstrap-datetimepicker.min.css" />
{% endblock %}

{% block page_js %}
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/js/bootstrap-datetimepicker.min.js"></script>

  <script type="text/javascript">
      $(function() {
          $('#id_time').datetimepicker({
            format: 'MM/DD/YYYY HH:mm',
            extraFormats: ['YYYY-MM-DD HH:mm:ss'],
            sideBySide: true
          });
      });
  </script>
{% endblock %}
```

We are not leaving this form yet. Instead, let's take a closer look at one of its widgets: the datepicker.

## Appointment Form Datepicker

To make it easier for our users to enter the date and time of an appointment, we'll use a JavaScript datepicker widget.

In this case, [bootstrap-datetimepicker](https://eonasdan.github.io/bootstrap-datetimepicker/) is a good fit. We include the necessary CSS and JS files from content delivery networks and then add a little custom JavaScript to initialize the widget on the form input for our time field.

```html title="Datepicker widget" description="templates/reminders/appointment_form.html"
<!-- !mark(40:57) -->
{% extends "base.html" %}

{% load bootstrap_tags %}

{% block title %}New reminder{% endblock title %}

{% block content %}
  <div class="row">
    <div class="col-lg-9">
      <div class="page-header">
        <h1>
          {% if not object.pk %}
            New appointment
          {% else %}
            Edit appointment
          {% endif %}
          </h1>
      </div>

      <form class="form-horizontal" method="post">
        {% csrf_token %}
        {{ form|as_bootstrap_horizontal }}
        <div class="form-group">
          <div class="col-sm-offset-2 col-sm-10">
            <a href="#back" class="btn btn-default">Cancel</a>
            <button type="submit" class="btn btn-primary">
              {% if not object.pk %}
                Create appointment
              {% else %}
                Update appointment
              {% endif %}
            </button>
          </div>
        </div>
      </form>
    </div>
  </div>
{% endblock %}

{% block page_css %}
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/css/bootstrap-datetimepicker.min.css" />
{% endblock %}

{% block page_js %}
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/js/bootstrap-datetimepicker.min.js"></script>

  <script type="text/javascript">
      $(function() {
          $('#id_time').datetimepicker({
            format: 'MM/DD/YYYY HH:mm',
            extraFormats: ['YYYY-MM-DD HH:mm:ss'],
            sideBySide: true
          });
      });
  </script>
{% endblock %}
```

Now let's go back to our `Appointment` model to see what happens after we successfully post this form.

## Add a get\_absolute\_url() Method

When a user clicks "Submit" on our new appointment form, their input will be received by our `AppointmentCreateView` and then validated against the fields we specified in our `Appointment` model.

If everything looks good, Django will save the new appointment to the database. We need to tell our `AppointmentCreateView` where to send our user next.

We could specify a `success_url` property on our `AppointmentCreateView`, but by default Django's [CreateView](https://docs.djangoproject.com/en/2.1/ref/class-based-views/mixins-editing/#django.views.generic.edit.ModelFormMixin.get_success_url) class will use the newly created object's `get_absolute_url` method to figure out where to go next.

So we'll define a `get_absolute_url` method on our `Appointment` model, which uses Django's [reverse](https://docs.djangoproject.com/en/2.1/ref/urlresolvers/#reverse) utility function to build a URL for this appointment's detail page. You can see that template at `templates/reminders/appointment_detail.html`.

And now our users are all set to create new appointments.

```py title="get_absolute_url method" description="reminders/models.py"
# !mark(15:30)
from __future__ import unicode_literals

import redis

from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from django.urls import reverse
from six import python_2_unicode_compatible
from timezone_field import TimeZoneField

import arrow


@python_2_unicode_compatible
class Appointment(models.Model):
    name = models.CharField(max_length=150)
    phone_number = models.CharField(max_length=15)
    time = models.DateTimeField()
    time_zone = TimeZoneField(default='UTC')

    # Additional fields not visible to users
    task_id = models.CharField(max_length=50, blank=True, editable=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return 'Appointment #{0} - {1}'.format(self.pk, self.name)

    def get_absolute_url(self):
        return reverse('reminders:view_appointment', args=[str(self.id)])

    def clean(self):
        """Checks that appointments are not scheduled in the past"""

        appointment_time = arrow.get(self.time, self.time_zone.zone)

        if appointment_time < arrow.utcnow():
            raise ValidationError(
                'You cannot schedule an appointment for the past. '
                'Please check your time and time_zone')

    def schedule_reminder(self):
        """Schedule a Dramatiq task to send a reminder for this appointment"""

        # Calculate the correct time to send this reminder
        appointment_time = arrow.get(self.time, self.time_zone.zone)
        reminder_time = appointment_time.shift(minutes=-30)
        now = arrow.now(self.time_zone.zone)
        milli_to_wait = int(
            (reminder_time - now).total_seconds()) * 1000

        # Schedule the Dramatiq task
        from .tasks import send_sms_reminder
        result = send_sms_reminder.send_with_options(
            args=(self.pk,),
            delay=milli_to_wait)

        return result.options['redis_message_id']

    def save(self, *args, **kwargs):
        """Custom save method which also schedules a reminder"""

        # Check if we have scheduled a reminder for this appointment before
        if self.task_id:
            # Revoke that task in case its time has changed
            self.cancel_task()

        # Save our appointment, which populates self.pk,
        # which is used in schedule_reminder
        super(Appointment, self).save(*args, **kwargs)

        # Schedule a new reminder task for this appointment
        self.task_id = self.schedule_reminder()

        # Save our appointment again, with the new task_id
        super(Appointment, self).save(*args, **kwargs)

    def cancel_task(self):
        redis_client = redis.Redis(host=settings.REDIS_LOCAL, port=6379, db=0)
        redis_client.hdel("dramatiq:default.DQ.msgs", self.task_id)
```

We are now able to create new appointments. Nex, let's quickly implement a few other basic features: listing, updating, and deleting appointments.

## Interacting with Appointments

> As a user, I want to view a list of all future appointments, and be able to edit and delete those appointments.

If you're an organization that handles a lot of appointments, you probably want to be able to view and manage them in a single interface. That's what we'll tackle in this user story. We'll create a UI to:

* Show all appointments
* Edit individual appointments
* Delete individual appointments

Because these are basic [CRUD-like](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations, we'll keep using [Django's generic class-based views](https://docs.djangoproject.com/en/2.1/topics/class-based-views/generic-editing/#model-forms) to save us a lot of work.

```py title="Interacting with appointments" description="reminders/views.py"
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView
from django.views.generic.list import ListView

from .models import Appointment


class AppointmentListView(ListView):
    """Shows users a list of appointments"""

    model = Appointment


class AppointmentDetailView(DetailView):
    """Shows users a single appointment"""

    model = Appointment


class AppointmentCreateView(SuccessMessageMixin, CreateView):
    """Powers a form to create a new appointment"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully created.'


class AppointmentUpdateView(SuccessMessageMixin, UpdateView):
    """Powers a form to edit existing appointments"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully updated.'


class AppointmentDeleteView(DeleteView):
    """Prompts users to confirm deletion of an appointment"""

    model = Appointment
    success_url = reverse_lazy('list_appointments')
```

We have the high level view of the task, so let's start with listing all the upcoming appointments.

## Showing a List of Appointments

Django's [ListView](https://docs.djangoproject.com/en/2.1/ref/class-based-views/generic-display/#listview) class was born for this.

All we need to do it's to point it at our `Appointment` model and it will handle building a QuerySet of all appointments for us.

```python
from .views import AppointmentListView

re_path(r'^$', AppointmentListView.as_view(), name='list_appointments'),
```

```py title="List appointments" description="reminders/views.py"
# !mark(12:15)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView
from django.views.generic.list import ListView

from .models import Appointment


class AppointmentListView(ListView):
    """Shows users a list of appointments"""

    model = Appointment


class AppointmentDetailView(DetailView):
    """Shows users a single appointment"""

    model = Appointment


class AppointmentCreateView(SuccessMessageMixin, CreateView):
    """Powers a form to create a new appointment"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully created.'


class AppointmentUpdateView(SuccessMessageMixin, UpdateView):
    """Powers a form to edit existing appointments"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully updated.'


class AppointmentDeleteView(DeleteView):
    """Prompts users to confirm deletion of an appointment"""

    model = Appointment
    success_url = reverse_lazy('list_appointments')
```

Our view is ready, now let's check out the template to display this list of appointments.

## Appointment List Template

Our `AppointmentListView` passes its list of appointment objects to our template in the `object_list` variable.

If that variable is empty, we include a `<p>` tag saying there are no upcoming appointments.

Otherwise we populate a table with a row for each appointment in our list. We can use our handy `get_absolute_url` method again to include a link to each appointment's detail page.

We also use the [\{% url %}](https://docs.djangoproject.com/en/1.8/ref/templates/builtins/#url) template tag to include links to our edit and delete views.

```html title="Render Appointment list" description="templates/reminders/appointment_list.html"
{% extends "base.html" %}

{% block title %}Upcoming reminders{% endblock title %}

{% block content %}
  <div class="row">
    <div class="col-lg-9">

      <div class="page-header">
        <h1>Appointments</h1>
      </div>

      {% if not object_list %}
        <p><strong>No upcoming appointments.</strong> Why not <a href="{% url 'new_appointment' %}">schedule one?</a>
      {% endif %}

      <table class="table table-striped">
        <thead>
          <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Phone number</th>
            <th>Time</th>
            <th>Created at</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {% for appointment in object_list %}
            <tr>
              <td><a href="{{ appointment.get_absolute_url }}">{{ appointment.pk }}</a></td>
              <td>{{ appointment.name }}</td>
              <td>{{ appointment.phone_number }}</td>
              <td>{{ appointment.time }}</td>
              <td>{{ appointment.created }}</td>
              <td>
                <a class="btn btn-default btn-xs" href="{% url 'edit_appointment' appointment.pk %}">Edit</a>
                <a class="btn btn-xs btn-danger" href="{% url 'delete_appointment' appointment.pk %}">Delete</a>
              </td>
            </tr>
          {% endfor %}
        </tbody>
      </table>

      <a class="btn btn-primary" href="{% url 'new_appointment' %}">New</a>
    </div>
  </div>
{% endblock %}
```

And now that our appointment listing requirement is complete, let's see how we can use the new Appointment form to update existing appointments.

## Tweaking our Form Template

Django's [UpdateView](https://docs.djangoproject.com/en/2.1/ref/class-based-views/generic-editing/#django.views.generic.edit.UpdateView) allows you to add a view for updating appointments. Our form template needs a few tweaks, though, to handle prepopulated data from an existing appointment.

Django will store our datetimes precisely, down to the second, but we don't want to bother our users by forcing them to pick the precise second an appointment starts.

To fix this problem we use the [extraFormats configuration option](https://eonasdan.github.io/bootstrap-datetimepicker/Options/#extraformats) of bootstrap-datetimepicker.

By configuring our datetimepicker with a `format` value that doesn't ask users for seconds, and an `extraFormat` value that does accept datetimes with seconds, our form will populate correctly when Django provides a full datetime to our template.

```html title="Tweaking new Appointment form" description="templates/reminders/appointment_form.html"
<!-- !mark(44:57) -->
{% extends "base.html" %}

{% load bootstrap_tags %}

{% block title %}New reminder{% endblock title %}

{% block content %}
  <div class="row">
    <div class="col-lg-9">
      <div class="page-header">
        <h1>
          {% if not object.pk %}
            New appointment
          {% else %}
            Edit appointment
          {% endif %}
          </h1>
      </div>

      <form class="form-horizontal" method="post">
        {% csrf_token %}
        {{ form|as_bootstrap_horizontal }}
        <div class="form-group">
          <div class="col-sm-offset-2 col-sm-10">
            <a href="#back" class="btn btn-default">Cancel</a>
            <button type="submit" class="btn btn-primary">
              {% if not object.pk %}
                Create appointment
              {% else %}
                Update appointment
              {% endif %}
            </button>
          </div>
        </div>
      </form>
    </div>
  </div>
{% endblock %}

{% block page_css %}
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/css/bootstrap-datetimepicker.min.css" />
{% endblock %}

{% block page_js %}
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.7.14/js/bootstrap-datetimepicker.min.js"></script>

  <script type="text/javascript">
      $(function() {
          $('#id_time').datetimepicker({
            format: 'MM/DD/YYYY HH:mm',
            extraFormats: ['YYYY-MM-DD HH:mm:ss'],
            sideBySide: true
          });
      });
  </script>
{% endblock %}
```

We now have everything to `List`, `Create` and `Update` an `Appointment`. All that is left is handle the `Delete`.

## Delete View

[DeleteView](https://docs.djangoproject.com/en/2.1/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView) is an especially handy view class. It shows users a confirmation page before deleting the specified object.

Like UpdateView, DeleteView finds the object to delete by using the `pk` parameter in its URL, declared in `reminders/urls.py`:

```python
from .views import AppointmentDeleteView

re_path(r'^/(?P[0-9]+)/delete$', AppointmentDeleteView.as_view(), name='delete_appointment'),
```

We also need to specify a `success_url` property on our view class. This property tells Django where to send users after a successful deletion. In our case, we'll send them back to the list of appointments at the URL named `list_appointments`.

When a Django project starts running, it evaluates views before URLs, so we need to use the [reverse\_lazy](https://docs.djangoproject.com/en/2.1/ref/urlresolvers/#reverse-lazy) utility function to get our appointment list URL instead of `reverse`.

By default, our `AppointmentDeleteView` will look for a template named `appointment_confirm_delete.html`. You can check out ours in the `templates/reminders` directory.

And that closes out this user story.

```py title="Import generic views" description="reminders/views.py"
# !mark(2,40:44)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from django.views.generic.edit import DeleteView
from django.views.generic.edit import UpdateView
from django.views.generic.list import ListView

from .models import Appointment


class AppointmentListView(ListView):
    """Shows users a list of appointments"""

    model = Appointment


class AppointmentDetailView(DetailView):
    """Shows users a single appointment"""

    model = Appointment


class AppointmentCreateView(SuccessMessageMixin, CreateView):
    """Powers a form to create a new appointment"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully created.'


class AppointmentUpdateView(SuccessMessageMixin, UpdateView):
    """Powers a form to edit existing appointments"""

    model = Appointment
    fields = ['name', 'phone_number', 'time', 'time_zone']
    success_message = 'Appointment successfully updated.'


class AppointmentDeleteView(DeleteView):
    """Prompts users to confirm deletion of an appointment"""

    model = Appointment
    success_url = reverse_lazy('list_appointments')
```

Our users now have everything they need to manage appointments - all that's left to implement is sending the reminders.

## Sending the Reminder

> As an appointment system, I want to notify a customer via SMS an arbitrary interval before a future appointment.

To satisfy this user story, we need to make our application work asynchronously - on its own independent of any individual user interaction.

One of the most popular Python library for asynchronous tasks is [Dramatiq](https://dramatiq.io/). To integrate Dramatiq with our application, we need to make a few changes:

* Create a new function that sends an SMS message using information from an `Appointment` object
* Register that function as a task with Dramatiq so it can be executed asynchronously
* Run a separate Dramatiq worker process alongside our Django application to call our SMS reminder function at the right time for each appointment

If you're brand new to Dramatiq, you might want to skim its [Introduction to Dramatiq](https://docs.celeryproject.org/en/latest/getting-started/introduction.html) page before proceeding.

```py title="Send SMS reminder task" description="reminders/tasks.py"
from __future__ import absolute_import

import arrow
import dramatiq

from django.conf import settings
from twilio.rest import Client

from .models import Appointment


# Uses credentials from the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN
# environment variables
client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)


@dramatiq.actor
def send_sms_reminder(appointment_id):
    """Send a reminder to a phone using Twilio SMS"""
    # Get our appointment from the database
    try:
        appointment = Appointment.objects.get(pk=appointment_id)
    except Appointment.DoesNotExist:
        # The appointment we were trying to remind someone about
        # has been deleted, so we don't need to do anything
        return

    appointment_time = arrow.get(appointment.time, appointment.time_zone.zone)
    body = 'Hi {0}. You have an appointment coming up at {1}.'.format(
        appointment.name,
        appointment_time.format('h:mm a')
    )

    client.messages.create(
        body=body,
        to=appointment.phone_number,
        from_=settings.TWILIO_NUMBER,
    )
```

Next we will configure Dramatiq to work with our project.

## Setting up Dramatiq

Dramatiq and Django are both big Python projects, but they can work together.

[By following the instructions in the Dramatiq docs](https://github.com/Bogdanp/django_dramatiq#installation), we can include our Dramatiq settings in our Django settings modules. We can also write our Dramatiq tasks in `tasks.py` modules that live inside our Django apps, which keeps our project layout consistent and organized.

To use Dramatiq, you also need a separate service to be your message broker. We used [Redis](https://docs.celeryq.dev/en/latest/getting-started/backends-and-brokers/redis.html) for this project.

The Dramatiq-specific settings in our `common.py` settings module is `DRAMATIQ_BROKER`.

If you want to see all the steps to get Django, Dramatiq, Redis, and Postgres working on your machine [check out the README for this project on GitHub](https://github.com/TwilioDevEd/appointment-reminders-django).

```py title="Dramatiq settings" description="appointments/settings/common.py"
# !mark(29:43)
"""
Common Django settings for the appointments project.

See the local, test, and production settings modules for the values used
in each environment.

For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'not-so-secret'

# Twilio API
TWILIO_NUMBER = os.environ.get('TWILIO_NUMBER')
TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID')
TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN')

DRAMATIQ_BROKER = {
    "BROKER": "dramatiq.brokers.redis.RedisBroker",
    "OPTIONS": {
        "url": 'redis://localhost:6379/0',
    },
    "MIDDLEWARE": [
        "dramatiq.middleware.Prometheus",
        "dramatiq.middleware.AgeLimit",
        "dramatiq.middleware.TimeLimit",
        "dramatiq.middleware.Callbacks",
        "dramatiq.middleware.Retries",
        "django_dramatiq.middleware.AdminMiddleware",
        "django_dramatiq.middleware.DbConnectionsMiddleware",
    ]
}

# Reminder time: how early text messages are sent in advance of appointments
REMINDER_TIME = 30  # minutes

ALLOWED_HOSTS = []

# Application definition

DJANGO_APPS = (
    'django_dramatiq',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles'
)

THIRD_PARTY_APPS = (
    'bootstrap3',
    'django_forms_bootstrap',
    'timezone_field'
)

LOCAL_APPS = (
    'reminders',
)

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

MIDDLEWARE = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
)

ROOT_URLCONF = 'appointments.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates/'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

CRISPY_TEMPLATE_PACK = 'bootstrap3'

WSGI_APPLICATION = 'appointments.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'appointment_reminders'
    }
}


# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/

STATIC_ROOT = BASE_DIR + '/staticfiles'

STATIC_URL = '/static/'
```

Now that Dramatiq is working with our project, it's time to write a new task for sending a customer an SMS message about their appointment.

## Creating a Dramatiq task

Our task takes an appointment's ID - it's primary key - as its only argument. We could pass the `Appointment` object itself as the argument, but this best practice ensures our SMS will use the most up-to-date version of our appointment's data.

It also gives us an opportunity to check if the appointment has been deleted before the reminder was sent, which we do at the top of our function. This way we won't send SMS reminders for appointments that don't exist anymore.

```py title="Fetch appointments on Dramatiq task" description="reminders/tasks.py"
# !mark(17:26)
from __future__ import absolute_import

import arrow
import dramatiq

from django.conf import settings
from twilio.rest import Client

from .models import Appointment


# Uses credentials from the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN
# environment variables
client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)


@dramatiq.actor
def send_sms_reminder(appointment_id):
    """Send a reminder to a phone using Twilio SMS"""
    # Get our appointment from the database
    try:
        appointment = Appointment.objects.get(pk=appointment_id)
    except Appointment.DoesNotExist:
        # The appointment we were trying to remind someone about
        # has been deleted, so we don't need to do anything
        return

    appointment_time = arrow.get(appointment.time, appointment.time_zone.zone)
    body = 'Hi {0}. You have an appointment coming up at {1}.'.format(
        appointment.name,
        appointment_time.format('h:mm a')
    )

    client.messages.create(
        body=body,
        to=appointment.phone_number,
        from_=settings.TWILIO_NUMBER,
    )
```

Let's stay in our task a bit longer, because the next step is to compose the text of our SMS message.

## Sending an SMS Message

We use the handy [arrow](https://github.com/crsmithdev/arrow) library to format our appointment's time. After that, we use the [twilio-python](https://github.com/twilio/twilio-python) library to send our message.

We instantiate a Twilio REST client at the top of the module, which looks for `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` environment variables to authenticate itself. You can find the correct values for you in [your account dashboard](/user/account/settings).

To send the SMS message itself, you'll call `client.messages.create()`, passing arguments for the body of the SMS message, the recipient's phone number, and the Twilio phone number you want to send this message from. Twilio will deliver the SMS message immediately.

```py title="Send SMS on Dramatiq task" description="reminders/tasks.py"
from __future__ import absolute_import

import arrow
import dramatiq

from django.conf import settings
from twilio.rest import Client

from .models import Appointment


# Uses credentials from the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN
# environment variables
client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)


@dramatiq.actor
def send_sms_reminder(appointment_id):
    """Send a reminder to a phone using Twilio SMS"""
    # Get our appointment from the database
    try:
        appointment = Appointment.objects.get(pk=appointment_id)
    except Appointment.DoesNotExist:
        # The appointment we were trying to remind someone about
        # has been deleted, so we don't need to do anything
        return

    appointment_time = arrow.get(appointment.time, appointment.time_zone.zone)
    body = 'Hi {0}. You have an appointment coming up at {1}.'.format(
        appointment.name,
        appointment_time.format('h:mm a')
    )

    client.messages.create(
        body=body,
        to=appointment.phone_number,
        from_=settings.TWILIO_NUMBER,
    )
```

With our `send_sms_reminder` task complete, let's look at how to call it when our appointments are created or updated.

## Calling our Reminder Task

We added a new method on our `Appointment` model to help schedule a reminder for an individual appointment.

Our method starts by using [arrow](https://github.com/crsmithdev/arrow) again to build a new datetime with the appointment's `time` and `time_zone`.

Moving backward in time can be tricky in normal Python, but arrow's `.replace()` method lets us subtract minutes from our `appointment_time`. The `REMINDER_TIME` setting defaults to 30 minutes.

We finish by invoking our Dramatiq task, using the [delay](https://dramatiq.io/guide.html#scheduling-messages) parameter to tell Dramatiq when this task should execute.

We can't import the `send_sms_reminder` task at the top of our `models.py` module because the `tasks.py` module imports the `Appointment` model. Importing it in our `schedule_reminder` method avoids a circular dependency.

```py title="Schedule a new Dramatiq task" description="reminders/models.py"
# !mark(42:58)
from __future__ import unicode_literals

import redis

from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from django.urls import reverse
from six import python_2_unicode_compatible
from timezone_field import TimeZoneField

import arrow


@python_2_unicode_compatible
class Appointment(models.Model):
    name = models.CharField(max_length=150)
    phone_number = models.CharField(max_length=15)
    time = models.DateTimeField()
    time_zone = TimeZoneField(default='UTC')

    # Additional fields not visible to users
    task_id = models.CharField(max_length=50, blank=True, editable=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return 'Appointment #{0} - {1}'.format(self.pk, self.name)

    def get_absolute_url(self):
        return reverse('reminders:view_appointment', args=[str(self.id)])

    def clean(self):
        """Checks that appointments are not scheduled in the past"""

        appointment_time = arrow.get(self.time, self.time_zone.zone)

        if appointment_time < arrow.utcnow():
            raise ValidationError(
                'You cannot schedule an appointment for the past. '
                'Please check your time and time_zone')

    def schedule_reminder(self):
        """Schedule a Dramatiq task to send a reminder for this appointment"""

        # Calculate the correct time to send this reminder
        appointment_time = arrow.get(self.time, self.time_zone.zone)
        reminder_time = appointment_time.shift(minutes=-30)
        now = arrow.now(self.time_zone.zone)
        milli_to_wait = int(
            (reminder_time - now).total_seconds()) * 1000

        # Schedule the Dramatiq task
        from .tasks import send_sms_reminder
        result = send_sms_reminder.send_with_options(
            args=(self.pk,),
            delay=milli_to_wait)

        return result.options['redis_message_id']

    def save(self, *args, **kwargs):
        """Custom save method which also schedules a reminder"""

        # Check if we have scheduled a reminder for this appointment before
        if self.task_id:
            # Revoke that task in case its time has changed
            self.cancel_task()

        # Save our appointment, which populates self.pk,
        # which is used in schedule_reminder
        super(Appointment, self).save(*args, **kwargs)

        # Schedule a new reminder task for this appointment
        self.task_id = self.schedule_reminder()

        # Save our appointment again, with the new task_id
        super(Appointment, self).save(*args, **kwargs)

    def cancel_task(self):
        redis_client = redis.Redis(host=settings.REDIS_LOCAL, port=6379, db=0)
        redis_client.hdel("dramatiq:default.DQ.msgs", self.task_id)
```

The last thing we need to do is ensure Django calls our `schedule_reminder` method every time an `Appointment` object is created or updated.

## Overriding the Appointment Save Method

The best way to do that is to [override our model's save method](https://docs.djangoproject.com/en/2.1/topics/db/models/#overriding-predefined-model-methods), including an extra call to `schedule_reminder` after the object's primary key has been assigned.

**Avoiding duplicate or mistimed reminders**

Scheduling a Dramatiq task every time an appointment is saved has an unfortunate side effect - our customers will receive duplicate reminders if an appointment was saved more than once. And those reminders could be sent at the wrong time if an appointment's `time` field was changed after its creation.

To fix this, we keep track of each appointment's reminder task through the `task_id` field, which stores Dramatiq's unique identifier for each task.

We then look for a previously scheduled task at the top of our custom `save` method and cancel it if present.

This guarantees that one and exactly one reminder will be sent for each appointment in our database, and that it will be sent at the most recent `time` provided for that appointment.

```py title="Overridden save() method to call schedule_reminder()" description="reminders/models.py"
# !mark(60:76)
from __future__ import unicode_literals

import redis

from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from django.urls import reverse
from six import python_2_unicode_compatible
from timezone_field import TimeZoneField

import arrow


@python_2_unicode_compatible
class Appointment(models.Model):
    name = models.CharField(max_length=150)
    phone_number = models.CharField(max_length=15)
    time = models.DateTimeField()
    time_zone = TimeZoneField(default='UTC')

    # Additional fields not visible to users
    task_id = models.CharField(max_length=50, blank=True, editable=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return 'Appointment #{0} - {1}'.format(self.pk, self.name)

    def get_absolute_url(self):
        return reverse('reminders:view_appointment', args=[str(self.id)])

    def clean(self):
        """Checks that appointments are not scheduled in the past"""

        appointment_time = arrow.get(self.time, self.time_zone.zone)

        if appointment_time < arrow.utcnow():
            raise ValidationError(
                'You cannot schedule an appointment for the past. '
                'Please check your time and time_zone')

    def schedule_reminder(self):
        """Schedule a Dramatiq task to send a reminder for this appointment"""

        # Calculate the correct time to send this reminder
        appointment_time = arrow.get(self.time, self.time_zone.zone)
        reminder_time = appointment_time.shift(minutes=-30)
        now = arrow.now(self.time_zone.zone)
        milli_to_wait = int(
            (reminder_time - now).total_seconds()) * 1000

        # Schedule the Dramatiq task
        from .tasks import send_sms_reminder
        result = send_sms_reminder.send_with_options(
            args=(self.pk,),
            delay=milli_to_wait)

        return result.options['redis_message_id']

    def save(self, *args, **kwargs):
        """Custom save method which also schedules a reminder"""

        # Check if we have scheduled a reminder for this appointment before
        if self.task_id:
            # Revoke that task in case its time has changed
            self.cancel_task()

        # Save our appointment, which populates self.pk,
        # which is used in schedule_reminder
        super(Appointment, self).save(*args, **kwargs)

        # Schedule a new reminder task for this appointment
        self.task_id = self.schedule_reminder()

        # Save our appointment again, with the new task_id
        super(Appointment, self).save(*args, **kwargs)

    def cancel_task(self):
        redis_client = redis.Redis(host=settings.REDIS_LOCAL, port=6379, db=0)
        redis_client.hdel("dramatiq:default.DQ.msgs", self.task_id)
```

Fun tutorial, right? Where can we take it from here?

## Finishing the Django Appointment Reminder Implementation

We used [Django's class-based views](https://docs.djangoproject.com/en/2.1/topics/class-based-views/) to help us build out the features to support CRUD operations on our `Appointment` model.

We then integrated [Dramatiq](https://dramatiq.io/) into our project and used the [twilio-python](https://github.com/twilio/twilio-python) SDK to send SMS reminders about our appointments asynchronously.

You'll find instructions to run this project locally in [its GitHub README](https://github.com/TwilioDevEd/appointment-reminders-django).

```py title="The Appointment Model" description="reminders/models.py"
from __future__ import unicode_literals

import redis

from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from django.urls import reverse
from six import python_2_unicode_compatible
from timezone_field import TimeZoneField

import arrow


@python_2_unicode_compatible
class Appointment(models.Model):
    name = models.CharField(max_length=150)
    phone_number = models.CharField(max_length=15)
    time = models.DateTimeField()
    time_zone = TimeZoneField(default='UTC')

    # Additional fields not visible to users
    task_id = models.CharField(max_length=50, blank=True, editable=False)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return 'Appointment #{0} - {1}'.format(self.pk, self.name)

    def get_absolute_url(self):
        return reverse('reminders:view_appointment', args=[str(self.id)])

    def clean(self):
        """Checks that appointments are not scheduled in the past"""

        appointment_time = arrow.get(self.time, self.time_zone.zone)

        if appointment_time < arrow.utcnow():
            raise ValidationError(
                'You cannot schedule an appointment for the past. '
                'Please check your time and time_zone')

    def schedule_reminder(self):
        """Schedule a Dramatiq task to send a reminder for this appointment"""

        # Calculate the correct time to send this reminder
        appointment_time = arrow.get(self.time, self.time_zone.zone)
        reminder_time = appointment_time.shift(minutes=-30)
        now = arrow.now(self.time_zone.zone)
        milli_to_wait = int(
            (reminder_time - now).total_seconds()) * 1000

        # Schedule the Dramatiq task
        from .tasks import send_sms_reminder
        result = send_sms_reminder.send_with_options(
            args=(self.pk,),
            delay=milli_to_wait)

        return result.options['redis_message_id']

    def save(self, *args, **kwargs):
        """Custom save method which also schedules a reminder"""

        # Check if we have scheduled a reminder for this appointment before
        if self.task_id:
            # Revoke that task in case its time has changed
            self.cancel_task()

        # Save our appointment, which populates self.pk,
        # which is used in schedule_reminder
        super(Appointment, self).save(*args, **kwargs)

        # Schedule a new reminder task for this appointment
        self.task_id = self.schedule_reminder()

        # Save our appointment again, with the new task_id
        super(Appointment, self).save(*args, **kwargs)

    def cancel_task(self):
        redis_client = redis.Redis(host=settings.REDIS_LOCAL, port=6379, db=0)
        redis_client.hdel("dramatiq:default.DQ.msgs", self.task_id)
```

## Where to Next?

And with a little code and a dash of configuration, we're ready to get automated appointment reminders firing in our application. Good work!

If you are a Python developer working with Twilio, you might want to check out the following resources for Python:

* **[Browser Call](/docs/voice/sdks/javascript/get-started)**: Put a button on your web page that connects visitors to live support or sales people via telephone.
* **[Verify Python Quickstart](/docs/verify/quickstarts/python-flask)**: Verify phone numbers and add an additional layer of security to your Python app by using [Twilio Verify](/docs/verify).
