sitio personal de Rodrigo Garcia.

Ver el codigo fuente

# -*- coding: utf-8 -*-
Monomotapa - A Micro CMS
Copyright (C) 2014, Paul Munday.

PO Box 28228, Portland, OR, USA 97228
paul at

Modificado por: Rodrigo Garcia 2017

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero  Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <>.

    a city whose inhabitants are bounded by deep feelings of friendship, 
    so that they intuit one another's most secret needs and desire. 
    For instance, if one dreams that his friend is sad, the friend will
    perceive the distress and rush to the sleepers rescue.

    (Jean de La Fontaine, *Fables choisies, mises en vers*, VIII:11 Paris, 
    2nd ed., 1678-9)

cited in : 
Alberto Manguel and Gianni Guadalupi, *The Dictionary of Imaginary Places*, 
Bloomsbury, London, 1999.

A micro cms written using the Flask microframework, orignally to manage my 
personal site. It is designed so that publishing a page requires no more than
dropping a markdown page in the appropriate directory (though you need to edit
a json file if you want it to appear in the top navigation). 

It can also display its own source code and run its own unit tests.

The name 'monomotapa' was chosen more or less at random (it shares an initial
with me) as I didn't want to name it after the site and be typing import 
paulmunday, or something similar,  as that would be strange.


from flask import render_template, abort, Markup, escape, request #, make_response
from flask import redirect
from werkzeug import secure_filename

from pygments import highlight
from pygments.lexers import PythonLexer, HtmlDjangoLexer, TextLexer
from pygments.formatters import HtmlFormatter

import markdown

from time import gmtime, strptime, strftime, ctime, mktime
import datetime

import os.path
import os
import subprocess
import json
from collections import OrderedDict

from simplemotds import SimpleMotd

from monomotapa import app
from monomotapa.config import ConfigError

from monomotapa.utils import captcha_comprobar_respuesta, captcha_pregunta_opciones_random
from monomotapa.utils import categorias_de_post, categoriasDePost, categoriasList, cabezaPost
from monomotapa.utils import titulo_legible, metaTagsAutomaticos

from markdown.extensions.toc import TocExtension

json_pattrs = {}
with open(os.path.join('monomotapa','pages.json'), 'r') as pagefile:
    json_pattrs = json.load(pagefile)

simplemotd = SimpleMotd("config_simplemotds.json")

class MonomotapaError(Exception):
    """create classs for own errors"""

def get_page_attributes(jsonfile):
    """Returns dictionary of page_attributes.
    Defines additional static page attributes loaded from json file.
    N.B. static pages do not need to have attributes defined there,
    it is sufficient to have a in src for each /page 
    possible values are src (name of markdown file to be rendered)
    heading, title, and trusted (i.e. allow embeded html in markdown)"""
        with open(src_file(jsonfile), 'r') as pagesfile:
            page_attributes = json.load(pagesfile)
    except IOError:
        page_attributes = []
    return page_attributes

def get_page_attribute(attr_src, page, attribute):
    """returns attribute of page if it exists, else None.
    attr_src = dictionary(from get_page_attributes)"""
    if page in attr_src and attribute in attr_src[page]:
        return attr_src[page][attribute]
        return None

