OMG a ridiculous commit to overhaul import handler config etc.
- add `MasterView.configurable` concept, `/configure.mako` template - add new master view for DataSync Threads (needs content) - tweak view config for DataSync Changes accordingly - update the Configure DataSync page per `configurable` concept - add new Configure Import/Export page, per `configurable` - add basic views for Raw Permissions
This commit is contained in:
parent
282185c5af
commit
cc4b2278e7
175
tailbone/templates/configure.mako
Normal file
175
tailbone/templates/configure.mako
Normal file
|
@ -0,0 +1,175 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/page.mako" />
|
||||
|
||||
<%def name="title()">Configure ${config_title}</%def>
|
||||
|
||||
<%def name="save_undo_buttons()">
|
||||
<div class="buttons"
|
||||
v-if="settingsNeedSaved">
|
||||
<b-button type="is-primary"
|
||||
@click="saveSettings"
|
||||
:disabled="savingSettings"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ savingSettings ? "Working, please wait..." : "Save All Settings" }}
|
||||
</b-button>
|
||||
<once-button tag="a" href="${request.current_route_url()}"
|
||||
@click="undoChanges = true"
|
||||
icon-left="undo"
|
||||
text="Undo All Changes">
|
||||
</once-button>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="purge_button()">
|
||||
<b-button type="is-danger"
|
||||
@click="purgeSettingsInit()"
|
||||
icon-pack="fas"
|
||||
icon-left="trash">
|
||||
Remove All Settings
|
||||
</b-button>
|
||||
</%def>
|
||||
|
||||
<%def name="buttons_row()">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
|
||||
<div class="level-item">
|
||||
<p class="block">
|
||||
This tool lets you modify the ${config_title} configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="level-item">
|
||||
${self.save_undo_buttons()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
${self.purge_button()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
<br />
|
||||
|
||||
${self.buttons_row()}
|
||||
|
||||
<b-modal has-modal-card
|
||||
:active.sync="purgeSettingsShowDialog">
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Remove All Settings</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
If you like we can remove all ${config_title}
|
||||
settings from the DB.
|
||||
</p>
|
||||
<p class="block">
|
||||
Note that the tool normally removes all settings first,
|
||||
every time you click "Save Settings" - here though you can
|
||||
"just remove and not save" the settings.
|
||||
</p>
|
||||
<p class="block">
|
||||
Note also that this will of course
|
||||
<span class="is-italic">not</span> remove any settings from
|
||||
your config files, so after removing from DB,
|
||||
<span class="is-italic">only</span> your config file
|
||||
settings should be in effect.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="purgeSettingsShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
${h.form(request.current_route_url())}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('remove_settings', 'true')}
|
||||
<b-button type="is-danger"
|
||||
native-type="submit"
|
||||
:disabled="purgingSettings"
|
||||
icon-pack="fas"
|
||||
icon-left="trash"
|
||||
@click="purgingSettings = true">
|
||||
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
||||
</b-button>
|
||||
${h.end_form()}
|
||||
</footer>
|
||||
</div>
|
||||
</b-modal>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
ThisPageData.purgeSettingsShowDialog = false
|
||||
ThisPageData.purgingSettings = false
|
||||
|
||||
ThisPageData.settingsNeedSaved = false
|
||||
ThisPageData.undoChanges = false
|
||||
ThisPageData.savingSettings = false
|
||||
|
||||
ThisPage.methods.purgeSettingsInit = function() {
|
||||
this.purgeSettingsShowDialog = true
|
||||
}
|
||||
|
||||
ThisPage.methods.settingsCollectParams = function() {
|
||||
return {}
|
||||
}
|
||||
|
||||
ThisPage.methods.saveSettings = function() {
|
||||
this.savingSettings = true
|
||||
|
||||
let url = ${json.dumps(request.current_route_url())|n}
|
||||
let params = this.settingsCollectParams()
|
||||
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)
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -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'):
|
||||
<li>${h.link_to("View DataSync Threads", url('datasync'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/page.mako" />
|
||||
|
||||
<%def name="title()">Configure DataSync</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
<br />
|
||||
<%inherit file="/configure.mako" />
|
||||
|
||||
<%def name="buttons_row()">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
|
||||
<div class="level-item">
|
||||
<p class="block">
|
||||
This tool lets you modify the DataSync configuration.
|
||||
|
@ -19,24 +16,13 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="level-item buttons"
|
||||
v-if="settingsNeedSaved">
|
||||
<b-button type="is-primary"
|
||||
@click="saveSettings"
|
||||
:disabled="savingSettings"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
{{ saveSettingsButtonText }}
|
||||
</b-button>
|
||||
<once-button tag="a" href="${request.current_route_url()}"
|
||||
@click="undoChanges = true"
|
||||
icon-left="undo"
|
||||
text="Undo All Changes">
|
||||
</once-button>
|
||||
<div class="level-item">
|
||||
${self.save_undo_buttons()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
|
||||
<div class="level-item">
|
||||
${h.form(url('datasync.restart'), **{'@submit': 'submitRestartDatasyncForm'})}
|
||||
${h.csrf_token(request)}
|
||||
|
@ -50,56 +36,16 @@
|
|||
</b-button>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
|
||||
<div class="level-item">
|
||||
<b-button type="is-danger"
|
||||
@click="purgeSettingsInit()"
|
||||
icon-pack="fas"
|
||||
icon-left="trash">
|
||||
Remove All Settings
|
||||
</b-button>
|
||||
${self.purge_button()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<b-modal has-modal-card
|
||||
:active.sync="purgeSettingsShowDialog">
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Remove All Settings</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block">
|
||||
If you like we can remove all DataSync settings from the DB.
|
||||
</p>
|
||||
<p class="block">
|
||||
Note that this tool normally removes all settings first,
|
||||
every time you click "Save Settings". Here though you
|
||||
can "just remove" and <span class="is-italic">not</span>
|
||||
save the current settings.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="purgeSettingsShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
${h.form(request.current_route_url())}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('purge_settings', 'true')}
|
||||
<b-button type="is-danger"
|
||||
native-type="submit"
|
||||
:disabled="purgingSettings"
|
||||
icon-pack="fas"
|
||||
icon-left="trash"
|
||||
@click="purgingSettings = true">
|
||||
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
||||
</b-button>
|
||||
${h.end_form()}
|
||||
</footer>
|
||||
</div>
|
||||
</b-modal>
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
<b-notification type="is-warning"
|
||||
:active.sync="showConfigFilesNote">
|
||||
|
@ -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'):
|
||||
|
|
19
tailbone/templates/datasync/index.mako
Normal file
19
tailbone/templates/datasync/index.mako
Normal file
|
@ -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'):
|
||||
<li>${h.link_to("View DataSync Changes", url('datasyncchanges'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="render_grid_component()">
|
||||
<b-notification :closable="false">
|
||||
TODO: this page coming soon...
|
||||
</b-notification>
|
||||
${parent.render_grid_component()}
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
197
tailbone/templates/importing/configure.mako
Normal file
197
tailbone/templates/importing/configure.mako
Normal file
|
@ -0,0 +1,197 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/configure.mako" />
|
||||
|
||||
<%def name="page_content()">
|
||||
${parent.page_content()}
|
||||
|
||||
<h3 class="is-size-3">Designated Handlers</h3>
|
||||
|
||||
<b-table :data="handlersData"
|
||||
narrowed
|
||||
icon-pack="fas"
|
||||
:default-sort="['host_title', 'asc']">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="host_title" label="Data Source" sortable>
|
||||
{{ props.row.host_title }}
|
||||
</b-table-column>
|
||||
<b-table-column field="local_title" label="Data Target" sortable>
|
||||
{{ props.row.local_title }}
|
||||
</b-table-column>
|
||||
<b-table-column field="direction" label="Direction" sortable>
|
||||
{{ props.row.direction_display }}
|
||||
</b-table-column>
|
||||
<b-table-column field="handler_spec" label="Handler Spec" sortable>
|
||||
{{ props.row.handler_spec }}
|
||||
</b-table-column>
|
||||
<b-table-column field="cmd" label="Command" sortable>
|
||||
{{ props.row.command }} {{ props.row.subcommand }}
|
||||
</b-table-column>
|
||||
<b-table-column field="runas" label="Default Runas" sortable>
|
||||
{{ props.row.default_runas }}
|
||||
</b-table-column>
|
||||
<b-table-column label="Actions">
|
||||
<a href="#" class="grid-action"
|
||||
@click.prevent="editHandler(props.row)">
|
||||
<i class="fas fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>
|
||||
<b-icon
|
||||
pack="fas"
|
||||
icon="fas fa-sad-tear"
|
||||
size="is-large">
|
||||
</b-icon>
|
||||
</p>
|
||||
<p>Nothing here.</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-modal :active.sync="editHandlerShowDialog">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<b-field :label="editingHandlerDirection" horizontal expanded>
|
||||
{{ editingHandlerHostTitle }} -> {{ editingHandlerLocalTitle }}
|
||||
</b-field>
|
||||
|
||||
<b-field label="Handler Spec"
|
||||
:type="editingHandlerSpec ? null : 'is-danger'">
|
||||
<b-select v-model="editingHandlerSpec">
|
||||
<option v-for="option in editingHandlerSpecOptions"
|
||||
:key="option"
|
||||
:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="Command"
|
||||
:type="editingHandlerCommand ? null : 'is-danger'">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item" style="margin-right: 0;">
|
||||
bin/
|
||||
</div>
|
||||
<div class="level-item" style="margin-left: 0;">
|
||||
<b-input v-model="editingHandlerCommand">
|
||||
</b-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Subcommand"
|
||||
:type="editingHandlerSubcommand ? null : 'is-danger'">
|
||||
<b-input v-model="editingHandlerSubcommand">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Default Runas">
|
||||
<b-input v-model="editingHandlerRunas">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-button @click="editHandlerShowDialog = false"
|
||||
class="control">
|
||||
Cancel
|
||||
</b-button>
|
||||
|
||||
<b-button type="is-primary"
|
||||
class="control"
|
||||
@click="updateHandler()"
|
||||
:disabled="updateHandlerDisabled">
|
||||
Update Handler
|
||||
</b-button>
|
||||
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
ThisPageData.handlersData = ${json.dumps(handlers_data)|n}
|
||||
|
||||
ThisPageData.editHandlerShowDialog = false
|
||||
ThisPageData.editingHandler = null
|
||||
ThisPageData.editingHandlerHostTitle = null
|
||||
ThisPageData.editingHandlerLocalTitle = null
|
||||
ThisPageData.editingHandlerDirection = 'import'
|
||||
ThisPageData.editingHandlerSpec = null
|
||||
ThisPageData.editingHandlerSpecOptions = []
|
||||
ThisPageData.editingHandlerCommand = null
|
||||
ThisPageData.editingHandlerSubcommand = null
|
||||
ThisPageData.editingHandlerRunas = null
|
||||
|
||||
ThisPageData.settingsNeedSaved = false
|
||||
ThisPageData.undoChanges = false
|
||||
ThisPageData.savingSettings = false
|
||||
|
||||
ThisPage.computed.updateHandlerDisabled = function() {
|
||||
if (!this.editingHandlerSpec) {
|
||||
return true
|
||||
}
|
||||
if (!this.editingHandlerCommand) {
|
||||
return true
|
||||
}
|
||||
if (!this.editingHandlerSubcommand) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
ThisPage.methods.editHandler = function(row) {
|
||||
this.editingHandler = row
|
||||
|
||||
this.editingHandlerHostTitle = row.host_title
|
||||
this.editingHandlerLocalTitle = row.local_title
|
||||
this.editingHandlerDirection = row.direction_display
|
||||
this.editingHandlerSpec = row.handler_spec
|
||||
this.editingHandlerSpecOptions = row.spec_options
|
||||
this.editingHandlerCommand = row.command
|
||||
this.editingHandlerSubcommand = row.subcommand
|
||||
this.editingHandlerRunas = row.default_runas
|
||||
|
||||
this.editHandlerShowDialog = true
|
||||
}
|
||||
|
||||
ThisPage.methods.updateHandler = function() {
|
||||
let row = this.editingHandler
|
||||
|
||||
row.handler_spec = this.editingHandlerSpec
|
||||
row.command = this.editingHandlerCommand
|
||||
row.subcommand = this.editingHandlerSubcommand
|
||||
row.default_runas = this.editingHandlerRunas
|
||||
|
||||
this.settingsNeedSaved = true
|
||||
this.editHandlerShowDialog = false
|
||||
}
|
||||
|
||||
ThisPage.methods.settingsCollectParams = function() {
|
||||
return {
|
||||
handlers: this.handlersData,
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -162,6 +162,9 @@
|
|||
<li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li>
|
||||
% endif
|
||||
% endif
|
||||
% if master.configurable and master.has_perm('configure'):
|
||||
<li>${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="grid_tools()">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,8 +127,32 @@ 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',
|
||||
|
|
|
@ -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),
|
||||
|
|
58
tailbone/views/permissions.py
Normal file
58
tailbone/views/permissions.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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)
|
Loading…
Reference in a new issue