Primeros pasos con el framework DJANGO

Vamos a construir una aplicación muy sencilla para conocer el framework Django.

Instalación de DJANGO

La forma más sencilla de instalarlo es mediante pip

pip install django

Comprobamos que funciona correctamente…

miguelm$ python
Python 2.7.10 (default, Aug 17 2018, 17:41:52) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> django.VERSION
(1, 11, 18, u'final', 0)

Creación de primer proyecto y aplicación

Entramos en nuestro directorio de trabajo y creamos un nuevo proyecto, al que vamos a llamar Plantillas. Un proyecto es una instancia de un cierto conjunto de aplicaciones de Django y las configuraciones de éstas. Lo entenderemos mejor después.

django-admin startproject plantillas
cd plantillas

A continuación vamos a crear nuestra primera Aplicación dentro del proyecto, a la que llamaremos primera (en un alarde de originalidad). Una aplicación es un conjunto portable de alguna funcionalidad de Django, típicamente incluye modelos y vistas, que conviven en un solo paquete de Python.

django-admin startapp primera

Lo siguiente es indicar a nuestro proyecto “plantillas” que use nuestra aplicación primera. Para eso editamos la variable INSTALLED_APPS en plantillas/settings.py e incluimos primera:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
]

También nos aseguramos que la variable APP_DIRS dentro de TEMPLATES esté a True en plantillas/settings.py. APP_DIRS indica si el motor debe buscar plantillas dentro de las aplicaciones instaladas. Cada backend define un nombre convencional para el subdirectorio dentro de las aplicaciones donde se deben almacenar sus plantillas (templates), lo veremos más adelante. De momento, comprobamos que APP_DIRS está a True:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Lo siguiente será crear la carpeta de la aplicación donde dejaremos las plantillas html de nuestra aplicación primera:

mkdir primera/templates

El fichero que enruta las peticiones (tal url la resuelve tal función) es urls.py. Lo normal es que haya un fichero urls.py general del proyecto (plantillas/urls.py) y después un fichero urls.py dentro de cada aplicación (será primera/urls.py). Le decimos al fichero de rutas del proyecto (plantillas/urls.py) que todas las rutas que sean del tipo /primera las atienda con el fichero de rutas de nuestra aplicación primera, es decir, derivamos el control hacia primera/urls.py.

El fichero de routing del proyecto, plantillas/urls.py quedará así:

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

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^primera/', include('primera.urls')),
]

A continuación voy a crear la primera ruta de mi aplicación primera. Para ello edito primera/urls.py y le digo que para cualquier ruta que reciba este controlador, se ejecute la función current_datetime, que definiremos más adelante en primera/views.py.

from django.conf.urls import include, url
from . import views
	
urlpatterns = [
    url(r'^$', views.current_datetime),
] 

Y definiremos la función current_datetime, lo que haremos editando primera/views.py, que quedará así.

	
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render # nos permite renderizar plantillas de forma cómoda
from datetime import datetime # Este import lo hacemos para poder usarlo en la func current_datetime
	
def current_datetime(request):
    return render(request, 'hora.html', {'current_date' : str(datetime.now()) })

NOTA: Hay una forma, un poco más “sucia”, pero mucho más rápida, de pasar las variables locales definidas en una función (por ejemplo dentro de current_date) del controlador a la vista, que pasaría por usar la función locals() en la llamada a render. Si lo hacemos así, TODAS las variables definidas en current_datetime pasarán a la vista (es cómodo, pero a veces puedes estar pasando variables que no necesites, y además construyes el diccionario de forma dinámica, lo que no es súper eficiente). Pero bueno, es cómodo, y sería así:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render # nos permite renderizar plantillas de forma cómoda
from datetime import datetime # Este import lo hacemos para poder usarlo en la func current_datetime
	
def current_datetime(request):
    current_date = str(datetime.now())
    return render(request, 'hora.html', locals())

Ya solo nos falta definir el template hora.html, que quedaría en primera/templates/hora.html y podría ser algo tan sencillo como ésto:

<html><head><title>Mi pagina</title></head><body>La fecha es {{ current_date }}</body></html>

Sería poco eficiente y repetiríamos código si cada página construye su plantilla desde cero. Es mejor tener una plantilla “padre”, que define la estructura común que tendrán todas las páginas, y plantillas “hijas”, que hereden del padre y sobreescriban determinadas zonas. Para eso vamos a crear una plantilla padre a la que llamaremos primera/templates/layout_3col.html y que estará basada en Bootstrap, que tendrá éste contenido:

