Introducción a Django con Docker

Miguel Cantillana <mcantillana@linets.cl>

¿Que veremos hoy?

  • Introducción a docker
  • Django + docker
    • Creación de un proyecto
    • Models
    • Admin
    • Views & Templates
    • Forms
    • Context processor
    • Messages
  • Deploy con docker compose

¿Quién Soy?

  • Miguel Cantillana <mcantillana@linets.cl>
    • Ingeniero Civil Informático
    • Gerente Desarrollo E-Commerce at Linets​
    • Profesor  adjunto UNAB
    • Áreas de interés
      • Ingeniería de software
      • Desarrollo Web
      • E-Commerce
      • Infraestructura y alta disponibilidad
  • Python lovers!

¿Qué es docker?

Docker es una plataforma que permite construir, ejecutar y compartir aplicaciones mediante contenedores. La idea base de docker es poder empaquetar aplicaciones que puedan ser compartidas mediante contenedores.

¿Qué es docker?

  • Con docker podemos definir nuestros componentes de ejecución y luego llevar estas definiciones a productivo si fuese necesario o bice versa
  • Nuestro ambiente local puede estar homologado al ambiente productivo
  • Podemos empaquetar de manera simple nuestras aplicaciones

Algunas ventajas

¿Qué es Django?

El web framework para perfeccionistas con deadline

¿Qué es Django?

  • Django es un entorno de desarrollo web escrito en Python que fomenta el desarrollo rápido y el diseño limpio y pragmático (más rápido y con menos código)
  • La meta fundamental de Django es facilitar la creación de sitios web complejos
  • Django pone énfasis en el re-uso, la conectividad y extensibilidad de componentes, el desarrollo rápido y el principio No te repitas

  • y lo mejor..es de código abierto!!

Características

  • Utiliza ORM
  • Aplicaciones "enchufables" que pueden instalarse en cualquier página gestionada con Django.
  • Un sistema extensible de plantillas .
  • Soporte de internacionalización, incluyendo traducciones incorporadas de la interfaz de administración.
  • Un sistema incorporado de "vistas genéricas" que ahorra tener que escribir la lógica de ciertas tareas comunes

El Zen de Python

  • Hermoso es mejor que feo.
  • Explícito es mejor que implícito.
  • Simple es mejor que complejo.
  • Complejo es mejor que complicado.
  • Sencillo es mejor que anidado.
  • Escaso es mejor que denso.
  • La legibilidad cuenta.

Arquitetura de Django​

Arquitetura de Django​

Arquitetura de Django​

A codear!

Qué necesitamos!

  • Docker
  • Docker compose
  • Editor de código
  • Alguna shell (e.g. git bash)
  • git*

¿Qué desarrollaremos?

https://demo.pycon.e2l.dev/

¿Qué desarrollaremos?

https://github.com/mcantillana/django-pycon

Instalación y primera aplicación

docker-compose.yml

version: '3'

services:
  web:
    image: mcantillana/django_pycon:latest
    command: python manage.py runserver 0:8000
    ports:
      - "8000:8000"
    volumes:
      - .:/code
    networks:
      - net-pycon

volumes:
  db_data: 

networks:
  net-pycon:
docker-compose run web django-admin startproject main .
├── docker-compose.yml
├── main
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py

Estructura del proyecto

docker-compose run web python manage.py migrate 

docker-compose up

docker-compose up -d

settings.py

apps

Django Apps

  • Un proyecto puede contener "n" Aplicaciones
  • Una aplicación de Django se puede usar con varios proyectos
  • Una Apps debe tener alta Cohesión y bajo acoplamiento 

 

docker-compose run web python manage.py startapp catalog

├── catalog
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── docker-compose.yml
├── Dockerfile
├── main
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
INSTALLED_APPS = [
    ...
    'catalog.apps.CatalogConfig',
]

Registrando APP

models.py

Modelo relacional

Product


class Product(models.Model):
    """
    Clase que permite modelar un producto
    """

    name = models.CharField(max_length=144)
    image = models.ImageField(upload_to='catalog/')
    sku = models.CharField(max_length=50, unique=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.IntegerField(default=0)
    short_description = models.TextField(blank=True, null=True)
    description = models.TextField(blank=True, null=True)
    status = models.BooleanField()
    sort_order = models.PositiveIntegerField(default=0)
    
    categories = models.ManyToManyField(Category)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

Category


class Category(models.Model):
    """
    Modelo de categorias
    """
    name = models.CharField(max_length=144)
    status = models.BooleanField(default=True)
    sort_order = models.PositiveIntegerField(default=0)

    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

Migraciones

## creamos una migración del módelo
docker-compose exec web python manage.py makemigrations

## Aplicamos una migración a la DB
docker-compose exec web python manage.py migrate

admin.py

docker-compose exec web python manage.py createsuperuser

<url base>:<port>/admin

Registrando modelo Category

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'status')

