Trucos wkhtmltopdf y PDFkit en Debian

PDFkit es una forma sencilla de generar archivos PDF en Rails a partir de código HTML.

PDFkit en realidad delega la creación de los PDF a wkhtmltopdf, que a su vez renderiza las páginas usando webkit.

En mi experiencia, siempre que puedo instalar algo con los paquetes de Debian es mucho mejor porque ahorro tiempo de compilación y además hay quienes se dedican a darle mantenimiento a los paquetes. Sin embargo, hay algunos problemas para usar wkhtmltopdf que se instala con Debian.

  • CSS. En vez de incluir links para cargar los archivos CSS puede resultar mejor agregar el CSS en el encabezado con una etiqueta 
    <style media=’all’ type=’text/css’>
  • Imágenes Para que aparezcan las imágenes locales en el PDF, conviene usar file en el url: <img src=”file:///path/imagen.png”>
  • QT y Xserver Wkhtmltopdf Así como está compilado en Debian, requiere un Xserver. Para resolver este problema cuando se requiere generar un pdf en una máquina que no tiene tarjeta de video y mucho menos un Xserver, se puede usar el comando xvfb-run que permite crear un Xserver virtual.

Pasos a seguir

Instalar paquetes

aptitude install xvfb wkhtmltopdf

Script

Para simplificar el llamado a /usr/bin/wkhtmltopdf a través de xvfb, agregué el script /usr/local/bin/wkhtmltopdf-wrapper con el contenido:

#!/bin/bash
xvfb-run -a --server-args="-screen 0, 1024x768x24" -- wkhtmltopdf "$@"

Simplemente llama a wkhtmltopdf para que se ejecute en el entrono de xvfb y pasa todos los parámetros. La configuración del servidor X es importante para que se rendericen correctamente las imágenes.

PDFkit

En el initializer de PDFkit, agregué la opción para distinguir si se ejecuta directamente wkhtmltopdf o se usa el wrapper. Esto porque en el servidor de producción se usa versión diferente y precompilada de wkhtmltopdf que no funciona en Debian 9 que es el que actualmente estoy usando para desarrollo.

config/initializers/pdfkit.rb

PDFKit.configure do |config|
  if File.exist?("/usr/local/bin/wkhtmltopdf-wrapper")
    config.wkhtmltopdf = '/usr/local/bin/wkhtmltopdf-wrapper' #wraper para usar xvfb-run para versión wkhtmltopdf debian
  else
    config.wkhtmltopdf = '/usr/local/bin/wkhtmltopdf'
  end
end

Debug

Usar la opción ‘-e’ en xvfb

Anuncios

Deja un comentario

La verdad del misterioso caso de las sumas del PREP 2017 del Estado de México.

Aclaración inicial: Nunca en mi vida he votado por el PRI. No quisiera que Del Mazo fuera el gobernador del Estado de México, aunque no creo tampoco que alguno de los otros candidatos o candidatas hiciera demasiada diferencia. Respeto a quienes con convicción han votado por el PRI. Si bien las sumas del PREP realmente no están mal, no quiere decir que piense que no hubo inconsistencias e ilegalidades en la elección. Todas las cifras y ejemplos son tomados del ultimo PREP aún disponible en la página de Aristegui Noticias. Ojalá otras personas pudieran repetir este análisis.

Resumen técnico

Para los que saben SQL y no quieren ver el detalle, les digo que
todo este relajo se armó porque les faltó poner un COALESCE en el campo c_pri_pvem_na_pes.

La ‘base de datos’ es un csv medio raro porque las cantidades están como cadenas y no enteros e incluyen textos como ‘SIN DATO’ e ‘ILEGIBLE’.

Antecedentes

En el PREP 2017 Estado de México el candidato Alfredo del Mazo aparece con 1,955,347 votos. Sin embargo, al sumar los resultados por casilla, que aparecen en la página, no se llega a esa cantidad. Aparentemente, aparecen de la nada votos a favor de Del Mazo. Los votos no son inventados, son resultado de un error en la presentación de los datos. Lo cual no deja de ser muy grave sobre todo por la falta de confianza en las instituciones que genera. A continuación trataré de explicar que fue lo que sucedió. Desafortunadamente, es un error técnico y la explicación también tiene que ser técnica. Si bien es demasiado simple para quienes tenemos conocimientos básicos de bases de datos relacionales y SQL, quienes no tienen esas bases técnicas podrían encontrar la explicación muy rebuscada.

Desde que inició el Programa de Resultados Preeliminares (PREP) en las elecciones del pasado 4 de junio, surgieron varias observaciones de que estaban mal sumados los votos del PRI o, para ser más precisos, los votos del candidato Alfredo del Mazo. Aquí algunas referencias:

