Mercurial > libervia-web
view src/server/blog.py @ 712:bf562fb9c273
server_side: use Jinja2 template engine for static blog
author | souliane <souliane@mailoo.org> |
---|---|
date | Mon, 13 Jul 2015 18:11:38 +0200 |
parents | e9a6cbb924e6 |
children | 29b84af2ff7b |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # Libervia: a Salut à Toi frontend # Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson <goffi@goffi.org> # Copyright (C) 2013, 2014, 2015 Adrien Cossa <souliane@mailoo.org> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General 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 Affero 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/>. from sat.core.i18n import _, D_ from sat_frontends.tools.strings import addURLToText from sat.core.log import getLogger from django.conf.urls import url log = getLogger(__name__) from twisted.internet import defer from twisted.web import server from twisted.web.resource import Resource from twisted.words.protocols.jabber.jid import JID from jinja2 import Environment, PackageLoader from datetime import datetime from sys import path import uuid import re import os from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML from libervia.server.constants import Const as C class TemplateProcessor(object): THEME = 'default' def __init__(self, host): self.host = host # add Libervia's themes directory to the python path path.append(os.path.dirname(os.path.normpath(self.host.themes_dir))) themes = os.path.basename(os.path.normpath(self.host.themes_dir)) self.env = Environment(loader=PackageLoader(themes, self.THEME)) def useTemplate(self, request, tpl, data=None): root_url = '../' * len(request.postpath) theme_url = os.path.join(root_url, 'themes', self.THEME) data_ = {'images': os.path.join(theme_url, 'images'), 'styles': os.path.join(theme_url, 'styles'), } if data: data_.update(data) template = self.env.get_template('%s.html' % tpl) return template.render(**data_).encode('utf-8') class MicroBlog(Resource, TemplateProcessor): isLeaf = True def __init__(self, host): self.host = host Resource.__init__(self) TemplateProcessor.__init__(self, host) self.host.bridge.register('entityDataUpdated', self.entityDataUpdatedCb) self.host.bridge.register('actionResult', self.actionResultCb) # FIXME: actionResult is to be removed self.avatars_cache = {} self.waiting_deferreds = {} def entityDataUpdatedCb(self, entity_jid_s, key, value, dummy): """Retrieve the avatar we've been waiting for and fires the callback for self.getAvatar to return. @param entity_jid_s (str): JID of the contact @param key (str): entity data key @param value (str): entity data value @param dummy (str): that would be C.SERVICE_PROFILE """ if key != 'avatar': return entity_jid_s = entity_jid_s.lower() log.debug(_(u"Received a new avatar for entity %s") % entity_jid_s) avatar = C.AVATARS_DIR + value self.avatars_cache[entity_jid_s] = avatar try: self.waiting_deferreds[entity_jid_s][1].callback(avatar) del self.waiting_deferreds[entity_jid_s] except KeyError: pass def actionResultCb(self, answer_type, action_id, data, dummy): """Fires the callback for self.getAvatar to return @param answer_type (str): 'SUPPRESS' or another value that we would ignore @param action_id (str): the request ID @param data (dict): ignored @param dummy (str): that would be C.SERVICE_PROFILE """ # FIXME: actionResult is to be removed. For now we use it to get notified # when the requested vCard hasn't been found. Replace with the new system. if answer_type != 'SUPPRESS': return try: entity_jid_s = [key for (key, value) in self.waiting_deferreds.items() if value[0] == action_id][0] except IndexError: # impossible to guess the entity return log.debug(_(u"Using default avatar for entity %s") % entity_jid_s) self.avatars_cache[entity_jid_s] = C.DEFAULT_AVATAR_URL self.waiting_deferreds[entity_jid_s][1].callback(C.DEFAULT_AVATAR_URL) del self.waiting_deferreds[entity_jid_s] def getAvatar(self, profile): """Get the avatar of the given profile @param profile(unicode): %(doc_profile)s @return: deferred avatar path, relative to the server's root """ jid_s = (profile + '@' + self.host.bridge.getNewAccountDomain()).lower() if jid_s in self.avatars_cache: return defer.succeed(self.avatars_cache[jid_s]) # FIXME: request_id is no more needed when actionResult is removed request_id = self.host.bridge.getCard(jid_s, C.SERVICE_PROFILE) self.waiting_deferreds[jid_s] = (request_id, defer.Deferred()) return self.waiting_deferreds[jid_s][1] def render_GET(self, request): if not request.postpath: return self.useTemplate(request, "static_blog_error", {'message': "You must indicate a nickname"}) prof_requested = request.postpath[0] #TODO : char check: only use alphanumeric chars + some extra(_,-,...) here prof_found = self.host.bridge.getProfileName(prof_requested) if not prof_found or prof_found == C.SERVICE_PROFILE: return self.useTemplate(request, "static_blog_error", {'message': "Invalid nickname"}) d = defer.Deferred() JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d.callback, errback=d.errback)) d.addCallbacks(lambda pub_jid_s: self.gotJID(pub_jid_s, request, prof_found)) return server.NOT_DONE_YET def gotJID(self, pub_jid_s, request, profile): pub_jid = JID(pub_jid_s) d = defer.Deferred() self.parseURLParams(request) item_id, rsm_ = request.item_id, request.rsm_data max_items = int(rsm_['max_']) if request.atom: d.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, profile], None) self.host.bridge.getGroupBlogsAtom(pub_jid.userhost(), rsm_, C.SERVICE_PROFILE, d.callback, d.errback) return d.addCallbacks(self.render_html_blog, self.render_error_blog, [request, profile], None, [request, profile], None) if item_id: if max_items > 0: # display one message and its comments self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], {}, max_items, C.SERVICE_PROFILE, d.callback, d.errback) else: # display one message, count its comments self.host.bridge.getGroupBlogs(pub_jid.userhost(), [item_id], {}, True, C.SERVICE_PROFILE, d.callback, d.errback) else: if max_items == 1: # display one message and its comments self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [], rsm_, C.RSM_MAX_COMMENTS, C.SERVICE_PROFILE, d.callback, d.errback) else: # display the last messages, count their comments self.host.bridge.getGroupBlogs(pub_jid.userhost(), [], rsm_, True, C.SERVICE_PROFILE, d.callback, d.errback) def parseURLParams(self, request): """Parse the request URL parameters. @param request: HTTP request """ request.item_id = None request.atom = False if len(request.postpath) > 1: if request.postpath[1] == 'atom.xml': # return the atom feed request.atom = True else: try: # check if the given path is a valid UUID uuid.UUID(request.postpath[1]) request.item_id = request.postpath[1] except ValueError: pass self.parseURLParamsRSM(request) request.display_single = (request.item_id is not None) or int(request.rsm_data['max_']) == 1 def parseURLParamsRSM(self, request): """Parse RSM request data from the URL parameters. @param request: HTTP request """ rsm_ = {} try: rsm_['max_'] = request.args['max'][0] except (ValueError, KeyError): rsm_['max_'] = unicode(C.RSM_MAX_ITEMS if request.item_id else C.RSM_MAX_COMMENTS) try: rsm_['index'] = request.args['index'][0] except (ValueError, KeyError): try: rsm_['before'] = request.args['before'][0] except KeyError: try: rsm_['after'] = request.args['after'][0] except KeyError: pass request.rsm_data = rsm_ def render_html_blog(self, mblog_data, request, profile): """Retrieve the user parameters before actually rendering the static blog @param mblog_data (list): couple (list, dict) with: - a list of microblog data, or a list of couple containing: - microblog data (main item) - couple (comments data, RSM response data for the comments) - RSM response data for the main items @param request: HTTP request @param profile """ d_list = [] options = {} def getCallback(param_name): d = defer.Deferred() d.addCallback(lambda value: options.update({param_name: value})) d_list.append(d) return d.callback eb = lambda failure: self.render_error_blog(failure, request, profile) self.getAvatar(profile).addCallbacks(getCallback('avatar'), eb) for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION): self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb) cb = lambda dummy: self.__render_html_blog(mblog_data, options, request, profile) defer.DeferredList(d_list).addCallback(cb) def __render_html_blog(self, mblog_data, options, request, profile): """Actually render the static blog. If mblog_data is a list of dict, we are missing the comments items so we just display the main items. If mblog_data is a list of couple, each couple is associating a main item data with the list of its comments, so we render all. @param mblog_data (list): couple (list, dict) with: - a list of microblog data, or a list of couple containing: - microblog data (main item) - couple (comments data, RSM response data for the comments) - RSM response data for the main items @param options: dict defining the blog's parameters @param request: the HTTP request @profile """ if not isinstance(options, dict): options = {} user = sanitizeHtml(profile).encode('utf-8') root_url = '../' * len(request.postpath) base_url = root_url + 'blog/' + user def getOption(key): return sanitizeHtml(options[key]).encode('utf-8') if key in options else '' def getImageParams(key, default, alt): """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" url = options[key].encode('utf-8') if key in options else '' regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" if re.match(regexp, url): url = url else: url = default return BlogImage(url, alt) avatar = os.path.normpath(root_url + getOption('avatar')) title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user data = {'base_url': base_url, 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS), 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), 'title': title, 'favicon': avatar, 'banner_img': getImageParams(C.STATIC_BLOG_PARAM_BANNER, avatar, title) } mblog_data, main_rsm = mblog_data mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data] mblog_data.sort(key=lambda entry: (-float(entry[0].get('updated', 0)))) data['navlinks'] = NavigationLinks(request, mblog_data, main_rsm, base_url) data['messages'] = [BlogMessage(request, base_url, entry, comments[0]) for entry, comments in mblog_data] request.write(self.useTemplate(request, 'static_blog', data)) request.finish() def render_atom_feed(self, feed, request): request.write(feed.encode('utf-8')) request.finish() def render_error_blog(self, error, request, profile): request.write(self.useTemplate(request, "static_blog_error", {'message': "Can't access requested data"})) request.finish() class NavigationLinks(object): def __init__(self, request, mblog_data, rsm_data, base_url): """Build the navigation links. @param mblog_data (dict): the microblogs that are displayed on the page @param rsm_data (dict): rsm data @param base_url (unicode): the base URL for this user's blog @return: dict """ for key in ('later_message', 'later_messages', 'older_message', 'older_messages'): count = int(rsm_data.get('count', 0)) setattr(self, key, '') # key must exist when using the template if count <= 0 or (request.display_single == key.endswith('s')): continue index = int(rsm_data['index']) link_data = {'base_url': base_url, 'suffix': ''} if key.startswith('later_message'): if index <= 0: continue link_data['item_id'] = rsm_data['first'] link_data['post_arg'] = 'before' else: if index + len(mblog_data) >= count: continue link_data['item_id'] = rsm_data['last'] link_data['post_arg'] = 'after' if request.display_single: link_data['suffix'] = '&max=1' link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data setattr(self, key, BlogLink(link, key, key.replace('_', ' '))) class BlogImage(object): def __init__(self, url_, alt): self.url = url_ self.alt = alt class BlogLink(object): def __init__(self, url_, style, text): self.url = url_ self.style = style self.text = text class BlogMessage(object): def __init__(self, request, base_url, entry, comments=None): """ @param request: HTTP request @param base_url (unicode): the base URL @param entry (dict{unicode:unicode]): microblog entry received from the backend @param comments (list[dict]): comments """ timestamp = float(entry.get('published', 0)) is_comment = entry['type'] == 'comment' self.date = datetime.fromtimestamp(timestamp) self.type = entry['type'] self.style = 'mblog_comment' if entry['type'] == 'comment' else '' self.content = self.getText(entry, 'content') if is_comment: self.author = (_("from %s") % entry['author']).encode('utf-8') else: self.author = ' ' self.url = (u"%s/%s" % (base_url, entry['id'])).encode('utf-8') self.title = self.getText(entry, 'title') comments_count = int(entry['comments_count']) count_text = lambda count: D_('comments') if count > 1 else D_('comment') self.comments_text = "%s %s" % (comments_count, count_text(comments_count)) delta = comments_count - len(comments) if request.display_single and delta > 0: prev_url = "%s?max=%s" % (self.url, entry['comments_count']) prev_text = D_("show %(count)d previous %(comments)s") % \ {'count': delta, 'comments': count_text(delta)} self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text) if comments: comments.sort(key=lambda entry: float(entry.get('published', 0))) self.comments = [BlogMessage(request, base_url, comment) for comment in comments] def getText(self, entry, key): if ('%s_xhtml' % key) in entry: return entry['%s_xhtml' % key].encode('utf-8') elif key in entry: processor = addURLToText if key.startswith('content') else sanitizeHtml return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8') return None