# Navigation
def top_navigation(page):
    """Generates navigation as an OrderedDict from navigation.json.
    Navigation.json consists of a json array(list) "nav_order"
    containing the names of the top navigation elements and 
    a json object(dict) called "nav_elements"
    if a page is to show up in the top navigation
    there must be an entry present in nav_order but there need not
    be one in nav_elements. However if there is the key must be the same.
    Possible values for nav_elements are link_text, url and urlfor
    The name  from nav_order will be used to set the link text, 
    unless link_text is present in nav_elements.
    url and urlfor are optional, however if ommited the url wil be
    generated in the navigation by  url_for('staticpage', page=[key])
    equivalent to  @app.route"/page"; def page())
    which may not be correct. If a url is supplied  it will be used 
    otherwise if urlfor is supplied it the url will be
    generated with url_for(urlfor). url takes precendence so it makes
    no sense to supply both.
    Web Sign-in is supported by adding a "rel": "me" attribute.
    with open(src_file('navigation.json'), 'r') as  navfile:
        navigation = json.load(navfile)
    base_nav = OrderedDict({})
    for key in navigation["nav_order"]:
        nav = {}
        nav['base'] = key
        nav['link_text'] = key
        if key in navigation["nav_elements"]:
            elements = navigation["nav_elements"][key]
        base_nav[key] = nav
    return {'navigation' :  base_nav, 'page' : page}

# For pages
class Page:
    """Generates  pages as objects"""
    def __init__(self, page, **kwargs):
        """Define attributes for  pages (if present).
        Sets, self.title, self.heading, self.trusted etc
        This is done through indirection so we can update the defaults 
        (defined  in the 'attributes' dictionary) with values from config.json
        or pages.json easily without lots of if else statements.
        If css is supplied it will overide any default css. To add additional
        style sheets on a per page basis specifiy them in pages.json.
        The same also applies with hlinks.
        css is used to set locally hosted stylesheets only. To specify 
        external stylesheets use hlinks: in config.json for 
        default values that will apply on all pages unless overidden, set here
        to override the default. Set in pages.json to add after default.
        # set default attributes = page.rstrip('/')
        self.defaults = get_page_attributes('defaults.json')
        self.pages = get_page_attributes('pages.json')
        title = titulo_legible(page.lower())
        heading = titulo_legible(page.capitalize())
        self.categorias = categoriasDePost(
            self.default_template = self.defaults['template']
        except KeyError:
            raise ConfigError('template not found in default.json')
        # will become, self.title, self.heading, 
        # self.footer, self.internal_css, self.trusted
        attributes = {'name' :, 'title' : title,
                      'navigation' : top_navigation(,
                      'heading' : heading, 'footer' : None,
                      'css' : None , 'hlinks' :None, 'internal_css' : None,
                      'trusted': False,
                      'preview-chars': 250
        # contexto extra TODO: revisar otra forma de incluir un contexto
        self.contexto = {}
        self.contexto['consejo'] = simplemotd.getMotdContent()

        # set from defaults
        # override with kwargs
        # meta tags
            setattr(self, self.pages, metaTagsAutomaticos(, self.pages.get(, {})))
            #self.pages[] = metaTagsAutomaticos(, self.pages.get(, {}))
        except Exception as e: 
            print('...error.', str(e))

        # override attributes if set in pages.json
        if page in self.pages:
        # set attributes (as etc)  using indirection
        for attribute, value in attributes.items():
            setattr(self, attribute, value)
            setattr(self,attribute, value)

        # reset these as we want to append rather than overwrite if supplied
        if 'css' in kwargs:
            self.css = kwargs['css']
        elif 'css' in self.defaults:
            self.css = self.defaults['css']
        if 'hlinks' in kwargs:
            self.hlinks = kwargs['hlinks']
        elif 'hlinks' in self.defaults:
            self.hlinks = self.defaults['hlinks']
        # append hlinks and css from pages.json rather than overwriting
        # if css or hlinks are not supplied they are set to default
        if page in self.pages:
            if 'css' in self.pages[page]:
                self.css = self.css + self.pages[page]['css']
            if 'hlinks' in self.pages[page]:
                self.hlinks = self.hlinks + self.pages[page]['hlinks']
        # append heading to default if set in config
        if app.config['default_title']:
            self.title = app.config['default_title'] + self.title

    def _get_markdown(self):
        """returns rendered markdown or 404 if source does not exist"""
        src = self.get_page_src(, 'src', 'md') 
        if src is None:
            return render_markdown(src, self.trusted)

    def get_page_src(self, page, directory=None, ext=None):
        """"return path of file (used to generate page) if it exists,
        or return none.
        Also returns the template used to render that page, defaults
        to static.html.
        It will optionally add an extension, to allow 
        specifiying pages by route."""
        # is it stored in a config
        pagename = get_page_attribute(self.pages, page, 'src')
        if not pagename:
            pagename = page + get_extension(ext)
        if os.path.exists(src_file(pagename , directory)):
            return src_file(pagename, directory)
            return None

    def get_template(self, page):
        """returns the template for the page"""
        pagetemplate = get_page_attribute(self.pages, page, 'template')
        if not pagetemplate:
            pagetemplate = self.default_template
        if os.path.exists(src_file(pagetemplate , 'templates')):
            return pagetemplate
            raise MonomotapaError("Template: %s not found" % pagetemplate)

    def generate_page(self, contents=None):
        """return a page generator function.
        For static pages written in Markdown under src/.
        contents are automatically rendered.
        N.B. See note above in about headers"""
        if not contents:
            contents = self._get_markdown()
        template = self.get_template(
        return render_template(template, 
                contents = Markup(contents),

# helper functions
def src_file(name, directory=None):
    """return potential path to file in this app"""
    if not directory:
        return os.path.join( 'monomotapa', name)
        return os.path.join('monomotapa', directory, name)

def get_extension(ext):
    '''constructs extension, adding or stripping leading . as needed.
    Return null string for None'''
    if ext is None:
        return ''
    elif ext[0] == '.':
        return ext
        return '.%s' % ext

def render_markdown(srcfile, trusted=False):
    """Return markdown file rendered as html. Defaults to untrusted:
        html characters (and character entities) are escaped 
        so will not be rendered. This departs from markdown spec 
        which allows embedded html."""
        with open(srcfile, 'r') as f:
            src =
            if trusted == True:
                return markdown.markdown(src,
                return markdown.markdown(escape(src),

    except IOError:
        return None

def render_pygments(srcfile, lexer_type):
    """returns src(file) marked up with pygments"""
    if lexer_type == 'python':
        with open(srcfile, 'r') as f:
            src =
            contents = highlight(src, PythonLexer(), HtmlFormatter())
    elif lexer_type == 'html':
        with open(srcfile, 'r') as f:
            src =
            contents = highlight(src, HtmlDjangoLexer(), HtmlFormatter())
    # default to TextLexer for everything else
        with open(srcfile, 'r') as f:
            src =
            contents = highlight(src, TextLexer(), HtmlFormatter())
    return contents

def get_pygments_css(style=None):
    """returns css for pygments, use as internal_css"""
    if style is None:
        style = 'friendly'
    return HtmlFormatter(style=style).get_style_defs('.highlight')

def heading(text, level):
    """return as html heading at h[level]"""
    heading_level = 'h%s' % str(level)
    return '\n<%s>%s</%s>\n' % (heading_level, text, heading_level)

def posts_list(ordenar_por_fecha=True, ordenar_por_nombre=False):
    '''Retorna una lista con los nombres de archivos con extension .md
    dentro de la cappeta src/posts, por defecto retorna una lista con
    la tupla (nombre_archivo, fecha_subida)'''
    lista_posts = []
    lp = []
    if ordenar_por_nombre:
            ow = os.walk("monomotapa/src/posts")
            p , directorios , archs = ow.__next__()
        except OSError:
            print ("[posts] - Error: Cant' os.walk() on monomotapa/src/posts except OSError")
            for arch in archs:
                if arch.endswith(".md") and not arch.startswith("#") \
                   and not arch.startswith("~") and not arch.startswith("."):
        return lista_posts

    if ordenar_por_fecha:
            ow = os.walk("monomotapa/src/posts")
        except OSError:
            print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
            for f in files:
                nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
                if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
                    secs_modificacion = SecsModificacionPostDesdeJson(f, json_pattrs)
                    ultima_modificacion = os.path.getmtime(nombre_con_ruta)
                    lp.append((secs_modificacion, ultima_modificacion, f))
            # colocando fecha en formato
            for tupla in lp:
                #fecha = strftime("a, %d %b %Y %H:%M:%S", ctime(tupla[0]))
                cfecha = ctime(tupla[1])
                #fecha = strptime("%a %b %d %H:%M:%S %Y", cfecha)
                lista_posts.append((cfecha, tupla[2]))

        return lista_posts

def categorias_list(categoria=None):
    """ Rotorna una lista con los nombres de posts y el numero de posts que
    pertenecen a la categoria dada o a cada categoria. 
    Las categorias se obtienen analizando la primera linea de cada archivo .md
    an la carpeta donde se almacenan los posts.
    Si no se especifica `categoria' cada elemento de la lista devuelta es:
    (nombre_categoria, numero_posts, [nombres_posts])

    si se especifica `categoria' cada elemento de la lista devuelta es:
    (numero_posts, [nombres_posts]

    lista_posts = posts_list(ordenar_por_nombre=True)
    lista_categorias = []

    if categoria is not None:
        c = 0
        posts = []
        for post in lista_posts:
            nombre_arch = "monomotapa/src/posts/"+post
            with open(nombre_arch, 'r') as file:
                linea = file.readline().decode("utf-8")
                lc = linea.split("[#")[1:]
                for cad in lc:
                    cat = cad.split("]")[0]
                    if cat == categoria:
                        c += 1
        lista_categorias = (c, posts)
        return lista_categorias

    dic_categorias = {}
    for post in lista_posts:
        nombre_arch = "monomotapa/src/posts/"+post
        with open(nombre_arch, 'r') as fil:
            linea = fil.readline().decode('utf-8') # primera linea
            # extrayendo las categorias y registrando sus ocurrencias
            # ejemplo: catgorías: [#reflexión](categoria/reflexion) [#navidad](categoria/navidad)
            # extrae: [reflexion,navidad]
            lc = linea.split("[#")[1:]
            for cad in lc:
                cat = cad.split("]")[0]
                if cat not in dic_categorias:
                    dic_categorias[cat] = (1,[post]) # nuevo registro por categoria
                    tupla = dic_categorias[cat]
                    c = tupla[0] + 1
                    lis = tupla[1]
                    if post not in lis:
                    dic_categorias[cat] = (c, lis)
    # convirtiendo en lista
    for k, v in dic_categorias.iteritems():
    return lista_categorias

def cabeza_post(archivo , max_caracteres=250, categorias=True):
    """ Devuelve las primeras lineas de una archivo de post (en formato markdown)
    con un maximo numero de caracteres excluyendo titulos en la cabeza devuelta.

    Si se especifica `categorias' en True
    Se devuelve una lista de la forma:
    (cabeza_post, categorias)
    donde categorias son cadenas con los nombres de las categorias a la que
    pertenece el post
    cabeza_post = ""
    cats = []
    with open(os.path.join("monomotapa/src/posts",archivo)) as file:
        # analizando si hay titulos al principio
        # Se su pone que la primera linea es de categorias
        for linea in file.readlines():
            linea = linea.decode("utf-8")
            if linea.startswith(u"categorías:") or linea.startswith("categorias"):
                if categorias:
                    cats = categoriasDePost(archivo)
                    #cats = categorias_de_post(archivo)
                # evitando h1, h2
                if linea.startswith("##") or linea.startswith("#"):
                    cabeza_post += " "
                    cabeza_post += linea
            if len(cabeza_post) >= max_caracteres:
        cabeza_post = cabeza_post[0:max_caracteres-1]
    if categorias:
        return (cabeza_post, cats)
    return cabeza_post

def ultima_modificacion_archivo(archivo):
    """ Retorna una cadena indicando la fecha de ultima modificacion del 
    `archivo' dado, se asume que `archivo' esta dentro la carpeta "monomotapa/src"
    Retorna una cadena vacia en caso de no poder abrir `archivo'
        ts = strptime(ctime(os.path.getmtime("monomotapa/src/"+archivo+".md")))
        return strftime("%d %B %Y", ts)
    except OSError:
        return ""

def SecsModificacionPostDesdeJson(archivo, dict_json):
    ''' dado el post con nombre 'archivo' busca en 'dict_json' el
    attribute 'date' y luego obtiene los segundos totales desde
    esa fecha.
    Si no encuentra 'date' para 'archivo' en 'dict.json'
    retorna los segundos totales desde la ultima modificacion
    del archivo de post directamente (usa os.path.getmtime)
    nombre = archivo.split('.md')[0] # no contar extension .md
    nombre_con_ruta = os.path.join("monomotapa/src/posts", archivo)
    date_str = dict_json.get('posts/'+nombre, {}).\
    if date_str == '':
        # el post no tiene "date" en pages.json
        return os.path.getmtime(nombre_con_ruta)
        time_struct = strptime(date_str, '%Y-%m-%d')
        dt = datetime.datetime.fromtimestamp(mktime(time_struct))
        return (dt - datetime.datetime(1970,1,1)).total_seconds()
def noticias_recientes(cantidad=11, max_caracteres=250,
                       categoria=None, pagina=0):
    '''Devuelve una lista con hasta `cantidad' de posts mas recientes, 
    un maximo de `max_caracteres' de caracteres del principio del post y
    el numero total de posts encontrados

    Si se proporciona `categoria' devuelve la lista de posts solamente 
    pertenecientes esa categoria.

    Si `pagina' > 0 se devulve hasta `cantidad' numero de posts en el
    rango de [ cantidad*pagina : cantidad*(pagina+1)]

    Cada elemento de la lista devuelta contiene:
    (nombre_post, ultima_modificacion, cabeza_archivo, categorias)
    Al final se retorna: (lista_posts, numero_de_posts)
    lista_posts = []
    lp = []
    num_posts = 0
    posts_en_categoria = []
    if categoria is not None:
        #posts_en_categoria = categorias_list(categoria)[1]
        posts_en_categoria = categoriasList(categoria)[1]
        # categoria especial fotos
        if categoria == "fotos":
            l = []
            for p in posts_en_categoria:
                l.append(p + '.md')
            posts_en_categoria = l
        ow = os.walk("monomotapa/src/posts")
        p,d,files = ow.__next__()
    except OSError:
        print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
        for f in files:
            nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
            if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
                secs_modificacion = SecsModificacionPostDesdeJson(f, json_pattrs)
                ultima_modificacion = os.path.getmtime(nombre_con_ruta)
                previewChars = json_pattrs.get('posts/'+f[:-3], {}).\
                    get('attributes', {}).\
                    get('preview-chars', max_caracteres)

                if categoria is not None:
                    if f in posts_en_categoria:
                        num_posts += 1
                    num_posts += 1
        # seleccionado por paginas
        lp = lp[cantidad*pagina : cantidad*(pagina+1)]

        # colocando fecha en formato
        for tupla in lp:
            cfecha = ctime(tupla[1])
            nombre_post = tupla[3].split(os.sep)[-1]
            previewChars = tupla[2]
            #contenido = cabeza_post(tupla[3], max_caracteres=previewChars)[0]
            #categorias = cabeza_post(tupla[3], max_caracteres=previewChars)[1]
            contenido = cabezaPost(tupla[3], max_caracteres=previewChars)[0]
            categorias = cabezaPost(tupla[3], max_caracteres=previewChars)[1]
            cabeza_archivo = markdown.markdown(escape(contenido))
            lista_posts.append((nombre_post[:-3], cfecha, \
                                cabeza_archivo, categorias))

        return (lista_posts, num_posts)

def noticias_relacionadas(cantidad=5, nombre=None):
    """Retorna una lista con posts relacionadas, es decir que tienen son de las
    mismas categorias que el post con nombre `nombre'.

    Cada elemento de la lista de posts contiene el nombre del post
    #categorias = categorias_de_post(nombre) ## TODO: corregir categorias de post
    categorias = categoriasDePost(nombre)
    numero = 0
    if categorias is None:
        return None
    posts = []
    for categoria in categorias:
        #lista = categorias_list(categoria)[1] # nombres de posts
        lista = categoriasList(categoria)[1]
        numero += len(lista)
        for nombre_post in lista:
            if nombre_post + '.md' != nombre:
        if numero >= cantidad:
            return posts
    return posts

def rss_ultimos_posts_jinja(cantidad=15):
    """Retorna una lista de los ultimos posts preparados para 
    ser renderizados (usando jinja) como un feed rss

    Examina cada post del mas reciente al menos reciente, en
    total `cantidad' posts. Por cada post devuelve:
    id: id which identifies the entry using a
    universally unique and permanent URI
    author: Get or set autor data. An author element is a dict containing a
    name, an email adress and a uri.

    category:  A categories has the following fields:
    - *term* identifies the category
    - *scheme* identifies the categorization scheme via a URI.
    - *label* provides a human-readable label for display

    comments: Get or set the the value of comments which is the url of the
    comments page for the item.
    content: Get or set the cntent of the entry which contains or links to the
    complete content of the entry.
    description(no contiene): Get or set the description value which is the item synopsis.
    Description is an RSS only element.
    link: Get or set link data. An link element is a dict with the fields
    href, rel, type, hreflang, title, and length. Href is mandatory for

    pubdate(no contiene): Get or set the pubDate of the entry which indicates when the entry
    was published.

    title: the title value of the entry. It should contain a human
    readable title for the entry.

    updated: the updated value which indicates the last time the entry
    was modified in a significant way.
    lista_posts = []
    lp = []
    num_posts = 0
        ow = os.walk("monomotapa/src/posts")
    except OSError:
        print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
        for f in files:
            nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
            if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
                lp.append((os.path.getmtime(nombre_con_ruta), f))
                num_posts += 1
            if num_posts > cantidad:
        # colocando fecha en formato
        for tupla in lp:
            nombre_post = tupla[1].split(os.sep)[-1]
            #contenido = cabeza_post(tupla[1], max_caracteres=149999)
            contenido = cabezaPost(tupla[1], max_caracteres=149999)
            id_post = ""+nombre_post[:-3]
            #categorias = categorias_de_post(nombre_post)
            categorias = categoriasDePost(nombre_post)
            dict_categorias = {}
            c = ""
            for cat in categorias:
                c += cat + " "
            dict_categorias['label'] = c
            #dict_categorias['term'] = c
            html =  markdown.markdown(escape(contenido), ['codehilite'])
            link = id_post
            pubdate = ctime(tupla[0])
            title = titulo_legible(nombre_post[:-3]) # no incluir '.md'
            updated = pubdate
            dict_feed_post = {
                "author": "Rodrigo Garcia",
                "category" : categorias,
                "content": html,
                "link" : id_post,
                "updated" : updated,
                "title": title
    return lista_posts

###### Define routes

def page_not_found(e):
    """ provides basic 404 page"""
    defaults = get_page_attributes('defaults.json')
        css = defaults['css']
    except KeyError:
        css = None
    pages = get_page_attributes('pages.json')
    if '404' in pages:
        if'css' in pages['404']:
            css = pages['404']['css']
    return render_template('static.html', 
            title = "404::page not found", heading = "Page Not Found", 
            navigation = top_navigation('404'),
            css = css,
            contents = Markup(
                "This page is not there, try somewhere else.")), 404

@app.route('/users/', defaults={'page': 1})

@app.route("/", defaults={'pagina':0})
def index(pagina):
    """provides index page"""
    index_page = Page('index')
    lista_posts_recientes, total_posts = noticias_recientes(pagina=pagina)
    index_page.contexto['lista_posts_recientes'] = lista_posts_recientes
    index_page.contexto['total_posts'] = total_posts
    index_page.contexto['pagina_actual'] = int(pagina)
    return index_page.generate_page()

# default route is it doe not exist elsewhere
def staticpage(page):
    """ display a static page rendered from markdown in src
    i.e. displays /page or /page/ as long as src/ exists.
    srcfile, title and heading may be set in the pages global 
    (ordered) dictionary but are not required"""
    static_page = Page(page)
    return static_page.generate_page()

def rposts(page):
    """ Mustra las paginas dentro la carpeta posts, no es coincidencia 
    que en este ultimo directorio se guarden los posts.
    Ademas incrusta en el diccionario de contexto de la pagina la 
    fecha de ultima modificacion del post
    static_page = Page("posts/"+page)
    ultima_modificacion = ultima_modificacion_archivo("posts/"+page)
    static_page.contexto['relacionadas'] = noticias_relacionadas(nombre=page+".md")
    static_page.contexto['ultima_modificacion'] = ultima_modificacion
    return static_page.generate_page()

def indice_posts():
    """ Muestra una lista de todos los posts
    lista_posts_fecha = posts_list()
    #lista_posts_categoria = categorias_list()
    lista_posts_categoria = categoriasList()
    static_page = Page("posts")
    static_page.contexto['lista_posts_fecha'] = lista_posts_fecha
    static_page.contexto['lista_posts_categoria'] = lista_posts_categoria
    return static_page.generate_page()

def lista_categorias():
    """ Muestra una lista de las categorias , los posts pertenecen
    a cada una y un conteo"""
    #lista_categorias = categorias_list()
    lista_categorias = categoriasList()
    static_page = Page("categorias")
    static_page.contexto['lista_posts_categoria'] = lista_categorias
    #return (str(lista_categorias))
    return static_page.generate_page()

def posts_de_categoria(categoria):
    """ Muestra los posts que perteneces a la categoria dada
    lista_posts = []

    if categoria == "fotos": # caegoria especial fotos
        lista_posts, total_posts = noticias_recientes(max_caracteres=1250,categoria=categoria)
        static_page = Page("fotos")
        static_page.contexto['categoria_actual'] = categoria
        static_page.contexto['lista_posts_recientes'] = lista_posts
        return static_page.generate_page()

    #lista_posts = categorias_list(categoria=categoria)
    lista_posts = categoriasList(categoria=categoria)
    static_page = Page("categorias")
    static_page.contexto['categoria_actual'] = categoria
    static_page.contexto['lista_posts_categoria'] = lista_posts
    return static_page.generate_page()

@app.route("/posts/recientes", defaults={'pagina':0})
def posts_recientes(pagina):
    """ muestra una lista de los posts mas recientes
    TODO: terminar
    lista_posts, total_posts = noticias_recientes(max_caracteres=368,

    static_page = Page("recientes")
    static_page.contexto['lista_posts_recientes'] = lista_posts
    static_page.contexto['total_posts'] = total_posts
    static_page.contexto['pagina_actual'] = pagina
    #return (str(lista_posts))
    return static_page.generate_page()

@app.route("/contacto", methods=['GET'])
def contacto():
    tupla_captcha = captcha_pregunta_opciones_random()
    if tupla_captcha is None:
        return ("<br>Parece un error interno!</br>")
    pregunta = tupla_captcha[0]
    opciones = tupla_captcha[1]
    static_page = Page("contacto")
    static_page.contexto['pregunta'] = pregunta
    static_page.contexto['opciones'] = opciones
    return static_page.generate_page()

@app.route("/contactoe", methods=['POST'])
def comprobar_mensaje():
    """ Comprueba que el mensaje enviado por la caja de texto sea valido
    y si lo es, guarda un archivo de texto con los detalles"""
    errors = []
    if request.method == "POST":
        # comprobando validez
        nombre = request.form["nombre"]
        dir_respuesta = request.form['dir_respuesta']
        mensaje = request.form['mensaje']

        pregunta = request.form['pregunta']
        respuesta = request.form['respuesta']

        if len(mensaje) < 2 or mensaje.startswith("   "):
            errors.append("Mensaje invalido")
        if not captcha_comprobar_respuesta(pregunta, respuesta):
            errors.append("Captcha invalido")

        if len(errors) > 0:
            return str(errors)
        # guardando texto
        texto = "Remitente: "+nombre
        texto += "\nResponder_a: "+dir_respuesta
        texto += "\n--- mensaje ---\n"
        texto += mensaje
        # TODO: cambiar a direccion especificada en archivo de configuracion
        dt =
        nombre = "m_"+str("_"+str(dt.month)+\
        with open(os.path.join("fbs",nombre), "wb") as f:
        return redirect("/mensaje_enviado", code=302)

def mensaje_enviado():
    static_page = Page("mensaje_enviado")
    return static_page.generate_page()

def rss_feed():
    """Genera la cadena rss con las 15 ultimas noticias del sitio
    TODO: Agregar mecenismo para no generar los rss feeds y solo
    devolver el archivo rss.xml generado anteriormente. Esto
    quiere decir solamente generar el rss_feed cuando se haya hecho
    un actualizacion en los posts mas reciente que la ultima vez
    que se genero el rss_feed
    #return str(rss_ultimos_posts_jinja())
    return render_template("rss.html", 
                           contents = rss_ultimos_posts_jinja())

##### specialized pages
def source():
    """Display source files used to render a page"""
    source_page = Page('source', title = "view the source code", 
            #heading = "Ver el código fuente",
                       heading = "Ver el codigo fuente",
            internal_css = get_pygments_css())
    page = request.args.get('page')
    # get source for markdown if any. 404's for non-existant markdown
    # unless special page eg source
    pagesrc = source_page.get_page_src(page, 'src', 'md')
    special_pages = ['source', 'unit-tests', '404']
    if not page in special_pages and pagesrc is None:
    # set enable_unit_tests  to true  in config.json to allow 
    #  unit tests to be run  through the source page
    if app.config['enable_unit_tests']:
        contents = '''<p><a href="/unit-tests" class="button">Run unit tests
        # render if needed
        if page == 'unit-tests':
            contents += heading('', 2)
            contents += render_pygments('', 'python')
        contents = ''
    # render
    contents += heading('', 2)
    contents += render_pygments(source_page.get_page_src(''), 
    # render markdown if present
    if pagesrc:
        contents += heading(os.path.basename(pagesrc), 2)
        contents += render_pygments(pagesrc, 'markdown')
    # render jinja templates
    contents += heading('base.html', 2)
    contents += render_pygments(
        source_page.get_page_src('base.html', 'templates'), 'html')
    template = source_page.get_template(page)
    contents += heading(template, 2)
    contents += render_pygments(
        source_page.get_page_src(template, 'templates'), 'html')
    return source_page.generate_page(contents)

def unit_tests():
    """display results of unit tests"""
    unittests = Page('unit-tests', heading = "Test Results", 
            internal_css = get_pygments_css())
    # exec unit tests in subprocess, capturing stderr
    capture = subprocess.Popen(["python", ""], 
            stdout = subprocess.PIPE, stderr = subprocess.PIPE)
    output = capture.communicate()
    results = output[1]
    contents = '''<p>
    <a href="/unit-tests" class="button">Run unit tests</a>
    <div class="output" style="background-color:'''
    if 'OK' in results:
        color = "#ddffdd"
        result = "TESTS PASSED"
        color = "#ffaaaa"
        result = "TESTS FAILING"
    contents += ('''%s">\n<strong>%s</strong>\n<pre>%s</pre>\n</div>\n'''
            % (color, result, results))
    # render 
    contents += heading('', 2)
    contents += render_pygments('', 'python')
    return unittests.generate_page(contents)

# Navidad 2019 #

Continuando con las reflexiones de navidad y resumiendo un poco el 2019 desde mi percepción.


Fue un año especial de cambios, probablemente perpetrado por "cúmulos" de cosas que han encontrado la salida y se han manifestado a lo largo de este año.

## Acumulando

Desde algunos meses comenzó a crecer el descontento con acciones del gobierno boliviano, como una acumulación de malestares y temas que la gente veía como pendientes. Por ejemplo las políticas depredadoras al medio ambiente justificadas con "desarrollo" económico agropecuario que el gobierno a la cabeza de Evo Morales fue promoviendo. Al parecer cediendo poco a poco ante las exigencias del sector ganadero y empresarial de Santa Cruz.

Estas políticas desencadenaron en un desastre ambiental, un incendio masivo del bosque seco de la Chiquitania Boliviana, una gran pérdida de flora y fauna de este ecosistema. Todo empezó poco después de aprobado el Decreto Supremo 3973 (9 de julio 2019, que modificó el Art 5 del DS 26075 del 2001) y que autoriza el desmonte y las quemas controladas inclusive en tierras de producción forestal permanente (TPFP). Este DS si bien es reciente, con seguridad ha funcionado como un “gatillo” para el desmonte, chaqueo y quemas en la Chiquitania. [ [1] ](

También se fue acumulando el descontento desde el 21 de Febrero de 2016, cuando Evo Morales perdío la consulta popular para modificar la constitución política del estado y viabilizar un nuevo mandato. Sin embargo, uso un recurso internacional para repostularse por ser un "derecho humano".

Adicionalmente una serie hechos de corrupción, mal uso de bienes del estado y falta de cumplimiento a los compromisos en los casi 14 años de mandato, fueron el combustible necesario acumulado para la explosión final.

### ¿Fraude?

Tras las elecciones del 20 de Octubre de 2019 que como resultado señalaron al partido del MAS como ganador en primera vuelta (por un poco más del 10% de diferencia) con el segundo partido Comunidad Ciudadana (CC), empezarón serias denuncias de fraude electoral por parte del MAS.

Se desplegó un sistema de conteo rápido en vivo (TREP) que publicaba los resultados del conteo rápido. El conteo rápido tenía una tendencia de subida para el MAS, pero en algún momento se corto la transmisión en vivo del TREP y surgieron las denuncias de fraude indicando que durante el corte en la transmisión del TREP se modificó la Base de datos del conteo de elecciones y se "ajustaron" los resultados para que el ganador sea el MAS en primera vuelta.

Al volver el TREP (que no es vinculante sino solamente un conteo rápido referencial) y con los resultados finales del cómputo oficial se declaró ganador al MAS con un poco más de la diferencia mínima para ganar en primera vuelta. 

La población muy molesta salió a las calles a reclamar primero anulación de las elecciones, ante la según mi opinión soberbia negativa del entonces presidente Evo Morales de auditar el proceso, el reclamo se fue convirtiendo rápidamente en la renuncia de Evo Morales. El presidente trató de calmar los ánimos convocando a una auditoría de la OEA, pero el descontento ya era tan grande que el país se convulsionó con un paro indefinido y gente saliendo a las calles a exigir respeto a la democracia.

A mi parecer las discrepancias entre el TREP y el cómputo oficial no deberían ser razones suficientes para declarar un fraude y tampoco solo el hecho de mostrar algunas actas mal contadas o modificadas a propósito para dar ventaja al MAS, siempre y cuando sea un número pequeño no se podría considerar un fraude. Sin embargo, el proceso electoral fue llevado a cabo de manera muy como le decimos los informáticos "parchada", sin las características necesarias como para garantizar resultados los suficientemente confiables para la población.

Hubieron muchas denuncias y supuestos de fraude pero pocas "pruebas irrefutables" de un fraude masivo según percibí, pero la histeria colectiva y el descontento acumulado ya era tan grande que razonar objetivamente en esas circunstancias estaba casi fuera de alcance.

Note que muchos actores políticos nacionales y extranjeros aprovecharon para ganar protagonismo y hacerse con el poder y aceptación popular, tal y como se hizo en Brasil con Bolsonaro y sus mensajes de odio. Vi muchas noticias falsas en redes sociales, periódicos desinformando, ocultando información y una serie de técnicas que terminaron obligando a Evo Morales a renunciar y a un gobierno "transitorio" a aprovechar y tomar el poder.

El discurso de democracia se ha usado mucho, pero la democracia no se trata solo de "escoger" entre una serie de representantes que se presentan y — dejar que ellos hagan el trabajo de gobernar(nos). Democracia es algo más grande y valioso, es el gobierno del pueblo y para defenderla es mejor construir y participar activamente en decisiones importantes y propuestas para el futuro.

## ¿Navidad en 2019?

En esta navidad he visto mucho de la anterior, muchas personas con una navidad feliz y otras en la cama de un hospital con la esperanza de recuperarse y pronto ir a trabajar o volver a casa para conocer a sus hijos recién nacidos o reconstruir lazos y continuar proyectos personales y colectivos.

Pero todos estos acontecimientos me han hecho dar cuenta que la acción es importante el 2020, ya construiremos mejores navidades reforestando y compartiendo más con gente que más lo necesita, acompañando a quién se sienta sol@ y descubriendo nuevas y valiosas cosas en este mundo enorme.

[<--tamaño original](/static/imgs/posts/fotos/planta_de_cerca6.jpg)

1. [](


<!DOCTYPE html>

Monomotapa - A Micro CMS
Copyright (C) 2014, Paul Munday.

PO Box 28228, Portland, OR, USA 97228
paul at

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero  Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <>.

There should also be a copy of the AGPL in src/ that should be
accessible by going to <a href ="/license">/license<a> on this site.
<title>{% if title -%}{{title}}{% endif %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="lang" content="es"/>
<meta name="author" content="Rodrigo Garcia Saenz"/>
<meta property="article:publisher" content=""/>
<meta property="og:site_name" content="Sitio personal de Rodrigo Garcia Saenz"/>
<!-- meta datos dinamicos -->
{%- for metadato in meta -%}
  <meta {{metadato.tag}}="{{}}" content="{{metadato.content}}"/>
{%- endfor -%}
<!-- -->
<link rel="stylesheet" type="text/css" title="oscuro" href="/static/style_rmgss.css">
<link rel="alternate stylesheet" type="text/css" title="claro" href="/static/style_rmgss_claro.css">

<!-- {%- if css -%} -->
<!-- {%- for file in css %} -->
<!--     <link href="{{ url_for('static', filename=file) }}" rel="stylesheet" type="text/css"  /> -->
<!--     {%- endfor -%} -->
<!-- {%- endif %} -->
{% if internal_css %}
  <style type="text/css">
{% endif %}
{%- if hlinks -%}
  {%- for item in hlinks -%}
       {%- if item.href %} href="{{item.href}}"{% endif -%}
       {%- if item.rel %} rel="{{item.rel}}"{% endif -%} 
       {%- if item.type %} type="{{item.type}}"{% endif -%}
       {%- if %} type="{{}}"{% endif -%}
       {%- if item.hreflang %} type="{{item.hreflang}}"{% endif -%}
       {%- if item.charset %} type="{{item.charset}}"{% endif -%}
     {% endfor %}
   {%- endif -%}

<link rel="apple-touch-icon" sizes="76x76" href="{{ url_for('static', filename='imgs/favicon/apple-touch-icon.png')}}">

<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='imgs/favicon/favicon-32x32.png')}}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='imgs/favicon/favicon-16x16.png')}}">
<link rel="manifest" href="{{ url_for('static', filename='imgs/favicon/site.webmanifest')}}">
<link rel="mask-icon" href="{{ url_for('static', filename='imgs/favicon/safari-pinned-tab.svg')}}" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">

{# <link rel="shortcut icon" type="image/png" href="/{{ url_for('static', filename='imgs/favicon.png') }}> #}

<body onload="set_style_from_cookie()">

<div id="wrap">
    <div class="row">
	<div class="col fifth">
	    <!-- Importante dejar este -->
	<!-- Contenido -->
	<div class="col fill">
	    <!-- <button type="button" -->
	    <!--   onclick="document.getElementById('nav_right').style.color = 'red'"> -->
	    <!--   Click Me!</button> -->
	    <!-- Cabecera -->
		    <a href="/">
			<img src="/static/imgs/cabecera1.png">
		    <span style="font-size:11px;">
			<q><b>sitio personal</b> de Rodrigo Garcia.</q>

	    <div style="margin-bottom: 7px">
		    <input type="submit" onclick="switch_style('oscuro'); return false;" name="theme" value="☻" id="oscuro" style="background-color: #333933; color: #C3C4C2; border-radius:3px;">
		    <input type="submit" onclick="switch_style('claro');return false;" name="theme" value="☺" id="claro" style="background-color: #D1E9D3; color: #131914; border-radius:3px;">
	    <!-- Contenido -->
	    {% block content %}{% endblock %}


	<div class="col fifth">
	    <div id="nav_right">
			<a href="/">
			    <img src="/static/imgs/inicio.png" width="32" heigth="32">
			<a href="/posts">
			    <img src="/static/imgs/entradas.png" width="32" heigth="32">
				<a href="/posts/categorias">
				    <img src="/static/imgs/categorias.png" width="16" heigth="16">
				<a href="/posts/recientes">
				    <img src="/static/imgs/reloj1.png" width="16" heigth="16">
			<img src="/static/imgs/misc.png" width="32" heigth="32">
				<a href="">
				    <img src="/static/imgs/botadero.png" width="16" heigth="16">
				    Almacén de archivos
			    <!-- <dd>
				 <a href="/repositorios">
				 <img src="/static/imgs/repos.png" width="16" heigth="16">
				 </dd> -->

				<a href="/posts/categoria/fotos">
				    <img src="/static/imgs/fotos.png" width="16" heigth="16">

				<a href="/posts/categoria/software">
				    <img src="/static/imgs/sf.png" width="16" heigth="16">
				<a href="/posts/categoria/proyectos">
				    <img src="/static/imgs/proyectos.png" width="16" heigth="16">
				<a href="/acerca_de_mi">
				    <img src="/static/imgs/acercade.png" width="16" heigth="16">
				    Acerca de mi



			<a href="/contacto">
			    <img src="/static/imgs/contacto.png" width="16" heigth="16">
		    {%- for item in navigation.navigation.values() -%}
			<li><a href="
				     {%- if item.url -%}{{item.url}}
				     {%- elif item.urlfor -%}
				     {%- if item.urlfor == "source" -%}
				     {{ url_for(item.urlfor, }}
				     {%- else -%}
				     {{ url_for(item.urlfor) }}
				     {%- endif -%}
				     {%- else -%}
				     {{ url_for('staticpage', page=item.base) }}
				     {%- endif -%}
				     {%- if item.rel -%}
				     " rel="{{item.rel}} 
				     {%- endif -%}
		    {% endfor -%}
		    <a href="/rss" >
			<img src="/static/imgs/rss.png" width="24" heigth="24">

	    <!-- Noticias Realcionadas si es un post -->
	    {% if contexto is defined %}
		{% if contexto['relacionadas'] is defined %}
		    <h3>Posts/Noticias relacionadas</h3>
		    {% for post_relacionado in contexto['relacionadas'] %}
			    <a href="/posts/{{ post_relacionado }}">
				{{ post_relacionado|n_heading }}

		    {% endfor %}
		{% endif %}
	    {% endif %}

	    <div id="consejo_del_dia">
		<b>Consejo del día</b>
		{% if contexto %}
		  {{ contexto['consejo']|safe }}
		{% endif %}


    </div> <!-- row -->


<div id="footer">
    <p id="footer">
	Este sitio web es software libre aquí el <a href="">código fuente</a>.<br>
	El contenido de este sitio esta bajo una licencia Creative Commons <a href="">Attribution 4.0 International (CC BY 4.0)</a> a menos que se indique lo contrario.
	<!-- footer goes here -->
	{% if footer %}
	{% endif %}


<!-- javascript -->
<script language="javascript">
 // *** TO BE CUSTOMISED ***

 var style_cookie_name = "style" ;
 var style_cookie_duration = 15 ;
 var style_domain = "" ;

 // You do not need to customise anything below this line

 function switch_style ( css_title )
     // You may use this script on your site free of charge provided
     // you do not remove this notice or the URL below. Script from
     var i, link_tag ;
     for (i = 0, link_tag = document.getElementsByTagName("link") ;
	 i < link_tag.length ; 
	 i++ ) {
	 if ((link_tag[i].rel.indexOf( "stylesheet" ) != -1) &&
	     link_tag[i].title) {
	     link_tag[i].disabled = true ;
	     if (link_tag[i].title == css_title) {
		 link_tag[i].disabled = false ;
	 set_cookie( style_cookie_name, css_title,
		     style_cookie_duration, style_domain );
 function set_style_from_cookie()
     var css_title = get_cookie( style_cookie_name );
     if (css_title.length) {
	 switch_style( css_title );
 function set_cookie ( cookie_name, cookie_value,
		       lifespan_in_days, valid_domain )
     var domain_string = valid_domain ?
			 ("; domain=" + valid_domain) : '' ;
     document.cookie = cookie_name +
                       "=" + encodeURIComponent( cookie_value ) +
                       "; max-age=" + 60 * 60 *
     24 * lifespan_in_days +
                       "; path=/" + domain_string ;
 function get_cookie ( cookie_name )
     var cookie_string = document.cookie ;
     if (cookie_string.length != 0) {
         var cookie_value = cookie_string.match (
             '(^|;)[\s]*' +
             cookie_name +
             '=([^;]*)' );
         return decodeURIComponent ( cookie_value[2] ) ;
     return '' ;
<!-- --->



{% extends "base.html" %}
{% block content %}
  <article class="h-entry">
      {% if heading %}<h1 class="p-name">{{heading}}</h1>{% endif %}
    {# <hr> #}
    {# {{ meta }} #}
    {# <hr> #}
    {% if attributes %}
      <div id="attributes">
	<p id="post-details">
	  {% if attributes['date'] %}
            <time class="dt-published" datetime="{{attributes['date']}}">
	      {% if attributes['date-text'] %} 
	      {% else %}
	      {% endif %}
          {% endif %}
	  {% if attributes['author'] %}
	    <span class="p-author">{{attributes['author']}}</span>
	  {% endif %}
	  <a class="u-url" href="/{{name}}">permalink.</a>
	{% if attributes['summary'] %}
	  <p class="p-summary">{{attributes['summary']}}</p>
	{% endif %}
    {% endif %}
    <div class='e-content'> 

      <div align="right">
	{% if contexto['ultima_modificacion'] %}
	  <small>Actualizado - {{ contexto['ultima_modificacion'] }}</small>
	{% endif %}

      {% if categorias %} 
      	<div class="categorias"> 
      	{% for cat in categorias %}
      	  <a href="/posts/categoria/{{ cat }}">#{{ cat }}</a>
      	{% endfor %} 
      {% endif %} 
{% endblock %}
Consejo del día

Trata de comprar artículos de tiendas o puestos de mercado familiares, esto por que cuando lo haces apoyas directamente a familias o personas individuales que luchan por subsistir.

En cambio cuando compras de supermercados o cadenas de tiendas grandes, estás dando ganacias adicionales a empresarios o empresarias grandes principalmente.