Mercurial > libervia-web
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 = ' ' | 536 self.author = ' ' |
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 |