<html>
<head>
    <title>{% block titlepag %} Killer App {% endblock %}</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>

<body>

    <div class="jumbotron text-center">
        {% block titulo %}
            <h1> titulo principal </h1>
        {%endblock %}
    </div>

    <div class="container">
        <div class="row">
            <div class="col-sm-4">
                {% block columna1 %}
                    <h3>Column 1</h3>
                    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
                    <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
                {%endblock %}
            </div>
            <div class="col-sm-4">
             {% block columna2 %}
            
              <h3>Column 2</h3>
              <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
              <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
            
            {%endblock %}
            </div>
            <div class="col-sm-4">
            {% block content %}
            
              <h3>Column 3</h3>
              <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
              <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...</p>
            
            {%endblock %}
   
            </div>
            
        </div>
    </div>
</body>
</html>
</html>

Y ahora vamos a escribir una plantilla “hija”, que heredará desde layout_3col.html y sobreescribirá únicamente el contenido del bloque definido como content. El fichero quedará en primera/templates/hora.html y tendrá éste contenido:

{% extends "layout_3col.html" %}

{% block content %}
La hora es {{ current_date }}
{% endblock %}

Ya solo nos falta decirle a nuestra función controlador (que es la función current_datetime definida en primera/views.py) que emplee esta nueva plantilla. Para ello dejaremos el contenido de así:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.shortcuts import render

from datetime import datetime

def current_datetime(request):
    current_date = str(datetime.now())
    return render(request, 'hora.html', locals())

Arrancando el servidor de pruebas

Django provee de una utilidad que permite levantar un servidor de pruebas de forma muy sencilla. Para ello únicamente hemos de hacer:

python manage.py runserver

Y nos levantará un servidor, así que consultaremos http://127.0.0.1:8000/primera/ y veremos algo así:
primeros pasos con django

Manejando archivos estáticos

Si necesitas servir ficheros estáticos (por ejemplo, imágenes, js, css, etc), necesitarás comprobar que en tu fichero de settings del proyecto (plantillas/settings.py), en la parte de INSTALLED_APPS, se encuentra la aplicación django.contrib.staticfiles. Debería ser algo así:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
]

El siguiente paso será definir, en este mismo archivo plantillas/settings.py, la ruta desde donde se servirán los recursos estáticos. Para ello se emplea la variable STATIC_URL, a la que damos valor:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'

A continuación creamos esta carpeta, que quedará en primera/static y metemos una imagen de pruebas (que quedará en primera/static/img_snowtops.jpg)

mkdir primera/static
cd primera/static
wget https://www.w3schools.com/w3css/img_snowtops.jpg
cd ../..

Y modificamos nuestra plantilla primera/templates/layout_3col.html para incluir esa imagen. La vamos a meter, por defecto, en la columna1. Para eso modificamos la parte del bloque columna1 del template, que quedará así:

{% block columna1 %}
        <<h3>Column 1<</h3>
        <<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...<</p>
        <<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris...<</p>
        {% load static %}
        <<img src="{% static "img_snowtops.jpg" %}" alt="My image"/>
{%endblock %}

Es probable que el proyecto plantillas también tenga activos estáticos que no estén vinculados a una aplicación en particular, como podría ser primera. Además de usar un directorio /static dentro de sus aplicaciones, puede definir una lista de directorios (STATICFILES_DIRS) en su archivo de configuración del proyecto (plantillas/settings.py) donde Django también buscará archivos estáticos. Por ejemplo vamos a crear el directorio plantillas/static y /another-path/to/static-files/:

mkdir static #quedará al mismo nivel que la raíz del proyecto, es decir, al mismo nivel que manage.py
mkdir /another-path/to/static-files

Y metemos en el primer directorio una imagen de pruebas:

cd static
wget https://www.unizar.es/sites/default/files/identidadCorporativa/imagen/logoUZ.png
mv logoUZ.png logo.png

Hay que indicarle a Django que busque estáticos en estos paths, además de dentro de las carpetas static de las aplicaciones. Para ello editamos plantillas/settings.py y añadimos al final:

