Disclaimer: This is my first post in English I apologize in advance for any errors, feel free to correct me.

In short django-form-field provides a model field wich allows to associate custom forms to model instances. It uses a small DSL (Domain specific language) to define a form, and then generates standard Django forms from that. The DSL is based in the one described in this presentation and the syntax is almost identical, you can define a form like this:

width: Decimal-> min: 30.0 max:300.0
height: Decimal-> min: 50.0 max: 700.0
engraving: Text-> required: False

which is equivalent to this django form:

from django import forms

class CustomForm(forms.Form):
    width = forms.DecimalField(min_value=30, max_value=300)
    height = forms.DecimalField(min_value=50, max_value=700)
    engraving = forms.TextField(required=False)

Usage

As I allways learn most and easiest by example, let's write one

# models.py
from django.db import models
from param_field import ParamField, ParamDict
from jsonfield import JSONField

class CustomProduct(models.Model):
    name = models.CharField(max_length=44)
    description = models.CharField(max_length=500)
    ....
    params = ParamField(blank=True, max_length=3000)

class CustomProductRequest(models.Model):
    """To store client submited forms as a dictionary in a JSON Field"""
    product = models.ForeignKey(CustomProduct)
    user_params = JSONField()
    ...

Those could be some of the models from an e-commerce site, where all the products can be customized and each one requires a different form. Custom_product has a ParamField that contains the form, and CustomProductRequest has a JSONField that is used to store the valid returned form.

We can now add a new product to the DB with:

>> custom_params ="""
    width: Decimal-> min: 15.0 max: 35.0
    depth: Decimal-> min: 25.0 max: 20.0
    height: Decimal-> min: 10.0 max:20.0
    lacquer: Bool"""

>> CustomProduct.objects.create(
        name = "Wooden Box",
        description = "Not any box, A CUSTOM wooden box!",
        params = custom_params)

The view serving the product details along with it's form should be similar to this:

# views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import FormView
from django import forms
from .models import CustomProduct

class CustomProductFormView(FormView):
    template_name = 'product_form.html'
    form_class = forms.Form
    success_url = 'cart/'

    def dispatch(self, request, *args, **kwargs):
        """Find requested CustomProduct needed to generate the form"""
        pk = self.kwargs['pk']
        self.product = get_object_or_404(CustomProduct, pk=pk)
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        """Send product info to template"""
        context = super().get_context_data(**kwargs)
        context['product'] = self.product
        return context

    def get_form(self, form_class=None):
        """Generate form from param_field"""
        # NOTE: params.form(...) will return None when it doesn't
        # containt any field.
        return self.product.params.form(**self.get_form_kwargs())

    def form_valid(self, form):
        """At this point the form is a valid django form like any other"""
        # Just store the user request
        prod_request = CustomProductRequest.objects.create(
            user = self.request.user,
            product = self.product,
            user_params = form.cleaned_data.copy())

        # Do more things
        ...

        return super().form_valid(form)

It just generates the form, and store the valid responses in a CustomProductRequest model.

File support

Of course all of the above only works for simple fields, but if we enable support to File and Image parameters in the field:

...
params = ParamField(blank=True, file_support=True)
...

We can now define forms that accept files:

requested_image: Image-> required:True
scale: Decimal-> default: 1.0

To save those files we will need an extra model to store them, and modify form_valid() to handle them separately.

So we add another model called UserUploadedFile to store the file:

# models.py
...
class UserUploadedFile(models.Model):
    """To store client submited files"""
    product = models.ForeignKey(CustomProductRequest)
    name = models.CharField(max_length=80)
    user_file = models.FileField(upload_to="uploads/")

And lastly we modify form_valid() to separate File fields from the others:

# views.py
from django.core.files.uploadedfile import UploadedFile

def form_valid(self, form):

    fields = {}
    file_fields = {}

    # Separate File fields from the rest and dicard empty ones
    for key, value  in form.cleaned_data.items():
        if isinstance(value, UploadedFile):
            file_fields[key] = value
        elif value is not None:
            fields[key] = value

    # Store all fields except File and Image into JSONfield
    product_request = CustomProductRequest.objects.create(
            user = self.request.user,
            product = self.product,
            user_params = fields)

    # Link all uploaded files to product_request
    for key, value in file_fields.items():
        UserUploadedFile.objects.create(
            product = product_request,
            name = key,
            user_file=value)

    return super().form_valid(form)

That's all, and yes I know some of you DB guys are shaking your heads and saying that this is not how it is done, I understand. But sometimes you need something simple and/or fast.

Ver comentarios


En casi cualquier aplicación que use AJAX existe algún formulario con un campo que acepta JSON, para que la aplicación sea mínimamente robusta además es necesario verificar que todo el código JSON recibido está correctamente formado antes de procesarlo, y la mejor manera de hacer esto es usar un validador para que el formulario lo detecte y genere los errores. En este pequeño post trato de explicar como puedes hacerlo con algunos ejemplos.

Aunque Django no incluye un campo especifico JSON (por ahora), si que existe una app llamada django-json-field que nos proporciona el campo JSONFormField:

