From 2f08932a769cfd077ff0fdbfb21a56d7a593fe44 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Reu=C3=9Fe?= <seb@wirrsal.net>
Date: Sun, 15 Jul 2018 19:46:01 +0200
Subject: [PATCH 1/4] Support feed discovery and enumeration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds support for a Jinja function/filter, “atom_feeds”, which can be used
to enumerate all feeds defined in the project (“atom_feeds()”), or those
relevant to the page being generated (“atom_feeds(for_page=this)”).

This is convenient, e.g., to define a generic site header which automatically
displays a “subscribe” link on those pages which generate feeds.
---
 README.md                                     | 12 ++++++
 lektor_atom.py                                | 33 ++++++++++++++++
 tests/demo-project/configs/atom.ini           | 10 +++++
 .../content/no-feed-content/contents.lr       |  3 ++
 tests/test_lektor_atom.py                     | 39 +++++++++++++++++++
 5 files changed, 97 insertions(+)
 create mode 100644 tests/demo-project/content/no-feed-content/contents.lr

diff --git a/README.md b/README.md
index edce238..3c5b9b5 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,18 @@ Link to the feed in a template like this:
 {{ 'blog@atom/main'|url }}
 ```
 
+The plugin also defines a function to enumerate all feeds or a subset of feeds
+relevant to the current page.
+
+```
+{% for feed in atom_feeds(for_page=this) %}
+    {{ feed | url }}
+{% endfor %}
+```
+
+When the argument `for_page` is omitted, the function will enumerate all feeds
+defined in your project.
+
 # 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 de80ec6..1400515 100644
--- a/lektor_atom.py
+++ b/lektor_atom.py
@@ -184,6 +184,9 @@ def get_atom_config(self, feed_id, key):
     def on_setup_env(self, **extra):
         self.env.add_build_program(AtomFeedSource, AtomFeedBuilderProgram)
 
+        self.env.jinja_env.filters['atom_feeds'] = self.atom_feeds
+        self.env.jinja_env.globals['atom_feeds'] = self.atom_feeds
+
         @self.env.virtualpathresolver('atom')
         def feed_path_resolver(node, pieces):
             if len(pieces) != 1:
@@ -204,3 +207,33 @@ def generate_feeds(source):
             for _id in self.get_config().sections():
                 if source.path == self.get_atom_config(_id, 'source_path'):
                     yield AtomFeedSource(source, _id, self)
+
+    def _all_feeds(self):
+        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))
+            if feed:
+                feeds.append(feed)
+
+        return feeds
+
+    def _feeds_for(self, page):
+        ctx = get_ctx()
+        record = page.record
+
+        feeds = []
+        for section in self.get_config().sections():
+            feed = ctx.pad.get('%s@atom/%s' % (record.path, section))
+            if feed:
+                feeds.append(feed)
+
+        return feeds
+
+    def atom_feeds(self, for_page=None):
+        if not for_page:
+            return self._all_feeds()
+        else:
+            return self._feeds_for(for_page)
diff --git a/tests/demo-project/configs/atom.ini b/tests/demo-project/configs/atom.ini
index 420cdbc..5d596c2 100644
--- a/tests/demo-project/configs/atom.ini
+++ b/tests/demo-project/configs/atom.ini
@@ -17,3 +17,13 @@ item_body_field = contents
 item_author_field = writer
 item_date_field = published
 item_model = custom-blog-post
+
+[feed-four]
+name = Feed Three (uncensored)
+source_path = /custom-blog
+filename = nsfw.xml
+item_title_field = headline
+item_body_field = contents
+item_author_field = writer
+item_date_field = published
+item_model = custom-blog-post
diff --git a/tests/demo-project/content/no-feed-content/contents.lr b/tests/demo-project/content/no-feed-content/contents.lr
new file mode 100644
index 0000000..731412a
--- /dev/null
+++ b/tests/demo-project/content/no-feed-content/contents.lr
@@ -0,0 +1,3 @@
+_model: page
+---
+contents: Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo (cf. Wikipedia)
diff --git a/tests/test_lektor_atom.py b/tests/test_lektor_atom.py
index c163f25..b6f65cb 100644
--- a/tests/test_lektor_atom.py
+++ b/tests/test_lektor_atom.py
@@ -1,5 +1,6 @@
 import os
 
+from lektor.context import Context
 from lxml import objectify
 
 
@@ -113,3 +114,41 @@ def test_dependencies(pad, builder, reporter):
         'configs/atom.ini',
     ])
 
+
+def feeds_from_template(pad, template):
+    with Context(pad=pad):
+        return set(
+            pad.env.jinja_env.from_string(template)
+                             .render()
+                             .split()
+        )
+
+
+def test_discover_all(pad):
+    template = r'''
+    {% for feed in atom_feeds() %}
+        {{ feed.feed_id }}
+    {% endfor %}
+    '''
+    all_feeds = set(['feed-one', 'feed-two',
+                     'feed-three', 'feed-four'])
+    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')) %}
+        {{ feed.feed_id }}
+    {% endfor %}
+    '''
+    feeds_blog = feeds_from_template(pad, template_blog)
+    assert feeds_blog == set(['feed-three', 'feed-four'])
+
+    template_noblog = r'''
+    {% for feed in atom_feeds(for_page=site.get('/no-feed-content')) %}
+        {{ feed.feed_id }}
+    {% endfor %}
+    '''
+    feeds_noblog = feeds_from_template(pad, template_noblog)
+    assert len(feeds_noblog) == 0

From b09d8f9a6ec128b903ff7d56da5a90a69818e669 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Reu=C3=9Fe?= <seb@wirrsal.net>
Date: Wed, 8 Aug 2018 17:23:26 +0200
Subject: [PATCH 2/4] Fix: use correct XML base URL in feeds built from page
 attachments

---
 lektor_atom.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/lektor_atom.py b/lektor_atom.py
index 1400515..20f8eec 100644
--- a/lektor_atom.py
+++ b/lektor_atom.py
@@ -138,10 +138,15 @@ def build_artifact(self, artifact):
                 item_author_field = feed_source.item_author_field
                 item_author = get(item, item_author_field) or blog_author
 
+                base_url = url_to(
+                    item.parent if item.is_attachment else item,
+                    external=True
+                )
+
                 feed.add(
                     get_item_title(item, feed_source.item_title_field),
                     get_item_body(item, feed_source.item_body_field),
-                    xml_base=url_to(item, external=True),
+                    xml_base=base_url,
                     url=url_to(item, external=True),
                     content_type='html',
                     id=get_id(u'%s/%s' % (

From ad6502f5fe19662ff0ac498337cf3a5ce6b29776 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Reu=C3=9Fe?= <seb@wirrsal.net>
Date: Wed, 8 Aug 2018 19:15:20 +0200
Subject: [PATCH 3/4] Add alternatives support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

While lektor-atom currently supports creating feed variants by relying on
specially-crafted item query expressions, this is only useful for item model
fields that contain natural language strings. When creating feeds based on
structured data, this is not helpful, since field values are identical across
two alts.

E.g., when we would like to publish a feed based on PDF attachments that have a
`volume_number` set, we are currently unable to have the English feed display
item titles as »Volume 1« in the English feed resp. »Ausgabe 1« in the German
alternative.

This commit adds support for such use-cases by adding two new mechanisms:

1. Instead of supplying field names to map records to Atom entries, the user
   supplies a Jinja template. These expressions are evaluated with `this` bound
   to the blog resp. the item record.

2. For a feed named `feed`, configuration values are first looked-up in the
   config file section `[feed.ALT]`, where `ALT` is the alternative currently
   being generated. This allows settings defaults in `[feed]`, and overriding
   only those settings that are locale-specific by adding them to `[feed.ALT]`.

As a side-effect, this also benefits users that don’t use alternatives, since it
enables them to compose item titles, bodies, etc. using multiple fields at the
same time.

Fixes #3, #13.
---
 README.md                                     |  66 ++++++--
 lektor_atom.py                                | 152 ++++++++++++------
 tests/demo-project/Website.lektorproject      |  10 ++
 tests/demo-project/configs/atom.ini           |  24 ++-
 .../custom-blog/filtered_post/contents.lr     |   2 +-
 .../content/custom-blog/post1/contents.lr     |   2 +-
 .../content/custom-blog/post2/contents.lr     |   2 +-
 .../content/multilang-blog/contents.lr        |   6 +
 .../multilang-blog/post1/contents+de.lr       |   5 +
 .../content/multilang-blog/post1/contents.lr  |   5 +
 .../content/multilang-blog/post2/contents.lr  |   7 +
 .../demo-project/models/custom-blog-post.ini  |   2 +-
 tests/test_lektor_atom.py                     |  59 ++++++-
 13 files changed, 260 insertions(+), 82 deletions(-)
 create mode 100644 tests/demo-project/content/multilang-blog/contents.lr
 create mode 100644 tests/demo-project/content/multilang-blog/post1/contents+de.lr
 create mode 100644 tests/demo-project/content/multilang-blog/post1/contents.lr
 create mode 100644 tests/demo-project/content/multilang-blog/post2/contents.lr

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'
+

From 663fa0f679aa57e901ea2fb9f73d9cfd08ec2af4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Reu=C3=9Fe?= <seb@wirrsal.net>
Date: Thu, 9 Aug 2018 11:31:10 +0200
Subject: [PATCH 4/4] Fix: use correct link URL when using attachment items
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Lektor sometimes yields invalid URLs for attachment records that have ‘alt’ set.
We work around this by forcing the primary alt when linking to attachments.
---
 lektor_atom.py                                | 11 +++++--
 tests/demo-project/configs/atom.ini           |  9 ++++++
 .../content/attachments-feed/1.txt            |  0
 .../content/attachments-feed/1.txt.lr         |  2 ++
 .../content/attachments-feed/contents.lr      |  6 ++++
 tests/demo-project/models/attachment.ini      |  6 ++++
 .../demo-project/models/attachments-page.ini  |  6 ++++
 .../templates/attachments-page.html           |  0
 tests/test_lektor_atom.py                     | 30 ++++++++++++++++++-
 9 files changed, 67 insertions(+), 3 deletions(-)
 create mode 100644 tests/demo-project/content/attachments-feed/1.txt
 create mode 100644 tests/demo-project/content/attachments-feed/1.txt.lr
 create mode 100644 tests/demo-project/content/attachments-feed/contents.lr
 create mode 100644 tests/demo-project/models/attachment.ini
 create mode 100644 tests/demo-project/models/attachments-page.ini
 create mode 100644 tests/demo-project/templates/attachments-page.html

diff --git a/lektor_atom.py b/lektor_atom.py
index 41c0745..99856de 100644
--- a/lektor_atom.py
+++ b/lektor_atom.py
@@ -156,7 +156,14 @@ def build_artifact(self, artifact):
                     item,
                     ctx.env
                 ) or blog_author
-
+                # FIXME Work-around Lektor #583. When the item is an attachment,
+                # we will get an invalid path here unless we force the
+                # `_primary` alt.
+                url = (
+                    item.url_to(item.path, external=True, alt='_primary')
+                    if item.is_attachment
+                    else url_to(item, external=True)
+                )
                 base_url = url_to(
                     item.parent if item.is_attachment else item,
                     external=True
@@ -175,7 +182,7 @@ def build_artifact(self, artifact):
                     title,
                     body,
                     xml_base=base_url,
-                    url=url_to(item, external=True),
+                    url=url,
                     content_type='html',
                     id=get_id(u'%s/%s' % (
                         ctx.env.project.id,
diff --git a/tests/demo-project/configs/atom.ini b/tests/demo-project/configs/atom.ini
index 2efe1d2..5917c21 100644
--- a/tests/demo-project/configs/atom.ini
+++ b/tests/demo-project/configs/atom.ini
@@ -35,3 +35,12 @@ item_title = {{ this.title }} ({{ this.pub_date | dateformat }})
 
 [feed-five.de]
 name = Feed Fünf
+
+[feed-six]
+name = Attachments feed
+source_path = /attachments-feed
+items = site.get("/attachments-feed").attachments
+item_title = {{ this.title }}
+item_body =
+blog_summary = Lots of downloads
+blog_author = Kim Dotcom
diff --git a/tests/demo-project/content/attachments-feed/1.txt b/tests/demo-project/content/attachments-feed/1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/demo-project/content/attachments-feed/1.txt.lr b/tests/demo-project/content/attachments-feed/1.txt.lr
new file mode 100644
index 0000000..38d7ae0
--- /dev/null
+++ b/tests/demo-project/content/attachments-feed/1.txt.lr
@@ -0,0 +1,2 @@
+title: ASCII ramblings
+---
diff --git a/tests/demo-project/content/attachments-feed/contents.lr b/tests/demo-project/content/attachments-feed/contents.lr
new file mode 100644
index 0000000..bd8ab78
--- /dev/null
+++ b/tests/demo-project/content/attachments-feed/contents.lr
@@ -0,0 +1,6 @@
+_model: attachments-page
+---
+author: A. Jesse Jiryu Davis
+---
+summary: My Summary
+---
diff --git a/tests/demo-project/models/attachment.ini b/tests/demo-project/models/attachment.ini
new file mode 100644
index 0000000..9339b9f
--- /dev/null
+++ b/tests/demo-project/models/attachment.ini
@@ -0,0 +1,6 @@
+[model]
+name = Attached document
+label = Attachment
+
+[fields.title]
+type = string
diff --git a/tests/demo-project/models/attachments-page.ini b/tests/demo-project/models/attachments-page.ini
new file mode 100644
index 0000000..cdea8e6
--- /dev/null
+++ b/tests/demo-project/models/attachments-page.ini
@@ -0,0 +1,6 @@
+[model]
+name = Document downloads page
+label = Document downloads
+
+[attachments]
+model = attachment
diff --git a/tests/demo-project/templates/attachments-page.html b/tests/demo-project/templates/attachments-page.html
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lektor_atom.py b/tests/test_lektor_atom.py
index 00ffbff..5004bbc 100644
--- a/tests/test_lektor_atom.py
+++ b/tests/test_lektor_atom.py
@@ -171,7 +171,7 @@ def test_discover_all(pad):
     '''
     all_feeds = set(['feed-one', 'feed-two',
                      'feed-three', 'feed-four',
-                     'feed-five'])
+                     'feed-five', 'feed-six'])
     feeds_discovered = feeds_from_template(pad, template)
     assert feeds_discovered == all_feeds
 
@@ -201,3 +201,31 @@ def test_localized_config(pad):
     assert plugin.get_atom_config('feed-five', 'name', alt='de') \
         == u'Feed Fünf'
 
+
+def test_attachments_feed(pad, builder):
+    failures = builder.build_all()
+    assert not failures
+
+    def test_feed(path, prefix=''):
+        feed_path = os.path.join(builder.destination_path, prefix, path)
+        feed = objectify.parse(open(feed_path)).getroot()
+
+        assert 'Attachments feed' == feed.title
+        assert 'Lots of downloads' == str(feed.subtitle).strip()
+        assert 'html' == feed.subtitle.attrib['type']
+        assert 'Kim Dotcom' == feed.author.name
+        assert 'http://x.com/%sattachments-feed/' % prefix \
+            == feed.link[0].attrib['href']
+        assert 'http://x.com/%sattachments-feed/feed.xml' % prefix \
+            == feed.link[1].attrib['href']
+
+        assert len(feed.entry) == 1
+        attachment1 = feed.entry[0]
+        assert attachment1.title == "ASCII ramblings"
+        base = attachment1.attrib['{http://www.w3.org/XML/1998/namespace}base']
+        assert base == 'http://x.com/attachments-feed/'
+        assert attachment1.link.attrib['href'] \
+            == 'http://x.com/attachments-feed/1.txt'
+
+    test_feed('attachments-feed/feed.xml')
+    test_feed('attachments-feed/feed.xml', prefix='de/')