add support for config setup
This commit is contained in:
41
README.md
41
README.md
@@ -1,9 +1,14 @@
|
|||||||
# Lektor Plugin: @groupby
|
# Lektor Plugin: groupby
|
||||||
|
|
||||||
A generic grouping / clustering plugin. Can be used for tagging and similar tasks.
|
A generic grouping / clustering plugin. Can be used for tagging or similar tasks.
|
||||||
|
|
||||||
|
Overview:
|
||||||
|
- the [basic example](#usage-basic-example) goes into detail how this plugin works.
|
||||||
|
- the [quick config](#usage-quick-config) example show how you can use the plugin config to setup a quick and easy tagging system.
|
||||||
|
- the [complex example](#usage-a-slightly-more-complex-example) touches on the potential of what is possible.
|
||||||
|
|
||||||
|
|
||||||
## Usage: Simple Example
|
## Usage: Basic example
|
||||||
|
|
||||||
Lets start with a simple example: adding a tags field to your model.
|
Lets start with a simple example: adding a tags field to your model.
|
||||||
Assuming you have a `blog-entry.ini` that is used for all children of `/blog` path.
|
Assuming you have a `blog-entry.ini` that is used for all children of `/blog` path.
|
||||||
@@ -107,8 +112,37 @@ This is: <GroupBySource attribute="myvar" group="latest-news" template="myvar.ht
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Usage: Quick config
|
||||||
|
|
||||||
|
The whole example above can be simplified with a plugin config:
|
||||||
|
|
||||||
|
#### `configs/groupby.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[myvar]
|
||||||
|
root = /blog/
|
||||||
|
slug = tag/{group}/index.html
|
||||||
|
template = myvar.html
|
||||||
|
split = ' '
|
||||||
|
```
|
||||||
|
|
||||||
|
You still need to add a separate attribute to your model (step 1), but anything else is handled by the config file.
|
||||||
|
All of these fields are optional and fallback to the default values stated above.
|
||||||
|
|
||||||
|
The newly introduced option `split` will be used as string delimiter.
|
||||||
|
This allows to have a field with `string` type instead of `strings` type.
|
||||||
|
If you do not provide the `split` option, the whole field value will be used as group key.
|
||||||
|
Note: split is only used on str fields (`string` type), not lists (`strings` type).
|
||||||
|
|
||||||
|
The emitted `extra-info` for the child is the original key value.
|
||||||
|
E.g., `Latest News,Awesome` with `split = ,` yields `('latest-news', 'Latest News')` and `('awesome', 'Awesome')`.
|
||||||
|
|
||||||
|
|
||||||
## Usage: A slightly more complex example
|
## Usage: A slightly more complex example
|
||||||
|
|
||||||
|
There are situations though, where a simple config file is not enough.
|
||||||
|
The following plugin will find all model fields with attribute `inlinetags` and search for in-text occurrences of `{{Tagname}}` etc.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from lektor.markdown import Markdown
|
from lektor.markdown import Markdown
|
||||||
from lektor.types.formats import MarkdownDescriptor
|
from lektor.types.formats import MarkdownDescriptor
|
||||||
@@ -129,7 +163,6 @@ def on_groupby_init(self, groupby, **extra):
|
|||||||
yield slugify(tag), tag
|
yield slugify(tag), tag
|
||||||
```
|
```
|
||||||
|
|
||||||
This will find all model fields with attribute `inlinetags` and search for in-text occurrences of `{{Tagname}}`, etc.
|
|
||||||
This generic approach does not care what data-type the field value is:
|
This generic approach does not care what data-type the field value is:
|
||||||
`strings` fields will be expanded and enumerated, Markdown will be unpacked.
|
`strings` fields will be expanded and enumerated, Markdown will be unpacked.
|
||||||
You can combine this mere tag-detector with text-replacements to point to the actual tags-page.
|
You can combine this mere tag-detector with text-replacements to point to the actual tags-page.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from lektor.reporter import reporter
|
|||||||
from lektor.sourceobj import SourceObject, VirtualSourceObject
|
from lektor.sourceobj import SourceObject, VirtualSourceObject
|
||||||
from lektor.types.flow import Flow, FlowType
|
from lektor.types.flow import Flow, FlowType
|
||||||
from lektor.utils import bool_from_string, build_url, prune_file_and_folder
|
from lektor.utils import bool_from_string, build_url, prune_file_and_folder
|
||||||
|
# for quick config
|
||||||
|
from lektor.utils import slugify
|
||||||
|
|
||||||
from typing import \
|
from typing import \
|
||||||
NewType, NamedTuple, Tuple, Dict, Set, List, Optional, Iterator, Callable
|
NewType, NamedTuple, Tuple, Dict, Set, List, Optional, Iterator, Callable
|
||||||
@@ -47,8 +49,9 @@ class GroupProducer(NamedTuple):
|
|||||||
attribute: AttributeKey
|
attribute: AttributeKey
|
||||||
func: GroupingCallback
|
func: GroupingCallback
|
||||||
flatten: bool = True
|
flatten: bool = True
|
||||||
template: Optional[str] = None
|
|
||||||
slug: Optional[str] = None
|
slug: Optional[str] = None
|
||||||
|
template: Optional[str] = None
|
||||||
|
dependency: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class GroupComponent(NamedTuple):
|
class GroupComponent(NamedTuple):
|
||||||
@@ -84,14 +87,16 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
attribute: AttributeKey,
|
attribute: AttributeKey,
|
||||||
group: GroupKey,
|
group: GroupKey,
|
||||||
children: List[GroupComponent] = [],
|
children: List[GroupComponent] = [],
|
||||||
|
slug: Optional[str] = None, # default: "{attrib}/{group}/index.html"
|
||||||
template: Optional[str] = None, # default: "groupby-attribute.html"
|
template: Optional[str] = None, # default: "groupby-attribute.html"
|
||||||
slug: Optional[str] = None # default: "{attrib}/{group}/index.html"
|
dependency: Optional[str] = None
|
||||||
):
|
):
|
||||||
super().__init__(record)
|
super().__init__(record)
|
||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
self.group = group
|
self.group = group
|
||||||
self.children = children
|
self.children = children
|
||||||
self.template = template or 'groupby-{}.html'.format(self.attribute)
|
self.template = template or 'groupby-{}.html'.format(self.attribute)
|
||||||
|
self.dependency = dependency
|
||||||
# custom user path
|
# custom user path
|
||||||
slug = slug or '{attrib}/{group}/index.html'
|
slug = slug or '{attrib}/{group}/index.html'
|
||||||
slug = slug.replace('{attrib}', self.attribute)
|
slug = slug.replace('{attrib}', self.attribute)
|
||||||
@@ -110,6 +115,8 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
return build_url([self.record.path, self.slug])
|
return build_url([self.record.path, self.slug])
|
||||||
|
|
||||||
def iter_source_filenames(self) -> Iterator[str]:
|
def iter_source_filenames(self) -> Iterator[str]:
|
||||||
|
if self.dependency:
|
||||||
|
yield self.dependency
|
||||||
for record, _ in self.children:
|
for record, _ in self.children:
|
||||||
yield from record.iter_source_filenames()
|
yield from record.iter_source_filenames()
|
||||||
|
|
||||||
@@ -194,6 +201,7 @@ class GroupByCreator:
|
|||||||
self._models: Dict[AttributeKey, Dict[str, Dict[str, str]]] = {}
|
self._models: Dict[AttributeKey, Dict[str, Dict[str, str]]] = {}
|
||||||
self._func: Dict[str, Set[GroupProducer]] = {}
|
self._func: Dict[str, Set[GroupProducer]] = {}
|
||||||
self._resolve_map: Dict[str, UrlResolverConf] = {} # only for server
|
self._resolve_map: Dict[str, UrlResolverConf] = {} # only for server
|
||||||
|
self._watched_once: Set[GroupingCallback] = set()
|
||||||
|
|
||||||
# --------------
|
# --------------
|
||||||
# Initialize
|
# Initialize
|
||||||
@@ -243,8 +251,9 @@ class GroupByCreator:
|
|||||||
root: str,
|
root: str,
|
||||||
attrib: AttributeKey, *,
|
attrib: AttributeKey, *,
|
||||||
flatten: bool = True, # if False, dont explode FlowType
|
flatten: bool = True, # if False, dont explode FlowType
|
||||||
|
slug: Optional[str] = None, # default: "{attrib}/{group}/index.html"
|
||||||
template: Optional[str] = None, # default: "groupby-attrib.html"
|
template: Optional[str] = None, # default: "groupby-attrib.html"
|
||||||
slug: Optional[str] = None # default: "{attrib}/{group}/index.html"
|
dependency: Optional[str] = None
|
||||||
) -> Callable[[GroupingCallback], None]:
|
) -> Callable[[GroupingCallback], None]:
|
||||||
'''
|
'''
|
||||||
Decorator to subscribe to attrib-elements. Converter for groupby().
|
Decorator to subscribe to attrib-elements. Converter for groupby().
|
||||||
@@ -256,14 +265,30 @@ class GroupByCreator:
|
|||||||
template: "groupby-attrib.html"
|
template: "groupby-attrib.html"
|
||||||
slug: "{attrib}/{group}/index.html"
|
slug: "{attrib}/{group}/index.html"
|
||||||
'''
|
'''
|
||||||
|
root = root.rstrip('/') + '/'
|
||||||
|
|
||||||
def _decorator(fn: GroupingCallback):
|
def _decorator(fn: GroupingCallback):
|
||||||
if root not in self._func:
|
if root not in self._func:
|
||||||
self._func[root] = set()
|
self._func[root] = set()
|
||||||
self._func[root].add(
|
self._func[root].add(
|
||||||
GroupProducer(attrib, fn, flatten, template, slug))
|
GroupProducer(attrib, fn, flatten, template, slug, dependency))
|
||||||
|
|
||||||
return _decorator
|
return _decorator
|
||||||
|
|
||||||
|
def watch_once(self, *args, **kwarg) -> Callable[[GroupingCallback], None]:
|
||||||
|
''' Same as watch() but listener is auto removed after build. '''
|
||||||
|
def _decorator(fn: GroupingCallback):
|
||||||
|
self._watched_once.add(fn)
|
||||||
|
self.watch(*args, **kwarg)(fn)
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
def remove_watch_once(self) -> None:
|
||||||
|
''' Remove all watch-once listeners. '''
|
||||||
|
for k, v in self._func.items():
|
||||||
|
not_once = {x for x in v if x.func not in self._watched_once}
|
||||||
|
self._func[k] = not_once
|
||||||
|
self._watched_once.clear()
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Helper
|
# Helper
|
||||||
# ----------
|
# ----------
|
||||||
@@ -346,11 +371,11 @@ class GroupByCreator:
|
|||||||
def make_cluster(self, root: lektor.db.Record) -> Iterator[GroupBySource]:
|
def make_cluster(self, root: lektor.db.Record) -> Iterator[GroupBySource]:
|
||||||
''' Group by attrib and build Artifacts. '''
|
''' Group by attrib and build Artifacts. '''
|
||||||
assert isinstance(root, lektor.db.Record)
|
assert isinstance(root, lektor.db.Record)
|
||||||
for attrib, fn, flat, temp, slug in self._func.get(root.url_path, []):
|
for attr, fn, fl, temp, slug, dep in self._func.get(root.url_path, []):
|
||||||
groups = self.groupby(attrib, root, func=fn, flatten=flat)
|
groups = self.groupby(attr, root, func=fn, flatten=fl)
|
||||||
for group_key, children in groups.items():
|
for group_key, children in groups.items():
|
||||||
obj = GroupBySource(root, attrib, group_key, children,
|
obj = GroupBySource(root, attr, group_key, children,
|
||||||
template=temp, slug=slug)
|
template=temp, slug=slug, dependency=dep)
|
||||||
self.track_dev_server_path(obj)
|
self.track_dev_server_path(obj)
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
@@ -365,7 +390,7 @@ class GroupByCreator:
|
|||||||
if len(pieces) >= 2:
|
if len(pieces) >= 2:
|
||||||
attrib: AttributeKey = pieces[0] # type: ignore[assignment]
|
attrib: AttributeKey = pieces[0] # type: ignore[assignment]
|
||||||
group: GroupKey = pieces[1] # type: ignore[assignment]
|
group: GroupKey = pieces[1] # type: ignore[assignment]
|
||||||
for attr, _, _, _, slug in self._func.get(node.url_path, []):
|
for attr, _, _, _, slug, _ in self._func.get(node.url_path, []):
|
||||||
if attr == attrib:
|
if attr == attrib:
|
||||||
# TODO: do we need to provide the template too?
|
# TODO: do we need to provide the template too?
|
||||||
return GroupBySource(node, attr, group, slug=slug)
|
return GroupBySource(node, attr, group, slug=slug)
|
||||||
@@ -420,10 +445,35 @@ class GroupByPlugin(Plugin):
|
|||||||
if self.creator.should_process(node):
|
if self.creator.should_process(node):
|
||||||
yield from self.creator.make_cluster(node)
|
yield from self.creator.make_cluster(node)
|
||||||
|
|
||||||
|
def _quick_config(self):
|
||||||
|
config = self.get_config()
|
||||||
|
for attrib in config.sections():
|
||||||
|
sect = config.section_as_dict(attrib)
|
||||||
|
root = sect.get('root', '/')
|
||||||
|
slug = sect.get('slug')
|
||||||
|
temp = sect.get('template')
|
||||||
|
split = sect.get('split')
|
||||||
|
|
||||||
|
@self.creator.watch_once(root, attrib, template=temp, slug=slug,
|
||||||
|
dependency=self.config_filename)
|
||||||
|
def _fn(args):
|
||||||
|
val = args.field
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = val.split(split) if split else [val] # make list
|
||||||
|
if isinstance(val, list):
|
||||||
|
for tag in val:
|
||||||
|
yield slugify(tag), tag
|
||||||
|
|
||||||
def on_before_build_all(self, builder, **extra):
|
def on_before_build_all(self, builder, **extra):
|
||||||
|
# load config file quick listeners (before initialize!)
|
||||||
|
self._quick_config()
|
||||||
# parse all models to detect attribs of listeners
|
# parse all models to detect attribs of listeners
|
||||||
self.creator.initialize(builder.pad.db)
|
self.creator.initialize(builder.pad.db)
|
||||||
|
|
||||||
|
def on_after_build_all(self, builder, **extra):
|
||||||
|
# remove all quick listeners (will be added again in the next build)
|
||||||
|
self.creator.remove_watch_once()
|
||||||
|
|
||||||
def on_after_prune(self, builder, **extra):
|
def on_after_prune(self, builder, **extra):
|
||||||
# TODO: find better way to prune unreferenced elements
|
# TODO: find better way to prune unreferenced elements
|
||||||
GroupByPruner.prune(builder)
|
GroupByPruner.prune(builder)
|
||||||
|
|||||||
Reference in New Issue
Block a user