miércoles, 18 de mayo de 2016

La pequeña y estúpida aplicación fichero_app

Tomo un momento para hablar de una pequeña aplicación que vamos a integrar con pyramid. Integrar quizá no: Vamos a publicar la aplicación con pyramid.

Como se verá, la aplicación es totalmente funcional por si misma. Bueno, que si se quiere ver es una pequeña biblioteca, casi inútil desde un punto de vista práctico. Vamos, que ni siquiera esta capturando excepciones. Sin embargo, yo voy a suponer que esta aplicación es la joya de la corona, que dentro de cada función encierra grandes operaciones de tratamiento de ficheros que me gustaría que estuvieran disponibles para otras aplicaciones, sobre todo, para un cliente web que funcione con AngularJS

Con ustedes, la aplicación ficheros.py:
# coding: utf-8
 
import os
import json
 
class Usuarios():
    """ Manejo de usuarios por medio de ficheros json
    """

    def __init__(self):
        self.direccion = os.getcwd() + '/archivos/'
     
    def listado(self):
        """ Retorna lista con usuarios registrados
        Returns:
            listado: (list) Lista de usuarios registrados
        """
        try:
            lista = os.listdir(self.direccion)
            usuarios = [usuario.split('.')[0] for usuario in lista]
        except IOError as e:
            return {'error': e.args}
        return usuarios
     
    def detalle(self, usuario):
        """ Retorna diccionario con información de usuario
        Args:
            usuario: (str) usuario del que se desean detalles
        Returns:
            dict: Información del usuario
        """
        try: 
            fichero = open(self.direccion + '/' + usuario + '.json') 
            contenido = json.load(fichero)
        except ValueError as e:
            return {'error': e.message}
        except IOError as e:
            return {'error': e.args[1]}
        return contenido
    
    def creacion(self, usuario, datos):
        """ Retorna diccionario de datos con los que se ha creado al usuario
        Args:
            usuario: (str) nombre del usuario a crear
            datos: (dict) datos de la información del usuario
        Returns:
            dict: Datos con los que se ha creado al usuario
        """
        try:
            fichero = open(self.direccion + '/' + usuario + '.json', 'w')
            contenido = json.dump(datos, fichero)
        except ValueError as e:
            return {'error': e.message}
        except IOError as e:
            return {'error': e.args[1]}
        return datos
     
    def modificacion(self, usuario, datos):
        """ Retorna diccionario con los datos modificados del usuario
        Args:
            usuario: (str) nombre del usuario a modificar
            datos: (dict) datos a modificar del usuario
        Returns:
            dict: Datos con los que se ha creado al usuario
        """
        try:
            fichero = open(self.direccion + '/' + usuario + '.json', 'w+')
            contenido = json.dump(datos, fichero)
        except ValueError as e:
            return {'error': e.message}
        except IOError as e:
            return {'error': e.args[1]}
        return datos
    
    def borrado(self, usuario):
        """ Retorna nombre de usuario borrado
        Args: 
            usuario: (str) nombre del usuario a borrar
        Returns:
            usuario: (str) nombre del usuario borrado
        """
        try:
            fichero =  self.direccion + '/' + usuario + '.json'
            os.remove(fichero)
        except OSError as e:
            return {'error': e.args[1]}
        return usuario

Como puede intuirse del código, tenemos cinco operaciones definidas.
  • Listar los ficheros que se encuentran en ./archivos/ (listar())
  • Ver en detalle un fichero .json que se encuentra dentro de ./archivos/ (detalle()), y que básicamente consiste en retornar el contenido json del fichero traducido a un tipo python válido.
  • Crear un nuevo fichero json dentro de ./archivos/ (creacion())
  • Modificar un fichero json, reemplazando todo el contenido actual con el contenido que tiene las modificaciones ya aplicadas. Parece ser que la precisión en las definiciones es algo importante
  • Borrar un fichero json dentro de ./archivos/ 
Que sí, que me estoy inspirando en CRUD para hacer esto. Es una forma muy cómoda para pensar una aplicación.

