# HG changeset patch # User souliane # Date 1436803898 -7200 # Node ID bf562fb9c2736fbb99ab1ef218a4018d6f3769bb # Parent 052d1d19016d690634807e7f4e1a9de0f309a3c4 server_side: use Jinja2 template engine for static blog diff -r 052d1d19016d -r bf562fb9c273 setup.py --- a/setup.py Mon Jul 13 13:33:01 2015 +0200 +++ b/setup.py Mon Jul 13 18:11:38 2015 +0200 @@ -297,6 +297,6 @@ scripts=[], zip_safe=False, dependency_links=['http://www.blarg.net/%7Esteveha/pyfeed-0.7.4.tar.gz', 'http://www.blarg.net/%7Esteveha/xe-0.7.4.tar.gz'], - install_requires=['sat', 'twisted', 'pyfeed', 'xe', 'txJSON-RPC', 'zope.interface', 'pyopenssl'], + install_requires=['sat', 'twisted', 'pyfeed', 'xe', 'txJSON-RPC', 'zope.interface', 'pyopenssl', 'jinja2'], cmdclass={'install': CustomInstall}, ) diff -r 052d1d19016d -r bf562fb9c273 src/server/blog.py --- a/src/server/blog.py Mon Jul 13 13:33:01 2015 +0200 +++ b/src/server/blog.py Mon Jul 13 18:11:38 2015 +0200 @@ -3,6 +3,7 @@ # Libervia: a Salut à Toi frontend # Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson +# Copyright (C) 2013, 2014, 2015 Adrien Cossa # 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 @@ -20,15 +21,16 @@ 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 importlib import uuid import re import os @@ -45,22 +47,22 @@ self.host = host # add Libervia's themes directory to the python path - path.append(os.path.dirname(self.host.themes_dir)) + 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) - # import the theme module - themes = os.path.basename(os.path.dirname(os.path.dirname(self.host.themes_dir))) - theme = importlib.import_module("%s.templates" % self.THEME, themes) - data_ = {'theme': theme_url, - 'images': os.path.join(theme_url, 'images'), + data_ = {'images': os.path.join(theme_url, 'images'), 'styles': os.path.join(theme_url, 'styles'), } if data: data_.update(data) - return getattr(theme, tpl.upper()).encode('utf-8').format(**data_) + + template = self.env.get_template('%s.html' % tpl) + return template.render(**data_).encode('utf-8') class MicroBlog(Resource, TemplateProcessor): @@ -126,20 +128,20 @@ 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 need when actionResult is removed + # 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, "error", {'message': "You must indicate a nickname"}) + return self.useTemplate(request, "static_blog_error", {'message': "You must indicate a nickname"}) prof_requested = request.postpath[0] - #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here + #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, "error", {'message': "Invalid nickname"}) + 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)) @@ -266,43 +268,48 @@ def getOption(key): return sanitizeHtml(options[key]).encode('utf-8') if key in options else '' - def getImageOption(key, default, alt): + 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 - suffix = "
" else: url = default - suffix = "" - return self.useTemplate(request, "banner", {'alt': alt, 'url': url, 'suffix': suffix}) + 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, - 'user': user, 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS), 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), - 'title': getOption(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user, + 'title': title, 'favicon': avatar, - 'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title), - 'title_elt': title, + '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 = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('updated', 0)))) + mblog_data.sort(key=lambda entry: (-float(entry[0].get('updated', 0)))) - data.update(self.getNavigationLinks(request, mblog_data, main_rsm, base_url)) - request.write(self.useTemplate(request, 'header', data)) + 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] - BlogMessages(self.host, request, base_url, mblog_data).render() - - request.write(self.useTemplate(request, "footer", data)) + request.write(self.useTemplate(request, 'static_blog', data)) request.finish() - def getNavigationLinks(self, request, mblog_data, rsm_data, base_url): + 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 @@ -310,10 +317,9 @@ @param base_url (unicode): the base URL for this user's blog @return: dict """ - data = {} for key in ('later_message', 'later_messages', 'older_message', 'older_messages'): count = int(rsm_data.get('count', 0)) - data[key] = '' # key must exist when using the template + setattr(self, key, '') # key must exist when using the template if count <= 0 or (request.display_single == key.endswith('s')): continue @@ -337,33 +343,64 @@ link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data - link_data = {'link': link, 'class': key, 'text': key.replace('_', ' ')} - data[key] = (self.useTemplate(request, 'nav_link', link_data)).encode('utf-8') + setattr(self, key, BlogLink(link, key, key.replace('_', ' '))) - return data + +class BlogImage(object): - def render_atom_feed(self, feed, request): - request.write(feed.encode('utf-8')) - request.finish() + def __init__(self, url_, alt): + self.url = url_ + self.alt = alt + - def render_error_blog(self, error, request, profile): - request.write(self.useTemplate(request, "error", {'message': "Can't access requested data"})) - request.finish() +class BlogLink(object): + + def __init__(self, url_, style, text): + self.url = url_ + self.style = style + self.text = text -class BlogMessages(TemplateProcessor): +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') - def __init__(self, host, request, base_url, mblog_data): - TemplateProcessor.__init__(self, host) - self.request = request - self.base_url = base_url - self.mblog_data = mblog_data + 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') - def render(self): - for entry, comments_data in self.mblog_data: - comments, comments_rsm = comments_data - comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0)))) - self.render_html(entry, comments) + 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: @@ -372,51 +409,3 @@ processor = addURLToText if key.startswith('content') else sanitizeHtml return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8') return None - - def render_html(self, entry, comments=None): - """Render one microblog entry. - @param entry: the microblog entry - @param base_url: the base url of the blog - @param request: the HTTP request - """ - timestamp = float(entry.get('published', 0)) - is_comment = entry['type'] == 'comment' - - data = {'date': datetime.fromtimestamp(timestamp), - 'comments_link': '', - 'previous_comments': '', - } - - if is_comment: - author = (_("from %s") % entry['author']).encode('utf-8') - else: - author = ' ' - message_link = (u"%s/%s" % (self.base_url, entry['id'])).encode('utf-8') - - count_text = lambda count: D_('comments') if count > 1 else D_('comment') - - comments_count = int(entry['comments_count']) - delta = comments_count - len(comments) - if self.request.display_single and delta > 0: - data['comments_link'] = ("%s?max=%s" % (message_link, entry['comments_count'])) - data['previous_comments'] = D_("Show %(count)d previous %(comments)s") % \ - {'count': delta, 'comments': count_text(delta)} - - data.update({'comments_count': comments_count, - 'comments_text': count_text(comments_count), - 'message_link': message_link, - 'message_title': self.getText(entry, 'title'), - }) - - data.update({'author': author, - 'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '', - 'content': self.getText(entry, 'content'), - }) - - tpl = "%s%s" % ("" if data.get('message_title', None) else "micro_", "comment" if is_comment else "message") - self.request.write(self.useTemplate(self.request, tpl, data)) - - if comments: - for comment in comments: - self.render_html(comment) - diff -r 052d1d19016d -r bf562fb9c273 src/twisted/plugins/libervia_server.py --- a/src/twisted/plugins/libervia_server.py Mon Jul 13 13:33:01 2015 +0200 +++ b/src/twisted/plugins/libervia_server.py Mon Jul 13 18:11:38 2015 +0200 @@ -55,7 +55,7 @@ if not os.path.isfile(os.path.join(html, C.LIBERVIA_MAIN_PAGE)): raise ValueError("%s is not a Libervia's browser HTML directory" % os.path.realpath(html)) themes_dir = os.path.join(value, C.THEMES_DIR) - if not os.path.isfile(os.path.join(themes_dir, 'default/index.html')): + if not os.path.isfile(os.path.join(themes_dir, 'default/styles/blog.css')): raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(themes_dir)) return value diff -r 052d1d19016d -r bf562fb9c273 themes/default/__init__.py diff -r 052d1d19016d -r bf562fb9c273 themes/default/static_blog.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/themes/default/static_blog.html Mon Jul 13 18:11:38 2015 +0200 @@ -0,0 +1,111 @@ +{# +Libervia: a Salut à Toi frontend +Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson +Copyright (C) 2013, 2014, 2015 Adrien Cossa + + +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 . +#} + +{% macro message(entry) -%} + +
+ {% if entry.type == "comment" %} +
+ +
+ {% else %} + +
+ +
+
+ {% endif %} + + {% if entry.message_title %} +