from django import forms
from json_field.fields import JSONFormField

class UserRequest(forms.Form):
    request = JSONFormField()

Desafortunadamente JSONFormField tiene problemas con los validadores, en lugar de aplicarlos tras convertir el valor JSON a objetos python como hacen los campos nativos, los aplica antes de la conversión cuando el valor es aún una cadena. Por eso a continuación describo un par de soluciones a este problema, si estas usando un campo distinto puedes saltar al final del post.

Custom Form Field

La primera y mi favorita es modificar la método clean del campo para que pueda aplicar validadores extra tras la conversión de JSON:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError
from django import forms

class JSONValidatedFormField(JSONFormField):

    def __init__(self, *args, **kwargs):
        self.json_validators = kwargs.pop('json_validators', [])
        super(JSONValidatedFormField).__init__(*args, **kwargs)

    def clean(self, value):
        value=super(JSONValidatedFormField, self).clean(value)
        original_validators = self.validators
        self.validators = self.json_validators

        try:
            self.run_validators(value)
            self.validators = original_validators
        except Exception as e:
            self.validators = original_validators
            raise

        return value

JSONValidatedFormField acepta un nuevo parámetro extra llamado json_validators que es una lista de validadores que se aplican DESPUES de la conversión del valor a python. Por ejemplo:

def validar_peticion(value):
    """ Valida que el formato JSON es:
        {"request": "NEW",
        "data": "Texto del nuevo post"}
    """
    if not isinstance(value, dict):
        raise ValidationError("Se esperaba un diccionario.")
    if not all (k in foo for k in ("request","data")):
        raise ValidationError("formato de peticion erroneo.")


class RequestForm(forms.Form):
    request = JSONValidatedFormField(json_validators=[validar_peticion])

Si usas el campo de forma extensiva dentro de la aplicación también puedes crear un campo customizado:

class JSONRequestFormField(JSONValidatedFormField):

    def __init__(self, *args, **kwargs):
        super(JSONRequestFormField, self).__init__(*args, **kwargs)
        self.json_validators.append(validar_peticion)

Validador

La segunda opción es crear un validador que aplique su propio parser JSON a los datos antes de validarlos.

from django.core.exceptions import ValidationError
import json

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion=opcion

    def __call__(self, value):
        try:
            request = json.loads(value)
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

El problema es que inserta duplicidades de código e inconsistencias entre el parser JSON del campo y del validador, esto lo puedes mejorar usando el método clean del propio campo como parser:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion = opcion

    def __call__(self, value):
        try:
            json_field = JSONFormField()
            request = json_field.clean(value)
        except ValidationError:
            raise
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

Esta solución es parcial puesto que para algunos valores de incialización del campo en el formulario el parser puede diferir.

JSON Schema

Una vez tengas funcionando JSONFormField o el campo de tu elección, el siguiente paso es crear el validador para el formato que estés usando, en casos simples puedes usar un sistema similar al de los ejemplos anteriores comprobando manualmente los valores, para casos más complejos no es práctico y te recomiendo la biblioteca jsonschema. Con ella puedes definir un esquema que describe el formato JSON que es aceptable y luego usarlo para validar los datos recibidos. Su sintaxis es potente y flexible:

from django.core.exceptions import ValidationError
from jsonschema import validate

class JSONSchemaValidator(object):

    def __init__(self, schema=None, message="Invalid JSON object"):
        self.schema = schema
        self.message = message

    def __call__(self, value):
        try:
            validate(value, self.schema)
        except Exception:
            raise ValidationError(self.message, code=self.code, params={'value':value})


class JSONRequestValidator(JSONSchemaValidator):

    def __init__(self, schema=None, message=):
        self.schema = {
            "title": "User Request Schema",
            "type": "object",
            "properties": {
                "request": {
                    "type": "string"
                },
                "data": {
                    "type": "string"
                }
            },
            "required": ["request", "data"]
        }
        self.message="Invalid JSON Request"

En la documentación tienes ejemplos más avanzados de lo que se puede llegar a hacer:

{
    "title": "Example Schema",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        },
        "lastName": {
            "type": "string"
        },
        "age": {
            "description": "Age in years",
            "type": "integer",
            "minimum": 0
        }
    },
    "required": ["firstName", "lastName"]
}

O con expresiones regulares:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "/": {}
    },
    "patternProperties": {
        "^(/[^/]+)+$": {}
    },
    "additionalProperties": false,
    "required": [ "/" ]
}

Seguro que encuentras alguna manera de adaptarlo a tu problema.

Ver comentarios


django logo

Ya está disponible la primera alfa de Django 1.9, no es una versión "Long Time Support" como la 1.8 y saltársela probablemente sea la mejor decisión, pero aun así tiene algunas novedades que me han parecido más interesantes:

  • Validación de claves para prevenir el uso de claves débiles. Incluye longitud mínima, claves comunes, claves numéricas, y claves similares a los atributos del usuario.
  • Ejecución de tests en paralelo con la opción --parallel.
  • Nuevos Mixins de permisos para Class Based Views AccessMixin, LoginRequiredMixin, PermissionRequiredMixin, and UserPassesTestMixin.
  • Campos de formularios ahora tienen la opción disabled para que los widget aparezcan desactivados en el navegador.

