Django Workshop

The web framework for perfectionists with deadlines

RaumZeitLabor Mannheim, 13.12.2014

Raphael Michel
<mail@raphaelmichel.de>

Agenda

Wir bauen eine Blogsoftware.

(Was besseres ist mir auch nicht eingefallen.)

Warum Django?

  • Monolithisch, „batteries included“; aber austauschbar
  • Große, freundliche Community
  • Stabil und erprobt
  • Lernkurve ist akzeptabel
  • Gesunder Pragmatismus bei Design-Entscheidungen
  • DRY, wenig Code-Aufwand für viel Ergebnis

Vorkenntnisse: Python


def square(a):
    return a*a
print(square(3))  # 9
						

class Foo(Bar):
    def __init__(self, a):
        self.var = a
						

Vorkenntnisse: HTML


<!DOCTYPE html>
<html>
<head>
	<title>Hallo</title>
</head>
<body>
	<h1>Hallo</h1>
	<p>Dies ist eine Website!</p>
</body>
</html>
						

Vorkenntnisse: Regular Expressions


^([a-zA-Z0-9.-_]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})$
						

Unser Basissetup


							raphael $ python --version
Python 3.4.2
raphael $ pip --version
pip 1.5.6 from /usr/lib/python3.4/site-packages (python 3.4)
						
Wir wollen Python 3!

Virtual Environments

Python <3.4


							raphael $ sudo pip install virtualenv
raphael $ virtualenv env
						

Python 3.4+


							raphael $ pyvenv env
						

Virtual Environments


							raphael $ source env/bin/activate
(env) raphael $
						

Django itself


							(env) raphael $ pip install Django==1.7.1
						

Project setup


(env) raphael $ django-admin.py startproject workshopproject
(env) raphael $ cd workshopproject/
(env) raphael workshopproject/ $
						

Project setup


(env) raphael workshopproject/ master $ tree .
.
├── manage.py
└── workshopproject
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

1 directory, 5 files
						

settings.py


INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)
						

settings.py


DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
						

settings.py


LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
USE_L10N = True
USE_TZ = True
						

Datenbank erstellen


(env) raphael workshopproject/ $ python manage.py migrate
Operations to perform:
Apply all migrations: auth, admin, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying sessions.0001_initial... OK
						

Run it!


(env) raphael workshopproject/ $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
November 08, 2014 - 09:38:18
Django version 1.7.1, using settings 'workshopproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
						

http://localhost:8000

Es kann losgehen!

Wir brauchen eine App


(env) raphael workshopproject/ $ python manage.py startapp blog
						

(env) raphael workshopproject/ $ tree blog
blog
├── admin.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

1 directory, 6 files
						

settings.py


INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
)
						

Unser Datenmodell

blog/models.py


from django.db import models

class Blogpost(models.Model):
    title = models.CharField(max_length=255)
    text = models.TextField()
    pub_date = models.DateTimeField()

class Comment(models.Model):
    post = models.ForeignKey(Blogpost, related_name="comments")
    name = models.CharField(max_length=255)
    text = models.TextField()
    pub_date = models.DateTimeField()

Das Datenbank-Layout


(env) raphael workshopproject/ $ python manage.py makemigrations blog
Migrations for 'blog':
  0001_initial.py:
    - Create model Blogpost
    - Create model Comment
						

Das Datenbank-Layout


(env) raphael workshopproject/ $ python manage.py sqlmigrate blog 0001
BEGIN;
CREATE TABLE "blog_blogpost" (
	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
	"title" varchar(255) NOT NULL, "text" text NOT NULL, 
	"pub_date" datetime NOT NULL);
CREATE TABLE "blog_comment" (
	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
	"name" varchar(255) NOT NULL, "text" text NOT NULL, 
	"pub_date" datetime NOT NULL, 
	"post_id" integer NOT NULL REFERENCES "blog_blogpost" ("id"));
CREATE INDEX blog_comment_f3aa1999 ON "blog_comment" ("post_id");
COMMIT;
						

Das Datenbank-Layout


(env) raphael workshopproject/ $ python manage.py migrate
Operations to perform:
  Apply all migrations: auth, admin, sessions, blog, contenttypes
Running migrations:
  Applying blog.0001_initial... OK
						

Die Model-API


