changeset 704:5319110a862c

server_side: static blog uses the default template
author souliane <souliane@mailoo.org>
date Thu, 04 Jun 2015 12:39:27 +0200
parents 1a19ee7d8d8a
children 531eacb82e9f
files src/server/blog.py src/server/server.py src/twisted/plugins/libervia_server.py
diffstat 3 files changed, 216 insertions(+), 224 deletions(-) [+]
line wrap: on
line diff
--- a/src/server/blog.py	Thu Jun 04 11:56:34 2015 +0200
+++ b/src/server/blog.py	Thu Jun 04 12:39:27 2015 +0200
@@ -27,6 +27,8 @@
 from twisted.web.resource import Resource
 from twisted.words.protocols.jabber.jid import JID
 from datetime import datetime
+from sys import path
+import importlib
 import uuid
 import re
 import os
@@ -35,24 +37,39 @@
 from libervia.server.constants import Const as C
 
 
-class MicroBlog(Resource):
-    isLeaf = True
+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(self.host.themes_dir))
+
+    def useTemplate(self, request, tpl, data=None):
+        root_url = '../' * len(request.postpath)
+        theme_url = os.path.join(root_url, 'themes', self.THEME)
 
-    ERROR_TEMPLATE = """
-                <html>
-                <head profile="http://www.w3.org/2005/10/profile">
-                    <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
-                    <title>MICROBLOG ERROR</title>
-                </head>
-                <body>
-                    <h1 style='text-align: center; color: red;'>%(message)s</h1>
-                </body>
-                </html>
-                """
+        # 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'),
+                 'styles': os.path.join(theme_url, 'styles'),
+                 }
+        if data:
+            data_.update(data)
+        return (getattr(theme, tpl.upper()).format(**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 = {}
@@ -116,72 +133,73 @@
 
     def render_GET(self, request):
         if not request.postpath:
-            return MicroBlog.ERROR_TEMPLATE % {'root': '',
-                                               'message': "You must indicate a nickname"}
-        else:
-            prof_requested = request.postpath[0]
-            #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
-            prof_found = self.host.bridge.getProfileName(prof_requested)
-            if not prof_found or prof_found == C.SERVICE_PROFILE:
-                return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
-                                                    'message': "Invalid nickname"}
+            return self.useTemplate(request, "error", {'message': "You must indicate a nickname"})
+
+        prof_requested = request.postpath[0]
+        #TODO: char check: only use alphanumerical 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"})
+
+        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()
+        item_id = None
+        atom = None
+
+        if len(request.postpath) > 1:
+            if request.postpath[1] == 'atom.xml':  # return the atom feed
+                atom = True
             else:
-                def got_jid(pub_jid_s):
-                    pub_jid = JID(pub_jid_s)
-                    d2 = defer.Deferred()
-                    item_id = None
-                    atom = None
-                    rsm_ = {}
+                try:  # check if the given path is a valid UUID
+                    uuid.UUID(request.postpath[1])
+                    item_id = request.postpath[1]
+                except ValueError:
+                    pass
 
-                    if len(request.postpath) > 1:
-                        if request.postpath[1] == 'atom.xml':  # return the atom feed
-                            atom = True
-                        else:
-                            try:  # check if the given path is a valid UUID
-                                uuid.UUID(request.postpath[1])
-                                item_id = request.postpath[1]
-                            except ValueError:
-                                pass
+        rsm_ = self.parseURLParams(request, item_id)
+        max_items = int(rsm_['max'])
 
-                    # retrieve RSM request data from URL parameters
-                    try:
-                        max_items = int(request.args['max'][0])
-                    except (ValueError, KeyError):
-                        max_items = C.RSM_MAX_ITEMS if item_id else C.RSM_MAX_COMMENTS
-                    rsm_['max'] = unicode(max_items)
-                    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
+        if atom is not None:
+            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)
 