No lo podía creer. ¿Sería la autoridad electoral capaz de hacer un fraude tan burdo como aumentar votos en las sumas totales? Hice una prueba simple: Fui a la página del PREP, en la sección de candidatos, abrí un distrito, copié la tabla, la pegué en una hoja de cálculo, sumé los votos de Del Mazo y efectivamente, las sumas no cuadraban.

No es posible, los que hicieron el sistema se quieren pasar de listos o son medio chafas.

Vi que se podía descargar la “base de datos”. Entonces decidí revisar con detalle, a partir de dicha base de datos.

Para los que no sepan nada de SQL, los invito a hacer esta prueba:

  1. Abran el PREP en los resultados por candidato.
  2. Busquen en cualquier distrito, una casilla que diga ‘SIN DATO’ en los votos de Del Mazo y tenga números en los otros candidatos.
  3. Vayan al mismo distrito pero ahora en los resultados por partido, busquen la misma casilla.
  4. Sumen manualmente las quince opciones que se supone que le dan votos a Del Mazo: PRI, PVEM, Nueva Alianza, Encuentro social y sus combinaciones. Esos son los votos que no le pusieron a Del Mazo en la vista por candidato pero si están en la sumados en los totales.

Se puede ver claramente que hay inconsistencias en el PREP, pero es un problema en la presentación de los datos por casilla del candidato Del Mazo y no en las sumas.

El ejercicio más abajo lo hice en una base de datos y salen exactamente las cifras que han reportado en las referencias de arriba.

La ‘base de datos’ del PREP

La mencionada base de datos, tiene tres archivos:

  1. Cat_Candidatos_Gobernador.csv
  2. MEX_GOB_2017.csv
  3. LEEME.txt

En el archivo LEEME.txt se explica como está organizada la información de los otros dos archivos que son textos separados por comas (en donde las comas se usan para separar la información de las columnas de cada renglón). A mi parecer, en el archivo donde están los resultados no debieron haber mezclado la información del avance del PREP con la información capturada de las casillas.

Las primeras diez líneas se ven así:

head -n 10 MEX_GOB_2017.csv
ELECCIÓN DE GOBERNADOR DEL ESTADO DE MÉXICO
05/06/2017 13:00 (UTC-5)
ACTAS_ESPERADAS,ACTAS_FUERA_DE_CATÁLOGO,ACTAS_REGISTRADAS,ACTAS_CAPTURADAS,PORCENTAJE_ACTAS_CAPTURADAS,ACTAS_CONTABILIZADAS,PORCENTAJE_ACTAS_CONTABILIZADAS,PORCENTAJE_ACTAS_CON_INCONSISTENCIAS,ACTAS_NO_CONTABILIZADAS,PORCENTAJE_PARTICIPACION_CIUDADANA
18606,0,18173,18173,97.6727,17772,97.7934,2.2065,   401,52.4964


id_estado,estado,id_distrito_local,distrito_local,seccion,id_casilla,tipo_casilla,extraordinariacontigua,ubicacion_casilla,tipo_de_acta,boletas_sobrantes,total_ciudadanos_votaron,num_boletas_extraidas,pan,pri,prd,pt,pvem,na,morena,pes,c_pri_pvem_na_pes,c_pri_pvem_na,c_pri_pvem_pes,c_pri_na_pes,c_pri_pvem,c_pri_na,c_pri_pes,c_pvem_na_pes,c_pvem_na,c_pvem_pes,c_na_pes,cand_ind_1,no_registrados,nulos,total_votos,lista_nominal,observaciones,contabilizada,mecanismo_de_traslado,sha,fecha_hora_acopio,fecha_hora_captura,fecha_hora_registro
"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",648,1,"B",0.00,2,"2",250,425,427,"14","15","118","10","14","4","134","4","0","0","0","0","0","4","0","0","0","0","0","4",0,6,327,659,"","1","DAT","4bfe26003cb45e5bdc79e9d3bdd034cc97d98d5484c8506a528541407bdbd84a",04/06/2017 22:50:00,04/06/2017 22:57:43,04/06/2017 22:59:03
"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",648,1,"C",0.00,2,"2",257,415,418,"15","94","128","3","8","10","136","7","0","0","0","0","0","0","0","0","0","0","0","7",0,9,417,658,"","1","DAT","11e570fc15ddde1d4b28732f1928994078e74be07df67c54071b513f537d95e9",05/06/2017 00:23:00,05/06/2017 00:35:32,05/06/2017 00:36:33
"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",648,2,"C",0.00,2,"2",234,433,442,"13","105","136","0","15","3","143","7","0","1","0","0","0","0","0","0","0","0","0","8",0,11,442,658,"","1","DAT","949e9519f3af9a2d10433fca66843a40f498aa106d8ef50ef886cb9253a79ab0",05/06/2017 00:26:00,05/06/2017 00:39:24,05/06/2017 00:40:49