Para empezar a usar la aplicación, bastaría con crear el directorio ./archivos/ junto con algunos ficheros de prueba. Pues que lo que viene a decir la documentación es que cada método de la aplicación recibe, en su mayoría, un usuario y un diccionario datos. Y en su mayoría retorna otro diccionario
$ mkdir ./archivos
$ echo '{"palabras": ["ambiente", "publico"], "nombre": "Alexander", "apellido": "Ort\u00edz"}' > ./archivos/alortiz.json
$ echo '{"palabras": ["contenido", "fichero"], "nombre": "Karen", "apellido": "Pe\u00f1ate"}' > archivos/kpenate.json
$ echo '{"palabras": ["posix", "permisos"], "nombre": "Olga", "apellido": "Pineda"}' > archivos/opineda.json
Por último, convertiremos esta diminuta aplicación en una paquete python válido. Tomando en cuenta lo sencillo que es en python: Creamos un directorio, ubicamos todos los ficheros creados dentro y creamos un ficheros __init__.py
$ mkdir ./fichero_app
$ mv archivos/ ficheros.py fichero_app/
$ touch fichero_app/__init__.py
Un script python que tenga el mismo directorio raíz que fichero_app (En este momento, ./ambiente/aplicacion/aplicacion/fichero_app), puede importar la clase Fichero de la siguiente forma:
from fichero_app.ficheros import Usuarios
La cuestión sobre el directorio a usar es un poco espinosa, producto de no parametrizarla como debiera. El truco es que la búsqueda del directorio ficheros ocurrirá en la base desde donde se ejecute la aplicación que llama a nuestra pequeña aplicación como módulo.

El siguiente paso va en la línea de la guía con Pyramid: De como publicamos con pyramid una aplicación python

lunes, 16 de mayo de 2016

Primeros pasos con Pyramid

Que quiero hacer con python una una aplicación web simple y precisa, que no consuma de una base de datos y que tampoco produzca HTML sino contenido JSON (Con lo puedo usar AngularJS para el respectivo cliente web). Así que decidí usar Pyramid, que tenía algunas de las cosas que a primera vista necesito (O más bien que creo necesitar).

La siguiente guía está en constante desarrollo. Básicamente estoy haciendo los primeros pasos después de revisar la guía oficial más completa
La recomendación oficial es crear un entorno virtual en el cual instalar los paquetes
$ virtualenv ambiente
$ cd ambiente/
Activamos el entorno, lo que no significa más que configurar unas cuantas variables del sistema para que usen el ambiente virtual, lo que puede verificarse por curiosidad al revisar el path correspondiente a pip
$ source bin/activate
$ which pip
/home/vtacius/public_html/ambiente/bin/pip
Lo siguiente será instalar pyramid, y para empezar bien y rápido, usaremos pcreate para el scaffolding (Término que no es del todo correcto en estas condiciones) de la aplicación
$ pip install pyramid
$ pcreate -s starter aplicacion
$ cd aplicacion/
Este nivel de la aplicación (./ambiente/aplicacion/) aprovechamos para cambiar el fichero setup.py para retirar en el arreglo requires a pyramid_chameleon porque es el paquete para producir HTML por medio de plantillas, y no pienso usarlo. Agregamos nose y webtest porque no se han incluido en el proyecto
import os

from setuptools import setup, find_packages

here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'README.txt')) as f:
    README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f:
    CHANGES = f.read()

requires = [
    'pyramid',
    'pyramid_debugtoolbar',
    'waitress',
    'nose',
    'webtest'
    ]

(...)
Ahora instalamos todos los paquetes requeridos por la aplicación en nuestro entorno virtual:
$ python setup.py develop
A este nivel (./ambiente/aplicacion/) hay que volver cuando se quiere realizar los test y correr waitress.

Ahora entramos dentro de nuestra aplicación propiamente, llamada aplicacion, y acomodamos según nuestras necesidades: eliminamos el directorio template y static porque no pienso usarlo, y volvemos un paquete a views porque planeo crecer con la aplicación aunque sea un poquito:
$ cd aplicacion/
$ rm -rf views.py templates/ static
$ mkdir views
$ touch views/__init__.py
Del fichero __init__.py eliminamos las referencias a pyramid_chameleon, y la configuración de contenido estático que nos es innecesaria:
    config.include('pyramid_chameleon')
    config.add_static_view('static', 'static', cache_max_age=3600)
