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 name="output_file_template_field(key)"> + <% tmpl = output_file_templates[key] %> + + + + + + + + + + + + + + + + + + + + + + + Click to upload + + + + {{ outputFileTemplateUploads['${tmpl['key']}'].name }} + + + + + + + + +<%def name="output_file_templates_section()"> +

Output File Templates

+
+ % for key in output_file_templates: + ${self.output_file_template_field(key)} + % endfor +
+ + <%def name="form_content()"> <%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 + 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 name="output_file_templates_section()"> + ${tailbone_base.output_file_templates_section()} + + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + + 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)