transition | logo |
---|---|
fade |
Marseille (France) - 2017/07/05
- :fa-twitter:
@julienmaupetit
- PhD in structural bioinformatics (Paris Diderot)
- Research engineer (RPBS platform, Paris Diderot)
- Co-founder of TailorDev (Clermont-Ferrand)
What about you?
Do you
git
,bash
,python
?
- A brief theoretical introduction to Django
- Bootstrap your project
- Create yout first Django application
For the practical part, you will need:
- a UNIX shell (
bash
,zsh
, …) git
python >= 3.4
ping www.google.fr
Ready?
Develop
climate
, a Django application that displays average temperature records by country since the 18th century.
- Load data from the Climate Change: Earth Surface Temperature Data Kaggle dataset (csv),
- Store data in a database,
- View tabular data per country,
- Plot data per country (optional).
Model - Template - View
ORM = Object-Relational Mapping
# A model is a python class
class Record(models.Model):
"""Temperature record"""
date = models.DateField()
temperature = models.DecimalField(
"Average temperature",
help_text="In Celcius",
decimal_places=3,
max_digits=6,
null=True,
blank=True,
)
uncertainty = models.DecimalField(
"Average temperature uncertainty",
help_text="The 95% confidence interval around the average",
decimal_places=3,
max_digits=6,
null=True,
blank=True,
)
country = models.ForeignKey(
'Country',
on_delete=models.CASCADE
)
class Meta:
verbose_name = "Record"
verbose_name_plural = "Records"
ordering = ('country', 'date')
unique_together = ('date', 'country')
def __str__(self):
return '{} - {}'.format(self.country, self.date
Read Django docs for the list of Field
types.
import datetime
from .models import Record
# Get all records
records = Record.objects.all()
# Get records from 2017
current_year_records = Record.objects.filter(date__year=2017)
Django as generic views for CRUD operations and more 🎉
from django.views.generic.list import DetailView
from .models import Record
class RecordDetailView(DetailView):
model = Record
HTML + template language
<!-- my_app/record_detail.html -->
<html>
<head>
<title>Record detail</title>
</head>
<body>
<h1>Record {{ record.id }}</h1>
Sampling date: {{ record.date | date }}
...
</body>
</html>
Routing is mapping URLs and views
from django.conf.urls import url
from .views import RecordDetailView
urlpatterns = [
url(r'^record/(?P<pk>\d+)/$', RecordDetailView.as_view(), name='record_detail'),
]
Create a virtualenv
for your first django project:
$ cd my/projects/directory
$ mkdir django-101
$ cd django-101
$ python3 -m venv venv
Activate the newly created virtualenv
:
$ source venv/bin/activate
# Now your prompt should be modified:
(venv) $
Install the latest Django release with pip
:
(venv) $ pip install Django
And create your Django project:
(venv) $ django-admin startproject climate .
# Have you seen the `.` at the end of this latest command?
A typical Django project tree looks like the following:
.
├── climate
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── venv
└── ...
Django management command is central to the Django ecosystem.
# See available commands
(venv) $ python manage.py
# Run the development server
(venv) $ python manage.py runserver
# Quit the server with CTRL+C
You should see a warning message about migrations, no worries, we will fix that later.
You can browse to: http://127.0.0.1:8000/ to check that everything works 🎉
Now it's time to start versioning our project with git
:
(venv) $ git init .
# Download a .gitignore template for Python projects from github
(venv) $ curl -qs 'https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore' > .gitignore
# Add project requirements
(venv) $ pip freeze | grep Django > requirements.txt
# Make your first commit
(venv) $ git add .gitignore climate/ manage.py requirements.txt
(venv) $ git commit -m 'Start climate project'
Remark: your venv
directory should be ignored (see .gitignore
file rules)
(venv) $ python manage.py startapp temperature
(venv) $ git add temperature
(venv) $ git commit -m 'Add temperature base application'
The temperature
application should look like:
temperature
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
Look at the GlobalLandTemperaturesByCountry.csv
dataset [1], and propose a relational model to store the data.
[1] https://www.kaggle.com/berkeleyearth/climate-change-earth-surface-temperature-data
Let's create our first Django models for the temperature application:
# temperature/models.py
from django.db import models
class Country(models.Model):
"""Country where the data have been recorded"""
name = models.CharField(
max_length=100,
unique=True
)
class Record(models.Model):
"""Temperature record"""
date = models.DateField()
temperature = models.DecimalField(
"Average temperature",
help_text="In Celcius",
decimal_places=3,
max_digits=6,
null=True,
blank=True,
)
uncertainty = models.DecimalField(
"Average temperature uncertainty",
help_text="The 95% confidence interval around the average",
decimal_places=3,
max_digits=6,
null=True,
blank=True,
)
country = models.ForeignKey(
'Country',
on_delete=models.CASCADE
)
class Meta:
unique_together = ('date', 'country')
You should explicitely add your application to the project to activate it:
# climate/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'temperature', # your Django application
]
Django migrations are suites of python scripts that incrementally makes your database schema evolve.
# create temperature app migrations
(venv) $ python manage.py makemigrations temperature
# Perform all migrations
(venv) $ python manage.py migrate
You need to create a migration everytime you change your models or add new models to your application.
# Ignore sqlite database versioning
(venv) $ echo 'db.sqlite3' >> .gitignore
# Commit your work
(venv) $ git add \
.gitignore \
climate/settings.py \
temperature/models.py \
temperature/migrations/0001_initial.py
(venv) $ git commit -m 'Add temperature models'
Make your data visible in Django admin interface:
# temperature/admin.py
from django.contrib import admin
from .models import Country, Record
admin.site.register(Country)
admin.site.register(Record)
# Commit
(venv) $ git add temperature/admin.py
(venv) $ git commit -m 'Register temperature models for admin'
To access Django admin, we need to create a superuser (aka administrator) allowed to log in:
# create a super user
(venv) $ python manage.py createsuperuser \
--username watson \
--email [email protected]
You will be asked to type
watson
user password twice
Now go to: http://127.0.0.1:8000/admin/ 🎉
Download the file GlobalLandTemperaturesByCountry.csv
from the Climate Change: Earth Surface Temperature Data project.
# This is where we will store the data
(venv) $ mkdir temperature/fixtures
# Register to Kaggle, download it and save it as temperature/fixtures/temperature_by_country.csv
# or download it from this project repository
(venv) $ curl -qs 'https://github.com/tailordev-commons/climate/blob/master/temperature/fixtures/temperature_by_country.csv?raw=true' > temperature/fixtures/temperature_by_country.csv
# Commit the data
(venv) $ git add temperature/fixtures/temperature_by_country.csv
(venv) $ git commit -m 'Add temperatures by country fixture'
# Prepare management command tree
(venv) $ mkdir -p temperature/management/commands
(venv) $ touch \
temperature/management/__init__.py \
temperature/management/commands/__init__.py \
temperature/management/commands/load_records.py
Your management
folder should look like:
temperature/management
├── __init__.py
└── commands
├── __init__.py
└── load_records.py
# temperature/management/commands/load_records.py
import csv
import os.path
from datetime import datetime
from decimal import Decimal as D
from django.core.management.base import BaseCommand, CommandError
from django.db.utils import IntegrityError
from temperature.models import Country, Record
def _import_record_from_csv_row(dt, temperature, uncertainty, country_name):
"""Import a record from a csv file row data"""
country, _ = Country.objects.get_or_create(name=country_name)
try:
Record.objects.create(
date=dt,
temperature=temperature if len(temperature) else None,
uncertainty=uncertainty if len(uncertainty) else None,
country=country
)
except IntegrityError:
# Unique constraint (date, country) raise an IntegrityError when trying
# to save the same record twice. We ignore this error here as we want to
# be able to run the load_records command more than once.
pass
def load_data(csv_file_name):
"""Load data from input csv_file file"""
# TODO
# Optimize importation by using Record.objects.bulk_create on a country
# basis
with open(csv_file_name) as csv_file:
dataset_reader = csv.DictReader(csv_file)
for row in dataset_reader:
_import_record_from_csv_row(*row.values())
class Command(BaseCommand):
help = "Load temperature records from a csv file"
def add_arguments(self, parser):
parser.add_argument('CSV_FILE')
def handle(self, *args, **options):
csv_file_name = options['CSV_FILE']
self.stdout.write(
"Will import dataset from file: {}".format(csv_file_name)
)
# check that the file exists
if not os.path.exists(csv_file_name):
raise CommandError(
"CSV file {} does not exists".format(csv_file_name)
)
load_data(csv_file_name)
self.stdout.write(
self.style.SUCCESS(
"Temperature dataset has been successfully imported"
)
)
(venv) $ git add temperature/management
(venv) $ git commit -m 'Add load records management command'
Add __str__
methods to your models:
# temperature/models.py
class Country(models.Model):
# [...]
def __str__(self):
return self.name
class Record(models.Model):
# [...]
def __str__(self):
return '{} - {}'.format(self.date, self.country)
Remember to
git commit
this!
Create a dedicated ModelAdmin
for records:
# temperature/admin.py
from django.contrib import admin
from .models import Country, Record
class RecordAdmin(admin.ModelAdmin):
date_hierarchy = 'date'
list_display = ('country', 'date', 'temperature', 'uncertainty')
list_filter = ('country', )
admin.site.register(Country)
admin.site.register(Record, RecordAdmin)
What should you do? Hint: it starts with
git
.
In a first approach, we will define a generic view to list records:
# temperature/views.py
from django.views.generic.list import ListView
from .models import Record
class RecordListView(ListView):
model = Record
paginate_by = 50
Use http://ccbv.co.uk to help you develop your generic class-based view.
You must define an url that point to your view:
# temperature/urls.py
from django.conf.urls import url
from .views import RecordListView
urlpatterns = [
url(r'^$', RecordListView.as_view(), name='record_list'),
]
Add the temperature
application urls to your project:
# climate/urls.py
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('temperature.urls'))
]
Now browse to: http://127.0.0.1:8000
What is missing?
Create a Django template for your view:
(venv) $ mkdir temperature/templates/temperature
(venv) $ touch temperature/templates/temperature/record_list.html
This template could be designed as follows:
<!-- temperature/templates/temperature/record_list.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Temperature records</title>
</head>
<body>
<h1>Temperature records</h1>
<table>
<thead>
<tr>
<th>Date</th>
<th>Average Temperature (° C)</th>
<th>Uncertainty</th>
<th>Country</th>
</tr>
</thead>
<tbody>
{% for record in record_list %}
<tr>
<td>{{ record.date | date }}</td>
<td>{{ record.temperature | default:"N/A" }}</td>
<td>{{ record.uncertainty | default:"N/A" }}</td>
<td>{{ record.country.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">< previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next ></a>
{% endif %}
</span>
</div>
</body>
</html>
Now browse to: http://127.0.0.1:8000 🎉
And commit your work:
(venv) $ git add \
temperature/views.py \
temperature/urls.py \
temperature/templates/ \
climate/urls.py
(venv) $ git commit -m 'Add paginated record list view'
(venv) $ mkdir -p temperature/static/temperature/css
(venv) $ touch temperature/static/temperature/css/temperature.css
*optional
// temperature/static/temperature/css/temperature.css
body {
max-width: 600px;
margin-left: auto;
margin-right: auto;
color: #333;
}
h1 {
text-align: center;
}
table {
width: 100%;
}
table thead tr th {
border-bottom: 1px solid #333;
}
table tr:nth-child(even) {
background-color: #e6e6e6;
}
table tr > * {
width: 25%;
text-align: right;
padding: 0.4rem;
}
table tr > *:first-child {
text-align: left;
}
.pagination {
margin-top: 1rem;
padding-top: 1rem;
text-align: center;
border-top: 1px solid #333;
}
Invite your stylesheet to join the party:
{% load static %} <!-- load static templatetags -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Temperature records</title>
<!-- Add your stylesheet here -->
<link rel="stylesheet" href="{% static "temperature/css/temperature.css" %}">
</head>
<body>
<!-- [...] -->
And don't forget to? Commit your work, yes!
(venv) $ pip install pytest pytest-django
(venv) $ echo -e 'pytest\npytest-django' > requirements-dev.txt
(venv) $ touch pytest.ini
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE=climate.settings
addopts = -vs
testpaths = temperature/tests
Prepare your test tree:
(venv) $ git rm temperature/tests.py
(venv) $ mkdir temperature/tests
(venv) $ touch temperature/tests/{__init__.py,test_views.py}
Commit your work before writting your first test:
(venv) $ git add pytest.ini requirements-dev.txt temperature/tests
(venv) $ git commit -m 'Add & configure pytest'
Write a first test for your record_list
view:
# temperature/tests/test_views.py
import pytest
from django.core.urlresolvers import reverse
from django.test import TestCase
@pytest.mark.django_db
class RecordListViewTests(TestCase):
"""Tests for the RecordListView"""
def setUp(self):
self.url = reverse('record_list')
def test_get(self):
"""Test the RecordListView get method"""
response = self.client.get(self.url)
# Test response code and used template
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed('temperature/record_list.html')
Your smoke test should pass:
(venv) $ pytest
We need to write many more tests now (management command, models, etc.)