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">«</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">«</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">»</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">»</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