Como se puede ver, la información por casilla que es la realmente útil comienza en la linea 7, asi que quité las primeras 6 lineas y el resultado lo puse en un archivo nuevo codificado en UTF-8, con saltos de línea estilo UNIX y lo llamé datos_casillas.txt. ¿Cuántas casillas contiene?

wc datos_casillas.csv 
18607  188268 6290708 datos_casillas.csv

El archivo tiene 18,607 renglones, como uno es de encabezados, significa que están las 18,606 actas que dice el PREP que existen. Lo cual significa que están los renglones que corresponden incluso a las actas no capturadas.

Un detalle importante y que es la raíz de todo el problema es que, existen columnas que en vez de tener número tienen la leyenda ‘SIN DATO’ las primeras tres apariciones están en las lineas 13, 24 y 27:

$ grep -n "SIN DATO" datos_casillas.csv | head -n 3
13:"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",652,1,"B",0.00,2,"2",217,422,424,"23","84","113","1","7","1","180","3","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","1","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","4",0,7,424,623,"","1","DAT","2f068c784209bf6ceb5556df893e90aff3fe333aa64cf57bb4787a3fa3e5cc0b",04/06/2017 23:47:00,04/06/2017 23:52:01,04/06/2017 23:53:06
24:"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",937,5,"C",0.00,1,"2",413,291,292,"17","96","46","2","1","4","99","9","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","3",0,15,292,689,"","1","DAT","830f882618473a0a45c04c80b5d1cdd532b5d793ea86495dc8663a4523c2615b",05/06/2017 02:32:00,05/06/2017 02:43:01,05/06/2017 02:44:38
27:"15","Estado de México",1,"CHALCO DE DIAZ COVARRUBIAS",937,8,"C",0.00,1,"2",419,287,289,"17","98","60","6","4","1","79","5","SIN DATO","SIN DATO","SIN DATO","SIN DATO","1","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","SIN DATO","5",0,13,289,689,"","1","DAT","55e76c04054bd3d4d8cc7a15ace515878ec1eebec6e8cb13004be28f755a91a3",05/06/2017 01:11:00,05/06/2017 01:23:43,05/06/2017 01:25:32

En total, hay 4,206 renglones que contienen al menos una vez la palabra ‘SIN DATO’:

grep "SIN DATO" datos_casillas.csv | wc
4206   73115 1638783

¿Que significa ‘SIN DATO’? ¿’SIN DATO’ significa 0? No. En realidad significa NULO o NULL. No es número, no es un espacio en blanco ni una cadena vacia. Como NULL no es un número, no se puede sumar.

Por ejemplo, en PostgreSQL:

psql -U postgres -h localhost prep
prep=# SELECT 1 + NULL AS suma;
 suma 
------
     
(1 fila)

Al querer sumar un número con un NULL el resultado es NULL o, dicho coloquialemente: No tengo resultado al sumar un número con algo que no es nada.

Lo que normalmente se hace en estos casos, es que se define como deben considerarse los valores NULL (con la función COALESCE). Lo más común es considerarlos como cero:

SELECT 1 + COALESCE(NULL, 0) AS suma;
 suma 
------
    1
(1 fila)

Y el problema de las sumas del PREP es, exactamente, que no se manejaron correctamente los nulos. Cosa que, dicho sea de paso, es un error de principiantes, máxime tratándose de un sistema de tanto impacto y que nos ha costado tanto dinero.

Además de los ‘SIN DATO’, hay otras columnas que dicen ‘ILEGIBLE’, en este caso son 479 renglones:

grep ILEGIBLE datos_casillas.csv | wc
479    4524  203138

Detalles del error en las sumas

A continuación pongo el ejercicio en el que se podrá ver con claridad dónde están los 241 mil votos que arroja el estudio que publica Proceso.

El primer paso es crear una tabla para almacenar la información en el mismo formato en el que está el CSV.

CREATE TABLE actas (
  id_estado                integer,
  estado                   varchar(255),
  id_distrito_local        integer,
  distrito_local           varchar(255),
  seccion                  integer,
  id_casilla               integer,
  tipo_casilla             varchar(255),
  extraordinariacontigua   decimal, -- ¿¡!?
  ubicacion_casilla        integer,
  tipo_de_acta             integer,
  boletas_sobrantes        integer,
  total_ciudadanos_votaron integer,
  num_boletas_extraidas    integer,
  pan                      integer,
  pri                      integer,
  prd                      integer,
  pt                       integer,
  pvem                     integer,
  na                       integer,
  morena                   integer,
  pes                      integer,
  c_pri_pvem_na_pes        integer,
  c_pri_pvem_na            integer,
  c_pri_pvem_pes           integer,
  c_pri_na_pes             integer,
  c_pri_pvem               integer,
  c_pri_na                 integer,
  c_pri_pes                integer,
  c_pvem_na_pes            integer,
  c_pvem_na                integer,
  c_pvem_pes               integer,
  c_na_pes                 integer,
  cand_ind_1               integer,
  no_registrados           integer,
  nulos                    integer,
  total_votos              integer,
  lista_nominal            integer,
  observaciones            text,
  contabilizada            varchar(1),
  mecanismo_de_traslado    varchar(255),
  sha                      varchar(255),
  fecha_hora_acopio        timestamp,
  fecha_hora_captura       timestamp,
  fecha_hora_registro      timestamp
);

