comparison src/server/blog.py @ 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 d94feb0d849e
children 531eacb82e9f
comparison
equal deleted inserted replaced
703:1a19ee7d8d8a 704:5319110a862c
25 from twisted.internet import defer 25 from twisted.internet import defer
26 from twisted.web import server 26 from twisted.web import server
27 from twisted.web.resource import Resource 27 from twisted.web.resource import Resource
28 from twisted.words.protocols.jabber.jid import JID 28 from twisted.words.protocols.jabber.jid import JID
29 from datetime import datetime 29 from datetime import datetime
30 from sys import path
31 import importlib
30 import uuid 32 import uuid
31 import re 33 import re
32 import os 34 import os
33 35
34 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML 36 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML
35 from libervia.server.constants import Const as C 37 from libervia.server.constants import Const as C
36 38
37 39
38 class MicroBlog(Resource): 40 class TemplateProcessor(object):
41
42 THEME = 'default'
43
44 def __init__(self, host):
45 self.host = host
46
47 # add Libervia's themes directory to the python path
48 path.append(os.path.dirname(self.host.themes_dir))
49
50 def useTemplate(self, request, tpl, data=None):
51 root_url = '../' * len(request.postpath)
52 theme_url = os.path.join(root_url, 'themes', self.THEME)
53
54 # import the theme module
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'),
60 }
61 if data:
62 data_.update(data)
63 return (getattr(theme, tpl.upper()).format(**data_)).encode('utf-8')
64
65
66 class MicroBlog(Resource, TemplateProcessor):
39 isLeaf = True 67 isLeaf = True
40
41 ERROR_TEMPLATE = """
42 <html>
43 <head profile="http://www.w3.org/2005/10/profile">
44 <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
45 <title>MICROBLOG ERROR</title>
46 </head>
47 <body>
48 <h1 style='text-align: center; color: red;'>%(message)s</h1>
49 </body>
50 </html>
51 """
52 68
53 def __init__(self, host): 69 def __init__(self, host):
54 self.host = host 70 self.host = host
55 Resource.__init__(self) 71 Resource.__init__(self)
72 TemplateProcessor.__init__(self, host)
56 self.host.bridge.register('entityDataUpdated', self.entityDataUpdatedCb) 73 self.host.bridge.register('entityDataUpdated', self.entityDataUpdatedCb)
57 self.host.bridge.register('actionResult', self.actionResultCb) # FIXME: actionResult is to be removed 74 self.host.bridge.register('actionResult', self.actionResultCb) # FIXME: actionResult is to be removed
58 self.avatars_cache = {} 75 self.avatars_cache = {}
59 self.waiting_deferreds = {} 76 self.waiting_deferreds = {}
60 77
114 self.waiting_deferreds[jid_s] = (request_id, defer.Deferred()) 131 self.waiting_deferreds[jid_s] = (request_id, defer.Deferred())
115 return self.waiting_deferreds[jid_s][1] 132 return self.waiting_deferreds[jid_s][1]
116 133
117 def render_GET(self, request): 134 def render_GET(self, request):
118 if not request.postpath: 135 if not request.postpath:
119 return MicroBlog.ERROR_TEMPLATE % {'root': '', 136 return self.useTemplate(request, "error", {'message': "You must indicate a nickname"})
120 'message': "You must indicate a nickname"} 137
138 prof_requested = request.postpath[0]
139 #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
140 prof_found = self.host.bridge.getProfileName(prof_requested)
141 if not prof_found or prof_found == C.SERVICE_PROFILE:
142 return self.useTemplate(request, "error", {'message': "Invalid nickname"})
143
144 d = defer.Deferred()
145 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))
147 return server.NOT_DONE_YET
148
149 def gotJID(self, pub_jid_s, request, profile):
150 pub_jid = JID(pub_jid_s)
151 d = defer.Deferred()
152 item_id = None
153 atom = None
154
155 if len(request.postpath) > 1:
156 if request.postpath[1] == 'atom.xml': # return the atom feed
157 atom = True
158 else:
159 try: # check if the given path is a valid UUID
160 uuid.UUID(request.postpath[1])
161 item_id = request.postpath[1]
162 except ValueError:
163 pass
164
165 rsm_ = self.parseURLParams(request, item_id)
166 max_items = int(rsm_['max'])
167
168 if atom is not None:
169 d.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, profile], None)
170 self.host.bridge.getGroupBlogsAtom(pub_jid.userhost(), rsm_, C.SERVICE_PROFILE, d.callback, d.errback)
171 return
172
173 d.addCallbacks(self.render_html_blog, self.render_error_blog, [request, profile], None, [request, profile], None)
174 if item_id:
175 if max_items > 0: # display one message and its comments
176 self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], {}, max_items, C.SERVICE_PROFILE, d.callback, d.errback)
177 else: # display one message, count its comments
178 self.host.bridge.getGroupBlogs(pub_jid.userhost(), [item_id], {}, True, C.SERVICE_PROFILE, d.callback, d.errback)
121 else: 179 else:
122 prof_requested = request.postpath[0] 180 if max_items == 1: # display one message and its comments
123 #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here 181 self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [], rsm_, C.RSM_MAX_COMMENTS, C.SERVICE_PROFILE, d.callback, d.errback)
124 prof_found = self.host.bridge.getProfileName(prof_requested) 182 else: # display the last messages, count their comments
125 if not prof_found or prof_found == C.SERVICE_PROFILE: 183 self.host.bridge.getGroupBlogs(pub_jid.userhost(), [], rsm_, True, C.SERVICE_PROFILE, d.callback, d.errback)
126 return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath), 184
127 'message': "Invalid nickname"} 185 def parseURLParams(self, request, item_id):
128 else: 186 # retrieve RSM request data from URL parameters
129 def got_jid(pub_jid_s): 187 rsm_ = {}
130 pub_jid = JID(pub_jid_s) 188 try:
131 d2 = defer.Deferred() 189 rsm_['max'] = request.args['max'][0]
132 item_id = None 190 except (ValueError, KeyError):
133 atom = None 191 rsm_['max'] = unicode(C.RSM_MAX_ITEMS if item_id else C.RSM_MAX_COMMENTS)
134 rsm_ = {} 192 try:
135 193 rsm_['index'] = request.args['index'][0]
136 if len(request.postpath) > 1: 194 except (ValueError, KeyError):
137 if request.postpath[1] == 'atom.xml': # return the atom feed 195 try:
138 atom = True 196 rsm_['before'] = request.args['before'][0]
139 else: 197 except KeyError:
140 try: # check if the given path is a valid UUID 198 try:
141 uuid.UUID(request.postpath[1]) 199 rsm_['after'] = request.args['after'][0]
142 item_id = request.postpath[1] 200 except KeyError:
143 except ValueError: 201 pass
144 pass 202 return rsm_
145
146 # retrieve RSM request data from URL parameters
147 try:
148 max_items = int(request.args['max'][0])
149 except (ValueError, KeyError):
150 max_items = C.RSM_MAX_ITEMS if item_id else C.RSM_MAX_COMMENTS
151 rsm_['max'] = unicode(max_items)
152 try:
153 rsm_['index'] = request.args['index'][0]
154 except (ValueError, KeyError):
155 try:
156 rsm_['before'] = request.args['before'][0]
157 except KeyError:
158 try:
159 rsm_['after'] = request.args['after'][0]
160 except KeyError:
161 pass
162
163 if atom is not None:
164 d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None)
165 self.host.bridge.getGroupBlogsAtom(pub_jid.userhost(), rsm_, C.SERVICE_PROFILE, d2.callback, d2.errback)
166 return
167
168 d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None)
169 if item_id:
170 if max_items > 0: # display one message and its comments
171 self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], {}, max_items, C.SERVICE_PROFILE, d2.callback, d2.errback)
172 else: # display one message, count its comments
173 self.host.bridge.getGroupBlogs(pub_jid.userhost(), [item_id], {}, True, C.SERVICE_PROFILE, d2.callback, d2.errback)
174 else:
175 if max_items == 1: # display one message and its comments
176 self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [], rsm_, C.RSM_MAX_COMMENTS, C.SERVICE_PROFILE, d2.callback, d2.errback)
177 else: # display the last messages, count their comments
178 self.host.bridge.getGroupBlogs(pub_jid.userhost(), [], rsm_, True, C.SERVICE_PROFILE, d2.callback, d2.errback)
179
180 d1 = defer.Deferred()
181 JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
182 d1.addCallbacks(got_jid)
183
184 return server.NOT_DONE_YET
185 203
186 def render_html_blog(self, mblog_data, request, profile): 204 def render_html_blog(self, mblog_data, request, profile):
187 """Retrieve the user parameters before actually rendering the static blog 205 """Retrieve the user parameters before actually rendering the static blog
188 206
189 @param mblog_data (list): couple (list, dict) with: 207 @param mblog_data (list): couple (list, dict) with:
243 url = url 261 url = url
244 suffix = "<br/>" 262 suffix = "<br/>"
245 else: 263 else:
246 url = default 264 url = default
247 suffix = "" 265 suffix = ""
248 return "<img src='%(url)s' alt='%(alt)s'/>%(suffix)s" % {'alt': alt, 'url': url, 'suffix': suffix} 266 return self.useTemplate(request, "banner", {'alt': alt, 'url': url, 'suffix': suffix})
249 267
250 avatar = os.path.normpath(root_url + getOption('avatar')) 268 avatar = os.path.normpath(root_url + getOption('avatar'))
251 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user 269 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user
252 request.write(""" 270 data = {'base_url': base_url,
253 <html> 271 'user': user,
254 <head> 272 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS),
255 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 273 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION),
256 <meta name="keywords" content="%(keywords)s"> 274 'title': getOption(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user,
257 <meta name="description" content="%(description)s"> 275 'favicon': avatar,
258 <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/> 276 'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title),
259 <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" /> 277 'title_elt': title,
260 <link rel="icon" type="image/png" href="%(favicon)s"> 278 }
261 <title>%(title)s</title> 279
262 </head>
263 <body>
264 <div class="mblog_title"><a href="%(base)s">%(banner_elt)s%(title_elt)s</a></div>
265 """ % {'base': base_url,
266 'root': root_url,
267 'user': user,
268 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS),
269 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION),
270 'title': getOption(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user,
271 'favicon': avatar,
272 'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, avatar, title),
273 'title_elt': title,
274 })
275 mblog_data, main_rsm = mblog_data 280 mblog_data, main_rsm = mblog_data
276 display_single = len(mblog_data) == 1
277
278 # build the navigation links
279 count = int(main_rsm['count']) if 'count' in main_rsm else 0
280 if count > 0:
281 index = int(main_rsm['index'])
282 if index > 0:
283 before_link = ("%(base)s?before=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['first']}).encode('utf-8')
284 if display_single:
285 before_link += '&max=1'
286 tmp_text = D_("later message")
287 class_ = 'later_message'
288 else:
289 tmp_text = D_("later messages")
290 class_ = 'later_messages'
291 before_tag = """<a href="%(link)s" class="%(class)s">%(text)s</a>""" % {'link': before_link, 'class': class_, 'text': tmp_text}
292 else:
293 before_tag = None
294 if index + len(mblog_data) < count:
295 after_link = ("%(base)s?after=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['last']}).encode('utf-8')
296 if display_single:
297 after_link += '&max=1'
298 text = D_("older message")
299 class_ = 'older_message'
300 else:
301 text = D_("older messages")
302 class_ = 'older_messages'
303 after_tag = """<a href="%(link)s" class="%(class)s">%(text)s</a>""" % {'link': after_link, 'class': class_, 'text': text}
304 else:
305 after_tag = None
306
307 # display navigation header
308 request.write("""<div class="header">""")
309 request.write("""<div class="header_content">""")
310 if before_tag:
311 request.write(before_tag)
312 if display_single and after_tag:
313 request.write(after_tag)
314 request.write("""</div></div>""")
315
316 mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data] 281 mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data]
317 mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('updated', 0)))) 282 mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('updated', 0))))
318 for main_data, comments_data in mblog_data: 283
319 self.__render_html_entry(main_data, base_url, request) 284 data.update(self.getNavigationLinks(request, mblog_data, main_rsm, base_url))
285 request.write(self.useTemplate(request, 'header', data))
286
287 BlogMessages(self.host, request, base_url, mblog_data).render()
288
289 request.write(self.useTemplate(request, "footer", data))
290 request.finish()
291
292 def getNavigationLinks(self, request, mblog_data, rsm_data, base_url):
293 """Build the navigation links.
294
295 @param mblog_data (dict): the microblogs that are displayed on the page
296 @param rsm_data (dict): rsm data
297 @param base_url (unicode): the base URL for this user's blog
298 @return: dict
299 """
300 data = {}
301 for key in ('later_message', 'later_messages', 'older_message', 'older_messages'):
302 count = int(rsm_data.get('count', 0))
303 display_single = len(mblog_data) == 1
304 data[key] = '' # key must exist when using the template
305 if count <= 0 or (display_single == key.endswith('s')):
306 continue
307
308 index = int(rsm_data['index'])
309
310 link_data = {'base_url': base_url, 'suffix': ''}
311
312 if key.startswith('later_message'):
313 if index <= 0:
314 continue
315 link_data['item_id'] = rsm_data['first']
316 link_data['post_arg'] = 'before'
317 else:
318 if index + len(mblog_data) >= count:
319 continue
320 link_data['item_id'] = rsm_data['last']
321 link_data['post_arg'] = 'after'
322
323 if display_single:
324 link_data['suffix'] = '&max=1'
325
326 link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data
327
328 link_data = {'link': link, 'class': key, 'text': key.replace('_', ' ')}
329 data[key] = (self.useTemplate(request, 'nav_link', link_data)).encode('utf-8')
330
331 return data
332
333 def render_atom_feed(self, feed, request):
334 request.write(feed.encode('utf-8'))
335 request.finish()
336
337 def render_error_blog(self, error, request, profile):
338 request.write(self.useTemplate(request, "error", {'message': "Can't access requested data"}))
339 request.finish()
340
341
342 class BlogMessages(TemplateProcessor):
343
344 def __init__(self, host, request, base_url, mblog_data):
345 TemplateProcessor.__init__(self, host)
346 self.request = request
347 self.base_url = base_url
348 self.mblog_data = mblog_data
349
350 def render(self):
351 for entry, comments_data in self.mblog_data:
320 comments, comments_rsm = comments_data 352 comments, comments_rsm = comments_data
321
322 # eventually display the link to show all comments
323 comments_count = int(main_data['comments_count'])
324 delta = comments_count - len(comments)
325 if display_single and delta > 0:
326 link = ("%(base)s/%(item_id)s?max=%(max)s" % {'base': base_url,
327 'item_id': main_data['id'],
328 'max': main_data['comments_count']}).encode('utf-8')
329 text = D_("Show %(count)d previous %(comments)s") % {'count': delta,
330 'comments': D_('comments') if delta > 1 else D_('comment')}
331 request.write("""<a href="%(link)s" class="comments_link">%(text)s</a>""" % {'link': link, 'text': text})
332
333 comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0)))) 353 comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0))))
334 for comment in comments: 354 self.render_html(entry, comments)
335 self.__render_html_entry(comment, base_url, request) 355
336 356 def getText(self, entry, key):
337 # display navigation footer 357 if ('%s_xhtml' % key) in entry:
338 request.write("""<div class="footer">""") 358 return entry['%s_xhtml' % key].encode('utf-8')
339 request.write("""<div class="footer_content">""") 359 elif key in entry:
340 if not display_single and after_tag: 360 processor = addURLToText if key.startswith('content') else sanitizeHtml
341 request.write(after_tag) 361 return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8')
342 request.write("""</div></div>""") 362 return None
343 363
344 request.write('</body></html>') 364 def render_html(self, entry, comments=None):
345 request.finish()
346
347 def __render_html_entry(self, entry, base_url, request):
348 """Render one microblog entry. 365 """Render one microblog entry.
349 @param entry: the microblog entry 366 @param entry: the microblog entry
350 @param base_url: the base url of the blog 367 @param base_url: the base url of the blog
351 @param request: the HTTP request 368 @param request: the HTTP request
352 """ 369 """
353 timestamp = float(entry.get('published', 0)) 370 timestamp = float(entry.get('published', 0))
354 datetime_ = datetime.fromtimestamp(timestamp)
355 is_comment = entry['type'] == 'comment' 371 is_comment = entry['type'] == 'comment'
356 372
357 def getText(key): 373 data = {'date': datetime.fromtimestamp(timestamp),
358 if ('%s_xhtml' % key) in entry: 374 'comments_link': '',
359 return entry['%s_xhtml' % key].encode('utf-8') 375 'previous_comments': '',
360 elif key in entry: 376 }
361 processor = addURLToText if key.startswith('content') else sanitizeHtml
362 return convertNewLinesToXHTML(processor(entry[key])).encode('utf-8')
363 return ''
364
365 def addMainItemLink(elem):
366 if not item_link or not elem:
367 return elem
368 return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem}
369 377
370 if is_comment: 378 if is_comment:
371 author = (_("from %s") % entry['author']).encode('utf-8') 379 author = (_("from %s") % entry['author']).encode('utf-8')
372 item_link = ''
373 footer = ''
374 else: 380 else:
375 author = '&nbsp;' 381 author = '&nbsp;'
376 item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8') 382 message_link = ("%s/%s" % (self.base_url, entry['id'])).encode('utf-8')
383
384 count_text = lambda count: D_('comments') if count > 1 else D_('comment')
385
377 comments_count = int(entry['comments_count']) 386 comments_count = int(entry['comments_count'])
378 comments_text = (D_('comments') if comments_count > 1 else D_('comment')).encode('utf-8') 387 delta = comments_count - len(comments)
379 footer = addMainItemLink("""<div class="mblog_footer mblog_footer_main"> 388 if len(self.mblog_data) == 1 and delta > 0:
380 <div class="mblog_metadata"> 389 data['comments_link'] = ("%s?max=%s" % (message_link, entry['comments_count']))
381 <div class="mblog_comments">%(count)s %(comments)s</div> 390 data['previous_comments'] = D_("Show %(count)d previous %(comments)s") % \
382 </div> 391 {'count': delta, 'comments': count_text(delta)}
383 </div>""" % {'count': comments_count, 392
384 'comments': comments_text}) 393 data.update({'comments_count': comments_count,
385 394 'comments_text': count_text(comments_count),
386 header = """<div class="mblog_header %(class)s"> 395 'message_link': message_link,
387 <div class="mblog_metadata"> 396 'message_title': self.getText(entry, 'title'),
388 <div class="mblog_author">%(author)s</div> 397 })
389 <div class="mblog_timestamp">%(date)s</div> 398
390 </div> 399 data.update({'author': author,
391 </div>""" % {'author': author, 'date': datetime_, 400 'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
392 'class': '' if is_comment else 'mblog_header_main'} 401 'content': self.getText(entry, 'content'),
393 if not is_comment: 402 })
394 header = addMainItemLink(header) 403
395 404 tpl = "%s%s" % ("" if data.get('message_title', None) else "micro_", "comment" if is_comment else "message")
396 title = addMainItemLink(getText('title')) 405 self.request.write(self.useTemplate(self.request, tpl, data))
397 body = getText('content') 406
398 if title: # insert the title within the body 407 if comments:
399 body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body} 408 for comment in comments:
400 409 self.render_html(comment)
401 request.write("""<div class="mblog_entry %(extra_style)s"> 410
402 %(header)s
403 <span class="mblog_content">%(content)s</span>
404 %(footer)s
405 </div>""" % {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
406 'item_link': item_link,
407 'header': header,
408 'content': body,
409 'footer': footer})
410
411 def render_atom_feed(self, feed, request):
412 request.write(feed.encode('utf-8'))
413 request.finish()
414
415 def render_error_blog(self, error, request, profile):
416 request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
417 'message': "Can't access requested data"})
418 request.finish()