Django: Setting Up First App + Intro to Models and Commands

Seth Massarsky
10 min readMay 23, 2021

--

This post is a continuation of my last, which focused on setting up a Django project. The project that I’m currently working on is a Django backend for a fantasy hockey application. I’ve already built out a lot of this in Ruby on Rails, so it’s mostly a project for me to reacclimate to Python and Django. If you’d like to check out the repo, you can find it here. This week I focused on creating my first app, which I’m using to pull data from the NHL’s stats API. I’m saving teams, players, games and scoring events, and we’ll use this data later on to make our ‘matchups’ come to life. In this post we’ll focus on creating a new app, writing models and creating commands that will fetch data from the NHL’s stats API and seed our database.

Creating Your App

So we’ll start out with creating an app. If this is your first time working in Django, I’d suggest reading through the documentation. Particularly this spot explains the difference between a project (which we’ve already set up) and an app. Quick summary: a project is the whole thing including configuration and a collection of apps. An app is a part of a project with specific functionality. In this case, our whole project is going to be the backend for the fantasy hockey game. The app we’re writing today will contain the models for the data from the NHL’s stats API and the code to fetch and seed our database.

To create the app, make sure you’re in the same directory as manage.py, and run (replace ‘api’ with the name of your app. Admittedly I didn’t name my app very well and I’ll need to change it soon):

python manage.py startapp api

Since all we’re working on today is models, fetching and seeding the database, we can skip the part in the tutorial about views for the time being. Since we’re using PostgreSQL, make sure your server is up:

sudo service postgresql start

Django comes with a bunch of models built in, so you can go ahead and run the initial migrations:

python manage.py migrate

Here I’m going to go a little bit out of order from the documentation, and we’re going to add our api app to the INSTALLED_APPS in settings.py. So in django_fantasy_hockey/settings.py, edit INSTALLED_APPS as follows (just tack api.apps.ApiConfig on the end):

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api.apps.ApiConfig'
]

This’ll tell our project about the app, and we’ll be able to migrate without taking this step later on.

Models

So now we’ve got our app started, and we can begin working on the models. For reference, this is what I drew up for the model diagram when working in Ruby on Rails (we’ll only be working with the right side plus GamePlayer and Player today):

The final schema should work out to be nearly identical, but we’ll get to go at it with Django’s approach. To compare and contrast a bit, Django’s approach here is quite cool. In Ruby on Rails, we created a migration that inherits ActiveRecord::Migration, tell ActiveRecord pretty much exactly what we want the database to look like (including foreign keys) in that migration, then separately tell our models how to interact with each other after migrating. In Django, everything goes in the models.py file. We declare the models, their fields and how they relate to each other all in one place, then run makemigrations and Django creates our database schema for us. Say we’ve migrated and want to make changes, we just make changes to the models.py file and makemigrations — Django will diff it and create a migration that will update our schema. Cool stuff!

Ok now that I’m off that tangent, lets dive a bit into models. As discussed above, we’ll be dealing with the following models from the picture: Team, Game, Player, GameTeam, GamePlayer, Goaland Assist. To start out, all models will be created in the models.py file. When you created the app, the following import was set at the top of the file:

from django.db import models

All of your models will inherit from models.Model:

class Team(models.Model):class Game(models.Model):

Fields

As mentioned above, data to be saved in each model’s table is referenced by using one of the Fields available through the models import. Examples are models.CharField, for storing text, models.IntegerField, for storing an Integer, and models.DateTimeField, for storing a DateTime. As an example of this in action, lets take a look at my Team model:

class Team(models.Model):
api_id = models.IntegerField()
name = models.CharField(max_length=50)
abbreviation = models.CharField(max_length = 3)
city = models.CharField(max_length = 20)
division = models.CharField(max_length = 20)
conference = models.CharField(max_length = 20)
website = models.URLField()

def __str__(self):
return f"{self.city} {self.name}"

