Django workshop : let's make a blog

Preview:

DESCRIPTION

This is a small introduction to the django framework I did for fellow engineers at Anevia. It touches on many of the major concepts and core features of django and gives a typical workflow built about the example of a minimalist blog engine.

Citation preview

django workshop

Pierre Sudron (psudron@anevia.com)

Anevia

May 21, 2014

django : a short introduction

django is :

a python web framework

batteries included : ORM and DB migrations, template language,cache, i18n, automatic admin pages, users and group management

DRY + KISS

If django doesn’t include a feature out of the box, it can be extendedwith third-party extensions.

Workshop outline

We will discover some of the framework’s basics and see how to make ablog engine with django :

MVT pattern and how django answers a request

designing the blog model entities

implementing some logic with views

displaying article and comments with templates

filling and validating forms

Part I:django fundamentals

The MVT template

The Movel-View-Template pattern plays a central role in theframework, every valid django application implements it.

Dive into models (1)

Model classes

Models are python classes that describe the database layout.

elements fetched from the database are instances of a model class

each attribute of the model represents a database column

An example with our blog’s Articles:

class Article(models.Model):

""" Blog article, has title, date, author and some content.

"""

title = models.CharField(’article title’, max_length=200)

author = models.ForeignKey(User)

pub_date = models.DateTimeField(’date published’)

content = models.TextField(’article content’)

Don’t forget : model classes must inherit from models.Model

Dive into models (2)

Models are ”synced” with the database, our Article class will generatethe following SQL :

CREATE TABLE "blog_article" (

"id" integer NOT NULL PRIMARY KEY,

"title" varchar(200) NOT NULL,

"author_id" integer NOT NULL REFERENCES "auth_user" ("id"),

"pub_date" datetime NOT NULL,

"content" text NOT NULL,

);

Take control with views

Views (classes or functions)

Views are the entry point of HTTP requests.

queries data from the DB, validates forms

returns rendered templates using collected data

def home(request):

""" Blog homepage.

"""

articles = Article.objects.all().order_by(’-pub_date’)

return render(request, ’blog_home.html’,

{

’articles’: articles,

})

Shape you centent with templates

Template files

Canvases that are used to generate pages with data provided by thecalling view

insert data, make DB queries (to some extent)

filters, tests, iterate over collections

template inheritance

{% extends "base.html" %}

{% block content %}

{% for article in articles %}

<div class="blog-post">

<h2>{{ article.title }} </h2>

<p>{{ article.pub_date }} by {{ article.author }} </p>

<p>{{ article.content }} </p>

</div>

{% endfor %}

{% endblock %}

Dispatching requests with patterns

url(r’^$’, ’blog.views.home’),

url(r’^article/(?P<article_id>\w+)/$’, ’blog.views.view_article’),

Request handling with django : the full cycle

Part II:let’s build a blog

First : let’s design the model

Weed need several entities to make a blog. Translating thisrepresentation is straightforward.

Articles

Comments

Users

ArticleTags

Models (1/3) : Article

unique id (auto)

title

author (User objectprovided)

publication date

content

tags

from django.contrib.auth.models import User

class Article(models.Model):

title = models.CharField(’article title’,

max_length=200)

author = models.ForeignKey(User)

pub_date = models.DateTimeField(

’date published’,

auto_now_add=True)

content = models.TextField(’content’)

tags = models.ManyToManyField(ArticleTag,

blank=True)

Models (2/3) : Comment

unique id (auto)

author name

parent Article

publication date

content

class Comment(models.Model):

author = models.CharField(’author name’,

max_length=100)

article = models.ForeignKey(Article)

pub_date = models.DateTimeField(

’date published’,

auto_now_add=True)

content = models.TextField(’comment’)

Models (3/3) : Article tag

unique id (auto)

tag name

class ArticleTag(models.Model):

tag_title = models.CharField(’tag title’,

max_length=30)

The article/tag relationship was added in the Article class, not need toadd it twice.

Model fields

integersnote = models.IntegerField(’just an int’)

number_nodes = models.PositiveIntegerField(’always be positive!’)

over9000 = models.BigIntegerField(’8 bytes integer’)

