tailbone/tailbone/templates/master/index.mako
Lance Edgar fa25857680 Let master view control context menu items for page
that really does not belong in the template if we can help it.  some
templates still define context menu items but can hopefully phase
those out over time
2024-06-02 15:54:42 -05:00

637 lines
24 KiB
Mako

## -*- coding: utf-8; -*-
## ##############################################################################
##
## Default master 'index' template. Features a prominent data table and
## exposes a way to filter and sort the data, etc. Some index pages also
## include a "tools" section, just above the grid on the right.
##
## ##############################################################################
<%inherit file="/page.mako" />
<%def name="title()">${index_title}</%def>
<%def name="content_title()"></%def>
<%def name="grid_tools()">
## grid totals
% if master.supports_grid_totals:
<div style="display: flex; align-items: center;">
<b-button v-if="gridTotalsDisplay == null"
:disabled="gridTotalsFetching"
@click="gridTotalsFetch()">
{{ gridTotalsFetching ? "Working, please wait..." : "Show Totals" }}
</b-button>
<div v-if="gridTotalsDisplay != null"
class="control">
Totals: {{ gridTotalsDisplay }}
</div>
</div>
% endif
## download search results
% if master.results_downloadable and master.has_perm('download_results'):
<div>
<b-button type="is-primary"
icon-pack="fas"
icon-left="download"
@click="showDownloadResultsDialog = true"
:disabled="!total">
Download Results
</b-button>
${h.form(url('{}.download_results'.format(route_prefix)), ref='download_results_form')}
${h.csrf_token(request)}
<input type="hidden" name="fmt" :value="downloadResultsFormat" />
<input type="hidden" name="fields" :value="downloadResultsFieldsIncluded" />
${h.end_form()}
<b-modal :active.sync="showDownloadResultsDialog">
<div class="card">
<div class="card-content">
<p>
There are
<span class="is-size-4 has-text-weight-bold">
{{ total.toLocaleString('en') }} ${model_title_plural}
</span>
matching your current filters.
</p>
<p>
You may download this set as a single data file if you like.
</p>
<br />
<b-notification type="is-warning" :closable="false"
v-if="downloadResultsFormat == 'xlsx' && total >= 1000">
Excel downloads for large data sets can take a long time to
generate, and bog down the server in the meantime. You are
encouraged to choose CSV for a large data set, even though
the end result (file size) may be larger with CSV.
</b-notification>
<div style="display: flex; justify-content: space-between">
<div>
<b-field label="Format">
<b-select v-model="downloadResultsFormat">
% for key, label in master.download_results_supported_formats().items():
<option value="${key}">${label}</option>
% endfor
</b-select>
</b-field>
</div>
<div>
<div v-show="downloadResultsFieldsMode != 'choose'"
class="has-text-right">
<p v-if="downloadResultsFieldsMode == 'default'">
Will use DEFAULT fields.
</p>
<p v-if="downloadResultsFieldsMode == 'all'">
Will use ALL fields.
</p>
<br />
</div>
<div class="buttons is-right">
<b-button type="is-primary"
v-show="downloadResultsFieldsMode != 'default'"
@click="downloadResultsUseDefaultFields()">
Use Default Fields
</b-button>
<b-button type="is-primary"
v-show="downloadResultsFieldsMode != 'all'"
@click="downloadResultsUseAllFields()">
Use All Fields
</b-button>
<b-button type="is-primary"
v-show="downloadResultsFieldsMode != 'choose'"
@click="downloadResultsFieldsMode = 'choose'">
Choose Fields
</b-button>
</div>
<div v-show="downloadResultsFieldsMode == 'choose'">
<div style="display: flex;">
<div>
<b-field label="Excluded Fields">
<b-select multiple native-size="8"
expanded
v-model="downloadResultsExcludedFieldsSelected"
ref="downloadResultsExcludedFields">
<option v-for="field in downloadResultsFieldsExcluded"
:key="field"
:value="field">
{{ field }}
</option>
</b-select>
</b-field>
</div>
<div>
<br /><br />
<b-button style="margin: 0.5rem;"
@click="downloadResultsExcludeFields()">
&lt;
</b-button>
<br />
<b-button style="margin: 0.5rem;"
@click="downloadResultsIncludeFields()">
&gt;
</b-button>
</div>
<div>
<b-field label="Included Fields">
<b-select multiple native-size="8"
expanded
v-model="downloadResultsIncludedFieldsSelected"
ref="downloadResultsIncludedFields">
<option v-for="field in downloadResultsFieldsIncluded"
:key="field"
:value="field">
{{ field }}
</option>
</b-select>
</b-field>
</div>
</div>
</div>
</div>
</div>
</div> <!-- card-content -->
<footer class="modal-card-foot">
<b-button @click="showDownloadResultsDialog = false">
Cancel
</b-button>
<once-button type="is-primary"
@click="downloadResultsSubmit()"
icon-pack="fas"
icon-left="download"
:disabled="!downloadResultsFieldsIncluded.length"
text="Download Results">
</once-button>
</footer>
</div>
</b-modal>
</div>
% endif
## download rows for search results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'):
<b-button type="is-primary"
icon-pack="fas"
icon-left="download"
@click="downloadResultsRows()"
:disabled="downloadResultsRowsButtonDisabled">
{{ downloadResultsRowsButtonText }}
</b-button>
${h.form(url('{}.download_results_rows'.format(route_prefix)), ref='downloadResultsRowsForm')}
${h.csrf_token(request)}
${h.end_form()}
% endif
## merge 2 objects
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
${h.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})}
${h.csrf_token(request)}
<input type="hidden"
name="uuids"
:value="checkedRowUUIDs()" />
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="object-ungroup"
:disabled="mergeFormSubmitting || checkedRows.length != 2">
{{ mergeFormButtonText }}
</b-button>
${h.end_form()}
% endif
## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'):
${h.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')}
${h.csrf_token(request)}
${h.hidden('uuids', v_model='selected_uuids')}
<b-button :disabled="enableSelectedDisabled"
@click="enableSelectedSubmit()">
{{ enableSelectedText }}
</b-button>
${h.end_form()}
${h.form(url('{}.disable_set'.format(route_prefix)), ref='disable_selected_form', class_='control')}
${h.csrf_token(request)}
${h.hidden('uuids', v_model='selected_uuids')}
<b-button :disabled="disableSelectedDisabled"
@click="disableSelectedSubmit()">
{{ disableSelectedText }}
</b-button>
${h.end_form()}
% endif
## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'):
${h.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')}
${h.csrf_token(request)}
${h.hidden('uuids', v_model='selected_uuids')}
<b-button type="is-danger"
:disabled="deleteSelectedDisabled"
@click="deleteSelectedSubmit()"
icon-pack="fas"
icon-left="trash">
{{ deleteSelectedText }}
</b-button>
${h.end_form()}
% endif
## delete search results
% if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
${h.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')}
${h.csrf_token(request)}
<b-button type="is-danger"
:disabled="deleteResultsDisabled"
:title="total ? null : 'There are no results to delete'"
@click="deleteResultsSubmit()"
icon-pack="fas"
icon-left="trash">
{{ deleteResultsText }}
</b-button>
${h.end_form()}
% endif
</%def>
<%def name="page_content()">
% if download_results_path:
<b-notification type="is-info">
Your download should start automatically, or you can
${h.link_to("click here", '{}?filename={}'.format(url('{}.download_results'.format(route_prefix)), h.os.path.basename(download_results_path)))}
</b-notification>
% endif
% if download_results_rows_path:
<b-notification type="is-info">
Your download should start automatically, or you can
${h.link_to("click here", '{}?filename={}'.format(url('{}.download_results_rows'.format(route_prefix)), h.os.path.basename(download_results_rows_path)))}
</b-notification>
% endif
${self.render_grid_component()}
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
${h.form('#', ref='deleteObjectForm')}
${h.csrf_token(request)}
${h.end_form()}
% endif
</%def>
<%def name="make_grid_component()">
## TODO: stop using |n filter?
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
</%def>
<%def name="render_grid_component()">
<${grid.component} ref="grid" :csrftoken="csrftoken"
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
@deleteActionClicked="deleteObject"
% endif
>
</${grid.component}>
</%def>
<%def name="make_this_page_component()">
## define grid
${self.make_grid_component()}
${parent.make_this_page_component()}
## finalize grid
<script>
${grid.component_studly}.data = () => { return ${grid.component_studly}Data }
Vue.component('${grid.component}', ${grid.component_studly})
</script>
</%def>
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
% if master.supports_grid_totals:
${grid.component_studly}Data.gridTotalsDisplay = null
${grid.component_studly}Data.gridTotalsFetching = false
${grid.component_studly}.methods.gridTotalsFetch = function() {
this.gridTotalsFetching = true
let url = '${url(f'{route_prefix}.fetch_grid_totals')}'
this.simpleGET(url, {}, response => {
this.gridTotalsDisplay = response.data.totals_display
this.gridTotalsFetching = false
}, response => {
this.gridTotalsFetching = false
})
}
${grid.component_studly}.methods.appliedFiltersHook = function() {
this.gridTotalsDisplay = null
this.gridTotalsFetching = false
}
% endif
## maybe auto-redirect to download latest results file
% if download_results_path:
ThisPage.methods.downloadResultsRedirect = function() {
location.href = '${url('{}.download_results'.format(route_prefix))}?filename=${h.os.path.basename(download_results_path)}';
}
ThisPage.mounted = function() {
// we give this 1 second before attempting the redirect; otherwise
// the FontAwesome icons do not seem to load properly. so this way
// the page should fully render before redirecting
window.setTimeout(this.downloadResultsRedirect, 1000)
}
% endif
## maybe auto-redirect to download latest "rows for results" file
% if download_results_rows_path:
ThisPage.methods.downloadResultsRowsRedirect = function() {
location.href = '${url('{}.download_results_rows'.format(route_prefix))}?filename=${h.os.path.basename(download_results_rows_path)}';
}
ThisPage.mounted = function() {
// we give this 1 second before attempting the redirect; otherwise
// the FontAwesome icons do not seem to load properly. so this way
// the page should fully render before redirecting
window.setTimeout(this.downloadResultsRowsRedirect, 1000)
}
% endif
% if request.session.pop('{}.results_csv.generated'.format(route_prefix), False):
ThisPage.mounted = function() {
location.href = '${url('{}.results_csv_download'.format(route_prefix))}';
}
% endif
% if request.session.pop('{}.results_xlsx.generated'.format(route_prefix), False):
ThisPage.mounted = function() {
location.href = '${url('{}.results_xlsx_download'.format(route_prefix))}';
}
% endif
## delete single object
% if master.deletable and master.has_perm('delete') and master.delete_confirm == 'simple':
ThisPage.methods.deleteObject = function(url) {
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
let form = this.$refs.deleteObjectForm
form.action = url
form.submit()
}
}
% endif
## download results
% if master.results_downloadable and master.has_perm('download_results'):
${grid.component_studly}Data.downloadResultsFormat = '${master.download_results_default_format()}'
${grid.component_studly}Data.showDownloadResultsDialog = false
${grid.component_studly}Data.downloadResultsFieldsMode = 'default'
${grid.component_studly}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
${grid.component_studly}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsExcludedFieldsSelected = []
${grid.component_studly}Data.downloadResultsIncludedFieldsSelected = []
${grid.component_studly}.computed.downloadResultsFieldsExcluded = function() {
let excluded = []
this.downloadResultsFieldsAvailable.forEach(field => {
if (!this.downloadResultsFieldsIncluded.includes(field)) {
excluded.push(field)
}
}, this)
return excluded
}
${grid.component_studly}.methods.downloadResultsExcludeFields = function() {
const selected = Array.from(this.downloadResultsIncludedFieldsSelected)
if (!selected) {
return
}
selected.forEach(field => {
let index
// remove field from selected
index = this.downloadResultsIncludedFieldsSelected.indexOf(field)
if (index >= 0) {
this.downloadResultsIncludedFieldsSelected.splice(index, 1)
}
// remove field from included
// nb. excluded list will reflect this change too
index = this.downloadResultsFieldsIncluded.indexOf(field)
if (index >= 0) {
this.downloadResultsFieldsIncluded.splice(index, 1)
}
})
}
${grid.component_studly}.methods.downloadResultsIncludeFields = function() {
const selected = Array.from(this.downloadResultsExcludedFieldsSelected)
if (!selected) {
return
}
selected.forEach(field => {
let index
// remove field from selected
index = this.downloadResultsExcludedFieldsSelected.indexOf(field)
if (index >= 0) {
this.downloadResultsExcludedFieldsSelected.splice(index, 1)
}
// add field to included
// nb. excluded list will reflect this change too
this.downloadResultsFieldsIncluded.push(field)
})
}
${grid.component_studly}.methods.downloadResultsUseDefaultFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
this.downloadResultsFieldsMode = 'default'
}
${grid.component_studly}.methods.downloadResultsUseAllFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
this.downloadResultsFieldsMode = 'all'
}
${grid.component_studly}.methods.downloadResultsSubmit = function() {
this.$refs.download_results_form.submit()
}
% endif
## download rows for results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'):
${grid.component_studly}Data.downloadResultsRowsButtonDisabled = false
${grid.component_studly}Data.downloadResultsRowsButtonText = "Download Rows for Results"
${grid.component_studly}.methods.downloadResultsRows = function() {
if (confirm("This will generate an Excel file which contains "
+ "not the results themselves, but the *rows* for "
+ "each.\n\nAre you sure you want this?")) {
this.downloadResultsRowsButtonDisabled = true
this.downloadResultsRowsButtonText = "Working, please wait..."
this.$refs.downloadResultsRowsForm.submit()
}
}
% endif
## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'):
${grid.component_studly}Data.enableSelectedSubmitting = false
${grid.component_studly}Data.enableSelectedText = "Enable Selected"
${grid.component_studly}.computed.enableSelectedDisabled = function() {
if (this.enableSelectedSubmitting) {
return true
}
if (!this.checkedRowUUIDs().length) {
return true
}
return false
}
${grid.component_studly}.methods.enableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
return
}
if (! confirm("Are you sure you wish to ENABLE the " + uuids.length + " selected objects?")) {
return
}
this.enableSelectedSubmitting = true
this.enableSelectedText = "Working, please wait..."
this.$refs.enable_selected_form.submit()
}
${grid.component_studly}Data.disableSelectedSubmitting = false
${grid.component_studly}Data.disableSelectedText = "Disable Selected"
${grid.component_studly}.computed.disableSelectedDisabled = function() {
if (this.disableSelectedSubmitting) {
return true
}
if (!this.checkedRowUUIDs().length) {
return true
}
return false
}
${grid.component_studly}.methods.disableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
return
}
if (! confirm("Are you sure you wish to DISABLE the " + uuids.length + " selected objects?")) {
return
}
this.disableSelectedSubmitting = true
this.disableSelectedText = "Working, please wait..."
this.$refs.disable_selected_form.submit()
}
% endif
## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'):
${grid.component_studly}Data.deleteSelectedSubmitting = false
${grid.component_studly}Data.deleteSelectedText = "Delete Selected"
${grid.component_studly}.computed.deleteSelectedDisabled = function() {
if (this.deleteSelectedSubmitting) {
return true
}
if (!this.checkedRowUUIDs().length) {
return true
}
return false
}
${grid.component_studly}.methods.deleteSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
return
}
if (! confirm("Are you sure you wish to DELETE the " + uuids.length + " selected objects?")) {
return
}
this.deleteSelectedSubmitting = true
this.deleteSelectedText = "Working, please wait..."
this.$refs.delete_selected_form.submit()
}
% endif
% if master.bulk_deletable and master.has_perm('bulk_delete'):
${grid.component_studly}Data.deleteResultsSubmitting = false
${grid.component_studly}Data.deleteResultsText = "Delete Results"
${grid.component_studly}.computed.deleteResultsDisabled = function() {
if (this.deleteResultsSubmitting) {
return true
}
if (!this.total) {
return true
}
return false
}
${grid.component_studly}.methods.deleteResultsSubmit = function() {
// TODO: show "plural model title" here?
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
return
}
this.deleteResultsSubmitting = true
this.deleteResultsText = "Working, please wait..."
this.$refs.delete_results_form.submit()
}
% endif
% if master.mergeable and master.has_perm('merge'):
${grid.component_studly}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
${grid.component_studly}Data.mergeFormSubmitting = false
${grid.component_studly}.methods.submitMergeForm = function() {
this.mergeFormSubmitting = true
this.mergeFormButtonText = "Working, please wait..."
}
% endif
</script>
</%def>
${parent.body()}