Skip to content
Django Workshop/Tutorial Creating REST Endpoints
Branch: master
Clone or download

README.md

Workshop Outline:

  1. Project Startup (prerequisite)
  2. Create Event Model
  3. Explore Django Admin
  4. Event Serializer, View and URLs
  5. Command Line Inspection of Model Objects vs Serialized Object

Bonus:

  1. Deploy to Heroku
  2. Repeat Model, Serializer, View and URLs flow with Locations

1 - Backend Startup

Base folder: mkdir django-workshop && cd django-workshop

Create backend environment with packages:

  • mkdir events-backend && cd events-backend
  • pip install pipenv
  • pipenv install django djangorestframework

Activate Backend Shell: pipenv shell

Start Django Project: django-admin startproject main_project .

(if you did not use the ., move into your project: cd main_project)

Create Events App: python manage.py startapp events

Start Server: python manage.py runserver

Cool - can't really do anything yet tho 😎

Housekeeping

Make Git Ignore

Create a file in the root directory: .gitnignore

Place the contents from this URL: https://www.gitignore.io/api/django

Migrate

Migrate the DB with the baked in models (user, etc):

python manage.py migrate

Inject Apps into main_project

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # import rest_framework for viewset and serializer helpers
    'rest_framework',
    # add `events` application to main project
    'events'
]

COMMIT 1

Started projet and application. Imported DRF.

We end up with a file structure like this:

▾ events/
  ▸ migrations/
  - __init__.py
  - admin.py
  - apps.py
  - models.py
  - tests.py
  - views.py
▾ main_project/
  - __init__.py
  - settings.py
  - urls.py
  - wsgi.py
- manage.py
- Pipfile
- Pipfile.lock

Overview of Backend

The parts of our django project

Building Model, Serializer, View and URL for Events

To build an API endpoint (URL to GET, POST, PUT and DELETE) we will do the following:

  1. Create our new model (and make migrations)
  2. Register Locations in Django Admin
  3. Create a serializer for events
  4. Create views for our events
  5. Route up our URLs to our views

2 - Create Event Model

Create our Events Model

Add the following code to: /events/models.py

from django.db import models
from django.utils import timezone
from django.contrib.auth import get_user_model

# basic information for our events 
class Event(models.Model):
    # Only required field is title
    title = models.CharField(max_length=256)
    presenter = models.ForeignKey(
      get_user_model(),
      on_delete=models.SET_NULL,
      null=True,
      blank=True,
    )
    # Default time is now if not provided
    time = models.DateTimeField(default=timezone.now)
    location = models.CharField(max_length=256, blank=True)
    description = models.TextField(blank=True)

And migrate the database:

python manage.py makemigrations

python manage.py migrate

3 - Explore Django Admin

Django Admin

Think of the Django Admin as your CMS and user management portal.

Register Events Model in Admin

Navigate to: /events/admin.py and add the following code to register our model in our django admin:

from django.contrib import admin
from .models import Event

class EventAdmin(admin.ModelAdmin):
    list_display = ('title', 'presenter', 'time', 'location')

admin.site.register(Event, EventAdmin)

Create Superuser

python manage.py createsuperuser

Enter information required and then startup server:

python manage.py runserver

Log in with your login credentials. Test the admin interface by doing the following:

  1. Create two new events
  2. Create a new user
  3. Edit those user permissions so they (1) are staff and (2) can create and view events
  4. Log out, then log in as that user
  5. Notice that you can view, but not edit the events you created
  6. Create an event. Notice create and view events is all you can do.
  7. Log back in as your superuser and edit some events

COMMIT 2

Create events model. Add events to django admin.

4 - Event Serializer, View and URLs

Make Serializer for Events

Create the file /events/serializers.py file in events and create serializer:

from django.contrib.auth.models import User, Group
from .models import Event
from rest_framework import serializers


# serializers to convert from Django model objects to JSON
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'groups')

# or specify all fields
class EventSerializer(serializers.ModelSerializer):
    class Meta:
        model = Event
        fields = '__all__'

Creating a View for our Events

Navigate to the views.py file in our events app. Create viewsets:

from django.contrib.auth.models import User
from rest_framework import viewsets
from .models import Event
from .serializers import UserSerializer, EventSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer

class EventViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    resource_name = 'events'
    queryset = Event.objects.all().order_by('-time')
    serializer_class = EventSerializer

Connecting our URLs

In api/urls.py, register the EventViewSet and the UserViewSet

from django.urls import include, path
from django.contrib import admin
from rest_framework import routers
from events import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'events', views.EventViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('admin/', admin.site.urls),
]

Start Server and Witness the Magic DRF API

Run: python manage.py runserver

And navigate to Events API: http://localhost:8000/api/events/

Play around and try using the GUI provided for free from DRF to Create, Edit, Update and Delete events!

COMMIT 3

Backend API endpoint created for events

5 - Command Line Inspection of Model Objects vs Serialized Objects

What's a serializer doin anyhow?!

Serialization from Django Model to JSON

Test It Out

Let's take a look at what we created by using the python shell by running python manage.py shell. This is the basic python shell you would get by running python butmanage.py sets the DJANGO_SETTINGS_MODULE variable, which gives Django the Python import path to your api/settings.py file.

