sitio personal de Rodrigo Garcia Saenz.

Consejo del día

Si tienes la posiblidad, haz donaciones a organizaciones sin ánimo de lucro para apoyar su trabajo.

Muchas de estas organizaciones se mantienen gracias a la gente que les apoya con donaciones monetarias.

Ver el codigo fuente


views.py

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

PO Box 28228, Portland, OR, USA 97228
paul at paulmunday.net

Modificado por: Rodrigo Garcia 2017 https://rmgs.com.bo/contacto

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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
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 <http://www.gnu.org/licenses/>.


Monomotapa:
    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, request #, make_response
from flask import redirect
from markupsafe import Markup, escape
from werkzeug.utils 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
import traceback
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"""
    pass

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 page.md 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)"""
    try:
        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]
    else:
        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]
            nav.update(elements)
        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.name, 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
        self.page = page.rstrip('/')
        self.defaults = get_page_attributes('defaults.json')
        self.pages = get_page_attributes('pages.json')
        self.url_base = self.defaults['url_base']
        title = titulo_legible(page.lower())
        heading = titulo_legible(page.capitalize())
        self.categorias = categoriasDePost(self.page)
        self.exclude_toc = True
        try:
            self.default_template = self.defaults['template']
        except KeyError:
            raise ConfigError('template not found in default.json')
        # will become self.name, self.title, self.heading, 
        # self.footer, self.internal_css, self.trusted
        attributes = {'name' : self.page, 'title' : title,
                      'navigation' : top_navigation(self.page),
                      '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
        attributes.update(self.defaults)
        # override with kwargs
        attributes.update(kwargs)
        # override attributes if set in pages.json
        if page in self.pages:
            attributes.update(self.pages[page])
            # set attributes (as self.name etc)  using indirection
        for attribute, value in attributes.items():
            # print('attribute', attribute, '=-==>', value)
            setattr(self, attribute, value)
        # meta tags
        try:
            self.pages[self.page]['title'] = attributes['title']
            self.pages[self.page]['url_base'] = self.url_base
            metaTags = metaTagsAutomaticos(self.page, self.pages.get(self.page, {}))
            self.meta = metaTags
            # for key, value in self.pages[self.page].items():
            #     print(' ', key, ' = ', value)
        except Exception as e:
            tb = traceback.format_exc()
            print('Error assigning meta:', str(e), '\n', str(tb))

        # 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
        self.title = self.title + app.config.get('default_title', '')

    def _get_markdown(self):
        """returns rendered markdown or 404 if source does not exist"""
        src = self.get_page_src(self.page, 'src', 'md') 
        if src is None:
            abort(404)
        else:
            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)
        else:
            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
        else:
            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"""
        toc = '' # table of contents
        if not contents:
            contents, toc = self._get_markdown()
        # print('////', toc)
        template = self.get_template(self.page)
        # print('......................')
        # def mos(**kwargs):
        #     for k in kwargs:
        #         print(k, end=',')
        # mos(**vars(self))
        return render_template(template, 
                contents = Markup(contents),
                toc=toc,
                **vars(self))

# 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)
    else:
        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
    else:
        return '.%s' % ext

def render_markdown(srcfile, trusted=False):
    """ Returns markdown file rendered as html and the table of contents 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."""
    try:
        with open(srcfile, 'r') as f:
            src = f.read()
            md = markdown.Markdown(extensions=['toc', 'codehilite'])
            md.convert(src)
            toc = md.toc
            if trusted == True:
                content =  markdown.markdown(src,
                                             extensions=['codehilite',
                                                         TocExtension(permalink=True)])
            else:
                content = markdown.markdown(escape(src),
                                            extensions=['codehilite',
                                                        TocExtension(permalink=True)])
            return content, toc
    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 = f.read()
            contents = highlight(src, PythonLexer(), HtmlFormatter())
    elif lexer_type == 'html':
        with open(srcfile, 'r') as f:
            src = f.read()
            contents = highlight(src, HtmlDjangoLexer(), HtmlFormatter())
    # default to TextLexer for everything else
    else:
        with open(srcfile, 'r') as f:
            src = f.read()
            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:
        try:
            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")
        else:
            for arch in archs:
                if arch.endswith(".md") and not arch.startswith("#") \
                   and not arch.startswith("~") and not arch.startswith("."):
                    lista_posts.append(arch)
        lista_posts.sort()
        return lista_posts

    if ordenar_por_fecha:
        try:
            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.")
        else:
            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))
            lp.sort()
            lp.reverse()
            # 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
                        posts.append(post)
        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
                else:
                    tupla = dic_categorias[cat]
                    c = tupla[0] + 1
                    lis = tupla[1]
                    if post not in lis:
                        lis.append(post)
                    dic_categorias[cat] = (c, lis)
    # convirtiendo en lista
    for k, v in dic_categorias.iteritems():
        lista_categorias.append((k,v[0],v[1]))
    lista_categorias.sort()
    lista_categorias.reverse()
    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)
            else:
                # evitando h1, h2
                if linea.startswith("##") or linea.startswith("#"):
                    cabeza_post += " "
                else:
                    cabeza_post += linea
            if len(cabeza_post) >= max_caracteres:
                break
        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'
    """
    try:
        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, {}).\
      get('attributes',{}).\
      get('date','')
    if date_str == '':
        # el post no tiene "date" en pages.json
        return os.path.getmtime(nombre_con_ruta)
    else:
        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
    try:
        ow = os.walk("monomotapa/src/posts")
        p,d,files = ow.__next__()
        #p,d,files=ow.next()
    except OSError:
        print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
    else:
        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:
                        lp.append((secs_modificacion,
                                   ultima_modificacion,
                                   previewChars,
                                   f))
                        num_posts += 1
                else:
                    lp.append((secs_modificacion,
                               ultima_modificacion,
                               previewChars,
                               f))
                    num_posts += 1
        lp.sort()
        lp.reverse()
        # 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:
                posts.append(nombre_post)
        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
    ATOM.

    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
    
    try:
        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.")
    else:
        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:
                break
        lp.sort()
        lp.reverse()
        # 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 = "https://rmgss.net/posts/"+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))
            link = id_post
            pubdate = ctime(tupla[0])
            title = titulo_legible(nombre_post[:-3]) # no incluir '.md'
            updated = pubdate
            
            dict_feed_post = {
                "id":id_post,
                "author": "Rodrigo Garcia",
                "category" : categorias,
                "content": html,
                "link" : id_post,
                "updated" : updated,
                "title": title
                }
            lista_posts.append(dict_feed_post)
    return lista_posts

