Skip to content

Commit

Permalink
Add alternatives support
Browse files Browse the repository at this point in the history
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 lektor#3, lektor#13.
  • Loading branch information
eigengrau committed Aug 9, 2018
1 parent 2051cb5 commit d2fd328
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 82 deletions.
66 changes: 49 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand Down
152 changes: 104 additions & 48 deletions lektor_atom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -99,24 +107,28 @@ 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)

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)

Expand All @@ -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)

Expand All @@ -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',
Expand All @@ -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)
Expand All @@ -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)
10 changes: 10 additions & 0 deletions tests/demo-project/Website.lektorproject
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Loading

0 comments on commit d2fd328

Please sign in to comment.