comparison 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
comparison
equal deleted inserted replaced
711:052d1d19016d 712:bf562fb9c273
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # Libervia: a Salut à Toi frontend 4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson <goffi@goffi.org> 5 # Copyright (C) 2011, 2012, 2013, 2014, 2015 Jérôme Poisson <goffi@goffi.org>
6 # Copyright (C) 2013, 2014, 2015 Adrien Cossa <souliane@mailoo.org>
6 7
7 # This program is free software: you can redistribute it and/or modify 8 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by 9 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or 10 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version. 11 # (at your option) any later version.
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 20
20 from sat.core.i18n import _, D_ 21 from sat.core.i18n import _, D_
21 from sat_frontends.tools.strings import addURLToText 22 from sat_frontends.tools.strings import addURLToText
22 from sat.core.log import getLogger 23 from sat.core.log import getLogger
24 from django.conf.urls import url
23 log = getLogger(__name__) 25 log = getLogger(__name__)
24 26
25 from twisted.internet import defer 27 from twisted.internet import defer
26 from twisted.web import server 28 from twisted.web import server
27 from twisted.web.resource import Resource 29 from twisted.web.resource import Resource
28 from twisted.words.protocols.jabber.jid import JID 30 from twisted.words.protocols.jabber.jid import JID
31 from jinja2 import Environment, PackageLoader
29 from datetime import datetime 32 from datetime import datetime
30 from sys import path 33 from sys import path
31 import importlib
32 import uuid 34 import uuid
33 import re 35 import re
34 import os 36 import os
35 37
36 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML 38 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML
43 45
44 def __init__(self, host): 46 def __init__(self, host):
45 self.host = host 47 self.host = host
46 48
47 # add Libervia's themes directory to the python path 49 # add Libervia's themes directory to the python path
48 path.append(os.path.dirname(self.host.themes_dir)) 50 path.append(os.path.dirname(os.path.normpath(self.host.themes_dir)))
51 themes = os.path.basename(os.path.normpath(self.host.themes_dir))
52 self.env = Environment(loader=PackageLoader(themes, self.THEME))
49 53
50 def useTemplate(self, request, tpl, data=None): 54 def useTemplate(self, request, tpl, data=None):
51 root_url = '../' * len(request.postpath) 55 root_url = '../' * len(request.postpath)
52 theme_url = os.path.join(root_url, 'themes', self.THEME) 56 theme_url = os.path.join(root_url, 'themes', self.THEME)
53 57
54 # import the theme module 58 data_ = {'images': os.path.join(theme_url, 'images'),
55 themes = os.path.basename(os.path.dirname(os.path.dirname(self.host.themes_dir)))
56 theme = importlib.import_module("%s.templates" % self.THEME, themes)
57 data_ = {'theme': theme_url,
58 'images': os.path.join(theme_url, 'images'),
59 'styles': os.path.join(theme_url, 'styles'), 59 'styles': os.path.join(theme_url, 'styles'),
60 } 60 }
61 if data: 61 if data:
62 data_.update(data) 62 data_.update(data)
63 return getattr(theme, tpl.upper()).encode('utf-8').format(**data_) 63
64 template = self.env.get_template('%s.html' % tpl)
65 return template.render(**data_).encode('utf-8')
64 66
65 67
66 class MicroBlog(Resource, TemplateProcessor): 68 class MicroBlog(Resource, TemplateProcessor):
67 isLeaf = True 69 isLeaf = True
68 70
124 @return: deferred avatar path, relative to the server's root 126 @return: deferred avatar path, relative to the server's root
125 """ 127 """
126 jid_s = (profile + '@' + self.host.bridge.getNewAccountDomain()).lower() 128 jid_s = (profile + '@' + self.host.bridge.getNewAccountDomain()).lower()
127 if jid_s in self.avatars_cache: 129 if jid_s in self.avatars_cache:
128 return defer.succeed(self.avatars_cache[jid_s]) 130 return defer.succeed(self.avatars_cache[jid_s])
129 # FIXME: request_id is no more need when actionResult is removed 131 # FIXME: request_id is no more needed when actionResult is removed
130 request_id = self.host.bridge.getCard(jid_s, C.SERVICE_PROFILE) 132 request_id = self.host.bridge.getCard(jid_s, C.SERVICE_PROFILE)
131 self.waiting_deferreds[jid_s] = (request_id, defer.Deferred()) 133 self.waiting_deferreds[jid_s] = (request_id, defer.Deferred())
132 return self.waiting_deferreds[jid_s][1] 134 return self.waiting_deferreds[jid_s][1]
133 135
134 def render_GET(self, request): 136 def render_GET(self, request):
135 if not request.postpath: 137 if not request.postpath:
136 return self.useTemplate(request, "error", {'message': "You must indicate a nickname"}) 138 return self.useTemplate(request, "static_blog_error", {'message': "You must indicate a nickname"})
137 139
138 prof_requested = request.postpath[0] 140 prof_requested = request.postpath[0]
139 #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here 141 #TODO : char check: only use alphanumeric chars + some extra(_,-,...) here
140 prof_found = self.host.bridge.getProfileName(prof_requested) 142 prof_found = self.host.bridge.getProfileName(prof_requested)
141 if not prof_found or prof_found == C.SERVICE_PROFILE: 143 if not prof_found or prof_found == C.SERVICE_PROFILE:
142 return self.useTemplate(request, "error", {'message': "Invalid nickname"}) 144 return self.useTemplate(request, "static_blog_error", {'message': "Invalid nickname"})
143 145
144 d = defer.Deferred() 146 d = defer.Deferred()
145 JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d.callback, errback=d.errback)) 147 JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d.callback, errback=d.errback))
146 d.addCallbacks(lambda pub_jid_s: self.gotJID(pub_jid_s, request, prof_found)) 148 d.addCallbacks(lambda pub_jid_s: self.gotJID(pub_jid_s, request, prof_found))
147 return server.NOT_DONE_YET 149 return server.NOT_DONE_YET
264 base_url = root_url + 'blog/' + user 266 base_url = root_url + 'blog/' + user
265 267
266 def getOption(key): 268 def getOption(key):
267 return sanitizeHtml(options[key]).encode('utf-8') if key in options else '' 269 return sanitizeHtml(options[key]).encode('utf-8') if key in options else ''
268 270
269 def getImageOption(key, default, alt): 271 def getImageParams(key, default, alt):
270 """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" 272 """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
271 url = options[key].encode('utf-8') if key in options else '' 273 url = options[key].encode('utf-8') if key in options else ''
272 regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" 274 regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
273 if re.match(regexp, url): 275 if re.match(regexp, url):
274 url = url 276 url = url
275 suffix = "<br/>"
276 else: 277 else:
277 url = default 278 url = default
278 suffix = "" 279 return BlogImage(url, alt)
279 return self.useTemplate(request, "banner", {'alt': alt, 'url': url, 'suffix': suffix})
280 280
281 avatar = os.path.normpath(root_url + getOption('avatar')) 281 avatar = os.path.normpath(root_url + getOption('avatar'))
282 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user 282 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user
283 data = {'base_url': base_url, 283 data = {'base_url': base_url,
284 'user': user,
285 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS), 284 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS),
286 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), 285 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION),
287 'title': getOption(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user, 286 'title': title,
288 'favicon': avatar, 287 'favicon': avatar,
289 'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title), 288 'banner_img': getImageParams(C.STATIC_BLOG_PARAM_BANNER, avatar, title)
290 'title_elt': title,
291 } 289 }
292 290
293 mblog_data, main_rsm = mblog_data 291 mblog_data, main_rsm = mblog_data
294 mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data] 292 mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data]
295 mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('updated', 0)))) 293 mblog_data.sort(key=lambda entry: (-float(entry[0].get('updated', 0))))
296 294
297 data.update(self.getNavigationLinks(request, mblog_data, main_rsm, base_url)) 295 data['navlinks'] = NavigationLinks(request, mblog_data, main_rsm, base_url)
298 request.write(self.useTemplate(request, 'header', data)) 296 data['messages'] = [BlogMessage(request, base_url, entry, comments[0]) for entry, comments in mblog_data]
299 297
300 BlogMessages(self.host, request, base_url, mblog_data).render() 298 request.write(self.useTemplate(request, 'static_blog', data))
301
302 request.write(self.useTemplate(request, "footer", data))
303 request.finish() 299 request.finish()
304 300
305 def getNavigationLinks(self, request, mblog_data, rsm_data, base_url): 301 def render_atom_feed(self, feed, request):
302 request.write(feed.encode('utf-8'))
303 request.finish()
304
305 def render_error_blog(self, error, request, profile):
306 request.write(self.useTemplate(request, "static_blog_error", {'message': "Can't access requested data"}))
307 request.finish()
308
309
310 class NavigationLinks(object):
311
312 def __init__(self, request, mblog_data, rsm_data, base_url):
306 """Build the navigation links. 313 """Build the navigation links.
307 314
308 @param mblog_data (dict): the microblogs that are displayed on the page 315 @param mblog_data (dict): the microblogs that are displayed on the page
309 @param rsm_data (dict): rsm data 316 @param rsm_data (dict): rsm data
310 @param base_url (unicode): the base URL for this user's blog 317 @param base_url (unicode): the base URL for this user's blog
311 @return: dict 318 @return: dict
312 """ 319 """
313 data = {}
314 for key in ('later_message', 'later_messages', 'older_message', 'older_messages'): 320 for key in ('later_message', 'later_messages', 'older_message', 'older_messages'):
315 count = int(rsm_data.get('count', 0)) 321 count = int(rsm_data.get('count', 0))
316 data[key] = '' # key must exist when using the template 322 setattr(self, key, '') # key must exist when using the template
317 if count <= 0 or (request.display_single == key.endswith('s')): 323 if count <= 0 or (request.display_single == key.endswith('s')):
318 continue 324 continue
319 325
320 index = int(rsm_data['index']) 326 index = int(rsm_data['index'])
321 327
335 if request.display_single: 341 if request.display_single:
336 link_data['suffix'] = '&max=1' 342 link_data['suffix'] = '&max=1'
337 343
338 link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data 344 link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data
339 345
340 link_data = {'link': link, 'class': key, 'text': key.replace('_', ' ')} 346 setattr(self, key, BlogLink(link, key, key.replace('_', ' ')))
341 data[key] = (self.useTemplate(request, 'nav_link', link_data)).encode('utf-8') 347
342 348
343 return data 349 class BlogImage(object):
344 350
345 def render_atom_feed(self, feed, request): 351 def __init__(self, url_, alt):
346 request.write(feed.encode('utf-8')) 352 self.url = url_
347 request.finish() 353 self.alt = alt
348 354
349 def render_error_blog(self, error, request, profile): 355
350 request.write(self.useTemplate(request, "error", {'message': "Can't access requested data"})) 356 class BlogLink(object):
351 request.finish() 357
352 358 def __init__(self, url_, style, text):
353 359 self.url = url_
354 class BlogMessages(TemplateProcessor): 360 self.style = style
355 361 self.text = text
356 def __init__(self, host, request, base_url, mblog_data): 362
357 TemplateProcessor.__init__(self, host) 363
358 self.request = request 364 class BlogMessage(object):
359 self.base_url = base_url 365
360 self.mblog_data = mblog_data 366 def __init__(self, request, base_url, entry, comments=None):
361 367 """
362 def render(self): 368
363 for entry, comments_data in self.mblog_data: 369 @param request: HTTP request
364 comments, comments_rsm = comments_data 370 @param base_url (unicode): the base URL
365 comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0)))) 371 @param entry (dict{unicode:unicode]): microblog entry received from the backend
366 self.render_html(entry, comments) 372 @param comments (list[dict]): comments
373 """
374 timestamp = float(entry.get('published', 0))
375 is_comment = entry['type'] == 'comment'
376
377 self.date = datetime.fromtimestamp(timestamp)
378 self.type = entry['type']
379 self.style = 'mblog_comment' if entry['type'] == 'comment' else ''
380 self.content = self.getText(entry, 'content')
381
382 if is_comment:
383 self.author = (_("from %s") % entry['author']).encode('utf-8')
384 else:
385 self.author = '&nbsp;'
386 self.url = (u"%s/%s" % (base_url, entry['id'])).encode('utf-8')
387 self.title = self.getText(entry, 'title')
388
389 comments_count = int(entry['comments_count'])
390 count_text = lambda count: D_('comments') if count > 1 else D_('comment')
391
392 self.comments_text = "%s %s" % (comments_count, count_text(comments_count))
393
394 delta = comments_count - len(comments)
395 if request.display_single and delta > 0:
396 prev_url = "%s?max=%s" % (self.url, entry['comments_count'])
397 prev_text = D_("show %(count)d previous %(comments)s") % \
398 {'count': delta, 'comments': count_text(delta)}
399 self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text)
400
401 if comments:
402 comments.sort(key=lambda entry: float(entry.get('published', 0)))
403 self.comments = [BlogMessage(request, base_url, comment) for comment in comments]
367 404
368 def getText(self, entry, key): 405 def getText(self, entry, key):
369 if ('%s_xhtml' % key) in entry: 406 if ('%s_xhtml' % key) in entry:
370 return entry['%s_xhtml' % key].encode('utf-8') 407 return entry['%s_xhtml' % key].encode('utf-8')
371 elif key in entry: 408 elif key in entry:
372 processor = addURLToText if key.startswith('content') else sanitizeHtml 409 processor = addURLToText if key.startswith('content') else sanitizeHtml
373 return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8') 410 return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8')
374 return None 411 return None
375
376 def render_html(self, entry, comments=None):
377 """Render one microblog entry.
378 @param entry: the microblog entry
379 @param base_url: the base url of the blog
380 @param request: the HTTP request
381 """
382 timestamp = float(entry.get('published', 0))
383 is_comment = entry['type'] == 'comment'
384
385 data = {'date': datetime.fromtimestamp(timestamp),
386 'comments_link': '',
387 'previous_comments': '',
388 }
389
390 if is_comment:
391 author = (_("from %s") % entry['author']).encode('utf-8')
392 else:
393 author = '&nbsp;'
394 message_link = (u"%s/%s" % (self.base_url, entry['id'])).encode('utf-8')
395
396 count_text = lambda count: D_('comments') if count > 1 else D_('comment')
397
398 comments_count = int(entry['comments_count'])
399 delta = comments_count - len(comments)
400 if self.request.display_single and delta > 0:
401 data['comments_link'] = ("%s?max=%s" % (message_link, entry['comments_count']))
402 data['previous_comments'] = D_("Show %(count)d previous %(comments)s") % \
403 {'count': delta, 'comments': count_text(delta)}
404
405 data.update({'comments_count': comments_count,
406 'comments_text': count_text(comments_count),
407 'message_link': message_link,
408 'message_title': self.getText(entry, 'title'),
409 })
410
411 data.update({'author': author,
412 'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
413 'content': self.getText(entry, 'content'),
414 })
415
416 tpl = "%s%s" % ("" if data.get('message_title', None) else "micro_", "comment" if is_comment else "message")
417 self.request.write(self.useTemplate(self.request, tpl, data))
418
419 if comments:
420 for comment in comments:
421 self.render_html(comment)
422