Para este primer ejercicio, basta con crear un fichero en views/ de cualquier nombre (Sí, cualquier nombre, la magia de scan) de la siguiente forma:
# coding: utf-8
from pyramid.view import view_config

@view_config(route_name='home', renderer='json')
def get_actividad_list(request):
    return {'home': '¡Hola Mundo!'}
Volvemos un directorio atrás y corremos la aplicación:
$ pserve development.ini --reload
Ahora lo probamos, desde consola porque somos gente hardcore que odia a los navegadores (¡Muerte a los navegadores!)
$ curl -i -X GET http://localhost:6543
HTTP/1.1 200 OK
Content-Length: 29
Content-Type: application/json; charset=UTF-8
Date: Tue, 17 May 2016 04:11:18 GMT
Server: waitress


{"home": "\u00a1Hola Mundo!"}
Preparé una especie de test que sale más extenso que el código que se quiere testar, tendrá que valer para primer esfuerzo. El punto más discutible es el de la fixture, sin embargo la idea debería estar en ese sentido. El contenido de tests.py queda de la siguiente forma:
# coding: utf-8
from unittest import TestCase

from pyramid import testing

class ViewTests(TestCase):
    def setUp(self):
        self.config = testing.setUp()
        # Atentos a nuestro intento de fixture
        self.res_get_actividad_list = {'home': '¡Hola Mundo!'}

    def tearDown(self):
        testing.tearDown()

    def test_get_actividad_list(self):
        # Hacemos las importaciones acá en lugar de globalmente para que el test sea más sincero
        from .views.actividades import get_actividad_list
        peticion = testing.DummyRequest()
        respuesta = get_actividad_list(peticion)
        self.assertEqual(respuesta['home'], self.res_get_actividad_list['home'])

class ViewFunctionalTest(TestCase):
    def setUp(self):
        # Este método debería considerarse un __init__ más apropiado en la cuestión de testeo
        # Hacemos las importaciones acá en lugar de globalmente para que el test sea más sincero
        from aplicacion import main
        app = main({})
        from webtest import TestApp
      
        self.testapp = TestApp(app)

        # Atentos a nuestro intento de fixture
        self.res_get_actividad_list = {'home': u'¡Hola Mundo!'}
  
    def test_get_actividad_list(self):
        respuesta = self.testapp.get('/', status=200, xhr=True)
        # Es discutible que haga esto, supongo que me aseguro que no vaya a cambiar la respuesta por accidente
        self.assertEqual('application/json', respuesta.content_type) 
        # Trato de usar la fixture para verificar contenido ya conocido
        proposicion = self.res_get_actividad_list['home']
        self.assertEqual(respuesta.json_body['home'], proposicion)

Ahora corremos el test en el directorio raíz de la aplicación
(ambiente)vtacius@ilaria:~/public_html/ambiente/aplicacion> nosetests
..
Name                              Stmts   Miss  Cover   Missing
---------------------------------------------------------------
aplicacion.py                         8      0   100%   
aplicacion/tests.py                  25      0   100%   
aplicacion/views.py                   0      0   100%   
aplicacion/views/actividades.py       3      0   100%   
---------------------------------------------------------------
TOTAL                                36      0   100%   
----------------------------------------------------------------------
Ran 2 tests in 1.123s

OK
Claro, los test automatizados son realmente algo increíble. Apenas pueden reemplazar al hecho de usar curl desde consola, pero todo lo compensan con el hecho de ser automatizados.
El siguiente paso es revisar la aplicación que vamos a publicar por medio de pyramid: fichero_app

miércoles, 23 de diciembre de 2015

Configurando Django para entrar en producción

En realidad estoy probando esta configuración para verificar las posibilidades de Django para entrar en producción. Pues que voy a usarlo de esta forma para desarrollo, aunque el servidor que trae para ese caso pueda ser suficiente. En todo caso vamos:
Estoy tomando en consideración:
  • La instalación de Debian Jessie es la mínima necesaria para que el sistema funcione
  • El usuario mafi es un usuario sin privilegios administrativos. Ni siquiera tiene permisos especiales mediante sudo
  • Lo anterior significa que el proceso uwsgi corre con los permisos y limitaciones de un usuario corriente. Que andar corriendo servicios como root es del diablo