Si te interesa en la nota de lanzamiento encontrarás todos los detalles.

Ver comentarios


Antes de empezar quiero confesar que no soy ningún especialista en diseño web (como se puede apreciar en este blog), siempre me ha parecido mas interesante la programación, por lo que intento conseguir los mejores resultados dedicando el mínimo tiempo y esfuerzo posible a HTML y CSS. Para ello uso frameworks como Bootstrap o Foundation en la mayoría de mis proyectos, y los adapto usando el sistema de plantillas de Django.

El proceso es sencillo y en este tutorial describo como hacerlo para Bootstrap, aunque es válido para cualquier otro framework.

Base

El sistema de plantillas de Django es jerárquico de manera que unas plantillas extienden o heredan de otras, todo empieza con base.html la plantilla a partir de la cual se crean las demás, esta contiene la estructura mínima HTML5 así como los elementos necesario para el funcionamiento del framework Bootstrap (CSS y librarias JS).

{% load staticfiles %}

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>{% block title %}Dominio | Titulo pagina {% endblock %}</title>
        <meta name="description" content="{% block description %}{% endblock %}"/>

        <link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet">
        <link href="{% static "bootstrap/css/local.min.css" %}" rel="stylesheet">

        <!--[if lt IE 9]>
            <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
            <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
        {% block head %}
        {% endblock %}
    </head>

    <body>
        {% include "navbar.html" %}

        <div class="container">
            {% block content %}
            {% endblock %}
        </div>

        {% include "footer.html" %}

        <!-- All that javascript -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <script src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>

        {% block scripts %}
        {% endblock %}
    </body>
</html>

base.html define 6 bloques que pueden ser modificados/extendidos por otras plantillas:

  • title: Título de la página, con un valor por defecto.
  • description: Descripción de la página.
  • head: Bloque para añadir cualquier tag extra a la cabecera, por ejemplo CSS específico de una página.
  • content: Bloque principal donde se añadirá todo el contenido desde otras plantillas.
  • scripts: Para añadir librerías y código javascript extra.

Las barras de navegación y el pie de página se han añadido usando include por claridad, pero pueden incluirse directamente en base.html. Un archivo navbar.html puede ser algo similar a esto:

<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Project name</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li class="active"><a href="/">Home</a></li>
                <li><a href="{% url 'about' %}">About</a></li>
                <li><a href="{% url 'contact' %}">Contact</a></li>
            </ul>
        </div><!--/.nav-collapse -->
    </div>
</nav>

Si en tu diseño la barra de navegación o el pie de página no son fijos, en su lugar deja sendos bloques vacios para añadirlos posteriormente, también puedes añadir cualquier otro bloque que creas que vas a necesitar.

Estructura

El siguiente paso es extender base.html y crear plantillas que plasman las distintas estructuras que usarán las páginas de la aplicación, para ello usa el framework elegido y añade bloques extensibles en cada uno de los elementos.

Por ejemplo para recrear un diseño simple de dos columnas como el de la figura inferior, solo es necesario añadir los bloques main-content y sidebar puesto que la cabecera y pie de página ya están presentes.


Diseño

Eso se puede crear fácilmente usando Bootstrap:

{% extends "base.html" %}

{% block content %}

    <div class="row">
        <div class="col-md-8 col-sm-12">
            {% block main-content %}{% endblock %}
        </div>
        <div class="col-md-4 hidden-sm">
            {% block sidebar %}{% endblock %}
        </div>
    </div>

{% endblock %}

En este caso he usado la clase hidden-sm para que la barra lateral desaparezca, y col-sm-12 para que la de contenido ocupe toda la ventana cuando el viewport del navegador sea menor de 750px.

Es importante que estas plantillas sean versátiles, por lo que deben contener el mínimo número de elementos necesarios para crear el diseño/estructura, de esta manera que puedan ser reusadas en múltiples páginas. Piensa que es fácil extender plantillas y añadir elementos mas adelante, en cambio es imposible eliminar los elementos existentes.

Includes

Las plantillas con la estructura son extendidas para crear las plantillas finales usadas para renderizar en las vistas, por ejemplo una página de contacto:

{% extends "base-2col.html" %}

{% block title %}Información de contacto{% endblock %}

{% block main-content %}
    <h1>Contacto</h1>
    <hr>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. </p>

    <p>...</p>
{% endblock %}

{% block scripts %}
    <script src="{% static "js/bling.js" %}"></script>
{% endblock %}

Aquí comienza a verse la flexibilidad que ofrece el sistema de plantillas de Django y con que facilidad permite crear nuevas páginas. Aún así queda un problema por solucionar ya que en algunos casos te vas a encontrar varias plantillas con código duplicado, algo que va contra el principio DRY. Por suerte el código se puede refactorizar usando el tag include.

