comparison piecrust/appconfig.py @ 805:fd694f1297c7

config: Cleanup config loading code. Add support for a `local.yml` config.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 10 Oct 2016 21:41:59 -0700
parents 58ebf50235a5
children b8f089092281
comparison
equal deleted inserted replaced
804:08e6484a2600 805:fd694f1297c7
5 import urllib 5 import urllib
6 import logging 6 import logging
7 import hashlib 7 import hashlib
8 import collections 8 import collections
9 import yaml 9 import yaml
10 from piecrust import ( 10 from piecrust import APP_VERSION, CACHE_VERSION, DEFAULT_DATE_FORMAT
11 APP_VERSION, CACHE_VERSION, 11 from piecrust.appconfigdefaults import (
12 DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS, 12 default_configuration,
13 DEFAULT_DATE_FORMAT, DEFAULT_THEME_SOURCE) 13 default_theme_content_model_base,
14 default_content_model_base,
15 get_default_content_model, get_default_content_model_for_blog)
14 from piecrust.cache import NullCache 16 from piecrust.cache import NullCache
15 from piecrust.configuration import ( 17 from piecrust.configuration import (
16 Configuration, ConfigurationError, ConfigurationLoader, 18 Configuration, ConfigurationError, ConfigurationLoader,
17 try_get_dict_value, try_get_dict_values, 19 try_get_dict_values, try_get_dict_value, set_dict_value,
18 set_dict_value, merge_dicts, visit_dict) 20 merge_dicts, visit_dict,
21 MERGE_NEW_VALUES, MERGE_OVERWRITE_VALUES, MERGE_PREPEND_LISTS,
22 MERGE_APPEND_LISTS)
19 from piecrust.sources.base import REALM_USER, REALM_THEME 23 from piecrust.sources.base import REALM_USER, REALM_THEME
20 24
21 25
22 logger = logging.getLogger(__name__) 26 logger = logging.getLogger(__name__)
27
28
29 class InvalidConfigurationPathError(Exception):
30 pass
23 31
24 32
25 class VariantNotFoundError(Exception): 33 class VariantNotFoundError(Exception):
26 def __init__(self, variant_name, message=None): 34 def __init__(self, variant_name, message=None):
27 super(VariantNotFoundError, self).__init__( 35 super(VariantNotFoundError, self).__init__(
28 message or ("No such configuration variant: %s" % 36 message or ("No such configuration variant: %s" %
29 variant_name)) 37 variant_name))
30 38
31 39
32 class PieCrustConfiguration(Configuration): 40 class PieCrustConfiguration(Configuration):
33 def __init__(self, *, path=None, theme_path=None, values=None, 41 def __init__(self, *, path=None, theme_path=None, values=None,
34 cache=None, validate=True, theme_config=False): 42 cache=None, validate=True, theme_config=False):
42 self._custom_paths = [] 50 self._custom_paths = []
43 self._post_fixups = [] 51 self._post_fixups = []
44 self.theme_config = theme_config 52 self.theme_config = theme_config
45 # Set the values after we set the rest, since our validation needs 53 # Set the values after we set the rest, since our validation needs
46 # our attributes. 54 # our attributes.
47 if values: 55 if values is not None:
48 self.setAll(values, validate=validate) 56 self.setAll(values, validate=validate)
49 57
50 def addPath(self, p): 58 def addPath(self, p):
59 if not p:
60 raise InvalidConfigurationPathError()
51 self._ensureNotLoaded() 61 self._ensureNotLoaded()
52 self._custom_paths.append(p) 62 self._custom_paths.append(p)
53 63
54 def addVariant(self, variant_path, raise_if_not_found=True): 64 def addVariant(self, variant_path, raise_if_not_found=True):
55 self._ensureNotLoaded() 65 self._ensureNotLoaded()
56 if os.path.isfile(variant_path): 66 if os.path.isfile(variant_path):
57 self.addPath(variant_path) 67 self.addPath(variant_path)
58 elif raise_if_not_found: 68 elif raise_if_not_found:
59 logger.error( 69 logger.error(
60 "Configuration variants should now be `.yml` files " 70 "Configuration variants should now be `.yml` files "
61 "located in the `configs/` directory of your website.") 71 "located in the `configs/` directory of your website.")
62 raise VariantNotFoundError(variant_path) 72 raise VariantNotFoundError(variant_path)
63
64 73
65 def addVariantValue(self, path, value): 74 def addVariantValue(self, path, value):
66 def _fixup(config): 75 def _fixup(config):
67 set_dict_value(config, path, value) 76 set_dict_value(config, path, value)
68 77
69 self._post_fixups.append(_fixup) 78 self._post_fixups.append(_fixup)
70 79
71 def setAll(self, values, validate=False): 80 def setAll(self, values, validate=False):
72 # Override base class implementation 81 # Override base class implementation
73 values = self._combineConfigs({}, values) 82 values = self._processConfigs({}, values)
74 if validate: 83 if validate:
75 values = self._validateAll(values) 84 values = self._validateAll(values)
76 self._values = values 85 self._values = values
77 86
78 def _ensureNotLoaded(self): 87 def _ensureNotLoaded(self):
79 if self._values is not None: 88 if self._values is not None:
80 raise Exception("The configurations has been loaded.") 89 raise Exception("The configurations has been loaded.")
81 90
82 def _load(self): 91 def _load(self):
83 # Figure out where to load this configuration from. 92 # Figure out where to load this configuration from.
84 paths = [self._theme_path, self._path] + self._custom_paths 93 paths = []
85 paths = list(filter(lambda i: i is not None, paths)) 94 if self._theme_path:
95 paths.append(self._theme_path)
96 if self._path:
97 paths.append(self._path)
98 paths += self._custom_paths
99
100 if len(paths) == 0:
101 raise ConfigurationError(
102 "No paths to load configuration from. "
103 "Specify paths, or set the values directly.")
86 104
87 # Build the cache-key. 105 # Build the cache-key.
88 path_times = [os.path.getmtime(p) for p in paths] 106 path_times = [os.path.getmtime(p) for p in paths]
89 cache_key_hash = hashlib.md5( 107 cache_key_hash = hashlib.md5(
90 ("version=%s&cache=%d" % ( 108 ("version=%s&cache=%d" % (
91 APP_VERSION, CACHE_VERSION)).encode('utf8')) 109 APP_VERSION, CACHE_VERSION)).encode('utf8'))
92 for p in paths: 110 for p in paths:
93 cache_key_hash.update(("&path=%s" % p).encode('utf8')) 111 cache_key_hash.update(("&path=%s" % p).encode('utf8'))
94 cache_key = cache_key_hash.hexdigest() 112 cache_key = cache_key_hash.hexdigest()
95 113
96 # Check the cache for a valid version. 114 # Check the cache for a valid version.
97 if self._cache.isValid('config.json', path_times): 115 if self._cache.isValid('config.json', path_times):
98 logger.debug("Loading configuration from cache...") 116 logger.debug("Loading configuration from cache...")
99 config_text = self._cache.read('config.json') 117 config_text = self._cache.read('config.json')
100 self._values = json.loads( 118 self._values = json.loads(
101 config_text, 119 config_text,
102 object_pairs_hook=collections.OrderedDict) 120 object_pairs_hook=collections.OrderedDict)
103 121
104 actual_cache_key = self._values.get('__cache_key') 122 actual_cache_key = self._values.get('__cache_key')
105 if actual_cache_key == cache_key: 123 if actual_cache_key == cache_key:
106 # The cached version has the same key! Awesome! 124 # The cached version has the same key! Awesome!
107 self._values['__cache_valid'] = True 125 self._values['__cache_valid'] = True
108 return 126 return
109 logger.debug("Outdated cache key '%s' (expected '%s')." % ( 127 logger.debug("Outdated cache key '%s' (expected '%s')." % (
110 actual_cache_key, cache_key)) 128 actual_cache_key, cache_key))
111 129
112 # Nope, load from the paths. 130 # Nope, load from the paths.
113 try: 131 try:
114 # Theme config. 132 # Theme values.
115 theme_values = {} 133 theme_values = None
116 if self._theme_path: 134 if self._theme_path:
135 logger.debug("Loading theme layer from: %s" % self._theme_path)
117 theme_values = self._loadFrom(self._theme_path) 136 theme_values = self._loadFrom(self._theme_path)
118 137
119 # Site config. 138 # Site and variant values.
139 site_paths = []
140 if self._path:
141 site_paths.append(self._path)
142 site_paths += self._custom_paths
143
120 site_values = {} 144 site_values = {}
121 if self._path: 145 for path in site_paths:
122 site_values = self._loadFrom(self._path) 146 logger.debug("Loading config layer from: %s" % path)
123 147 cur_values = self._loadFrom(path)
124 # Combine! 148 merge_dicts(site_values, cur_values)
125 logger.debug("Processing loaded configurations...") 149
126 values = self._combineConfigs(theme_values, site_values) 150 # Do it!
127 151 values = self._processConfigs(theme_values, site_values)
128 # Load additional paths.
129 if self._custom_paths:
130 logger.debug("Loading %d additional configuration paths." %
131 len(self._custom_paths))
132 for p in self._custom_paths:
133 loaded = self._loadFrom(p)
134 if loaded:
135 merge_dicts(values, loaded)
136
137 # Run final fixups
138 if self._post_fixups:
139 logger.debug("Applying %d configuration fixups." %
140 len(self._post_fixups))
141 for f in self._post_fixups:
142 f(values)
143
144 self._values = self._validateAll(values) 152 self._values = self._validateAll(values)
145 except Exception as ex: 153 except Exception as ex:
146 logger.exception(ex) 154 logger.exception(ex)
147 raise Exception( 155 raise Exception(
148 "Error loading configuration from: %s" % 156 "Error loading configuration from: %s" %
149 ', '.join(paths)) from ex 157 ', '.join(paths)) from ex
150 158
151 logger.debug("Caching configuration...") 159 logger.debug("Caching configuration...")
152 self._values['__cache_key'] = cache_key 160 self._values['__cache_key'] = cache_key
153 config_text = json.dumps(self._values) 161 config_text = json.dumps(self._values)
154 self._cache.write('config.json', config_text) 162 self._cache.write('config.json', config_text)
157 165
158 def _loadFrom(self, path): 166 def _loadFrom(self, path):
159 logger.debug("Loading configuration from: %s" % path) 167 logger.debug("Loading configuration from: %s" % path)
160 with open(path, 'r', encoding='utf-8') as fp: 168 with open(path, 'r', encoding='utf-8') as fp:
161 values = yaml.load( 169 values = yaml.load(
162 fp.read(), 170 fp.read(),
163 Loader=ConfigurationLoader) 171 Loader=ConfigurationLoader)
164 if values is None: 172 if values is None:
165 values = {} 173 values = {}
166 return values 174 return values
167 175
168 def _combineConfigs(self, theme_values, site_values): 176 def _processConfigs(self, theme_values, site_values):
169 # Start with the default configuration. 177 # Start with the default configuration.
170 values = copy.deepcopy(default_configuration) 178 values = copy.deepcopy(default_configuration)
171 179
172 if not self.theme_config: 180 # If we have a theme, apply the theme on that. So stuff like routes
173 # If the theme config wants the default model, add it. 181 # will now look like:
174 theme_sitec = theme_values.setdefault( 182 # [custom theme] + [default theme] + [default]
175 'site', collections.OrderedDict()) 183 if theme_values is not None:
176 gen_default_theme_model = bool(theme_sitec.setdefault( 184 self._processThemeLayer(theme_values, values)
177 'use_default_theme_content', True)) 185 merge_dicts(values, theme_values)
178 if gen_default_theme_model:
179 self._generateDefaultThemeModel(values)
180
181 # Now override with the actual theme config values.
182 values = merge_dicts(values, theme_values)
183 186
184 # Make all sources belong to the "theme" realm at this point. 187 # Make all sources belong to the "theme" realm at this point.
185 srcc = values['site'].get('sources') 188 srcc = values['site'].get('sources')
186 if srcc: 189 if srcc:
187 for sn, sc in srcc.items(): 190 for sn, sc in srcc.items():
188 sc['realm'] = REALM_THEME 191 sc['realm'] = REALM_THEME
189 192
190 # If the site config wants the default model, add it. 193 # Now we apply the site stuff. We want to end up with:
191 site_sitec = site_values.setdefault( 194 # [custom site] + [default site] + [custom theme] + [default theme] +
192 'site', collections.OrderedDict()) 195 # [default]
193 gen_default_site_model = bool(site_sitec.setdefault( 196 if site_values is not None:
194 'use_default_content', True)) 197 self._processSiteLayer(site_values, values)
195 if gen_default_site_model: 198 merge_dicts(values, site_values)
196 self._generateDefaultSiteModel(values, site_values)
197
198 # And override with the actual site config values.
199 values = merge_dicts(values, site_values)
200 199
201 # Set the theme site flag. 200 # Set the theme site flag.
202 if self.theme_config: 201 if self.theme_config:
203 values['site']['theme_site'] = True 202 values['site']['theme_site'] = True
204 203
204 # Run final fixups
205 if self._post_fixups:
206 logger.debug("Applying %d configuration fixups." %
207 len(self._post_fixups))
208 for f in self._post_fixups:
209 f(values)
210
205 return values 211 return values
212
213 def _processThemeLayer(self, theme_values, values):
214 # Generate the default theme model.
215 gen_default_theme_model = bool(try_get_dict_values(
216 (theme_values, 'site/use_default_theme_content'),
217 (values, 'site/use_default_theme_content'),
218 default=True))
219 if gen_default_theme_model:
220 self._generateDefaultThemeModel(theme_values, values)
221
222 def _processSiteLayer(self, site_values, values):
223 # Default site content.
224 gen_default_site_model = bool(try_get_dict_values(
225 (site_values, 'site/use_default_content'),
226 (values, 'site/use_default_content'),
227 default=True))
228 if gen_default_site_model:
229 self._generateDefaultSiteModel(site_values, values)
230
231 def _generateDefaultThemeModel(self, theme_values, values):
232 logger.debug("Generating default theme content model...")
233 cc = copy.deepcopy(default_theme_content_model_base)
234 merge_dicts(values, cc)
235
236 def _generateDefaultSiteModel(self, site_values, values):
237 logger.debug("Generating default content model...")
238 cc = copy.deepcopy(default_content_model_base)
239 merge_dicts(values, cc)
240
241 dcm = get_default_content_model(site_values, values)
242 merge_dicts(values, dcm)
243
244 blogsc = try_get_dict_values(
245 (site_values, 'site/blogs'),
246 (values, 'site/blogs'))
247 if blogsc is None:
248 blogsc = ['posts']
249 set_dict_value(site_values, 'site/blogs', blogsc)
250
251 is_only_blog = (len(blogsc) == 1)
252 for blog_name in reversed(blogsc):
253 blog_cfg = get_default_content_model_for_blog(
254 blog_name, is_only_blog, site_values, values,
255 theme_site=self.theme_config)
256 merge_dicts(values, blog_cfg)
206 257
207 def _validateAll(self, values): 258 def _validateAll(self, values):
208 if values is None: 259 if values is None:
209 values = {} 260 values = {}
210 261
233 284
234 visit_dict(values, _visitor) 285 visit_dict(values, _visitor)
235 286
236 return values 287 return values
237 288
238 def _generateDefaultThemeModel(self, values):
239 logger.debug("Generating default theme content model...")
240 cc = copy.deepcopy(default_theme_content_model_base)
241 merge_dicts(values, cc)
242
243 def _generateDefaultSiteModel(self, values, user_overrides):
244 logger.debug("Generating default content model...")
245 cc = copy.deepcopy(default_content_model_base)
246 merge_dicts(values, cc)
247
248 dcm = get_default_content_model(values, user_overrides)
249 merge_dicts(values, dcm)
250
251 blogsc = try_get_dict_value(user_overrides, 'site/blogs')
252 if blogsc is None:
253 blogsc = ['posts']
254 set_dict_value(user_overrides, 'site/blogs', blogsc)
255
256 is_only_blog = (len(blogsc) == 1)
257 for blog_name in blogsc:
258 blog_cfg = get_default_content_model_for_blog(
259 blog_name, is_only_blog, values, user_overrides,
260 theme_site=self.theme_config)
261 merge_dicts(values, blog_cfg)
262
263 289
264 class _ConfigCacheWriter(object): 290 class _ConfigCacheWriter(object):
265 def __init__(self, cache_dict): 291 def __init__(self, cache_dict):
266 self._cache_dict = cache_dict 292 self._cache_dict = cache_dict
267 293
268 def write(self, name, val): 294 def write(self, name, val):
269 logger.debug("Caching configuration item '%s' = %s" % (name, val)) 295 logger.debug("Caching configuration item '%s' = %s" % (name, val))
270 self._cache_dict[name] = val 296 self._cache_dict[name] = val
271
272
273 default_theme_content_model_base = collections.OrderedDict({
274 'site': collections.OrderedDict({
275 'sources': collections.OrderedDict({
276 'theme_pages': {
277 'type': 'default',
278 'ignore_missing_dir': True,
279 'fs_endpoint': 'pages',
280 'data_endpoint': 'site.pages',
281 'default_layout': 'default',
282 'item_name': 'page',
283 'realm': REALM_THEME
284 }
285 }),
286 'routes': [
287 {
288 'url': '/%slug%',
289 'source': 'theme_pages',
290 'func': 'pcurl'
291 }
292 ],
293 'theme_tag_page': 'theme_pages:_tag.%ext%',
294 'theme_category_page': 'theme_pages:_category.%ext%',
295 'theme_month_page': 'theme_pages:_month.%ext%',
296 'theme_year_page': 'theme_pages:_year.%ext%'
297 })
298 })
299
300
301 default_configuration = collections.OrderedDict({
302 'site': collections.OrderedDict({
303 'title': "Untitled PieCrust website",
304 'root': '/',
305 'default_format': DEFAULT_FORMAT,
306 'default_template_engine': DEFAULT_TEMPLATE_ENGINE,
307 'enable_gzip': True,
308 'pretty_urls': False,
309 'trailing_slash': False,
310 'date_format': DEFAULT_DATE_FORMAT,
311 'auto_formats': collections.OrderedDict([
312 ('html', ''),
313 ('md', 'markdown'),
314 ('textile', 'textile')]),
315 'default_auto_format': 'md',
316 'default_pagination_source': None,
317 'pagination_suffix': '/%num%',
318 'slugify_mode': 'encode',
319 'themes_sources': [DEFAULT_THEME_SOURCE],
320 'cache_time': 28800,
321 'enable_debug_info': True,
322 'show_debug_info': False,
323 'use_default_content': True,
324 'use_default_theme_content': True,
325 'theme_site': False
326 }),
327 'baker': collections.OrderedDict({
328 'no_bake_setting': 'draft',
329 'workers': None,
330 'batch_size': None
331 })
332 })
333
334
335 default_content_model_base = collections.OrderedDict({
336 'site': collections.OrderedDict({
337 'posts_fs': DEFAULT_POSTS_FS,
338 'default_page_layout': 'default',
339 'default_post_layout': 'post',
340 'post_url': '/%year%/%month%/%day%/%slug%',
341 'year_url': '/archives/%year%',
342 'tag_url': '/tag/%tag%',
343 'category_url': '/%category%',
344 'posts_per_page': 5
345 })
346 })
347
348
349 def get_default_content_model(values, user_overrides):
350 default_layout = try_get_dict_value(
351 user_overrides, 'site/default_page_layout',
352 values['site']['default_page_layout'])
353 return collections.OrderedDict({
354 'site': collections.OrderedDict({
355 'sources': collections.OrderedDict({
356 'pages': {
357 'type': 'default',
358 'ignore_missing_dir': True,
359 'data_endpoint': 'site.pages',
360 'default_layout': default_layout,
361 'item_name': 'page'
362 }
363 }),
364 'routes': [
365 {
366 'url': '/%slug%',
367 'source': 'pages',
368 'func': 'pcurl'
369 }
370 ],
371 'taxonomies': collections.OrderedDict([
372 ('tags', {
373 'multiple': True,
374 'term': 'tag'
375 }),
376 ('categories', {
377 'term': 'category',
378 'func_name': 'pccaturl'
379 })
380 ])
381 })
382 })
383
384
385 def get_default_content_model_for_blog(
386 blog_name, is_only_blog, values, user_overrides, theme_site=False):
387 # Get the global (default) values for various things we're interested in.
388 defs = {}
389 names = ['posts_fs', 'posts_per_page', 'date_format',
390 'default_post_layout', 'post_url', 'year_url']
391 for n in names:
392 defs[n] = try_get_dict_value(
393 user_overrides, 'site/%s' % n,
394 values['site'][n])
395
396 # More stuff we need.
397 if is_only_blog:
398 url_prefix = ''
399 page_prefix = ''
400 fs_endpoint = 'posts'
401 data_endpoint = 'blog'
402 item_name = 'post'
403 tpl_func_prefix = 'pc'
404
405 if theme_site:
406 # If this is a theme site, show posts from a `sample` directory
407 # so it's clearer that those won't show up when the theme is
408 # actually applied to a normal site.
409 fs_endpoint = 'sample/posts'
410 else:
411 url_prefix = blog_name + '/'
412 page_prefix = blog_name + '/'
413 data_endpoint = blog_name
414 fs_endpoint = 'posts/%s' % blog_name
415 item_name = try_get_dict_value(user_overrides,
416 '%s/item_name' % blog_name,
417 '%spost' % blog_name)
418 tpl_func_prefix = try_get_dict_value(user_overrides,
419 '%s/func_prefix' % blog_name,
420 'pc%s' % blog_name)
421
422 # Figure out the settings values for this blog, specifically.
423 # The value could be set on the blog config itself, globally, or left at
424 # its default. We already handle the "globally vs. default" with the
425 # `defs` map that we computed above.
426 blog_cfg = user_overrides.get(blog_name, {})
427 blog_values = {}
428 for n in names:
429 blog_values[n] = blog_cfg.get(n, defs[n])
430
431 posts_fs = blog_values['posts_fs']
432 posts_per_page = blog_values['posts_per_page']
433 date_format = blog_values['date_format']
434 default_layout = blog_values['default_post_layout']
435 post_url = '/' + url_prefix + blog_values['post_url'].lstrip('/')
436 year_url = '/' + url_prefix + blog_values['year_url'].lstrip('/')
437
438 year_archive = 'pages:%s_year.%%ext%%' % page_prefix
439 if not theme_site:
440 theme_year_page = values['site'].get('theme_year_page')
441 if theme_year_page:
442 year_archive += ';' + theme_year_page
443
444 cfg = collections.OrderedDict({
445 'site': collections.OrderedDict({
446 'sources': collections.OrderedDict({
447 blog_name: collections.OrderedDict({
448 'type': 'posts/%s' % posts_fs,
449 'fs_endpoint': fs_endpoint,
450 'data_endpoint': data_endpoint,
451 'item_name': item_name,
452 'ignore_missing_dir': True,
453 'data_type': 'blog',
454 'items_per_page': posts_per_page,
455 'date_format': date_format,
456 'default_layout': default_layout
457 })
458 }),
459 'generators': collections.OrderedDict({
460 ('%s_archives' % blog_name): collections.OrderedDict({
461 'type': 'blog_archives',
462 'source': blog_name,
463 'page': year_archive
464 })
465 }),
466 'routes': [
467 {
468 'url': post_url,
469 'source': blog_name,
470 'func': ('%sposturl' % tpl_func_prefix)
471 },
472 {
473 'url': year_url,
474 'generator': ('%s_archives' % blog_name),
475 'func': ('%syearurl' % tpl_func_prefix)
476 }
477 ]
478 })
479 })
480
481 # Add a generator and a route for each taxonomy.
482 taxonomies_cfg = values.get('site', {}).get('taxonomies', {}).copy()
483 taxonomies_cfg.update(
484 user_overrides.get('site', {}).get('taxonomies', {}))
485 for tax_name, tax_cfg in taxonomies_cfg.items():
486 term = tax_cfg.get('term', tax_name)
487
488 # Generator.
489 page_ref = 'pages:%s_%s.%%ext%%' % (page_prefix, term)
490 if not theme_site:
491 theme_page_ref = values['site'].get('theme_%s_page' % term)
492 if theme_page_ref:
493 page_ref += ';' + theme_page_ref
494 tax_gen_name = '%s_%s' % (blog_name, tax_name)
495 tax_gen = collections.OrderedDict({
496 'type': 'taxonomy',
497 'source': blog_name,
498 'taxonomy': tax_name,
499 'page': page_ref
500 })
501 cfg['site']['generators'][tax_gen_name] = tax_gen
502
503 # Route.
504 tax_url_cfg_name = '%s_url' % term
505 tax_url = try_get_dict_values(
506 (blog_cfg, tax_url_cfg_name),
507 (user_overrides, 'site/%s' % tax_url_cfg_name),
508 (values, 'site/%s' % tax_url_cfg_name),
509 default=('%s/%%%s%%' % (term, term)))
510 tax_url = '/' + url_prefix + tax_url.lstrip('/')
511 tax_func_name = try_get_dict_values(
512 (user_overrides, 'site/taxonomies/%s/func_name' % tax_name),
513 (values, 'site/taxonomies/%s/func_name' % tax_name),
514 default=('%s%surl' % (tpl_func_prefix, term)))
515 tax_route = collections.OrderedDict({
516 'url': tax_url,
517 'generator': tax_gen_name,
518 'taxonomy': tax_name,
519 'func': tax_func_name
520 })
521 cfg['site']['routes'].append(tax_route)
522
523 return cfg
524 297
525 298
526 # Configuration value validators. 299 # Configuration value validators.
527 # 300 #
528 # Make sure we have basic site stuff. 301 # Make sure we have basic site stuff.