sábado, 15 de septiembre de 2018

Refrescando Django (1)

A lo tonto, Django ya tiene unos cuantos añitos. Van por la versión 2.1 en el momento de escribir estas líneas (septiembre de 2018). Refresquemos un poco Django.

Vamos a hacer una web que sirva para guardar marcadores, parecido a lo que hacía Del.icio.us (qué tiempos...)

Requisitos


- La app debe presentar una pantalla de login y ofrecer al usuario la posibilidad de registrarse. El email será la clave para identificar al usuario.

- Una vez autentificado, la aplicación listará los marcadores que el usuario haya ido guardando, así como las etiquetas que el usuario haya asignado al marcador.

- En la pantalla de listado, se puede filtrar por etiquetas.

- Para añadir un marcador, se presenta un formulario al usuario. El sistema debe tratar de hacer una vista previa del marcador (extraer el título, algún resumen, foto representativa...)
El usuario puede asignar etiquetas a cada marcador. Las etiquetas se pueden sugerir autocompletando a partir de las que ya hay.

Preparación del proyecto


Empecemos instalando Django según las instrucciones de la documentación. Una vez instalado, inicializamos el proyecto.


django-admin startproject bookmarks

Lo primero que vamos a hacer es definir los diferentes entidades y sus correspondientes modelos.

Tag: representa una etiqueta para uno o varios marcadores. Sus campos serían, aparte del id, que ya lo autogenera Django, name y description. Sólo es obligatorio name.

Bookmark: representa un marcador. Sus campos serían title, description, url e image. Son obligatorios title y url.


Un marcador puede tener muchas etiquetas, a su vez, una etiqueta puede estar presente en muchos marcadores. Esto nos sugiere una relación ManyToMany. Solo es necesario hacer la asociación entre marcadores y etiquetas en un punto, según nos indican en la documentación. Lo más razonable parece hacer la asociación en la entidad Bookmark, puesto que es la entidad sobre la que todo gira.

User: representa un usuario. Vamos a reutilizar todo el sistema de gestión de usuarios de Django.

Una vez definidos las vistas que queremos y los modelos que vamos a necesitar, empezamos el desarrollo.

Dentro de un proyecto Django podemos tener varias apps, en este caso, para un proyecto tan pequeño, probablemente sólo necesitaremos una app. El proyecto es un contenedor, lo que "hace algo" son las diferentes apps que hay en un proyecto. Llamemos a la app bookmarks.

python3 manage.py startapp bookmarks 

CommandError: 'bookmarks' conflicts with the name of an existing Python module and cannot be used as an app name. Please try another name

No podemos, lógicamente. Ya hay un módulo llamado bookmarks (el correspondiente al proyecto). Tenemos que pensar un nombre para la aplicación que no entre en conflicto y que describa un poco la naturaleza de la función de esta aplicación. Llamémosla manager, ya que esta app será el gestor de marcadores.

En este punto tenemos en el proyecto dos módulos, uno llamado "bookmarks", que corresponde al proyecto en su globalidad, con sus ajustes comunes, rutado... y otro llamado "manager", que corresponde a la aplicación (para este proyecto probablemente sea la única app que lleguemos a hacer).



La carpeta de "fuera", la que contiene el proyecto, nos da igual como se llame, de hecho la vamos a renombrar.



Mejor así. Comprobemos que funciona:

BookmarksManager david$ python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

