comparison libervia/pages/blog/view/page_meta.py @ 1216:b2d067339de3

python 3 port: /!\ Python 3.6+ is now needed to use libervia /!\ instability may occur and features may not be working anymore, this will improve with time /!\ TxJSONRPC dependency has been removed The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs for details). Removed now deprecated code (Pyjamas compiled browser part, legacy blog, JSON RPC related code). Adapted code to work without `html` and `themes` dirs.
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:12:31 +0200
parents 67ec22356457
children 0f0c36992f3c
comparison
equal deleted inserted replaced
1215:f14ab8a25e8b 1216:b2d067339de3
1 #!/usr/bin/env python2.7 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 import unicodedata 3 import unicodedata
4 import re 4 import re
5 import cgi 5 import cgi
6 from libervia.server.constants import Const as C 6 from libervia.server.constants import Const as C
17 from sat.core.log import getLogger 17 from sat.core.log import getLogger
18 18
19 log = getLogger(__name__) 19 log = getLogger(__name__)
20 20
21 """generic blog (with service/node provided)""" 21 """generic blog (with service/node provided)"""
22 name = u'blog_view' 22 name = 'blog_view'
23 template = u"blog/articles.html" 23 template = "blog/articles.html"
24 uri_handlers = {(u'pubsub', u'microblog'): 'microblog_uri'} 24 uri_handlers = {('pubsub', 'microblog'): 'microblog_uri'}
25 25
26 RE_TEXT_URL = re.compile(ur'[^a-zA-Z,_]+') 26 RE_TEXT_URL = re.compile(r'[^a-zA-Z,_]+')
27 TEXT_MAX_LEN = 60 27 TEXT_MAX_LEN = 60
28 TEXT_WORD_MIN_LENGHT = 4 28 TEXT_WORD_MIN_LENGHT = 4
29 URL_LIMIT_MARK = 90 # if canonical URL is longer than that, text will not be appended 29 URL_LIMIT_MARK = 90 # if canonical URL is longer than that, text will not be appended
30 30
31 31
32 def microblog_uri(self, uri_data): 32 def microblog_uri(self, uri_data):
33 args = [uri_data[u'path'], uri_data[u'node']] 33 args = [uri_data['path'], uri_data['node']]
34 if u'item' in uri_data: 34 if 'item' in uri_data:
35 args.extend([u'id', uri_data[u'item']]) 35 args.extend(['id', uri_data['item']])
36 return self.getURL(*args) 36 return self.getURL(*args)
37 37
38 def parse_url(self, request): 38 def parse_url(self, request):
39 """URL is /[service]/[node]/[filter_keyword]/[item]|[other] 39 """URL is /[service]/[node]/[filter_keyword]/[item]|[other]
40 40
47 data = self.getRData(request) 47 data = self.getRData(request)
48 48
49 try: 49 try:
50 service = self.nextPath(request) 50 service = self.nextPath(request)
51 except IndexError: 51 except IndexError:
52 data['service'] = u'' 52 data['service'] = ''
53 else: 53 else:
54 try: 54 try:
55 data[u"service"] = jid.JID(service) 55 data["service"] = jid.JID(service)
56 except Exception: 56 except Exception:
57 log.warning(_(u"bad service entered: {}").format(service)) 57 log.warning(_("bad service entered: {}").format(service))
58 self.pageError(request, C.HTTP_BAD_REQUEST) 58 self.pageError(request, C.HTTP_BAD_REQUEST)
59 59
60 try: 60 try:
61 node = self.nextPath(request) 61 node = self.nextPath(request)
62 except IndexError: 62 except IndexError:
63 node = u'@' 63 node = '@'
64 data['node'] = u'' if node == u'@' else node 64 data['node'] = '' if node == '@' else node
65 65
66 try: 66 try:
67 filter_kw = data['filter_keyword'] = self.nextPath(request) 67 filter_kw = data['filter_keyword'] = self.nextPath(request)
68 except IndexError: 68 except IndexError:
69 filter_kw = u'@' 69 filter_kw = '@'
70 else: 70 else:
71 if filter_kw == u'@': 71 if filter_kw == '@':
72 # No filter, this is used when a subpage is needed, notably Atom feed 72 # No filter, this is used when a subpage is needed, notably Atom feed
73 pass 73 pass
74 elif filter_kw == u'id': 74 elif filter_kw == 'id':
75 try: 75 try:
76 data[u'item'] = self.nextPath(request) 76 data['item'] = self.nextPath(request)
77 except IndexError: 77 except IndexError:
78 self.pageError(request, C.HTTP_BAD_REQUEST) 78 self.pageError(request, C.HTTP_BAD_REQUEST)
79 # we get one more argument in case text has been added to have a nice URL 79 # we get one more argument in case text has been added to have a nice URL
80 try: 80 try:
81 self.nextPath(request) 81 self.nextPath(request)
82 except IndexError: 82 except IndexError:
83 pass 83 pass
84 elif filter_kw == u'tag': 84 elif filter_kw == 'tag':
85 try: 85 try:
86 data[u'tag'] = self.nextPath(request) 86 data['tag'] = self.nextPath(request)
87 except IndexError: 87 except IndexError:
88 self.pageError(request, C.HTTP_BAD_REQUEST) 88 self.pageError(request, C.HTTP_BAD_REQUEST)
89 else: 89 else:
90 # invalid filter keyword 90 # invalid filter keyword
91 log.warning(_(u"invalid filter keyword: {filter_kw}").format( 91 log.warning(_("invalid filter keyword: {filter_kw}").format(
92 filter_kw=filter_kw)) 92 filter_kw=filter_kw))
93 self.pageError(request, C.HTTP_BAD_REQUEST) 93 self.pageError(request, C.HTTP_BAD_REQUEST)
94 94
95 # if URL is parsed here, we'll have atom.xml available and we need to 95 # if URL is parsed here, we'll have atom.xml available and we need to
96 # add the link to the page 96 # add the link to the page
97 atom_url = self.getURLByPath( 97 atom_url = self.getURLByPath(
98 SubPage(u'blog_view'), 98 SubPage('blog_view'),
99 service, 99 service,
100 node, 100 node,
101 filter_kw, 101 filter_kw,
102 SubPage(u'blog_feed_atom'), 102 SubPage('blog_feed_atom'),
103 ) 103 )
104 request.template_data[u'atom_url'] = atom_url 104 request.template_data['atom_url'] = atom_url
105 request.template_data.setdefault(u'links', []).append({ 105 request.template_data.setdefault('links', []).append({
106 u"href": atom_url, 106 "href": atom_url,
107 u"type": "application/atom+xml", 107 "type": "application/atom+xml",
108 u"rel": "alternate", 108 "rel": "alternate",
109 u"title": "{service}'s blog".format(service=service)}) 109 "title": "{service}'s blog".format(service=service)})
110 110
111 111
112 @defer.inlineCallbacks 112 @defer.inlineCallbacks
113 def appendComments(self, blog_items, identities, profile): 113 def appendComments(self, blog_items, identities, profile):
114 for blog_item in blog_items: 114 for blog_item in blog_items:
115 if identities is not None: 115 if identities is not None:
116 author = blog_item.author_jid 116 author = blog_item.author_jid
117 if not author: 117 if not author:
118 log.warning(_(u"no author found for item {item_id}").format(item_id=blog_item.id)) 118 log.warning(_("no author found for item {item_id}").format(item_id=blog_item.id))
119 else: 119 else:
120 if author not in identities: 120 if author not in identities:
121 identities[author] = yield self.host.bridgeCall(u'identityGet', author, profile) 121 identities[author] = yield self.host.bridgeCall('identityGet', author, profile)
122 for comment_data in blog_item.comments: 122 for comment_data in blog_item.comments:
123 service = comment_data[u'service'] 123 service = comment_data['service']
124 node = comment_data[u'node'] 124 node = comment_data['node']
125 try: 125 try:
126 comments_data = yield self.host.bridgeCall(u'mbGet', 126 comments_data = yield self.host.bridgeCall('mbGet',
127 service, 127 service,
128 node, 128 node,
129 C.NO_LIMIT, 129 C.NO_LIMIT,
130 [], 130 [],
131 {C.KEY_ORDER_BY: C.ORDER_BY_CREATION}, 131 {C.KEY_ORDER_BY: C.ORDER_BY_CREATION},
132 profile) 132 profile)
133 except Exception as e: 133 except Exception as e:
134 log.warning(_(u"Can't get comments at {node} (service: {service}): {msg}").format( 134 log.warning(_("Can't get comments at {node} (service: {service}): {msg}").format(
135 service=service, 135 service=service,
136 node=node, 136 node=node,
137 msg=e)) 137 msg=e))
138 continue 138 continue
139 139
146 try: 146 try:
147 if item_id: 147 if item_id:
148 items_id = [item_id] 148 items_id = [item_id]
149 else: 149 else:
150 items_id = [] 150 items_id = []
151 blog_data = yield self.host.bridgeCall(u'mbGet', 151 blog_data = yield self.host.bridgeCall('mbGet',
152 service.userhost(), 152 service.userhost(),
153 node, 153 node,
154 C.NO_LIMIT, 154 C.NO_LIMIT,
155 items_id, 155 items_id,
156 extra, 156 extra,
157 profile) 157 profile)
158 except Exception as e: 158 except Exception as e:
159 # FIXME: need a better way to test errors in bridge errback 159 # FIXME: need a better way to test errors in bridge errback
160 if u"forbidden" in unicode(e): 160 if "forbidden" in str(e):
161 self.pageError(request, 401) 161 self.pageError(request, 401)
162 else: 162 else:
163 log.warning(_(u"can't retrieve blog for [{service}]: {msg}".format( 163 log.warning(_("can't retrieve blog for [{service}]: {msg}".format(
164 service = service.userhost(), msg=e))) 164 service = service.userhost(), msg=e)))
165 blog_data = ([], {}) 165 blog_data = ([], {})
166 166
167 defer.returnValue(data_objects.BlogItems(blog_data)) 167 defer.returnValue(data_objects.BlogItems(blog_data))
168 168
169 @defer.inlineCallbacks 169 @defer.inlineCallbacks
170 def prepare_render(self, request): 170 def prepare_render(self, request):
171 data = self.getRData(request) 171 data = self.getRData(request)
172 page_max = data.get(u"page_max", 10) 172 page_max = data.get("page_max", 10)
173 # if the comments are not explicitly hidden, we show them 173 # if the comments are not explicitly hidden, we show them
174 service, node, item_id, show_comments = data.get(u'service', u''), data.get(u'node', u''), data.get(u'item'), data.get(u'show_comments', True) 174 service, node, item_id, show_comments = data.get('service', ''), data.get('node', ''), data.get('item'), data.get('show_comments', True)
175 profile = self.getProfile(request) 175 profile = self.getProfile(request)
176 if profile is None: 176 if profile is None:
177 profile = C.SERVICE_PROFILE 177 profile = C.SERVICE_PROFILE
178 profile_connected = False 178 profile_connected = False
179 else: 179 else:
184 extra = {} 184 extra = {}
185 else: 185 else:
186 extra = self.getPubsubExtra(request, page_max=page_max) 186 extra = self.getPubsubExtra(request, page_max=page_max)
187 tag = data.get('tag') 187 tag = data.get('tag')
188 if tag: 188 if tag:
189 extra[u'mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag 189 extra['mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag
190 190
191 ## main data ## 191 ## main data ##
192 # we get data from backend/XMPP here 192 # we get data from backend/XMPP here
193 items = yield getBlogItems(self, request, service, node, item_id, extra, profile) 193 items = yield getBlogItems(self, request, service, node, item_id, extra, profile)
194 194
195 ## navigation ## 195 ## navigation ##
196 # no let's fill service, node and pagination URLs 196 # no let's fill service, node and pagination URLs
197 template_data = request.template_data 197 template_data = request.template_data
198 if u'service' not in template_data: 198 if 'service' not in template_data:
199 template_data[u'service'] = service 199 template_data['service'] = service
200 if u'node' not in template_data: 200 if 'node' not in template_data:
201 template_data[u'node'] = node 201 template_data['node'] = node
202 target_profile = template_data.get(u'target_profile') 202 target_profile = template_data.get('target_profile')
203 203
204 if items: 204 if items:
205 if not item_id: 205 if not item_id:
206 self.setPagination(request, items.metadata) 206 self.setPagination(request, items.metadata)
207 else: 207 else:
210 # we must return an error 210 # we must return an error
211 self.pageError(request, C.HTTP_NOT_FOUND) 211 self.pageError(request, C.HTTP_NOT_FOUND)
212 212
213 ## identities ## 213 ## identities ##
214 # identities are used to show nice nickname or avatars 214 # identities are used to show nice nickname or avatars
215 identities = template_data[u'identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities 215 identities = template_data['identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities
216 216
217 ## Comments ## 217 ## Comments ##
218 # if comments are requested, we need to take them 218 # if comments are requested, we need to take them
219 if show_comments: 219 if show_comments:
220 yield appendComments(self, items, identities, profile) 220 yield appendComments(self, items, identities, profile)
221 221
222 ## URLs ## 222 ## URLs ##
223 # We will fill items_http_uri and tags_http_uri in template_data with suitable urls 223 # We will fill items_http_uri and tags_http_uri in template_data with suitable urls
224 # if we know the profile, we use it instead of service + blog (nicer url) 224 # if we know the profile, we use it instead of service + blog (nicer url)
225 if target_profile is None: 225 if target_profile is None:
226 blog_base_url_item = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'id') 226 blog_base_url_item = self.getPageByName('blog_view').getURL(service.full(), node or '@', 'id')
227 blog_base_url_tag = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'tag') 227 blog_base_url_tag = self.getPageByName('blog_view').getURL(service.full(), node or '@', 'tag')
228 else: 228 else:
229 blog_base_url_item = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'id'])]) 229 blog_base_url_item = self.getURLByNames([('user', [target_profile]), ('user_blog', ['id'])])
230 blog_base_url_tag = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'tag'])]) 230 blog_base_url_tag = self.getURLByNames([('user', [target_profile]), ('user_blog', ['tag'])])
231 # we also set the background image if specified by user 231 # we also set the background image if specified by user
232 bg_img = yield self.host.bridgeCall(u'asyncGetParamA', u'Background', u'Blog page', u'value', -1, template_data[u'target_profile']) 232 bg_img = yield self.host.bridgeCall('asyncGetParamA', 'Background', 'Blog page', 'value', -1, template_data['target_profile'])
233 if bg_img: 233 if bg_img:
234 template_data['dynamic_style'] = safe(u""" 234 template_data['dynamic_style'] = safe("""
235 :root { 235 :root {
236 --bg-img: url("%s"); 236 --bg-img: url("%s");
237 } 237 }
238 """ % cgi.escape(bg_img, True)) 238 """ % cgi.escape(bg_img, True))
239 239
240 template_data[u'items'] = data[u'items'] = items 240 template_data['items'] = data['items'] = items
241 if request.args.get('reverse') == ['1']: 241 if request.args.get('reverse') == ['1']:
242 template_data[u'items'].items.reverse() 242 template_data['items'].items.reverse()
243 template_data[u'items_http_uri'] = items_http_uri = {} 243 template_data['items_http_uri'] = items_http_uri = {}
244 template_data[u'tags_http_uri'] = tags_http_uri = {} 244 template_data['tags_http_uri'] = tags_http_uri = {}
245 245
246 246
247 for item in items: 247 for item in items:
248 blog_canonical_url = u'/'.join([blog_base_url_item, utils.quote(item.id)]) 248 blog_canonical_url = '/'.join([blog_base_url_item, utils.quote(item.id)])
249 if len(blog_canonical_url) > URL_LIMIT_MARK: 249 if len(blog_canonical_url) > URL_LIMIT_MARK:
250 blog_url = blog_canonical_url 250 blog_url = blog_canonical_url
251 else: 251 else:
252 # we add text from title or body at the end of URL 252 # we add text from title or body at the end of URL
253 # to make it more human readable 253 # to make it more human readable
254 text = item.title or item.content 254 text = item.title or item.content
255 # we change special chars to ascii one, trick found at https://stackoverflow.com/a/3194567 255 # we change special chars to ascii one, trick found at https://stackoverflow.com/a/3194567
256 text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore') 256 text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode('utf-8')
257 text = RE_TEXT_URL.sub(u' ', text).lower() 257 text = RE_TEXT_URL.sub(' ', text).lower()
258 text = u'-'.join([t for t in text.split() if t and len(t)>=TEXT_WORD_MIN_LENGHT]) 258 text = '-'.join([t for t in text.split() if t and len(t)>=TEXT_WORD_MIN_LENGHT])
259 while len(text) > TEXT_MAX_LEN: 259 while len(text) > TEXT_MAX_LEN:
260 if u'-' in text: 260 if '-' in text:
261 text = text.rsplit(u'-', 1)[0] 261 text = text.rsplit('-', 1)[0]
262 else: 262 else:
263 text = text[:TEXT_MAX_LEN] 263 text = text[:TEXT_MAX_LEN]
264 if text: 264 if text:
265 blog_url = blog_canonical_url + u'/' + text 265 blog_url = blog_canonical_url + '/' + text
266 else: 266 else:
267 blog_url = blog_canonical_url 267 blog_url = blog_canonical_url
268 268
269 items_http_uri[item.id] = self.host.getExtBaseURL(request, blog_url) 269 items_http_uri[item.id] = self.host.getExtBaseURL(request, blog_url)
270 for tag in item.tags: 270 for tag in item.tags:
271 if tag not in tags_http_uri: 271 if tag not in tags_http_uri:
272 tag_url = u'/'.join([blog_base_url_tag, utils.quote(tag)]) 272 tag_url = '/'.join([blog_base_url_tag, utils.quote(tag)])
273 tags_http_uri[tag] = self.host.getExtBaseURL(request, tag_url) 273 tags_http_uri[tag] = self.host.getExtBaseURL(request, tag_url)
274 274
275 # if True, page should display a comment box 275 # if True, page should display a comment box
276 template_data[u'allow_commenting'] = data.get(u'allow_commenting', profile_connected) 276 template_data['allow_commenting'] = data.get('allow_commenting', profile_connected)
277 277
278 # last but not least, we add a xmpp: link to the node 278 # last but not least, we add a xmpp: link to the node
279 uri_args = {u'path': service.full()} 279 uri_args = {'path': service.full()}
280 if node: 280 if node:
281 uri_args[u'node'] = node 281 uri_args['node'] = node
282 if item_id: 282 if item_id:
283 uri_args[u'item'] = item_id 283 uri_args['item'] = item_id
284 template_data[u'xmpp_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', **uri_args) 284 template_data['xmpp_uri'] = uri.buildXMPPUri('pubsub', subtype='microblog', **uri_args)
285 285
286 286
287 @defer.inlineCallbacks 287 @defer.inlineCallbacks
288 def on_data_post(self, request): 288 def on_data_post(self, request):
289 profile = self.getProfile(request) 289 profile = self.getProfile(request)
290 if profile is None: 290 if profile is None:
291 self.pageError(request, C.HTTP_FORBIDDEN) 291 self.pageError(request, C.HTTP_FORBIDDEN)
292 type_ = self.getPostedData(request, u'type') 292 type_ = self.getPostedData(request, 'type')
293 if type_ == u'comment': 293 if type_ == 'comment':
294 service, node, body = self.getPostedData(request, (u'service', u'node', u'body')) 294 service, node, body = self.getPostedData(request, ('service', 'node', 'body'))
295 295
296 if not body: 296 if not body:
297 self.pageError(request, C.HTTP_BAD_REQUEST) 297 self.pageError(request, C.HTTP_BAD_REQUEST)
298 comment_data = {u"content": body} 298 comment_data = {"content": body}
299 try: 299 try:
300 yield self.host.bridgeCall(u'mbSend', 300 yield self.host.bridgeCall('mbSend',
301 service, 301 service,
302 node, 302 node,
303 data_format.serialise(comment_data), 303 data_format.serialise(comment_data),
304 profile) 304 profile)
305 except Exception as e: 305 except Exception as e:
306 if u"forbidden" in unicode(e): 306 if "forbidden" in str(e):
307 self.pageError(request, 401) 307 self.pageError(request, 401)
308 else: 308 else:
309 raise e 309 raise e
310 else: 310 else:
311 log.warning(_(u"Unhandled data type: {}").format(type_)) 311 log.warning(_("Unhandled data type: {}").format(type_))