STATICFILES_DIRS = [
        os.path.join(BASE_DIR, "static"),
        "/another-path/to/static-files/",
]

Volvemos a modificar la plantilla de nuestra aplicación, es decir, primera/templates/layout_3col.html, para incluir la imagen que acabamos de descargar. La incluiremos por ejemplo en el bloque titulo. Quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "logo.png" %}" alt="My LOGO"/>
{%endblock %}

Hay que notar que este método funcionará en entorno de desarrollo, siempre que la aplicación django.contrib.staticfiles esté entre las incluídas en el proyecto, y si DEBUG está a True. Para servir ficheros en producción, consulta éste enlace: https://docs.djangoproject.com/en/1.11/howto/static-files/deployment/

Y, ¿qué sucede si tengo dos aplicaciones, por ejemplo primera y segunda, ambas con sus directorios de static, y dentro de ellos un fichero que se llame igual, por ejemplo, logo.png, pero se trata de un fichero distinto?

Vamos a probarlo. Para ello nos vamos al raíz de nuestro proyecto plantillas y creamos una nueva aplicación:

django-admin startapp segunda # crea la aplicación "segunda" en el directorio "segunda"

La damos de alta entre las INSTALLED_APPS del proyecto. Para eso editamos plantillas/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'primera',
    'segunda',
]

Creamos el directorio templates y static dentro de "segunda":

mkdir segunda/templates
mkdir segunda/static
cd segunda/static

#traemos una imagen de logo que sea distinta a la anterior. por ejemplo:
wget https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/ITunes_12.2_logo.png/600px-ITunes_12.2_logo.png
mv 600px-ITunes_12.2.logo.png logo.png
cd ..

