# -*- 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, Markup, escape, request #, make_response
from flask import redirect
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)
Vivimos en un entorno social que nos enseña que mientras más se consiga, más felicidad y bienestar hay. ¿Conseguir más, es realmente beneficioso al final de cuentas?
¿Por qué parece que la mayor parte de la humanidad se la pasa buscando obtener más? Parece que la sociedad valora y premia a quien tiene **por demás**.
## ¿Un acuerdo social?
Resulta que quien tiene más se considera una persona exitosa, se la respeta más y entre otras cosas se le abren más oportunidades. Como un tipo de acuerdo social —destinado a beneficiar a las personas que acumulan más y quitar a quien tiene menos—, o al menos dejarles con lo suficiente para que sigan "alimentando a los sectores más beneficiados".
¿En qué momento la sociedad ha aceptado este acuerdo? Quizá desde los tiempos de las monarquías, sistemas feudales u otra forma de organización que normalizaba los abusos y la explotación del hombre hacia otros hombres y animales de este mundo. Validando comportamientos deplorables como la esclavitud o las invasiones para hacerse con los recursos o habitantes de otros pueblos. O quizá el impulso de supervivencia llevó a los seres humanos al extremo de tomar actitudes egoístas como estrategia crucial para mantenerse vivos.
En la actualidad sin embargo, la supervivencia es mucho más viable que en tiempos antiguos, ya se rompieron las monarquías legitimadas (al menos en gran parte del planeta) y ya es más factible pensar en que **el egoísmo** puro, no es necesario para vivir y peor para vivir bien. Por eso pienso que este «acuerdo social» es más una tendencia difícil de eludir que una necesidad sin remedio.
## Interiorizando el "siempre conseguir más" a nivel personal
La presión social es fuerte, a tal punto ha hecho que este comportamiento de querer tener más se transmita de padres a hijos, hermanos a hermanitos y también entre amistades. Desde que somos pequeños se nos entrena para conseguir cosas, para siempre "tener hambre", ser ambiciosos, desear crecer, superarse, incrementar, acumular, tener más, incluso cuando no hace falta y tal vez sin razón justificada. Desde las escuelas se dirige buena parte de la educación para generar riquezas encaminando actividades tan naturales como el arte, la investigación o incluso la curiosidad, hacia el progreso económico que incluso es usado como mero sinónimo de bienestar. Y esta educación no viene sólo desde el sistema educativo, también de nuestras propias familias, el núcleo mismo de la sociedad.
Tanto se nos repite y empuja que asumimos que lo normal es vivir para hacerse rico, alcanzar estatus o sobresalir. Con esa idea, salimos al mundo para aprender a hacerlo y dependiendo de los principios éticos de cada persona, este camino podría ser una cruzada donde se aniquile cualquier obstáculo sin detenerse a pensar en el daño ocasionado.
### Cuán dañina puede ser la auto-superación extrema
Sabemos que el [consumismo](https://es.wikipedia.org/wiki/Consumismo) está orientado a acumular bienes materiales y servicios incitando al gasto desmedido en cosas que no son siempre necesarias. Pero no sólo gastar en exceso y consumir es de lo que intento hablar, también de intentar obtener cosas más allá, por ejemplo bienes o logros que aseguren estatus.
Por la narrativa de la «superación constante» que dice que la meta es siempre "mejorar" y exceder expectativas anteriores, hay muchas personas que interiorizan tanto este comportamiento que están todo el tiempo buscando superar récords o logros de otras personas, intentar ser el mejor en algo y cuando parece que ya no hay nadie más con quien competir, la competencia se torna contra ellos mismos.
Quizá esto se refleja claramente en el ámbito profesional, por ejemplo cuando un ingeniero de software está aprendiendo nuevas tecnologías, tomando cursos y obteniendo certificados para mejorar su hoja de vida y así pueda mostrar cuanto conocimiento tiene y cuan hábil es. Si bien contar con acreditaciones que de alguna forma validen el conocimiento o experiencia pueden ser necesarias para aspirar a mejores oportunidades laborales, cuando ya se cuentan con las suficientes para ir creciendo y aún así se las intenta obtener sin descanso, podría ser una señal de que este ingeniero no se siente suficientemente valioso o que simplemente va muy rápido y no se toma el tiempo necesario para pensar en **qué gana realmente**.
O un deportista o jugador empedernido que sólo busca romper marcas o ganar. ¿Será para mejorar su autoestima?, para poder decirse a sí mismo que ahora es mejor que antes. O quizá esta narrativa de «superación constante» tiene la irremediable consecuencia de que quien la sigue está constantemente intentando alimentar su ego y engrandecerse a sí mismo para no sentirse por debajo de sus propias expectativas. Muchas personas pueden caer en esta trampa —competir constantemente, sin detenerse a pensar en las consecuencias.
Y también puede pasar que en el intento de superarse y no siempre o "nunca" lograrlo, la persona se frustra tanto que la misma finalidad de aumentar el autoestima no se cumple y provoca lo contrario. Es decir, frustrarse excesivamente y sentirse inferior, incapaz y hacerse la idea de que vale poco como persona por que tiene tan arraigado este discurso de la auto-superación que se percibe como una persona sin valor. Así —reduciendo su persona a «que tan bueno puede ser en algo en particular».
Lo peor es que incluso si fuese una persona que aparentemente siempre logra "superarse", podría caer en lo mismo. En quererse a si mismo no en un sentido equilibrado y completo, sino sólo a su capacidad de superarse en algo concreto sin detenerse a considerar que la vida y el amor propio son inherentemente más grandes y tienen significados más profundos. En todo caso y eventualmente, la persona podría perder la capacidad de "auto superarse" y no habría aprendido a quererse tal y con todo lo que es.
### Pero querer algo es natural ¿no?
Pues sí, si hay una cosa que no tiene límites es el querer. Jugando un poco se puede: `querer algo` o `querer querer algo`. o también: `querer que una persona quiera no querer algo` y así sucesivamente.
Pero los recursos no son ilimitados, y tampoco nuestra capacidad de hacer cosas, entonces en algún punto se tiene que ser realista y entender que no se puede tener todo lo que se quiere. La idea de esta reflexión **no es contraria a querer cosas, a tener sueños**, sino a tratar de entender lo que se puede provocar al intentar obtener algo sin detenerse a pensar en lo que está a nuestro alcance o está disponible. Los deseos son muy importantes, nos dan razones fuertes para seguir adelante más no lo son todo.
## ¿Y qué hay del progreso?
Hay muchas definiciones de progreso que van en constante cambio y adaptación, podrían los capitalistas pensar en que el progreso es obtener riquezas económicas, o alguien que sigue un pensamiento marxista dirá que el progreso sólo es posible en una sociedad comunista. Una definición de progreso aceptada a viva voz sería la del desarrollo sostenible de la organización de naciones unidas [[1]](https://www.un.org/sustainabledevelopment/es/2023/08/what-is-sustainable-development/) que dice:
> El desarrollo sostenible implica cómo debemos vivir hoy si queremos un futuro mejor, ocupándose de las necesidades presentes sin comprometer las oportunidades de las generaciones futuras de cumplir con las suyas.
También dice:
> Es una especie de acto de malabarismo. Hay que mantener en el aire tres bolas diferentes a la vez: crecimiento económico, inclusión social y protección del medio ambiente.
Esta definición es en cierta forma equilibrada aunque a mi parecer está más orientada a nivel de «nación occidental», aquí la economía es crucial porque permite la obtención de recursos que a su vez pueden invertirse en el bienestar de los habitantes. Esta es contrastada por la importancia del medio ambiente y su cuidado, para no poner al desarrollo económico por encima de la misma fuente de las riquezas, esta fuente es el medio ambiente y finalmente el desarrollo social apuntando a mejorar la calidad de vida y convivencia entre los habitantes del mundo.
Sin embargo, esta definición de desarrollo viene de la "idea de progreso moderna", parece que esta idea pretende reemplazar a otra en la que el ser humano es un ser insignificante y limitado a quien se le impone cierto destino. En la definición de hombre moderno, el ser humano no tiene límites en su progreso y es creador de historia. Así progresivamente evolucionando en una búsqueda constante de un progreso que se traducía en la **acumulación** de conocimientos, virtudes, fuerzas productivas o riquezas que acercan al hombre a un estado de armonía y perfección. —“Más” pasará a ser equivalente a “mejor”— [[2]](https://es.wikipedia.org/wiki/Progreso#El_surgimiento_de_la_idea_moderna_del_progreso). Pero como no hay recursos infinitos y no todas las personas tienen acceso igualitario a estos, surgen los **conflictos sociales** derivados de factores económicos que son acentuados por una falta de conciencia social y solidaridad por parte de quienes poseen o mejor dicho controlaban el acceso a los recursos. La división de clases se hace evidente y Karl Marx abordó desarrollando su perspectiva en el "Manifiesto Comunista" y otros textos para eliminar las injusticias perpetuadas por las clases dominantes hacia las clases trabajadoras.
Tal vez el progreso desde el punto de vista moderno **desarrollado en Europa**, se base en el crecimiento económico como el motor principal sin tomar mucho en cuenta otros principios como la convivencia armoniosa entre seres humanos, respeto a la naturaleza, espiritualidad, estudio y compresión del universo como lo hacían muchas otras culturas y civilizaciones. Quizá la mayoría de estas culturas fueron menospreciadas por su supuesta "falta de desarrollo". Me parece importante comprender que la idea de "progreso occidental" ha sido desarrollada en sociedades que han sometido a otros pueblos a nivel filosófico, moral y en su visión respecto a progreso. Es decir en lugar de enriquecerse intentando desarrollar ideas nuevas como resultado de la interacción y convivencia entre pueblos diferentes, **han impuesto** su visión de progreso y también intentado eliminar otras ideas que entren en conflicto con la búsqueda permanente de superioridad a partir de la economía que promete bienestar, situaciones favorables y abre un gran número de expectativas sobre qué hacer con esas riquezas.
La economía es muy importante claro que sí, si no se consiguen recursos ¿Cómo se va a poder evitar hambrunas o malnutrición?. Estamos forzados a contar con recursos y tenemos que ver la forma de conseguirlos, pero con moderación y no como un fin en sí mismo. El desarrollo económico es una necesidad en nuestras sociedades actuales, sin embargo, es indispensable determinar las razones que hacen que los más necesitados no puedan acceder a las riquezas económicas. ¿No será que hay todo un sistema que lo impide?.
Hay movimientos que se **hacen llamar liberales** que defienden ante todo la libertad individual y limitación de poderes del estado. Dan una importancia crucial al derecho a la [propiedad privada](https://es.wikipedia.org/wiki/Derecho_de_propiedad) y el libre mercado. El problema es que ahora mismo **hay un gran desbalance** en la propiedad de los recursos y medios de producción, es decir hay un grupo de pocos individuos que poseen gran cantidad de propiedades y por ende quienes no las tengan se ven obligados a someterse a estos grupos aventajados. En cuanto al libre mercado, no es realmente libre ni igualitario, ya que hay quienes poseen grandes ventajas y aparentemente sin quebrantar ninguna ley, tienen la capacidad de imponer reglas y controlar el comportamiento de este.
Será muy difícil conseguir un progreso colectivo equitativo si se siguen solamente estos principios porque estamos en un escenario con serias diferencias que no se van a solucionar por sí solas, esto porque los «dueños de la propiedad» tienden a tener más control y mantener los privilegios con los que cuentan, dificultando o bloqueando a otros individuos que intenten acceder a la propiedad o los medios de producción.
Pienso que lo que está esencialmente mal en estos modelos liberales capitalistas o más extremos como el anarcocapitalismo, es que el derecho y progreso individual es lo más importante, y esto provoca que las personas constantemente se centren en sí mismas restando importancia a los intereses colectivos. Lo comunitario pierde valor y los logros personales o familiares son lo más importante, por eso tanta importancia a la propiedad privada y menos a la propiedad colectiva. Sin embargo, incluso si todos empezarán con la misma cantidad de recursos, habrían inevitablemente individuos que encuentren maneras de controlar, recursos, mercados o trabajo ajeno rompiendo la premisa que afirma que el mercado tiende a autorregularse. Pienso no sería ilógico decir que poner el progreso individual como lo más importante, causará que cada individuo lo intente conseguir sin importarle los demás.
En un análisis [[3]](https://revistas.um.es/sh/article/view/243631/224361) se menciona un ironía que dice: *El progreso es concebido originalmente como la desvinculación de la experiencia y la apertura de un amplio horizonte de expectativas. Pero a medida que aumentan las experiencias llevadas a cabo con el progreso posible, menores son las expectativas de que tenga lugar un progreso real*.
A mi parecer, las experiencias serían la comprensión de la realidad, los saberes y el experimentar la vida intentando entenderla. Y las expectativas todas esas cosas que se desean. Entonces si el progreso se enfoca en lo que se quiere tener **mucho más** que el intentar comprender la realidad mediante las experiencias, tarde o temprano esos deseos son frustrados por el fuerte desbalance entre lo que se quiere y lo que hay. Y nuestro sueño de progreso constante será interrumpido por el duro y crudo golpe de la realidad.
### ¿Hacia qué "progreso" se está dirigiendo la sociedad actual?
Que obtener riquezas económicas, estatus social u otros privilegios sean el objetivo común, hace que la sociedad se este dirigiendo a un **falso progreso**, la realidad es que hay todo un sistema establecido y diseñado para enriquecer a "los dueños de las tierras" que ahora se están convirtiendo también en dueños incluso de nuestras interacciones a través de las plataformas digitales.
Durante el capitalismo se han privatizado las tierras cultivables o con recursos explotables. Luego de que se aboliera la esclavitud y el pongueaje (servidumbre agraria), los dueños de las tierras ni siquiera tenían que trabajarlas ya que empezaron a cobrar rentas por el uso de "sus" tierras. Luego por el uso de fórmulas, técnicas o ideas que probablemente ni siquiera inventaron ellos mismos mediante algo llamado sistema de patentes y **propiedad intelectual**, es decir — han privatizado hasta las ideas.
Para seguir maximizando sus ganancias, se están privatizando las comunicaciones a través de las plataformas digitales, toda interacción entre seres humanos pasa en su mayoría por un grupo de servicios en manos de gigantes tecnológicos como google, meta, amazon, microsoft y otros. Nuestros datos son el nuevo oro y "los nuevos señores" se están apropiando de todo [[4]](https://maxmurphy.xyz/p/goodbye-surveillance-capitalism-hello), además muchas personas lo entienden y defienden como progreso de la humanidad.
Los poderosos han arrinconado al pueblo a través **del control económico** o incluso de nuestra necesidad natural de interacción privatizando las comunicaciones en los medios digitales, y de paso nos han metido la idea «por la fuerza» de que eso es progreso, de que **tener más es la meta**. El neoliberalismo, ha hecho que ahora los señores capitalistas tengan a los gobiernos como sus capataces y todo sigue evolucionando ¿Nos estamos dirigiendo hacia un tecno feudalismo?.
## Intentando "pisar tierra"
Cada realidad es diferente y sin embargo más allá de una retórica de auto-superación extrema o de progreso con tendencia a lo privado, intentar comprender la situación actual y reflexionar para reformular una sólida base de ideas hará una gran diferencia.
Nuestro valor como personas no debería ser algo que dependa de cuanto tenemos o conseguimos, ni tampoco de que tan productivos somos. En lugar de obsesionarse con «siempre mejorar», aprendamos primero a querernos a nosotros mismos tal cual somos, respetar y valorar nuestro entorno, revalorizar la importancia de la comunidad y —lo demás vendrá por nuestra voluntad natural de mejorar y descubrir. Detenerse e incluso volver atrás para entender o explorar soluciones sería más beneficioso que avanzar ciegamente sólo para decir que tan lejos hemos llegado.
¿Hacia dónde nos están dirigiendo las ideas de progreso de la sociedad actual? ¿Vale la pena "enriquecerse" sin importar en el efecto sobre los demás? ¿Acaso el ser humano tiene que ser siempre el ser superior? ¿Qué hay de otros seres como los animales y plantas, se piensa en su bienestar también o se los ha empezado a ver como meros instrumentos para satisfacer una necesidad de sentirse superiores? ¿Qué hay de otras ideas de progreso que intentan dar más importancia al ser humano y su papel cómo **otro** ser vivo más y no dueño del mundo?
Para terminar y sin darle menos importancia a otros temas sobre los que he intentado reflexionar en este ensayo, me gustaría citar una frase que se le atribuye a Pepe Mujica.
> "Pobres no son los que tienen poco. Pobres son los que quieren más, infinitamente más y nunca les alcanza"
## Referencias
1. [https://www.un.org/sustainabledevelopment/es/2023/08/what-is-sustainable-development/](https://www.un.org/sustainabledevelopment/es/2023/08/what-is-sustainable-development/) - Objetivos del desarrollo sostenible de Naciones unidas.
2. [https://es.wikipedia.org/wiki/Progreso#El_surgimiento_de_la_idea_moderna_del_progreso](https://es.wikipedia.org/wiki/Progreso#El_surgimiento_de_la_idea_moderna_del_progreso) - El progreso humano, el surgimiento de la idea moderna del progreso.
3. [https://revistas.um.es/sh/article/view/243631/224361](https://revistas.um.es/sh/article/view/243631/224361) - Progreso y modernidad: el problema con la autonomía.
4. [https://maxmurphy.xyz/p/goodbye-surveillance-capitalism-hello](https://maxmurphy.xyz/p/goodbye-surveillance-capitalism-hello) - Ensayo sobre el fin de la vigilancia capitalista y la llegada de la vigilancia fascista.