-                    if atom is not None:
-                        d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None)
-                        self.host.bridge.getGroupBlogsAtom(pub_jid.userhost(), rsm_, C.SERVICE_PROFILE, d2.callback, d2.errback)
-                        return
-
-                    d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], 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, d2.callback, d2.errback)
-                        else:  # display one message, count its comments
-                            self.host.bridge.getGroupBlogs(pub_jid.userhost(), [item_id], {}, True, C.SERVICE_PROFILE, d2.callback, d2.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, d2.callback, d2.errback)
-                        else:  # display the last messages, count their comments
-                            self.host.bridge.getGroupBlogs(pub_jid.userhost(), [], rsm_, True, C.SERVICE_PROFILE, d2.callback, d2.errback)
-
-                d1 = defer.Deferred()
-                JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
-                d1.addCallbacks(got_jid)
-
-                return server.NOT_DONE_YET
+    def parseURLParams(self, request, item_id):
+        # retrieve RSM request data from URL parameters
+        rsm_ = {}
+        try:
+            rsm_['max'] = request.args['max'][0]
+        except (ValueError, KeyError):
+            rsm_['max'] = unicode(C.RSM_MAX_ITEMS if 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
+        return rsm_
 
     def render_html_blog(self, mblog_data, request, profile):
         """Retrieve the user parameters before actually rendering the static blog
@@ -245,174 +263,148 @@
             else:
                 url = default
                 suffix = ""
-            return "<img src='%(url)s' alt='%(alt)s'/>%(suffix)s" % {'alt': alt, 'url': url, 'suffix': suffix}
+            return self.useTemplate(request, "banner", {'alt': alt, 'url': url, 'suffix': suffix})
 
         avatar = os.path.normpath(root_url + getOption('avatar'))
         title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user
-        request.write("""
-            <html>
-            <head>
-                <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-                <meta name="keywords" content="%(keywords)s">
-                <meta name="description" content="%(description)s">
-                <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/>
-                <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" />
-                <link rel="icon" type="image/png" href="%(favicon)s">
-                <title>%(title)s</title>
-            </head>
-            <body>
-            <div class="mblog_title"><a href="%(base)s">%(banner_elt)s%(title_elt)s</a></div>
-            """ % {'base': base_url,
-                   'root': root_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,
-                   'favicon': avatar,
-                   'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title),
-                   'title_elt': title,
-                   })
-        mblog_data, main_rsm = mblog_data
-        display_single = len(mblog_data) == 1
+        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,
+                'favicon': avatar,
+                'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title),
+                'title_elt': title,
+                }
 
-        # build the navigation links
-        count = int(main_rsm['count']) if 'count' in main_rsm else 0
-        if count > 0:
-            index = int(main_rsm['index'])
-            if index > 0:
-                before_link = ("%(base)s?before=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['first']}).encode('utf-8')
-                if display_single:
-                    before_link += '&max=1'
-                    tmp_text = D_("later message")
-                    class_ = 'later_message'
-                else:
-                    tmp_text = D_("later messages")
-                    class_ = 'later_messages'
-                before_tag = """<a href="%(link)s" class="%(class)s">%(text)s</a>""" % {'link': before_link, 'class': class_, 'text': tmp_text}
-            else:
-                before_tag = None
-            if index + len(mblog_data) < count:
-                after_link = ("%(base)s?after=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['last']}).encode('utf-8')
-                if display_single:
-                    after_link += '&max=1'
-                    text = D_("older message")
-                    class_ = 'older_message'
-                else:
-                    text = D_("older messages")
-                    class_ = 'older_messages'
-                after_tag = """<a href="%(link)s" class="%(class)s">%(text)s</a>""" % {'link': after_link, 'class': class_, 'text': text}
-            else:
-                after_tag = None
-
-        # display navigation header
-        request.write("""<div class="header">""")
-        request.write("""<div class="header_content">""")
-        if before_tag:
-            request.write(before_tag)
-        if display_single and after_tag:
-            request.write(after_tag)
-        request.write("""</div></div>""")
-
+        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))))
-        for main_data, comments_data in mblog_data:
-            self.__render_html_entry(main_data, base_url, request)
-            comments, comments_rsm = comments_data
+
+        data.update(self.getNavigationLinks(request, mblog_data, main_rsm, base_url))
+        request.write(self.useTemplate(request, 'header', data))
 
-            # eventually display the link to show all comments
-            comments_count = int(main_data['comments_count'])
-            delta = comments_count - len(comments)
-            if display_single and delta > 0:
-                link = ("%(base)s/%(item_id)s?max=%(max)s" % {'base': base_url,
-                                                              'item_id': main_data['id'],
-                                                              'max': main_data['comments_count']}).encode('utf-8')
-                text = D_("Show %(count)d previous %(comments)s") % {'count': delta,
-                                                                    'comments': D_('comments') if delta > 1 else D_('comment')}
-                request.write("""<a href="%(link)s" class="comments_link">%(text)s</a>""" % {'link': link, 'text': text})
+        BlogMessages(self.host, request, base_url, mblog_data).render()
 
-            comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0))))
-            for comment in comments:
-                self.__render_html_entry(comment, base_url, request)
-
-        # display navigation footer
-        request.write("""<div class="footer">""")
-        request.write("""<div class="footer_content">""")
-        if not display_single and after_tag:
-            request.write(after_tag)
-        request.write("""</div></div>""")
-
-        request.write('</body></html>')
+        request.write(self.useTemplate(request, "footer", data))
         request.finish()
 
-    def __render_html_entry(self, entry, base_url, request):
+    def getNavigationLinks(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
+        """
+        data = {}
+        for key in ('later_message', 'later_messages', 'older_message', 'older_messages'):
+            count = int(rsm_data.get('count', 0))
+            display_single = len(mblog_data) == 1
+            data[key] = ''  # key must exist when using the template
+            if count <= 0 or (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 display_single:
+                link_data['suffix'] = '&max=1'
+
+            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')
+
+        return data
+
+    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, "error", {'message': "Can't access requested data"}))
+        request.finish()
+
+
+class BlogMessages(TemplateProcessor):
+
+    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
+
+    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)
+
+    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
+
+    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))
-        datetime_ = datetime.fromtimestamp(timestamp)
         is_comment = entry['type'] == 'comment'
 