Include es junto a extends uno de los mecanismos con los que se puede reutilizar código entre plantillas. Permite cargar una plantilla dentro de otra, renderizarla usando el contexto actual, e incluir el resultado en su lugar. Por ejemplo en un blog donde más de una vista muestra listas de posts (últimos posts, mas visitados, ...), tendrás varias plantillas con el mismo bucle renderizandolas. Con include se puede crear una plantilla que contiene el bucle, y luego incluirla cada vez que sea necesario monstrar listas de posts:

{% for p in posts %}
    <div class="blog-post">
        <h2 class="blog-post-title">{{p.title}}</h2>
        <p class="blog-post-meta">{{p.date}} by {{p.poster.name}}</p>
        <hr>
        {{p.content}}
    </div>
{% endfor %}

Y luego en la plantilla que tiene que mostrar la lista de posts:

{% extends "base-2col.html" %}

{% block title %}Últimos posts{% endblock %}

{% block main-content %}
    <h1>Ultimos posts</h1>
    {% include "post-list.html" with posts=last-posts %}
{% endblock %}

En este caso la variable de contexto last-posts contiene la lista de los posts que ha de mostrar.

Formularios

Casi todos los frameworks requieren añadir algún tag alrededor de los formularios para poder aplicar estilos, si ese formato no es el generado por django será necesario editar manualmente las plantillas. Por suerte si estas usando Bootstrap existe el app django-bootstrap-form que permite generar formularios usando su formato. Su funcionamiento es muy sencillo:

{% load bootstrap %}

<form role="form">
    <legend>Form Title</legend>
    {% csrf_token %}
    {{ form|bootstrap }}
    <div class="form-group">
      <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

Simplemente hay que cargar los tags al inicio de la plantilla y usar bootstrap o bootstrap_horizontal junto al tag del formulario.

Rendimiento

Por último ten en cuenta que cuantas más veces uses extends e include, más bloques tengan tus plantillas, y mas variables uses más lento será su renderizado. Esto tiene múltiples soluciones, desde cambiar a un sistema de plantillas más rápido como Jinja, a activar el sistema de cacheado de Django , o minimizar el uso de extends uniendo todas las plantillas. Puedes encontrar múltiples artículos sobre el tema:

Yo te recomiendo que lo ignores hasta que se convierta en un problema real, la optimización prematura según dicen es la raíz de todos los males.

Ver comentarios


Portada del libro Two Scoops of Django 1.8

Se acaba de publicar la nueva entrega de Two Scoops of Django la guía definitiva de buenas practicas para Django 1.8, tengo la versión anterior y he comprado esta. No es un libro para principiantes, pero se lo recomiendo a todos los desarrolladores experimentados que seguro que encuentran algún truco o consejo útil. Además la versión de Django 1.8 tiene la particularidad de ser “Long-Term Support” (LTS) por lo que tiene soporte de 3 años, así que el libro va a estar actualizado durante más tiempo que versiones anteriores.

Por ahora sólo está disponible en inglés y lo puedes comprar en Amazon

Ver comentarios


Hoy he estado programando en Django una vista para la descarga de informes, en la que los usuarios registrados de una página pueden pedir un informe de la actividad para un determinado año. La particularidad es que esos informes se generan de forma dinámica en el momento de la petición, nada complicado pero antes de empezar a programar he buscado que paquetes había disponibles, y me he encontrado django-downloadview. Con eso y un poco de código extra para comprobar que el usuario está autenticado he solucionado el problema. Algunas veces CBV parecen magia.

#views.py
from django.shortcuts import render
from django.core.files.base import ContentFile
from django_downloadview import VirtualDownloadView
from django.http import HttpResponseForbidden, HttpResponseServerError


class LoginRequiredMixin(object):

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseForbidden()
        else:
            return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)


class DownloadReportView(LoginRequiredMixin, VirtualDownloadView):

    def generar_report(self, user, year):
        # Generar contenido del informe para el usuario y fecha
        return "Report content for {} {}".format(user.username, year)

    def get_file(self):
        # Metodo de VirtualDownloadView que devuelve el archivo virtual
        report_name = 'report.txt'
        return ContentFile(self.report_content, name=report_name)

    def get(self, request, *args, **kwargs):

        report_year = kwargs.get('year', None)

        # El contenido del informe se genera aquí en lugar de en get_file, para
        # simplificar la
        try:
            self.report_content=self.generate_report(request.user, report_year)
        except Exception:
            return HttpResponseServerError("There was an error while generating the report")

        return super(DownloadReportView, self).get(request, *args, **kwargs)

Y el archivo urls.py para quien le pueda interesar:

#urls.py
from django.conf.urls import url
from .views import DownloadReportView

urlpatterns = [
        url( # Handle report downloads
                regex = r'^download_report/(?P<year>[0-9]{4})$',
                view  = DownloadReportView.as_view(),
                name  = 'download_user_report'),
]

Ver comentarios


