diff --git a/planet/config.py b/planet/config.py index 77c4512..024541b 100644 --- a/planet/config.py +++ b/planet/config.py @@ -64,17 +64,19 @@ def __init__(): # planet wide options define_planet('name', "Unconfigured Planet") - define_planet('link', "Unconfigured Planet") + define_planet('link', '') define_planet('cache_directory', "cache") define_planet('log_level', "WARNING") define_planet('feed_timeout', 20) define_planet('date_format', "%B %d, %Y %I:%M %p") + define_planet('new_date_format', "%B %d, %Y") define_planet('generator', 'Venus') define_planet('generator_uri', 'http://intertwingly.net/code/venus/') define_planet('owner_name', 'Anonymous Coward') define_planet('owner_email', '') define_planet('output_theme', '') define_planet('output_dir', 'output') + define_planet('feed', None) define_planet_list('template_files') define_planet_list('bill_of_materials') @@ -160,8 +162,25 @@ def cache_lists_directory(): else: return os.path.join(cache_directory(), 'lists') -def feeds(): - """ list the feeds defined """ +def feed(): + if parser.has_option('Planet', 'feed'): + parser.get('Planet', 'feed') + elif link(): + for template_file in template_files: + name = os.path.splitext(os.path.basename(template_file))[0] + if name.find('atom')>=0 or name.find('rss')>=0: + return urlparse.urljoin(link(), name) + +def feedtype(): + if parser.has_option('Planet', 'feedtype'): + parser.get('Planet', 'feedtype') + elif feed() and feed().find('atom')>=0: + return 'atom' + elif feed() and feed().find('rss')>=0: + return 'rss' + +def subscriptions(): + """ list the feed subscriptions """ return filter(lambda feed: feed!='Planet' and feed not in template_files(), parser.sections()) diff --git a/planet/shell/tmpl.py b/planet/shell/tmpl.py index d36a113..b566f1c 100644 --- a/planet/shell/tmpl.py +++ b/planet/shell/tmpl.py @@ -1,5 +1,5 @@ from xml.sax.saxutils import escape -import sgmllib, time, os, sys +import sgmllib, time, os, sys, new, urlparse from planet import config, feedparser, htmltmpl class stripHtml(sgmllib.SGMLParser): @@ -54,6 +54,9 @@ def Plain(value): def PlanetDate(value): return time.strftime(config.date_format(), value) +def NewDate(value): + return time.strftime(config.new_date_format(), value) + def Rfc822(value): return time.strftime("%a, %d %b %Y %H:%M:%S +0000", value) @@ -64,7 +67,6 @@ def Rfc3399(value): Base = [ ['author', String, 'author'], ['author_name', String, 'author_detail', 'name'], - ['feed', String, 'links', {'rel':'self'}, 'href'], ['generator', String, 'generator'], ['id', String, 'id'], ['icon', String, 'icon'], @@ -76,9 +78,9 @@ Base = [ ['subtitle', String, 'subtitle_detail', 'value'], ['title', String, 'title_detail', 'value'], ['title_plain', Plain, 'title_detail', 'value'], + ['url', String, 'links', {'rel':'self'}, 'href'], ] -# ? new_date, new_channel Items = [ ['author', String, 'author'], ['author_email', String, 'author_detail', 'email'], @@ -95,6 +97,9 @@ Items = [ ['date_iso', Rfc3399, 'updated_parsed'], ['id', String, 'id'], ['link', String, 'links', {'rel': 'alternate'}, 'href'], + ['new_channel', String, 'id'], + ['new_date', NewDate, 'published_parsed'], + ['new_date', NewDate, 'updated_parsed'], ['rights', String, 'rights_detail', 'value'], ['title_language', String, 'title_detail', 'language'], ['title_plain', Plain, 'title_detail', 'value'], @@ -108,14 +113,6 @@ Items = [ ['published_iso', Rfc3399, 'published_parsed'], ] -Channels = [ - ['url', None], - ['link', None], - ['message', None], - ['title_plain', None], - ['name', None], -] - # Add additional rules for source information for rule in Base: Items.append(['channel_'+rule[0], rule[1], 'source'] + rule[2:]) @@ -161,13 +158,67 @@ def tmpl_mapper(source, rules): return output +def _end_planet_source(self): + self._end_source() + context = self._getContext() + if not context.has_key('sources'): context['sources'] = [] + context.sources.append(context.source) + del context['source'] + def template_info(source): """ get template information from a feedparser output """ + + # wire in support for planet:source, call feedparser, unplug planet:source + mixin=feedparser._FeedParserMixin + mixin._start_planet_source = mixin._start_source + mixin._end_planet_source = \ + new.instancemethod(_end_planet_source, None, mixin) data=feedparser.parse(source) + del mixin._start_planet_source + del mixin._end_planet_source + + # apply rules to convert feed parser output to htmltmpl input output = {'Channels': [], 'Items': []} - output['Channels'].append(tmpl_mapper(data.feed, Base)) + output.update(tmpl_mapper(data.feed, Base)) + sources = [(source.get('planet_name',None),source) + for source in data.feed.get('sources',[])] + sources.sort() + for name, feed in sources: + output['Channels'].append(tmpl_mapper(feed, Base)) for entry in data.entries: output['Items'].append(tmpl_mapper(entry, Items)) + + # feed level information + output['generator'] = config.generator_uri() + output['name'] = config.name() + output['link'] = config.link() + output['owner_name'] = config.owner_name() + output['owner_email'] = config.owner_email() + if config.feed(): + output['feed'] = config.feed() + output['feedtype'] = config.feed().find('rss')>=0 and 'rss' or 'atom' + + # date/time information + date = time.gmtime() + output['date'] = PlanetDate(date) + output['date_iso'] = Rfc3399(date) + output['date_822'] = Rfc822(date) + + # remove new_dates and new_channels that aren't "new" + date = channel = None + for item in output['Items']: + if item.has_key('new_date'): + if item['new_date'] == date: + del item['new_date'] + else: + date = item['new_date'] + + if item.has_key('new_channel'): + if item['new_channel'] == channel: + del item['new_channel'] + else: + channel = item['new_channel'] + return output def run(script, doc, output_file=None): @@ -177,6 +228,10 @@ def run(script, doc, output_file=None): tp = htmltmpl.TemplateProcessor(html_escape=0) for key,value in template_info(doc).items(): tp.set(key, value) + + reluri = os.path.splitext(os.path.basename(output_file))[0] + tp.set('url', urlparse.urljoin(config.link(),reluri)) + output = open(output_file, "w") output.write(tp.process(template)) output.close() diff --git a/planet/spider.py b/planet/spider.py index 7fb49bb..e39b241 100644 --- a/planet/spider.py +++ b/planet/spider.py @@ -147,5 +147,5 @@ def spiderPlanet(configFile): log = planet.getLogger(config.log_level()) planet.setTimeout(config.feed_timeout()) - for feed in config.feeds(): + for feed in config.subscriptions(): spiderFeed(feed) diff --git a/planet/splice.py b/planet/splice.py index f2ec5e8..9fa29b3 100644 --- a/planet/splice.py +++ b/planet/splice.py @@ -42,7 +42,7 @@ def splice(configFile): # insert subscription information feed.setAttribute('xmlns:planet',planet.xmlns) sources = config.cache_sources_directory() - for sub in config.feeds(): + for sub in config.subscriptions(): data=feedparser.parse(filename(sources,sub)) if not data.feed: continue xdoc=minidom.parseString(''' - foo foo + + foo + diff --git a/tests/data/filter/tmpl/source_author.xml b/tests/data/filter/tmpl/source_author.xml index 1cb6a62..93c2195 100644 --- a/tests/data/filter/tmpl/source_author.xml +++ b/tests/data/filter/tmpl/source_author.xml @@ -4,9 +4,6 @@ Expect: Channels[0]['author'] == 'John Doe' and Channels[0]['author_name'] --> - - John Doe - @@ -14,5 +11,10 @@ Expect: Channels[0]['author'] == 'John Doe' and Channels[0]['author_name'] + + + John Doe + + diff --git a/tests/data/filter/tmpl/source_icon.xml b/tests/data/filter/tmpl/source_icon.xml index ad49609..a25d133 100644 --- a/tests/data/filter/tmpl/source_icon.xml +++ b/tests/data/filter/tmpl/source_icon.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['icon'] == 'http://www.example.com/favicon.ico' and It --> - http://www.example.com/favicon.ico http://www.example.com/favicon.ico + + http://www.example.com/favicon.ico + diff --git a/tests/data/filter/tmpl/source_id.xml b/tests/data/filter/tmpl/source_id.xml index 751f8de..612819f 100644 --- a/tests/data/filter/tmpl/source_id.xml +++ b/tests/data/filter/tmpl/source_id.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['id'] == 'http://example.com/' and Items[0]['channel_i --> - http://example.com/ http://example.com/ + + http://example.com/ + diff --git a/tests/data/filter/tmpl/source_logo.xml b/tests/data/filter/tmpl/source_logo.xml index d56f44b..9e43049 100644 --- a/tests/data/filter/tmpl/source_logo.xml +++ b/tests/data/filter/tmpl/source_logo.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['logo'] == 'http://www.example.com/logo.jpg' and Items --> - http://www.example.com/logo.jpg http://www.example.com/logo.jpg + + http://www.example.com/logo.jpg + diff --git a/tests/data/filter/tmpl/source_planet_name.xml b/tests/data/filter/tmpl/source_planet_name.xml index e21b24c..3c5b070 100644 --- a/tests/data/filter/tmpl/source_planet_name.xml +++ b/tests/data/filter/tmpl/source_planet_name.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['name'] == 'foo' and Items[0]['channel_name'] == 'foo' --> - foo foo + + foo + diff --git a/tests/data/filter/tmpl/source_rights.xml b/tests/data/filter/tmpl/source_rights.xml index 0da67d9..fb96c14 100644 --- a/tests/data/filter/tmpl/source_rights.xml +++ b/tests/data/filter/tmpl/source_rights.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['rights'] == '© 2006' and Items[0]['channel_right --> - &copy; 2006 &copy; 2006 + + &copy; 2006 + diff --git a/tests/data/filter/tmpl/source_subtitle.xml b/tests/data/filter/tmpl/source_subtitle.xml index ace4902..7472e91 100644 --- a/tests/data/filter/tmpl/source_subtitle.xml +++ b/tests/data/filter/tmpl/source_subtitle.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['subtitle'] == 'snarky phrase' and Items[0]['channel_s --> - snarky phrase snarky phrase + + snarky phrase + diff --git a/tests/data/filter/tmpl/source_title.xml b/tests/data/filter/tmpl/source_title.xml index 2e5ebd3..be9afcd 100644 --- a/tests/data/filter/tmpl/source_title.xml +++ b/tests/data/filter/tmpl/source_title.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['title'] == 'visible name' and Channels[0]['title_plai --> - visible name visible name + + visible name + diff --git a/tests/data/filter/tmpl/source_updated.xml b/tests/data/filter/tmpl/source_updated.xml index 225b003..a2ca53f 100644 --- a/tests/data/filter/tmpl/source_updated.xml +++ b/tests/data/filter/tmpl/source_updated.xml @@ -4,11 +4,13 @@ Expect: Channels[0]['last_updated_iso'] == '2004-02-29T02:14:55+00:00' and --> - 2004-02-28T18:14:55-08:00 2004-02-28T18:14:55-08:00 + + 2004-02-28T18:14:55-08:00 + diff --git a/tests/test_config.py b/tests/test_config.py index 8773a46..14c0239 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -14,7 +14,7 @@ class ConfigTest(unittest.TestCase): config.template_files()) def test_feeds(self): - feeds = config.feeds() + feeds = config.subscriptions() feeds.sort() self.assertEqual(['feed1', 'feed2'], feeds) @@ -24,7 +24,7 @@ class ConfigTest(unittest.TestCase): self.assertEqual('Test Configuration', config.name()) def test_link(self): - self.assertEqual('Unconfigured Planet', config.link()) + self.assertEqual('', config.link()) # per template configuration diff --git a/tests/test_filter_tmpl.py b/tests/test_filter_tmpl.py index 26e090c..49d258b 100644 --- a/tests/test_filter_tmpl.py +++ b/tests/test_filter_tmpl.py @@ -1,20 +1,22 @@ #!/usr/bin/env python import unittest, os, sys, glob, new, re, StringIO, time +from planet import config from planet.shell import tmpl -testfiles = 'tests/data/filter/tmpl/%s.xml' +testfiles = 'tests/data/filter/tmpl/%s.%s' class FilterTmplTest(unittest.TestCase): - desc_re = re.compile("Description:\s*(.*?)\s*Expect:\s*(.*)\s*-->") + desc_feed_re = re.compile("Description:\s*(.*?)\s*Expect:\s*(.*)\s*-->") + desc_config_re = re.compile(";\s*Description:\s*(.*?)\s*;\s*Expect:\s*(.*)") simple_re = re.compile("^(\S+) == (u?'[^']*'|\([0-9, ]+\))$") - def eval(self, name): + def eval_feed(self, name): # read the test case try: - testcase = open(testfiles % name) + testcase = open(testfiles % (name,'xml')) data = testcase.read() - description, expect = self.desc_re.search(data).groups() + description, expect = self.desc_feed_re.search(data).groups() testcase.close() except: raise RuntimeError, "can't parse %s" % name @@ -29,9 +31,37 @@ class FilterTmplTest(unittest.TestCase): lhs, rhs = self.simple_re.match(expect).groups() self.assertEqual(eval(rhs), eval(lhs, results)) -# build a test method for each test file -for testcase in glob.glob(testfiles % '*'): + def eval_config(self, name): + # read the test case + try: + testcase = open(testfiles % (name,'ini')) + data = testcase.read() + description, expect = self.desc_config_re.search(data).groups() + testcase.close() + except: + raise RuntimeError, "can't parse %s" % name + + # map to template info + config.load(testfiles % (name,'ini')) + results = tmpl.template_info("") + + # verify the results + if not self.simple_re.match(expect): + self.assertTrue(eval(expect, results), expect) + else: + lhs, rhs = self.simple_re.match(expect).groups() + self.assertEqual(eval(rhs), eval(lhs, results)) + +# build a test method for each xml test file +for testcase in glob.glob(testfiles % ('*','xml')): root = os.path.splitext(os.path.basename(testcase))[0] - func = lambda self, name=root: self.eval(name) + func = lambda self, name=root: self.eval_feed(name) + method = new.instancemethod(func, None, FilterTmplTest) + setattr(FilterTmplTest, "test_" + root, method) + +# build a test method for each ini test file +for testcase in glob.glob(testfiles % ('*','ini')): + root = os.path.splitext(os.path.basename(testcase))[0] + func = lambda self, name=root: self.eval_config(name) method = new.instancemethod(func, None, FilterTmplTest) setattr(FilterTmplTest, "test_" + root, method) diff --git a/tests/test_rlists.py b/tests/test_rlists.py index 155b7df..612da9f 100644 --- a/tests/test_rlists.py +++ b/tests/test_rlists.py @@ -19,7 +19,7 @@ class ReadingListTest(unittest.TestCase): # administrivia def test_feeds(self): - feeds = [split(feed)[1] for feed in config.feeds()] + feeds = [split(feed)[1] for feed in config.subscriptions()] feeds.sort() self.assertEqual(['testfeed0.atom', 'testfeed1a.atom', 'testfeed2.atom', 'testfeed3.rss'], feeds) @@ -27,7 +27,7 @@ class ReadingListTest(unittest.TestCase): # dictionaries def test_feed_options(self): - feeds = dict([(split(feed)[1],feed) for feed in config.feeds()]) + feeds = dict([(split(feed)[1],feed) for feed in config.subscriptions()]) feed1 = feeds['testfeed1a.atom'] self.assertEqual('one', config.feed_options(feed1)['name']) diff --git a/tests/test_themes.py b/tests/test_themes.py index b92eb41..eede839 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -20,7 +20,7 @@ class ConfigTest(unittest.TestCase): self.assertTrue('index.html.xslt' in config.template_files()) def test_feeds(self): - feeds = config.feeds() + feeds = config.subscriptions() feeds.sort() self.assertEqual(['feed1', 'feed2'], feeds) @@ -30,7 +30,7 @@ class ConfigTest(unittest.TestCase): self.assertEqual('Test Configuration', config.name()) def test_link(self): - self.assertEqual('Unconfigured Planet', config.link()) + self.assertEqual('', config.link()) # per template configuration diff --git a/themes/asf/config.ini b/themes/asf/config.ini index 4a883e1..6e43cc4 100644 --- a/themes/asf/config.ini +++ b/themes/asf/config.ini @@ -1,10 +1,9 @@ -# This template is based on the one originally developed by Stefano Mazzocci +# This theme is based on the one originally developed by Stefano Mazzocci # for planetapache.org, and modified by Sam Ruby for planet.intertwingly.net [Planet] template_files: atom.xml.xslt - rss20.xml.tmpl foafroll.xml.xslt index.html.xslt opml.xml.xslt diff --git a/themes/classic_fancy/config.ini b/themes/classic_fancy/config.ini new file mode 100644 index 0000000..e3ac0c8 --- /dev/null +++ b/themes/classic_fancy/config.ini @@ -0,0 +1,20 @@ +# This theme is based on the one contained in Planet V2.0. It demonstrates +# that one can mix the use of htmltmpl and xslt templates. + +[Planet] +template_files: + atom.xml.xslt + foafroll.xml.xslt + index.html.tmpl + opml.xml.xslt + rss10.xml.tmpl + rss20.xml.tmpl + +template_directories: + ../common + +bill_of_materials: + planet.css + images/feed-icon-10x10.png + images/logo.png + images/planet.png diff --git a/themes/classic_fancy/index.html.tmpl b/themes/classic_fancy/index.html.tmpl new file mode 100644 index 0000000..3ade246 --- /dev/null +++ b/themes/classic_fancy/index.html.tmpl @@ -0,0 +1,126 @@ + + + +### Fancy Planet HTML template. +### +### When combined with the stylesheet and images in the output/ directory +### of the Planet source, this gives you a much prettier result than the +### default examples template and demonstrates how to use the config file +### to support things like faces +### +### For documentation on the more boring template elements, see +### examples/config.ini and examples/index.html.tmpl in the Planet source. + + +<TMPL_VAR name> + +"> + + +" title="" type="application/+xml"> + + + + +

