Menu

Rate this page:

Thanks for rating this page!

We are always striving to improve our documentation quality, and your feedback is valuable to us. How could this documentation serve you better?

Appointment Reminders with Python and Django

Download the Code

Ready to implement SMS appointment reminders in your Django web application? We'll use the Twilio Python Helper Library and Twilio SMS 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.

Appointment Reminder Building Blocks

Here are the technologies we'll use:

  • Django to create a database-driven web application
  • The Messages Resource from Twilio's REST API to send text messages
  • Celery 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 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.

Let's get started!

Meet our Django Appointment Reminder Stack

We're building this app for Django 1.8 on Python 3.4, but all our code works in Python 2.7 too. We're big fans of Two Scoops of Django and we will use many best practices outlined there.

In addition to Celery, we will use a few other Python libraries to make our task easier:

We will also use PostgreSQL for our database and Redis as our Celery message broker.

Loading Code Sample...
      
      
          
          
          
          
        
      requirements.txt

      Project dependencies

      requirements.txt

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

      Creating an 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 appoinment. Now let's start by looking at the model, where we decide what information we want to store with the appointment.

      Visit our Appointment model

      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.

      Python 2/3 Compatibility

      To keep our project compatible with Python 2, we use the python_2_unicode_compatible decorator on our model.

      This decorator tells Django to use our model's __str__ method to make a __unicode__ method when needed for Python 2.

      You can read more about it in the Porting to Python 3 section of the Django documentation.

      Loading Code Sample...
          
          
              
              
              
              
            
          reminders/models.py

          Appointment model fields

          reminders/models.py

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

          Check out the new Appointment view

          New Appointment View

          Django lets developers write views as functions or classes.

          Class-based views are great when your views need to support simple, CRUD-like features - perfect for our appointments project.

          To make a view for creating new Appointment objects, we'll use Django's generic 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 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.

          This mixin tells our view to pass the success_message property of our class to the Django messages framework after a successful creation. We will display those messages to the user in our templates.

          Loading Code Sample...
              
              
                  
                  
                  
                  
                
              reminders/views.py

              Create new Appointment

              reminders/views.py

              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 URL with the view we just created

              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.

              Loading Code Sample...
                  
                  
                      
                      
                      
                      
                    
                  reminders/urls.py

                  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.

                  Check out the new Appointment 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 for the front end of our app, and we use the 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.

                  Loading Code Sample...
                      
                      
                          
                          
                          
                          
                        
                      templates/reminders/appointment_form.html

                      New Appointment form

                      templates/reminders/appointment_form.html

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

                      Take a look at the datepicker widget

                      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 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.

                      Loading Code Sample...
                          
                          
                              
                              
                              
                              
                            
                          templates/reminders/appointment_form.html

                          Datepicker widget

                          templates/reminders/appointment_form.html

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

                          Adding a get_absolute_url method

                          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 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 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.

                          Loading Code Sample...
                              
                              
                                  
                                  
                                  
                                  
                                
                              reminders/models.py

                              get_absolute_url method

                              reminders/models.py

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

                              See what we need to interact with 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 appoinments

                              Because these are basic CRUD-like operations, we'll keep using Django's generic class-based views to save us a lot of work.

                              Loading Code Sample...
                                  
                                  
                                      
                                      
                                      
                                      
                                    
                                  reminders/views.py

                                  Interacting with appointments

                                  reminders/views.py

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

                                  Check out the view to list appointments

                                  Showing a List of Appointments

                                  Django's 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.

                                  And wiring up this view in our reminders/urls.py module is just as easy as our AppointmentCreateView:

                                  from .views import AppointmentListView
                                  
                                  url(r'^$', AppointmentListView.as_view(), name='list_appointments'),
                                  
                                  Loading Code Sample...
                                      
                                      
                                          
                                          
                                          
                                          
                                        
                                      reminders/views.py

                                      List appointments

                                      reminders/views.py

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

                                      See the Appointment list template

                                      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 %} template tag to include links to our edit and delete views.

                                      Loading Code Sample...
                                          
                                          
                                              
                                              
                                              
                                              
                                            
                                          templates/reminders/appointment_list.html

                                          Render Appointment list

                                          templates/reminders/appointment_list.html

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

                                          See how the new Appointment form can be used to update appointments

                                          Tweaking our Form Template

                                          Django's UpdateView makes it easy 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 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.

                                          Loading Code Sample...
                                              
                                              
                                                  
                                                  
                                                  
                                                  
                                                
                                              templates/reminders/appointment_form.html

                                              Tweaking new Appointment form

                                              templates/reminders/appointment_form.html

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

                                              DeleteView to the rescue

                                              Delete View

                                              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:

                                              from .views import AppointmentDeleteView
                                              
                                              url(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 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.

                                              Loading Code Sample...
                                                  
                                                  
                                                      
                                                      
                                                      
                                                      
                                                    
                                                  reminders/views.py

                                                  Import generic views

                                                  reminders/views.py

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

                                                  Check out the requirements to send a reminder

                                                  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.

                                                  The most popular Python library for asynchronous tasks is Celery. To integrate Celery 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 Celery so it can be executed asynchronously
                                                  • Run a separate Celery 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 Celery, you might want to skim its Introduction to Celery page before proceeding.

                                                  Loading Code Sample...
                                                      
                                                      
                                                          
                                                          
                                                          
                                                          
                                                        
                                                      reminders/tasks.py

                                                      Send SMS reminder task

                                                      reminders/tasks.py

                                                      Next we will configure Celery to work with our project.

                                                      Setting up Celery

                                                      Setting up Celery

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

                                                      By following the instructions in the Celery docs, we can include our Celery settings in our Django settings modules. We can also write our Celery tasks in tasks.py modules that live inside our Django apps, which keeps our project layout consistent and simple.

                                                      To use Celery, you also need a separate service to be your message broker. We used Redis for this project.

                                                      The two Celery-specific settings in our common.py settings module are BROKER_URL and BROKER_POOL_LIMIT.

                                                      BROKER_POOL_LIMIT limits how many connections Celery will make to our broker - useful when deploying to a Platform-as-a-Service (PaaS) like Heroku where the free tiers of Redis services limit your application to a handful of connections.

                                                      If you want to see all the steps to get Django, Celery, Redis, and Postgres working on your machine check out the README for this project on GitHub.

                                                      Loading Code Sample...
                                                          
                                                          
                                                              
                                                              
                                                              
                                                              
                                                            
                                                          appointments/settings/common.py

                                                          Celery settings

                                                          appointments/settings/common.py

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

                                                          Check how to create a new Celery task

                                                          Creating a Celery 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.

                                                          Loading Code Sample...
                                                              
                                                              
                                                                  
                                                                  
                                                                  
                                                                  
                                                                
                                                              reminders/tasks.py

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

                                                              See how to send an SMS with Twilio Python client

                                                              Sending an SMS Message

                                                              We use the handy arrow library to format our appointment's time. After that, we use the 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.

                                                              Sending the SMS message itself is as easy as calling client.messages.create(), passing arguments for the body of the SMS message, the receipient's phone number, and the Twilio phone number you want to send this message from. Twilio will deliver the SMS message immediately.

                                                              Loading Code Sample...
                                                                  
                                                                  
                                                                      
                                                                      
                                                                      
                                                                      
                                                                    
                                                                  reminders/tasks.py

                                                                  Send SMS on Celery task

                                                                  reminders/tasks.py

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

                                                                  How do we call this task?

                                                                  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 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 easily subtract minutes from our appointment_time. The REMINDER_TIME setting defaults to 30 minutes.

                                                                  We finish by invoking our Celery task, using the eta parameter to tell Celery 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.

                                                                  Loading Code Sample...
                                                                      
                                                                      
                                                                          
                                                                          
                                                                          
                                                                          
                                                                        
                                                                      reminders/models.py

                                                                      Schedule a new Celery task

                                                                      reminders/models.py

                                                                      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

                                                                      Overriding the Appointment Save Method

                                                                      The best way to do that is to override our model's save method, including an extra call to schedule_reminder after the object's primary key has been assigned.

                                                                      Avoiding duplicate or mistimed reminders

                                                                      Scheduling a Celery 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 Celery's unique identifier for each task.

                                                                      We then look for a previously scheduled task at the top of our custom save method and revoke 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.

                                                                      Loading Code Sample...
                                                                          
                                                                          
                                                                              
                                                                              
                                                                              
                                                                              
                                                                            
                                                                          reminders/models.py

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

                                                                          Where to next?

                                                                          Finishing the Django Appointment Reminder Implementation

                                                                          We used Django's class-based views to help us quickly build out the features to support simple CRUD operations on our Appointment model.

                                                                          We then integrated Celery into our project and used the twilio-python helper library to send SMS reminders about our appointments asynchronously.

                                                                          You'll find instructions to run this project locally in its GitHub README. You can also deploy this project to Heroku in moments by following instructions in the same place.

                                                                          Loading Code Sample...
                                                                              
                                                                              
                                                                                  
                                                                                  
                                                                                  
                                                                                  
                                                                                
                                                                              reminders/models.py

                                                                              The Appointment Model

                                                                              reminders/models.py

                                                                              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 other tutorials in Python:

                                                                              Browser Call

                                                                              Put a button on your web page that connects visitors to live support or sales people via telephone.

                                                                              Two-Factor Authentication

                                                                              Improve the security of your Python app's login functionality by adding two-factor authentication via text message.

                                                                              Did this help?

                                                                              Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!

                                                                              Andrew Baker Agustin Camino David Prothero Hector Ortega Paul Kamp  Kat King

                                                                              Need some help?

                                                                              We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

                                                                              Loading Code Sample...