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'})