diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako new file mode 100644 index 00000000..b0bfb14e --- /dev/null +++ b/tailbone/templates/configure.mako @@ -0,0 +1,175 @@ +## -*- coding: utf-8; -*- +<%inherit file="/page.mako" /> + +<%def name="title()">Configure ${config_title} + +<%def name="save_undo_buttons()"> +
+ + {{ savingSettings ? "Working, please wait..." : "Save All Settings" }} + + + +
+ + +<%def name="purge_button()"> + + Remove All Settings + + + +<%def name="buttons_row()"> +
+
+ +
+

+ This tool lets you modify the ${config_title} configuration. +

+
+ +
+ ${self.save_undo_buttons()} +
+
+ +
+
+ ${self.purge_button()} +
+
+
+ + +<%def name="page_content()"> + ${parent.page_content()} + +
+ + ${self.buttons_row()} + + + + + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/datasync/changes/index.mako b/tailbone/templates/datasync/changes/index.mako index c28076fe..7a79010f 100644 --- a/tailbone/templates/datasync/changes/index.mako +++ b/tailbone/templates/datasync/changes/index.mako @@ -3,8 +3,8 @@ <%def name="context_menu_items()"> ${parent.context_menu_items()} - % if master.has_perm('configure'): - ${h.link_to("Configure DataSync", url('datasync.configure'))} + % if request.has_perm('datasync.list'): +
  • ${h.link_to("View DataSync Threads", url('datasync'))}
  • % endif diff --git a/tailbone/templates/datasync/configure.mako b/tailbone/templates/datasync/configure.mako index 161328f7..0bed21e3 100644 --- a/tailbone/templates/datasync/configure.mako +++ b/tailbone/templates/datasync/configure.mako @@ -1,13 +1,10 @@ ## -*- coding: utf-8; -*- -<%inherit file="/page.mako" /> - -<%def name="title()">Configure DataSync - -<%def name="page_content()"> -
    +<%inherit file="/configure.mako" /> +<%def name="buttons_row()">
    +

    This tool lets you modify the DataSync configuration.  @@ -19,24 +16,13 @@

    -
    - - {{ saveSettingsButtonText }} - - - +
    + ${self.save_undo_buttons()}
    +
    ${h.form(url('datasync.restart'), **{'@submit': 'submitRestartDatasyncForm'})} ${h.csrf_token(request)} @@ -50,56 +36,16 @@ ${h.end_form()}
    +
    - - Remove All Settings - + ${self.purge_button()}
    + - - - +<%def name="page_content()"> + ${parent.page_content()} @@ -496,13 +442,6 @@ ThisPageData.restartCommand = ${json.dumps(restart_command)|n} - ThisPageData.purgeSettingsShowDialog = false - ThisPageData.purgingSettings = false - - ThisPageData.settingsNeedSaved = false - ThisPageData.undoChanges = false - ThisPageData.savingSettings = false - ThisPage.computed.filteredProfilesData = function() { if (this.showDisabledProfiles) { return this.profilesData @@ -539,13 +478,6 @@ return false } - ThisPage.computed.saveSettingsButtonText = function() { - if (this.savingSettings) { - return "Working, please wait..." - } - return "Save All Settings" - } - ThisPage.methods.toggleDisabledProfiles = function() { this.showDisabledProfiles = !this.showDisabledProfiles } @@ -743,53 +675,11 @@ } } - ThisPage.methods.purgeSettingsInit = function() { - this.purgeSettingsShowDialog = true - } - - ThisPage.methods.saveSettings = function() { - this.savingSettings = true - let url = ${json.dumps(request.current_route_url())|n} - - let params = { + ThisPage.methods.settingsCollectParams = function() { + return { profiles: this.profilesData, restart_command: this.restartCommand, } - - let headers = { - 'X-CSRF-TOKEN': this.csrftoken, - } - - this.$http.post(url, params, {headers: headers}).then((response) => { - if (response.data.success) { - this.settingsNeedSaved = false - location.href = url // reload page - } else { - this.$buefy.toast.open({ - message: "Save failed: " + (response.data.error || "(unknown error)"), - type: 'is-danger', - duration: 4000, // 4 seconds - }) - } - }).catch((error) => { - this.$buefy.toast.open({ - message: "Save failed: (unknown error)", - type: 'is-danger', - duration: 4000, // 4 seconds - }) - }) - } - - // cf. https://stackoverflow.com/a/56551646 - ThisPage.methods.beforeWindowUnload = function(e) { - if (this.settingsNeedSaved && !this.undoChanges) { - e.preventDefault() - e.returnValue = '' - } - } - - ThisPage.created = function() { - window.addEventListener('beforeunload', this.beforeWindowUnload) } % if request.has_perm('datasync.restart'): diff --git a/tailbone/templates/datasync/index.mako b/tailbone/templates/datasync/index.mako new file mode 100644 index 00000000..fd7c39c6 --- /dev/null +++ b/tailbone/templates/datasync/index.mako @@ -0,0 +1,19 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/index.mako" /> + +<%def name="context_menu_items()"> + ${parent.context_menu_items()} + % if request.has_perm('datasync_changes.list'): +
  • ${h.link_to("View DataSync Changes", url('datasyncchanges'))}
  • + % endif + + +<%def name="render_grid_component()"> + + TODO: this page coming soon... + + ${parent.render_grid_component()} + + + +${parent.body()} diff --git a/tailbone/templates/importing/configure.mako b/tailbone/templates/importing/configure.mako new file mode 100644 index 00000000..462a5215 --- /dev/null +++ b/tailbone/templates/importing/configure.mako @@ -0,0 +1,197 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="page_content()"> + ${parent.page_content()} + +

    Designated Handlers

    + + + + + + + +
    +
    + + + {{ editingHandlerHostTitle }} -> {{ editingHandlerLocalTitle }} + + + + + + + + + + + +
    +
    +
    + bin/ +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + + +
    + + + + + Cancel + + + + Update Handler + + + + +
    +
    +
    + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 8e855422..f58a59d1 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -162,6 +162,9 @@
  • ${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
  • % endif % endif + % if master.configurable and master.has_perm('configure'): +
  • ${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}
  • + % endif <%def name="grid_tools()"> diff --git a/tailbone/views/datasync.py b/tailbone/views/datasync.py index 0fe1e709..cff9553f 100644 --- a/tailbone/views/datasync.py +++ b/tailbone/views/datasync.py @@ -32,54 +32,44 @@ import logging from rattail.db import model from rattail.datasync.config import load_profiles -from rattail.datasync.util import get_lastrun, purge_datasync_settings +from rattail.datasync.util import purge_datasync_settings from tailbone.views import MasterView -from tailbone.util import csrf_token log = logging.getLogger(__name__) -class DataSyncChangeView(MasterView): +class DataSyncThreadView(MasterView): """ - Master view for the DataSyncChange model. + Master view for DataSync itself. + + This should (eventually) show all running threads in the main + index view, with status for each, sort of akin to "dashboard". + For now it only serves the config view. """ - model_class = model.DataSyncChange - url_prefix = '/datasync/changes' - permission_prefix = 'datasync' + normalized_model_name = 'datasyncthread' + model_title = "DataSync Thread" + model_key = 'key' + route_prefix = 'datasync' + url_prefix = '/datasync' + viewable = False creatable = False editable = False - bulk_deletable = True + deletable = False + filterable = False + pageable = False - labels = { - 'batch_id': "Batch ID", - } + configurable = True + config_title = "DataSync" grid_columns = [ - 'source', - 'batch_id', - 'batch_sequence', - 'payload_type', - 'payload_key', - 'deletion', - 'obtained', - 'consumer', + 'key', ] - def configure_grid(self, g): - super(DataSyncChangeView, self).configure_grid(g) - - # batch_sequence - g.set_label('batch_sequence', "Batch Seq.") - g.filters['batch_sequence'].label = "Batch Sequence" - - g.set_sort_defaults('obtained') - g.set_type('obtained', 'datetime') - - def template_kwargs_index(self, **kwargs): - kwargs['allow_filemon_restart'] = bool(self.rattail_config.get('tailbone', 'filemon.restart')) - return kwargs + def get_data(self, session=None): + data = [] + return data def restart(self): cmd = self.rattail_config.getlist('tailbone', 'datasync.restart', @@ -93,23 +83,7 @@ class DataSyncChangeView(MasterView): self.request.session.flash("DataSync daemon could not be restarted; result was: {}".format(result), 'error') return self.redirect(self.request.get_referrer(default=self.request.route_url('datasyncchanges'))) - def configure(self): - """ - View for configuring the DataSync daemon. - """ - if self.request.method == 'POST': - # if self.request.is_xhr and not self.request.POST: - if self.request.POST.get('purge_settings'): - self.delete_settings() - self.request.session.flash("Settings have been removed.") - return self.redirect(self.request.current_route_url()) - else: - data = self.request.json_body - self.save_settings(data) - self.request.session.flash("Settings have been saved. " - "You should probably restart DataSync now.") - return self.json_response({'success': True}) - + def configure_get_context(self): profiles = load_profiles(self.rattail_config, include_disabled=True, ignore_problems=True) @@ -148,27 +122,21 @@ class DataSyncChangeView(MasterView): profiles_data.append(data) return { - 'master': self, - # TODO: really only buefy themes are supported here - 'use_buefy': self.get_use_buefy(), - 'index_title': "DataSync Changes", - 'index_url': self.get_index_url(), 'profiles': profiles, 'profiles_data': profiles_data, 'restart_command': self.rattail_config.get('tailbone', 'datasync.restart'), 'system_user': getpass.getuser(), } - def save_settings(self, data): - model = self.model - - # collect new settings + def configure_gather_settings(self, data): settings = [] watch = [] + for profile in data['profiles']: pkey = profile['key'] if profile['enabled']: watch.append(pkey) + settings.extend([ {'name': 'rattail.datasync.{}.watcher'.format(pkey), 'value': profile['watcher_spec']}, @@ -183,10 +151,12 @@ class DataSyncChangeView(MasterView): {'name': 'rattail.datasync.{}.consumers.runas'.format(pkey), 'value': profile['watcher_default_runas']}, ]) + consumers = [] if profile['watcher_consumes_self']: consumers = ['self'] else: + for consumer in profile['consumers_data']: ckey = consumer['key'] if consumer['enabled']: @@ -205,10 +175,12 @@ class DataSyncChangeView(MasterView): {'name': 'rattail.datasync.{}.consumer.{}.runas'.format(pkey, ckey), 'value': consumer['consumer_runas']}, ]) + settings.extend([ {'name': 'rattail.datasync.{}.consumers'.format(pkey), 'value': ', '.join(consumers)}, ]) + settings.extend([ {'name': 'rattail.datasync.watch', 'value': ', '.join(watch)}, @@ -216,15 +188,9 @@ class DataSyncChangeView(MasterView): 'value': data['restart_command']}, ]) - # delete all current settings - self.delete_settings() + return settings - # create all new settings - for setting in settings: - self.Session.add(model.Setting(name=setting['name'], - value=setting['value'])) - - def delete_settings(self): + def configure_remove_settings(self): purge_datasync_settings(self.rattail_config, self.Session()) @classmethod @@ -235,33 +201,65 @@ class DataSyncChangeView(MasterView): @classmethod def _datasync_defaults(cls, config): permission_prefix = cls.get_permission_prefix() + route_prefix = cls.get_route_prefix() + url_prefix = cls.get_url_prefix() - # fix permission group title - config.add_tailbone_permission_group(permission_prefix, label="DataSync") - - # restart datasync + # restart config.add_tailbone_permission(permission_prefix, '{}.restart'.format(permission_prefix), label="Restart the DataSync daemon") - config.add_route('datasync.restart', '/datasync/restart', + config.add_route('{}.restart'.format(route_prefix), + '{}/restart'.format(url_prefix), request_method='POST') config.add_view(cls, attr='restart', - route_name='datasync.restart', + route_name='{}.restart'.format(route_prefix), permission='{}.restart'.format(permission_prefix)) - # configure datasync - config.add_tailbone_permission(permission_prefix, - '{}.configure'.format(permission_prefix), - label="Configure the DataSync daemon") - config.add_route('datasync.configure', '/datasync/configure') - config.add_view(cls, attr='configure', - route_name='datasync.configure', - permission='{}.configure'.format(permission_prefix), - renderer='/datasync/configure.mako') + +class DataSyncChangeView(MasterView): + """ + Master view for the DataSyncChange model. + """ + model_class = model.DataSyncChange + url_prefix = '/datasync/changes' + permission_prefix = 'datasync_changes' + creatable = False + editable = False + bulk_deletable = True + + labels = { + 'batch_id': "Batch ID", + } + + grid_columns = [ + 'source', + 'batch_id', + 'batch_sequence', + 'payload_type', + 'payload_key', + 'deletion', + 'obtained', + 'consumer', + ] + + def configure_grid(self, g): + super(DataSyncChangeView, self).configure_grid(g) + + # batch_sequence + g.set_label('batch_sequence', "Batch Seq.") + g.filters['batch_sequence'].label = "Batch Sequence" + + g.set_sort_defaults('obtained') + g.set_type('obtained', 'datetime') + + def template_kwargs_index(self, **kwargs): + kwargs['allow_filemon_restart'] = bool(self.rattail_config.get('tailbone', 'filemon.restart')) + return kwargs # TODO: deprecate / remove this DataSyncChangesView = DataSyncChangeView def includeme(config): + DataSyncThreadView.defaults(config) DataSyncChangeView.defaults(config) diff --git a/tailbone/views/importing.py b/tailbone/views/importing.py index 23a039cd..80f54c37 100644 --- a/tailbone/views/importing.py +++ b/tailbone/views/importing.py @@ -35,6 +35,7 @@ import time import json import six +import sqlalchemy as sa from rattail.exceptions import ConfigurationError from rattail.threads import Thread @@ -66,14 +67,19 @@ class ImportingView(MasterView): filterable = False pageable = False + configurable = True + config_title = "Import / Export" + labels = { 'host_title': "Data Source", 'local_title': "Data Target", + 'direction_display': "Direction", } grid_columns = [ 'host_title', 'local_title', + 'direction_display', 'handler_spec', ] @@ -84,6 +90,7 @@ class ImportingView(MasterView): 'handler_spec', 'host_title', 'local_title', + 'direction_display', 'models', ] @@ -105,18 +112,14 @@ class ImportingView(MasterView): app = self.get_rattail_app() data = [] - for Handler in app.all_import_handlers(): - handler = Handler(self.rattail_config) + for handler in app.get_designated_import_handlers( + ignore_errors=True, sort=True): data.append(self.normalize(handler)) - data.sort(key=lambda handler: (handler['host_title'], - handler['local_title'])) return data - def normalize(self, handler): - Handler = handler.__class__ - return { - '_handler': handler, + def normalize(self, handler, keep_handler=True): + data = { 'key': handler.get_key(), 'generic_title': handler.get_generic_title(), 'host_key': handler.host_key, @@ -124,7 +127,31 @@ class ImportingView(MasterView): 'local_key': handler.local_key, 'local_title': handler.get_generic_local_title(), 'handler_spec': handler.get_spec(), - } + 'direction': handler.direction, + 'direction_display': handler.direction.capitalize(), + } + + if keep_handler: + data['_handler'] = handler + + alternates = getattr(handler, 'alternate_handlers', None) + if alternates: + data['alternates'] = [] + for alternate in alternates: + data['alternates'].append(self.normalize( + alternate, keep_handler=keep_handler)) + + cmd = self.get_cmd_for_handler(handler, ignore_errors=True) + if cmd: + data['cmd'] = ' '.join(cmd) + data['command'] = cmd[0] + data['subcommand'] = cmd[1] + + runas = self.get_runas_for_handler(handler) + if runas: + data['default_runas'] = runas + + return data def configure_grid(self, g): super(ImportingView, self).configure_grid(g) @@ -139,9 +166,9 @@ class ImportingView(MasterView): """ key = self.request.matchdict['key'] app = self.get_rattail_app() - for Handler in app.all_import_handlers(): - if Handler.get_key() == key: - return self.normalize(Handler(self.rattail_config)) + handler = app.get_designated_import_handler(key, ignore_errors=True) + if handler: + return self.normalize(handler) raise self.notfound() def get_instance_title(self, handler_info): @@ -206,8 +233,8 @@ class ImportingView(MasterView): def cache_runjob_form_values(self, handler, form): handler_key = handler.get_key() - def make_key(key): - return 'rattail.importing.{}.{}'.format(handler_key, key) + def make_key(field): + return 'rattail.importing.{}.{}'.format(handler_key, field) for field in form.fields: key = make_key(field) @@ -216,8 +243,8 @@ class ImportingView(MasterView): def read_cached_runjob_values(self, handler, form): handler_key = handler.get_key() - def make_key(key): - return 'rattail.importing.{}.{}'.format(handler_key, key) + def make_key(field): + return 'rattail.importing.{}.{}'.format(handler_key, field) for field in form.fields: key = make_key(field) @@ -331,8 +358,10 @@ class ImportingView(MasterView): # invoke handler command via subprocess try: - result = subprocess.run(cmd, check=True, capture_output=True) - output = result.stderr.decode('utf_8').strip() + result = subprocess.run(cmd, check=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + output = result.stdout.decode('utf_8').strip() except Exception as error: log.warning("failed to invoke handler cmd: %s", cmd, exc_info=True) @@ -346,14 +375,14 @@ class ImportingView(MasterView): {} ``` -And here is the STDERR output: +And here is the output: ``` {} ``` """.format(handler.direction.capitalize(), ' '.join(cmd), - error.stderr.decode('utf_8').strip()) + error.stdout.decode('utf_8').strip()) msg = markdown.markdown(msg, extensions=['fenced_code']) msg = HTML.literal(msg) msg = HTML.tag('div', class_='tailbone-markdown', c=[msg]) @@ -394,21 +423,35 @@ And here is the STDERR output: notes = HTML.literal(notes) return HTML.tag('div', class_='tailbone-markdown', c=[notes]) - def make_runjob_cmd(self, handler, form, typ, port=None): + def get_cmd_for_handler(self, handler, ignore_errors=False): handler_key = handler.get_key() - option = '{}.cmd'.format(handler_key) - cmd = self.rattail_config.getlist('rattail.importing', option) + cmd = self.rattail_config.getlist('rattail.importing', + '{}.cmd'.format(handler_key)) if not cmd or len(cmd) != 2: - msg = ("Missing or invalid config; please set '{}' in the " - "[rattail.importing] section of your config file".format(option)) - raise ConfigurationError(msg) + cmd = self.rattail_config.getlist('rattail.importing', + '{}.default_cmd'.format(handler_key)) - command, subcommand = cmd + if not cmd or len(cmd) != 2: + msg = ("Missing or invalid config; please set '{}.default_cmd' in the " + "[rattail.importing] section of your config file".format(handler_key)) + if ignore_errors: + return + raise ConfigurationError(msg) - option = '{}.runas'.format(handler_key) - runas = self.rattail_config.require('rattail.importing', option) + return cmd + def get_runas_for_handler(self, handler): + handler_key = handler.get_key() + runas = self.rattail_config.get('rattail.importing', + '{}.runas'.format(handler_key)) + if runas: + return runas + return self.rattail_config.get('rattail', 'runas.default') + + def make_runjob_cmd(self, handler, form, typ, port=None): + command, subcommand = self.get_cmd_for_handler(handler) + runas = self.get_runas_for_handler(handler) data = form.validated if typ == 'true': @@ -460,7 +503,10 @@ And here is the STDERR output: cmd.append('--dry-run') if data['warnings']: - cmd.append('--warnings') + if typ == 'true': + cmd.append('--warnings') + else: + cmd.append('-W') return cmd @@ -479,6 +525,54 @@ cd {prefix} self.request.session['rattail.importing.runjob.notes'] = markdown.markdown( notes, extensions=['fenced_code', 'codehilite']) + def configure_get_context(self): + app = self.get_rattail_app() + handlers_data = [] + + for handler in app.get_designated_import_handlers( + with_alternates=True, + ignore_errors=True, sort=True): + + data = self.normalize(handler, keep_handler=False) + + data['spec_options'] = [handler.get_spec()] + for alternate in handler.alternate_handlers: + data['spec_options'].append(alternate.get_spec()) + data['spec_options'].sort() + + handlers_data.append(data) + + return { + 'handlers_data': handlers_data, + } + + def configure_gather_settings(self, data): + settings = [] + + for handler in data['handlers']: + key = handler['key'] + + settings.extend([ + {'name': 'rattail.importing.{}.handler'.format(key), + 'value': handler['handler_spec']}, + {'name': 'rattail.importing.{}.cmd'.format(key), + 'value': '{} {}'.format(handler['command'], + handler['subcommand'])}, + {'name': 'rattail.importing.{}.runas'.format(key), + 'value': handler['default_runas']}, + ]) + + return settings + + def configure_remove_settings(self): + model = self.model + self.Session.query(model.Setting)\ + .filter(sa.or_( + model.Setting.name.like('rattail.importing.%.handler'), + model.Setting.name.like('rattail.importing.%.cmd'), + model.Setting.name.like('rattail.importing.%.runas')))\ + .delete(synchronize_session=False) + @classmethod def defaults(cls, config): cls._defaults(config) @@ -493,7 +587,7 @@ cd {prefix} # run job config.add_tailbone_permission(permission_prefix, '{}.runjob'.format(permission_prefix), - "Run an arbitrary import / export job") + "Run an arbitrary Import / Export Job") config.add_route('{}.runjob'.format(route_prefix), '{}/runjob'.format(instance_url_prefix)) config.add_view(cls, attr='runjob', diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 2a3189c4..cdd958a0 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -114,6 +114,7 @@ class MasterView(View): execute_progress_initial_msg = None supports_prev_next = False supports_import_batch_from_file = False + configurable = False # set to True to add "View *global* Objects" permission, and # expose / leverage the ``local_only`` object flag @@ -2032,6 +2033,16 @@ class MasterView(View): """ return getattr(cls, 'index_title', cls.get_model_title_plural()) + @classmethod + def get_config_title(cls): + """ + Returns the view's "config title". + """ + if hasattr(cls, 'config_title'): + return cls.config_title + + return cls.get_model_title_plural() + def get_action_url(self, action, instance, **kwargs): """ Generate a URL for the given action on the given instance @@ -2075,6 +2086,7 @@ class MasterView(View): 'permission_prefix': self.get_permission_prefix(), 'index_title': self.get_index_title(), 'index_url': self.get_index_url(), + 'config_title': self.get_config_title(), 'action_url': self.get_action_url, 'grid_index': self.grid_index, 'help_url': self.get_help_url(), @@ -3982,7 +3994,46 @@ class MasterView(View): return diffs.Diff(old_data, new_data, **kwargs) ############################## - # Config Stuff + # Configuration Views + ############################## + + def configure(self): + """ + Generic view for configuring some aspect of the software. + """ + if self.request.method == 'POST': + if self.request.POST.get('remove_settings'): + self.configure_remove_settings() + self.request.session.flash("Settings have been removed.") + return self.redirect(self.request.current_route_url()) + else: + data = self.request.json_body + settings = self.configure_gather_settings(data) + self.configure_remove_settings() + self.configure_save_settings(settings) + self.request.session.flash("Settings have been saved.") + return self.json_response({'success': True}) + + context = self.configure_get_context() + return self.render_to_response('configure', context) + + def configure_get_context(self): + return {} + + def configure_gather_settings(self, data): + return [] + + def configure_remove_settings(self): + pass + + def configure_save_settings(self, settings): + model = self.model + for setting in settings: + self.Session.add(model.Setting(name=setting['name'], + value=setting['value'])) + + ############################## + # Pyramid View Config ############################## @classmethod @@ -4025,6 +4076,7 @@ class MasterView(View): model_key = cls.get_model_key() model_title = cls.get_model_title() model_title_plural = cls.get_model_title_plural() + config_title = cls.get_config_title() if cls.has_rows: row_model_title = cls.get_row_model_title() @@ -4087,6 +4139,17 @@ class MasterView(View): config.add_view(cls, attr='download_results_rows', route_name='{}.download_results_rows'.format(route_prefix), permission='{}.download_results_rows'.format(permission_prefix)) + # configure + if cls.configurable: + config.add_tailbone_permission(permission_prefix, + '{}.configure'.format(permission_prefix), + label="Configure {}".format(config_title)) + config.add_route('{}.configure'.format(route_prefix), + '{}/configure'.format(url_prefix)) + config.add_view(cls, attr='configure', + route_name='{}.configure'.format(route_prefix), + permission='{}.configure'.format(permission_prefix)) + # quickie (search) if cls.supports_quickie_search: config.add_tailbone_permission(permission_prefix, '{}.quickie'.format(permission_prefix), diff --git a/tailbone/views/permissions.py b/tailbone/views/permissions.py new file mode 100644 index 00000000..67f6e9b1 --- /dev/null +++ b/tailbone/views/permissions.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2021 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Raw Permission Views +""" + +from __future__ import unicode_literals, absolute_import + +from sqlalchemy import orm + +from rattail.db import model + +from tailbone.views import MasterView + + +class PermissionView(MasterView): + """ + Master view for the permissions model. + """ + model_class = model.Permission + model_title = "Raw Permission" + editable = False + bulk_deletable = True + + grid_columns = [ + 'role', + 'permission', + ] + + def query(self, session): + model = self.model + query = super(PermissionView, self).query(session) + query = query.options(orm.joinedload(model.Permission.role)) + return query + + +def includeme(config): + PermissionView.defaults(config)