Este es el primer artículo de una series en la que describo como usar Django en conjunción con DigitalOcean para crear tu propio proveedor de de servidores VPS. Algo que hace años hubiera sido impensable sin un equipo de administradores y programadores, y que hoy puede implementarse con unos pocos miles de lineas de código python.

Este artículo es una introducción al API de DigitalOcean y a su libreria de python, la cual usaré en futuros artículos para implementar el servicio.

API de DigitalOcean

El API de Digitalocean actualmente se encuentre en su segunda versión y es muy extenso, no solo permite crear, destruir, y manejar Droplets (nombre que usa para sus VPS), sino que permite controlar todos los aspectos de sus servicios, desde la gestión de DNS, hasta la creación backups, o gestión imágenes. Puede acceder a la documentación del API aquí pero por suerte ya existe una libreria para python llamada python-digitalocean que simplifica enormemente el proceso, crear un Droplet puede ser tan sencillo como:

import digitalocean
droplet = digitalocean.Droplet(token="digitalocean-personal-access-token",
                           name='servidor.midominio.com',
                           region='ams2', # Amsterdam
                           image='ubuntu-14-04-x64', # Ubuntu 14.04 x64
                           size_slug='512mb')
droplet.create()
droplet_id = droplet.id

El identificador devuelto durante la creación del Droplet luego puede ser usado para manejarlo, en el siguiente ejemplo se reinicia, apaga y después elimina:

import digitalocean

manager = digitalocean.Manager(token="digitalocean-personal-access-token")
droplet = manager.get_droplet(droplet_id)

droplet.reboot()
droplet.power_off()
droplet.destroy()

La librería también permite redirigir dominios gestionados desde la DNS de DigitalOcean, por ejemplo si tienes la dirección IP y el hostname de un Droplet puedes redirigir un subdominio:

import digitalocean
import tldextract

droplet_ip = "83.54.134.34"
droplet_hostname = "servidor.midominio.com"

# Crear subdominio usando el hostname del servidor
d_subdomain, d_domain, d_suffix = tldextract.extract(droplet_hostname)

domain = digitalocean.Domain(token="digitalocean-personal-access-token",
                            name=d_domain+"."+d_suffix)

result = domain.create_new_domain_record(type="A", name=d_subdomain, data=droplet_ip)

domain_record_id = result['domain_record']['id']

# Eliminar registro creado
record = digitalocean.Record(
            id=domain_record_id,
            domain_name=d_domain+"."+d_suffix,
            token="digitalocean-personal-access-token")

record.destroy()

Y con esto tenemos los fundamentos para manejar Droplets, pero antes de poder entregarlo al usuario es necesario configurarlo/customizarlo al gusto del usuario.

Metadata y Cloudinit

DigitalOcean incluye un API de Metadatos, que permite a los Droplets acceder a sus propios datos, suministrar datos de usuario durante su creación, y procesar esos datos usando CloudInit si es que contienen alguno de los formatos soportados. En el siguiente ejemplo se envía una cadena que contiene dos variables:

import digitalocean

data = 'USUARIO="admin"\nCLAVE="secreto"'

droplet = digitalocean.Droplet(token=settings.DIGITALOCEAN_SECRET_TOKEN,
                           name='servidor.midominio.com',
                           region='ams2', # Amsterdam
                           image='ubuntu-14-04-x64',
                           size_slug='512mb',
                           user_data=data)
droplet.create()
droplet_id = droplet.id

Desde el droplet se puede acceder a los datos suministrados:

$ curl -sS http://169.254.169.254/metadata/v1/user-data
USUARIO="admin"
CLAVE="secreto"

CloudInit como su nombre indica es una herramienta para inicializar servidores en la nube, la primera vez que arranca el servidor extrae los datos de configuración, y si contienen alguno de los formatos soportados los procesa. Uno de esos formatos es shell script, por ejemplo podemos enviar el siguiente script para crear un archivo de swap de 2GB:

#!/bin/bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
echo '/swapfile none swap sw 0 0' >>/etc/fstab

Cloud Config es otro formato muy versátil, que permite realizar la mayoría de las tareas más comunes de configuración de forma muy sencilla, y ademas proporciona funcionalidad extra con su sistema de módulos, en el siguiente ejemplo se añade los mismos 2GB de swap:

#cloud-config
swap:
    filename: /swapfile
    size: 2000000000

Por último el formato Include File consiste en una lista de URLS cuyo contenido es descargado y procesado usando las mismas reglas que si fueran los datos proporcionados con user-data, por ejemplo:

#include
https://myvpsservice.com/scripts/swap-2GB
https://myvpsservice.com/scripts/install-apache
https://myvpsservice.com/scripts/add-keys

Seguro que puedes ver el potencial de éste formato para esta aplicación. Si te interesa conocer el resto de los formatos y módulos disponibles te recomiendo que heches un vistazo a la documentación de cloud-init.

En la segunda parte de esta serie profundizare más en como implementar un sistema de configuración de VPS con Django.

Ver comentarios


Si vas a crear un formulario donde se pueda introducir un DNI/NIF no necesitas crear tu propio DNIField o programar un validador, ya existe en un paquete llamado django-localflavor que agrupa ese tipo de campos para los distintos países.