September 15, 2018 - 11:10:40
Django version 2.1.1, using settings 'bookmarks.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[15/Sep/2018 11:10:49] "GET / HTTP/1.1" 200 16348
[15/Sep/2018 11:10:49] "GET /static/admin/css/fonts.css HTTP/1.1" 200 423
[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 200 80304
[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Bold-webfont.woff HTTP/1.1" 200 82564

[15/Sep/2018 11:10:49] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 200 81348

Ya hemos terminado de montar la estructura del proyecto.

Definición de modelos

Vamos con los modelos del ámbito de la aplicación: Bookmark y Tag




Empezamos desde el principio utilizando la riqueza de tipos que Django nos aporta. Por ejemplo, un URLField no es más que un campo de texto con un validador adicional que nos comprueba si la URL que pretendemos guardar está bien formada.

Al utilizar un campo ImageField nos salta un error:

ERRORS:
manager.Bookmark.image: (fields.E210) Cannot use ImageField because Pillow is not installed.

HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "pip install Pillow".


Nos recuerda que para tratar imágenes, debemos instalar Pillow.

Instalamos la aplicación editando el settings.py del proyecto:



Nos quedan definir las relaciones entre Bookmark y Tag y los usuarios del sistema. Para ello, vamos a heredar nuestra entidad BookmarkUser de la clase AbstractUser.



Ejecutamos los comandos makemigrations y migrate y ya tenemos la base de datos preparada.



Preparando el admin

Para que en la aplicación admin de Django aparezcan estas entidades debemos registrarlas en admin.py de la aplicación.



También vamos a crear un usuario administrativo para la interfaz administrativa. Este usuario no es un usuario de la aplicación de marcadores, es un usuario interno de Django. Lo llamaremos "root", en analogía a UNIX.



Si accedemos a la interfaz admin con este usuario, ya podemos empezar a cacharrear con las entidades.




Hasta ahora no hemos hecho nada diferente de lo que el tutorial de Django nos enseña. Vamos a empezar a desarrollar en este punto nuestras funcionalidades.







martes, 4 de septiembre de 2018

Editando fecha EXIF de muchas fotos en bloque

No sé si todo el mundo es tan manazas con la fotografía como yo, pero me suele pasar a menudo que tengo fotos sin metadatos de fecha (las típicas que te pasan por Whatsapp, o capturas de pantalla, escaneados,...)

El siguiente script (en Python) recorre recursivamente un directorio y para cada foto que se encuentra, busca si tiene un dato de fecha de creación en los metadatos EXIF.

Si no lo tiene, y el formato del nombre del fichero es del tipo *yyyymmdd*, añade una fecha en los datos EXIF de la foto basada en el nombre del fichero.

#!/usr/bin/env python3

import sys, os, re, piexif


if len(sys.argv) < 2:
    print ("Usage:", __file__ , "DIRECTORY")
    sys.exit(1)


date_tag = piexif.ExifIFD.DateTimeOriginal
exif_key = 'Exif'
regexp = re.compile(r'\b(20(\d{6}))\b') 

if os.path.isdir(sys.argv[1]):
    base_dir = os.path.abspath(sys.argv[1])
    #print ("Searching photos in ", base_dir)
    for root, dirs, files in os.walk(base_dir):
        for f in files:
            full_path = os.path.join(root, f)
            filename, ext = os.path.splitext(full_path)
            if ext.lower() in ('.jpg', '.jpeg'):
                try:
                    exif_data = piexif.load(full_path) 
                    if not date_tag in exif_data[exif_key]:
                        # print ("No date-time data for", full_path)
                        match = regexp.search(filename)
                        if match:
                            new_date = "%s:%s:%s 00:00:00" % (match[0][:4], match[0][4:6], match[0][6:])
                            #print ("The file", full_path, "seems to have a date-time", match[0], "=>", new_date)
                            exif_data[exif_key][date_tag] = new_date
                            exif_bytes = piexif.dump(exif_data)
                            try:
                                piexif.insert(exif_bytes, full_path)
                                print ('"'+full_path+'" modified with date-time', new_date)
                            except Exception as e2:
                                print (e2)                      

                except Exception as e1:
                    pass
                    #print ("Error reading", full_path, "Corrupted file?")


else:
    print (sys.argv[1], "is not a valid directory.")
    sys.exit(1)

Es especialmente interesante el módulo piexif, no lo conocía. Funciona un poco a bajo nivel, pero para hacer estas manipulaciones rápidas va muy bien.