Es un diseño muy simple, sin llaves primarias, sin índices, que permite campos nulos en cualquier lugar, pero es suficiente para demostrar el error del PREP.

Para incluir los datos, en necesario quitar todos los ‘SIN DATO’ del CSV en vi lo hice así: :1,$s/SIN DATO//g, aunque podría usarse sed. Algo similar tuve que hacer para ‘ILEGIBLE’ y para las columnas de fecha y hora de las actas no capturadas que venían en vez de la fecha y hora traían los caracteres: ‘/ / : :’. En fin, una vez limpiados, ya pude importar los datos:

copy actas from '/tmp/datos_casillas_limpios.csv' CSV HEADER;
COPY 18606

Los votos del Del Mazo son los del PRI, PVEM, Nueva Alianza, Encuentro Social y todas las combinaciones posibles, que corresponden a las columnas: pri, pvem, na, pes, c_pri_pvem_na_pes, c_pri_pvem_na, c_pri_pvem_pes, c_pri_na_pes, c_pri_pvem, c_pri_na, c_pri_pes, c_pvem_na_pes, c_pvem_na, c_pvem_pes y c_na_pes. Es decir, 15 de las 20 columnas con números de votos cuentan para Del Mazo.

La primera casilla del Distrito de Chalco corresponde al primer renglón de datos del CSV, dice que Del Mazo obtuvo 41 votos. Podemos consultar el detalle y la suma de dicha casilla:

prep=# \x
Se ha activado el despliegue expandido.
prep=# SELECT 
  pri, pvem, na, pes, c_pri_pvem_na_pes, c_pri_pvem_na, c_pri_pvem_pes, c_pri_na_pes, c_pri_pvem, c_pri_na, c_pri_pes, c_pvem_na_pes, c_pvem_na, c_pvem_pes, c_na_pes,
  pri + pvem + na + pes + c_pri_pvem_na_pes + c_pri_pvem_na + c_pri_pvem_pes + c_pri_na_pes + c_pri_pvem + c_pri_na + c_pri_pes + c_pvem_na_pes + c_pvem_na + c_pvem_pes + c_na_pes
  AS votos_del_mazo
FROM actas
WHERE seccion = 648 AND tipo_casilla = 'B'
;
-[ RECORD 1 ]-----+---
pri               | 15
pvem              | 14
na                | 4
pes               | 4
c_pri_pvem_na_pes | 0
c_pri_pvem_na     | 0
c_pri_pvem_pes    | 0
c_pri_na_pes      | 0
c_pri_pvem        | 0
c_pri_na          | 4
c_pri_pes         | 0
c_pvem_na_pes     | 0
c_pvem_na         | 0
c_pvem_pes        | 0
c_na_pes          | 0
votos_del_mazo    | 41

prep=# \x
Se ha desactivado el despliegue expandido.

¿Y que pasa con el acta de la casilla básica de la sección 652 que es la primera en la que detectamos ‘SIN DATO’?

prep=# \x
Se ha activado el despliegue expandido.
prep=# SELECT 
prep-#   pri, pvem, na, pes, c_pri_pvem_na_pes, c_pri_pvem_na, c_pri_pvem_pes, c_pri_na_pes, c_pri_pvem, c_pri_na, c_pri_pes, c_pvem_na_pes, c_pvem_na, c_pvem_pes, c_na_pes,
prep-#   pri + pvem + na + pes + c_pri_pvem_na_pes + c_pri_pvem_na + c_pri_pvem_pes + c_pri_na_pes + c_pri_pvem + c_pri_na + c_pri_pes + c_pvem_na_pes + c_pvem_na + c_pvem_pes + c_na_pes
prep-#   AS votos_del_mazo
prep-# FROM actas
prep-# WHERE seccion = 652 AND tipo_casilla = 'B'
prep-# ;
-[ RECORD 1 ]-----+---
pri               | 84
pvem              | 7
na                | 1
pes               | 3
c_pri_pvem_na_pes | 
c_pri_pvem_na     | 
c_pri_pvem_pes    | 
c_pri_na_pes      | 
c_pri_pvem        | 
c_pri_na          | 1
c_pri_pes         | 
c_pvem_na_pes     | 
c_pvem_na         | 
c_pvem_pes        | 
c_na_pes          | 
votos_del_mazo    | 