Preparación del sistema base:
Como usuario root:
Instalamos paquetes necesarios
aptitude install python-pip
virtualenv nginx python-dev gcc
Creamos el directorio sobre el cual vamos a crear el entorno con virtualenv
mkdir /var/www/yacto
usermod -G -mafi www-data
chown mafi:www-data /var/www/yacto
Como el usuario sin privilegios (mafi en mi caso) Ahora, como el usuario corriente con el que vamos a trabajar
cd /var/www
virtualenv yacto/
cd yacto/
source bin/activate
Notará que hubo un cambio en la shell, y eso es bueno, de la siguiente forma:
mafi@dev:/var/www/yacto$
(yacto)mafi@dev:/var/www/yacto$
Ahora podemos instalar los paquetes en el entorno virtual
pip install Django uwsgi psycopg2
Inicializamos el proyecto
django-admin startproject yacto
Como usuario postgres (Incluso, en otro servidor dedicado para tal fin)
Si es que acaso es tu primera vez, basta con hacer lo siguiente
createuser -DRSP yacto
Ingrese la contraseña para el nuevo rol:
Ingrésela nuevamente:
createdb -O yacto yacto
Como el usuario sin privilegios (mafi en mi caso)
La configuración por defecto para un proyecto con Django se almacena en yacto/settings.py, así que lo configuramos de la siguiente forma:

Para configurar el uso de la base de datos anteriormente creada, modificamos el diccionario DATABASES de la siguiente forma:
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'yactividades_2',
        'USER': 'yactividades_2',
        'PASSWORD': 'yactividades_2',
        'HOST': '192.168.2.3',
        'POST': '5432',
        'client_encoding': 'UTF8',
        'default_transaction_isolation': 'read committed',
        'timezone': 'UTC'
    }
}
Lo de 'client_encoding', 'default_transaction_isolation' y 'timezone' lo explican acá: Optimizing PostgreSQL’s configuration

Hacia el final del mencionado fichero, encontramos la sección "Static files". Al final de ella, agregamos la constante STATIC_ROOT
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

Como consejo adicional, nunca esta de más configurar "Internationalization", sección que esta precisamente arriba de "Static Files"
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/

LANGUAGE_CODE = 'es-SV'

TIME_ZONE = 'America/El_Salvador'

USE_I18N = True

USE_L10N = True

USE_TZ = True
Empezamos a crear los ficheros de configuración:

El directorio referenciado en  para almacenar contenido estático:
mkdir static
aplicacion=$(basename $PWD)
touch $aplicacion.log
Creamos el fichero $aplicacion\_nginx.conf para configuración del sitio web con el siguiente contenido. Las opciones son las básicas funcionales, y basta con cambiar yacto por el nombre de su aplicación, aunque
# $aplicacion\_nginx.conf
# Conexión que Nginx hace con Django
upstream yacto {
    server unix:///var/www/yacto/yacto/yacto.sock;
}

# Configuración del servidor
server {
    # Puerto para servir el sitio
    listen     80;
    # Sustituir por IP o FQDN donde se quiera publicar tal sitio
    server_name yacto.salud.gob.sv;
    charset     utf-8;

    # max upload size
    client_max_body_size 75M;

    # Ficheros estáticos del proyecto, referenciado en STATIC_ROOT del fichero settings.py del proyecto
    location /static {
        alias /var/www/yacto/yacto/static/;
    }

    # Finally, send all non-media requests to the Django server.
    location / {
        uwsgi_pass yacto;
        #include /var/www/yacto/yacto/uwsgi_params;
        include /etc/nginx/uwsgi_params;
    }

}
Ahora creamos el fichero yacto_uwsgi.ini para configurar varias opciones a tomar por parte de uwsgi, las cuales bien pueden ser configuradas al arrancar la aplicación, pero respecto a configuraciones, siempres será más ordenado un archivo de configuración (Obvio, supongo), que guardar en alguna parte las opciones que un comando ha de usar.
# $aplicacion\_uwsgi.ini file
[uwsgi]

