Gibe 0.11.1 released
14 Jul
Fearing I'd never actually getting around to making a neutral theme to put into my web log project, Gibe, I've now just bundled the BloggingPro theme by DesignDisease into the base release.
I've also bundled the tags plugin for an improved default experience.
And I've removed the custom Google search keyed to my domain. You know, in case someone actually would like to use it on _their_ site and not mine...
Next release will have "pages" - just generic pages that aren't web log entries, so I can stop maintaining stuff in HTML manually.
And, hopefully, I'll be able to optionally support postmarkup for the editing of posts and pages. Especially pages - custom postmarkup tags being defined in plugins would be very cool!
And then begins the descent into madness that is creating a from-scratch CMS system for Pylons (maybe using a TurboGears 2 template for Pylons), using the lessons learned from Gibe. It is going to be called Mazarine (after the Mazarine Blue butterfly).
Gibe now generally available
19 Apr
After much poking, cajoling, and downright finger pointing and laughing by Bryn and a bunch of other "friends", I now have Gibe, the little project saving my sanity from the tedium of burn-out and under-stimulation, in a publically accessible place for more people to do more finger pointing and laughing.
Gibe is just your standard web log software - people can log in and add posts, other people can read the posts and make comments on them, and there's anti-spam (using akismet) and there's also a bit of a beginnings of a plugin architecture there for people who want to expand it beyond what it does now.
Return visitors may notice a slight difference today. For the first time ever, I'm running a web site theme that someone else created. While it's obviously better than my attempts, that's not why it's here (and who knows how long it'll stay). I'm testing out the theme support in Gibe, my web log engine, which ended up not being particularly hard to do at all, thanks to the way TurboGears' expose decorator works, and a simple decorator of my own.
So far, I've been looking at modifying existing pages in Gibe (my still as-yet-unreleased TurboGears blog application) - adding widgets to post pages, dynamically adding the comment field and handling it for different comment formats, and adding additional fields at blog entry create/edit-time and handling these fields to add tagging (or whatever). Adding new pages (or replacing the default ones) is pretty much necessary - for example, to add a page where there is a list of all pages with a particular tag.
I use Routes for dispatching incoming URLs to functions in Gibe. It's not the default dispatcher in TurboGears, but it's pretty easy to set up (there's a TurboGears/Routes integration recipe on the TurboGears wiki).
Why go through the bother?
It makes adding new pages easy - no matter how complicated the URL structure is and where the dynamic portions are. It also makes it easy to pass through the dynamic portions, and also to pass through defaults if the dynamic portions don't exist. The killer function is named routes, which allows me to look up where something is (ie, generate the URL for it), and not hard-code the link to where the page is. That means that I can totally change the URL structure of the site without changing any code.
In terms of dynamicism, the worst cases I've explained so far in Gibe are the little comment format hack to allow the use of Postmarkup instead of HTML in comments, and the adding of little trivial plugins to add visual widgets at the top and bottom of blog entries. Certainly not rocket science. And, well, neither is this...
My next task was to look at how one could add additional fields to the blog entry create/edit screen - to allow plugins to ask for additional information like tags, geographical location, your mood, what you're currently listening to, and other vain things that nobody really cares about (I mean, it's a blog, it's not like it's useful...).
I used tags as my test case, since that is at least something I can see some value in, and it's something that's already around except for the actual entry of the tags. Until now, I've been manually typing in things like:
INSERT INTO post_tag
SELECT 625 AS post_id, tag_id FROM tag
WHERE name IN
('gibe', 'python', 'code','amatomu',
'blogs','me','tgsociable');
The last time I was talking code (I mean, besides the little example usage snippet when I was announcing TGOpenIDLogin, in between my reporting back about the GeekDinner, the Western Cape Linux User Group meeting, the Cape Town Python User Group meeting, and so forth), I was showing how I converted the Wordpress Sociable plugin to a TurboGears widget (innovatively named TGSociable).
The ultimate purpose of this was to make a plugin for Gibe (my little weblog engine) which would add the sociable icons with the correct URLs to blog entry pages, without having to put anything sociable-specific in the code.
When writing the comment format "plugin system" so that Gibe could use something other than the built-in TinyMCE, I used pkg_resources to create an "entry point" so that other packages could provide functionality to Gibe, and Gibe would know about them without anything but installing the package.
So, I started with the near-simplest possible Plugin class:
class Plugin(object):
def post_top_widgets(self, post, widgets, context):
# widgets to put above the post
pass
I've been trying to make Gibe, my not-yet-released web log software, less and less about my needs and more about being able to cope with others' needs. That means ripping out a lot of stuff that's custom to me, but somehow making it still available on my own web log. I've made some progress with that - I've made the comment format customisable, as well as found a solution to add custom scripts like Google Analytics and a syntax highlighter. It also means making it easy to add new stuff, which is what my experiment over the weekend was - to add a set of social bookmarking links to posts, so that people can post interesting links to places like Digg, Reddit, del.icio.us, and South Africa's Muti.
The sociable Wordpress plugin is one I've seen around, and I figured I might as well see how it was put together. At its core, it is a list of social bookmarking sites and how to post to them:
$sociable_known_sites = Array(
'blinkbits' => Array(
'favicon' => 'blinkbits.png',
'url' => 'http://www.blinkbits.com/bookmarklets/save.php?v=1&source_url=PERMALINK&title=TITLE&body=TITLE',
),
'BlinkList' => Array(
'favicon' => 'blinklist.png',
'url' => 'http://www.blinklist.com/index.php?Action=Blink/addblink.php&Url=PERMALINK&Title=TITLE',
'description' => 'Description',
),
'BlogMemes' => Array(
'favicon' => 'blogmemes.png',
'url' => 'http://www.blogmemes.net/post.php?url=PERMALINK&title=TITLE',
),
...
);
First order of business was to convert that into a Python data structure. Besides taking a detour via the itertools module just for fun, it didn't take too much effort:
#!/usr/bin/env python
import sys
import itertools
import re
if len(sys.argv) > 1:
fp = open(sys.argv[1])
else:
fp = sys.stdin
lines = fp.readlines()
beg = re.compile('^\$sociable_known_sites')
end = re.compile('^\);')
def find_beginning(item):
return not beg.match(item)
def find_end(item):
return not end.match(item)
def dropbefore(lines):
for line in itertools.dropwhile(find_beginning, lines):
yield line
def dropafter(lines):
for line in itertools.takewhile(find_end, lines):
yield line
start_item = re.compile(r"""^\t('[^']*') => Array\(""")
favicon_item = re.compile(r"""\t\t('favicon') => '([^']*)',""")
mid_item = re.compile(r"""\t\t('[^']*') => ('[^']*'),""")
end_item = re.compile(r"""\t\),""")
def start_item_handle(m):
print "\t%s: {" % (m.groups()[0],)
def favicon_item_handle(m):
k, v = m.groups()
print "\t\t%s: turbogears.url('/tg_widgets/tgsociable/images/%s')," % (k, v)
def mid_item_handle(m):
k, v = m.groups()
v = v.replace("&", "&")
print "\t\t%s: %s," % (k, v)
def end_item_handle(m):
print "\t},"
handlers = [
(start_item, start_item_handle),
(favicon_item, favicon_item_handle),
(mid_item, mid_item_handle),
(end_item, end_item_handle),
]
def handle_lines(lines):
for line in dropafter(dropbefore(lines)):
line = line.rstrip()
for regex, handler in handlers:
m = regex.search(line)
if m:
handler(m)
break
print "import turbogears"
print "all_sites = {"
handle_lines(lines)
print "}"
This outputs the expected Python code (using turbogears.url in a not-to-correct way...):
import turbogears
all_sites = {
'blinkbits': {
'favicon': turbogears.url('/tg_widgets/tgsociable/images/blinkbits.png'),
'url': 'http://www.blinkbits.com/bookmarklets/save.php?v=1&source_url=PERMALINK&title=TITLE&body=TITLE',
},
'BlinkList': {
'favicon': turbogears.url('/tg_widgets/tgsociable/images/blinklist.png'),
'url': 'http://www.blinklist.com/index.php?Action=Blink/addblink.php&Url=PERMALINK&Title=TITLE',
},
...
}
Next, displaying the actual HTML. At this point, the PHP is not conducive to programmatic Python conversion:
$html .= "\n<div class=\"sociable\">\n<span class=\"sociable_tagline\">\n";
$html .= get_option("sociable_tagline");
$html .= "\n\t<span>" . __("These icons link to social bookmarking sites where readers can share and discover new web pages.", 'sociable') . "</span>";
$html .= "\n</span>\n<ul>\n";
foreach($display as $sitename) {
// if they specify an unknown or inactive site, ignore it
if (!in_array($sitename, $active_sites))
continue;
$site = $sociable_known_sites[$sitename];
$html .= "\t<li>";
$url = $site['url'];
$url = str_replace('PERMALINK', $permalink, $url);
$url = str_replace('TITLE', $title, $url);
$url = str_replace('RSS', $rss, $url);
$url = str_replace('BLOGNAME', $blogname, $url);
$url = str_replace('VERSION', $sociable_version, $url);
$html .= "<a href=\"$url\" title=\"$sitename\"";
if ($site['description'])
$html .= " onfocus=\"sociable_description_link(this, '{$site['description']}')\"";
$html .= ">";
$html .= "<img src=\"$imagepath{$site['favicon']}\" title=\"$sitename\" alt=\"$sitename\" class=\"sociable-hovers";
if ($site['class'])
$html .= " sociable_{$site['class']}";
$html .= "\" />";
$html .= "</a></li>\n";
}
$html .= "</ul>\n</div>\n";
return $html;
This turns out a lot prettier thanks to the templating engine (Kid, in this case):
class SociableWidget(widgets.Widget):
template = """
<div class="sociable" xmlns:py="http://purl.org/kid/ns#">
<span class="sociable_tagline">
<strong py:content="sociable_tagline">get_option("sociable_tagline");</strong>
<span py:content="sociable_tagline_description">_("These icons link to social bookmarking sites where readers can share and discover new web pages.", 'sociable')</span>
</span>
<ul>
<li py:for="site in sites">
<a py:attrs="site['anchor_attrs']">
<img py:attrs="site['img_attrs']" />
</a>
</li>
</ul>
</div>
"""
There's a bit more work to make it automatically include the right CSS and Javascript, and to make it configurable (including allowing for extra sites to be added) from the caller of the widget:
css = [
widgets.CSSLink("tgsociable", "css/sociable.css"),
]
javascript = [
widgets.JSLink("tgsociable", "javascript/description_selection.js"),
]
params_doc = {
'active_sites' : 'Sites to display sociable icons for',
'sociable_tagline' : 'Tag line heading',
'sociable_tagline_description' : 'Tag line explanation',
'extra_sites' : 'Sites not in the existing sites list that you want to use',
}
params = params_doc.keys()
active_sites = ["Digg", "Reddit", "del.icio.us"]
sociable_tagline = "Share and Enjoy:"
sociable_tagline_description = "These icons link to social bookmarking sites where readers can share and discover new web pages."
extra_sites = {
# Example:
# 'muti': {
# 'favicon': 'http://muti.co.za/images/favicon.ico',
# 'url': 'http://muti.co.za/submit?url=PERMALINK&title=TITLE',
# },
}
def update_params(self, d):
super(SociableWidget, self).update_params(d)
active_sites = d['active_sites']
d['sites'] = []
my_all_sites = all_sites.copy()
my_all_sites.update(d['extra_sites'])
for sitename in active_sites:
if sitename not in my_all_sites:
continue
site = my_all_sites[sitename]
url = site['url'];
url = url.replace('PERMALINK', d['post_url'])
url = url.replace('TITLE', d['post_title'])
url = url.replace('RSS', d['blog_rss'])
url = url.replace('BLOGNAME', d['blog_name'])
sociable_version = "2.0"
url = url.replace('VERSION', sociable_version)
anchor_attrs = {}
anchor_attrs['href'] = url
if 'description' in site:
anchor_attrs['onfocus'] = "sociable_description_link(this, '%s')" % (site['description'],)
img_attrs = {}
img_attrs['src'] = site['favicon']
img_attrs['title'] = sitename
img_attrs['alt'] = sitename
img_attrs['class'] = "sociable_hovers"
if 'class' in site:
img_attrs['class'] += " " + site['class']
d['sites'].append(dict(anchor_attrs = anchor_attrs, img_attrs = img_attrs))
To use the widget is trivial. First, create the widget instance:
# No configuration - shows Digg, Reddit, and del.icio.us
widget = SociableWidget()
# Choose which sites to show:
widget = SociableWidget(active_sites = [#39;del.icio.us'])
# Add extra sites of your own
extra_sites = {
'muti': {
'favicon': 'http://muti.co.za/images/favicon.ico',
'url': 'http://muti.co.za/submit?url=PERMALINK&title=TITLE',
}
widget = SociableWidget(extra_sites = extra_sites, active_sites = ['muti', 'del.icio.us'])
You can then pass this widget into a template. In the template, you need to provide the post URL, post title, RSS feed, and blog name when you display the widget - something like:
<span py:replace="ET(widget.display(post_url=tg.base_url,
post_title=post.title, blog_name = blog.name,
blog_rss = tg.url_for('rss2.0.xml')))" />
At this point, this is just a plain TurboGears widget, that can be used in any TurboGears application (converting it to ToscaWidgets would be pretty trivial, and then it'll also be usable from Pylons and other platforms that are supported). Hooking it up so that it automatically gets displayed in Gibe posts took a bit more work, and I'll post about that later.
You can download the TGSociable widget on my TGSociable page. You can also follow other posts about TGSociable here - I'll use the tag tgsociable.
When I first started writing gibe, I wasn't too concerned about much beyond the adventure of writing my own software. In fact, I'm still not particularly concerned about more than that. But my initial three-second decision and implementation of TinyMCE for comments has caused at least as much trouble as I expected it to, and I've now crossed off the "make it so that different comment formats are supported" item in my checklist.
Gibe was always intended to have a plugin architecture. Plugins would add new URLs to the routes router, allowing me to add RSS, tags pages, and so forth, all without changing any core code. Plugins would register themselves as using pkg_resources. It would be a sunny, but cool, day at the beach with the dogs. And so forth.
But until that wonderful day (I'm hoping this weekend), I'll have to settle with the comment format problem.
Initially, the comment form was generated using TurboGears's wonderful widgets system (and will soon use its heir, ToscaWidgets):
class CommentFormFields(widgets.WidgetsList):
name = widgets.TextField(validator=validators.NotEmpty,
label = "Your name", attrs=dict(size=60))
email = widgets.TextField(
validator=validators.All(validators.Email, validators.NotEmpty),
label = "Email", attrs=dict(size=60))
website = widgets.TextField(validator=validators.URL,
label = "Site", attrs=dict(size=60))
comment = TinyMCE(validator=validators.NotEmpty, label = "Comment",
mce_options = dict(
mode = "exact",
theme = "advanced",
plugins = "fullscreen",
relative_urls = False,
theme_advanced_buttons2_add = "fullscreen",
extended_valid_elements = "a[href|target|name]",
paste_auto_cleanup_on_paste = True,
paste_convert_headers_to_strong = True,
paste_strip_class_attributes = "all",
theme_advanced_buttons3 = "",
remove_linebreaks = False,
browsers = "msie,gecko",
),
rows=15,
cols=60,
)
postid = widgets.HiddenField()
comment_form = widgets.TableForm(fields=CommentFormFields(), submit_text="Post")
The comments were displayed directly from the model from the templates, assuming HTML output:
<div class="blogcomment" py:for="comment in post.comments" py:if="comment.approved">
<div id="au${comment.comment_id}">
<p><span class="blog_comment_post_time"
py:content="comment.posted_time.strftime(str('%B %d, %y %X'))">September 20, 2006 22:00</span>
|
<span class="reference" py:content="tg.ET(comment.getReference())"><a href="http://www.greenman.co.za/b2evolution/blogs/">Ian</a></span></p>
<p py:content="HTML(comment.content)">Any idea what the other three were using?</p>
</div>
</div>
Next step - add a getContentHtml method to the model (could've been a property, I suppose. Can always change later...), and use that from the template. First, just the plain method for the current case:
def getContentHtml(self):
return self.content
Then, add a content_format column to my Comment model (I'm currently using ActiveMapper on SQLAlchemy, but I'll be moving to the Elixir declarative layer over SQLAlchemy soon), defaulting to html if no content_format is provided, so that a default conversion to HTML can be done for it:
class Comment(ActiveMapper):
class mapping:
comment_id = column(Integer, primary_key=True)
post_id = column(Integer, foreign_key=ForeignKey('post.post_id'))
blog_id = column(Integer, foreign_key=ForeignKey('blog.blog_id'))
author_id = column(Integer, foreign_key=ForeignKey('user.user_id'))
author_name = column(Unicode(255))
author_url = column(String(255))
author_email = column(String(255))
content = column(Unicode())
posted_time = column(DateTime)
approved = column(Boolean, default=False)
content_format = column(Unicode(25), default = "html")
I decided to use the dispatch module (sorry, that's the best link I could find) to decide how to convert from the comment's content in its format to HTML - it allowed me to easily provide alternate implementations in the same place before going fully into the plugin space:
@dispatch.generic()
def getContentHtml(self):
pass
@getContentHtml.when('self.content_format == "html"')
def getContentHtmlfromHtml(self):
return self.content
Everything still works, but there's no way to change the comment form field, and no way to specify the comment format and do anything necessary before writing it to the database. I now added an entry point, a means by which modules can advertise themselves (or their classes or functions) as available for a particular topic. In this case, a comment format "topic", which is basically a registry of comment formats and how to deal with them. First, edit setup.py to add an entry point for the current TinyMCE-based system:
entry_points = """
[gibe.comment_formats]
tinymce = gibe.tinymcesupport
postmarkup = gibe.postmarkupsupport
""",
I'm probably committing a major faux pas, but I read the entry points into a simple format registry dictionary:
format_registry = {}
for comment_format_mod in pkg_resources.iter_entry_points("gibe.comment_formats"):
mod = comment_format_mod.load()
format_registry[comment_format_mod.name] = mod
Now the comment form needs to be updated so that it can use whichever format it is configured to use in the configuration file. The comment form fields need to be modified by the comment format plugin, and a content_format field needs to be added to the comment form too (and should be validated to be a format we can handle). This turns out to be pretty easy:
def fields():
formats = []
formats_to_check = [
cherrypy.config.get('gibe.comment_format.preferred'),
cherrypy.config.get('gibe.comment_format.fallback', None),
['postmarkup', 'tinymce'],
]
for fs in formats_to_check:
if isinstance(fs, (str, unicode)):
fs = [fs]
for f in fs:
if f not in formats:
formats.append(f)
for format in formats:
if format in format_registry:
class CommentFormFields(widgets.WidgetsList):
postid = widgets.HiddenField()
content_format = widgets.HiddenField(default=format,
validator=validators.OneOf(format_registry.keys()))
name = widgets.TextField(validator=validators.NotEmpty,
label = "Your name", attrs=dict(size=60))
email = widgets.TextField(label = "Email",
validator=validators.All(validators.Email, validators.NotEmpty),
attrs=dict(size=60))
website = widgets.TextField(validator=validators.URL,
label = "Site", attrs=dict(size=60))
comment_form_fields = CommentFormFields()
format_registry[format].addCommentFields(comment_form_fields)
return comment_form_fields
comment_form = widgets.TableForm(fields=fields(), submit_text="Post")
On the TinyMCE format plugin side, the addCommentFields function adds the TinyMCE form field to the widgets list:
from tinymce import TinyMCE
from turbogears import widgets, validators
def addCommentFields(wl):
class CommentFormFieldsExtra(widgets.WidgetsList):
comment = TinyMCE(validator=validators.NotEmpty, label = "Comment",
mce_options = dict(
mode = "exact",
theme = "advanced",
plugins = "fullscreen",
relative_urls = False,
theme_advanced_buttons2_add = "fullscreen",
extended_valid_elements = "a[href|target|name]",
paste_auto_cleanup_on_paste = True,
paste_convert_headers_to_strong = True,
paste_strip_class_attributes = "all",
theme_advanced_buttons3 = "",
remove_linebreaks = False,
browsers = "msie,gecko",
),
rows=15,
cols=60,
)
wl.extend(CommentFormFieldsExtra())
Almost done with the core code - the add comment method needs to allow the comment format plugin to convert, sanitise, or reject the incoming comment. In the original, the Genshi HTMLSanitizer filter is used to sanitise the HTML. I decided that the plugin modules would have a class named Commenting available for doing conversion and rejection and possibly for post-save actions. (This also allows for modules to convert from a format to HTML, and then change the content_format variable to html, and not have to write their getContentHtml implementation.)
@error_handler(post)
@validate(form=comment_form)
def add_comment(self, blog, **kw):
post = Post.get_by(post_id = kw['postid'])
if not post.accept_comments:
flash("Post does not allow comments")
raise routes.redirect_to('posts', post = post)
content_format = kw['content_format']
commenting = format_registry[content_format].Commenting()
commenting.convert(kw)
ckw = {
'post_id': kw['postid'],
'blog_id': blog.blog_id,
'author_name': kw['name'],
'author_url': kw['website'],
'author_email': kw['email'],
'content': kw['comment'],
'approved': True,
'posted_time': datetime.now(),
'content_format': kw['content_format'],
}
c = Comment(**ckw)
commenting.post_save(kw)
self._check_for_spam(blog, c, kw)
raise routes.redirect_to('posts', post = post)
Implementing this again is quite trivial:
import commenting
from gibe.util import sanitise
class Commenting(commenting.Commenting):
def convert(self, kw):
kw['comment'] = sanitise(kw['comment']).decode('utf-8')
And that's about it for the core code, and recreating the TinyMCE support. Of course, the whole point was to offer other comment formats, and so I implemented the newly created postmarkup module which provides BBCode-like support for user-supplied data in a controlled fashion.
First, the postmarkup object needs to be created:
pm = postmarkup.PostMarkup().default_tags()
pm.add_tag('link', postmarkup.LinkTag, 'link')
pm.add_tag('quote', postmarkup.QuoteTag)
pm.add_tag('code', CodeTag)
Then, a function to convert from postmarkup to HTML:
from gibe.model import Comment
@Comment.getContentHtml.when('self.content_format == "postmarkup"')
def getContentHtmlPostMarkup(self):
return pm.render_to_html(self.content.encode('utf-8')).decode('utf-8')
An addCommentFields function:
def addCommentFields(wl):
class CommentFormFieldsExtra(widgets.WidgetsList):
comment = widgets.TextArea(label = "Your comment", rows=15, cols=45,
validator=validators.NotEmpty,
)
explanation = PostMarkupExplanation()
wl.extend(CommentFormFieldsExtra())
No conversion necessary to write to the database, so just an empty Commenting class:
class Commenting(commenting.Commenting):
def convert(self, kw):
pass
def post_save(self, kw):
pass
For bonus points, I added an explanation to the add comment form so that people know how to format their comments:
from turbogears import widgets, validators
class PostMarkupExplanation(widgets.FormField):
template = """
<div xmlns:py="http://purl.org/kid/ns#"
class="${field_class}"
id="${field_id}"
>
<p>The text area above accepts Post Markup, a BBCode work-alike.</p>
<pre>
[b]foo[/b]: <strong>foo</strong>
[i]foo[/i]: <em>foo</em>
[link]http://nxsy.org/[/link]: <a href="http://nxsy.org/">http://nxsy.org/</a> [nxsy.org]
[link http://nxsy.org/]Neil[/link]: <a href="http://nxsy.org/">Neil</a> [nxsy.org]
</pre>
<p>You can also use:</p>
<pre>
[code python]
import foo
[/code]
</pre>
</div>
"""
Add it to the entry_points in setup.py, and we're done.
As I've said before, one of the great things TurboGears has given us is "widgets", which bundle together the visual/behavioural/content aspects of a component in terms of Javascript, form fields, or other HTML, as well as the validation and conversion of whatever is entered into the browser into something useful to you as a programmer. Toscawidgets is the chosen heir of the "widgets" component in TurboGears, hopefully delivering the concept to the other Python frameworks.
This is devastatingly effective for form fields, and great for displaying interesting widgets on your screen, but you can also just use it to add simple things like a single CSS or Javascript file unrelated to specific widgets.
In TurboGears 1.0, there is a slot for CSS from widgets, and there are three slots for Javascript widgets - named "head", "bodytop", and "bodybottom". But, unfortunately, nothing as simple as, say, the link to my RSS for RSS auto-discovery in gibe, my web log engine (ToscaWidgets has genericised everything into resources that can be in those same locations). But we can just cheat by pretending to be CSS until I switch over to ToscaWidgets:
class FeedLink(widgets.Source):
template = """
<link rel="alternate" href="$src" title="$title" type="$type" />
"""
params = ['title', 'type']
params_doc = {
'src': 'The feed URL',
'title': 'Title for the feed',
'type': 'MIME type for the feed'
}
retrieve_css = widgets.set_with_self
class FeedLinks(widgets.Widget):
def retrieve_css(self):
return [
FeedLink(src=routes.url_for('rss2.0.xml'), title="RSS 2.0",
type="application/rss+xml"),
FeedLink(src=routes.url_for('atom1_0'), title="Atom 1.0",
type="application/atom+xml"),
]
feedlinks = FeedLinks()
The first (FeedLink) is a widget that describes a single auto-discovery (or other "alternate") link, based on URL, title, and type provided. As you can see, retrieve_css is being used - set_with_self does some magic and eventually the template given in the template attribute is rendered.
The second (FeedLinks) is a simple widget that builds up multiple FeedLink objects and returns them in a list.
While not strictly required, I create a FeedLinks object named feedlinks.
Behind the scenes, I add gibe.feeds.feedlinks to the tg.include_widgets list in my config/app.cfg, which ensures that this widget is available on every page.
The end result is kinda boring, actually:
<link href="/blog/rss2.0.xml" type="application/rss+xml"
rel="alternate" title="RSS 2.0">
<link href="/blog/atom1.0" type="application/atom+xml"
rel="alternate" title="Atom 1.0">
I did something similar to add Javascript-based syntax highlighting. TurboGears already comes with a dp.SyntaxHighlighter widget in turbogears.widgets.links, named (aptly) SyntaxHighlighter. Unfortunately, I don't like that dp.SyntaxHighlighter uses textareas as the base for source code. I'd rather use a <pre> and a class. So I had to change it. But if you're happy with the way it does things, it's really as simple as adding turbogears.widgets.SyntaxHighlighter to your tg.include_widgets in config/app.cfg, and any appropriately named and classed textarea will be automatically syntax highlighted.
Adding Google Analytics at the bottom of the page was just as simple. I created the following module:
class Analytics(widgets.Widget):
template = """
<div py:strip="True" xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/">
<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
</script>
<script type="text/javascript">
_uacct = "$analytics_key";
urchinTracker();
</script>
</div>
"""
params = ['analytics_key']
params_doc = {'analytics_key': 'Google Analytics Key'}
location = widgets.js_location.bodybottom
def retrieve_javascript(self):
if not self.analytics_key:
return []
else:
return widgets.set_with_self(self)
def add_for_location(self, location):
return location == self.location
analytics_key = cherrypy.config.get("analytics.key", None)
analytics_js = Analytics(analytics_key = analytics_key)
Add to tg.include_widgets in config/app.cfg, eventually coming out to:
tg.include_widgets = ['gibe.syntaxhighlighting.syntaxhighlighter','gibe.feeds.feedlinks','gibe.analytics.analytics_js']
The beautiful part of all of this is that I didn't need to change any existing code or any of my templates to add RSS auto-discovery, syntax highlighting, and Google Analytics. When I have a proper "plugin architecture" in gibe, managing tg.include_widgets manually won't be necessary either - just add the plugin and you have the functionality.
Over the weekend, the hard drive in the server most of my life is on died. Well, about 1% of the data on the system is readable. Amazingly, it's pretty much the most important 1%.
I have somewhat up-to-date backups (I was once a middling-good sysadmin, after all). Backups that I haven't restored from, mostly because I've come to realise that I just don't care about that content anymore. It's rather freeing, actually.
The hosting company I lease the server from replaced the drive and reinstalled on that drive (Goodbye Debian stale, Hello Ubuntu Dapper Drake with LTS).
The things that I thought I'd miss that I've found I haven't is my email. Oh, I'll grab a copy of my aliases file for the people I want to keep in contact with, but the exact emails aren't all that important. And, anyway, I'd already moved my mail life over to Google Mail (I'll write something up about Google Apps For Your Domain someday).
I recovered my 4.0.24 MySQL tables for gibe (the blogging engine for TurboGears I'm working on and this runs on), and for engal (the photo gallery for TurboGears I wrote). I installed 4.0.24 (not a simple task via package management with Dapper, so I resorted to a custom install), discovered only the "visit" tables were broken, exported, and installed on package-managed MySQL 5 (on my way to PostgreSQL, of course).
I recovered my SVN repository from the disk itself, which contains my web site and all the code for gibe and engal. The static resource content (mostly images of Dante linked to from my posts) was also all fine.
/etc was toast (Input/Output error if you tried to change working directory to it), which meant my exim and name server configuration and zones weren't recoverable from the drive. But since I'd moved to Google Mail, everything was simplified, and I added back the backup MXs I run for friends. The name server configuration was basically duplicated on another server, and that was about five minutes of work.
The backups are still useful - they're the "archive" of stuff I once thought was interesting. I'll eventually get it whittled down to what I care about, and get it down locally (which may not make sense if you don't realise how bad Internet connectivity in South Africa is), and archive it here.
And, of course, if I never had the backups, I would almost certainly have not been lucky enough to be able to recover without using them.
(And, of course, this post will almost certainly cause people to notice that everything isn't perfectly put back together - but then, I'm counting on that so that I can find out about it... )