prep=# \x
Se ha desactivado el despliegue expandido.

Como se puede ver, la suma no da votos para Del Mazo, aunque debería tener 96. En el PREP se puede ver claramente que esa acta muestra ‘SIN DATO’ en el total de Del Mazo.

El error es que esas celdas que dicen ‘SIN DATO’ en realidad podrían tener datos. La suma correcta debió hacerse así:

Una consulta más correcta sería:

prep=# \x
Se ha activado el despliegue expandido.
prep=# SELECT
  pri, pvem, na, pes, c_pri_pvem_na_pes, c_pri_pvem_na, c_pri_pvem_pes, c_pri_na_pes, c_pri_pvem, c_pri_na, c_pri_ipes, c_pvem_na_pes, c_pvem_na, c_pvem_pes, c_na_pes,
  COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + 
 COALESCE(c_pri_pvem_na_pes, 0) + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0)
  AS votos_del_mazo
FROM actas
WHERE seccion = 652 AND tipo_casilla = 'B'
;
-[ RECORD 1 ]-----+---
pri               | 84
pvem              | 7
na                | 1
pes               | 3
c_pri_pvem_na_pes | 
c_pri_pvem_na     | 
c_pri_pvem_pes    | 
c_pri_na_pes      | 
c_pri_pvem        | 
c_pri_na          | 1
c_pri_pes         | 
c_pvem_na_pes     | 
c_pvem_na         | 
c_pvem_pes        | 
c_na_pes          | 
votos_del_mazo    | 96

prep=# \x
Se ha desactivado el despliegue expandido.

Para ser más precisos, el error en la suma es provocado por los nulos en la columna c_pri_pvem_na_pes Es decir, las actas en las que se capturó un valor NULO en el campo c_pri_pvem_na_pes aparecen ‘SIN DATO’ en la página de resultados por candidato.

La siguiente consulta muestra con toda precisión la diferencia que es exactamente igual a la mostrada en el Semanario Zeta:

SELECT id_distrito_local AS id, distrito_local, sum(votos_del_mazo_error) AS suma_error,
  sum(votos_del_mazo_correctos) AS suma_correcta,
  sum(votos_del_mazo_correctos) - sum(votos_del_mazo_error) AS diferencia
FROM (
  SELECT 
    id_distrito_local, distrito_local, seccion, id_casilla, tipo_casilla, 
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + c_pri_pvem_na_pes + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_error,
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + COALESCE(c_pri_pvem_na_pes, 0) + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_correctos
  FROM actas
  ORDER BY seccion, id_casilla, tipo_casilla
) AS votos_del_mazo
GROUP BY id_distrito_local, distrito_local
ORDER BY id_distrito_local
;

Nótese que la única diferencia entre la suma correcta y la incorrecta es que la incorrecta dice c_pri_pvem_na_pes y la correcta dice COALESCE(c_pri_pvem_na_pes, 0).

El resultado de la consulta anterior es el siguiente:

 id |           distrito_local            | error | correcta |  dif  