# Django-related settings
# the base directory (full path)
chdir           = /var/www/yacto/yacto
# Django's wsgi file
module          = yacto.wsgi
# the virtualenv (full path)
home            = /var/www/yacto

# process-related settings
# master
master          = true
# maximum number of worker processes
processes       = 10
# the socket (use the full path to be safe
socket          = yacto.sock
# Recordar que en Debian los ficheros se crean 755, lo cual hace necesario que esta opción se use, so riesgo de errores relacionados a permisos
chmod-socket    = 660
# clear environment on exit
vacuum          = true
# El proceso corre como demonio y loguea en yacto.log
# Lo mejor es usarlo sólo en conjunto a emperor, es un poco engorroso de usar de lo contrario
# daemonize     = yacto.log

Teniendo todo esto listo, preparamos a Django para irse en línea:
python manage.py makemigrations
No changes detected
python manage.py migrate
Operations to perform:
  Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying sessions.0001_initial... OK

python manage.py createsuperuser
Username (leave blank to use 'mafi'):
Email address: mafi@yacto.com
Password:
Password (again):
Superuser created successfully.

python manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings:

    /var/www/yacto/yacto/static

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes

Copying '/var/www/yacto/local/lib/python2.7/site-packages/django/contrib/admin/static/admin/fonts/LICENSE.txt'
Copying '/var/www/yacto/local/lib/python2.7/site-packages/django/contrib/admin/static/admin/fonts/README.txt'
[... Contenido suprimido ...]
Copying '/var/www/yacto/local/lib/python2.7/site-packages/django/contrib/admin/static/admin/js/vendor/jquery/jquery.min.js'
Copying '/var/www/yacto/local/lib/python2.7/site-packages/django/contrib/admin/static/admin/js/vendor/jquery/LICENSE-JQUERY.txt'

56 static files copied to '/var/www/yacto/yacto/static'.

Y luego nos volvemos root de nuevo y hacemos lo siguiente para "declarar" el sitio en nginx
ln -s /var/www/yacto/yacto/yacto_nginx.conf /etc/nginx/sites-enabled/
systemctl status nginx.service

Fuentes
Pues que nada, esto es la versión más resumida de Setting up Django and your web server with uWSGI and nginx. La referida guía se detiene en bastante detalles que explican bastante el funcionamiento, incluso esta haciendo pruebas continuamente.

Tiene una ligera influencia de Setup Django on Debian 8

Y obviamente las fuentes oficiales: How to install DjangoManaging static files (e.g. images, JavaScript,CSS)

lunes, 30 de noviembre de 2015

Instalando Oysttyer (¡Cliente de twitter desde la terminal!)

Siempre con la intención de no evitar salir de nuestra zona de confort (Algunos le llaman "flujo de trabajo", pero no estoy del todo seguro), resulta que hace tiempo venía usando ttytter como cliente de twitter a usar desde consola.
Tenía varias razones para ello: Usa menos recursos, es bastante sencillo de usar (De hecho puede llegar a automatizarse bastante bien). Y tus compañeros de trabajo creen que estás a punto de compilar algún driver exótico.

Pues resulta que esta muerto desde hace años. La versión 2.1.0 data de Diciembre de 2012. La última vez que lo instalé, el año pasado, pasé por alto el banner a la entrada del sitio, supongo.

La cuestión es que existe un fork oficial llamado oysttyer que cuenta con la bendición del creador de ttytter. Instalar el fork es tan fácil como lo era instalar al padre.

Es posible clonar el repo desde git en el repositorio que puse arriba. O pueden bajar archivo comprimido desde la "página oficial" del proyecto.

Oysttyer, al igual que ttyter, consta básicamente de un sólo fichero perl. Yo lo tiré a /usr/bin, no quisé complicarme demasiado con esas cosas:

Como root:
wget https://github.com/oysttyer/oysttyer/tarball/master -O master.tgz

tar -xzvf master.tgz

