diff --git a/README.md b/README.md index 3c5b9b5..cfe8064 100644 --- a/README.md +++ b/README.md @@ -38,21 +38,21 @@ The section names, like `blog` and `coffee`, are just used as internal identifie ### Options -|Option | Default | Description -|---------------------|------------|------------------------------------------------------------------------- -|source\_path | / | Where in the content directory to find items' parent source -|name | | Feed name: default is section name -|filename | feed.xml | Name of generated Atom feed file -|url\_path | | Feed's URL on your site: default is source's URL path plus the filename -|blog\_author\_field | author | Name of source's author field -|blog\_summary\_field | summary | Name of source's summary field -|items | None | A query expression: default is the source's children -|limit | 50 | How many recent items to include -|item\_title\_field | title | Name of items' title field -|item\_body\_field | body | Name of items' content body field -|item\_author\_field | author | Name of items' author field -|item\_date\_field | pub\_date | Name of items' publication date field -|item\_model | None | Name of items' model +| Option | Default | Description | +|-------------------|--------------------|-------------------------------------------------------------------------| +| source\_path | / | Where in the content directory to find items' parent source | +| name | | Feed name: default is section name | +| filename | feed.xml | Name of generated Atom feed file | +| url\_path | | Feed's URL on your site: default is source's URL path plus the filename | +| blog\_author | {{ this.author }} | Global blog author or blog editor | +| blog\_summary | {{ this.summary }} | Blog summary | +| items | None | A query expression: default is the source's children | +| limit | 50 | How many recent items to include | +| item\_title | {{ this.title }} | Blog post title | +| item\_body | {{ this.body }} | Blog post body | +| item\_author | {{ this.author }} | Blog post author | +| item\_date\_field | pub\_date | Name of items' publication date field | +| item\_model | None | Name of items' model | ### Customizing the plugin for your models @@ -73,8 +73,8 @@ Then add to atom.ini: ``` [main] -blog_author_field = writer -blog_summary_field = short_description +blog_author = {{ this.writer }} +blog_summary = {{ this.short_description }} ``` See [tests/demo-project/configs/atom.ini](https://github.com/ajdavis/lektor-atom/blob/master/tests/demo-project/configs/atom.ini) for a complete example. @@ -112,6 +112,38 @@ relevant to the current page. When the argument `for_page` is omitted, the function will enumerate all feeds defined in your project. +## Alternatives + +If your site is using Lektor’s alternative system, you can set +alternative-specific configuration values in your `configs/atom.ini`: + +``` +[blog] +name = My Blog +source_path = / +item_model = blog-post + +[blog.de] +name = Mein Blog +``` + +When lektor-atom is trying to retrieve a configuration value, it will first +look-up the config file section `[feed.ALT]`, where `ALT` is replaced by the +name of the alternative that is being generated. When such a value does not +exist, lektor-atom will get the value from the global section (`[feed]`), or, if +this does not succeed, lektor-atom will fall back on the hardcoded default. + +If you are using pybabel and have the Jinja i18n extension enabled, you can +alternatively localize your feeds by using `{% trans %}` blocks inside template +expressions in your `atom.ini`. To extract translation strings using babel, just +add the following to your `babel.cfg`: + +``` +[jinja2: site/configs/atom.ini] +encoding=utf-8 +silent=False +``` + # Changes 2016-06-02: Version 0.2. Python 3 compatibility (thanks to Dan Bauman), diff --git a/lektor_atom.py b/lektor_atom.py index 20f8eec..41c0745 100644 --- a/lektor_atom.py +++ b/lektor_atom.py @@ -8,7 +8,7 @@ import pkg_resources from lektor.build_programs import BuildProgram from lektor.db import F -from lektor.environment import Expression +from lektor.environment import Expression, FormatExpression from lektor.pluginsystem import Plugin from lektor.context import get_ctx, url_to from lektor.sourceobj import VirtualSourceObject @@ -37,21 +37,28 @@ def path(self): @property def url_path(self): - p = self.plugin.get_atom_config(self.feed_id, 'url_path') + p = self.plugin.get_atom_config(self.feed_id, 'url_path', + alt=self.alt) if p: + cfg = self.plugin.env.load_config() + primary_alts = '_primary', cfg.primary_alternative + if self.alt not in primary_alts: + p = "/%s%s" % (self.alt, p) return p return build_url([self.parent.url_path, self.filename]) def __getattr__(self, item): try: - return self.plugin.get_atom_config(self.feed_id, item) + return self.plugin.get_atom_config(self.feed_id, item, + alt=self.alt) except KeyError: raise AttributeError(item) @property def feed_name(self): - return self.plugin.get_atom_config(self.feed_id, 'name') or self.feed_id + return self.plugin.get_atom_config(self.feed_id, 'name', alt=self.alt) \ + or self.feed_id def get(item, field, default=None): @@ -71,13 +78,6 @@ def get_item_title(item, field): return item.record_label -def get_item_body(item, field): - if field not in item: - raise RuntimeError('Body field %r not found in %r' % (field, item)) - with get_ctx().changed_base_url(item.url_path): - return text_type(escape(item[field])) - - def get_item_updated(item, field): if field in item: rv = item[field] @@ -89,6 +89,14 @@ def get_item_updated(item, field): class AtomFeedBuilderProgram(BuildProgram): + def format_expression(self, expression, record, env): + with get_ctx().changed_base_url(record.url_path): + return FormatExpression(env, expression).evaluate( + record.pad, + this=record, + alt=record.alt + ) + def produce_artifacts(self): self.declare_artifact( self.source.url_path, @@ -99,13 +107,17 @@ def build_artifact(self, artifact): feed_source = self.source blog = feed_source.parent - summary = get(blog, feed_source.blog_summary_field) or '' - if hasattr(summary, '__html__'): - subtitle_type = 'html' - summary = text_type(summary.__html__()) - else: - subtitle_type = 'text' - blog_author = text_type(get(blog, feed_source.blog_author_field) or '') + summary = self.format_expression( + feed_source.blog_summary, + blog, + ctx.env) + + blog_author = self.format_expression( + feed_source.blog_author, + blog, + ctx.env + ) + generator = ('Lektor Atom Plugin', 'https://github.com/ajdavis/lektor-atom', pkg_resources.get_distribution('lektor-atom').version) @@ -113,10 +125,10 @@ def build_artifact(self, artifact): feed = AtomFeed( title=feed_source.feed_name, subtitle=summary, - subtitle_type=subtitle_type, + subtitle_type='html', author=blog_author, - feed_url=url_to(feed_source, external=True), - url=url_to(blog, external=True), + feed_url=url_to(feed_source, external=True, alt=feed_source.alt), + url=url_to(blog, external=True, alt=feed_source.alt), id=get_id(ctx.env.project.id), generator=generator) @@ -127,6 +139,10 @@ def build_artifact(self, artifact): else: items = blog.children + # Don’t force the user to think about alt when specifying an items + # query. + items.alt = feed_source.alt + if feed_source.item_model: items = items.filter(F._model == feed_source.item_model) @@ -135,17 +151,29 @@ def build_artifact(self, artifact): for item in items: try: - item_author_field = feed_source.item_author_field - item_author = get(item, item_author_field) or blog_author + item_author = self.format_expression( + feed_source.item_author, + item, + ctx.env + ) or blog_author base_url = url_to( item.parent if item.is_attachment else item, external=True ) - + body = self.format_expression( + feed_source.item_body, + item, + ctx.env + ) + title = self.format_expression( + feed_source.item_title, + item, + ctx.env + ) feed.add( - get_item_title(item, feed_source.item_title_field), - get_item_body(item, feed_source.item_body_field), + title, + body, xml_base=base_url, url=url_to(item, external=True), content_type='html', @@ -171,20 +199,30 @@ class AtomPlugin(Plugin): 'name': None, 'url_path': None, 'filename': 'feed.xml', - 'blog_author_field': 'author', - 'blog_summary_field': 'summary', + 'blog_author': '{{ this.author }}', + 'blog_summary': '{{ this.summary }}', 'items': None, 'limit': 50, - 'item_title_field': 'title', - 'item_body_field': 'body', - 'item_author_field': 'author', + 'item_title': '{{ this.title or this.record_label }}', + 'item_body': '{{ this.body }}', + 'item_author': '{{ this.author }}', 'item_date_field': 'pub_date', 'item_model': None, } - def get_atom_config(self, feed_id, key): + def get_atom_config(self, feed_id, key, alt=None): default_value = self.defaults[key] - return self.get_config().get('%s.%s' % (feed_id, key), default_value) + config = self.get_config() + primary_value = config.get( + "%s.%s" % (feed_id, key), + default_value + ) + localized_value = ( + config.get("%s.%s.%s" % (feed_id, alt, key)) + if alt + else None + ) + return localized_value or primary_value def on_setup_env(self, **extra): self.env.add_build_program(AtomFeedSource, AtomFeedBuilderProgram) @@ -199,46 +237,64 @@ def feed_path_resolver(node, pieces): _id = pieces[0] - config = self.get_config() - if _id not in config.sections(): + if _id not in self._feed_ids(): return - source_path = self.get_atom_config(_id, 'source_path') + source_path = self.get_atom_config(_id, 'source_path', + alt=node.alt) if node.path == source_path: return AtomFeedSource(node, _id, plugin=self) @self.env.generator def generate_feeds(source): - for _id in self.get_config().sections(): - if source.path == self.get_atom_config(_id, 'source_path'): + for _id in self._feed_ids(): + if source.path == self.get_atom_config(_id, 'source_path', + alt=source.alt): yield AtomFeedSource(source, _id, self) - def _all_feeds(self): + def _feed_ids(self): + feed_ids = set() + for section in self.get_config().sections(): + if '.' in section: + feed_id, _alt = section.split(".") + else: + feed_id = section + feed_ids.add(feed_id) + + return feed_ids + + def _all_feeds(self, alt=None): ctx = get_ctx() feeds = [] - for feed_id in self.get_config().sections(): - path = self.get_atom_config(feed_id, 'source_path') - feed = ctx.pad.get('%s@atom/%s' % (path, feed_id)) + for feed_id in self._feed_ids(): + path = self.get_atom_config(feed_id, 'source_path', alt=alt) + feed = ctx.pad.get( + '%s@atom/%s' % (path, feed_id), + alt=alt or ctx.record.alt + ) if feed: feeds.append(feed) return feeds - def _feeds_for(self, page): + def _feeds_for(self, page, alt=None): ctx = get_ctx() record = page.record feeds = [] - for section in self.get_config().sections(): - feed = ctx.pad.get('%s@atom/%s' % (record.path, section)) + for section in self._feed_ids(): + feed = ctx.pad.get( + '%s@atom/%s' % (record.path, section), + alt=alt or ctx.record.alt + ) if feed: feeds.append(feed) return feeds - def atom_feeds(self, for_page=None): + def atom_feeds(self, for_page=None, alt=None): if not for_page: - return self._all_feeds() + return self._all_feeds(alt=alt) else: - return self._feeds_for(for_page) + return self._feeds_for(for_page, alt=alt) diff --git a/tests/demo-project/Website.lektorproject b/tests/demo-project/Website.lektorproject index 2744af8..441693b 100644 --- a/tests/demo-project/Website.lektorproject +++ b/tests/demo-project/Website.lektorproject @@ -4,3 +4,13 @@ url = http://x.com [packages] lektor-atom + +[alternatives.en] +name = Elvish +primary = yes +locale = en_US + +[alternatives.de] +name = Dwarvish +locale = de_DE +url_prefix = /de/ diff --git a/tests/demo-project/configs/atom.ini b/tests/demo-project/configs/atom.ini index 5d596c2..2efe1d2 100644 --- a/tests/demo-project/configs/atom.ini +++ b/tests/demo-project/configs/atom.ini @@ -9,12 +9,12 @@ source_path = /typical-blog2 name = Feed Three source_path = /custom-blog filename = atom.xml -blog_author_field = editor -blog_summary_field = description +blog_author = {{ this.editor }} +blog_summary = {{ this.description }} items = site.query('/custom-blog').filter(F.headline != "I'm filtered out") -item_title_field = headline -item_body_field = contents -item_author_field = writer +item_title = {{ this.headline }} +item_body = {{ this.content }} +item_author = {{ this.writer }} item_date_field = published item_model = custom-blog-post @@ -22,8 +22,16 @@ item_model = custom-blog-post name = Feed Three (uncensored) source_path = /custom-blog filename = nsfw.xml -item_title_field = headline -item_body_field = contents -item_author_field = writer +item_title = {{ this.headline }} +item_body = {{ this.content }} +item_author = {{ this.writer }} item_date_field = published item_model = custom-blog-post + +[feed-five] +name = Feed Five +source_path = /multilang-blog +item_title = {{ this.title }} ({{ this.pub_date | dateformat }}) + +[feed-five.de] +name = Feed Fünf diff --git a/tests/demo-project/content/custom-blog/filtered_post/contents.lr b/tests/demo-project/content/custom-blog/filtered_post/contents.lr index 6cc88df..85b9cc1 100644 --- a/tests/demo-project/content/custom-blog/filtered_post/contents.lr +++ b/tests/demo-project/content/custom-blog/filtered_post/contents.lr @@ -2,4 +2,4 @@ headline: I'm filtered out --- published: 2015-12-12 15:00:00 --- -contents: baz +content: baz diff --git a/tests/demo-project/content/custom-blog/post1/contents.lr b/tests/demo-project/content/custom-blog/post1/contents.lr index 63e0ff0..93464bb 100644 --- a/tests/demo-project/content/custom-blog/post1/contents.lr +++ b/tests/demo-project/content/custom-blog/post1/contents.lr @@ -2,4 +2,4 @@ headline: Post 1 --- published: 2015-12-12 12:34:56 --- -contents: foo +content: foo diff --git a/tests/demo-project/content/custom-blog/post2/contents.lr b/tests/demo-project/content/custom-blog/post2/contents.lr index ebdda08..1a4215b 100644 --- a/tests/demo-project/content/custom-blog/post2/contents.lr +++ b/tests/demo-project/content/custom-blog/post2/contents.lr @@ -4,4 +4,4 @@ writer: Armin Ronacher --- published: 2015-12-13 00:00:00 --- -contents: bar +content: bar diff --git a/tests/demo-project/content/multilang-blog/contents.lr b/tests/demo-project/content/multilang-blog/contents.lr new file mode 100644 index 0000000..2f4e07d --- /dev/null +++ b/tests/demo-project/content/multilang-blog/contents.lr @@ -0,0 +1,6 @@ +_model: blog +--- +author: Guy de Maupassant +--- +summary: High-impact multilingual blog +--- diff --git a/tests/demo-project/content/multilang-blog/post1/contents+de.lr b/tests/demo-project/content/multilang-blog/post1/contents+de.lr new file mode 100644 index 0000000..ba35e97 --- /dev/null +++ b/tests/demo-project/content/multilang-blog/post1/contents+de.lr @@ -0,0 +1,5 @@ +title: Post 1 +--- +pub_date: 2015-12-12 +--- +body: Achtung! diff --git a/tests/demo-project/content/multilang-blog/post1/contents.lr b/tests/demo-project/content/multilang-blog/post1/contents.lr new file mode 100644 index 0000000..b76e7f8 --- /dev/null +++ b/tests/demo-project/content/multilang-blog/post1/contents.lr @@ -0,0 +1,5 @@ +title: Post 1 +--- +pub_date: 2015-12-12 +--- +body: foo diff --git a/tests/demo-project/content/multilang-blog/post2/contents.lr b/tests/demo-project/content/multilang-blog/post2/contents.lr new file mode 100644 index 0000000..5e6a905 --- /dev/null +++ b/tests/demo-project/content/multilang-blog/post2/contents.lr @@ -0,0 +1,7 @@ +title: Post 2 +--- +author: Armin Ronacher +--- +pub_date: 2015-12-13 +--- +body: bar diff --git a/tests/demo-project/models/custom-blog-post.ini b/tests/demo-project/models/custom-blog-post.ini index 8beae0b..a66a692 100644 --- a/tests/demo-project/models/custom-blog-post.ini +++ b/tests/demo-project/models/custom-blog-post.ini @@ -10,5 +10,5 @@ type = string [fields.published] type = datetime -[fields.contents] +[fields.content] type = markdown diff --git a/tests/test_lektor_atom.py b/tests/test_lektor_atom.py index b6f65cb..00ffbff 100644 --- a/tests/test_lektor_atom.py +++ b/tests/test_lektor_atom.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import os from lektor.context import Context @@ -12,7 +14,7 @@ def test_typical_feed(pad, builder): assert 'Feed One' == feed.title assert 'My Summary' == feed.subtitle - assert 'text' == feed.subtitle.attrib['type'] + assert 'html' == feed.subtitle.attrib['type'] assert 'A. Jesse Jiryu Davis' == feed.author.name assert 'http://x.com/typical-blog/' == feed.link[0].attrib['href'] assert 'http://x.com/typical-blog/feed.xml' == feed.link[1].attrib['href'] @@ -78,6 +80,39 @@ def test_custom_feed(pad, builder): assert 'A. Jesse Jiryu Davis' == post1.author.name +def test_multilang_feed(pad, builder): + failures = builder.build_all() + assert not failures + + feed_path = os.path.join(builder.destination_path, + 'de/multilang-blog/feed.xml') + feed = objectify.parse(open(feed_path)).getroot() + + assert u'Feed Fünf' == feed.title + assert 'http://x.com/de/multilang-blog/' \ + == feed.link[0].attrib['href'] + assert 'http://x.com/de/multilang-blog/feed.xml' \ + == feed.link[1].attrib['href'] + assert feed.entry.title == 'Post 2 (13.12.2015)' + + base = feed.entry.attrib['{http://www.w3.org/XML/1998/namespace}base'] + assert 'http://x.com/de/multilang-blog/post2/' == base + + feed_path = os.path.join(builder.destination_path, + 'multilang-blog/feed.xml') + feed = objectify.parse(open(feed_path)).getroot() + + assert 'Feed Five' == feed.title + assert 'http://x.com/multilang-blog/' \ + == feed.link[0].attrib['href'] + assert 'http://x.com/multilang-blog/feed.xml' \ + == feed.link[1].attrib['href'] + assert feed.entry.title == 'Post 2 (Dec 13, 2015)' + + base = feed.entry.attrib['{http://www.w3.org/XML/1998/namespace}base'] + assert 'http://x.com/multilang-blog/post2/' == base + + def test_virtual_resolver(pad, builder): # Pass a virtual source path to url_to(). feed_path = '/typical-blog@atom/feed-one' @@ -98,6 +133,10 @@ def test_virtual_resolver(pad, builder): url_path = pad.get('custom-blog/post1').url_to(feed_instance) assert url_path == '../../custom-blog/atom.xml' + feed_instance = pad.get('multilang-blog@atom/feed-five', alt='de') + assert feed_instance and feed_instance.feed_name == u'Feed Fünf' + assert feed_instance.url_path == '/de/multilang-blog/feed.xml' + def test_dependencies(pad, builder, reporter): reporter.clear() @@ -126,19 +165,20 @@ def feeds_from_template(pad, template): def test_discover_all(pad): template = r''' - {% for feed in atom_feeds() %} + {% for feed in atom_feeds(alt='_primary') %} {{ feed.feed_id }} {% endfor %} ''' all_feeds = set(['feed-one', 'feed-two', - 'feed-three', 'feed-four']) + 'feed-three', 'feed-four', + 'feed-five']) feeds_discovered = feeds_from_template(pad, template) assert feeds_discovered == all_feeds def test_discover_local(pad): template_blog = r''' - {% for feed in atom_feeds(for_page=site.get('/custom-blog')) %} + {% for feed in atom_feeds(for_page=site.get('/custom-blog'), alt='_primary') %} {{ feed.feed_id }} {% endfor %} ''' @@ -146,9 +186,18 @@ def test_discover_local(pad): assert feeds_blog == set(['feed-three', 'feed-four']) template_noblog = r''' - {% for feed in atom_feeds(for_page=site.get('/no-feed-content')) %} + {% for feed in atom_feeds(for_page=site.get('/no-feed-content'), alt='_primary') %} {{ feed.feed_id }} {% endfor %} ''' feeds_noblog = feeds_from_template(pad, template_noblog) assert len(feeds_noblog) == 0 + + +def test_localized_config(pad): + plugin = pad.env.plugins['atom'] + assert plugin.get_atom_config('feed-five', 'name') \ + == 'Feed Five' + assert plugin.get_atom_config('feed-five', 'name', alt='de') \ + == u'Feed Fünf' +