----+-------------------------------------+-------+----------+-------
  1 | CHALCO DE DIAZ COVARRUBIAS          | 33135 |    37013 |  3878
  2 | TOLUCA DE LERDO                     | 36227 |    39843 |  3616
  3 | CHIMALHUACAN                        | 27937 |    33535 |  5598
  4 | LERMA DE VILLADA                    | 44797 |    50412 |  5615
  5 | CHICOLOAPAN DE JUAREZ               | 33766 |    37809 |  4043
  6 | ECATEPEC DE MORELOS                 | 23907 |    30080 |  6173
  7 | TENANCINGO DE DEGOLLADO             | 48073 |    54327 |  6254
  8 | ECATEPEC DE MORELOS                 | 25203 |    31187 |  5984
  9 | TEJUPILCO DE HIDALGO                | 65795 |    78582 | 12787
 10 | VALLE DE BRAVO                      | 70777 |    82077 | 11300
 11 | TULTITLAN DE MARIANO ESCOBEDO       | 29676 |    34606 |  4930
 12 | TEOLOYUCAN                          | 34591 |    38588 |  3997
 13 | ATLACOMULCO DE FABELA               | 71312 |    75573 |  4261
 14 | JILOTEPEC DE ANDRES MOLINA ENRIQUEZ | 64547 |    71779 |  7232
 15 | IXTLAHUACA DE RAYON                 | 77234 |    80330 |  3096
 16 | CIUDAD ADOLFO LOPEZ MATEOS          | 25421 |    30168 |  4747
 17 | HUIXQUILUCAN DE DEGOLLADO           | 43974 |    49507 |  5533
 18 | TLALNEPANTLA DE BAZ                 | 30668 |    36337 |  5669
 19 | SANTA MARIA TULTEPEC                | 27631 |    32221 |  4590
 20 | ZUMPANGO DE OCAMPO                  | 44371 |    50454 |  6083
 21 | ECATEPEC DE MORELOS                 | 32643 |    37381 |  4738
 22 | ECATEPEC DE MORELOS                 | 26562 |    31089 |  4527
 23 | TEXCOCO DE MORA                     | 43616 |    47790 |  4174
 24 | NEZAHUALCOYOTL                      | 18000 |    24202 |  6202
 25 | NEZAHUALCOYOTL                      | 21625 |    28065 |  6440
 26 | CUAUTITLAN IZCALLI                  | 29628 |    34877 |  5249
 27 | VALLE DE CHALCO SOLIDARIDAD         | 25024 |    29597 |  4573
 28 | AMECAMECA DE JUAREZ                 | 38040 |    44466 |  6426
 29 | NAUCALPAN DE JUAREZ                 | 26101 |    32881 |  6780
 30 | NAUCALPAN DE JUAREZ                 | 29169 |    35996 |  6827
 31 | LOS REYES ACAQUILPAN                | 39023 |    44154 |  5131
 32 | NAUCALPAN DE JUAREZ                 | 31143 |    38060 |  6917
 33 | TECAMAC DE FELIPE VILLANUEVA        | 47127 |    53047 |  5920
 34 | TOLUCA DE LERDO                     | 47551 |    50292 |  2741
 35 | METEPEC                             | 39413 |    44697 |  5284
 36 | SAN MIGUEL ZINACANTEPEC             | 42874 |    45525 |  2651
 37 | TLALNEPANTLA DE BAZ                 | 29603 |    35451 |  5848
 38 | COACALCO DE BERRIOZABAL             | 28028 |    34025 |  5997
 39 | ACOLMAN DE NEZAHUALCOYOTL           | 56505 |    61025 |  4520
 40 | IXTAPALUCA                          | 30804 |    34319 |  3515
 41 | NEZAHUALCOYOTL                      | 22531 |    26189 |  3658
 42 | ECATEPEC DE MORELOS                 | 25368 |    29332 |  3964
 43 | CUAUTITLAN IZCALLI                  | 29078 |    33396 |  4318
 44 | NICOLAS ROMERO                      | 47239 |    51628 |  4389
 45 | ALMOLOYA DE JUAREZ                  | 48429 |    53399 |  4970
 46 | VOTO EXTRANJERO                     |    36 |       36 |     0
(46 filas)

Una consulta con los totales quedaría asi:

SELECT sum(votos_del_mazo_error) AS error,
  sum(votos_del_mazo_correctos) AS correcta,
  sum(votos_del_mazo_correctos) - sum(votos_del_mazo_error) AS dif
FROM (
  SELECT 
    id_distrito_local, distrito_local, seccion, id_casilla, tipo_casilla, 
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + c_pri_pvem_na_pes + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_error,
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + COALESCE(c_pri_pvem_na_pes, 0) + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_correctos
  FROM actas
  ORDER BY seccion, id_casilla, tipo_casilla
) AS votos_del_mazo
;

El resultado es:

  error  | correcta |  dif
---------+----------+--------
 1714202 |  1955347 | 241145
(1 fila)

Para corroborar, se puede usar una consulta en la que solamente se consideren los renglones que tienen nulo el campo c_pri_pvem_na_pes

SELECT sum(votos_del_mazo) AS votos_no_contados_en_detalle_acta, count(*) AS numero_actas
FROM (
  SELECT 
      COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + COALESCE(c_pri_pvem_na_pes, 0) + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo
    FROM actas
    WHERE c_pri_pvem_na_pes IS NULL
) AS votos_no_sumados
;
 votos_no_contados_en_detalle_acta | numero_actas 
-----------------------------------+--------------
                            241145 |         2832
(1 fila)

Lo cual significa que los 241,145 votos que no aparecen en los detalles de las actas corresponden a 2,832 actas que en el sitio del PREP aprecerán como ‘SIN DATO’, pero que en realidad si tienen votos para Del Mazo.

Claro que eso de sumar quince columnas, no parece ser la forma correcta de manejar los datos. Realmente espero que esa no sea la estructura de la base de datos real. Si bien el hecho de que falte un COALESCE es un error que a cualquiera le puede pasar (quién nunca haya cometido un error similar en su trabajo que tire la primera piedra), abre la puerta a otras preguntas:

¿Cómo se validan y se prueban los sistemas? ¿Cómo están diseñadas las bases de datos? ¿Que control de calidad hay? ¿Los sistemas tienen pruebas unitarias?

En teoría, un buen diseño de la estructura de la base de datos, debería tener la restricción de no aceptar valores nulos ni menores a cero en los conteos de votos de las actas, de hecho, ni siquiera debería haber columnas para cada partido, ojalá que internamente si tengan una base de datos normalizada.