cd oysttyer-oysttyer-c6ff39e/

cp oysttyer-oysttyer-*/oysttyer.pl /usr/bin/oystter

Ahora, cada usuario del sistema puede usar oystter, basta con que acceda desde consola para configurarlo de la siguiente manera:
Copiamos la URL y la pegamos EN UNA LÍNEA en el navegador


Autorizamos la aplicación
Eso nos devuelve el pin de autorización por parte de Twitter
Retornamos a la consola y pegamos el pin
La próxima vez que abrimos la aplicación, aparece un pato

domingo, 25 de octubre de 2015

Compilando xscreensaver

Con mi nuevo equipo, una HP ENVY Notebook 15-k212la, resulta que casi todo funcionó a la primera, incluso la tarjeta inalámbrica, que suele ser un verdadero problema para algunos.

Sin embargo, los controles para el brillo de pantalla no estaban funcionando. No es especialmente un problema, pero era sumamente incómoda la situación. Claro que es posible configurar estos valores por consola, pero una laptop es una buena ocasión para introducir a usuarios comunes en el mundo del software libre.

Incluso tomé las recomendaciones de instalar Bumblebee en OpenSuSE, que antes ya lo había probado con Fedora, pero parece que al final ni siquiera funciona como se supone y seguía sin solucionar el problema.

Así que al volver sobre mis pasos, me encuentro con que la respuesta era más sencilla de lo que había pensado: Intel graphics de Arch Wiki y [SOLVED] Backlight control not working in X/KDE me estaban gritando que la solución era tan sencilla como realizar el procedimiento que se estipula en [SOLVED] Brightness problem in Ubuntu 12.04 Precise Pangolin.

Nota: Una tontería. Configurada de esta forma, en
/sys/class/backlight/
sólo queda el directorio que corresponde al vendor. Así que es el valor en
/sys/class/backlight/intel_backlight/actual_brightness
el que cambia con las teclas para control de brillo

Además, parece que las entradas en el registro del sistema respecto a
[ 2273.719866] atkbd serio0: Unknown key pressed (translated set 2, code 0xab on isa0060/serio0).
[ 2273.719870] atkbd serio0: Use 'setkeycodes e02b ' to make it known.
[ 2273.773690] atkbd serio0: Unknown key released (translated set 2, code 0xab on isa0060/serio0).
[ 2273.773694] atkbd serio0: Use 'setkeycodes e02b ' to make it known.

eran sólo una pista falsa.

Así que en Fedora, bastará con agregar acpi_backlight=vendor" a la línea GRUB_CMDLINE_LINUX del fichero /etc/default/grub:
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="rd.lvm.lv=fedora_ilaria/root rd.lvm.lv=fedora_ilaria/swap rhgb quiet acpi_backlight=vendor"
GRUB_DISABLE_RECOVERY="true"
Ahora ejecutamos para que vuelva a crear el fichero de configuración de grub (En este caso, para un equipo que usa UEFI) con el siguiente comando:
grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg
Y al reiniciar el equipo, las teclas correspondientes en el teclado será capaces de configurar el brillo de pantalla

lunes, 19 de octubre de 2015

Primeros pasos con Mustache

Mustache es un sistema de plantillas sin lógica, (Traducción literal de la descripción que hacen de si mismos en el sitio web), lo que significa que a diferencia de otros sistemas de plantillas para JavaScript este no define estructura de control, sino que trabaja definiendo etiquetas.

Es un gran adelanto usar un sistema de plantillas. Separar la "lógica de negocios" de la presentación siempre es un gusto, pese a los inconvenientes que pueda presentar en cuánto a rendimiento. Por otra parte, no es díficil aprender a usar Mustache, y su sencillez su no es problema para las capacidades que presenta:

Su uso puede resumirse de la siguiente forma:

  • Se crea una plantilla a usar sobre como han de tratarse los datos, tomando como referencia un objeto JSON. En mi caso, mi JSON es un array cuyos datos son los que necesito mostrar:

