Mercurial > hg > bitsyblog
comparison bitsyblog/bitsyblog.py @ 0:e3823be6a423
initial commit of bitsyblog, from https://svn.openplans.org/svn/standalone/bitsyblog/trunk/
author | k0s <k0scist@gmail.com> |
---|---|
date | Sat, 12 Sep 2009 16:06:57 -0400 |
parents | |
children | 1368be6c3b70 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:e3823be6a423 |
---|---|
1 """ | |
2 a tiny tiny blog. | |
3 this is the view class and is more bitsyblog than anything | |
4 else can claim to be | |
5 """ | |
6 | |
7 ### global variables | |
8 | |
9 # who can view which blog posts | |
10 roles = { 'public': ( 'public', ), | |
11 'friend': ( 'public', 'secret' ), | |
12 'author': ( 'public', 'secret', 'private' ), } | |
13 | |
14 ### imports | |
15 | |
16 import dateutil.parser # XXX separate, for now | |
17 import parser # bitsyblog dateutil parser | |
18 | |
19 import cgi | |
20 import datetime | |
21 import docutils | |
22 import docutils.core | |
23 import inspect | |
24 import markup | |
25 import os | |
26 import PyRSS2Gen | |
27 import re | |
28 import utils | |
29 | |
30 from blog import FileBlog | |
31 from docutils.utils import SystemMessage | |
32 from lxml import etree | |
33 from user import FilespaceUsers | |
34 from markup.form import Form | |
35 from cStringIO import StringIO | |
36 from webob import Request, Response, exc | |
37 | |
38 ### exceptions | |
39 | |
40 class BlogPathException(Exception): | |
41 """exception when trying to retrieve the blog""" | |
42 | |
43 ### the main course | |
44 | |
45 class BitsyBlog(object): | |
46 """a very tiny blog""" | |
47 | |
48 ### class level variables | |
49 defaults = { 'date_format': '%H:%M %F', | |
50 'file_dir': os.path.dirname(__file__), | |
51 'subject': '[ %(date)s ]:', | |
52 'n_links': 5, # number of links for navigation | |
53 'site_name': 'bitsyblog', | |
54 'help_file': None | |
55 } | |
56 | |
57 def __init__(self, **kw): | |
58 for key in self.defaults: | |
59 setattr(self, key, kw.get(key, self.defaults[key])) | |
60 self.n_links = int(self.n_links) # could be a string from the .ini | |
61 self.response_functions = { 'GET': self.get, | |
62 'POST': self.post, | |
63 'PUT': self.put | |
64 } | |
65 | |
66 # abstract attributes | |
67 self.users = FilespaceUsers(self.file_dir) | |
68 self.blog = FileBlog(self.file_dir) | |
69 self.cooker = self.restructuredText | |
70 | |
71 if hasattr(self, 'help_file') and os.path.exists(self.help_file): | |
72 help = file(self.help_file).read() | |
73 self.help = docutils.core.publish_string(help, | |
74 writer_name='html', | |
75 settings_overrides={'report_level': 5}) | |
76 | |
77 | |
78 # for BitsyAuth | |
79 self.newuser = self.users.new | |
80 | |
81 ### methods dealing with HTTP | |
82 | |
83 def __call__(self, environ, start_response): | |
84 self.request = Request(environ) | |
85 res = self.make_response(self.request.method) | |
86 return res(environ, start_response) | |
87 | |
88 def make_response(self, method): | |
89 return self.response_functions.get(method, self.error)() | |
90 | |
91 def get_response(self, text, content_type='text/html'): | |
92 res = Response(content_type=content_type, body=text) | |
93 res.content_length = len(res.body) | |
94 return res | |
95 | |
96 def get_index(self): | |
97 """returns material pertaining to the root of the site""" | |
98 path = self.request.path_info.strip('/') | |
99 | |
100 n_links = self.number_of_links() | |
101 | |
102 ### the front page | |
103 if not path: | |
104 return self.get_response(self.index(n_links)) | |
105 | |
106 ### feeds | |
107 | |
108 n_posts = self.number_of_posts() | |
109 | |
110 # site rss | |
111 if path == 'rss': | |
112 if n_posts is None: | |
113 n_posts = 10 | |
114 return self.get_response(self.site_rss(n_posts), content_type='text/xml') | |
115 | |
116 # site atom | |
117 if path == 'atom': | |
118 if n_posts is None: | |
119 n_posts = 10 | |
120 return self.get_response(self.atom(self.blog.latest(self.users.users(), n_posts)), content_type='text/xml') | |
121 | |
122 ### help | |
123 if path == 'help' and hasattr(self, 'help'): | |
124 return self.get_response(self.help) | |
125 | |
126 ### static files | |
127 | |
128 # site.css | |
129 if path == 'css/site.css': | |
130 css_file = os.path.join(self.file_dir, 'site.css') | |
131 return self.get_response(file(css_file).read(), content_type='text/css') | |
132 | |
133 # logo | |
134 if path == 'bitsyblog.png': | |
135 logo = os.path.join(self.file_dir, 'bitsyblog.png') | |
136 return self.get_response(file(logo, 'rb').read(), content_type='image/png') | |
137 | |
138 def get_user_space(self, user, path): | |
139 self.request.user = self.users[user] # user whose blog is viewed | |
140 check = self.check_user(user) # is this the authenticated user? | |
141 | |
142 feed = None # not an rss/atom feed by default (flag) | |
143 n_posts = self.number_of_posts(user) | |
144 | |
145 # special paths | |
146 if path == [ 'post' ]: | |
147 if check is not None: | |
148 return check | |
149 return self.get_response(self.form_post(user)) | |
150 | |
151 if path == [ 'preferences' ]: | |
152 if check is not None: | |
153 return check | |
154 return self.get_response(self.preferences(user)) | |
155 | |
156 if path == [ 'rss' ]: | |
157 feed = 'rss' | |
158 path = [] | |
159 if n_posts is None: | |
160 n_posts = 10 # TODO: allow to be configurable | |
161 | |
162 if path == [ 'atom' ]: | |
163 feed = 'atom' | |
164 path = [] | |
165 if n_posts is None: | |
166 n_posts = 10 # TODO: allow to be configurable | |
167 | |
168 if len(path) == 2: | |
169 if path[0] == 'css': | |
170 for i in self.request.user.settings['CSS']: | |
171 # find the right CSS file | |
172 if i['filename'] == path[1]: | |
173 return self.get_response(i['css'], content_type='text/css') | |
174 else: | |
175 return exc.HTTPNotFound('CSS "%s" not found' % path[1]) | |
176 | |
177 role = self.role(user) | |
178 | |
179 # get the blog | |
180 try: | |
181 blog = self.get_blog(user, path, role, n_items=n_posts) | |
182 except BlogPathException, e: | |
183 return exc.HTTPNotFound(str(e)) | |
184 except exc.HTTPException, e: | |
185 return e.wsgi_response | |
186 | |
187 if feed == 'rss': | |
188 content = self.rss(user, blog) # XXX different signatures | |
189 return self.get_response(content, content_type='text/xml') | |
190 | |
191 if feed == 'atom': | |
192 content = self.atom(blog, user) # XXX different signatures | |
193 return self.get_response(content, content_type='text/xml') | |
194 | |
195 # reverse the blog if necessary | |
196 if self.request.GET.get('sort') == 'forward': | |
197 blog = list(reversed(blog)) | |
198 | |
199 n_links = self.number_of_links(user) | |
200 # don't display navigation for short blogs | |
201 if len(blog) < 2: | |
202 n_links = 0 | |
203 | |
204 # write the blog | |
205 content = self.write_blog(user, blog, self.request.path_info, n_links) | |
206 | |
207 # return the content | |
208 return self.get_response(content) | |
209 | |
210 def get(self): | |
211 """ | |
212 display the blog or respond to a get request | |
213 """ | |
214 # front matter of the site | |
215 index = self.get_index() | |
216 if index is not None: | |
217 return index | |
218 | |
219 ### user space | |
220 user, path = self.userpath() | |
221 if user not in self.users: | |
222 return exc.HTTPNotFound("No blog found for %s" % user) | |
223 return self.get_user_space(user, path) | |
224 | |
225 def post(self): | |
226 """ | |
227 write a blog entry and other POST requests | |
228 """ | |
229 # TODO: posting to '/' ? | |
230 | |
231 # find user + path | |
232 user, path = self.userpath() | |
233 | |
234 if user not in self.users: | |
235 return exc.HTTPNotFound("No blog found for %s" % user) | |
236 self.request.user = self.users[user] | |
237 | |
238 check = self.check_user(user) | |
239 if check is not None: | |
240 return check | |
241 | |
242 if len(path): | |
243 if path == [ 'preferences' ]: | |
244 | |
245 # make the data look like we want | |
246 settings = {} | |
247 settings['Date format'] = self.request.POST.get('Date format') | |
248 settings['Subject'] = '%(date)s'.join((self.request.POST['Subject-0'], self.request.POST['Subject-2'])) | |
249 settings['Stylesheet'] = self.request.POST['Stylesheet'] | |
250 settings['CSS file'] = self.request.POST.get('CSS file') | |
251 settings['Friends'] = ', '.join(self.request.POST.getall('Friends')) | |
252 | |
253 errors = self.users.write_settings(user, **settings) | |
254 if errors: # re-display form with errors | |
255 return self.get_response(self.preferences(user, errors)) | |
256 | |
257 return self.get_response(self.preferences(user, message='Changes saved')) | |
258 elif len(path) == 1 and self.isentry(path[0]): | |
259 entry = self.blog.entry(user, path[0], roles['author']) | |
260 if entry is None: | |
261 return exc.HTTPNotFound("Blog entry %s not found %s" % path[0]) | |
262 privacy = self.request.POST.get('privacy') | |
263 datestamp = entry.datestamp() | |
264 if privacy: | |
265 self.blog.delete(user, datestamp) | |
266 self.blog.post(user, datestamp, entry.body, privacy) | |
267 return exc.HTTPSeeOther("Settings updated", location='/%s/%s' % (user, datestamp)) | |
268 else: | |
269 return exc.HTTPMethodNotAllowed("Not sure what you're trying to do") | |
270 | |
271 # get the body of the post | |
272 body = self.request.body | |
273 body = self.request.POST.get('form-post', body) | |
274 body = body.strip() | |
275 if not body: | |
276 return exc.HTTPSeeOther("Your post has no content! No blog for you", | |
277 location='/%s' % self.user_url(user, 'post')) | |
278 | |
279 # determine if the post is secret or private | |
280 privacy = self.request.GET.get('privacy') or self.request.POST.get('privacy') or 'public' | |
281 | |
282 # write the file | |
283 now = utils.datestamp(datetime.datetime.now()) | |
284 location = "/%s" % self.user_url(user, now) | |
285 self.blog.post(user, now, body, privacy) | |
286 | |
287 # point the user at the post | |
288 return exc.HTTPSeeOther("Post blogged by bitsy", location=location) | |
289 | |
290 def put(self): | |
291 """ | |
292 PUT several blog entries from a file | |
293 """ | |
294 | |
295 # find user + path | |
296 user, path = self.user() | |
297 | |
298 if user not in self.users.users(): | |
299 return exc.HTTPNotFound("No blog found for %s" % user) | |
300 | |
301 if len(path): | |
302 return exc.HTTPMethodNotAllowed("Not sure what you're trying to do") | |
303 | |
304 # find the dates + entries in the file | |
305 regex = '\[.*\]:' | |
306 entries = re.split(regex, self.request.body)[1:] | |
307 dates = [ date.strip().strip(':').strip('[]').strip() | |
308 for date in re.findall(regex, self.request.body) ] | |
309 dates = [ dateutil.parser.parse(date) for date in dates ] | |
310 | |
311 # write to the blog | |
312 for i in range(len(entries)): | |
313 datestamp = utils.datestamp(dates[i]) | |
314 self.blog.post(user, datestamp, entries[i], 'public') | |
315 | |
316 return exc.HTTPOk("%s posts blogged by bitsy" % len(entries)) | |
317 | |
318 | |
319 def error(self): | |
320 """deal with non-supported methods""" | |
321 methods = ', '.join(self.response_functions.keys()[:1]) | |
322 methods += ' and %s' % self.response_functions.keys()[-1] | |
323 return exc.HTTPMethodNotAllowed("Only %s operations are allowed" % methods) | |
324 | |
325 ### auth functions | |
326 | |
327 def passwords(self): | |
328 return self.users.passwords() | |
329 | |
330 def authenticated(self): | |
331 """return authenticated user""" | |
332 return self.request.environ.get('REMOTE_USER') | |
333 | |
334 def check_user(self, user): | |
335 """ | |
336 determine authenticated user | |
337 returns None on success | |
338 """ | |
339 authenticated = self.authenticated() | |
340 if authenticated is None: | |
341 return exc.HTTPUnauthorized('Unauthorized') | |
342 elif user != authenticated: | |
343 return exc.HTTPForbidden("Forbidden") | |
344 | |
345 def role(self, user): | |
346 """ | |
347 determine what role the authenticated member has | |
348 with respect to the user | |
349 """ | |
350 auth = self.authenticated() | |
351 if not auth: | |
352 return 'public' | |
353 if auth == user: | |
354 return 'author' | |
355 else: | |
356 if auth in self.request.user.settings['Friends']: | |
357 return 'friend' | |
358 else: | |
359 return 'public' | |
360 | |
361 ### user methods | |
362 | |
363 def userpath(self): | |
364 """user who's blog one is viewing""" | |
365 path = self.request.path_info.strip('/').split('/') | |
366 name = path[0] | |
367 path = path[1:] | |
368 if not name: | |
369 name = None | |
370 return name, path | |
371 | |
372 ### date methods | |
373 | |
374 def isentry(self, string): # TODO -> blog.py | |
375 return (len(string) == len(''.join(utils.timeformat))) and string.isdigit() | |
376 | |
377 def user_url(self, user, *args, **kw): | |
378 permalink = kw.get('permalink') | |
379 if permalink: | |
380 _args = [ self.request.host_url, user ] | |
381 else: | |
382 _args = [ user ] | |
383 _args.extend(args) | |
384 return '/'.join([str(arg) for arg in _args]) | |
385 | |
386 def permalink(self, blogentry): | |
387 return self.user_url(blogentry.user, blogentry.datestamp(), permalink=True) | |
388 | |
389 def entry_subject(self, blogentry): | |
390 if hasattr(self.request, 'user') and self.request.user.name == blogentry.user: | |
391 prefs = self.request.user.settings | |
392 else: | |
393 prefs = self.users[blogentry.user].settings | |
394 subject = prefs.get('Subject', self.subject) | |
395 date_format = prefs.get('Date format', self.date_format) | |
396 return subject % { 'date': blogentry.date.strftime(date_format) } | |
397 | |
398 def mangledurl(self, blogentry): | |
399 return self.user_url(blogentry.user, 'x%x' % (int(blogentry.datestamp()) * self.users.secret(blogentry.user)), permalink=True) | |
400 | |
401 def unmangleurl(self, url, user): | |
402 url = url.strip('x') | |
403 try: | |
404 value = int(url, 16) | |
405 except ValueError: | |
406 return None | |
407 | |
408 # XXX once one has a mangled url, one can obtain the secret | |
409 value /= self.users.secret(user) | |
410 | |
411 entry = str(value) | |
412 if self.isentry(entry): | |
413 return self.blog.entry(user, entry, ['public', 'secret', 'private']) | |
414 | |
415 ### blog retrival methods | |
416 | |
417 def number_of_posts(self, user=None): | |
418 """return the number of blog posts to display""" | |
419 # determine number of posts to display (None -> all) | |
420 n_posts = self.request.GET.get('posts', None) | |
421 if n_posts is not None: | |
422 try: | |
423 n_posts = int(n_posts) | |
424 if n_links > 0 and n_links > n_posts: | |
425 n_links = n_posts # don't display more links than posts | |
426 except (TypeError, ValueError): | |
427 n_posts = None | |
428 return n_posts | |
429 | |
430 def number_of_links(self, user=None): | |
431 """return the number of links to display in the navigation""" | |
432 # determine number of navigation links to display | |
433 n_links = self.request.GET.get('n') | |
434 if n_links == 'all': | |
435 return -1 | |
436 try: | |
437 n_links = int(n_links) | |
438 except (TypeError, ValueError): | |
439 n_links = self.n_links | |
440 return n_links | |
441 | |
442 def get_blog(self, user, path, role='public', n_items=None): | |
443 """retrieve the blog entry based on the path""" | |
444 | |
445 notfound = "blog entry not found" | |
446 | |
447 # get permissions | |
448 allowed = roles[role] | |
449 | |
450 # entire blog | |
451 if not path: | |
452 return self.blog(user, allowed, n_items) | |
453 | |
454 # mangled urls | |
455 if len(path) == 1 and path[0].startswith('x'): | |
456 entry = self.unmangleurl(path[0], user) | |
457 if entry: | |
458 return [ entry ] | |
459 else: | |
460 raise BlogPathException(notfound) | |
461 | |
462 # individual blog entry | |
463 if (len(path) == 1) and self.isentry(path[0]): | |
464 blog = self.blog.entry(user, path[0], allowed) | |
465 if not blog: | |
466 raise BlogPathException(notfound) | |
467 return [ blog ] | |
468 | |
469 # parse the path into a date path | |
470 n_date_vals = 3 # year, month, date | |
471 if len(path) > n_date_vals: | |
472 raise BlogPathException(notfound) | |
473 | |
474 # ensure the path conforms to expected values (ints): | |
475 try: | |
476 [ int(i) for i in path] | |
477 except ValueError: | |
478 raise BlogPathException(notfound) | |
479 | |
480 # get the blog | |
481 return self.blog.entries(user, allowed, *path) | |
482 | |
483 ### methods that write HTML | |
484 | |
485 def render(self, body, title=None, feedtitle=None): | |
486 """layout the page in a unified way""" | |
487 stylesheets = () | |
488 user = getattr(self.request, 'user', None) | |
489 _title = [ self.site_name ] | |
490 if user: | |
491 stylesheets = self.request.user['CSS'] | |
492 stylesheets = [ (("/%s" % self.user_url(user.name, 'css', css['filename'])), | |
493 css['name']) for css in stylesheets ] | |
494 _title.insert(0, self.request.user.name) | |
495 else: | |
496 stylesheets = (("/css/site.css", "Default"),) | |
497 | |
498 if title: | |
499 _title.insert(0, title) | |
500 | |
501 title = ' - '.join(_title) | |
502 head_markup = () | |
503 if feedtitle: | |
504 head_markup = ( '<link rel="alternate" type="application/atom+xml" title="%s" href="atom" />' % feedtitle, | |
505 '<link rel="alternate" type="application/rss+xml" title="%s" href="rss" />' % feedtitle,) | |
506 return markup.wrap(self.site_nav()+body, title, stylesheets, head_markup=head_markup) | |
507 | |
508 def site_nav(self): | |
509 """returns HTML for site navigation""" | |
510 links = [('/',), ] | |
511 user = self.authenticated() | |
512 if user: | |
513 links.extend([('/%s' % user, user), | |
514 ('/%s/post' % user, 'post'), | |
515 ('/%s/preferences' % user, 'preferences'), | |
516 ('/logout', 'logout')]) | |
517 else: | |
518 links.extend([('/login', 'login'), ('/join', 'join')]) | |
519 | |
520 if hasattr(self, 'help'): | |
521 links.append(('/help', 'help')) | |
522 | |
523 links = [ markup.link(*i) for i in links ] | |
524 return markup.listify(links, ordered=False, **{ 'class': 'site-nav'}) | |
525 | |
526 def index(self, n_links): | |
527 retval = StringIO() | |
528 print >> retval, '<h1><img src="bitsyblog.png" alt="bitsyblog"/></h1>' | |
529 | |
530 # get the blogs | |
531 blogs = {} | |
532 for user in self.users: | |
533 blog = self.blog(user, ('public',), n_links) | |
534 if blog: | |
535 blogs[user] = blog | |
536 users = blogs.keys() | |
537 | |
538 # display latest active user first | |
539 users.sort(key=lambda user: blogs[user][0].date, reverse=True) | |
540 | |
541 # display users' blogs | |
542 for user in users: | |
543 print >> retval, '<div id="%s" class="user">' % user | |
544 print >> retval, '<a href="%s">%s</a>' % (user, user) | |
545 blog = blogs[user] | |
546 print >> retval, self.navigation(user, blog, '/%s' % user, n_links) | |
547 print >> retval, '</div>' | |
548 | |
549 return self.render(retval.getvalue(), feedtitle=self.site_name) | |
550 | |
551 def navigation(self, user, blog, path, n_links, n_char=80): | |
552 prefs = self.users[user].settings | |
553 | |
554 if n_links == 0 or not len(blog): | |
555 return '' | |
556 retval = StringIO() | |
557 print >> retval, '<div class="navigation">' | |
558 more = '' | |
559 if (n_links != -1) and (len(blog) > n_links): | |
560 more = '<a href="%s?n=all">more</a>' % path | |
561 blog = blog[:n_links] | |
562 | |
563 entries = [] | |
564 for entry in blog: | |
565 id = entry.datestamp() | |
566 format = prefs.get('Date format', self.date_format) | |
567 datestamp = entry.date.strftime(format) | |
568 synopsis = entry.title() | |
569 if synopsis: | |
570 synopsis = ': %s' % cgi.escape(synopsis) | |
571 entries.append(markup.link("%s#%s" % (path, id), datestamp) + synopsis) | |
572 | |
573 print >> retval, markup.listify(entries) | |
574 print >> retval, more | |
575 print >> retval, '</div>' | |
576 return retval.getvalue() | |
577 | |
578 def blog_entry(self, user, entry): | |
579 """given the content string, return a marked-up blog entry""" | |
580 # XXX no need to pass user | |
581 | |
582 # user preferences | |
583 prefs = self.request.user.settings | |
584 format = prefs.get('Date format', self.date_format) | |
585 subject = prefs.get('Subject', self.subject) | |
586 | |
587 role = self.role(user) | |
588 | |
589 subject = subject % { 'date' : entry.date.strftime(format) } | |
590 subject = cgi.escape(subject) | |
591 html = StringIO() | |
592 id = entry.datestamp() | |
593 print >> html, '<div id="%s" class="blog-entry">' % id | |
594 print >> html, '<a name="%s" />' % id | |
595 print >> html, '<div class="subject">' | |
596 print >> html, '<a href="/%s">%s</a>' % (self.user_url(user, id), subject) | |
597 if (entry.privacy == 'secret') and (role == 'friend'): | |
598 print >> html, '<em>secret</em>' | |
599 print >> html, '</div>' | |
600 print >> html, self.cooker(entry.body) | |
601 | |
602 if role == 'author': | |
603 print >> html, '<div><form action="/%s" method="post">' % self.user_url(entry.user, id) | |
604 print >> html, self.privacy_settings(entry.privacy) | |
605 print >> html, '<input type="submit" name="submit" value="Change Privacy" />' | |
606 print >> html, '</form></div>' | |
607 if entry.privacy != 'public': | |
608 title = "You can give this URL so people may see this %s post without logging in" % entry.privacy | |
609 print >> html, '<div>' | |
610 print >> html, '<span title="%s">Mangled URL:</span>' % title | |
611 print >> html, markup.link(self.mangledurl(entry)) | |
612 print >> html, '</div>' | |
613 | |
614 print >> html, '</div>' | |
615 return html.getvalue() | |
616 | |
617 def write_blog(self, user, blog, path, n_links): | |
618 """return the user's blog in HTML""" | |
619 # XXX no need to pass path or user! | |
620 retval = StringIO() | |
621 print >> retval, self.navigation(user, blog, path, n_links, 0) | |
622 for entry in blog: | |
623 print >> retval, self.blog_entry(user, entry) | |
624 feedtitle=None | |
625 if self.request.path_info.strip('/') == user: | |
626 feedtitle = "%s's blog" % user | |
627 title = None | |
628 if len(blog) == 1: | |
629 format = self.request.user.settings.get('Date format', self.date_format) | |
630 title = blog[0].date.strftime(format) | |
631 return self.render(retval.getvalue(), title=title, feedtitle=feedtitle) | |
632 | |
633 def restructuredText(self, string): | |
634 origstring = string | |
635 settings = { 'report_level': 5 } | |
636 string = string.strip() | |
637 try: | |
638 | |
639 parts = docutils.core.publish_parts(string.strip(), | |
640 writer_name='html', | |
641 settings_overrides=settings) | |
642 body = parts['body'] | |
643 except SystemMessage, e: | |
644 lines = [ cgi.escape(i.strip()) for i in string.split('\n') ] | |
645 body = '<br/>\n'.join(lines) | |
646 | |
647 | |
648 retval = '<div class="blog-body">%s</div>' % body | |
649 return retval | |
650 | |
651 # this should be reenabled if 'system-message's again appear in the markup | |
652 try: | |
653 foo = etree.fromstring(retval) | |
654 except etree.XMLSyntaxError: | |
655 return retval | |
656 # should cleanup the <div class="system-message"> | |
657 for i in foo.getiterator(): | |
658 if dict(i.items()).get('class') == 'system-message': | |
659 i.clear() | |
660 | |
661 return etree.tostring(foo) | |
662 | |
663 ### feeds | |
664 | |
665 def site_rss(self, n_items=10): | |
666 blog = self.blog.latest(list(self.users.users()), n_items) | |
667 title = self.site_name + ' - rss' | |
668 link = self.request.host_url # XXX should be smarter | |
669 description = "latest scribblings on %s" % self.site_name | |
670 lastBuildDate = datetime.datetime.now() | |
671 items = [ self.rss_item(entry.user, entry) for entry in blog ] | |
672 rss = PyRSS2Gen.RSS2(title=title, | |
673 link=link, | |
674 description=description, | |
675 lastBuildDate=lastBuildDate, | |
676 items=items) | |
677 return rss.to_xml() | |
678 | |
679 def rss(self, user, blog): | |
680 """ | |
681 rss feed for a user's blog | |
682 done with PyRSS2Gen: | |
683 http://www.dalkescientific.com/Python/PyRSS2Gen.html | |
684 """ | |
685 title = "%s's blog" % user | |
686 link = os.path.split(self.request.url)[0] | |
687 description = "latest blog entries for %s on %s" % (user, self.site_name) | |
688 lastBuildDate = datetime.datetime.now() # not sure what this means | |
689 | |
690 items = [ self.rss_item(user, entry) for entry in blog ] | |
691 rss = PyRSS2Gen.RSS2(title=title, | |
692 link=link, | |
693 description=description, | |
694 lastBuildDate=lastBuildDate, | |
695 items=items) | |
696 return rss.to_xml() | |
697 | |
698 def rss_item(self, user, entry): | |
699 if hasattr(self.request, 'user') and self.request.user.name == user: | |
700 prefs = self.request.user.settings | |
701 else: | |
702 prefs = self.users[user].settings | |
703 subject = prefs.get('Subject', self.subject) | |
704 date_format = prefs.get('Date format', self.date_format) | |
705 title = entry.title() | |
706 return PyRSS2Gen.RSSItem(title=title, #subject % { 'date': entry.date.strftime(date_format) }, | |
707 link=self.permalink(entry), | |
708 description=unicode(entry.body, errors='replace'), | |
709 author=user, | |
710 guid=PyRSS2Gen.Guid(self.permalink(entry)), | |
711 pubDate=entry.date) | |
712 | |
713 | |
714 def atom(self, blog, author=None): | |
715 retval = StringIO() | |
716 print >> retval, """<?xml version="1.0" encoding="utf-8"?> | |
717 <feed xmlns="http://www.w3.org/2005/Atom"> | |
718 """ | |
719 if author: | |
720 title = "%s's blog" % author | |
721 link = self.request.host_url + '/' + author | |
722 else: | |
723 title = self.site_name + ' - atom' | |
724 link = self.request.host_url | |
725 | |
726 date = blog[0].date.isoformat() | |
727 | |
728 print >> retval, '<title>%s</title>' % title | |
729 print >> retval, '<link href="%s" />' % link | |
730 print >> retval, '<updated>%s</updated>' % date | |
731 if author: | |
732 print >> retval, """ | |
733 <author> | |
734 <name>%s</name> | |
735 </author>""" % author | |
736 | |
737 for entry in blog: | |
738 print >> retval, '<entry>' | |
739 print >> retval, '<title>%s</title>' % cgi.escape(entry.title()) | |
740 print >> retval, '<link href="%s" />' % self.permalink(entry) | |
741 print >> retval, '<updated>%s</updated>' % entry.date.isoformat() | |
742 print >> retval, '<summary>%s</summary>' % cgi.escape(entry.body) | |
743 | |
744 print >> retval, '</entry>' | |
745 | |
746 print >> retval, '</feed>' | |
747 return retval.getvalue() | |
748 | |
749 ### forms and accompanying display | |
750 | |
751 def form_post(self, user): | |
752 retval = StringIO() | |
753 print >> retval, '<form action="/%s" method="post">' % self.user_url(user) | |
754 print >> retval, '<textarea cols="80" rows="25" name="form-post"></textarea><br/>' | |
755 print >> retval, self.privacy_settings() | |
756 print >> retval, '<input type="submit" name="submit" value="Post" />' | |
757 print >> retval, '</form>' | |
758 return self.render(retval.getvalue()) | |
759 | |
760 def preferences_form(self, user): | |
761 prefs = self.request.user.settings | |
762 form = Form() | |
763 | |
764 # date format | |
765 format = prefs.get('Date format', self.date_format) | |
766 value = datetime.datetime.now().strftime(format) | |
767 form.add_element('textfield', 'Date format', value=value, | |
768 help='how to display dates in your blog post subject') | |
769 | |
770 # subject | |
771 subject = prefs.get('Subject', self.subject) | |
772 subject = subject.split('%(date)s', 1) | |
773 func = lambda name: value | |
774 form.add_elements('Subject', | |
775 ['textfield', func, 'textfield' ], | |
776 [ (), (), ()], # this is horrible! | |
777 [ dict(value=subject[0]), | |
778 {}, | |
779 dict(value=subject[1]) ], | |
780 help='how to display the subject line of your blog post' | |
781 ) | |
782 | |
783 # CSS files | |
784 css_files = [ i['name'] for i in prefs['CSS'] ] | |
785 form.add_element('menu', 'Stylesheet', css_files, | |
786 help='which CSS file should be the default') | |
787 | |
788 # or upload a CSS file | |
789 form.add_element('file_upload', 'CSS file', | |
790 help='upload a CSS file to theme your webpage') | |
791 | |
792 # Friends -- can see secret posts | |
793 users = [ i for i in list(self.users.users()) | |
794 if i != user ] | |
795 if users: | |
796 users.sort(key=str.lower) | |
797 form.add_element('checkboxes', 'Friends', | |
798 users, prefs.get('Friends', set()), | |
799 help='friends can see your secret posts') | |
800 | |
801 return form | |
802 | |
803 def preferences(self, user, errors=None, message=None): | |
804 """user preferences form""" | |
805 body = self.preferences_form(user)(errors) | |
806 if message: | |
807 body = '%s\n%s' % ( markup.p(markup.strong(message)), body ) | |
808 return self.render(body, title='preferences') | |
809 | |
810 def privacy_settings(self, default='public'): | |
811 """HTML snippet for privacy settings""" | |
812 settings = (('public', 'viewable to everyone'), | |
813 ('secret', 'viewable only to your friends'), | |
814 ('private', 'viewable only to you')) | |
815 form = Form() | |
816 return form.radiobuttons('privacy', settings, checked=default, joiner=' ') | |
817 | |
818 class BitsierBlog(BitsyBlog): | |
819 """single user version of bitsyblog""" | |
820 | |
821 def get(self): | |
822 ### user space | |
823 user, path = self.userpath() | |
824 if user not in self.users: | |
825 return exc.HTTPNotFound("No blog found for %s" % user) | |
826 | |
827 return self.get_user_space(user, path) | |
828 | |
829 def userpath(self): | |
830 path = self.request.path_info.strip('/').split('/') | |
831 if path == ['']: | |
832 path = [] | |
833 return self.user, path | |
834 | |
835 def user_url(self, user, *args, **kw): | |
836 permalink = kw.get('permalink') | |
837 if permalink: | |
838 _args = [ self.request.host_url ] | |
839 else: | |
840 _args = [ ] | |
841 _args.extend(args) | |
842 return '/'.join([str(arg) for arg in _args]) | |
843 | |
844 def passwords(self): | |
845 return { self.user: self.users.password(self.user) } | |
846 | |
847 def site_nav(self): | |
848 """returns HTML for site navigation""" | |
849 links = [('/', self.user), ] | |
850 user = self.authenticated() | |
851 if user: | |
852 links.extend([ | |
853 ('/post', 'post'), | |
854 ('/preferences', 'preferences'), | |
855 ('/logout', 'logout')]) | |
856 else: | |
857 links.append(('/login', 'login')) | |
858 | |
859 if hasattr(self, 'help'): | |
860 links.append(('/help', 'help')) | |
861 | |
862 links = [ markup.link(*i) for i in links ] | |
863 return markup.listify(links, ordered=False, **{ 'class': 'site-nav'}) |