El problema se maximiza si mostramos los datos de la candidata de Morena que quedó en segundo lugar:

SELECT sum(morena) as total_morena,
  sum(votos_del_mazo_error) AS del_mazo_error,
  sum(votos_del_mazo_correctos) AS del_mazo_correcta
FROM (
  SELECT 
    id_distrito_local, distrito_local, seccion, id_casilla, tipo_casilla, morena,
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + c_pri_pvem_na_pes + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_error,
    COALESCE(pri, 0) + COALESCE(pvem, 0) + COALESCE(na, 0) + COALESCE(pes, 0) + COALESCE(c_pri_pvem_na_pes, 0) + COALESCE(c_pri_pvem_na, 0) + COALESCE(c_pri_pvem_pes, 0) + COALESCE(c_pri_na_pes, 0) + COALESCE(c_pri_pvem, 0) + COALESCE(c_pri_na, 0) + COALESCE(c_pri_pes, 0) + COALESCE(c_pvem_na_pes, 0) + COALESCE(c_pvem_na, 0) + COALESCE(c_pvem_pes, 0) + COALESCE(c_na_pes, 0) AS votos_del_mazo_correctos
  FROM actas
  ORDER BY seccion, id_casilla, tipo_casilla
) AS votos_del_mazo
;
 total_morena | del_mazo_error | del_mazo_correcta 
--------------+----------------+-------------------
      1786962 |        1714202 |           1955347
(1 fila)

¿Entonces como hicieron la suma de distritos para que no se reflejara el error que existe en la consulta de las actas individuales?
Mi teoría es que primero hicieron la suma de los renglones y luego la de las columnas.

SELECT sum(pan) AS pan , sum(pri) + sum(pvem) + sum(na) + sum(pes) + sum(c_pri_pvem_na_pes) + sum(c_pri_pvem_na) + sum(c_pri_pvem_pes) + sum(c_pri_na_pes) + sum(c_pri_pvem) + sum(c_pri_na) + sum(c_pri_pes) + sum(c_pvem_na_pes) + sum(c_pvem_na) + sum(c_pvem_pes) + sum(c_na_pes) AS pri_pvem_na_pes, sum(morena) AS morena, sum(prd) AS prd,sum(pt) AS pt,
sum(cand_ind_1) AS indep, sum(no_registrados) no_reg, sum(nulos) AS nulos
FROM actas
;
  pan   | pri_pvem_na_pes | morena  |   prd   |  pt   | indep  | no_reg | nulos
--------+-----------------+---------+---------+-------+--------+--------+--------
 654681 |         1955347 | 1786962 | 1031791 | 62643 | 123324 |   7641 | 176168
(1 fila)

Que son exactamente los números reportados en la carátula del PREP.

Conclusiones

¿Hubo fraude en el PREP o no?

No hubo fraude con las sumas del PREP (lo cual es independiente de otros tipos de fraude que pudiera haber). Hay errores en las cifras de las actas, no en las de los totales.

¿Y encontrar que en realidad no se aumentaron votos al candidato Del Mazo limpia la elección?

No. El error del PREP es muy grave por el desconcierto que causa y las suspicacias que despierta. Es un error de principantes y me queda la incertidumbre sobre el diseño de los sistemas de las elecciones. Aún quedan muchas cosas quizá mas graves como las dichosas tarjetas de salario rosa.

¿Que pasa con las actas no contabilizadas?

Claramente, las actas 401 actas que en el resumen se indica que no fueron contabilizadas por inconsistencias, si están todas contabilizadas.

¿Hay mas cosas que revisar?

Todavía podríamos hacer un recuento ciudadano revisando la imagen de cada una de las actas y viendo que hayan sido correctamente capturadas. Pero necesitaríamos crear un sistema que distribuyera las imágenes de las actas del PREP (ya las descargué todas) y entre muchos validáramos la captura.

¿Valió la pena hacerle al Sherlock Holmes?
Pienso que es importante encontrar y descubrir los errores para que los encargados de desarrollar estos sistemas tomen cartas en el asunto. Nuestro país necesita tener confianza en las instituciones y este tipo de cosas no ayudan. Ya desde la famosa caída del sistema de 1988 sigue aumentando la desconfianza. Las elecciones nos salen demasiado caras como para tolerar este tipo de errores.

¿Hay algo positivo?
Creo que la posibilidad de obtener la base de datos de forma transparente es muy importante. Para el año próximo estaré listo para estar analizando los datos directo de la base de datos y no hacerle caso a las sumas que pongan en el PREP.

Deja un comentario

Detalles en el envío de email en Rails 3 via SMTP

Claro que toda la información está en la Guía de Rails, sin embargo, tuve que dedicarle un buen rato para poder enviar los correos a través de un SMTP que nunca había usado.