Registrando modelo product

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = (
        'name',
        'sku',
        'price',
        'quantity',
        'status',
        'sort_order',
        'image_tag'
    )

    def image_tag(self,obj):
        tag_image = '<img src="{0}" style="width: 45px; height:45px;" />'.format(
			obj.image.url)
        return format_html(tag_image)

    list_filter = ('status', 'categories')
    search_fields = ('sku', 'name')

views.py

from django.shortcuts import render


def category(request, category_id):
    template_name = 'category.html'
    data = {}
    return render(request, template_name, data)
from django.urls import path
from django.urls.conf import include
from catalog import views


urlpatterns = [
    path('category/<int:category_id>', views.category),
]

urls.py

from django.urls import path
from django.urls.conf import include
from catalog import views


urlpatterns = [
    path('category/<int:category_id>', views.category),
    path('category/<int:category_id>/product/<int:product_id>', views.product),
]

templates (HTML)

views.py + models.py 

from django.shortcuts import render
from catalog.models import Category, Product

def category(request, category_id):
    template_name = 'category.html'
    data = {}

    data['category'] = Category.objects.get(pk=category_id)
    data['products'] = Product.objects.filter(categories__id=category_id)

    return render(request, template_name, data)

def product(request, category_id, product_id):
    template_name = 'product.html'
    data = {}
    data['category'] = Category.objects.get(pk=category_id)
    data['product'] = Product.objects.get(pk=product_id)
    return render(request, template_name, data)

views.py

...
	<div class="row">
            <div class="col mb-2">
                <h2 class="page-title">{{category.name}}</h2>
            </div>
            <div class="row">
                {% for product in products %}
                <div class="col-md-3 col-6 mb-4">
                    <div class="card" >
                        <a href="{% url 'catalog:product' category.id product.id %}">
							<img src="{{product.image.url}}" class="card-img-top" alt="{{product.name}}">
						</a>
                        <div class="card-body">
                          <h5 class="card-title">{{product.name}}</h5>
                          <p class="card-text">{{product.sku}}</p>
                          <a href="{% url 'catalog:product' category.id product.id %}" class="btn btn-primary">Comprar</a>
                        </div>
                      </div>
                </div>
                {% endfor %}                                        
            </div>

        </div>
....

category.html


def category(request, category_id):
    template_name = 'category.html'
    data = {}

    page = request.GET.get('page', 1)

    data['category'] = Category.objects.get(pk=category_id)
    products = Product.objects.filter(categories__id=category_id).order_by('sort_order')
    paginator = Paginator(products, 1) # Show 25 contacts per page.

    try:
        data['products'] = paginator.page(page)
    except PageNotAnInteger:
        data['products'] = paginator.page(1)
    except EmptyPage:
        data['products'] = paginator.page(paginator.num_pages)
    

    return render(request, template_name, data)

Paginación (views.py)

              {% if products.has_other_pages %}
                <nav aria-label="Page navigation example">
                    <ul class="pagination justify-content-end">
                      {% if products.has_previous %}
                      <li class="page-item">
                        <a class="page-link" href="?page={{ product.previous_page_number }}" aria-label="Previous">
                          <span aria-hidden="true">&laquo;</span>
                        </a>
                      </li>
                      {% else %}
                      <li class="page-item disabled">
                        <a class="page-link">&laquo;</a>
                      </li>
                      {% endif %}

                      {% for page in products.paginator.page_range %}
                        {% if products.number == page %}
                          <li class="page-item active"><a class="page-link" href="?page={{ page }}">{{ page }}</a></li>
                        {% else %}
                          <li class="page-item"><a class="page-link" href="?page={{ page }}">{{ page }}</a></li>
                        {% endif %}
                      {% endfor %}
   
                      {% if products.has_next %}
                      <li class="page-item">
                        <a class="page-link" href="?page={{ products.next_page_number }}" aria-label="Next">
                          <span aria-hidden="true">&raquo;</span>
                        </a>
                      </li>
                      {% else %}
                      <li class="page-item disabled">
                        <a class="page-link">&raquo;</a>
                      </li>
                      {% endif %}
                    </ul>
                  </nav>
                  {% endif %}

Paginación (HTML)

Home

Nueva app

docker-compose exec web python manage.py startapp cms

Registrar App

INSTALLED_APPS = [
...
    'cms.apps.CmsConfig',
]

Crear urls.py

from django.urls import path
from cms import views

app_name = 'cms'
urlpatterns = [
    path('', views.home, name='home'),
]

Conectar URL de la app


urlpatterns = [
...
    path('', include('cms.urls')),
]

Crear home.html

{% extends "base.html" %}