Por ejemplo para España incluye:

  • ESPhoneNumberField - Números de teléfonos fijos y móviles.
  • ESIdentityCardNumberField - Numero de identificación NIF/CIF/NIE.
  • ESCCCField - Código de cuenta de cliente en formato EEEE-OOOO-CC-AAAAAAAAAA
  • ESProvinceSelect - Selección de provincia.

Instalación

Como siempre pip es la mejor opción para instalar el paquete:

$ pip install django-localflavor

Luego añade 'localflavor' a INSTALLED_APPS en settings.conf:

INSTALLED_APPS = (
    # ...
    'localflavor',
)

Uso

Un ejemplo de como crear un formulario sencillo:

#forms.py
from django import forms
from localflavor.es.forms import ESIdentityCardNumberField, ESPhoneNumberField, ESProvinceField

class ClienteForm(forms.Form):
    nombre = forms.CharField(max_length=200)
    dni = ESIdentityCardNumberField(only_nif=True)
    telefono = ESPhoneNumberField()
    ...

Al campo dni se le ha añadido la opción only_nif=True para que sólo acepte NIF y NIE (Numero identificación extranjeros), sin ella también aceptaría códigos CIF.

Ver comentarios


Algunas veces es necesario inicializar la base de datos antes de poder usar una aplicación, si esta aplicación no va a reutilizarse no es ningún problema, en cambio si es algo que planeas usar en varios proyectos, es mucho mas práctico añadir los datos de inicialización en la misma aplicación y cargarlos usando loaddata. Imaginemos una aplicación para la gestión de los productos de una tienda, en la que tenemos los siguientes modelos:

#tienda/models.py
from django.db import models

class Categoria(models.Model):

    nombre = models.CharField(max_length=80)
    slug = models.SlugField(unique=True, db_index=True)
    descripcion = models.TextField(max_length=2000)


class Producto(models.Model):

    nombre = models.Charfield(max_length=200)
    descripcion models.TextField(max_length=2000)
    precio = models.DecimalField()
    categoria = models.ForeignKey(Categoria)

Django permite crear un directorio llamado fixtures dentro de la aplicación donde almacenar archivos de datos que luego pueden ser volcados a la base de datos para inicializarla.

Estos archivos pueden crearse usado tres formatos JSON, YAML, XML, yo creo que los dos primeros son la mejor opción, más fáciles de leer y modificar manualmente. Veamos un ejemplo para JSON:

[
{
    "model": "tienda.categoria",
    "fields": {
        "nombre": "Ordenadores portatiles",
        "slug": "portatiles",
        "descripcion": "Ordenadores portatiles de 13 a 17 pulgadas",
    },
    "pk": 100000
},
{
    "model": "tienda.categoria",
    "fields": {
        "nombre": "Tablets",
        "slug": "tablets",
        "descripcion": "Tablets android",
    },
    "pk": 100001
},
{
    "model": "tienda.producto",
    "fields": {
        "nombre": "ASUS MeMO Pad 10 ME103K 16GB",
        "precio": "120.50",
        "descripcion": "La ASUS MeMO Pad 10 se creó pensando en...",
        "categoria": 100001,
    }
}
]

y los mismos datos en YAML:

- model: tienda.categoria
pk: 100000
fields:
    nombre: "Ordenadores portatiles"
    slug: "portatiles"
    descripcion: "Ordenadores portatiles de 13 a 17 pulgadas"

-model: tienda.categoria
pk: 100001
fields:
    nombre: "Tablets"
    slug: "tablets"
    descripcion: "Tablets Android"

- model: tienda.producto
pk: 100000
fields:
    nombre: "ASUS MeMO Pad 10 ME103K 16GB"
    precio: "120.50"
    descripcion: "La ASUS MeMO Pad 10 se creó pensando en..."
    categoria: 100001

Si tienes el archivo de inicialización en la ruta tienda/fixtures/categorias.json, se puede cargar los datos con:

$ python manage.py loaddata categorias

Por último aunque es posible crear y editar manualmente los archivos, Django proporciona una herramienta para volcar la base de datos a un archivo usando cualquiera de los formatos soportado, con lo que se puede usar el interfaz Admin para crear los datos y luego volcarlos con:

$ python manage.py dumpdata tienda --indent 4 --format json --output tienda/fixtures/categorias.json

Y eso es todo por hoy.

Ver comentarios


He leído infinidad de tutoriales sobre PayPal REST API, desde los más técnicos a los más prácticos, y en todos ellos lo que he echado de menos es que profundizaran un poco más en como integrar paypal en una aplicación real. Este tutorial es la primera parte de una serie, que pretende llenar ese hueco, y esta pensado como un complemento a esos otros tutoriales, mostrando distintos ejemplos de uso de PayPal.

Antes de empezar recomiendo que te familiarizes con la biblioteca Paypal Python SDK, porque el propósito de éste tutorial no es explicar su funcionamiento.

La tienda

