comparison src/server/blog.py @ 823:027139763511

server (blog): cleaning & improvments: - use a constant for themes url - moved RSM related constants to server only constants, and renamed theme STATIC_RSM* - raised the default number of items/comments to 10 - removed references to microblog namespace as it is managed by backend - many little improvments for better readability - dont use dynamic relative paths anymore - replaced use of old formatting syntax (%) by format() - profile name in url is now properly (un)quoted - removed max_items as it was used at the same time as RSM (TODO: check RSM support before using it) - renamed render_* methods using camelCase for consistency - put a limit for rsm_max, to avoid overloading - don't sort items after getting them anymore, as sorting is already done by backend/pubsub according to request - use urllib.urlencode when possible
author Goffi <goffi@goffi.org>
date Fri, 08 Jan 2016 14:42:39 +0100
parents f8a7a046ff9c
children d990ae5612df
comparison
equal deleted inserted replaced
822:2819e4241e78 823:027139763511
28 from twisted.web import server 28 from twisted.web import server
29 from twisted.web.resource import Resource 29 from twisted.web.resource import Resource
30 from twisted.words.protocols.jabber.jid import JID 30 from twisted.words.protocols.jabber.jid import JID
31 from jinja2 import Environment, PackageLoader 31 from jinja2 import Environment, PackageLoader
32 from datetime import datetime 32 from datetime import datetime
33 from sys import path
34 import uuid
35 import re 33 import re
36 import os 34 import os
35 import sys
36 import urllib
37 37
38 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML 38 from libervia.server.html_tools import sanitizeHtml, convertNewLinesToXHTML
39 from libervia.server.constants import Const as C 39 from libervia.server.constants import Const as C
40 40
41 41
42 NS_MICROBLOG = 'urn:xmpp:microblog:0' 42 PARAMS_TO_GET = (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION)
43 43
44 # TODO: chech disco features and use max_items when RSM is not available
44 45
45 class TemplateProcessor(object): 46 class TemplateProcessor(object):
46 47
47 THEME = 'default' 48 THEME = 'default'
48 49
49 def __init__(self, host): 50 def __init__(self, host):
50 self.host = host 51 self.host = host
51 52
52 # add Libervia's themes directory to the python path 53 # add Libervia's themes directory to the python path
53 path.append(os.path.dirname(os.path.normpath(self.host.themes_dir))) 54 sys.path.append(os.path.dirname(os.path.normpath(self.host.themes_dir)))
54 themes = os.path.basename(os.path.normpath(self.host.themes_dir)) 55 themes = os.path.basename(os.path.normpath(self.host.themes_dir))
55 self.env = Environment(loader=PackageLoader(themes, self.THEME)) 56 self.env = Environment(loader=PackageLoader(themes, self.THEME))
56 57
57 def useTemplate(self, request, tpl, data=None): 58 def useTemplate(self, request, tpl, data=None):
58 root_url = '../' * len(request.postpath) 59 theme_url = os.path.join('/', C.THEMES_URL, self.THEME)
59 theme_url = os.path.join(root_url, 'themes', self.THEME)
60 60
61 data_ = {'images': os.path.join(theme_url, 'images'), 61 data_ = {'images': os.path.join(theme_url, 'images'),
62 'styles': os.path.join(theme_url, 'styles'), 62 'styles': os.path.join(theme_url, 'styles'),
63 } 63 }
64 if data: 64 if data:
65 data_.update(data) 65 data_.update(data)
66 66
67 template = self.env.get_template('%s.html' % tpl) 67 template = self.env.get_template('{}.html'.format(tpl))
68 return template.render(**data_).encode('utf-8') 68 return template.render(**data_).encode('utf-8')
69 69
70 70
71 class MicroBlog(Resource, TemplateProcessor): 71 class MicroBlog(Resource, TemplateProcessor):
72 isLeaf = True 72 isLeaf = True
77 TemplateProcessor.__init__(self, host) 77 TemplateProcessor.__init__(self, host)
78 self.host.bridge.register('entityDataUpdated', self.entityDataUpdatedHandler) 78 self.host.bridge.register('entityDataUpdated', self.entityDataUpdatedHandler)
79 self.avatars_cache = {} 79 self.avatars_cache = {}
80 self.waiting_deferreds = {} 80 self.waiting_deferreds = {}
81 81
82 def _quote(self, value):
83 """Quote a value for use in url
84
85 @param value(unicode): value to quote
86 @return (str): quoted value
87 """
88 return urllib.quote(value.encode('utf-8'), '')
89
90 def _unquote(self, quoted_value):
91 """Unquote a value coming from url
92
93 @param unquote_value(str): value to unquote
94 @return (unicode): unquoted value
95 """
96 return urllib.unquote(quoted_value).decode('utf-8')
97
82 def entityDataUpdatedHandler(self, entity_s, key, value, dummy): 98 def entityDataUpdatedHandler(self, entity_s, key, value, dummy):
83 """Retrieve the avatar we've been waiting for and fires the callback. 99 """Retrieve the avatar we've been waiting for and fires the callback.
84 100
85 @param entity_s (str): JID of the contact 101 @param entity_s (str): JID of the contact
86 @param key (str): entity data key 102 @param key (str): entity data key
92 log.debug(_(u"Received a new avatar for entity %s") % entity_s) 108 log.debug(_(u"Received a new avatar for entity %s") % entity_s)
93 109
94 url = os.path.join(C.AVATARS_DIR, value) 110 url = os.path.join(C.AVATARS_DIR, value)
95 self.avatars_cache[entity_s] = url 111 self.avatars_cache[entity_s] = url
96 try: 112 try:
97 self.waiting_deferreds[entity_s].callback(url) 113 self.waiting_deferreds.pop(entity_s).callback(url)
98 del self.waiting_deferreds[entity_s]
99 except KeyError: 114 except KeyError:
100 pass 115 pass
101 116
102 def getAvatarURL(self, pub_jid): 117 def getAvatarURL(self, pub_jid):
103 """Return avatar of a jid if in cache, else ask for it. 118 """Return avatar of a jid if in cache, else ask for it.
115 self.waiting_deferreds[bare_jid_s] = d 130 self.waiting_deferreds[bare_jid_s] = d
116 return d 131 return d
117 return defer.succeed(url if url else C.DEFAULT_AVATAR_URL) 132 return defer.succeed(url if url else C.DEFAULT_AVATAR_URL)
118 133
119 def render_GET(self, request): 134 def render_GET(self, request):
120 if not request.postpath: 135 if not request.postpath or len(request.postpath) > 2:
121 return self.useTemplate(request, "static_blog_error", {'message': "You must indicate a nickname"}) 136 return self.useTemplate(request, "static_blog_error", {'message': "You must indicate a nickname"})
122 137
123 prof_requested = request.postpath[0] 138 prof_requested = self._unquote(request.postpath[0])
124 #TODO : char check: only use alphanumeric chars + some extra(_,-,...) here 139
125 try: 140 try:
126 prof_found = self.host.bridge.getProfileName(prof_requested) 141 prof_found = self.host.bridge.getProfileName(prof_requested)
127 except DBusException: 142 except DBusException:
128 prof_found = None 143 prof_found = None
129 if not prof_found or prof_found == C.SERVICE_PROFILE: 144 if not prof_found or prof_found == C.SERVICE_PROFILE:
130 return self.useTemplate(request, "static_blog_error", {'message': "Invalid nickname"}) 145 return self.useTemplate(request, "static_blog_error", {'message': "Invalid nickname"})
131 146
132 d = defer.Deferred() 147 d = defer.Deferred()
148 # TODO: jid caching
133 self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', profile_key=prof_found, callback=d.callback, errback=d.errback) 149 self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', profile_key=prof_found, callback=d.callback, errback=d.errback)
134 d.addCallbacks(lambda pub_jid_s: self.gotJID(pub_jid_s, request, prof_found)) 150 d.addCallback(self.render_gotJID, request, prof_found)
135 return server.NOT_DONE_YET 151 return server.NOT_DONE_YET
136 152
137 def gotJID(self, pub_jid_s, request, profile): 153 def render_gotJID(self, pub_jid_s, request, profile):
138 pub_jid = JID(pub_jid_s) 154 pub_jid = JID(pub_jid_s)
139 155
156 request.extra_dict = {} # will be used for RSM and MAM
140 self.parseURLParams(request) 157 self.parseURLParams(request)
141 if request.item_id: 158 if request.item_id:
159 # we want a specific item
142 item_ids = [request.item_id] 160 item_ids = [request.item_id]
143 max_items = 1 161 max_items = 1
144 else: 162 else:
145 item_ids = [] 163 item_ids = []
146 max_items = int(request.extra_dict['rsm_max']) 164 # max_items = int(request.extra_dict['rsm_max']) # FIXME
165 max_items = 0
166 # TODO: use max_items only when RSM is not available
147 167
148 if request.atom: 168 if request.atom:
149 self.host.bridge.mbGetAtom(pub_jid.userhost(), NS_MICROBLOG, max_items, item_ids, 169 self.host.bridge.mbGetAtom(pub_jid.userhost(), '', max_items, item_ids,
150 request.extra_dict, C.SERVICE_PROFILE, 170 request.extra_dict, C.SERVICE_PROFILE,
151 lambda feed: self.render_atom_feed(feed, request), 171 lambda feed: self.renderAtomFeed(feed, request),
152 lambda failure: self.render_error_blog(failure, request, pub_jid)) 172 lambda failure: self.renderError(failure, request, pub_jid))
153 elif request.item_id: 173 elif request.item_id:
154 self.getItemById(pub_jid, request.item_id, request.extra_dict, 174 self.getItemById(pub_jid, request.item_id, request.extra_dict,
155 request.extra_comments_dict, request, profile) 175 request.extra_comments_dict, request, profile)
156 else: 176 else:
157 self.getItems(pub_jid, max_items, request.extra_dict, 177 self.getItems(pub_jid, max_items, request.extra_dict,
158 request.extra_comments_dict, request, profile) 178 request.extra_comments_dict, request, profile)
159 179
180 ## URL parsing
181
160 def parseURLParams(self, request): 182 def parseURLParams(self, request):
161 """Parse the request URL parameters. 183 """Parse the request URL parameters.
162 184
163 @param request: HTTP request 185 @param request: HTTP request
164 """ 186 """
165 request.item_id = None
166 request.atom = False
167
168 if len(request.postpath) > 1: 187 if len(request.postpath) > 1:
169 if request.postpath[1] == 'atom.xml': # return the atom feed 188 if request.postpath[1] == 'atom.xml': # return the atom feed
170 request.atom = True 189 request.atom = True
190 request.item_id = None
171 else: 191 else:
192 request.atom = False
172 request.item_id = request.postpath[1] 193 request.item_id = request.postpath[1]
194 else:
195 request.item_id = None
196 request.atom = False
173 197
174 self.parseURLParamsRSM(request) 198 self.parseURLParamsRSM(request)
199 # XXX: request.display_single is True when only one blog post is visible
175 request.display_single = (request.item_id is not None) or int(request.extra_dict['rsm_max']) == 1 200 request.display_single = (request.item_id is not None) or int(request.extra_dict['rsm_max']) == 1
176 self.parseURLParamsCommentsRSM(request) 201 self.parseURLParamsCommentsRSM(request)
177 202
178 def parseURLParamsRSM(self, request): 203 def parseURLParamsRSM(self, request):
179 """Parse RSM request data from the URL parameters for main items 204 """Parse RSM request data from the URL parameters for main items
180 205
181 @param request: HTTP request 206 fill request.extra_dict accordingly
182 """ 207 @param request: HTTP request
183 request.extra_dict = {} 208 """
184 if request.item_id: # XXX: item_id and RSM are not compatible 209 if request.item_id: # XXX: item_id and RSM are not compatible
185 return 210 return
186 try: 211 try:
187 request.extra_dict['rsm_max'] = request.args['max'][0] 212 rsm_max = int(request.args['max'][0])
213 if rsm_max > C.STATIC_RSM_MAX_LIMIT:
214 log.warning(u"Request with rsm_max over limit ({})".format(rsm_max))
215 rsm_max = C.STATIC_RSM_MAX_LIMIT
216 request.extra_dict['rsm_max'] = unicode(rsm_max)
188 except (ValueError, KeyError): 217 except (ValueError, KeyError):
189 request.extra_dict['rsm_max'] = unicode(C.RSM_MAX_ITEMS) 218 request.extra_dict['rsm_max'] = unicode(C.STATIC_RSM_MAX_DEFAULT)
190 try: 219 try:
191 request.extra_dict['rsm_index'] = request.args['index'][0] 220 request.extra_dict['rsm_index'] = request.args['index'][0]
192 except (ValueError, KeyError): 221 except (ValueError, KeyError):
193 try: 222 try:
194 request.extra_dict['rsm_before'] = request.args['before'][0] 223 request.extra_dict['rsm_before'] = request.args['before'][0]
199 pass 228 pass
200 229
201 def parseURLParamsCommentsRSM(self, request): 230 def parseURLParamsCommentsRSM(self, request):
202 """Parse RSM request data from the URL parameters for comments 231 """Parse RSM request data from the URL parameters for comments
203 232
233 fill request.extra_dict accordingly
204 @param request: HTTP request 234 @param request: HTTP request
205 """ 235 """
206 request.extra_comments_dict = {} 236 request.extra_comments_dict = {}
207 if request.display_single: 237 if request.display_single:
208 try: 238 try:
209 request.extra_comments_dict['rsm_max'] = request.args['comments_max'][0] 239 rsm_max = int(request.args['comments_max'][0])
240 if rsm_max > C.STATIC_RSM_MAX_LIMIT:
241 log.warning(u"Request with rsm_max over limit ({})".format(rsm_max))
242 rsm_max = C.STATIC_RSM_MAX_LIMIT
243 request.extra_comments_dict['rsm_max'] = unicode(rsm_max)
210 except (ValueError, KeyError): 244 except (ValueError, KeyError):
211 request.extra_comments_dict['rsm_max'] = unicode(C.RSM_MAX_COMMENTS) 245 request.extra_comments_dict['rsm_max'] = unicode(C.STATIC_RSM_MAX_COMMENTS_DEFAULT)
212 else: 246 else:
213 request.extra_comments_dict['rsm_max'] = "0" 247 request.extra_comments_dict['rsm_max'] = "0"
248
249 ## Items retrieval
214 250
215 def getItemById(self, pub_jid, item_id, extra_dict, extra_comments_dict, request, profile): 251 def getItemById(self, pub_jid, item_id, extra_dict, extra_comments_dict, request, profile):
216 """ 252 """
217 253
218 @param pub_jid (jid.JID): publisher JID 254 @param pub_jid (jid.JID): publisher JID
225 261
226 def gotItems(items): 262 def gotItems(items):
227 items, metadata = items 263 items, metadata = items
228 item = items[0] # assume there's only one item 264 item = items[0] # assume there's only one item
229 265
230 def gotCount(items_bis): 266 def gotMetadata(result):
231 metadata_bis = items_bis[1] 267 dummy, rsm_metadata = result
232 metadata['rsm_count'] = metadata_bis['rsm_count'] 268 try:
233 index_key = "rsm_index" if metadata_bis.get("rsm_index") else "rsm_count" 269 metadata['rsm_count'] = rsm_metadata['rsm_count']
234 metadata['rsm_index'] = unicode(int(metadata_bis[index_key]) - 1) 270 except KeyError:
271 pass
272 try:
273 metadata['rsm_index'] = unicode(int(rsm_metadata['rsm_index'])-1)
274 except KeyError:
275 pass
276
235 metadata['rsm_first'] = metadata['rsm_last'] = item["id"] 277 metadata['rsm_first'] = metadata['rsm_last'] = item["id"]
236 278
237 def gotComments(comments): 279 def gotComments(comments):
238 # build the items as self.getItems would do it (and as self.render_html_blog expects them to be) 280 # build the items as self.getItems would do it (and as self.renderHTML expects them to be)
239 comments = [(item['comments_service'], item['comments_node'], "", comments[0], comments[1])] 281 comments = [(item['comments_service'], item['comments_node'], "", comments[0], comments[1])]
240 self.render_html_blog([(item, comments)], metadata, request, pub_jid, profile) 282 self.renderHTML([(item, comments)], metadata, request, pub_jid, profile)
241 283
242 # get the comments 284 # get the comments
243 max_comments = int(extra_comments_dict['rsm_max']) 285 # max_comments = int(extra_comments_dict['rsm_max']) # FIXME
286 max_comments = 0
287 # TODO: use max_comments only when RSM is not available
244 self.host.bridge.mbGet(item['comments_service'], item['comments_node'], max_comments, [], 288 self.host.bridge.mbGet(item['comments_service'], item['comments_node'], max_comments, [],
245 extra_comments_dict, C.SERVICE_PROFILE, callback=gotComments) 289 extra_comments_dict, C.SERVICE_PROFILE, callback=gotComments)
246 290
247 # XXX: retrieve RSM information related to the main item. We can't do it while 291 # XXX: retrieve RSM information related to the main item. We can't do it while
248 # retrieving the item, because item_ids and rsm should not be used together. 292 # retrieving the item, because item_ids and rsm should not be used together.
249 self.host.bridge.mbGet(pub_jid.userhost(), NS_MICROBLOG, 1, [], 293 self.host.bridge.mbGet(pub_jid.userhost(), '', 0, [],
250 {"rsm_max": "1", "rsm_after": item["id"]}, C.SERVICE_PROFILE, callback=gotCount) 294 {"rsm_max": "1", "rsm_after": item["id"]}, C.SERVICE_PROFILE, callback=gotMetadata)
251 295
252 # get the main item 296 # get the main item
253 self.host.bridge.mbGet(pub_jid.userhost(), NS_MICROBLOG, 1, [item_id], 297 self.host.bridge.mbGet(pub_jid.userhost(), '', 1, [item_id],
254 extra_dict, C.SERVICE_PROFILE, callback=gotItems) 298 extra_dict, C.SERVICE_PROFILE, callback=gotItems)
255 299
256 def getItems(self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile): 300 def getItems(self, pub_jid, max_items, extra_dict, extra_comments_dict, request, profile):
257 """ 301 """
258 302
266 def getResultCb(data, rt_session): 310 def getResultCb(data, rt_session):
267 remaining, results = data 311 remaining, results = data
268 for result in results: 312 for result in results:
269 service, node, failure, items, metadata = result 313 service, node, failure, items, metadata = result
270 if not failure: 314 if not failure:
271 self.render_html_blog(items, metadata, request, pub_jid, profile) 315 self.renderHTML(items, metadata, request, pub_jid, profile)
272 316
273 if remaining: 317 if remaining:
274 self._getResults(rt_session) 318 self._getResults(rt_session)
275 319
276 def getResult(rt_session): 320 def getResult(rt_session):
277 self.host.bridge.mbGetFromManyWithCommentsRTResult(rt_session, C.SERVICE_PROFILE, 321 self.host.bridge.mbGetFromManyWithCommentsRTResult(rt_session, C.SERVICE_PROFILE,
278 callback=lambda data: getResultCb(data, rt_session), 322 callback=lambda data: getResultCb(data, rt_session),
279 errback=lambda failure: self.render_error_blog(failure, request, pub_jid)) 323 errback=lambda failure: self.renderError(failure, request, pub_jid))
280 324
281 max_comments = int(extra_comments_dict['rsm_max']) 325 # max_comments = int(extra_comments_dict['rsm_max']) # FIXME
326 max_comments = 0
327 # TODO: use max_comments only when RSM is not available
282 self.host.bridge.mbGetFromManyWithComments(C.JID, [pub_jid.userhost()], max_items, 328 self.host.bridge.mbGetFromManyWithComments(C.JID, [pub_jid.userhost()], max_items,
283 max_comments, extra_dict, extra_comments_dict, 329 max_comments, extra_dict, extra_comments_dict,
284 C.SERVICE_PROFILE, callback=getResult) 330 C.SERVICE_PROFILE, callback=getResult)
285 331
286 def render_html_blog(self, items, metadata, request, pub_jid, profile): 332 ## rendering
333
334 def _updateDict(self, value, dict_, key):
335 dict_[key] = value
336
337 def _getImageParams(self, options, key, default, alt):
338 """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
339 url = options[key] if key in options else ''
340 regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
341 if re.match(regexp, url):
342 url = url
343 else:
344 url = default
345 return BlogImage(url, alt)
346
347 def renderError(self, failure, request, pub_jid):
348 request.write(self.useTemplate(request, "static_blog_error", {'message': "Can't access requested data"}))
349 request.finish()
350
351 def renderHTML(self, items, metadata, request, pub_jid, profile):
287 """Retrieve the user parameters before actually rendering the static blog 352 """Retrieve the user parameters before actually rendering the static blog
288 353
289 @param items(list[tuple(dict, list)]): same as in self.__render_html_blog 354 @param items(list[tuple(dict, list)]): same as in self._renderHTML
290 @param metadata(dict): original node metadata 355 @param metadata(dict): original node metadata
291 @param request: HTTP request 356 @param request: HTTP request
292 @param pub_jid (JID): publisher JID 357 @param pub_jid (JID): publisher JID
293 @param profile (unicode): %(doc_profile)s 358 @param profile (unicode): %(doc_profile)s
294 """ 359 """
295 d_list = [] 360 d_list = []
296 options = {} 361 options = {}
297 362
298 def getCallback(param_name): 363 d = self.getAvatarURL(pub_jid)
364 d.addCallback(self._updateDict, options, 'avatar')
365 d.addErrback(self.renderError, request, pub_jid)
366 d_list.append(d)
367
368 for param_name in PARAMS_TO_GET:
299 d = defer.Deferred() 369 d = defer.Deferred()
300 d.addCallback(lambda value: options.update({param_name: value})) 370 self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=d.callback, errback=d.errback)
371 d.addCallback(self._updateDict, options, param_name)
372 d.addErrback(self.renderError, request, pub_jid)
301 d_list.append(d) 373 d_list.append(d)
302 return d.callback 374
303 375 dlist_d = defer.DeferredList(d_list)
304 eb = lambda failure: self.render_error_blog(failure, request, pub_jid) 376 dlist_d.addCallback(lambda dummy: self._renderHTML(items, metadata, options, request, pub_jid))
305 377
306 self.getAvatarURL(pub_jid).addCallbacks(getCallback('avatar'), eb) 378 def _renderHTML(self, items, metadata, options, request, pub_jid):
307 for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION):
308 self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb)
309
310 cb = lambda dummy: self.__render_html_blog(items, metadata, options, request, pub_jid)
311 defer.DeferredList(d_list).addCallback(cb)
312
313 def __render_html_blog(self, items, metadata, options, request, pub_jid):
314 """Actually render the static blog. 379 """Actually render the static blog.
315 380
316 If mblog_data is a list of dict, we are missing the comments items so we just 381 If mblog_data is a list of dict, we are missing the comments items so we just
317 display the main items. If mblog_data is a list of couple, each couple is 382 display the main items. If mblog_data is a list of couple, each couple is
318 associating a main item data with the list of its comments, so we render all. 383 associating a main item data with the list of its comments, so we render all.
319
320 @param items(list[tuple(dict, list)]): list of 2-tuple with 384 @param items(list[tuple(dict, list)]): list of 2-tuple with
321 - item(dict): item microblog data 385 - item(dict): item microblog data
322 - comments_list(list[tuple]): list of 5-tuple with 386 - comments_list(list[tuple]): list of 5-tuple with
323 - service (unicode): pubsub service where the comments node is 387 - service (unicode): pubsub service where the comments node is
324 - node (unicode): comments node 388 - node (unicode): comments node
331 @param pub_jid (JID): publisher JID 395 @param pub_jid (JID): publisher JID
332 """ 396 """
333 if not isinstance(options, dict): 397 if not isinstance(options, dict):
334 options = {} 398 options = {}
335 user = sanitizeHtml(pub_jid.user) 399 user = sanitizeHtml(pub_jid.user)
336 root_url = '../' * len(request.postpath) 400 base_url = os.path.join('/blog/',user)
337 base_url = root_url + 'blog/' + user
338 401
339 def getOption(key): 402 def getOption(key):
340 return sanitizeHtml(options[key]) if key in options else '' 403 return sanitizeHtml(options[key]) if key in options else ''
341 404
342 def getImageParams(key, default, alt): 405 avatar = os.path.normpath('/{}'.format(getOption('avatar')))
343 """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
344 url = options[key] if key in options else ''
345 regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
346 if re.match(regexp, url):
347 url = url
348 else:
349 url = default
350 return BlogImage(url, alt)
351
352 avatar = os.path.normpath(root_url + getOption('avatar'))
353 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user 406 title = getOption(C.STATIC_BLOG_PARAM_TITLE) or user
354 data = {'base_url': base_url, 407 data = {'base_url': base_url,
355 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS), 408 'keywords': getOption(C.STATIC_BLOG_PARAM_KEYWORDS),
356 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION), 409 'description': getOption(C.STATIC_BLOG_PARAM_DESCRIPTION),
357 'title': title, 410 'title': title,
358 'favicon': avatar, 411 'favicon': avatar,
359 'banner_img': getImageParams(C.STATIC_BLOG_PARAM_BANNER, avatar, title) 412 'banner_img': self._getImageParams(options, C.STATIC_BLOG_PARAM_BANNER, avatar, title)
360 } 413 }
361
362 items.sort(key=lambda entry: (-float(entry[0].get('updated', 0))))
363 414
364 data['navlinks'] = NavigationLinks(request, items, metadata, base_url) 415 data['navlinks'] = NavigationLinks(request, items, metadata, base_url)
365 data['messages'] = [] 416 data['messages'] = []
366 for item in items: 417 for item in items:
367 item, comments_list = item 418 item, comments_list = item
375 data['messages'].append(BlogMessage(request, base_url, item, comments, comments_count)) 426 data['messages'].append(BlogMessage(request, base_url, item, comments, comments_count))
376 427
377 request.write(self.useTemplate(request, 'static_blog', data)) 428 request.write(self.useTemplate(request, 'static_blog', data))
378 request.finish() 429 request.finish()
379 430
380 def render_atom_feed(self, feed, request): 431 def renderAtomFeed(self, feed, request):
381 request.write(feed.encode('utf-8')) 432 request.write(feed.encode('utf-8'))
382 request.finish()
383
384 def render_error_blog(self, error, request, pub_jid):
385 request.write(self.useTemplate(request, "static_blog_error", {'message': "Can't access requested data"}))
386 request.finish() 433 request.finish()
387 434
388 435
389 class NavigationLinks(object): 436 class NavigationLinks(object):
390 437
394 @param items (list): list of items 441 @param items (list): list of items
395 @param rsm_data (dict): rsm data 442 @param rsm_data (dict): rsm data
396 @param base_url (unicode): the base URL for this user's blog 443 @param base_url (unicode): the base URL for this user's blog
397 @return: dict 444 @return: dict
398 """ 445 """
399 for key in ('later_message', 'later_messages', 'older_message', 'older_messages'): 446 if request.display_single:
400 count = int(rsm_data.get('rsm_count', 0)) 447 links = ('later_message', 'older_message')
401 setattr(self, key, '') # key must exist when using the template 448 # key must exist when using the template
402 if count <= 0 or (request.display_single == key.endswith('s')): 449 self.later_messages = self.older_messages = ''
403 continue 450 else:
404 451 links = ('later_messages', 'older_messages')
405 index = int(rsm_data['rsm_index']) 452 self.later_message = self.older_message = ''
406 453
407 link_data = {'base_url': base_url, 'suffix': ''} 454 for key in links:
455 query_data = {}
408 456
409 if key.startswith('later_message'): 457 if key.startswith('later_message'):
410 if index <= 0: 458 try:
411 continue 459 index = int(rsm_data['rsm_index'])
412 link_data['item_id'] = rsm_data['rsm_first'] 460 except (KeyError, ValueError):
413 link_data['post_arg'] = 'before' 461 pass
462 else:
463 if index == 0:
464 # we don't show this link on first page
465 setattr(self, key, '')
466 continue
467 try:
468 query_data['before'] = rsm_data['rsm_first'].encode('utf-8')
469 except KeyError:
470 pass
414 else: 471 else:
415 if index + len(items) >= count: 472 try:
416 continue 473 index = int(rsm_data['rsm_index'])
417 link_data['item_id'] = rsm_data['rsm_last'] 474 count = int(rsm_data.get('rsm_count'))
418 link_data['post_arg'] = 'after' 475 except (KeyError, ValueError):
476 # XXX: if we don't have index or count, we can't know if we
477 # are on the last page or not
478 pass
479 else:
480 # if we have index, we don't show the after link
481 # on the last page
482 if index + len(items) >= count:
483 setattr(self, key, '')
484 continue
485 try:
486 query_data['after'] = rsm_data['rsm_last'].encode('utf-8')
487 except KeyError:
488 pass
419 489
420 if request.display_single: 490 if request.display_single:
421 link_data['suffix'] = '&max=1' 491 query_data['max'] = 1
422 492
423 link = "%(base_url)s?%(post_arg)s=%(item_id)s%(suffix)s" % link_data 493 link = "{}?{}".format(base_url, urllib.urlencode(query_data))
424
425 setattr(self, key, BlogLink(link, key, key.replace('_', ' '))) 494 setattr(self, key, BlogLink(link, key, key.replace('_', ' ')))
426 495
427 496
428 class BlogImage(object): 497 class BlogImage(object):
429 498
460 self.type = "comment" if is_comment else "main_item" 529 self.type = "comment" if is_comment else "main_item"
461 self.style = 'mblog_comment' if is_comment else '' 530 self.style = 'mblog_comment' if is_comment else ''
462 self.content = self.getText(entry, 'content') 531 self.content = self.getText(entry, 'content')
463 532
464 if is_comment: 533 if is_comment:
465 self.author = (_("from %s") % entry['author']) 534 self.author = (_("from {}").format(entry['author']))
466 else: 535 else:
467 self.author = '&nbsp;' 536 self.author = '&nbsp;'
468 self.url = (u"%s/%s" % (base_url, entry['id'])) 537 self.url = "{}/{}".format(base_url, entry['id'].encode('utf-8'))
469 self.title = self.getText(entry, 'title') 538 self.title = self.getText(entry, 'title')
470 self.tags = list(common.dict2iter('tag', entry)) 539 self.tags = list(common.dict2iter('tag', entry))
471 540
472 count_text = lambda count: D_('comments') if count > 1 else D_('comment') 541 count_text = lambda count: D_('comments') if count > 1 else D_('comment')
473 542
474 self.comments_text = "%s %s" % (comments_count, count_text(comments_count)) 543 self.comments_text = u"{} {}".format(comments_count, count_text(comments_count))
475 544
476 delta = comments_count - len(comments) 545 delta = comments_count - len(comments)
477 if request.display_single and delta > 0: 546 if request.display_single and delta > 0:
478 prev_url = "%s?comments_max=%s" % (self.url, unicode(comments_count)) 547 prev_url = "{}?{}".format(self.url, urllib.urlencode({'comments_max', comments_count}))
479 prev_text = D_("show %(count)d previous %(comments)s") % \ 548 prev_text = D_("show {count} previous {comments}").format(
480 {'count': delta, 'comments': count_text(delta)} 549 count = delta, comments = count_text(delta))
481 self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text) 550 self.all_comments_link = BlogLink(prev_url, "comments_link", prev_text)
482 551
483 if comments: 552 if comments:
484 comments.sort(key=lambda comment: float(comment.get('published', 0)))
485 self.comments = [BlogMessage(request, base_url, comment) for comment in comments] 553 self.comments = [BlogMessage(request, base_url, comment) for comment in comments]
486 554
487 def getText(self, entry, key): 555 def getText(self, entry, key):
488 if ('%s_xhtml' % key) in entry: 556 try:
489 return entry['%s_xhtml' % key] 557 return entry['{}_xhtml'.format(key)]
490 elif key in entry: 558 except KeyError:
491 processor = addURLToText if key.startswith('content') else sanitizeHtml 559 try:
492 return convertNewLinesToXHTML(processor(entry[key])) 560 processor = addURLToText if key.startswith('content') else sanitizeHtml
493 return None 561 return convertNewLinesToXHTML(processor(entry[key]))
562 except KeyError:
563 return None