Once inside the shell, we can explore the model and creating instances:

>>> from events.models import Event # import the model that we've created
>>> Event.objects.all() # objects is the manager for the model. Here we see we have no instances
      <QuerySet []>
>>> Event.objects.create() # create an object
      <Event: Event object (1)>
>>> Event.objects.all() # confirm object is created
      <QuerySet [<Event: Event object (1)>]>
>>> Event.objects.get(id=1) # get first object
      <Event: Event object (1)>
>>> event1 = Event.objects.get(id=1) # assign instance to variable to we can edit itt
>>> event1.id # access id
      1
>>> event1.title # title is blank
      ''
>>> event1.time # event time defaults to creation time
      datetime.datetime(2019, 1, 30, 22, 11, 57, 487525, tzinfo=<UTC>)
>>> event1.title = 'API Driven Design with Django Rest Framework' # set the title
>>> event1.location = 'LenderClose' # set the location
>>> Event.objects.get(id=1).title # notice the model hasn't actually changed yet
      ''
>>> event1.save() # save the event
>>> Event.objects.get(id=1).title # make sure changes persisted
      'API Driven Design with Django Rest Framework'
>>> Event.objects.create() # create instance 2
      <Event: Event object (2)>
>>> Event.objects.create() # create instance 3
      <Event: Event object (3)>
>>> Event.objects.all() # check that there are three objects
      <QuerySet [<Event: Event object (1)>, <Event: Event object (2)>, <Event: Event object (3)>]>
>>> Event.objects.filter(title='') # two events have blank title
      <QuerySet [<Event: Event object (2)>, <Event: Event object (3)>]>
>>> Event.objects.filter(title='API Driven Design with Django Rest Framework') # one event has the persisted title
      <QuerySet [<Event: Event object (1)>]>

Similar to how we used the django shell to investigate our model to create an instance of our model Event, we can use it to seralize it into a python native datatype. We can then further use the DRF class JSONRenderer to render our dictionary as JSON:

>>> from events.models import Event # import our model
>>> from events.serializers import EventSerializer # import our serializer
>>> from rest_framework.renderers import JSONRenderer # import the class that will render to JSON
>>> events = Event.objects.all()
>>> events # confirm we still have data in the DB - otherwise make new ones
      <QuerySet [<Event: Event object (1)>, <Event: Event object (2)>, <Event: Event object (3)>]>
>>> event1 = events[0] # store first event as variable
>>> print(event1) # event1 is a model object/instance
      Event object (1)
>>> event1.title # access event1 title as model instance
      'API Driven Design with Django Rest Framework'
>>> serializer1 = EventSerializer(event1) # serialize event1 to python native datatype
>>> print(serializer1)
      EventSerializer(<Event: Event object (1)>):
         id = IntegerField(read_only=True)
         title = CharField(max_length=256)
         presenter = CharField(allow_blank=True, max_length=256, required=False)
         time = DateTimeField()
         location = CharField(allow_blank=True, max_length=256, required=False)
         coordinator = CharField(allow_blank=True, max_length=256, required=False)
         description = CharField(allow_blank=True, max_length=256, required=False)
>>> print(serializer1.data) # serializer.data contains a dictionary with object data
      {'id': 1, 'title': 'API Driven Design with Django Rest Framework', 'presenter': 'Michael Johnson', 'time': '2019-01-30T22:11:57.487525Z', 'location': '', 'coordinator': '', 'description': ''}
>>> print(serializer1.data['title']) # access the title like you would a dictionary
      API Driven Design with Django Rest Framework
>>> content = JSONRenderer().render(serializer1.data) # render the data as JSON
>>> print(content) # print JSON as bytes
      b'{"id":1,"title":"API Driven Design with Django Rest Framework","presenter":"Michael Johnson","time":"2019-01-30T22:11:57.487525Z","location":"","coordinator":"","description":""}'
>>> print(content.decode("utf-8")) # can convert bytes to string
      {"id":1,"title":"API Driven Design with Django Rest Framework","presenter":"Michael Johnson","time":"2019-01-30T22:11:57.487525Z","location":"","coordinator":"","description":""}
>>> type(content.decode("utf-8"))
      <class 'str'>

6 - Deploy to Heroku

Deploying Django on Heroku

Example for using Postgres with Django on Heroku

Deploying django applications onto heroku is similar to other application. Follow these steps:

  • If you don't have one, create an account at Heroku.com
  • Install reququired packages: pipenv install django-heroku gunicorn
  • Create file Procfile in the root directory and add to it: web: gunicorn project_name.wsgi
  • Add to settings.py:
import django_heroku 
# Then all the way at the bottom of the file
# ... 
django_heroku.settings(locals())
  • If you don't have the Heroku CLI, follow these instructions or: brew tap heroku/brew && brew install heroku
  • Login to heroku cli with the command: heroku login
  • Create your application with the command: heroku create
  • Make sure your commits are up today and push to heroku: git push heroku master similar to pushing to github remote
  • After this finished, run migrations: heroku run python manage.py migrate
  • If this doesn't work try the following debug tools
    • Check logs with: heroku logs --tail
    • See if you get errors spinning up web env locally: heroku local web