(env) raphael workshopproject/ $ python manage.py shell
Python 3.4.2 (default, Oct  8 2014, 13:44:52) 
[GCC 4.9.1 20140903 (prerelease)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
						

>>> from blog.models import Blogpost, Comment

>>> Blogpost.objects.all()
[]
						

>>> post = Blogpost(title="First blogpost", text="Hello World")

>>> from django.utils import timezone
>>> post.pub_date = timezone.now()

>>> post.save()

>>> post.id
1
						

>>> post.text = "Hallo Welt!"
>>> post.save()
						

>>> Blogpost.objects.all()
[<Blogpost: Blogpost object>]
						

Well…

blog/models.py


class Blogpost(models.Model):
    …
    def __str__(self):
        return self.title
						

$ ./manage.py shell 
>>> from blog.models import Blogpost, Comment
>>> Blogpost.objects.all()
[<Blogpost: First blogpost>]
						

Mehr Queries


>>> Blogpost.objects.filter(id=1)
[<Blogpost: First blogpost>]
						

>>> Blogpost.objects.filter(title__startswith="First")
[<Blogpost: First blogpost>]
						

>>> Blogpost.objects.filter(pub_date__year=2014)
[<Blogpost: First blogpost>]
						

>>> post = Blogpost.objects.get(id=1)
<Blogpost: First blogpost>
						

from django.utils import timezone
import datetime 

class Blogpost(models.Model):
    …

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

>>> post.was_published_recently()
True
						

>>> post = Blogpost.objects.get(id=1)
>>> post
<Blogpost: First blogpost>
						

>>> post.comments.count()
0
						

>>> from django.utils import timezone
>>> post.comments.create(name="Alice", text="Hallo!",
                         pub_date=timezone.now())
<Comment: Comment object>
						

>>> post.comments.all()
[<Comment: Comment object>]
						

>>> Comment.objects.filter(post__pub_date__year=2014)
[<Comment: Comment object>]
0
						

>>> c = Comment.objects.get(id=1)
>>> c.delete()
>>> post.comments.count()
						
  • filter, exclude
  • order_by, distinct, [0:30]
  • values, …
  • none, all
  • select_related, prefetch_related
  • get, first, last, …
  • aggregate
  • exists, count
  • create, update, delete

Djangos Adminbereich


(env) raphael workshopproject/ $ python manage.py createsuperuser
Username (leave blank to use 'raphael'): admin
Email address: admin@example.com
Password: 
Password (again): 
Superuser created successfully.
						

Run it!


(env) raphael workshopproject/ $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
November 08, 2014 - 09:38:18
Django version 1.7.1, using settings 'workshopproject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
						

http://localhost:8000/admin/

Wohooo!

Aber… wo sind die Blogposts?

blog/admin.py


from django.contrib import admin
from blog.models import Blogpost

admin.site.register(Blogpost)
						

Wohooo!

Aber… wo sind die Kommentare?

Der Adminbereich kann ganz viel tolles Zeug.

Aber das ist heute nicht das Thema.

Lest das offizielle Tutorial.

Views

Das erste View

blog/views.py


from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. This is my blog.")
						

URL Routing

blog/urls.py


from django.conf.urls import patterns, url

from blog import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index'),
)
						

workshopproject/urls.py


from django.conf.urls import patterns, url
from django.contrib import admin

urlpatterns = patterns('',
    url(r'^blog/', include('blog.urls')),
    url(r'^admin/', include(admin.site.urls)),
)
						
http://localhost:8000/blog/

Templates

blog/templates/blog/base.html


<!DOCTYPE html>
<html>
	<head>
		<title>Unser Blog</title>
	</head>
	<body>
		{% block content %}
		{% endblock %}
	</body>
</html>
						

blog/templates/blog/index.html


{% extends "blog/base.html" %}
{% block content %}
	Hallo!
{% endblock %}
						

Das View

blog/views.py


from django.shortcuts import render


def index(request):
    return render(request, "blog/index.html")
						

Echte Daten

blog/views.py


from django.shortcuts import render
from blog.models import Blogpost


def index(request):
    context = {
        'posts': Blogpost.objects.all().order_by('-pub_date')
    }
    return render(request, "blog/index.html", context)
						

blog/templates/blog/index.html


{% extends "blog/base.html" %}
{% block content %}
    {% for post in posts %}
        <article>    
            <h2>{{ post.title }}</h2>
            <p>{{ post.pub_date|date:"d.m.Y" }}</p>
            {{ post.text }}
        </article>
    {% endfor %}
{% endblock %}
						

blog/urls.py


urlpatterns = patterns('',
    url(r'^post/(?P<postid>[0-9]+)$', views.detail, name='detail'),
    url(r'^$', views.index, name='index'),
)
						