<tbody id="respuesta">
    <script id="respuesta-template" type="text/x-custom-template">
        {% verbatim %}
            {{#datos}}
                {{#.}}
                    <tr id="{{uid}}"><td>
                        <p class="col-md-12"><b>{{ cn }}</b></p>
                        <p class="col-md-12 col-sm-12 small">{{ #title }} {{ title }} en {{ /title }}{{ #ou }}{{ ou }} de {{ /ou }}{{ establecimiento }}</p>
                        <p class="col-md-6 col-sm-12 small">{{ mail }}</p>
                        <p class="col-md-6 col-sm-12 small">{{ telephoneNumber }</p>
                    </td></tr>
                {{/.}}
            {{/datos}}
        {% endverbatim %}
    </script>
</tbody>

  • El objeto JSON va definido, datos más, datos menos, de la siguiente forma:

{
    "datos": [
        {
            "cn": "AdaCruz",
            "mail": "acruz@salud.gob.sv",
            "uid": "acruz"
        },
        {
            "cn": "Adalberto Chavez",
            "mail": "achavez@salud.gob.sv",
            "uid": "achavez"
        }
    ],
    "mensajeError": null
}
La idea es que la etiqueta {{#datos}} permite acceder no a los datos de la clave en el objeto JSON, sino más bien a la funcionalidad por defecto que siendo un array es la de recorrerlo. Lo de {{#.}} es un extraño workaround ya que parece que el objeto JSON es recibido seccionado.
Por otra parte {{#.}}, podría ser útil para recorrer un objeto JSON que consista en una seria de array sin índice alguno.

Lo demás, es que cuando estemos recorriendo cada objeto json que compone el array tendremos el dato que cada índice contiene.

Luego, vale la pena mencionar el uso que se le ha vuelto a dar a la etiqueta de tipo {{ #etiqueta }} en el caso del segundo bloque:
<p class="col-md-12 col-sm-12 small">{{ #title }} {{ title }} en {{ /title }}{{ #ou }}{{ ou }} de {{ /ou }}{{ establecimiento }}</p>
Las etiquetas de este modo funcionan como un condicionante. Lo que este dentro de ellas no será mostrado a menos que haya contenido que mostrar. En este caso particular, lo he usado para construir la frase según haya uno u otro contenido, pero supongo que será posible enmarcar toda una etiqueta HTML si se antoja necesario.
  • Por último, el código que realiza todo el trabajo de presentación va de la siguiente forma:
var template = $('#respuesta-template').html();
Mustache.parse(template);
var contenido = Mustache.render(template, respuesta);
pmostrarError(respuesta);
pmostrarMensaje(respuesta);
$("#respuesta tr").remove();
$("#respuesta").append(contenido);

El resultado viene quedando de la siguiente forma:
Los íconos son parte del proyecto Material Icons de Google  y omití las etiquetas de imagen para hacer legible al ejemplo 

Tener en cuenta que este intento de manual ha obviado muchas cosas: El que este usando Bootstrap y JQuery por ejemplo.

Fuentes:
Defining a HTML "template" to append using JQuery
mustache.js - Logic-less {{mustache}} templates with JavaScript
Mustache.js + jQuery: what is the minimal working example ?
How to use Mustache with JS / jQuery – a working example

jueves, 17 de septiembre de 2015

Algunos apuntes sobre como sobrevivir a una implementación de ZoneMinder

Instalación:

Se describe en la wiki de ZoneMinder la instalación para muchas distribuciones, entre ellas en Debian: Debian 8.1 64-bit with Zoneminder 1.28.1 the Easy Way. Esta es la forma en que debe hacerse. No entiendo las razones (Y no tengo la disposición por ahora de descubrirlas) pero tuve un fallo en instalando en un servidor de prueba donde había un servidor apache previamente instalado. Los problemas obtenidos fueron miles, como por ejemplo:

01/07/12 19:49:32.342530 zmpkg[2016].ERR [Unable to run "/usr/bin/zmdc.pl startup", output is "Starting server"] 
Y no tuve más que virtualizar otro servidor de prueba, y empezar la instalación desde cero siguiente exactamente los pasos descritos en la wiki. Alguien en esta lista de usuarios Debian (Que a su vez referencia a un lista de usuarios de Ubuntu, atentos a #5 y #8) habla como si fuera un problema de orden en los pasos a seguir. Nunca a este nivel, pienso, pero no he podido ahondar más en las causas reales.

Configuración inicial: Siguen los problemas

Hacer el primer intento por agregar cámaras hizo que los siguientes problemas aparecieran:
Sep 17 03:42:58 zoneminder zmc_m2[28085]: INF [Starting Capture]
Sep 17 03:42:59 zoneminder web_php[20362]: ERR [socket_sendto( /var/run/zm/zms-933322s.sock ) failed: No such file or directory]
Sep 17 03:42:59 zoneminder web_php[20362]: ERR [getStreamCmdResponse stream error: socket_sendto( /var/run/zm/zms-933322s.sock ) failed: No such file or directory - checkStreamForErrors()]
Sep 17 03:43:01 zoneminder zmc_m1[26920]: ERR [Unable to open input rtsp://admin:12345@192.0.2.1//Streaming/Channels/1 due to: Operation now in progress]
Sep 17 03:43:03 zoneminder zmc_m2[28085]: ERR [RTP timed out]
Sep 17 03:43:06 zoneminder web_php[21144]: ERR [socket_sendto( /var/run/zm/zms-298700s.sock ) failed: No such file or directory]
Sep 17 03:43:06 zoneminder web_php[21144]: ERR [getStreamCmdResponse stream error: socket_sendto( /var/run/zm/zms-298700s.sock ) failed: No such file or directory - checkStreamForErrors()]
Sep 17 03:43:11 zoneminder zmc_m1[26920]: ERR [Unable to open input rtsp://admin:12345@192.0.2.1//Streaming/Channels/1 due to: Operation now in progress]
Sep 17 03:43:12 zoneminder zmc_m2[28085]: ERR [Failed to pre-capture monitor 2 (0/1)]
Sep 17 03:43:12 zoneminder zmc_m2[28085]: INF [Terminating Logger]
No entiendo si en realidad se debía a que las cámaras pudieran estar apagadas en ese momento (Algo que es posible debido al escaso control que tengo sobre ellas), o al error que pude solucionar en Apache, que solucioné de la forma más chapucera: En el fichero /etc/apache2/conf-enabled/serve-cgi-bin.conf
<ifmodule mod_alias.c="">
        <ifmodule mod_cgi.c="">
                Define ENABLE_USR_LIB_CGI_BIN
        </ifmodule>

        <ifmodule mod_cgid.c="">
                Define ENABLE_USR_LIB_CGI_BIN
        </ifmodule>

        <ifdefine enable_usr_lib_cgi_bin="">
        ScriptAlias /cgi-bin "/usr/lib/zoneminder/cgi-bin"
        <directory cgi-bin="" lib="" usr="" zoneminder="">
            Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
            AllowOverride All
            Require all granted
        </directory>
        </ifdefine>
</ifmodule>
es necesario borrar la configuración de los alias por defecto para cgi, lo cual puede ser un problema si acaso se usan otros en el mismo sistema, para que quede de la siguiente forma:
<ifmodule mod_alias.c="">
        <ifmodule mod_cgi.c="">
                Define ENABLE_USR_LIB_CGI_BIN
        </ifmodule>

        <ifmodule mod_cgid.c="">
                Define ENABLE_USR_LIB_CGI_BIN
        </ifmodule>
</ifmodule>
Con lo cual la única configuración al respecto será la contenida en el fichero /etc/apache2/conf-available/zoneminder.conf que debe cargarse en el momento justo en que la guía de configuración que referí arriba lo requiera

Configurando cámaras.

Aunque pensé que sería un dolor de cabeza, lo cierto es que soportan bastante hardware (Lo de basarse en protocolos abiertos, supongo). Mi equipo es un DS-2CD2132-I que alguién ya se había descrito en la wiki. Aunque es posible usar directamente RTSP en lugar de RTSP mediante ffmpeg, resulta que la opción directa presenta problemas de rendimiento brutales. Con ffmpeg tiene un nivel bastante aceptable, sobre todo en lo referente a la alarma por zonas.

Otros apuntes interesantes

Otros apuntes interesantes