feat: add "has output file templates" config option for master view

this is a bit hacky, a quick copy/paste job from the equivalent
feature for input file templates.

i assume this will get cleaned up when moved to wuttaweb..
This commit is contained in:
Lance Edgar 2024-08-20 19:09:56 -05:00
parent 07871188aa
commit 1def26a35b
3 changed files with 381 additions and 4 deletions

View file

@ -143,6 +143,68 @@
</div>
</%def>
<%def name="output_file_template_field(key)">
<% tmpl = output_file_templates[key] %>
<b-field grouped>
<b-field label="${tmpl['label']}">
<b-select name="${tmpl['setting_mode']}"
v-model="outputFileTemplateSettings['${tmpl['setting_mode']}']"
@input="settingsNeedSaved = true">
<option value="default">use default</option>
<option value="hosted">use uploaded file</option>
</b-select>
</b-field>
<b-field label="File"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted'"
:message="outputFileTemplateSettings['${tmpl['setting_file']}'] ? 'This file lives on disk at: ${output_file_option_dirs[tmpl['key']]}' : null">
<b-select name="${tmpl['setting_file']}"
v-model="outputFileTemplateSettings['${tmpl['setting_file']}']"
@input="settingsNeedSaved = true">
<option :value="null">-new-</option>
<option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']"
:key="option"
:value="option">
{{ option }}
</option>
</b-select>
</b-field>
<b-field label="Upload"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']">
<b-field class="file is-primary"
:class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="outputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="outputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
</b-field>
</b-field>
</%def>
<%def name="output_file_templates_section()">
<h3 class="block is-size-3">Output File Templates</h3>
<div class="block" style="padding-left: 2rem;">
% for key in output_file_templates:
${self.output_file_template_field(key)}
% endfor
</div>
</%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
</script>
</%def>

View file

@ -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()}
<script>
##############################
## input file templates
##############################
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
% endif
##############################
## 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
</script>
</%def>

View file

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