blog/templates/blog/detail.html


{% extends "blog/base.html" %}
{% block content %}
    <article>	
        <h2>{{ post.title }}</h2>
        <p>{{ post.pub_date|date:"d.m.Y" }}</p>
        {{ post.text }}
    </article>
    {% for comment in comments %}
        <article>
            <p>Kommentar von {{ comment.name }}</p>
            <p>{{ comment.text }}</p>
        </article>
    {% endfor %}
{% endblock %}

						

Das View

blog/views.py


from django.shortcuts import render, get_object_or_404
from blog.models import Blogpost, Comment

def detail(request, postid):
    post = get_object_or_404(Blogpost, id=postid)
    context = {
        'post': post,
		'comments': post.comments.all()
    }
    return render(request, "blog/detail.html", context)
						

Links!

blog/templates/blog/index.html


{% extends "blog/base.html" %}
{% block content %}
    {% for post in posts %}
        <article>    
            <h2><a href="{% url "detail" postid=post.id %}">
                {{ post.title }}
            </a></h2>
            <p>{{ post.pub_date|date:"d.m.Y" }}</p>
            {{ post.text }}
        </article>
    {% endfor %}
{% endblock %}
						

Formulare

blog/views.py


from django.utils.timezone import now
from django import forms


class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'text',)
						

blog/views.py


def detail(request, postid):
    post = get_object_or_404(Blogpost, id=postid)
    form = CommentForm()
    context = {
        'post': post,
        'comments': post.comments.all(),
        'form': form
    }
    return render(request, "blog/detail.html", context)
						

blog/templates/blog/detail.html


<form method="post" action="">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Kommentar absenden</button>
</form>
						

def detail(request, postid):
    post = get_object_or_404(Blogpost, id=postid)
    if request.method == "POST":
    	new_comment = Comment(post=post, pub_date=now())
        form = CommentForm(data=request.POST, instance=new_comment)
        if form.is_valid():
            form.save()
            # Empty form input
            form = CommentForm()
    else:
        form = CommentForm()
    context = {
        'post': post,
        'comments': post.comments.all(),
        'form': form
    }
    return render(request, "blog/detail.html", context)
						

Das wars schon?!

Generic views

blog/views.py


from django.views import generic


class IndexView(generic.ListView):
    template_name = 'blog/index.html'
    context_object_name = 'posts'

    def get_queryset(self):
        return Blogpost.objects.order_by('-pub_date')
						

blog/urls.py


url(r'^$', views.IndexView.as_view(), name='index'),
						

blog/views.py


from django.core.urlresolvers import reverse


class DetailView(generic.CreateView):
    template_name = 'blog/detail.html'
    model = Comment
    fields = ('name', 'text')

    @property
    def blogpost(self):
        return get_object_or_404(
            Blogpost, id=self.kwargs['postid'])

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['post'] = self.blogpost
        context['comments'] = context['post'].comments.all()
        return context

    def form_valid(self, form):
        form.instance.post = self.blogpost
        form.instance.pub_date = now()
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('detail', kwargs={
            'postid': self.blogpost.id
        })
						

blog/urls.py


url(r'^post/(?P<postid>[0-9]+)$', 
    views.DetailView.as_view(), name='detail'),
						

Static files

settings.py


STATIC_URL = '/static/'
						

blog/static/blog/style.css


body {
    font-family: sans-serif;
}
						

blog/templates/blog/base.html


{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>Unser Blog</title>
        <link rel="stylesheet" type="text/css" 
            href="{% static "blog/style.css" %}">
    </head>
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>
						

Deployment basics

WSGI

Weh Es … wat?

Gunicorn


pip install gunicorn
gunicorn myproject.wsgi
						
→ Dokumentation

Apache mod_wsgi


WSGIScriptAlias / /path/to/example.com/mysite/wsgi.py
WSGIPythonPath /path/to/example.com
Alias /media/ /path/to/example.com/media/
Alias /static/ /path/to/example.com/static/

<Directory /path/to/example.com/mysite>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
						
→ Dokumentation

DEBUG = False
ALLOWED_HOSTS = ['example.com']
							

ADMINS = (
    ('Raphael Michel', 'raphael@abiapp.net'),
)

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'include_html': True,
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}
							

SQLite nicht in Produktion benutzen!

→ Deployment checklist

Uncovered

Fragen?

Raphael Michel
<mail@raphaelmichel.de>