#copiamos las mismas plantillas de primera a segunda
cp primera/templates/* segunda/templates/

Generamos una nueva ruta en el fichero controlador principal plantillas/urls.py:

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^primera/', include('primera.urls')),
    url(r'^segunda/', include('segunda.urls')),
]

Creamos el fichero segunda/urls.py:

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url(r'^$', views.current_date),
]

Y también el fichero segunda/views.py:

from __future__ import unicode_literals

from django.shortcuts import render

def current_date(request):
    current_date = "No tengo reloj"
    return render(request, 'hora.html', locals())

Tenemos dos aplicaciones "iguales", pero dos logos distintos. ¿Cuál se cargará?

PRIMERA:

SEGUNDA:

Oh, oh. Problemas. Ambas me cargan la misma imagen, ¡pero deberían ser imágenes distintas!

¿Y si también tuviéramos un "logo.png" en nuestro directorio de estáticos del proyecto, que también fuera distinto? ¿Cuál cargaría?

Este comportamiento se indica en la documentación oficial de Django:

Now we might be able to get away with putting our static files directly in my_app/static/ (rather than creating another my_app subdirectory), but it would actually be a bad idea. Django will use the first static file it finds whose name matches, and if you had a static file with the same name in a different application, Django would be unable to distinguish between them. We need to be able to point Django at the right one, and the easiest way to ensure this is by namespacing them. That is, by putting those static files inside another directory named for the application itself.

Si quieres aprender más en detalle cómo Django busca estáticos, consulta éste enlace: https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-STATICFILES_FINDERS

Para desambiguar estos nombres iguales, lo mejor es darles un namespace.

Para ello:

mkdir -p primera/static/primera
mkdir -p segunda/static/segunda

mv primera/static/logo.png primera/static/primera/
mv segunda/static/logo.png segunda/static/segunda/

Y editamos el bloque titulo de las plantillas de ambas aplicaciones:

primera/templates/layout_3col.html quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "primera/logo.png" %}" alt="My LOGO"/>
{%endblock %}

segunda/templates/layout_3col.html quedaría así:

{% block titulo %}
    <h1> titulo principal </h1>
    {% load static %}
        <img src="{% static "segunda/logo.png" %}" alt="My LOGO"/>
{%endblock %}

Modelo de datos

Empezaremos configurando la base de datos. Por defecto viene con sqlite3, como se puede comprobar en settings.py:

# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases

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

Vamos a configurar un MySQL. Para ello es necesario tener instalado tanto el servidor como el cliente.

En OS X se puede instalar fácilmente con brew:

brew install mysql

Para arrancar el servidor, también lo haremos con brew:

brew services start mysql

==> Tapping homebrew/services
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-services'...
remote: Enumerating objects: 17, done.
remote: Counting objects: 100% (17/17), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 17 (delta 0), reused 12 (delta 0), pack-reused 0
Unpacking objects: 100% (17/17), done.
Tapped 1 command (50 files, 62.2KB).
==> Successfully started `mysql` (label: homebrew.mxcl.mysql)

Nos conectaremos como root (de momento lo dejamos sin password) y creamos una base de datos (la llamaremos ejemplo) y un usuario (django) que tenga permisos sobre ella:

mysql -u root

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.13 Homebrew

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE ejemplo;
Query OK, 1 row affected (0,11 sec)

mysql> CREATE USER django IDENTIFIED BY 'mypass';
Query OK, 0 rows affected (0,16 sec)

mysql> GRANT ALL PRIVILEGES ON ejemplo.* TO 'django';
Query OK, 0 rows affected (0,04 sec)

Además instalamos pymysql, que es como Django se conectará a MySQL. Y ya de paso instalamos el paquete cryptography que nos hará falta más adelante:

pip install pymysql
pip install cryptography

Y a continuación editaremos nuestro fichero __init__.py del proyecto (que seguramente estará vacío) y le pondremos el siguiente contenido:

import pymysql

pymysql.install_as_MySQLdb()

Ahora sí, podemos modificar settings.py para añadir nuestra BD. Podemos poner los datos en ese mismo fichero o, lo que sería más elegante, poner los datos de conexión en un fichero aparte. Tal que así:

Fichero settings.py:

DATABASES = {
    'default':{
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/path/to/mysql_data.cnf',
        }
    }
}

Fichero /path/to/mysql_data.cnf:

[client]
database = ejemplo
user = django
password = mypass
default-character-set = utf8

En Django, en general:
- Cada modelo es una clase python que hereda de django.db.models.Model
- Cada atributo del modelo representa un campo de la base de datos

Vamos a crear nuestro primer modelo. Se llamará Alumno y lo crearemos dentro del fichero nombredeapp/models.py

from django.db import models

class Alumno(models.Model):
    nombre = models.CharField(max_length=50)
    apellido1 = models.CharField(max_length=50)
    apellido2 = models.CharField(max_length=50)
    nip = models.PositiveIntegerField(max_length=6)

Una vez creado el modelo, Django se encargará de crear la correspondiente tabla en la Base de datos. Para ello:

python manage.py makemigrations 
# indica a django que hay cambios en el modelo y que deseamos 
# guardar esos cambios en el archivo de migración. 
# Generará por tanto en el directorio "migrations" de la aplicación 
# un fichero llamado 0001_initial.py

Y una vez se ha generado el fichero en el directorio migrations de la aplicación, utilizamos:

python manage.py migrate
# cogerá todas las migraciones que no han sido aplicadas aún
# (que django controla en una tabla especial de su bd llamada django_migrations)
# y las ejecuta contra la base de datos.

¿Y si ya tengo las tablas creadas? ¿Puede Django generar el modelo? Sí.

Vamos a verlo.

Creamos la tabla:

CREATE TABLE IF NOT EXISTS `clientes` (
`nif` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Clave primaria',
`nombre` varchar(50) NOT NULL COMMENT 'nombre cliente',
`apellidos` varchar(100) NOT NULL COMMENT 'Apellidos cliente',
`telefono` int(9) NOT NULL COMMENT 'móvil',
`codigo_postal` int(5) DEFAULT NULL,
`edad` int(3) DEFAULT NULL,
`sexo` char(1) NOT NULL,
`profesion` text NOT NULL,
PRIMARY KEY (`nif`),
UNIQUE KEY `telefono` (`telefono`),
KEY `nombre` (`nombre`),
FULLTEXT KEY `apellidos` (`apellidos`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='tabla de clientes';

Para generar el modelo:

python manage.py inspectdb > primera/models.py

Y si miramos primera/models.py... 🙂

more models.py
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from __future__ import unicode_literals

from django.db import models


class Clientes(models.Model):
    nif = models.AutoField(primary_key=True)
    nombre = models.CharField(max_length=50)
    apellidos = models.CharField(max_length=100)
    telefono = models.IntegerField(unique=True)
    codigo_postal = models.IntegerField(blank=True, null=True)
    edad = models.IntegerField(blank=True, null=True)
    sexo = models.CharField(max_length=1)
    profesion = models.TextField()

    class Meta:
        managed = False
        db_table = 'clientes'

Vamos a hacer primero un pequeño cambio en primera/models.py, cambiaremos Managed = True y así indicamos a Django que pueda gestionar la tabla.

A continuación, vamos a probar a generar algún Cliente. Para eso podemos usar el shell que nos provee Django:

python manage.py shell
>>> from primera.models import Clientes
>>> micli = Clientes(nif=1234, nombre="Maikel", apellidos="Night", telefono='600000000', profesion='tunante')
>>> help(Clientes)
>>> micli.save()
>>> help(Clientes.objects)
>>> todos = Clientes.objects.all()
>>> for cli in todos:
...     print("Cliente con NIF {}".format(cli.nif))
... 
Cliente con NIF 1234

Veamos ahora cómo gestionar las relaciones entre modelos.

Empezamos por el caso más sencillo: relación 1-N. Un modelo de coche tiene un fabricante, pero un fabricante fabrica más de un modelo de coche. Se sugiere que el campo de la FK se llame como el nombre del modelo con el que se relaciona, en minúsculas. Por ejemplo:

from django.db import models

class Manufacturer(models.Model):
    # ...
    pass

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    # ...

El siguiente caso: relación N-N. Por ejemplo, una pizza tiene N ingredientes y un ingrediente puede estar presente en varias pizzas. Se sugiere que el nombre de un ManyToManyField sea el plural en minúscula de cuyos objetos depende. El ManyToManyField solo se colocará en uno de los modelos, no en ambos. En general, las instancias ManyToManyField se colocaran en el objeto que se editará vía formulario. Django creará tres tablas, pizza, topping y pizza_toppings.

from django.db import models

class Topping(models.Model):
        name=models.CharField(max_length=50)
        def __unicode__(self):
                return self.name

class Pizza(models.Model):
    name=models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
    def __unicode__(self):
                return self.name

¿Y si hay que guardar atributos de la relación? Por ejemplo, si queremos saber si un ingrediente de una pizza en concreto está cortado o no. Haríamos algo así:

from django.db import models

class Topping(models.Model):
        name=models.CharField(max_length=50)
        def __unicode__(self):
                return self.name

class Pizza(models.Model):
    name=models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping, through='Estado')
    def __unicode__(self):
                return self.name

class Estado(models.Model):
    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
    topping = models.ForeignKey(Topping, on_delete=models.CASCADE)
    esta_cortado = models.BooleanField()

Veamos ahora la relación 1-1. Por ejemplo, si estuviera creando una base de datos de 'lugares', crearías elementos bastante estándar como la dirección, el número de teléfono, etc. en la base de datos. Luego, si deseas construir una base de datos de 'restaurantes' asociados a los lugares, en lugar de repetirlos y replicar esos campos en el modelo de 'restaurante', podrías hacer que el 'restaurante' tenga un OneToOneField a Lugar (porque un restaurante es un lugar; en de hecho, para manejar esto, normalmente se usa la herencia, que implica una relación uno a uno implícita).:

from django.db import models

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

    def __str__(self):
        return "%s the place" % self.name

class Restaurant(models.Model):
    place = models.OneToOneField(
    Place,
    on_delete=models.CASCADE,
    primary_key=True,
    )
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

    def __str__(self):
        return "%s the restaurant" % self.place.name

class Waiter(models.Model):
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
    name = models.CharField(max_length=50)

    def __str__(self):
        return "%s the waiter at %s" % (self.name, self.restaurant)

Y juguemos:

>>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton')
>>> p1.save()
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
Create a Restaurant. Pass the ID of the “parent” object as this object’s ID:

>>> r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
A Restaurant can access its place:

>>> r.place

A Place can access its restaurant, if available:

>>> p1.restaurant

Veamos ahora los validadores que ofrece Django. Un validador es una función que genera un error de validación ValidationError si no cumple con algunos criterios.

Por ejemplo definimos un validador de números pares:

from django.core.exceptions import ValidationError

def validate_even(value):
    if value % 2 != 0:
        raise ValidationError("no es par")

Y lo podemos añadir a un modelo:

from django.db import models

class MyModel(models.Model):
    even_field = models.IntegerField(validators=[validate_even])

Más adelante veremos cómo integrar validaciones en los formularios.