stings and texttitle = models.CharField(’rather small string’, max_length=100)

content = models.TextField(’you can write your biography here’)

url = models.URLField(’some website’)

mailing = models.EmailField(’valid email address’)

timebirthday = models.DateField(’just the date’)

timestamp = models.DateTimeField(’down to microseconds’)

filesfile = models.FileField(upload_to=’tmp_folder’)

lolcat = models.ImageField(upload_to=’img/cats/’)

Relationship fields

foreign key (many to one)

com_parent_article = models.ForeignKey(Article)

many to many

article_tags = models.ManyToManyField(ArticleTags)

one to one

secret_identity = models.OneToOneField(Superhero)

Smarter models

Validators can help keep logic inside models:from django.core.exceptions import ValidationError

def loves_pizza(user):

if not user.loves_pizza:

raise ValidationError(u’How can one not love pizza?’)

class PizzaFanMembership(object):

user = models.ForeignKey(User, validators=[loves_pizza])

limit values range (kinda enum-like)TOPPINGS = ( (0, ’none’),

(1, ’mushrooms’), (2, ’pepper’), ... )

pizza_topping = models.PositiveIntegerField(choices=TOPPINGS)

custom field types (eg. MarkdownTextField)

SQL-like constrains (not blank, defaults, etc.)

Back the blog : automatic admin pages !

Admin pages are generated from the model classes and give you completeCRUD functions with no sweat :

Part III:display our blog

Display data with views

Views (classes or functions)

Views are the entry point of HTTP requests.

queries data from the DB, validates forms

returns rendered templates using collected data

Roughly :

one page = one view

We will need 3 views for our blog :

”homepage” displaying all the articles (most recent first)

detailed view showing and article with all its comments

tag list page showing tag usage statistics

Homepage view

def home(request):

""" Blog homepage.

"""

articles = Article.objects.all().order_by(’-pub_date’)

return render(request, ’blog_home.html’,

{

’articles’: articles,

})

this view has no parameter other than the mandatory request

parameter

request constains a lot of useful stuff like sessions, POST, GET...

articles is a QuerySet and supports many SQL-like methods :count, get, filter, exclude, contains, comparisons...

render generates the html page from a template and with acontext containing the articles QuerySet

Article view

def view_article(request, article_id):

""" View a specific article.

"""

article = get_object_or_404(Article, id=article_id)

comments = Comment.objects.filter(article=article).order_by(

’pub_date’)

return render(request, "blog_article.html",

{

’article’: article,

’comments’: comments,

})

get object or 404 is a shortcut function

notice how the comparison inside filter acts like a WHERE clause

oder by ’row’ is DESC, whereas -’row’ is ASC

a QuerySet can be a collection (comments) as well as a single item(article)

Article view : which article by the way ?

Besides request, the view requires a article id parameter.def view_article(request, article_id):

This id comes from the requested url, parsed by the url pattern :url(r’^article/(?P<article_id>\w+)/$’, ’blog.views.view_article’),

Mind that the parameter inside the url pattern and in the functionprototype must be the same !Parameter’s order doesn’t matter though.

Beyond the render call

Template files

Canvases that are used to generate pages with data provided by thecalling view.

Templates can use data provided in the context dict, using item keys.

return render(request,

"blog_article.html",

{

’article’: article,

’comments’: comments,

})

In the blog article.html template,we will be able to access :

article : the Article model

comments : the list of relatedComment objects

Templating (1)

insert values<h2>{{ article.title }} </h2>

<p>{{ article.pub_date }} by {{ article.author }} </p>

use filters{{ article.pub_date | date:"D d M Y" }}

{{ article.content | truncatewords:150 }}

{{ boolean | yesno:"yeah,no,maybe" }}

perform tests{% if user.height < 1.5 %}

<p>You must be 1.5m to enter</p>

{% endif %}

loop over iterables{% for comment in comments %}

<p>{{ comment.author }} : {{ comment.content }}

{% endfor %}

Templating (2)

template inheritance<!-- base.html --> Some stuff around...

{% block footer %}

<!-- but no footer ! -->

{% endblock %}

<!-- other_page.html -->

{% extends base.html %}