{% block main %}
<main class="mt-4">
    <div class="container">
        <div class="row">
            <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel">
                <div class="carousel-indicators">
                  <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
                  <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="1" aria-label="Slide 2"></button>
                  <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="2" aria-label="Slide 3"></button>
                </div>
                <div class="carousel-inner">
                  <div class="carousel-item active">
                    <img src="https://via.placeholder.com/1500x600" class="d-block w-100" alt="...">
                  </div>
                  <div class="carousel-item">
                    <img src="https://via.placeholder.com/1500x600" class="d-block w-100" alt="...">
                  </div>
                  <div class="carousel-item">
                    <img src="https://via.placeholder.com/1500x600" class="d-block w-100" alt="...">
                  </div>
                </div>
                <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev">
                  <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                  <span class="visually-hidden">Previous</span>
                </button>
                <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next">
                  <span class="carousel-control-next-icon" aria-hidden="true"></span>
                  <span class="visually-hidden">Next</span>
                </button>
              </div>
        </div>
        
        <div class="row mt-4">
            <div class="col-12">
                <h3>Oportunidades</h3>
            </div>
            <div class="col-md-3 col-6 mb-3"><a href=""><img src="https://via.placeholder.com/500x500" class="img-fluid" alt=""></a></div>
            <div class="col-md-3 col-6 mb-3"><a href=""><img src="https://via.placeholder.com/500x500" class="img-fluid" alt=""></a></div>
            <div class="col-md-3 col-6 mb-3"><a href=""><img src="https://via.placeholder.com/500x500" class="img-fluid" alt=""></a></div>
            <div class="col-md-3 col-6 mb-3"><a href=""><img src="https://via.placeholder.com/500x500" class="img-fluid" alt=""></a></div>
        </div>
    </div>
</main>
{% endblock %}

views + home.html

from django.shortcuts import render

def home(request):
    template_name = 'home.html'
    data = {}

    return render(request, template_name, data)

Menú Dinámico

context_processor.py

# catalog/context_processors.py
from catalog.models import Category

def categories(request):

    items = Category.objects.filter(status=True)
    return {
        'items_categories': items
    }

Registrando context processors

## main/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
				...
                'catalog.context_processors.categories',
            ],
        },
    },
]

Utilizando context processors

...
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
              Productos
            </a>
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
              {% for category in items_categories %}
                <li><a class="dropdown-item" href="{% url 'catalog:category' category.pk %}">{{category.name}}</a></li>
              {% endfor %}
            </ul>
          </li>
...

forms.py

Creamos el forms.py

from django.forms import ModelForm
from contacts.models import Contact


class ContactForm(ModelForm):
     class Meta:
         model = Contact
         fields = '__all__'

Instanciamos el formulario en views.py

def add_contact(request):
    template_name = 'add_contact.html'
    data = {}

    if request.method == 'POST':

        data['form'] = ContactForm(request.POST)

        if data['form'].is_valid():
            
            data['form'].save()

			...
       
    else:
        data['form'] = ContactForm()

    return render(request, template_name, data)

Utilizamos el formulario en el template

...

  <form action="" method="post" class="form">
    {% csrf_token %}                    
    {% bootstrap_form form %}
    {% buttons %}
    <button type="submit" class="btn btn-primary">Submit</button>
    {% endbuttons %}

  </form>
...

Messages

str_msg = 'Registro exitoso!'
messages.add_message(
  request,
  messages.SUCCESS,
  str_msg
)
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
  {{ message }}
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}

Messages + bootstrap

MESSAGE_TAGS = {
    messages.DEBUG: 'alert-info',
    messages.INFO: 'alert-info',
    messages.SUCCESS: 'alert-success',
    messages.WARNING: 'alert-warning',
    messages.ERROR: 'alert-danger',
}

Deploy ...

Preparando el proyecto para producción

Variables de entorno

Estáticos

DEBUG=False

Preparando el servidor para producción

Instalando docker & docker compose

https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04-es

generar llaves para repositorios

ssh-keygen -t rsa

Clonar proyecto de github

git clone -b <branch> git@github.com:<user>/<repository>.git

Crear un docker-compose prod

cp docker-compose.yml docker-compose-prod.yml
## docker-compose-prod.yml
version: '3'

services:
  web:
    build: .
    image: mcantillana/pycon:latest
    command: gunicorn -b 0:8000 main.wsgi
    env_file: 
      - .env
    environment:
      - VIRTUAL_HOST=demo.pycon.e2l.dev
      - VIRTUAL_PORT=8000
      - LETSENCRYPT_HOST=demo.pycon.e2l.dev
      - LETSENCRYPT_EMAIL=mcantillana@linets.cl
    expose:
      - "8000"
        
    ports:
      - "8000:8000"
    volumes:
      - .:/code
    networks:
      - net-pycon

networks:
  net-pycon:
    external:
      name: nginx-proxy

nginx-proxy

clonar nginx-proxy

git clone https://github.com/mcantillana/nginx-proxy

levantar nginx-proxy

docker-compose -f letsencrypt.yml -f docker-compose.yml up -d

levantar nuestro proyecto

docker-compose -f docker-compose-prod.yml up -d

Done!

Pycon CL 2021 - Introducción a Django con Docker

By Miguel Cantillana

Pycon CL 2021 - Introducción a Django con Docker

  • 350