Sobre SMTP

En el principio existía el puerto 25 sin cifrar (y muchas veces sin autenticar) lo que daba lugar al espionaje y al spam. Sigue siendo usado y es la conexión mas simple, sobre todo si no se envía información sensible (lo cual nunca se sabe).

Con el tiempo, se agregaron otros puertos. El 465 que se usa para cifrar la conexión y sobre ella transmitir todo en texto plano. Y el 587 que primero autentica y luego inicia la capa TLS (STARTTLS)

Ver https://stackoverflow.com/a/21023502

El problema y la solución

Un inconveniente es que muchos de los servidores que usan SSL y/o TLS usan un certificado de seguridad diferente al del dominio que está enviando el correo.

Al usar el puerto 465, es necesario agregar a las configuraciones del SMTP, la opción de openssl_verify_mode => OpenSSL::SSL::VERIFY_NONE hace que se omita la verificación del certificado de seguridad que no se detecta tan fácil.

ActionMailer::Base.smtp_settings = {
      address: host.smtp,
      port: 465,
      authentication: :login, # o :plain según convenga
      ssl: true,
      enable_starttls_auto:  false,
      openssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
      user_name: "usuario",
      password: "contraseña"
    }

En el caso del puerto 587, es necesario activar :enable_starttls_auto

Deja un comentario

Leer email con Rails

La Guía de Rails (en este proyecto uso Rails 4) explica como procesar los correos, En el ejemplo, se lee de la entrada estándard. Sin embargo, el asunto medio complicado es como llevar los correos a la entrada estándard de Rails.

En el caso que me ocupa es leer correos de un servidor via POP3. La solución para mi fue combinar fetchmail con la aplicación de rails. Fetchmail es una joya, puede traer correo muy eficientemente casi de cualquier fuente. Una vez que trae el correo lo puede dejar en el buzón local del usuario o pasarlo a un MDA (Mail Deliver Agent). Cuando se pasa el correo al MDA lo que hace es iniciar el comando y pasar el mensaje a través de la entrada estándard. Ahí está todo el truco.

Primero, como uso rbenv, hice un script para iniciar el rails con un comando más simple el archivo /home/israel/bin/rails_app.sh

#!/bin/bash

export PATH=/home/israel/.rbenv/shims:/home/israel/.rbenv/bin:/home/israel/bin:/home/israel/.rbenv/shims:/home/israel/.rbenv/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

cd /home/israel/mi_app

bundle exec rails runner "RecibosMailer.receive(STDIN.read)"

Luego, en el archivo .fetchmailrc quedó así:

poll pop.gmail.com protocol POP3 port 995
  username "usuario@gmail.com" password "secreto"
  ssl mda "/home/israel/bin/rails_app.sh"

Cada vez que fetchmail trae un mensaje, lo pasa a la aplicación de rails que está escuchando en la entrada estándard. El método que lee el correo puede hacer lo que sea necesario, sirva como ejemplo, solo mostrar el remitente:

class RecibosMailer < ActionMailer::Base
  def receive(email)
    puts "CORREO DE #{email.from}"
  end
end

El detalle que encuentro con esta solución es con cada correo, se inicia una instancia de la aplicación que se termina al leer el mensaje. Sin embargo, no se espera un alto volumen de mensajes. Así que este detalle no creo que de mucho problema.

Deja un comentario

Openvpn Debian 8 Jessie

Para poder iniciar openvpn en Debian 8, es necesario, además de agregar el archivo de configuración /etc/openvpn/archivo.conf ejemplo:


client
dev tun
proto udp
remote servidor-vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun

ca ca.crt
cert certificado.crt
key llave.key

ns-cert-type server
comp-lzo
verb 3

Tambièn es necesario modificar el archivo /etc/default/openvpn , en mi caso, solo quité el comentario a la línea:

AUTOSTART="all"

Posteriormente, fué necesario recargar la configuración:


# systemctl daemon-reload
# service openvpn start

Deja un comentario

Ruby 1.8.7 gcc 4.7

Hay un error en los binarios de ruby compilados con gcc 4.7.
Para instalar ruby 1.8.7 con rbenv, el truco es:

export RUBY_CFLAGS="-O2 -fno-tree-dce -fno-optimize-sibling-calls"
rbenv install 1.8.7-p358

Referencia:
https://bugs.ruby-lang.org/issues/6383#note-1
http://stackoverflow.com/questions/10820323/install-bundler-using-rvm-ruby-1-8-7-with-gcc-4-7-on-linux

Deja un comentario

Imagen ISO a USB stick

Como root


cp archivo.iso  /dev/sdX
sync

X es la letra de la unidad, usar dmesg para saber que unidad se asignó. La unidad debe estar desmontada. Nótese que no es la partición.

Deja un comentario