diff --git a/README.md b/README.md index d39cd30..ec8f563 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ The section names, like `blog` and `coffee`, are just used as internal identifie |name | config section name | Feed name |filename | feed.xml | Name of generated Atom feed file |url\_path | source_path + filename | Feed's URL on your site -|blog\_author\_field | author | Name of source's author field -|blog\_summary\_field | summary | Name of source's summary field +|blog\_author | {{ this.author }} | Global blog author or blog editor +|blog\_summary | {{ this.summary }} | Blog summary |items | source_path's children | A query expression: e.g. `site.query('/').filter(F.type == 'post')` |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\_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 | Filters `items` on name of items' model @@ -86,8 +86,8 @@ Then add to atom.ini: ```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. @@ -125,6 +125,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 5c97e72..ee42fec 100644 --- a/lektor_atom.py +++ b/lektor_atom.py @@ -11,7 +11,7 @@ from lektor.context import get_ctx from lektor.context import url_to from lektor.db import F -from lektor.environment import Expression +from lektor.environment import Expression, FormatExpression from lektor.pluginsystem import Plugin from lektor.sourceobj import VirtualSourceObject from lektor.utils import build_url @@ -38,8 +38,13 @@ 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]) @@ -49,13 +54,15 @@ def iter_source_filenames(self): 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): @@ -75,13 +82,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] @@ -93,6 +93,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, sources=list(self.source.iter_source_filenames()) @@ -103,20 +111,24 @@ 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 + ) feed = Atom1Feed( title=feed_source.feed_name, subtitle=summary, author_name=blog_author, - feed_url=url_to(feed_source, external=True), - link=url_to(blog, external=True), + feed_url=url_to(feed_source, external=True, alt=feed_source.alt), + link=url_to(blog, external=True, alt=feed_source.alt), feed_guid=get_id(ctx.env.project.id), description=None, ) @@ -128,6 +140,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) @@ -136,13 +152,26 @@ 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 + + body = self.format_expression( + feed_source.item_body, + item, + ctx.env + ) + title = self.format_expression( + feed_source.item_title, + item, + ctx.env + ) feed.add_item( - title=get_item_title(item, feed_source.item_title_field), + title=title, description=None, - content=get_item_body(item, feed_source.item_body_field), + content=body, link=url_to(item, external=True), unique_id=get_id( u"%s/%s" % (ctx.env.project.id, item["_path"].encode("utf-8")) @@ -168,20 +197,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) @@ -196,46 +235,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 ce5fb8e..79982e8 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 @@ -93,6 +95,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" @@ -113,6 +148,10 @@ def test_virtual_resolver(pad, builder): url_path = pad.get("custom-blog/post1").url_to(feed_instance) assert urljoin("custom-blog/post1/", 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() @@ -142,19 +181,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 %} ''' @@ -162,9 +202,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' +