Team inherits from models.Model, then describes many fields that we’ll populate later on with information from the NHL’s API. We’ll be able to use these fields to reference that data, or query the database by (we’ll get into both of these further later, but here’s some examples):

>>> penguins = Team.objects.get(name = 'Penguins')
>>> penguins
<Team: Pittsburgh Penguins>
>>> penguins.api_id
5
>>> penguins.conference
'Eastern'

Once the database has been populated, we can search for a team in a few ways, but above we’re using Team.objects.get(). Above we’re searching for the Penguins by name. Since there is a team named ‘Penguins’, an instance of a Team is stored in the penguins variable so we can reference it later. Since we’ve defined a __str__ method for this class, when the team is referenced by itself, it’ll return a formatted string in the format “{team city} {team name}”:

def __str__(self):
return f"{self.city} {self.name}"

From here we can reference any of the fields we’ve defined in the model and saved in the database ( api_id => 5, conference => 'Eastern' ).

Relationships

Next lets dive a bit into defining relationships between models. Similar to storing data, relationships are defined using model Fields. There are three types that can be used — ForeignKeyField, ManyToManyFieldand OneToOneField.

I’m going to relate these to how model relationships are described in Rails. ForeignKey will create a create a :belongs_to, :has_many relationship. An example in my project would be between the Game and Goal models. A Goal belongs to a Game, and a Game can have many Goals. In Django, we’ll use ForeignKeyField in the Goal model:

class Goal(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='goals')

This will create a game_id column in the Goals table that references the primary key of the related game. Later on, when we have instances of goals and games, we can reference the game related to a goal by instance_of_goal.game, and the goals related to a game by instance_of_game.goals.

There are two required arguments for ForeignKey, and those are the related model and on_delete. Since python reads top down, if the model has already been defined it can be referenced by the model name directly ( Game ), or in quotations if the related model will be defined later ( 'Game' ). on_delete defines the action to be taken if the related object is deleted. Since our goal belongs to a game, what do we do with the goal if the game is deleted? In this case, we set on_delete=models.CASCADE. If the game is deleted, all of the goals are also deleted. This will help to maintain the integrity of our database — no foreign keys pointing to objects that no longer exist. CASCADE is only one way to handle this — please look at the documentation for further options.

I had a bit of trouble regarding the related names here, so discussing that briefly. In Rails, related names are specifically singular or plural of the related model ( instance_of_goal.game, instance_of_game.goals ). Django is slightly different here, that when referencing a has_many type relationship, the reverse relationship is the model name with _set appended to it. So by default, looking up the goals related to a game would be instance_of_game.goal_set. Coming from Rails, having the plural of the related name was more intuitive, so I’d note that the default can be overwritten by using the related_name option — above I’ve set the related_name='goals', so we can use my preferred version instance_of_game.goals. Fun stuff.

On to ManyToManyField. ManyToManyField describes a many-to-many relationship, the equivalent in rails being :has_many, through: :join_model. A good example of this in my project is Game and Player. A Game has many Players, and a Player plays in many Games. To represent this in a database, we have the games and players tables with a join table game_players. Django’s ManyToManyFieldwill do all of this for you in one line:

players = models.ManyToManyField(
'Player',
related_name='games'
)

Now a Game can reference its players ( instance_of_game.players ), and a Player can reference their games ( instance_of_player.games).

By default, this will automatically create a join table; however, sometimes you’d like to store some extra information in the join table. In my case, I’d like to store the position the player played in during the game and what jersey number they were wearing. Since these values can change game-to-game, we could store a most recent version of these in the Player model, but would want to have a separate record for each individual game. In order to do this, we need to create the join model ourselves, and use the through option on ManyToManyField:

class Game(models.Model):
players = models.ManyToManyField(
'Player',
through='GamePlayer',
related_name='games'
)
class Player(models.Model):
api_id = models.IntegerField()
name = models.CharField(max_length = 50)