{{entry.title}}

+ {% endif %} + {{entry.content}} +
+ {% if entry.type == "main_item" %} + + + + {% endif %} +
+ {% if entry.all_comments_link %} + {{ link(entry.all_comments_link) }} + {% endif %} + + {% for comment in entry.comments %} + {{ message(comment) }} + {% endfor %} + +{%- endmacro %} + +{% macro link(entry) -%} + {{entry.text}} +{%- endmacro %} + +{% macro image(entry) -%} + {{entry.alt}} +{%- endmacro %} + + + + + + + + + + + {{title}} + + + + + +
+
+ {{ link(navlinks.later_message) }} + {{ link(navlinks.later_messages) }} + {{ link(navlinks.older_message) }} +
+
+ + {% for entry in messages %} + {{ message(entry) }} + {% endfor %} + + + + diff -r 052d1d19016d -r bf562fb9c273 themes/default/static_blog_error.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/themes/default/static_blog_error.html Mon Jul 13 18:11:38 2015 +0200 @@ -0,0 +1,32 @@ +{# +Libervia: a Salut à Toi frontend +Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson +Copyright (C) 2013, 2014, 2015 Adrien Cossa + +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 . +#} + + + + + + + + Blog error + + + +

{{message}}

+ + diff -r 052d1d19016d -r bf562fb9c273 themes/default/styles/blog.css --- a/themes/default/styles/blog.css Mon Jul 13 13:33:01 2015 +0200 +++ b/themes/default/styles/blog.css Mon Jul 13 18:11:38 2015 +0200 @@ -128,7 +128,11 @@ .comments_link { text-decoration: none; text-align: center; + color: #2B73B7; + font-size: smaller; display: block; + left: 2%; + position: relative; } .header, .footer { diff -r 052d1d19016d -r bf562fb9c273 themes/default/templates.py --- a/themes/default/templates.py Mon Jul 13 13:33:01 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson - -# 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 . - -ERROR = u""" - - - - - - - MICROBLOG ERROR - - - -

{message}

- - -""" - -HEADER = u""" - - - - - - - - - - {title} - - - - - -
-
- {later_message} - {later_messages} - {older_message} -
-
-""" - -BANNER = u"""{alt}{suffix}""" - -NAV_LINK = u"""{text}""" - -MICRO_MESSAGE = u""" -
- -
- -
-
- {content} - - - -
-{previous_comments} -""" - -MICRO_COMMENT = u""" -
-
- -
- {content} -
-""" - -message_title = u"""

{message_title}

{content}""" -MESSAGE = MICRO_MESSAGE.replace('{content}', message_title) -COMMENT = MICRO_COMMENT.replace('{content}', message_title) - -FOOTER = u""" - - - -"""