changeset 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 052d1d19016d
children d75935e2b279
files setup.py src/server/blog.py src/twisted/plugins/libervia_server.py themes/default/__init__.py themes/default/static_blog.html themes/default/static_blog_error.html themes/default/styles/blog.css themes/default/templates.py
diffstat 7 files changed, 235 insertions(+), 210 deletions(-) [+]
line wrap: on
line diff
--- 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},
       )
--- 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 <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
@@ -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 = "<br/>"
             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 = '&nbsp;'
+            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 = '&nbsp;'
-            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)
-
--- 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
 
--- /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 <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/>.
+#}
+
+{% macro message(entry) -%}
+    
+    <div class="mblog_entry {{ entry.style }}">
+        {% if entry.type == "comment" %}
+            <div class="mblog_header">
+                <div class="mblog_metadata">
+                    <div class="mblog_author">{{entry.author}}</div>
+                    <div class="mblog_timestamp">{{entry.date}}</div>
+                </div>
+            </div>
+        {% else %}
+            <a href="{{entry.url}}" class="item_link">
+                <div class="mblog_header mblog_header_main">
+                    <div class="mblog_metadata">
+                        <div class="mblog_author">{{entry.author}}</div>
+                        <div class="mblog_timestamp">{{entry.date}}</div>
+                    </div>
+                </div>
+            </a>
+        {% endif %}
+        <span class="mblog_content">
+            {% if entry.message_title %}
+                <h1><a href="{{entry.url}}" class="item_link">{{entry.title}}</a></h1>
+            {% endif %}
+            {{entry.content}}
+        </span>
+        {% if entry.type == "main_item" %}
+            <a href="{{entry.url}}" class="item_link">
+	            <div class="mblog_footer mblog_footer_main">
+	                <div class="mblog_metadata">
+	                    <div class="mblog_comments">{{ entry.comments_text }}</div>
+	                </div>
+	            </div>
+            </a>
+        {% endif %}
+    </div>
+    {% if entry.all_comments_link %}
+        {{ link(entry.all_comments_link) }}
+    {% endif %}
+    
+    {% for comment in entry.comments %}
+        {{ message(comment) }}       
+    {% endfor %}
+
+{%- endmacro %}
+
+{% macro link(entry) -%}
+    <a href="{{entry.url}}" class="{{entry.style}}">{{entry.text}}</a>
+{%- endmacro %}
+
+{% macro image(entry) -%}
+    <img src="{{entry.url}}" alt="{{entry.alt}}"/>
+{%- endmacro %}
+
+<html>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta name="keywords" content="{{keywords}}">
+    <meta name="description" content="{{description}}">
+    <link rel="alternate" type="application/atom+xml" href="{{base_url}}/atom.xml"/>
+    <link rel='stylesheet' type="text/css" href='{{styles}}/blog.css'>
+    <link rel="icon" type="image/png" href="{{favicon}}">
+
+    <head profile="http://www.w3.org/2005/10/profile">
+        <title>{{title}}</title>
+    </head>
+
+    <body>
+        <div class="mblog_title"><a href="{{base_url}}">
+            {{ image(banner_img) }}
+            {{ title }}
+        </a></div>
+
+        <div class="header">
+            <div class="header_content">
+                {{ link(navlinks.later_message) }}
+                {{ link(navlinks.later_messages) }}
+                {{ link(navlinks.older_message) }}
+            </div>
+        </div>
+
+        {% for entry in messages %}
+            {{ message(entry) }}        
+        {% endfor %}
+
+        <div class="footer">
+            <div class="footer_content">
+                {{ link(navlinks.older_messages) }}
+            </div>
+        </div>
+    </body>
+</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 <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/>.
+#}
+
+<html>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <link rel='stylesheet' href='{{styles}}/blog.css'>
+    <link rel="icon" type="image/png" href="{{images}}/sat_logo_16.png">
+
+    <head profile="http://www.w3.org/2005/10/profile">
+        <title>Blog error</title>
+    </head>
+
+    <body>
+        <h1 class="error">{{message}}</h1>
+    </body>
+</html>
--- 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 {
--- 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 <goffi@goffi.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/>.
-
-ERROR = u"""
-<html>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <link rel='stylesheet' href='{styles}/blog.css'>
-    <link rel="icon" type="image/png" href="{images}/sat_logo_16.png">
-
-    <head profile="http://www.w3.org/2005/10/profile">
-        <title>MICROBLOG ERROR</title>
-    </head>
-
-    <body>
-        <h1 class="error">{message}</h1>
-    </body>
-</html>
-"""
-
-HEADER = u"""
-<html>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <meta name="keywords" content="{keywords}">
-    <meta name="description" content="{description}">
-    <link rel="alternate" type="application/atom+xml" href="{base_url}/atom.xml"/>
-    <link rel='stylesheet' href='{styles}/blog.css'>
-    <link rel="icon" type="image/png" href="{favicon}">
-
-    <head profile="http://www.w3.org/2005/10/profile">
-        <title>{title}</title>
-    </head>
-
-    <body>
-        <div class="mblog_title"><a href="{base_url}">{banner_elt}{title_elt}</a></div>
-
-        <div class="header">
-	        <div class="header_content">
-                {later_message}
-                {later_messages}
-	            {older_message}
-	        </div>
-        </div>
-"""
-
-BANNER = u"""<img src="{url}" alt="{alt}"/>{suffix}"""
-
-NAV_LINK = u"""<a href="{link}" class="{class}">{text}</a>"""
-
-MICRO_MESSAGE = u"""
-<div class="mblog_entry {extra_style}">
-    <a href="{message_link}" class="item_link">
-        <div class="mblog_header mblog_header_main">
-            <div class="mblog_metadata">
-                <div class="mblog_author">{author}</div>
-                <div class="mblog_timestamp">{date}</div>
-            </div>
-        </div>
-    </a>
-    <span class="mblog_content">{content}</span>
-    <a href="{message_link}" class="item_link">
-        <div class="mblog_footer mblog_footer_main">
-            <div class="mblog_metadata">
-                <div class="mblog_comments">{comments_count} {comments_text}</div>
-            </div>
-        </div>
-    </a>
-</div>
-<a href="{comments_link}" class="comments_link">{previous_comments}</a>
-"""
-
-MICRO_COMMENT = u"""
-<div class="mblog_entry {extra_style}">
-    <div class="mblog_header">
-        <div class="mblog_metadata">
-            <div class="mblog_author">{author}</div>
-            <div class="mblog_timestamp">{date}</div>
-        </div>
-    </div>
-    <span class="mblog_content">{content}</span>
-</div>
-"""
-
-message_title = u"""<h1><a href="{message_link}" class="item_link">{message_title}</a></h1>{content}"""
-MESSAGE = MICRO_MESSAGE.replace('{content}', message_title)
-COMMENT = MICRO_COMMENT.replace('{content}', message_title)
-
-FOOTER = u"""
-        <div class="footer">
-            <div class="footer_content">
-                {older_messages}
-            </div>
-        </div>
-    </body>
-</html>
-"""