-        def getText(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 ''
-
-        def addMainItemLink(elem):
-            if not item_link or not elem:
-                return elem
-            return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem}
+        data = {'date': datetime.fromtimestamp(timestamp),
+                'comments_link': '',
+                'previous_comments': '',
+                }
 
         if is_comment:
             author = (_("from %s") % entry['author']).encode('utf-8')
-            item_link = ''
-            footer = ''
         else:
             author = '&nbsp;'
-            item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8')
-            comments_count = int(entry['comments_count'])
-            comments_text = (D_('comments') if comments_count > 1 else D_('comment')).encode('utf-8')
-            footer = addMainItemLink("""<div class="mblog_footer mblog_footer_main">
-                                          <div class="mblog_metadata">
-                                            <div class="mblog_comments">%(count)s %(comments)s</div>
-                                          </div>
-                                        </div>""" % {'count': comments_count,
-                                                     'comments': comments_text})
+            message_link = ("%s/%s" % (self.base_url, entry['id'])).encode('utf-8')
+
+            count_text = lambda count: D_('comments') if count > 1 else D_('comment')
 
-        header = """<div class="mblog_header %(class)s">
-                      <div class="mblog_metadata">
-                        <div class="mblog_author">%(author)s</div>
-                        <div class="mblog_timestamp">%(date)s</div>
-                      </div>
-                    </div>""" % {'author': author, 'date': datetime_,
-                                 'class': '' if is_comment else 'mblog_header_main'}
-        if not is_comment:
-            header = addMainItemLink(header)
+            comments_count = int(entry['comments_count'])
+            delta = comments_count - len(comments)
+            if len(self.mblog_data) == 1 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)}
 
-        title = addMainItemLink(getText('title'))
-        body = getText('content')
-        if title:  # insert the title within the body
-            body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body}
+            data.update({'comments_count': comments_count,
+                         'comments_text': count_text(comments_count),
+                         'message_link': message_link,
+                         'message_title': self.getText(entry, 'title'),
+                         })
 
-        request.write("""<div class="mblog_entry %(extra_style)s">
-                           %(header)s
-                           <span class="mblog_content">%(content)s</span>
-                           %(footer)s
-                         </div>""" % {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
-                                      'item_link': item_link,
-                                      'header': header,
-                                      'content': body,
-                                      'footer': footer})
+        data.update({'author': author,
+                     'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
+                     'content': self.getText(entry, 'content'),
+                     })
 
-    def render_atom_feed(self, feed, request):
-        request.write(feed.encode('utf-8'))
-        request.finish()
+        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))
 
-    def render_error_blog(self, error, request, profile):
-        request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
-                                                  'message': "Can't access requested data"})
-        request.finish()
+        if comments:
+            for comment in comments:
+                self.render_html(comment)
+
--- a/src/server/server.py	Thu Jun 04 11:56:34 2015 +0200
+++ b/src/server/server.py	Thu Jun 04 12:39:27 2015 +0200
@@ -1057,7 +1057,7 @@
             coerceDataDir(self.data_dir)  # this is not done when using the default value
 
         self.html_dir = os.path.join(self.data_dir, C.HTML_DIR)
-        self.server_css_dir = os.path.join(self.data_dir, C.SERVER_CSS_DIR)
+        self.themes_dir = os.path.join(self.data_dir, C.THEMES_DIR)
 
         self._cleanup = []
 
@@ -1107,7 +1107,7 @@
             putChild('upload_radiocol', _upload_radiocol)
             putChild('upload_avatar', _upload_avatar)
             putChild('blog', MicroBlog(self))
-            putChild('css', ProtectedFile(self.server_css_dir))
+            putChild('themes', ProtectedFile(self.themes_dir))
             putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
             putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR)))
             putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"))  # We cheat for PoC because we know we are on the same host, so we use directly upload dir
--- a/src/twisted/plugins/libervia_server.py	Thu Jun 04 11:56:34 2015 +0200
+++ b/src/twisted/plugins/libervia_server.py	Thu Jun 04 12:39:27 2015 +0200
@@ -52,11 +52,11 @@
 
 def coerceDataDir(value):  # called from Libervia.OPT_PARAMETERS
     html = os.path.join(value, C.HTML_DIR)
-    if not os.path.isfile(os.path.join(html, 'libervia.html')):
+    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))
-    server_css = os.path.join(value, C.SERVER_CSS_DIR)
-    if not os.path.isfile(os.path.join(server_css, 'blog.css')):
-        raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(server_css))
+    themes_dir = os.path.join(value, C.THEMES_DIR)
+    if not os.path.isfile(os.path.join(themes_dir, 'default/index.html')):
+        raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(themes_dir))
     return value
 
 DATA_DIR_DEFAULT = ''