diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako
index 272aadce..6d9c2261 100644
--- a/tailbone/templates/configure.mako
+++ b/tailbone/templates/configure.mako
@@ -143,6 +143,68 @@
%def>
+<%def name="output_file_template_field(key)">
+ <% tmpl = output_file_templates[key] %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Click to upload
+
+
+
+ {{ outputFileTemplateUploads['${tmpl['key']}'].name }}
+
+
+
+
+
+
+%def>
+
+<%def name="output_file_templates_section()">
+
Output File Templates
+
+ % for key in output_file_templates:
+ ${self.output_file_template_field(key)}
+ % endfor
+
+%def>
+
<%def name="form_content()">%def>
<%def name="page_content()">
@@ -229,6 +291,7 @@
ThisPageData.settingsNeedSaved = false
ThisPageData.undoChanges = false
ThisPageData.savingSettings = false
+ ThisPageData.validators = []
ThisPage.methods.purgeSettingsInit = function() {
this.purgeSettingsShowDialog = true
@@ -260,7 +323,19 @@
}
ThisPage.methods.saveSettings = function() {
- let msg = this.validateSettings()
+ let msg
+
+ // nb. this is the future
+ for (let validator of this.validators) {
+ msg = validator.call(this)
+ if (msg) {
+ alert(msg)
+ return
+ }
+ }
+
+ // nb. legacy method
+ msg = this.validateSettings()
if (msg) {
alert(msg)
return
@@ -291,5 +366,35 @@
window.addEventListener('beforeunload', this.beforeWindowUnload)
}
+ ##############################
+ ## output file templates
+ ##############################
+
+ % if output_file_template_settings is not Undefined:
+
+ ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
+ ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
+ ThisPageData.outputFileTemplateUploads = {
+ % for key in output_file_templates:
+ '${key}': null,
+ % endfor
+ }
+
+ ThisPage.methods.validateOutputFileTemplateSettings = function() {
+ % for tmpl in output_file_templates.values():
+ if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
+ if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
+ if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
+ return "You must provide a file to upload for the ${tmpl['label']} template."
+ }
+ }
+ }
+ % endfor
+ }
+
+ ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
+
+ % endif
+
%def>
diff --git a/tailbone/templates/themes/waterpark/configure.mako b/tailbone/templates/themes/waterpark/configure.mako
index 9ac9a5cd..7a3e5261 100644
--- a/tailbone/templates/themes/waterpark/configure.mako
+++ b/tailbone/templates/themes/waterpark/configure.mako
@@ -1,2 +1,78 @@
## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/configure.mako" />
+<%namespace name="tailbone_base" file="tailbone:templates/configure.mako" />
+
+<%def name="input_file_templates_section()">
+ ${tailbone_base.input_file_templates_section()}
+%def>
+
+<%def name="output_file_templates_section()">
+ ${tailbone_base.output_file_templates_section()}
+%def>
+
+<%def name="modify_vue_vars()">
+ ${parent.modify_vue_vars()}
+
+%def>
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index a8365482..e4d6c3f6 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -117,6 +117,7 @@ class MasterView(View):
supports_prev_next = False
supports_import_batch_from_file = False
has_input_file_templates = False
+ has_output_file_templates = False
configurable = False
# set to True to add "View *global* Objects" permission, and
@@ -1820,6 +1821,26 @@ class MasterView(View):
path = os.path.join(basedir, filespec)
return self.file_response(path)
+ def download_output_file_template(self):
+ """
+ View for downloading an output file template.
+ """
+ key = self.request.GET['key']
+ filespec = self.request.GET['file']
+
+ matches = [tmpl for tmpl in self.get_output_file_templates()
+ if tmpl['key'] == key]
+ if not matches:
+ raise self.notfound()
+
+ template = matches[0]
+ templatesdir = os.path.join(self.rattail_config.datadir(),
+ 'templates', 'output_files',
+ self.get_route_prefix())
+ basedir = os.path.join(templatesdir, template['key'])
+ path = os.path.join(basedir, filespec)
+ return self.file_response(path)
+
def edit(self):
"""
View for editing an existing model record.
@@ -2848,6 +2869,12 @@ class MasterView(View):
kwargs['input_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
for tmpl in templates])
+ # add info for downloadable output file templates, if any
+ if self.has_output_file_templates:
+ templates = self.normalize_output_file_templates()
+ kwargs['output_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
+ for tmpl in templates])
+
return kwargs
def get_input_file_templates(self):
@@ -2922,6 +2949,81 @@ class MasterView(View):
return templates
+ def get_output_file_templates(self):
+ return []
+
+ def normalize_output_file_templates(self, templates=None,
+ include_file_options=False):
+ if templates is None:
+ templates = self.get_output_file_templates()
+
+ route_prefix = self.get_route_prefix()
+
+ if include_file_options:
+ templatesdir = os.path.join(self.rattail_config.datadir(),
+ 'templates', 'output_files',
+ route_prefix)
+
+ for template in templates:
+
+ if 'config_section' not in template:
+ if hasattr(self, 'output_file_template_config_section'):
+ template['config_section'] = self.output_file_template_config_section
+ else:
+ template['config_section'] = route_prefix
+ section = template['config_section']
+
+ if 'config_prefix' not in template:
+ template['config_prefix'] = '{}.{}'.format(
+ self.output_file_template_config_prefix,
+ template['key'])
+ prefix = template['config_prefix']
+
+ for key in ('mode', 'file', 'url'):
+
+ if 'option_{}'.format(key) not in template:
+ template['option_{}'.format(key)] = '{}.{}'.format(prefix, key)
+
+ if 'setting_{}'.format(key) not in template:
+ template['setting_{}'.format(key)] = '{}.{}'.format(
+ section,
+ template['option_{}'.format(key)])
+
+ if key not in template:
+ value = self.rattail_config.get(
+ section,
+ template['option_{}'.format(key)])
+ if value is not None:
+ template[key] = value
+
+ template.setdefault('mode', 'default')
+ template.setdefault('file', None)
+ template.setdefault('url', template['default_url'])
+
+ if include_file_options:
+ options = []
+ basedir = os.path.join(templatesdir, template['key'])
+ if os.path.exists(basedir):
+ for name in sorted(os.listdir(basedir)):
+ if len(name) == 4 and name.isdigit():
+ files = os.listdir(os.path.join(basedir, name))
+ if len(files) == 1:
+ options.append(os.path.join(name, files[0]))
+ template['file_options'] = options
+ template['file_options_dir'] = basedir
+
+ if template['mode'] == 'external':
+ template['effective_url'] = template['url']
+ elif template['mode'] == 'hosted':
+ template['effective_url'] = self.request.route_url(
+ '{}.download_output_file_template'.format(route_prefix),
+ _query={'key': template['key'],
+ 'file': template['file']})
+ else:
+ template['effective_url'] = template['default_url']
+
+ return templates
+
def template_kwargs_index(self, **kwargs):
"""
Method stub, so subclass can always invoke super() for it.
@@ -2969,6 +3071,12 @@ class MasterView(View):
items.append(tags.link_to(f"Download {template['label']} Template",
template['effective_url']))
+ if self.has_output_file_templates and self.has_perm('configure'):
+ templates = self.normalize_output_file_templates()
+ for template in templates:
+ items.append(tags.link_to(f"Download {template['label']} Template",
+ template['effective_url']))
+
# if self.viewing:
# # # TODO: either make this configurable, or just lose it.
@@ -5204,6 +5312,39 @@ class MasterView(View):
data[template['setting_file']] = os.path.join(numdir,
info['filename'])
+ if self.has_output_file_templates:
+ templatesdir = os.path.join(self.rattail_config.datadir(),
+ 'templates', 'output_files',
+ self.get_route_prefix())
+
+ def get_next_filedir(basedir):
+ nextid = 1
+ while True:
+ path = os.path.join(basedir, '{:04d}'.format(nextid))
+ if not os.path.exists(path):
+ # this should fail if there happens to be a race
+ # condition and someone else got to this id first
+ os.mkdir(path)
+ return path
+ nextid += 1
+
+ for template in self.normalize_output_file_templates():
+ key = '{}.upload'.format(template['setting_file'])
+ if key in uploads:
+ assert self.request.POST[template['setting_mode']] == 'hosted'
+ assert not self.request.POST[template['setting_file']]
+ info = uploads[key]
+ basedir = os.path.join(templatesdir, template['key'])
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+ filedir = get_next_filedir(basedir)
+ filepath = os.path.join(filedir, info['filename'])
+ shutil.copyfile(info['filepath'], filepath)
+ shutil.rmtree(info['filedir'])
+ numdir = os.path.basename(filedir)
+ data[template['setting_file']] = os.path.join(numdir,
+ info['filename'])
+
def configure_get_simple_settings(self):
"""
If you have some "simple" settings, each of which basically
@@ -5248,7 +5389,8 @@ class MasterView(View):
simple['option'])
def configure_get_context(self, simple_settings=None,
- input_file_templates=True):
+ input_file_templates=True,
+ output_file_templates=True):
"""
Returns the full context dict, for rendering the configure
page template.
@@ -5305,10 +5447,27 @@ class MasterView(View):
context['input_file_options'] = file_options
context['input_file_option_dirs'] = file_option_dirs
+ # add settings for output file templates, if any
+ if output_file_templates and self.has_output_file_templates:
+ settings = {}
+ file_options = {}
+ file_option_dirs = {}
+ for template in self.normalize_output_file_templates(
+ include_file_options=True):
+ settings[template['setting_mode']] = template['mode']
+ settings[template['setting_file']] = template['file']
+ settings[template['setting_url']] = template['url']
+ file_options[template['key']] = template['file_options']
+ file_option_dirs[template['key']] = template['file_options_dir']
+ context['output_file_template_settings'] = settings
+ context['output_file_options'] = file_options
+ context['output_file_option_dirs'] = file_option_dirs
+
return context
def configure_gather_settings(self, data, simple_settings=None,
- input_file_templates=True):
+ input_file_templates=True,
+ output_file_templates=True):
settings = []
# maybe collect "simple" settings
@@ -5354,10 +5513,30 @@ class MasterView(View):
settings.append({'name': template['setting_url'],
'value': data.get(template['setting_url'])})
+ # maybe also collect output file template settings
+ if output_file_templates and self.has_output_file_templates:
+ for template in self.normalize_output_file_templates():
+
+ # mode
+ settings.append({'name': template['setting_mode'],
+ 'value': data.get(template['setting_mode'])})
+
+ # file
+ value = data.get(template['setting_file'])
+ if value:
+ # nb. avoid saving if empty, so can remain "null"
+ settings.append({'name': template['setting_file'],
+ 'value': value})
+
+ # url
+ settings.append({'name': template['setting_url'],
+ 'value': data.get(template['setting_url'])})
+
return settings
def configure_remove_settings(self, simple_settings=None,
- input_file_templates=True):
+ input_file_templates=True,
+ output_file_templates=True):
app = self.get_rattail_app()
model = self.model
names = []
@@ -5376,6 +5555,14 @@ class MasterView(View):
template['setting_url'],
])
+ if output_file_templates and self.has_output_file_templates:
+ for template in self.normalize_output_file_templates():
+ names.extend([
+ template['setting_mode'],
+ template['setting_file'],
+ template['setting_url'],
+ ])
+
if names:
# nb. using thread-local session here; we do not use
# self.Session b/c it may not point to Rattail
@@ -5638,6 +5825,15 @@ class MasterView(View):
route_name='{}.download_input_file_template'.format(route_prefix),
permission='{}.create'.format(permission_prefix))
+ # download output file template
+ if cls.has_output_file_templates and cls.configurable:
+ config.add_route(f'{route_prefix}.download_output_file_template',
+ f'{url_prefix}/download-output-file-template')
+ config.add_view(cls, attr='download_output_file_template',
+ route_name=f'{route_prefix}.download_output_file_template',
+ # TODO: this is different from input file, should change?
+ permission=f'{permission_prefix}.configure')
+
# view
if cls.viewable:
cls._defaults_view(config)