{% block footer %}

Redefined footer ! This is fancier isn’t it ?

{% endblock %}

reverse urls (this is a killer feature !)<a href="{% url ’blog.views.article’ article.id %} ">

Read the article {{ article.title }}

</a>

Example : complete article template

{% extends "base.html" %}

{% block content %}

<h2>{{ article.title }} </h2>

<p>{{ article.pub_date }} by {{ article.author }} </p>

<p>

{% for tag in article.tags.all %} {{ tag }} , {% endfor %}

</p>

<p>{{ article.content }} </p>

<hr>

<h2>Comments</h2>

{% for comment in comments %}

<div class="comment">

<h5>{{ comment.author }} </h5>

<p>{{ comment.pub_date }} </p>

<p>{{ comment.content }} </p>

</div>

{% endfor %}

{% endblock %}

Part III:add, modify and delete content

Delete an article

Some views can be used to perform a specific task and then redirect.def delete_article(request, article_id):

""" Delete an article given its ID.

"""

article = get_object_or_404(Article, id=article_id)

article.delete()

return redirect(’blog.views.home’)

Add an url pattern and it’s ready to use.url(r’^delete/(?P<article_id>\w+)/$’, ’blog.views.delete_article’),

<a href="{% url ’blog.views.delete_article’ article.id %} ">

Click to delete article {{ article }}

</a>

Form classes

To create new model objects or edit existing ones, we need to use Forms.

custom formsclass ContactForm(forms.Form):

sender = forms.EmailField()

message = forms.CharField()

models forms generated from your modelsfrom .models import Article

class ArticleForm(forms.ModelForm):

""" Article creation or edition form.

"""

class Meta:

model = Article

fields = (’title’, ’content’)

Model forms use validation mechanisms defined inside the model.

Use a form to create an item

Forms are managed inside views.def create_article(request):

""" Write a new blog article.

"""

edition_form = ArticleForm(request.POST or None)

if edition_form.is_valid():

# creating an article and setting its author

article = edition_form.save(commit=False)

article.author = request.user

article.save()

# redirect to the newly created article

return redirect(’blog.views.view_article’,

article_id=article.id)

# render the edition form if the data is blank or invalid

return render(request, "edit_article.html",

{

’form’: edition_form,

})

Display forms inside templates

the ”quick and dirty” way<form action="" method="post">

{% csrf_token %}

{{ form.as_p }}

<button type="submit" class="btn btn-primary">Save</button>

<a class="btn btn-default" href="{% url ’blog.views.home’ %} ">

Cancel

</a>

</form>

custom field-by-field way{% csrf_token %}

{% for field in form %}

<label for={{ field.auto_id }} >{{ field.label }} </label>

{{ field }}

{% endfor %}

Edit an existing item

The same ModelForm can be used for item edition :def edit_article(request, article_id=None):

""" Edit a blog article.

"""

article = get_object_or_404(Article, id=article_id)

edition_form = ArticleForm(request.POST or None, instance=article)

if edition_form.is_valid():

# saving the edited article

edition_form.save()

return redirect(’blog.views.view_article’, article_id=article.id)

# render the edition form if the data is blank or invalid

return render(request, "edit_article.html",

{

’form’: edition_form,

})

Part IV:what’s next?

But there’s more !

migration: migrates the database when model classes are modified

authentication: built-in user objects, groups, session management

protect your views@permission_required(’blog.edit_comment’)

def moderation_view(request):

...

painless caching

cache views@cache_page(60 * 15)

def view_with_many_queries(request):

...

cache template blocks{% cache 500 sidebar %}

.. sidebar ..

{% endcache %}

unit testing, XSS protection, flatpages...

NIH & DRY

If you’re in need for feature, first check if it not already available :

the framework has a lot of features and ready to use tools

if the framework doesn’t provide you with what you need, look atthe many great extensions available (image caching and resizing,REST framework, LDAP, benchmarking. etc)

Django is all about keeping it simple, being productive while still makingreliable software.

Documentation & resources

official documentation : https://docs.djangoproject.com

Two scoops of django by Daniel Greenfeld and Audrey Roy

Thank you!Any questions?

Recommended