Para éste primer tutorial la aplicación que voy a usar como ejemplo es una tienda de libros, en concreto una tienda de ebooks. Su funcionamiento es muy sencillo, cada libro tiene un botón de compra que al ser pulsado redirige al cliente a la página de pago de paypal, cuando la transacción se finaliza, el libro es enviado a su dirección de correo.

El proyecto esta compuesto por dos "apps" principales, libros y pagos. La primera gestiona el contenido de la tienda, con cuatro modelos Autor, Genero, Editorial, y Libro. Siendo este último el modelo más importante.

# libros.models.py

class Libro(PublicadoMixin, ImagenMixin):
    """Novels are never displayed, and are used to link together
    all the editions, with the common information among them
    authors, genre..."""

    # Informacion basica del libro
    titulo = models.CharField(max_length=200, db_index=True)
    titulo_completo = models.CharField(max_length=300, blank=True)
    numero_paginas = models.PositiveIntegerField(default=0)
    resumen = models.TextField(max_length=2000, blank=True)
    fecha_publicacion = models.DateField(blank=True, null=True)
    idioma = LanguageField(default='es')

    # Informacion sobre edicion y editorial
    numero_edicion = models.PositiveSmallIntegerField(default=1)
    editorial = models.ForeignKey(Editorial, blank=True, null=True)

    # Algunos libros pueden tener multiples autores
    autores = models.ManyToManyField('Autor')

    # Generos del libro
    generos = models.ManyToManyField('Genero')

    # Ruta del ebook en los distintos formatos
    epub = models.FileField(upload_to='ebooks/epub/', blank=True)
    mobi = models.FileField(upload_to='ebooks/mobi/', blank=True)

    # ISBN del libro.
    isbn = ISBNField(blank=True)

    # Precio del libro
    precio = models.DecimalField(max_digits=6, decimal_places=2)

    class META:
        ordering = ['-fecha_publicacion']

    def get_absolute_url(self):
        return reverse('libro-detail', args=[str(self.id),])

    def __str__(self):
        return self.titulo

La segunda "app" pagos se encarga de generar y aceptar los pagos por PayPal, y tiene un único modelo que se usa para registrar la información de la transacción, en concepto de que libro se realizaron, y la información del cliente proporcionada por PayPal.

# pagos.models.py

from django.db import models
from decimal import Decimal
from libros.models import Libro

class PagoPaypalManager(models.Manager):
    def crear_pago(self, payment_id, libro):
        pago=self.create(libro=libro,
            payment_id=payment_id,
            precio=libro.precio)
        return pago

class PagoPaypal(models.Model):
    # Foreign Key hacia el libro de este pago
    libro = models.ForeignKey(Libro)

    # Identificador de paypal para este pago
    payment_id = models.CharField(max_length=64, db_index=True)

    # Id unico asignado por paypal a cada usuario no cambia aunque
    # la dirección de email lo haga.
    payer_id = models.CharField(max_length=128, blank=True, db_index=True)

    # Dirección de email del cliente proporcionada por paypal.
    payer_email = models.EmailField(blank=True)

    # Guardamos una copia del precio de libro, porque puede variar en el tiempo
    precio = models.DecimalField(max_digits=8, decimal_places=2,
                default = Decimal('0.00'))

    pagado = models.BooleanField(default=False)

    objects = PagoPaypalManager()

Por último para introducir los datos de libros, editoriales, y demás información se usa el interfaz de administración de Django.

Credenciales de Paypal

El primer paso para desarrollar una aplicación con PayPal es crear una cuenta, habilitar REST Api en Paypal Developer, y obtener las credenciales y cuentas de desarrollo. Después solo hay que añadirlas en el archivo settings.py

PAYPAL_MODE= "sandbox"
PAYPAL_CLIENT_ID  = "EBWKjlELKMYqRNQ6sYvFo64FtaRLRR5BdHEESmha49TM",
PAYPAL_CLIENT_SECRET = "EO422dn3gQLgDbuwqTjzrFgFtaRLRR5BdHEESmha49TM"

Paypal

En las páginas con la información de cada uno de los libros, se encuentran los respectivos botones de compra, con los cuales un cliente puede iniciar el proceso de pago a traves de PayPal.

Imagén de la pagina de compra de un libro

Veamos código del botón de compra en el template:

<a href="{% url 'pago-paypal' libro.pk %}" class="btn btn-info btn-lg pull-right">
    Comprar {{libro.precio}}€
</a>

El botón redirige al usuario a 'pago-paypal' con el identificador del libro como parámetro, ese alias corresponde con la vista pagos.views.PaypalView, donde se encuentra la lógica para iniciar el proceso de pago del libro.

# pagos.views.py