+ + + + + +### End
+
+### End
+
+
+
+

+ + + + +### End
+
+
+
+ +### Planet provides template variables for *all* configuration options for +### the channel (and defaults), even if it doesn't know about them. We +### exploit this here to add hackergotchi faces to our channels. Planet +### doesn't know about the "face", "facewidth" and "faceheight" configuration +### variables, but makes them available to us anyway. + +

" title="">

+ +" width="" height="" alt=""> + + + + +
lang=""> + + lang="">"> + +
+
lang=""> + +
+ +### Planet also makes available all of the information from the feed +### that it can. Use the 'planet-cache' tool on the cache file for +### a particular feed to find out what additional keys it supports. +### Comment extra fields are 'author' and 'category' which we +### demonstrate below. + +

+">by at under +

+
+
+ + +### End
+
+### End
+
+
+ + + + + + + diff --git a/themes/classic_fancy/planet.css b/themes/classic_fancy/planet.css new file mode 100644 index 0000000..05653c0 --- /dev/null +++ b/themes/classic_fancy/planet.css @@ -0,0 +1,150 @@ +body { + border-right: 1px solid black; + margin-right: 200px; + + padding-left: 20px; + padding-right: 20px; +} + +h1 { + margin-top: 0px; + padding-top: 20px; + + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + letter-spacing: -2px; + text-transform: lowercase; + text-align: right; + + color: grey; +} + +.admin { + text-align: right; +} + +h2 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + color: #200080; + + margin-left: -20px; +} + +h3 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + + background-color: #a0c0ff; + border: 1px solid #5080b0; + + padding: 4px; +} + +h3 a { + text-decoration: none; + color: inherit; +} + +h4 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: bold; +} + +h4 a { + text-decoration: none; + color: inherit; +} + +img.face { + float: right; + margin-top: -3em; +} + +.entry { + margin-bottom: 2em; +} + +.entry .date { + font-family: "Bitstream Vera Sans", sans-serif; + color: grey; +} + +.entry .date a { + text-decoration: none; + color: inherit; +} + +.sidebar { + position: absolute; + top: 0px; + right: 0px; + width: 200px; + + margin-left: 0px; + margin-right: 0px; + padding-right: 0px; + + padding-top: 20px; + padding-left: 0px; + + font-family: "Bitstream Vera Sans", sans-serif; + font-size: 85%; +} + +.sidebar h2 { + font-size: 110%; + font-weight: bold; + color: black; + + padding-left: 5px; + margin-left: 0px; +} + +.sidebar ul { + padding-left: 1em; + margin-left: 0px; + + list-style-type: none; +} + +.sidebar ul li:hover { + color: grey; +} + +.sidebar ul li a { + text-decoration: none; +} + +.sidebar ul li a:hover { + text-decoration: underline; +} + +.sidebar ul li a img { + border: 0; +} + +.sidebar p { + border-top: 1px solid grey; + margin-top: 30px; + padding-top: 10px; + + padding-left: 5px; +} + +.sidebar .message { + cursor: help; + border-bottom: 1px dashed red; +} + +.sidebar a.message:hover { + cursor: help; + background-color: #ff0000; + color: #ffffff !important; + text-decoration: none !important; +} + +a:hover { + text-decoration: underline !important; + color: blue !important; +} diff --git a/themes/common/images/logo.png b/themes/common/images/logo.png new file mode 100644 index 0000000..f277bf9 Binary files /dev/null and b/themes/common/images/logo.png differ diff --git a/themes/common/rss10.xml.tmpl b/themes/common/rss10.xml.tmpl new file mode 100644 index 0000000..cdaaa79 --- /dev/null +++ b/themes/common/rss10.xml.tmpl @@ -0,0 +1,37 @@ + + +"> + <TMPL_VAR name ESCAPE="HTML"> + + - + + + + + " /> + + + + + + +"> + <TMPL_VAR channel_name ESCAPE="HTML"><TMPL_IF title>: <TMPL_VAR title_plain ESCAPE="HTML"></TMPL_IF> + + + + + + + + + + + +