views.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. # -*- coding: utf-8 -*-
  2. """
  3. Monomotapa - A Micro CMS
  4. Copyright (C) 2014, Paul Munday.
  5. PO Box 28228, Portland, OR, USA 97228
  6. paul at paulmunday.net
  7. Modificado por: Rodrigo Garcia 2017 https://rmgss.net/contacto
  8. This program is free software: you can redistribute it and/or modify
  9. it under the terms of the GNU Affero Public License as published by
  10. the Free Software Foundation, either version 3 of the License, or
  11. (at your option) any later version.
  12. This program is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. GNU General Public License for more details.
  16. You should have received a copy of the GNU Affero General Public License
  17. along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. Monomotapa:
  19. a city whose inhabitants are bounded by deep feelings of friendship,
  20. so that they intuit one another's most secret needs and desire.
  21. For instance, if one dreams that his friend is sad, the friend will
  22. perceive the distress and rush to the sleepers rescue.
  23. (Jean de La Fontaine, *Fables choisies, mises en vers*, VIII:11 Paris,
  24. 2nd ed., 1678-9)
  25. cited in :
  26. Alberto Manguel and Gianni Guadalupi, *The Dictionary of Imaginary Places*,
  27. Bloomsbury, London, 1999.
  28. A micro cms written using the Flask microframework, orignally to manage my
  29. personal site. It is designed so that publishing a page requires no more than
  30. dropping a markdown page in the appropriate directory (though you need to edit
  31. a json file if you want it to appear in the top navigation).
  32. It can also display its own source code and run its own unit tests.
  33. The name 'monomotapa' was chosen more or less at random (it shares an initial
  34. with me) as I didn't want to name it after the site and be typing import
  35. paulmunday, or something similar, as that would be strange.
  36. """
  37. from flask import render_template, abort, Markup, escape, request #, make_response
  38. from flask import redirect
  39. from werkzeug.utils import secure_filename
  40. from pygments import highlight
  41. from pygments.lexers import PythonLexer, HtmlDjangoLexer, TextLexer
  42. from pygments.formatters import HtmlFormatter
  43. import markdown
  44. from time import gmtime, strptime, strftime, ctime, mktime
  45. import datetime
  46. import os.path
  47. import os
  48. import subprocess
  49. import json
  50. import traceback
  51. from collections import OrderedDict
  52. from simplemotds import SimpleMotd
  53. from monomotapa import app
  54. from monomotapa.config import ConfigError
  55. from monomotapa.utils import captcha_comprobar_respuesta, captcha_pregunta_opciones_random
  56. from monomotapa.utils import categorias_de_post, categoriasDePost, categoriasList, cabezaPost
  57. from monomotapa.utils import titulo_legible, metaTagsAutomaticos
  58. from markdown.extensions.toc import TocExtension
  59. json_pattrs = {}
  60. with open(os.path.join('monomotapa','pages.json'), 'r') as pagefile:
  61. json_pattrs = json.load(pagefile)
  62. simplemotd = SimpleMotd("config_simplemotds.json")
  63. class MonomotapaError(Exception):
  64. """create classs for own errors"""
  65. pass
  66. def get_page_attributes(jsonfile):
  67. """Returns dictionary of page_attributes.
  68. Defines additional static page attributes loaded from json file.
  69. N.B. static pages do not need to have attributes defined there,
  70. it is sufficient to have a page.md in src for each /page
  71. possible values are src (name of markdown file to be rendered)
  72. heading, title, and trusted (i.e. allow embeded html in markdown)"""
  73. try:
  74. with open(src_file(jsonfile), 'r') as pagesfile:
  75. page_attributes = json.load(pagesfile)
  76. except IOError:
  77. page_attributes = []
  78. return page_attributes
  79. def get_page_attribute(attr_src, page, attribute):
  80. """returns attribute of page if it exists, else None.
  81. attr_src = dictionary(from get_page_attributes)"""
  82. if page in attr_src and attribute in attr_src[page]:
  83. return attr_src[page][attribute]
  84. else:
  85. return None
  86. # Navigation
  87. def top_navigation(page):
  88. """Generates navigation as an OrderedDict from navigation.json.
  89. Navigation.json consists of a json array(list) "nav_order"
  90. containing the names of the top navigation elements and
  91. a json object(dict) called "nav_elements"
  92. if a page is to show up in the top navigation
  93. there must be an entry present in nav_order but there need not
  94. be one in nav_elements. However if there is the key must be the same.
  95. Possible values for nav_elements are link_text, url and urlfor
  96. The name from nav_order will be used to set the link text,
  97. unless link_text is present in nav_elements.
  98. url and urlfor are optional, however if ommited the url wil be
  99. generated in the navigation by url_for('staticpage', page=[key])
  100. equivalent to @app.route"/page"; def page())
  101. which may not be correct. If a url is supplied it will be used
  102. otherwise if urlfor is supplied it the url will be
  103. generated with url_for(urlfor). url takes precendence so it makes
  104. no sense to supply both.
  105. Web Sign-in is supported by adding a "rel": "me" attribute.
  106. """
  107. with open(src_file('navigation.json'), 'r') as navfile:
  108. navigation = json.load(navfile)
  109. base_nav = OrderedDict({})
  110. for key in navigation["nav_order"]:
  111. nav = {}
  112. nav['base'] = key
  113. nav['link_text'] = key
  114. if key in navigation["nav_elements"]:
  115. elements = navigation["nav_elements"][key]
  116. nav.update(elements)
  117. base_nav[key] = nav
  118. return {'navigation' : base_nav, 'page' : page}
  119. # For pages
  120. class Page:
  121. """Generates pages as objects"""
  122. def __init__(self, page, **kwargs):
  123. """Define attributes for pages (if present).
  124. Sets self.name, self.title, self.heading, self.trusted etc
  125. This is done through indirection so we can update the defaults
  126. (defined in the 'attributes' dictionary) with values from config.json
  127. or pages.json easily without lots of if else statements.
  128. If css is supplied it will overide any default css. To add additional
  129. style sheets on a per page basis specifiy them in pages.json.
  130. The same also applies with hlinks.
  131. css is used to set locally hosted stylesheets only. To specify
  132. external stylesheets use hlinks: in config.json for
  133. default values that will apply on all pages unless overidden, set here
  134. to override the default. Set in pages.json to add after default.
  135. """
  136. # set default attributes
  137. self.page = page.rstrip('/')
  138. self.defaults = get_page_attributes('defaults.json')
  139. self.pages = get_page_attributes('pages.json')
  140. self.url_base = self.defaults['url_base']
  141. title = titulo_legible(page.lower())
  142. heading = titulo_legible(page.capitalize())
  143. self.categorias = categoriasDePost(self.page)
  144. self.exclude_toc = True
  145. try:
  146. self.default_template = self.defaults['template']
  147. except KeyError:
  148. raise ConfigError('template not found in default.json')
  149. # will become self.name, self.title, self.heading,
  150. # self.footer, self.internal_css, self.trusted
  151. attributes = {'name' : self.page, 'title' : title,
  152. 'navigation' : top_navigation(self.page),
  153. 'heading' : heading, 'footer' : None,
  154. 'css' : None , 'hlinks' :None, 'internal_css' : None,
  155. 'trusted': False,
  156. 'preview-chars': 250,
  157. }
  158. # contexto extra TODO: revisar otra forma de incluir un contexto
  159. self.contexto = {}
  160. self.contexto['consejo'] = simplemotd.getMotdContent()
  161. # set from defaults
  162. attributes.update(self.defaults)
  163. # override with kwargs
  164. attributes.update(kwargs)
  165. # override attributes if set in pages.json
  166. if page in self.pages:
  167. attributes.update(self.pages[page])
  168. # set attributes (as self.name etc) using indirection
  169. for attribute, value in attributes.items():
  170. # print('attribute', attribute, '=-==>', value)
  171. setattr(self, attribute, value)
  172. # meta tags
  173. try:
  174. self.pages[self.page]['title'] = attributes['title']
  175. self.pages[self.page]['url_base'] = self.url_base
  176. metaTags = metaTagsAutomaticos(self.page, self.pages.get(self.page, {}))
  177. self.meta = metaTags
  178. # for key, value in self.pages[self.page].items():
  179. # print(' ', key, ' = ', value)
  180. except Exception as e:
  181. tb = traceback.format_exc()
  182. print('Error assigning meta:', str(e), '\n', str(tb))
  183. # reset these as we want to append rather than overwrite if supplied
  184. if 'css' in kwargs:
  185. self.css = kwargs['css']
  186. elif 'css' in self.defaults:
  187. self.css = self.defaults['css']
  188. if 'hlinks' in kwargs:
  189. self.hlinks = kwargs['hlinks']
  190. elif 'hlinks' in self.defaults:
  191. self.hlinks = self.defaults['hlinks']
  192. # append hlinks and css from pages.json rather than overwriting
  193. # if css or hlinks are not supplied they are set to default
  194. if page in self.pages:
  195. if 'css' in self.pages[page]:
  196. self.css = self.css + self.pages[page]['css']
  197. if 'hlinks' in self.pages[page]:
  198. self.hlinks = self.hlinks + self.pages[page]['hlinks']
  199. # append heading to default if set in config
  200. self.title = self.title + app.config.get('default_title', '')
  201. def _get_markdown(self):
  202. """returns rendered markdown or 404 if source does not exist"""
  203. src = self.get_page_src(self.page, 'src', 'md')
  204. if src is None:
  205. abort(404)
  206. else:
  207. return render_markdown(src, self.trusted)
  208. def get_page_src(self, page, directory=None, ext=None):
  209. """"return path of file (used to generate page) if it exists,
  210. or return none.
  211. Also returns the template used to render that page, defaults
  212. to static.html.
  213. It will optionally add an extension, to allow
  214. specifiying pages by route."""
  215. # is it stored in a config
  216. pagename = get_page_attribute(self.pages, page, 'src')
  217. if not pagename:
  218. pagename = page + get_extension(ext)
  219. if os.path.exists(src_file(pagename , directory)):
  220. return src_file(pagename, directory)
  221. else:
  222. return None
  223. def get_template(self, page):
  224. """returns the template for the page"""
  225. pagetemplate = get_page_attribute(self.pages, page, 'template')
  226. if not pagetemplate:
  227. pagetemplate = self.default_template
  228. if os.path.exists(src_file(pagetemplate , 'templates')):
  229. return pagetemplate
  230. else:
  231. raise MonomotapaError("Template: %s not found" % pagetemplate)
  232. def generate_page(self, contents=None):
  233. """return a page generator function.
  234. For static pages written in Markdown under src/.
  235. contents are automatically rendered.
  236. N.B. See note above in about headers"""
  237. toc = '' # table of contents
  238. if not contents:
  239. contents, toc = self._get_markdown()
  240. # print('////', toc)
  241. template = self.get_template(self.page)
  242. # print('......................')
  243. # def mos(**kwargs):
  244. # for k in kwargs:
  245. # print(k, end=',')
  246. # mos(**vars(self))
  247. return render_template(template,
  248. contents = Markup(contents),
  249. toc=toc,
  250. **vars(self))
  251. # helper functions
  252. def src_file(name, directory=None):
  253. """return potential path to file in this app"""
  254. if not directory:
  255. return os.path.join( 'monomotapa', name)
  256. else:
  257. return os.path.join('monomotapa', directory, name)
  258. def get_extension(ext):
  259. '''constructs extension, adding or stripping leading . as needed.
  260. Return null string for None'''
  261. if ext is None:
  262. return ''
  263. elif ext[0] == '.':
  264. return ext
  265. else:
  266. return '.%s' % ext
  267. def render_markdown(srcfile, trusted=False):
  268. """ Returns markdown file rendered as html and the table of contents as html.
  269. Defaults to untrusted:
  270. html characters (and character entities) are escaped
  271. so will not be rendered. This departs from markdown spec
  272. which allows embedded html."""
  273. try:
  274. with open(srcfile, 'r') as f:
  275. src = f.read()
  276. md = markdown.Markdown(extensions=['toc', 'codehilite'])
  277. md.convert(src)
  278. toc = md.toc
  279. if trusted == True:
  280. content = markdown.markdown(src,
  281. extensions=['codehilite',
  282. TocExtension(permalink=True)])
  283. else:
  284. content = markdown.markdown(escape(src),
  285. extensions=['codehilite',
  286. TocExtension(permalink=True)])
  287. return content, toc
  288. except IOError:
  289. return None
  290. def render_pygments(srcfile, lexer_type):
  291. """returns src(file) marked up with pygments"""
  292. if lexer_type == 'python':
  293. with open(srcfile, 'r') as f:
  294. src = f.read()
  295. contents = highlight(src, PythonLexer(), HtmlFormatter())
  296. elif lexer_type == 'html':
  297. with open(srcfile, 'r') as f:
  298. src = f.read()
  299. contents = highlight(src, HtmlDjangoLexer(), HtmlFormatter())
  300. # default to TextLexer for everything else
  301. else:
  302. with open(srcfile, 'r') as f:
  303. src = f.read()
  304. contents = highlight(src, TextLexer(), HtmlFormatter())
  305. return contents
  306. def get_pygments_css(style=None):
  307. """returns css for pygments, use as internal_css"""
  308. if style is None:
  309. style = 'friendly'
  310. return HtmlFormatter(style=style).get_style_defs('.highlight')
  311. def heading(text, level):
  312. """return as html heading at h[level]"""
  313. heading_level = 'h%s' % str(level)
  314. return '\n<%s>%s</%s>\n' % (heading_level, text, heading_level)
  315. def posts_list(ordenar_por_fecha=True, ordenar_por_nombre=False):
  316. '''Retorna una lista con los nombres de archivos con extension .md
  317. dentro de la cappeta src/posts, por defecto retorna una lista con
  318. la tupla (nombre_archivo, fecha_subida)'''
  319. lista_posts = []
  320. lp = []
  321. if ordenar_por_nombre:
  322. try:
  323. ow = os.walk("monomotapa/src/posts")
  324. p , directorios , archs = ow.__next__()
  325. except OSError:
  326. print ("[posts] - Error: Cant' os.walk() on monomotapa/src/posts except OSError")
  327. else:
  328. for arch in archs:
  329. if arch.endswith(".md") and not arch.startswith("#") \
  330. and not arch.startswith("~") and not arch.startswith("."):
  331. lista_posts.append(arch)
  332. lista_posts.sort()
  333. return lista_posts
  334. if ordenar_por_fecha:
  335. try:
  336. ow = os.walk("monomotapa/src/posts")
  337. p,d,files=ow.__next__()
  338. except OSError:
  339. print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
  340. else:
  341. for f in files:
  342. nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
  343. if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
  344. secs_modificacion = SecsModificacionPostDesdeJson(f, json_pattrs)
  345. ultima_modificacion = os.path.getmtime(nombre_con_ruta)
  346. lp.append((secs_modificacion, ultima_modificacion, f))
  347. lp.sort()
  348. lp.reverse()
  349. # colocando fecha en formato
  350. for tupla in lp:
  351. #fecha = strftime("a, %d %b %Y %H:%M:%S", ctime(tupla[0]))
  352. cfecha = ctime(tupla[1])
  353. #fecha = strptime("%a %b %d %H:%M:%S %Y", cfecha)
  354. lista_posts.append((cfecha, tupla[2]))
  355. return lista_posts
  356. def categorias_list(categoria=None):
  357. """ Rotorna una lista con los nombres de posts y el numero de posts que
  358. pertenecen a la categoria dada o a cada categoria.
  359. Las categorias se obtienen analizando la primera linea de cada archivo .md
  360. an la carpeta donde se almacenan los posts.
  361. Si no se especifica `categoria' cada elemento de la lista devuelta es:
  362. (nombre_categoria, numero_posts, [nombres_posts])
  363. si se especifica `categoria' cada elemento de la lista devuelta es:
  364. (numero_posts, [nombres_posts]
  365. """
  366. lista_posts = posts_list(ordenar_por_nombre=True)
  367. lista_categorias = []
  368. if categoria is not None:
  369. c = 0
  370. posts = []
  371. for post in lista_posts:
  372. nombre_arch = "monomotapa/src/posts/"+post
  373. with open(nombre_arch, 'r') as file:
  374. linea = file.readline().decode("utf-8")
  375. lc = linea.split("[#")[1:]
  376. for cad in lc:
  377. cat = cad.split("]")[0]
  378. if cat == categoria:
  379. c += 1
  380. posts.append(post)
  381. lista_categorias = (c, posts)
  382. return lista_categorias
  383. dic_categorias = {}
  384. for post in lista_posts:
  385. nombre_arch = "monomotapa/src/posts/"+post
  386. with open(nombre_arch, 'r') as fil:
  387. linea = fil.readline().decode('utf-8') # primera linea
  388. # extrayendo las categorias y registrando sus ocurrencias
  389. # ejemplo: catgorías: [#reflexión](categoria/reflexion) [#navidad](categoria/navidad)
  390. # extrae: [reflexion,navidad]
  391. lc = linea.split("[#")[1:]
  392. for cad in lc:
  393. cat = cad.split("]")[0]
  394. if cat not in dic_categorias:
  395. dic_categorias[cat] = (1,[post]) # nuevo registro por categoria
  396. else:
  397. tupla = dic_categorias[cat]
  398. c = tupla[0] + 1
  399. lis = tupla[1]
  400. if post not in lis:
  401. lis.append(post)
  402. dic_categorias[cat] = (c, lis)
  403. # convirtiendo en lista
  404. for k, v in dic_categorias.iteritems():
  405. lista_categorias.append((k,v[0],v[1]))
  406. lista_categorias.sort()
  407. lista_categorias.reverse()
  408. return lista_categorias
  409. def cabeza_post(archivo , max_caracteres=250, categorias=True):
  410. """ Devuelve las primeras lineas de una archivo de post (en formato markdown)
  411. con un maximo numero de caracteres excluyendo titulos en la cabeza devuelta.
  412. Si se especifica `categorias' en True
  413. Se devuelve una lista de la forma:
  414. (cabeza_post, categorias)
  415. donde categorias son cadenas con los nombres de las categorias a la que
  416. pertenece el post
  417. """
  418. cabeza_post = ""
  419. cats = []
  420. with open(os.path.join("monomotapa/src/posts",archivo)) as file:
  421. # analizando si hay titulos al principio
  422. # Se su pone que la primera linea es de categorias
  423. for linea in file.readlines():
  424. linea = linea.decode("utf-8")
  425. if linea.startswith(u"categorías:") or linea.startswith("categorias"):
  426. if categorias:
  427. cats = categoriasDePost(archivo)
  428. #cats = categorias_de_post(archivo)
  429. else:
  430. # evitando h1, h2
  431. if linea.startswith("##") or linea.startswith("#"):
  432. cabeza_post += " "
  433. else:
  434. cabeza_post += linea
  435. if len(cabeza_post) >= max_caracteres:
  436. break
  437. cabeza_post = cabeza_post[0:max_caracteres-1]
  438. if categorias:
  439. return (cabeza_post, cats)
  440. return cabeza_post
  441. def ultima_modificacion_archivo(archivo):
  442. """ Retorna una cadena indicando la fecha de ultima modificacion del
  443. `archivo' dado, se asume que `archivo' esta dentro la carpeta "monomotapa/src"
  444. Retorna una cadena vacia en caso de no poder abrir `archivo'
  445. """
  446. try:
  447. ts = strptime(ctime(os.path.getmtime("monomotapa/src/"+archivo+".md")))
  448. return strftime("%d %B %Y", ts)
  449. except OSError:
  450. return ""
  451. def SecsModificacionPostDesdeJson(archivo, dict_json):
  452. ''' dado el post con nombre 'archivo' busca en 'dict_json' el
  453. attribute 'date' y luego obtiene los segundos totales desde
  454. esa fecha.
  455. Si no encuentra 'date' para 'archivo' en 'dict.json'
  456. retorna los segundos totales desde la ultima modificacion
  457. del archivo de post directamente (usa os.path.getmtime)
  458. '''
  459. nombre = archivo.split('.md')[0] # no contar extension .md
  460. nombre_con_ruta = os.path.join("monomotapa/src/posts", archivo)
  461. date_str = dict_json.get('posts/'+nombre, {}).\
  462. get('attributes',{}).\
  463. get('date','')
  464. if date_str == '':
  465. # el post no tiene "date" en pages.json
  466. return os.path.getmtime(nombre_con_ruta)
  467. else:
  468. time_struct = strptime(date_str, '%Y-%m-%d')
  469. dt = datetime.datetime.fromtimestamp(mktime(time_struct))
  470. return (dt - datetime.datetime(1970,1,1)).total_seconds()
  471. def noticias_recientes(cantidad=11, max_caracteres=250,
  472. categoria=None, pagina=0):
  473. '''Devuelve una lista con hasta `cantidad' de posts mas recientes,
  474. un maximo de `max_caracteres' de caracteres del principio del post y
  475. el numero total de posts encontrados
  476. Si se proporciona `categoria' devuelve la lista de posts solamente
  477. pertenecientes esa categoria.
  478. Si `pagina' > 0 se devulve hasta `cantidad' numero de posts en el
  479. rango de [ cantidad*pagina : cantidad*(pagina+1)]
  480. Cada elemento de la lista devuelta contiene:
  481. (nombre_post, ultima_modificacion, cabeza_archivo, categorias)
  482. Al final se retorna: (lista_posts, numero_de_posts)
  483. '''
  484. lista_posts = []
  485. lp = []
  486. num_posts = 0
  487. posts_en_categoria = []
  488. if categoria is not None:
  489. #posts_en_categoria = categorias_list(categoria)[1]
  490. posts_en_categoria = categoriasList(categoria)[1]
  491. # categoria especial fotos
  492. if categoria == "fotos":
  493. l = []
  494. for p in posts_en_categoria:
  495. l.append(p + '.md')
  496. posts_en_categoria = l
  497. try:
  498. ow = os.walk("monomotapa/src/posts")
  499. p,d,files = ow.__next__()
  500. #p,d,files=ow.next()
  501. except OSError:
  502. print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
  503. else:
  504. for f in files:
  505. nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
  506. if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
  507. secs_modificacion = SecsModificacionPostDesdeJson(f, json_pattrs)
  508. ultima_modificacion = os.path.getmtime(nombre_con_ruta)
  509. previewChars = json_pattrs.get('posts/'+f[:-3], {}).\
  510. get('attributes', {}).\
  511. get('preview-chars', max_caracteres)
  512. if categoria is not None:
  513. if f in posts_en_categoria:
  514. lp.append((secs_modificacion,
  515. ultima_modificacion,
  516. previewChars,
  517. f))
  518. num_posts += 1
  519. else:
  520. lp.append((secs_modificacion,
  521. ultima_modificacion,
  522. previewChars,
  523. f))
  524. num_posts += 1
  525. lp.sort()
  526. lp.reverse()
  527. # seleccionado por paginas
  528. lp = lp[cantidad*pagina : cantidad*(pagina+1)]
  529. # colocando fecha en formato
  530. for tupla in lp:
  531. cfecha = ctime(tupla[1])
  532. nombre_post = tupla[3].split(os.sep)[-1]
  533. previewChars = tupla[2]
  534. #contenido = cabeza_post(tupla[3], max_caracteres=previewChars)[0]
  535. #categorias = cabeza_post(tupla[3], max_caracteres=previewChars)[1]
  536. contenido = cabezaPost(tupla[3], max_caracteres=previewChars)[0]
  537. categorias = cabezaPost(tupla[3], max_caracteres=previewChars)[1]
  538. cabeza_archivo = markdown.markdown(escape(contenido + ' ...'))
  539. lista_posts.append((nombre_post[:-3], cfecha, \
  540. cabeza_archivo, categorias))
  541. return (lista_posts, num_posts)
  542. def noticias_relacionadas(cantidad=5, nombre=None):
  543. """Retorna una lista con posts relacionadas, es decir que tienen son de las
  544. mismas categorias que el post con nombre `nombre'.
  545. Cada elemento de la lista de posts contiene el nombre del post
  546. """
  547. #categorias = categorias_de_post(nombre) ## TODO: corregir categorias de post
  548. categorias = categoriasDePost(nombre)
  549. numero = 0
  550. if categorias is None:
  551. return None
  552. posts = []
  553. for categoria in categorias:
  554. #lista = categorias_list(categoria)[1] # nombres de posts
  555. lista = categoriasList(categoria)[1]
  556. numero += len(lista)
  557. for nombre_post in lista:
  558. if nombre_post + '.md' != nombre:
  559. posts.append(nombre_post)
  560. if numero >= cantidad:
  561. return posts
  562. return posts
  563. def rss_ultimos_posts_jinja(cantidad=15):
  564. """Retorna una lista de los ultimos posts preparados para
  565. ser renderizados (usando jinja) como un feed rss
  566. Examina cada post del mas reciente al menos reciente, en
  567. total `cantidad' posts. Por cada post devuelve:
  568. id: id which identifies the entry using a
  569. universally unique and permanent URI
  570. author: Get or set autor data. An author element is a dict containing a
  571. name, an email adress and a uri.
  572. category: A categories has the following fields:
  573. - *term* identifies the category
  574. - *scheme* identifies the categorization scheme via a URI.
  575. - *label* provides a human-readable label for display
  576. comments: Get or set the the value of comments which is the url of the
  577. comments page for the item.
  578. content: Get or set the cntent of the entry which contains or links to the
  579. complete content of the entry.
  580. description(no contiene): Get or set the description value which is the item synopsis.
  581. Description is an RSS only element.
  582. link: Get or set link data. An link element is a dict with the fields
  583. href, rel, type, hreflang, title, and length. Href is mandatory for
  584. ATOM.
  585. pubdate(no contiene): Get or set the pubDate of the entry which indicates when the entry
  586. was published.
  587. title: the title value of the entry. It should contain a human
  588. readable title for the entry.
  589. updated: the updated value which indicates the last time the entry
  590. was modified in a significant way.
  591. """
  592. lista_posts = []
  593. lp = []
  594. num_posts = 0
  595. try:
  596. ow = os.walk("monomotapa/src/posts")
  597. p,d,files=ow.__next__()
  598. except OSError:
  599. print ("[posts] - Error: Can't os.walk() on monomotapa/src/posts except OSError.")
  600. else:
  601. for f in files:
  602. nombre_con_ruta = os.path.join("monomotapa/src/posts", f)
  603. if not f.endswith("~") and not f.startswith("#") and not f.startswith("."):
  604. lp.append((os.path.getmtime(nombre_con_ruta), f))
  605. num_posts += 1
  606. if num_posts > cantidad:
  607. break
  608. lp.sort()
  609. lp.reverse()
  610. # colocando fecha en formato
  611. for tupla in lp:
  612. nombre_post = tupla[1].split(os.sep)[-1]
  613. #contenido = cabeza_post(tupla[1], max_caracteres=149999)
  614. contenido = cabezaPost(tupla[1], max_caracteres=149999)
  615. id_post = "https://rmgss.net/posts/"+nombre_post[:-3]
  616. #categorias = categorias_de_post(nombre_post)
  617. categorias = categoriasDePost(nombre_post)
  618. dict_categorias = {}
  619. c = ""
  620. for cat in categorias:
  621. c += cat + " "
  622. dict_categorias['label'] = c
  623. #dict_categorias['term'] = c
  624. html = markdown.markdown(escape(contenido))
  625. link = id_post
  626. pubdate = ctime(tupla[0])
  627. title = titulo_legible(nombre_post[:-3]) # no incluir '.md'
  628. updated = pubdate
  629. dict_feed_post = {
  630. "id":id_post,
  631. "author": "Rodrigo Garcia",
  632. "category" : categorias,
  633. "content": html,
  634. "link" : id_post,
  635. "updated" : updated,
  636. "title": title
  637. }
  638. lista_posts.append(dict_feed_post)
  639. return lista_posts
  640. ###### Define routes
  641. @app.errorhandler(404)
  642. def page_not_found(e):
  643. """ provides basic 404 page"""
  644. defaults = get_page_attributes('defaults.json')
  645. try:
  646. css = defaults['css']
  647. except KeyError:
  648. css = None
  649. pages = get_page_attributes('pages.json')
  650. if '404' in pages:
  651. if'css' in pages['404']:
  652. css = pages['404']['css']
  653. return render_template('static.html',
  654. title = "404::page not found", heading = "Page Not Found",
  655. navigation = top_navigation('404'),
  656. css = css,
  657. contents = Markup(
  658. "This page is not there, try somewhere else.")), 404
  659. @app.route('/users/', defaults={'page': 1})
  660. @app.route('/users/page/<int:page>')
  661. @app.route("/", defaults={'pagina':0})
  662. @app.route('/<int:pagina>')
  663. def index(pagina):
  664. """provides index page"""
  665. index_page = Page('index')
  666. lista_posts_recientes, total_posts = noticias_recientes(pagina=pagina)
  667. index_page.contexto['lista_posts_recientes'] = lista_posts_recientes
  668. index_page.contexto['total_posts'] = total_posts
  669. index_page.contexto['pagina_actual'] = int(pagina)
  670. return index_page.generate_page()
  671. # default route is it doe not exist elsewhere
  672. @app.route("/<path:page>")
  673. def staticpage(page):
  674. """ display a static page rendered from markdown in src
  675. i.e. displays /page or /page/ as long as src/page.md exists.
  676. srcfile, title and heading may be set in the pages global
  677. (ordered) dictionary but are not required"""
  678. static_page = Page(page)
  679. return static_page.generate_page()
  680. @app.route("/posts/<page>")
  681. def rposts(page):
  682. """ Mustra las paginas dentro la carpeta posts, no es coincidencia
  683. que en este ultimo directorio se guarden los posts.
  684. Ademas incrusta en el diccionario de contexto de la pagina la
  685. fecha de ultima modificacion del post
  686. """
  687. static_page = Page("posts/"+page)
  688. ultima_modificacion = ultima_modificacion_archivo("posts/"+page)
  689. static_page.contexto['relacionadas'] = noticias_relacionadas(nombre=page+".md")
  690. static_page.contexto['ultima_modificacion'] = ultima_modificacion
  691. static_page.exclude_toc = False # no excluir Índice de contenidos
  692. return static_page.generate_page()
  693. @app.route("/posts")
  694. def indice_posts():
  695. """ Muestra una lista de todos los posts
  696. """
  697. lista_posts_fecha = posts_list()
  698. #lista_posts_categoria = categorias_list()
  699. lista_posts_categoria = categoriasList()
  700. static_page = Page("posts")
  701. static_page.contexto['lista_posts_fecha'] = lista_posts_fecha
  702. static_page.contexto['lista_posts_categoria'] = lista_posts_categoria
  703. return static_page.generate_page()
  704. @app.route("/posts/categorias")
  705. def lista_categorias():
  706. """ Muestra una lista de las categorias , los posts pertenecen
  707. a cada una y un conteo"""
  708. #lista_categorias = categorias_list()
  709. lista_categorias = categoriasList()
  710. static_page = Page("categorias")
  711. static_page.contexto['lista_posts_categoria'] = lista_categorias
  712. #return (str(lista_categorias))
  713. return static_page.generate_page()
  714. @app.route("/posts/categoria/<categoria>")
  715. def posts_de_categoria(categoria):
  716. """ Muestra los posts que perteneces a la categoria dada
  717. """
  718. lista_posts = []
  719. if categoria == "fotos": # caegoria especial fotos
  720. lista_posts, total_posts = noticias_recientes(max_caracteres=1250,categoria=categoria)
  721. static_page = Page("fotos")
  722. static_page.contexto['categoria_actual'] = categoria
  723. static_page.contexto['lista_posts_recientes'] = lista_posts
  724. return static_page.generate_page()
  725. #lista_posts = categorias_list(categoria=categoria)
  726. lista_posts = categoriasList(categoria=categoria)
  727. static_page = Page("categorias")
  728. static_page.contexto['categoria_actual'] = categoria
  729. static_page.contexto['lista_posts_categoria'] = lista_posts
  730. return static_page.generate_page()
  731. @app.route("/posts/recientes", defaults={'pagina':0})
  732. @app.route("/posts/recientes/<int:pagina>")
  733. def posts_recientes(pagina):
  734. """ muestra una lista de los posts mas recientes
  735. TODO: terminar
  736. """
  737. lista_posts, total_posts = noticias_recientes(max_caracteres=368,
  738. pagina=pagina)
  739. static_page = Page("recientes")
  740. static_page.contexto['lista_posts_recientes'] = lista_posts
  741. static_page.contexto['total_posts'] = total_posts
  742. static_page.contexto['pagina_actual'] = pagina
  743. #return (str(lista_posts))
  744. return static_page.generate_page()
  745. @app.route("/contacto", methods=['GET'])
  746. def contacto():
  747. tupla_captcha = captcha_pregunta_opciones_random()
  748. if tupla_captcha is None:
  749. return ("<br>Parece un error interno!</br>")
  750. pregunta = tupla_captcha[0]
  751. opciones = tupla_captcha[1]
  752. static_page = Page("contacto")
  753. static_page.contexto['pregunta'] = pregunta
  754. static_page.contexto['opciones'] = opciones
  755. return static_page.generate_page()
  756. @app.route("/contactoe", methods=['POST'])
  757. def comprobar_mensaje():
  758. """ Comprueba que el mensaje enviado por la caja de texto sea valido
  759. y si lo es, guarda un archivo de texto con los detalles"""
  760. errors = []
  761. if request.method == "POST":
  762. # comprobando validez
  763. nombre = request.form["nombre"]
  764. dir_respuesta = request.form['dir_respuesta']
  765. mensaje = request.form['mensaje']
  766. pregunta = request.form['pregunta']
  767. respuesta = request.form['respuesta']
  768. if len(mensaje) < 2 or mensaje.startswith(" "):
  769. errors.append("Mensaje invalido")
  770. if not captcha_comprobar_respuesta(pregunta, respuesta):
  771. errors.append("Captcha invalido")
  772. if len(errors) > 0:
  773. return str(errors)
  774. # guardando texto
  775. texto = "Remitente: "+nombre
  776. texto += "\nResponder_a: "+dir_respuesta
  777. texto += "\n--- mensaje ---\n"
  778. texto += mensaje
  779. # TODO: cambiar a direccion especificada en archivo de configuracion
  780. dt = datetime.datetime.now()
  781. nombre = "m_"+str(dt.day)+"_"+str(dt.month)+\
  782. "_"+str(dt.year)+"-"+str(dt.hour)+\
  783. "-"+str(dt.minute)+"-"+str(dt.second)
  784. with open(os.path.join("fbs",nombre), "wb") as f:
  785. f.write(texto.encode("utf-8"))
  786. return redirect("/mensaje_enviado", code=302)
  787. @app.route("/mensaje_enviado")
  788. def mensaje_enviado():
  789. static_page = Page("mensaje_enviado")
  790. return static_page.generate_page()
  791. @app.route("/rss")
  792. def rss_feed():
  793. """Genera la cadena rss con las 15 ultimas noticias del sitio
  794. TODO: Agregar mecenismo para no generar los rss feeds y solo
  795. devolver el archivo rss.xml generado anteriormente. Esto
  796. quiere decir solamente generar el rss_feed cuando se haya hecho
  797. un actualizacion en los posts mas reciente que la ultima vez
  798. que se genero el rss_feed
  799. """
  800. #return str(rss_ultimos_posts_jinja())
  801. return render_template("rss.html",
  802. contents = rss_ultimos_posts_jinja())
  803. #**vars(self)
  804. #)
  805. ##### specialized pages
  806. @app.route("/source")
  807. def source():
  808. """Display source files used to render a page"""
  809. source_page = Page('source', title = "view the source code",
  810. #heading = "Ver el código fuente",
  811. heading = "Ver el codigo fuente",
  812. internal_css = get_pygments_css())
  813. page = request.args.get('page')
  814. # get source for markdown if any. 404's for non-existant markdown
  815. # unless special page eg source
  816. pagesrc = source_page.get_page_src(page, 'src', 'md')
  817. special_pages = ['source', 'unit-tests', '404']
  818. if not page in special_pages and pagesrc is None:
  819. abort(404)
  820. # set enable_unit_tests to true in config.json to allow
  821. # unit tests to be run through the source page
  822. if app.config['enable_unit_tests']:
  823. contents = '''<p><a href="/unit-tests" class="button">Run unit tests
  824. </a></p>'''
  825. # render tests.py if needed
  826. if page == 'unit-tests':
  827. contents += heading('tests.py', 2)
  828. contents += render_pygments('tests.py', 'python')
  829. else:
  830. contents = ''
  831. # render views.py
  832. contents += heading('views.py', 2)
  833. contents += render_pygments(source_page.get_page_src('views.py'),
  834. 'python')
  835. # render markdown if present
  836. if pagesrc:
  837. contents += heading(os.path.basename(pagesrc), 2)
  838. contents += render_pygments(pagesrc, 'markdown')
  839. # render jinja templates
  840. contents += heading('base.html', 2)
  841. contents += render_pygments(
  842. source_page.get_page_src('base.html', 'templates'), 'html')
  843. template = source_page.get_template(page)
  844. contents += heading(template, 2)
  845. contents += render_pygments(
  846. source_page.get_page_src(template, 'templates'), 'html')
  847. return source_page.generate_page(contents)
  848. # @app.route("/unit-tests")
  849. # def unit_tests():
  850. # """display results of unit tests"""
  851. # unittests = Page('unit-tests', heading = "Test Results",
  852. # internal_css = get_pygments_css())
  853. # # exec unit tests in subprocess, capturing stderr
  854. # capture = subprocess.Popen(["python", "tests.py"],
  855. # stdout = subprocess.PIPE, stderr = subprocess.PIPE)
  856. # output = capture.communicate()
  857. # results = output[1]
  858. # contents = '''<p>
  859. # <a href="/unit-tests" class="button">Run unit tests</a>
  860. # </p><br>\n
  861. # <div class="output" style="background-color:'''
  862. # if 'OK' in results:
  863. # color = "#ddffdd"
  864. # result = "TESTS PASSED"
  865. # else:
  866. # color = "#ffaaaa"
  867. # result = "TESTS FAILING"
  868. # contents += ('''%s">\n<strong>%s</strong>\n<pre>%s</pre>\n</div>\n'''
  869. # % (color, result, results))
  870. # # render test.py
  871. # contents += heading('tests.py', 2)
  872. # contents += render_pygments('tests.py', 'python')
  873. # return unittests.generate_page(contents)