class PaypalView(RedirectView):

    permanent = False

    def _generar_lista_items(self, libro):
        """ """
        items = []
        items.append({
            "name":     str(libro),
            "sku":      str(libro.id),
            "price":    ('%.2f' % libro.precio),
            "currency": "EUR",
            "quantity": 1,
        })
        return items

    def _generar_peticion_pago_paypal(self, libro):
        """Crea el diccionario para genrar el pago paypal de libro"""
        peticion_pago = {
            "intent": "sale",
            "payer": {"payment_method": "paypal"},
            "redirect_urls": {
                "return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),
                "cancel_url": settings.SITE_URL},

            # Transaction -
            "transactions": [ {
                # ItemList
                "item_list":{
                    "items": self._generar_lista_items(libro)},

                # Amount
                "amount": {
                    "total": ('%.2f' % libro.precio),
                    "currency": 'EUR'},

                #Description
                "description": str(libro),}
            ]}

        return peticion_pago

    def _generar_pago_paypal(self, libro):
        """Genera un pago de paypal para libro"""
        paypalrestsdk.configure({
            "mode":         settings.PAYPAL_MODE,
            "client_id":    settings.PAYPAL_CLIENT_ID,
            "client_secret":settings.PAYPAL_CLIENT_SECRET,})

        pago_paypal = paypalrestsdk.Payment(self._generar_peticion_pago_paypal(libro))

        if pago_paypal.create():
            for link in pago_paypal.links:
                if link.method == "REDIRECT":
                    url_pago = link.href
        else:
            raise Exception(pago.error)

        return url_pago, pago_paypal

    def get_redirect_url(self, *args, **kwargs):
        """Extrae el libro que el usuario quiere comprar, genera un pago de
        paypal por el precio del libro, y devuelve la direccion de pago que
        paypal generó"""
        libro = get_object_or_404(Libro, pk=int(kwargs['libro_pk']))
        url_pago, pago_paypal = self._generar_pago_paypal(libro)

        # Se añade el identificador del pago a la sesion para que PaypalExecuteView
        # pueda identificar al ususuario posteriorment
        self.request.session['payment_id'] = pago_paypal.id

        # Por ultimo salvar la informacion del pago para poder determinar que
        # libro le corresponde, al terminar la transaccion.
        PagoPaypal.objects.crear_pago(pago_paypal.id, libro)

        return url_pago

Esta vista se encarga de crear un pago para el libro con el identificador proporcionado, y redirigir al usuario a la dirección proporcionada por PayPal para completar la transacción.

Para implementar esta vista he usado una Class Based View que hereda de RedirectView, y he redefinido el método get_redirect_url, para que obtenga el modelo del libro seleccionado, genere un pago de Paypal con los datos de ese libro, y por último devuelva la dirección a la que el usuario debe ser redirigido para finalizar el pago.

En la página de PayPal aparecerá el título del libro, su número de artículo, y el precio.

Imagen de la pagina de paypal para el pago de un libro

Una vez el usuario termina el pago es redirigido a la url de retorno, en la que se debe "ejecutar" el pago para finalizar la transacción, esta dirección de retorno se especificó en el método _generar_peticion_pago_paypal(), de la clase PaypalView.

"return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),

En esta url se encuentra la vista PaypalExecuteView que acepta el pago, guarda la información del cliente, marca el registro como pagado, y envía el email que contiene el libro a la dirección de email del usuario proporcionada por PayPal.

# pagos.views.py

class PaypalExecuteView(TemplateView):

    template_name = 'pagos/paypal_exito.html'

    def _enviar_ebook_email(self, registro_pago):
        """Enviar Email con el libro al cliente """
        #TODO: adjuntar los archivos del modelo libro
        libro = registro_pago.libro
        mensaje = "Gracias por su compra de %s" % libro.titulo
        send_mail('TiendaEbook', mensaje,
            from_email = settings.DEFAULT_FROM_EMAIL,
            recipient_list=[registro_pago.payer_email,])

    def _aceptar_pago_paypal(self, payment_id, payer_id):
        """Aceptar el pago del cliente, actualiza el registro con los datos
        del cliente proporcionados por paypal"""
        registro_pago = get_object_or_404(PagoPaypal, payment_id=payment_id)
        pago_paypal = paypalrestsdk.Payment.find(payment_id)
        if pago_paypal.execute({'payer_id': payer_id}):
            registro_pago.pagado = True
            registro_pago.payer_id = payer_id
            registro_pago.payer_email = pago_paypal.payer['payer_info']['email']
            registro_pago.save()else:
        else:
            raise HttpResponseBadRequest

        return registro_pago

    def get(self, request, *args, **kwargs):
        """Extraer identificacion de paypal del cliente, la id del pago,
        aceptar el pago, y enviar el email."""
        context = self.get_context_data(**kwargs)
        try:
            payer_id = request.GET['PayerID']
            payment_id = request.session['payment_id']
        except Exception:
            raise HttpResponseBadRequest

        registro_pago = self._aceptar_pago_paypal(payment_id, payer_id)
        self._enviar_ebook_email(registro_pago)

        return self.render_to_response(context)

Y eso es todo, el código está disponible en Github.

En la próxima entrega de este tutorial mejoraré algunas de las deficiencias de la aplicación, añadiendo un carro de la compra para simplificar la compra de varios libros, y un sistema de descarga para no tener que enviar el libro por correo.

Ver comentarios