###### Define routes

@app.errorhandler(404)
def page_not_found(e):
    """ provides basic 404 page"""
    defaults = get_page_attributes('defaults.json')
    try:
        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('/users/page/<int:page>')

@app.route("/", defaults={'pagina':0})
@app.route('/<int:pagina>')
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
@app.route("/<path:page>")
def staticpage(page):
    """ display a static page rendered from markdown in src
    i.e. displays /page or /page/ as long as src/page.md 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()

@app.route("/posts/<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
    static_page.exclude_toc = False # no excluir Índice de contenidos
    return static_page.generate_page()

@app.route("/posts")
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()

@app.route("/posts/categorias")
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()

@app.route("/posts/categoria/<categoria>")
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})
@app.route("/posts/recientes/<int:pagina>")
def posts_recientes(pagina):
    """ muestra una lista de los posts mas recientes
    TODO: terminar
    """
    lista_posts, total_posts = noticias_recientes(max_caracteres=368,
                                                  pagina=pagina)

    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 = datetime.datetime.now()
        nombre = "m_"+str(dt.day)+"_"+str(dt.month)+\
                 "_"+str(dt.year)+"-"+str(dt.hour)+\
                 "-"+str(dt.minute)+"-"+str(dt.second)
        with open(os.path.join("fbs",nombre), "wb") as f:
            f.write(texto.encode("utf-8"))
        return redirect("/mensaje_enviado", code=302)

@app.route("/mensaje_enviado")
def mensaje_enviado():
    static_page = Page("mensaje_enviado")
    return static_page.generate_page()

@app.route("/rss")
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())
    #**vars(self)
    #)

##### specialized pages
@app.route("/source")
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:
        abort(404)
    # 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
    </a></p>'''
        # render tests.py if needed
        if page == 'unit-tests':
            contents += heading('tests.py', 2)
            contents += render_pygments('tests.py', 'python')
    else:
        contents = ''
    # render views.py
    contents += heading('views.py', 2)
    contents += render_pygments(source_page.get_page_src('views.py'), 
            'python')
    # 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)

# @app.route("/unit-tests")
# 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", "tests.py"], 
#             stdout = subprocess.PIPE, stderr = subprocess.PIPE)
#     output = capture.communicate()
#     results = output[1]
#     contents = '''<p>
#     <a href="/unit-tests" class="button">Run unit tests</a>
#     </p><br>\n
#     <div class="output" style="background-color:'''
#     if 'OK' in results:
#         color = "#ddffdd"
#         result = "TESTS PASSED"
#     else:
#         color = "#ffaaaa"
#         result = "TESTS FAILING"
#     contents += ('''%s">\n<strong>%s</strong>\n<pre>%s</pre>\n</div>\n'''
#             % (color, result, results))
#     # render test.py 
#     contents += heading('tests.py', 2)
#     contents += render_pygments('tests.py', 'python')
#     return unittests.generate_page(contents)

Opiniones-sobre-ciudadanía-digital-en-Bolivia-a-2025.md

La idea de ciudadanía digital es optimizar los trámites estatales y reducir la burocracia en Bolivia. Se implementaron dos proyectos grandes: Firma Digital (ADSIB) y Ciudadanía digital (AGETIC).

> Esta reseña y opiniones tienen el objetivo de exponer las razones por las que pienso que el proyecto de ciudadanía digital realizado por AGETIC desde 2019 no ha tenido el impacto necesario y deseado. **Todo desde mi punto de vista** habiendo trabajado como programador de software en este proyecto y en la integración con algunas instituciones estatales.

## Reseña sobre ciudadanía digital

Ciudadanía digital tiene el objetivo de permitir a la ciudadanía ejercer derechos a través de medios digitales dándole [validez legal](https://ciudadaniadigital.bo/home/marco-normativo) a los procesos que se realicen usándola. Esto quiere decir que cuando has iniciado sesión en algún portal usando tus credenciales de ciudadanía digital, las acciones que realices ganan cierta validez legal primero **confirmando** que eres tú quien está realizando esas acciones. Apartir de ello existen tres principales servicios de ciudadanía digital:

1. **Proveedor de identidad**: El cimiento principal, un servicio de autenticación que otras plataformas pueden usar para que te identifiques (*te loguees*), como cuando usas tu cuenta de google o facebook para ingresar a sitios externos. La diferencia es que cuando te logueas con ciudadanía digital tus acciones adquieren validez legal reconocida por el estado boliviano.
2. **Notificaciones electrónicas**: En el ámbito legal existen las notificaciones hacia las partes que tradicionalmente se hacen con un aviso en papel firmado por una autoridad judicial. Con ciudadanía digital, cada ciudadano tiene un **buzón de notificaciones electrónicas** donde las autoridades pueden emitir notificaciones electrónicas que se consideran como entregadas en el momento en que estas llegan a su buzón electrónico independientemente de si el ciudadano revisa su buzón o no. Esto puede agilizar los procesos legales evitando que las partes nieguen haber recibido notificación de un proceso. Para asegurar integridad, trazabilidad y no repudio, las notificaciones se guardan en una **cadena de bloques** (*blockchain*) en el estado llamado: "Registro de orden cronológico e integridad de datos".
3. **Aprobación de documentos**: Mediante este servicio una entidad puede enviarle documentos digitales a un ciudadano y que este dé su aprobación con validez legal para continuar con trámites o procedimientos. Este servicio aprovecha la ciudadanía digital y la **cadena de bloques** estatal, de manera que el visto bueno sobre un documento adquiere validez legal (similar a firmarlo) y queda guardado en la cadena de bloques para poder ser verificado en cualquier momento. Esto puede facilitar y agilizar muchos trámites evitando tener que enviar documentos en físico y que el ciudadano tenga que devolverlos con su visto bueno.

Ciudadanía digital se ha construido enteramente con software libre y estándares abiertos como [openid connect](https://openid.net/developers/how-connect-works/). Hubo una versión que se publicó como software libre en el [repositorio estatal de software](https://gitlab.softwarelibre.gob.bo/agetic) pero por razones que desconozco fue removida.

### Construir ciudadanía digital
 
No fui parte del desarrollo del proyecto desde sus inicios que entiendo fue desde 2016. Recuerdo que había un equipo de al menos cuatro programadores dedicados a implementar el proveedor de identidad a partir del estándar openid y una implementación libre en javascript con node.js y para 2018 ya se hacían las primeras pruebas en un ambiente controlado. Como todo el proyecto se hizo con software libre, este debía ser desplegado en infraestructura estatal, es decir en servidores del estado exclusivamente. Recuerdo habían muchas dificultadas para consolidar el despliegue, un arduo trabajo de desarrollo, planificación, análisis, logística, infraestructura tecnológica y pruebas que permitió la primera versión hecha enteramente con personal boliviano aprovechando tecnologías libres.

Para respaldar este desarrollo se tuvo que hacer una revisión y propuesta de todo un marco legal y fue consolidado en la [ley 1080](https://www.lexivox.org/norms/BO-L-N1080.xhtml). A la par se tuvo que desarrollar un cliente móvil de ciudadanía digital y esta aplicación para android y iOS fue también un gran reto.

#### ¿Por qué desarrollar un proyecto tan grande con software libre?

Seguramente habría sido más fácil contratar a una empresa que desarrolle este gran proyecto usando software y tecnologías *enterprise* y por ende cerradas (privativas), casi como comprar un edificio y que luego te entreguen las llaves después de pagar precios  exorbitantes. Así, el estado no habría trabajado e invertido tanto tiempo en hacer la primera versión y — no se habría aprendido nada.

Un país no puede entregar su soberanía y siendo este un futuro sistema crítico, ciudadanía digital no podía ser desarrollada usando software o tecnologías restrictivas que a la larga pondrán en manos ajenas el control de este proyecto importante. Más allá del costo económico que debe ser un factor fundamental para la economía de un país, el poner esfuerzo, solucionar diferentes problemas, entender tecnología, desarrollar creatividad, optimizar recursos y aprender de los errores **es un aprendizaje invaluable** que sólo podía obtenerse asumiendo el reto de implementar este proyecto con software libre y en servidores propios. El reto que asumió el estado a través [AGETIC](https://es.wikipedia.org/wiki/AGETIC) durante la gestión de Nicolás Laguna de 2015 a 2019.

### Incluyéndome en el proyecto

Para 2018 trabajaba como consultor de línea desarrollando software para sistemas de desburocratización. Un día en una reunión el director llamó a voluntarios para colaborar en ciudadanía digital y para mi sorpresa sólo nos presentamos tres personas. Se nos presentaron las cosas que habían: Eran el proveedor de identidad de ciudadanía y la cadena de bloques aún en desarrollo, se supone que debíamos pensar ideas y a la semana siguiente se me ocurrió una idea torpe para validar documentos y en una reunión con el desarrollador principal le mostré un diagrama que yo mismo no acababa de entender. Afortunadamente él no me entendió y lanzó la idea de utilizar el proveedor de identidad y la cadena de bloques para hacer como una firma con ciudadanía. Capté la idea y esta se fue mejorando hasta convertirse en el servicio de aprobación de documentos que para fines de año fuimos desarrollando.

Recuerdo que cuando se me dió luz verde, me quedaba fácilmente hasta las 11 de la noche programando, corrigiendo errores y probando, se lo mostraba a alguien que me hacía notar fallas de lógica que luego iba a corregir. Así un día con una horrible interfaz de usuario hubo la primera demo que ni siquiera se conectaba bien a la cadena de bloques para introducir metadata de documentos aprobados. Posteriormente y ya con la ayuda de un equipo, todo eso se corrigió y salió la primera especificación y documentación para que otras entidades lo integren.

#### Fiscalía General — integración

Tras largas reuniones y acuerdos con otras instituciones, la fiscalía general del estado con su sistema «Justicia libre» fue el primer sistema que incluiría el acceso con ciudadanía digital y aprobación de documentos. Aprovechamos la plataforma de interoperabilidad desarrollada también por AGETIC para ser el nexo de comunicación entre las dos instituciones. Viajé a Sucre por poco más de una semana a colaborar con la integración, cuando se logró fue a mi entender la primera institución que se beneficiaba sustancialmente de ciudadanía digital, ya que permitía agilizar los casos de la fiscalía. Aquí un [video corto del sistema ya integrado](https://www.youtube.com/watch?v=-0PrrnmsQKI)

Al mismo tiempo empezaron los disturbios en 2019 que forzaron la renuncia de Evo Morales y el cambio al gobierno transitorio. Por tanto las integraciones con ciudadanía digital se pausaron tras la renuncia de Nicolás Laguna en un clima tenso.

## Análisis — Razones por las que no despegó

### 0. El registro de usuarios

Antes de poder usarla, es necesario darse de alta o [registrarse en ciudadanía digital](https://www.gob.bo/tramites/registro-en-ciudadania-digital). Este procedimiento es presencial por requerir pleno consentimiento de la persona. El(a) usuario(a) primero deben hacer un pre-registro con un teléfono personal y correo electrónico, luego usan su número de carnet como nombre de usuario y definen una contraseña. Suena simple, sin embargo en la práctica suelen haber contratiempos por ejemplo la persona debe hacer primero el pre-registro y luego pasar por puntos de registro para completar y darse de alta. En todo caso puede pasar al punto de registro para recibir ayuda para ambos procedimientos. Luego de registrarse, las personas pueden olvidar su contraseña, extraviar su teléfono y eso requiere otro procedimiento. 

El registro de usuarios es `el menor obstáculo` pero vale la pena mencionarlo.

### 1. Resistencia al cambio

Existe resistencia a la hora de cambiar aquello que hace la vida más difícil, aquí algunas cosas que pude observar de lo que yo considero es un `gran obstáculo` a la hora de implementar la digitalización de trámites y así ciudadanía digital.

#### 1.1 Instituciones y sus procesos

Cada institución define sus procedimientos, desde definir su estructura jerárquica, recibir cartas, definir un procedimiento para derivarlas y responderlas, solicitar información, designar presupuestos, solicitar recursos, normativas, etc. Cuando se pide desburocratización de procesos, es necesario hacer un estudio detallando los procesos y necesidades actuales. — Si alguien dice que va a desburocratizar sin entender las funciones y recursos de la institución, sin analizar qué se puede optimizar y su impacto, está vendiendo humo.

Para usar ciudadanía digital de forma efectiva, primero se deberían digitalizar los procesos y procedimientos **dentro de una institución**, si no hay procesos digitalizados no tendrá sentido pensar en usar este servicio pues en el fondo todo se sigue haciendo con cartas y memorándums de pesado papel, se repiten pasos y se toman los caminos más largos para cosas que podrían obtenerse inmediatamente. Se requiere una re ingeniería de procesos y procedimientos.

#### 1.2 El necesario "cambio de chip" 

El cambio de chip se refiere a la forma de pensar. Si en una institución, las cabezas piensan que las cosas ya están definidas y hay que seguir las reglas al pie de la letra sin cuestionarse si se pueden mejorar o si hay pasos que sólo hacen las cosas más difíciles tanto para la ciudadanía y para los funcionarios, difícilmente va a cambiar algo respecto a la optimización de trámites.

Y esto involucra también a los funcionarios, desde la recepcionista que está acostumbrada a recibir y sellar cartas manteniendo carpetas con copias duplicadas de cédulas de identidad (C.I.), pasando por los funcionarios que manejan registros importantes en hojas de "excel" o "google sheets" y las autoridades que no están dispuestas a invertir para implementar mejoras en los procesos y de paso comparten información sensible por chats de whatsapp.

El "cambiar el chip" significaría por ejemplo en dejar de pensar sólo en recibir cartas sino tener un sistema de recepción y seguimiento al flujo de una carta o nota. En lugar de hojas de excel para información importante, bases de datos con un sistema de consultas que permita fácilmente encontrar la información requerida y abra la posibilidad para **interoperar** esta información con otras instituciones cuando así se requiera. 

El cambio de chip también es necesario para el dpto. de sistemas, ya que muchos informáticos se resisten a cambiar de *stack tecnológico*, esto es, prefieren seguir usando software obsoleto o cómodo, pero que dificulta la adaptación para integrarse con otros sistemas dentro y fuera de la institución. Ciudadanía digital se integra mediante la [plataforma de interoperabilidad del estado](https://agetic.gob.bo/tramites-y-servicios/herramientas-y-servicios-digitales/plataforma-de-interoperabilidad-del-estado) y esta usa servicios web HTTP.

### 2. Falta de recursos

Pude ver como las instituciones tienen insuficiente personal de sistemas que tendría que ser el encargado de la digitalización de trámites. Y esto no es difícil de comprobar. Por ejemplo cuando ves que el ingeniero de sistemas es la misma persona que arregla las impresoras, atiende a los funcionarios cuando su computadora se cuelga y les hace mantenimiento, y también es quien levanta los sistemas internos cuando caen, mantiene copias de correos institucionales, tiene que corregir errores en servicios informáticos internos, etc. Es mucho trabajo para una persona y también lo sería si fuesen dos o tres solamente para instituciones de decenas o cientos de funcionario(a)s.

Aparte de los informáticos dedicados al desarrollo y mantenimiento de sistemas de la institución, se necesitan personas que entiendan de los procesos y procedimientos internos y profesionales para mejorarlos. También hay que considerar que los sistemas requieren infraestructura tecnológica como servidores para bases de datos, copias de seguridad, servicios internos, etc.

La falta de recursos es otro `gran obstáculo` que frena la implementación de ciudadanía digital.

### 3. Personal de infraestructura tecnológica

Ciudadanía digital está pensada para integrar sistemas o plataformas y estas requieren infraestructura, servidores, conexiones  de red, etc. Los servicios no son perfectos y pueden caer o requerir más recursos. Para esto se requiere personal que haga mantenimiento de la infraestructura de sistemas.

Veo esto en particular como un `obstáculo moderado` principalmente porque vi que no hay mucho personal capacitado en mantenimiento de servidores y menos aún que quiera trabajar para el estado.

### 4. Los resultados esperados

Muchas veces las autoridades institucionales quieren hacer cambios que conlleven beneficios inmediatos o a corto plazo, ó beneficios que se puedan fácilmente mostrar y así justificar su esfuerzo y de paso ganarse aplausos. Este pensamiento cortoplacista es contraproducente porque la desburocratización es un proceso que puede tomar tiempo considerable y no es algo que "vendas fácilmente". Sino se apuesta por cambios a largo plazo como la digitalización de procesos y procedimientos, difícilmente se podrán implementar soluciones transversales como la ciudadanía digital.

Considero esto como un `obstáculo moderado`.

### 5. Brecha digital, una realidad más fuerte que la innovación

Suena bonito hablar de digitalización, desburocratización, realización de trámites al alcance de la mano desde la comodidad del celular. Pero la realidad es que **sólo una porción de la población** se beneficiaría. Sólo las personas que tienen acceso a internet, un celular o computadora y que están acostumbradas a usarlos harían efectivos los beneficios de ciudadanía digital. Y en Bolivia una porción reducida de la población cumple estas condiciones, la mayoría de los adultos mayores tiene dificultades para usar plataformas digitales sobre todo para hacer sus trámites.

Hay problemas de conectividad, acceso a internet, equipos electrónicos y "cultura digital" que deben ser atendidos primero. Por eso, este es un `gran obstáculo`.

## Estado actual y conclusiones

Dados los problemas ya mencionados para impulsar ciudadanía digital, se empezó por obligar a una parte de los funcionarios públicos a darse de alta en ciudadanía digital. Un ejemplo importante es la fiscalía general del estado, la contraloría o el notariado, donde hay un sistema nacional para notarios que requiere que estos usen ciudadanía digital y aprobación de documentos para emitir trámites notariados. 

Estos son avances importantes pero no se han extendido más allá y claro, **la población fuera del sector público no se está beneficiando** notoriamente de proyectos como ciudadanía digital o la [firma digital de ADSIB](https://www.firmadigital.bo/).

### Lo que no se debería hacer

El desarrollo de ciudadanía digital es un logro muy importante y para que este funcione se han tenido que desarrollar otras soluciones como la plataforma de interoperabilidad, lineamientos para digitalización de trámites, marco normavitivo que permite su implementación, una cadena de bloques estatal, se intentó realizar la migración a software libre y estándares abiertos, se han desarrollado varios sistemas para digitalización de trámites que son compatibles con ciudadanía digital y se ha publicado [documentación técnica para las integraciones entre sistemas](https://developer.ciudadaniadigital.bo/).

Si alguien viene y dice: «Esto no ha funcionado y que hay que volver a hacerlo todo», `está cometiendo un error` y sería desperdiciar todo ese esfuerzo e inversión para volver a hacerlo y tal vez pasar por el mismo proceso innecesariamente. Ciudadanía digital es un proyecto escalable y compatible con varios sistemas, además está abierto a mejoras y adaptaciones. Al ser software libre funcionando en Bolivia — no requiere que Bolivia pague enormes cantidades de dinero por suscripciones o pago de servicios tecnológicos — y está en control soberano del estado, por tanto es mejor aprovechar todo este desarrollo y construir sobre y desde él.

**En conclusión** ciudadanía digital puede ser una herramienta clave para que la población ejerza sus derechos a través del mundo digital de forma más eficiente y equitativa, pero hay que entender que es un proceso que requiere apoyo y voluntad.

base.html

<!DOCTYPE html>
<!--

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

PO Box 28228, Portland, OR, USA 97228
paul at paulmunday.net

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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
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 <http://www.gnu.org/licenses/>.

There should also be a copy of the AGPL in src/license.md that should be
accessible by going to <a href ="/license">/license<a> on this site.
-->
<html>
<head>
<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="https://rmgs.com.bo"/>
<meta property="og:site_name" content="Sitio personal de Rodrigo Garcia Saenz"/>
<!-- meta datos dinamicos -->
{%- for metadato in meta -%}
  <meta {{metadato.tag}}="{{metadato.name}}" 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">
    {{internal_css}}
  </style>
{% endif %}
{%- if hlinks -%}
  {%- for item in hlinks -%}
    <link 
       {%- if item.href %} href="{{item.href}}"{% endif -%}
       {%- if item.rel %} rel="{{item.rel}}"{% endif -%} 
       {%- if item.type %} type="{{item.type}}"{% endif -%}
       {%- if item.media %} type="{{item.media}}"{% 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') }}> #}
</head>

<body onload="loadCssStyle()">

  <div id="wrap">
    <div>
      <p> 
	<a href="/">
	  <img src="/static/imgs/cabecera1.png">
	</a>
	<br>
	<span style="font-size:12px;">
	  <q><b>sitio personal</b> de Rodrigo Garcia Saenz.</q>
	</span>
      </p>
    </div>

    <div class="row">
      
      <!-- <div class="col fifth"> -->
      <!-- Importante dejar este -->
      <!-- </div> -->


      <div class="col fifth">

        <!-- Tabla de contenidos del post -->
        {%- if not exclude_toc -%}
        <div id="side_toc">
          <details>
            <summary>Índice del post ↪</summary>
            {{ toc|safe }}
          </details>
        </div>
        {%- endif -%}

	<div id="nav_left">
	  <ul>
	    <dd>
	      <a href="/">
                <img class="leftbaricon"
                     src="/static/imgs/inicio.svg" alt="Inicio / Start" title="Inicio 🏡">
	      </a>
	    </dd>
	    <dd>
	      <a href="/posts">
                <img class="leftbaricon" src="/static/imgs/misc.svg" alt="Posts" title="Posts">
                <br>
                Posts
	      </a>		
	      <ul>
		<dd>
		  <a href="/posts/categorias">
		    <img
                      class="leftbaricon"
                      src="/static/imgs/categorias.svg" alt="Categorías" title="Categorías">
                    <br>
		    Categorías
		  </a>
		</dd>
	      </ul>
	    </dd>
	    <dd>	
	      <a href="/posts/categoria/fotos">
		<img
                  class="leftbaricon"
                  src="/static/imgs/fotos.svg" alt="fotos" title="fotos 🖼">
                <br>
		Fotos
	      </a>
	      <dd>
		<a href="/acerca_de_mi">
		  <img src="/static/imgs/acercade.svg" alt="acerca de mi" title="Acerca de mi">
                  <br>
		  Acerca de mi
		</a>
	      </dd>
	      <dd>	
	        <a href="/contacto">
		  <img
                    class="leftbaricon"
                    src="/static/imgs/contacto.svg" alt="contacto" title="Contacto 📨">
                  <br>
		  Contacto
	        </a>
	        
	      </dd>
              
	  </ul>
	  <ul>
	    <hr>
	    {%- 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, page=navigation.page) }}
		           {%- else -%}
		           {{ url_for(item.urlfor) }}
		           {%- endif -%}
		           {%- else -%}
		           {{ url_for('staticpage', page=item.base) }}
		           {%- endif -%}
		           
		           {%- if item.rel -%}
		           " rel="{{item.rel}} 
			   {%- endif -%}
			   ">{{item.link_text}}</a></li>
	    {% endfor -%}
	  </ul>
	  <p>
	    <a href="/rss" >
	      <img src="/static/imgs/rss.png" width="24" heigth="24">
	      RSS
	    </a>
	  </p>
	</div>

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

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

      </div>


      
      <!-- Contenido -->
      <div class="col fill">
	
	<!-- Cabecera -->
	<div style="margin-bottom: 7px">
	  <form>
	    <input type="submit" onclick="cambiarEstilo('oscuro'); return false;" name="theme" value="☻" id="oscuro" style="background-color: #333933; color: #C3C4C2; border-radius:3px;">
	    
	    <input type="submit" onclick="cambiarEstilo('claro'); return false;" name="theme" value="☺" id="claro" style="background-color: #D1E9D3; color: #131914; border-radius:3px;">
	  </form> 
	</div>

	<!-- Contenido -->
	{% block content %}{% endblock %}
        
        <!-- nav bottom (para pantallas chicas) -->
        <div id="nav_bottom">
	  <ul>
	    <dd>
	      <a href="/">
                <img src="/static/imgs/inicio.svg"
                     class="leftbaricon"
                     title="Inicio 🏡"
                     alt="Inicio">
	      </a>
	    </dd>
	    <dd>
	      <a href="/posts">
                <img
                  class="leftbaricon"
                  title="Posts"
                  src="/static/imgs/misc.svg" alt="posts">
                Posts
	      </a>		
	      <ul>
	        <dd>
		  <a href="/posts/categorias">
		    <img
                      class="leftbaricon"
                      title="Categorías"
                      src="/static/imgs/categorias.svg" alt="categorías">
                    categorías
		  </a>
	        </dd>
	      </ul>
	    </dd>
	    <dd>
	      <a href="/posts/categoria/fotos">
		<img
                  class="leftbaricon"
                  title="Fotos 🖼"
                  src="/static/imgs/fotos.svg" alt="fotos">
                fotos
	      </a>

	    </dd>

	    <dd>
	      <a href="/acerca_de_mi">
		<img
                  class="leftbaricon"
                  title="Acerca de mi"
                  src="/static/imgs/acercade.svg" alt="Acerca de mi">
                acerca de
	      </a>
	    </dd>
	  </ul>

	  <dd>	
	    <a href="/contacto">
	      <img
                class="leftbaricon"
                title="Contacto 📨"
                src="/static/imgs/contacto.svg" alt="Contacto">
              contacto
	    </a>
	  </dd>
	  </ul>
	  <ul>
	    <hr>
	    {%- 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, page=navigation.page) }}
		   {%- else -%}
		   {{ url_for(item.urlfor) }}
		   {%- endif -%}
		   {%- else -%}
		   {{ url_for('staticpage', page=item.base) }}
		   {%- endif -%}
		   
		   {%- if item.rel -%}
		   " rel="{{item.rel}} 
		         {%- endif -%}
		         ">{{item.link_text}}</a></li>
	    {% endfor -%}
	  </ul>
	  <p>
	    <a href="/rss" >
	      <img src="/static/imgs/rss.png" width="24" heigth="24">
	      RSS
	    </a>
	  </p>
        </div>

	<!-- Noticias Realcionadas si es un post -->
        <div id="noticias_relacionadas_bottom">
	  {% if contexto is defined %}
	    {% if contexto['relacionadas'] is defined %}
	      <h3>Posts/Noticias relacionadas</h3>
	      {% for post_relacionado in contexto['relacionadas'] %}
	        <p>
	          <a href="/posts/{{ post_relacionado }}">
	            {{ post_relacionado|n_heading }}
	          </a>
	        </p>
	        <hr>
	      {% endfor %}
	    {% endif %}
	  {% endif %}
        </div>
        <!-- consejo del dia -->
	<div id="consejo_del_dia_bottom">
	  <b>Consejo del día</b>
	  <hr>
	  {% if contexto %}
	    {{ contexto['consejo']|safe }}
	  {% endif %}
	</div>

      </div>

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




  </div>

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

    </p>
  </div>

  <!-- javascript -->
  <script language="javascript">
   function loadCssStyle() {
     const _varStyleName = 'style_css';
     var lsStyle = window.localStorage.getItem(_varStyleName);
     if (!lsStyle) {
       // default
       window.localStorage.setItem('style_css', 'oscuro');
       lsStyle = window.localStorage.getItem(_varStyleName);
     }
     var i, link_tag = document.getElementsByTagName("link");
     // inspeccionando hojas de estilo css cargadas
     for (
       i = 0, 
       link_tag = document.getElementsByTagName("link") ; i < link_tag.length ;
       i++ ) {
       if ((link_tag[i].rel.indexOf( "stylesheet" ) != -1)) {
         link_tag[i].disabled = true;
         if (link_tag[i].title === lsStyle) {
	   link_tag[i].disabled = false ;
           console.log('enabling', link_tag[i].title);
         }
       }
     }
   }
   
   function cambiarEstilo(value) {
     window.localStorage.setItem('style_css', value);
     loadCssStyle();
   }
  </script>
  <!-- --->

</body>
</html>

post.html

{% extends "base.html" %}
{% block content %}
  <article class="h-entry">
    <div> 
      {% if heading %}<h1 class="p-name">{{heading}}</h1>{% endif %}
    </div> 
    {# <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'] %} 
		{{attributes['date-text']}}
	      {% else %}
		{{attributes['date']}}
	      {% endif %}
            </time>
          {% endif %}
	  {% if attributes['author'] %}
	    <span class="p-author">{{attributes['author']}}</span>
	  {% endif %}
	    <a class="u-url" href="/{{name}}">🔗 enlace</a>
	</p>
	{% if attributes['summary'] %}
	  <p class="p-summary">{{attributes['summary']}}</p>
	{% endif %}
      </div>
    {% endif %}
    <div class='e-content'> 

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

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

Si tienes la posiblidad, haz donaciones a organizaciones sin ánimo de lucro para apoyar su trabajo.

Muchas de estas organizaciones se mantienen gracias a la gente que les apoya con donaciones monetarias.