comparison libervia/pages/blog/view/page_meta.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/pages/blog/view/page_meta.py@ff503f23ac37
children 122dd136d1ab
comparison
equal deleted inserted replaced
1123:63a4b8fe9782 1124:28e3eb3bb217
1 #!/usr/bin/env python2.7
2 # -*- coding: utf-8 -*-
3 from libervia.server.constants import Const as C
4 from twisted.words.protocols.jabber import jid
5 from twisted.internet import defer
6 from sat.tools.common import data_objects
7 from libervia.server import session_iface
8 from sat.core.i18n import _
9 from sat.tools.common.template import safe
10 from sat.tools.common import uri
11 from libervia.server import utils
12 import unicodedata
13 import re
14 import cgi
15 from sat.core.log import getLogger
16 log = getLogger('pages/blog/view')
17
18 """generic blog (with service/node provided)"""
19 name = u'blog_view'
20 template = u"blog/articles.html"
21 uri_handlers = {(u'pubsub', u'microblog'): 'microblog_uri'}
22
23 RE_TEXT_URL = re.compile(ur'[^a-zA-Z,_]+')
24 TEXT_MAX_LEN = 60
25 TEXT_WORD_MIN_LENGHT = 4
26 URL_LIMIT_MARK = 90 # if canonical URL is longer than that, text will not be appended
27
28
29 def microblog_uri(self, uri_data):
30 args = [uri_data[u'path'], uri_data[u'node']]
31 if u'item' in uri_data:
32 args.extend([u'id', uri_data[u'item']])
33 return self.getURL(*args)
34
35 def parse_url(self, request):
36 """URL is /[service]/[node]/[filter_keyword]/[item]|[other]
37
38 if [node] is '@', default namespace is used
39 if a value is unset, default one will be used
40 keyword can be one of:
41 id: next value is a item id
42 tag: next value is a blog tag
43 """
44 data = self.getRData(request)
45
46 try:
47 service = self.nextPath(request)
48 except IndexError:
49 data['service'] = u''
50 else:
51 try:
52 data[u"service"] = jid.JID(service)
53 except Exception:
54 log.warning(_(u"bad service entered: {}").format(service))
55 self.pageError(request, C.HTTP_BAD_REQUEST)
56
57 try:
58 data['node'] = self.nextPath(request)
59 except IndexError:
60 data['node'] = u''
61 else:
62 if data['node'] == u'@':
63 data['node'] = u''
64
65 try:
66 filter_kw = data['filter_keyword'] = self.nextPath(request)
67 except IndexError:
68 pass
69 else:
70 if filter_kw == u'id':
71 try:
72 data[u'item'] = self.nextPath(request)
73 except IndexError:
74 self.pageError(request, C.HTTP_BAD_REQUEST)
75 # we get one more argument in case text has been added to have a nice URL
76 try:
77 self.nextPath(request)
78 except IndexError:
79 pass
80 elif filter_kw == u'tag':
81 try:
82 data[u'tag'] = self.nextPath(request)
83 except IndexError:
84 self.pageError(request, C.HTTP_BAD_REQUEST)
85 else:
86 # invalid filter keyword
87 log.warning(_(u"invalid filter keyword: {filter_kw}").format(filter_kw=filter_kw))
88 self.pageError(request, C.HTTP_BAD_REQUEST)
89
90
91 @defer.inlineCallbacks
92 def appendComments(self, blog_items, identities, profile):
93 for blog_item in blog_items:
94 if identities is not None:
95 author = blog_item.author_jid
96 if not author:
97 log.warning(_(u"no author found for item {item_id}").format(item_id=blog_item.id))
98 else:
99 if author not in identities:
100 identities[author] = yield self.host.bridgeCall(u'identityGet', author, profile)
101 for comment_data in blog_item.comments:
102 service = comment_data[u'service']
103 node = comment_data[u'node']
104 try:
105 comments_data = yield self.host.bridgeCall(u'mbGet',
106 service,
107 node,
108 C.NO_LIMIT,
109 [],
110 {},
111 profile)
112 except Exception as e:
113 log.warning(_(u"Can't get comments at {node} (service: {service}): {msg}").format(
114 service=service,
115 node=node,
116 msg=e))
117 continue
118
119 comments = data_objects.BlogItems(comments_data)
120 blog_item.appendCommentsItems(comments)
121 yield appendComments(self, comments, identities, profile)
122
123 @defer.inlineCallbacks
124 def getBlogData(self, request, service, node, item_id, extra, profile):
125 try:
126 if item_id:
127 items_id = [item_id]
128 else:
129 items_id = []
130 blog_data = yield self.host.bridgeCall(u'mbGet',
131 service.userhost(),
132 node,
133 C.NO_LIMIT,
134 items_id,
135 extra,
136 profile)
137 except Exception as e:
138 # FIXME: need a better way to test errors in bridge errback
139 if u"forbidden" in unicode(e):
140 self.pageError(request, 401)
141 else:
142 log.warning(_(u"can't retrieve blog for [{service}]: {msg}".format(
143 service = service.userhost(), msg=e)))
144 blog_data = ([], {})
145
146 items = data_objects.BlogItems(blog_data)
147 defer.returnValue((blog_data, items))
148
149 @defer.inlineCallbacks
150 def prepare_render(self, request):
151 data = self.getRData(request)
152 # if the comments are not explicitly hidden, we show them
153 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)
154 profile = self.getProfile(request)
155 if profile is None:
156 profile = C.SERVICE_PROFILE
157
158 ## pagination/filtering parameters
159 params = self.getAllPostedData(request, multiple=False)
160 if item_id:
161 extra = {}
162 else:
163 extra = {u'rsm_max': u'10'}
164 if u'after' in params:
165 extra[u'rsm_after'] = params[u'after']
166 elif u'before' in params:
167 extra[u'rsm_before'] = params[u'before']
168 tag = data.get('tag')
169 if tag:
170 extra[u'mam_filter_{}'.format(C.MAM_FILTER_CATEGORY)] = tag
171
172 ## main data ##
173 # we get data from backend/XMPP here
174 blog_data, items = yield getBlogData(self, request, service, node, item_id, extra, profile)
175
176 ## navigation ##
177 # no let's fill service, node and pagination URLs
178 template_data = request.template_data
179 if u'service' not in template_data:
180 template_data[u'service'] = service
181 if u'node' not in template_data:
182 template_data[u'node'] = node
183 target_profile = template_data.get(u'target_profile')
184
185 if items:
186 if not item_id:
187 last_id = items[-1].id
188 template_data['older_url'] = self.getParamURL(request, after=last_id)
189 if u'before' in params or u'after' in params:
190 first_id = items[0].id
191 template_data['newer_url'] = self.getParamURL(request, before=first_id)
192 else:
193 if item_id:
194 # if item id has been specified in URL and it's not found,
195 # we must return an error
196 self.pageError(request, C.HTTP_NOT_FOUND)
197
198 # no items, we have requested items before last post, or blog is empty
199 extra = {u'rsm_max': u'10'}
200 blog_data, items = yield getBlogData(self, request, service, node, None, extra, profile)
201 if items:
202 last_id = items[-1].id
203 template_data['older_url'] = self.getParamURL(request, after=last_id)
204
205 ## identities ##
206 # identities are use to show nice nickname or avatars
207 identities = template_data[u'identities'] = self.host.getSessionData(request, session_iface.ISATSession).identities
208
209 ## Comments ##
210 # if comments are requested, we need to take them
211 if show_comments:
212 yield appendComments(self, items, identities, profile)
213
214 ## URLs ##
215 # We will fill items_http_uri and tags_http_uri in template_data with suitable urls
216 # if we know the profile, we use it instead of service + blog (nicer url)
217 if target_profile is None:
218 blog_base_url_item = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'id')
219 blog_base_url_tag = self.getPageByName(u'blog_view').getURL(service.full(), node or u'@', u'tag')
220 else:
221 blog_base_url_item = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'id'])])
222 blog_base_url_tag = self.getURLByNames([(u'user', [target_profile]), (u'user_blog', [u'tag'])])
223 # we also set the background image if specified by user
224 bg_img = yield self.host.bridgeCall(u'asyncGetParamA', u'Background', u'Blog page', u'value', -1, template_data[u'target_profile'])
225 if bg_img:
226 template_data['dynamic_style'] = safe(u"""
227 :root {
228 --bg-img: url("%s");
229 }
230 """ % cgi.escape(bg_img, True))
231
232 template_data[u'items'] = data[u'items'] = items
233 if request.args.get('reverse') == ['1']:
234 template_data[u'items'].items.reverse()
235 template_data[u'items_http_uri'] = items_http_uri = {}
236 template_data[u'tags_http_uri'] = tags_http_uri = {}
237
238
239 for item in items:
240 blog_canonical_url = u'/'.join([blog_base_url_item, utils.quote(item.id)])
241 if len(blog_canonical_url) > URL_LIMIT_MARK:
242 blog_url = blog_canonical_url
243 else:
244 # we add text from title or body at the end of URL
245 # to make it more human readable
246 text = item.title or item.content
247 # we change special chars to ascii one, trick found at https://stackoverflow.com/a/3194567
248 text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore')
249 text = RE_TEXT_URL.sub(u' ', text).lower()
250 text = u'-'.join([t for t in text.split() if t and len(t)>=TEXT_WORD_MIN_LENGHT])
251 while len(text) > TEXT_MAX_LEN:
252 if u'-' in text:
253 text = text.rsplit(u'-', 1)[0]
254 else:
255 text = text[:TEXT_MAX_LEN]
256 if text:
257 blog_url = blog_canonical_url + u'/' + text
258 else:
259 blog_url = blog_canonical_url
260
261 items_http_uri[item.id] = self.host.getExtBaseURL(request, blog_url)
262 for tag in item.tags:
263 if tag not in tags_http_uri:
264 tag_url = u'/'.join([blog_base_url_tag, utils.quote(tag)])
265 tags_http_uri[tag] = self.host.getExtBaseURL(request, tag_url)
266
267 # if True, page should display a comment box
268 template_data[u'allow_commenting'] = data.get(u'allow_commenting', False)
269
270 # last but not least, we add a xmpp: link to the node
271 uri_args = {u'path': service.full()}
272 if node:
273 uri_args[u'node'] = node
274 if item_id:
275 uri_args[u'item'] = item_id
276 template_data[u'xmpp_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', **uri_args)
277
278
279 @defer.inlineCallbacks
280 def on_data_post(self, request):
281 profile = self.getProfile(request)
282 if profile is None:
283 self.pageError(request, C.HTTP_UNAUTHORIZED)
284 type_ = self.getPostedData(request, u'type')
285 if type_ == u'comment':
286 service, node, body = self.getPostedData(request, (u'service', u'node', u'body'))
287
288 if not body:
289 self.pageError(request, C.HTTP_BAD_REQUEST)
290 comment_data = {u"content": body}
291 try:
292 yield self.host.bridgeCall(u'mbSend', service, node, comment_data, profile)
293 except Exception as e:
294 if u"forbidden" in unicode(e):
295 self.pageError(request, 401)
296 else:
297 raise e
298 else:
299 log.warning(_(u"Unhandled data type: {}").format(type_))