def __str__(self):
return f"{self.name}"
class GamePlayer(models.Model):
game = models.ForeignKey(
Game,
on_delete=models.CASCADE,
related_name='game_players'
)
player = models.ForeignKey(
Player,
on_delete=models.CASCADE,
related_name='game_players'
)
position = models.CharField(max_length = 3)
jersey_num = models.IntegerField()

With the models defined in this way, we can look up players in a game ( instance_of_game.players.all()) and games a player has played in ( instance_of_player.games.all()) while holding the additional information about the player’s position and jersey number in the game. Cool stuff!

The last relationship is OneToOneField. This operates very similarly to ForeignKeyField, but only returns one object for the related model. This corresponds with the :belongs_to, :has_one type relationship in Rails. I don’t have an example of this yet in my project, but wanted to make sure to tack it on here.

After you’ve set up your models, you can run the following commands to migrate the database (here are my full models if you’d like to check them out):

python manage.py makemigrations
python manage.py migrate

Commands, Fetching and Seeding the Database

Last I want to touch on how I’m pulling the data from the NHL. In my Rails project I created a fetcher module in my services directory that I’d run in the rails console. My plans for the long term were to create a job that would run the fetcher at a set time of day to update the games for that day. I was having trouble figuring out how to port this over to Django until I ran across this article by Alexis Chilinski outlining how to seed your database with data from an external API.

The solution here is to create ‘Command’s, that can be run via the command line. The commands we’ll write here will fetch data from the API based on the parameters you put in and save it to the database. Later on, these commands can be linked to a cron job to be run at a certain time or interval.

For now I’m only going to go over the command that fetches teams, as this post is getting a little long. Maybe next week we can go a bit deeper into the one that fetches schedule and game data. If you’re following along, I’ll be referencing this file.

To start out, we’ll need to create a management directory in our api directory, and a commands directory inside management. Inside commands, we’ll create make_teams.py (the file referenced above):

In make_teams.py, we’ll need requests to fetch data from the API, BaseCommand to create our command and Team from our api.models so we can build teams here. the start of your file should look like this:

import requests
from django.core.management.base import BaseCommand
from api.models import Team

At the bottom of the file:

class Command(BaseCommand):
def handle(self, *args, **options):
fetch_teams()

This defines the command, which will call our fetch_teams method. fetch_teams will send a get request to the NHL’s API and attempt to build a team for every team in the response:

def fetch_teams():
url = 'https://statsapi.web.nhl.com/api/v1/teams'
response = requests.get(url, headers={'Content-Type': 'application/json'})
teams_dict = response.json() for team in teams_dict['teams']:
build_team(team)

build_team uses the Team.objects manager’s get_or_create() method to either find a team with the related ID, or create a new team.

def build_team(team): 
Team.objects.get_or_create(
api_id = team['id'],
defaults={
'name': team['teamName'],
'abbreviation': team['abbreviation'],
'city': team['locationName'],
'division': team['division']['name'],
'conference': team['conference']['name'],
'website': team['officialSiteUrl']
}
)

get_or_create operates very similarly to ActiveRecord’s find_or_create_by. All of the values not specified in defaults will be used to find a match of the current teams created. In our case, we’re using only api_id, as it’s unique per team on the NHL API. The keys and values provided in defaults will only be used to create a new team, and not for lookup / comparison. In this way, the first time we run this command, it’ll pull all of the teams. Then we can run it as many times as we want and it’ll never create another (unless a new team joins the league — looking at you Seattle Kraken with your null division and conference -.-).

There’s some more to commands — you can add keyword arguments, which can be pretty cool. I think I’ll touch on that a bit more next week unless I dive down another rabbit hole in the meantime. We’ll see.

But yeah, that’s a pretty good overview of what I’ve gone over this week in Django. Having fun with it so far and looking forward to diving in a bit more.

--

--

Seth Massarsky
Seth Massarsky

No responses yet