6 - Deploy to Heroku

COMMIT 4

Setup application to deploy on Heroku

7 - Repeat Model, Serializer, View and URLs flow with Locations

Wash, Rinse, Repeat: Building Model, Serializer, View and URL for Locations

To build an API endpoint (URL to GET, POST, PUT and DELETE) we will do the following:

  1. Create our new model (and make migrations)
  2. Register Locations in Django Admin
  3. Create a serializer for locations
  4. Create views for our locations
  5. Route up our URLs to our locations

Create our Locations Model

Update the following code in /events/models.py to look like:

from django.db import models
from django.utils import timezone
from django.contrib.auth import get_user_model

class Location(models.Model):
    # Only required field is title
    name = models.CharField(max_length=256)
    company = models.CharField(max_length=256, blank=True)
    address = models.CharField(max_length=256, blank=True)
    contact_primary = models.ForeignKey(
      get_user_model(),
      on_delete=models.SET_NULL,
      null=True,
      blank=True,
      related_name="contact_primary"
    )
    contact_secondary = models.ForeignKey(
      get_user_model(),
      on_delete=models.SET_NULL,
      null=True,
      blank=True,
      related_name="contact_secondary"
    )
    description = models.TextField(blank=True)

# basic information for our events
class Event(models.Model):
    # Only required field is title
    title = models.CharField(max_length=256)
    presenter = models.ForeignKey(
      get_user_model(),
      on_delete=models.SET_NULL,
      null=True,
      blank=True,
      related_name='events'
    )
    # Default time is now if not provided
    time = models.DateTimeField(default=timezone.now)
    location = models.ForeignKey(
      Location,
      on_delete=models.SET_NULL,
      null=True,
      blank=True,
      related_name='events'
    )
    description = models.TextField(blank=True)

And migrate the database:

python manage.py makemigrations

python manage.py migrate

Note that if you've created events already and assigned values to locations, you'll receive an error similar to: django.db.utils.IntegrityError: The row in table 'events_event' with primary key '1' has an invalid foreign key: events_event.location_id contains a value 'LenderClose' that does not have a corresponding value in events_location.id.

This is because we have the string 'LenderClose' in the location, and this is not a valid PK for our related field. The easiest way to do this is to delete your DB file (sqlite) and rerun makemigrations. A less destructive alternative is:

  • Undo the changes in the working directory. I recommend using git stash so we can pop them back on after we're done
  • Log into django admin
  • Delete the events that have been created
  • Pop the stashed changes with git stash pop
  • Then rerun makemigrations and migrate

This may not be the most elegant solution, but it works. I was also able to successfully create a location, then change the event location field to a pk value before running migrations. Another solution is to write a custom migration to change the data so that it tranitions smoothly. This is an important note that getting your database and related field structure right from the start helps from messy database changes down the line :)

Register Location Model in Admin

Navigate to: /events/admin.py and add the following code to register our model in our django admin:

from django.contrib import admin
from .models import Event, Location

class EventAdmin(admin.ModelAdmin):
    list_display = ('title', 'presenter', 'time', 'location')
class LocationAdmin(admin.ModelAdmin):
    list_display = ('name', 'company')

admin.site.register(Event, EventAdmin)
admin.site.register(Location, LocationAdmin)

Make Serializer for Events

Navigate to the file /events/serializers.py file update serializer. Additional to creating the Location serializer, we are creating some linked related serializer fields. This will allow us to get the events related to Users and Locations.

from django.contrib.auth.models import User, Group
from .models import Event, Location
from rest_framework import serializers


# serializers to convert from Django model objects to JSON
class UserSerializer(serializers.HyperlinkedModelSerializer):
    events = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='event-detail'
    )
    class Meta:
        model = User
        fields = ('url', 'first_name', 'last_name', 'username', 'email', 'groups', 'events')

# or specify all fields
class EventSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Event
        fields = '__all__'

class LocationSerializer(serializers.HyperlinkedModelSerializer):
    events = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='event-detail'
    )
    class Meta:
        model = Location
        fields = ('url', 'name', 'company', 'address', 'contact_primary', 'contact_secondary', 'description', 'events')

Creating a View for our Locations

Navigate to the views.py and update:

from django.contrib.auth.models import User
from rest_framework import viewsets
from .models import Event, Location
from .serializers import UserSerializer, EventSerializer, LocationSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer

class EventViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    resource_name = 'events'
    queryset = Event.objects.all().order_by('-time')
    serializer_class = EventSerializer

class LocationViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    resource_name = 'locations'
    queryset = Location.objects.all().order_by('name')
    serializer_class = LocationSerializer

Connecting Locations into our URLs

In api/urls.py, register the EventViewSet and the UserViewSet

from django.urls import include, path
from django.contrib import admin
from rest_framework import routers
from events import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'events', views.EventViewSet)
router.register(r'locations', views.LocationViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('admin/', admin.site.urls),
]

COMMIT 5

Backend API endpoint created for events-app

You can’t perform that action at this time.