Mercurial > libervia-web
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 = ' ' | |
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 = ' ' | |
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 |