Add basic Buefy support for App Settings page

also various buttons have been tweaked on some other "master view" pages
This commit is contained in:
Lance Edgar 2019-04-17 14:55:27 -05:00
parent e1ff4578e9
commit fcfc8b56bb
13 changed files with 245 additions and 52 deletions

View file

@ -67,7 +67,8 @@ class Grid(object):
Core grid class. In sore need of documentation. Core grid class. In sore need of documentation.
""" """
def __init__(self, key, data, columns=None, width='auto', request=None, mobile=False, model_class=None, def __init__(self, key, data, columns=None, width='auto', request=None, mobile=False,
model_class=None, model_title=None, model_title_plural=None,
enums={}, labels={}, renderers={}, extra_row_class=None, linked_columns=[], url='#', enums={}, labels={}, renderers={}, extra_row_class=None, linked_columns=[], url='#',
joiners={}, filterable=False, filters={}, use_byte_string_filters=False, joiners={}, filterable=False, filters={}, use_byte_string_filters=False,
sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc',
@ -84,6 +85,18 @@ class Grid(object):
self.model_class = model_class self.model_class = model_class
if self.model_class and self.columns is None: if self.model_class and self.columns is None:
self.columns = self.make_columns() self.columns = self.make_columns()
self.model_title = model_title
if not self.model_title and self.model_class:
self.model_title = self.model_class.get_model_title()
self.model_title_plural = model_title_plural
if not self.model_title_plural:
if self.model_class:
self.model_title_plural = self.model_class.get_model_title_plural()
if not self.model_title_plural:
self.model_title_plural = '{}s'.format(self.model_title)
self.enums = enums or {} self.enums = enums or {}
self.labels = labels or {} self.labels = labels or {}

View file

@ -7,11 +7,14 @@
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if not use_buefy:
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.appsettings.js') + '?ver={}'.format(tailbone.__version__))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.appsettings.js') + '?ver={}'.format(tailbone.__version__))}
% endif
</%def> </%def>
<%def name="extra_styles()"> <%def name="extra_styles()">
${parent.extra_styles()} ${parent.extra_styles()}
% if not use_buefy:
<style type="text/css"> <style type="text/css">
div.form { div.form {
float: none; float: none;
@ -27,6 +30,7 @@
width: 50em; width: 50em;
} }
</style> </style>
% endif
</%def> </%def>
<div class="form"> <div class="form">
@ -46,10 +50,17 @@
</div> </div>
% endif % endif
% if use_buefy:
<div id="app-settings-app">
<app-settings :groups="groups" :showing-group="showingGroup"></app-settings>
</div>
% else:
## not buefy
<div class="group-picker"> <div class="group-picker">
<div class="field-wrapper"> <div class="field-wrapper">
<label for="settings-group">Showing Group</label> <label for="settings-group">Showing Group</label>
<div class="field"> <div class="field select">
${h.select('settings-group', current_group, group_options, **{'auto-enhance': 'true'})} ${h.select('settings-group', current_group, group_options, **{'auto-enhance': 'true'})}
</div> </div>
</div> </div>
@ -88,11 +99,116 @@
</div><!-- panel-body --> </div><!-- panel-body -->
</div><! -- panel --> </div><! -- panel -->
% endfor % endfor
% endif
<div class="buttons"> <div class="buttons">
${h.submit('save', getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))} ${h.submit('save', getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")), class_='button is-primary')}
${h.link_to("Cancel", form.cancel_url, class_='cancel button{}'.format(' autodisable' if form.auto_disable_cancel else ''))} ${h.link_to("Cancel", form.cancel_url, class_='cancel button{}'.format(' autodisable' if form.auto_disable_cancel else ''))}
</div> </div>
${h.end_form()} ${h.end_form()}
</div> </div>
% if use_buefy:
<script type="text/x-template" id="app-settings-template">
<div class="app-wrapper">
<div class="field-wrapper">
<label for="settings-group">Showing Group</label>
<b-select @input="showGroup"
name="settings-group"
v-model="showingGroup">
<option value="">(All)</option>
<option v-for="group in groups"
:key="group.label"
:value="group.label">
{{ group.label }}
</option>
</b-select>
</div>
<div v-for="group in groups"
class="card"
v-show="!showingGroup || showingGroup == group.label"
style="margin-bottom: 1rem;">
<header class="card-header">
<p class="card-header-title">{{ group.label }}</p>
</header>
<div class="card-content">
<div v-for="setting in group.settings"
:class="'field-wrapper' + (setting.error ? ' with-error' : '')">
<div v-if="setting.error" class="field-error">
<span v-for="msg in setting.error_messages"
class="error-msg">
{{ msg }}
</span>
</div>
<div class="field-row">
<label :for="setting.field_name">{{ setting.label }}</label>
<div class="field">
<input v-if="setting.data_type == 'bool'"
type="checkbox"
:name="setting.field_name"
:id="setting.field_name"
v-model="setting.value"
value="true" />
<b-select v-else-if="setting.choices"
:name="setting.field_name"
:id="setting.field_name"
v-model="setting.value">
<option v-for="choice in setting.choices"
:value="choice">
{{ choice }}
</option>
</b-select>
<b-input v-else
:name="setting.field_name"
:id="setting.field_name"
v-model="setting.value" />
</div>
</div>
<span v-if="setting.helptext" class="instructions">
{{ setting.helptext }}
</span>
</div><!-- field-wrapper -->
</div><!-- card-content -->
</div><!-- card -->
</div><!-- app-wrapper -->
</script>
<script type="text/javascript">
Vue.component('app-settings', {
template: '#app-settings-template',
props: {
groups: Array,
showingGroup: String
},
methods: {
showGroup(group) {
console.log("SHOWING GROUP")
console.log(group)
}
}
})
new Vue({
el: '#app-settings-app',
data() {
return {
groups: ${json.dumps(buefy_data)|n},
showingGroup: ${json.dumps(current_group or '')|n}
}
}
})
</script>
% endif

View file

@ -21,7 +21,9 @@
<%def name="object_helpers()"> <%def name="object_helpers()">
${parent.object_helpers()} ${parent.object_helpers()}
% if instance.people:
${view_profiles_helper(instance.people)} ${view_profiles_helper(instance.people)}
% endif
</%def> </%def>
${parent.body()} ${parent.body()}

View file

@ -5,16 +5,16 @@
${parent.grid_tools()} ${parent.grid_tools()}
% if request.has_perm('datasync.restart'): % if request.has_perm('datasync.restart'):
${h.form(url('datasync.restart'), name='restart-datasync', class_='autodisable')} ${h.form(url('datasync.restart'), name='restart-datasync', class_='autodisable control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.submit('submit', "Restart DataSync", data_working_label="Restarting DataSync")} ${h.submit('submit', "Restart DataSync", data_working_label="Restarting DataSync", class_='button')}
${h.end_form()} ${h.end_form()}
% endif % endif
% if allow_filemon_restart and request.has_perm('filemon.restart'): % if allow_filemon_restart and request.has_perm('filemon.restart'):
${h.form(url('filemon.restart'), name='restart-filemon', class_='autodisable')} ${h.form(url('filemon.restart'), name='restart-filemon', class_='autodisable control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.submit('submit', "Restart FileMon", data_working_label="Restarting FileMon")} ${h.submit('submit', "Restart FileMon", data_working_label="Restarting FileMon", class_='button')}
${h.end_form()} ${h.end_form()}
% endif % endif

View file

@ -11,6 +11,7 @@
<input type="hidden" name="__start__" value="${name}:sequence" <input type="hidden" name="__start__" value="${name}:sequence"
tal:condition="multiple" /> tal:condition="multiple" />
<div class="select">
<select tal:attributes=" <select tal:attributes="
name name; name name;
id oid; id oid;
@ -36,6 +37,7 @@
value item[0]">${item[1]}</option> value item[0]">${item[1]}</option>
</tal:loop> </tal:loop>
</select> </select>
</div>
<input type="hidden" name="__end__" value="${name}:sequence" <input type="hidden" name="__end__" value="${name}:sequence"
tal:condition="multiple" /> tal:condition="multiple" />
<script tal:condition="not multiple" type="text/javascript"> <script tal:condition="not multiple" type="text/javascript">

View file

@ -67,7 +67,7 @@
<div class="grid-tools-wrapper"> <div class="grid-tools-wrapper">
% if tools: % if tools:
<div class="grid-tools"> <div class="grid-tools field is-grouped">
## TODO: stop using |n filter ## TODO: stop using |n filter
${tools|n} ${tools|n}
</div> </div>
@ -299,6 +299,14 @@
// apply current filters as normal, but add special directive // apply current filters as normal, but add special directive
const params = ['save-current-filters-as-defaults=true'] const params = ['save-current-filters-as-defaults=true']
this.applyFilters(params) this.applyFilters(params)
},
deleteResults(event) {
// submit form if user confirms
if (confirm("You are about to delete " + this.total + " ${grid.model_title_plural}.\n\nAre you sure?")) {
event.target.form.submit()
}
} }
} }

View file

@ -25,7 +25,7 @@
${h.csrf_token(request)} ${h.csrf_token(request)}
<div class="buttons"> <div class="buttons">
<a class="button" href="${form.cancel_url}">Whoops, nevermind...</a> <a class="button" href="${form.cancel_url}">Whoops, nevermind...</a>
${h.submit('submit', "Yes, please DELETE this data forever!")} ${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')}
</div> </div>
${h.end_form()} ${h.end_form()}
</%def> </%def>

View file

@ -45,7 +45,7 @@
% endif % endif
% if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)): % if not use_buefy and master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
$('form[name="bulk-delete"] button').click(function() { $('form[name="bulk-delete"] button').click(function() {
var count = $('.grid-wrapper').gridwrapper('results_count', true); var count = $('.grid-wrapper').gridwrapper('results_count', true);
@ -134,42 +134,53 @@
## merge 2 objects ## merge 2 objects
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)): % if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
${h.form(url('{}.merge'.format(route_prefix)), name='merge-things')} ${h.form(url('{}.merge'.format(route_prefix)), name='merge-things', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids')} ${h.hidden('uuids')}
<button type="submit">Merge 2 ${model_title_plural}</button> <button type="submit" class="button">Merge 2 ${model_title_plural}</button>
${h.end_form()} ${h.end_form()}
% endif % endif
## enable / disable selected objects ## enable / disable selected objects
% if master.supports_set_enabled_toggle and request.has_perm('{}.enable_disable_set'.format(permission_prefix)): % if master.supports_set_enabled_toggle and request.has_perm('{}.enable_disable_set'.format(permission_prefix)):
${h.form(url('{}.enable_set'.format(route_prefix)), name='enable-set')} ${h.form(url('{}.enable_set'.format(route_prefix)), name='enable-set', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids')} ${h.hidden('uuids')}
<button type="button">Enable Selected</button> <button type="button" class="button">Enable Selected</button>
${h.end_form()} ${h.end_form()}
${h.form(url('{}.disable_set'.format(route_prefix)), name='disable-set')} ${h.form(url('{}.disable_set'.format(route_prefix)), name='disable-set', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids')} ${h.hidden('uuids')}
<button type="button">Disable Selected</button> <button type="button" class="button">Disable Selected</button>
${h.end_form()} ${h.end_form()}
% endif % endif
## delete selected objects ## delete selected objects
% if master.set_deletable and request.has_perm('{}.delete_set'.format(permission_prefix)): % if master.set_deletable and request.has_perm('{}.delete_set'.format(permission_prefix)):
${h.form(url('{}.delete_set'.format(route_prefix)), name='delete-set')} ${h.form(url('{}.delete_set'.format(route_prefix)), name='delete-set', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.hidden('uuids')} ${h.hidden('uuids')}
<button type="button">Delete Selected</button> <button type="button" class="button">Delete Selected</button>
${h.end_form()} ${h.end_form()}
% endif % endif
## delete search results ## delete search results
% if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)): % if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
${h.form(url('{}.bulk_delete'.format(route_prefix)), name='bulk-delete')} ${h.form(url('{}.bulk_delete'.format(route_prefix)), name='bulk-delete', class_='control')}
${h.csrf_token(request)} ${h.csrf_token(request)}
% if use_buefy:
<b-button type="is-danger"
:disabled="! total"
:title="total ? null : 'There are no results to delete'"
@click="deleteResults"
icon-pack="fas"
icon-left="trash">
Delete Results
</b-button>
% else:
<button type="button">Delete Results</button> <button type="button">Delete Results</button>
% endif
${h.end_form()} ${h.end_form()}
% endif % endif

View file

@ -43,12 +43,12 @@ ${parent.body()}
% if instance.enabled and not instance.executing: % if instance.enabled and not instance.executing:
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')} ${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')}
${h.csrf_token(request)} ${h.csrf_token(request)}
${h.submit('execute', "Execute this upgrade")} ${h.submit('execute', "Execute this upgrade", class_='button is-primary')}
${h.end_form()} ${h.end_form()}
% elif instance.enabled: % elif instance.enabled:
<button type="button" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button> <button type="button" class="button is-primary" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button>
% else: % else:
<button type="button" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button> <button type="button" class="button is-primary" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button>
% endif % endif
</div> </div>
% endif % endif

View file

@ -73,6 +73,25 @@ class View(object):
def notfound(self): def notfound(self):
return httpexceptions.HTTPNotFound() return httpexceptions.HTTPNotFound()
def get_use_buefy(self):
"""
Returns a flag indicating whether or not the current theme supports
(and therefore should use) the Buefy JS library.
"""
# first check theme-specific setting, if one has been defined
theme = self.request.registry.settings['tailbone.theme']
buefy = self.rattail_config.getbool('tailbone', 'themes.{}.use_buefy'.format(theme))
if buefy is not None:
return buefy
# TODO: should not hard-code this surely, but works for now...
if theme == 'falafel':
return True
# TODO: probably should not use this fallback? it was the first setting
# i tested with, but is poorly named to say the least
return self.rattail_config.getbool('tailbone', 'grids.use_buefy', default=False)
def late_login_user(self): def late_login_user(self):
""" """
Returns the :class:`rattail:rattail.db.model.User` instance Returns the :class:`rattail:rattail.db.model.User` instance

View file

@ -33,7 +33,6 @@ import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
import colander import colander
from deform import widget as dfwidget
from pyramid.httpexceptions import HTTPNotFound from pyramid.httpexceptions import HTTPNotFound
from webhelpers2.html import HTML, tags from webhelpers2.html import HTML, tags
@ -220,7 +219,7 @@ class CustomersView(MasterView):
f.set_enum('email_preference', self.enum.EMAIL_PREFERENCE) f.set_enum('email_preference', self.enum.EMAIL_PREFERENCE)
preferences = list(self.enum.EMAIL_PREFERENCE.items()) preferences = list(self.enum.EMAIL_PREFERENCE.items())
preferences.insert(0, ('', "(no preference)")) preferences.insert(0, ('', "(no preference)"))
f.set_widget('email_preference', dfwidget.SelectWidget(values=preferences)) f.widgets['email_preference'].values = preferences
# person # person
if self.creating: if self.creating:

View file

@ -251,25 +251,6 @@ class MasterView(View):
labels.update(cls.row_labels) labels.update(cls.row_labels)
return labels return labels
def get_use_buefy(self):
"""
Returns a flag indicating whether or not the current theme supports
(and therefore should use) the Buefy JS library.
"""
# first check theme-specific setting, if one has been defined
theme = self.request.registry.settings['tailbone.theme']
buefy = self.rattail_config.getbool('tailbone', 'themes.{}.use_buefy'.format(theme))
if buefy is not None:
return buefy
# TODO: should not hard-code this surely, but works for now...
if theme == 'falafel':
return True
# TODO: probably should not use this fallback? it was the first setting
# i tested with, but is poorly named to say the least
return self.rattail_config.getbool('tailbone', 'grids.use_buefy', default=False)
############################## ##############################
# Available Views # Available Views
############################## ##############################
@ -368,6 +349,8 @@ class MasterView(View):
defaults = { defaults = {
'model_class': getattr(self, 'model_class', None), 'model_class': getattr(self, 'model_class', None),
'model_title': self.get_model_title(),
'model_title_plural': self.get_model_title_plural(),
'width': 'full', 'width': 'full',
'filterable': self.filterable, 'filterable': self.filterable,
'use_byte_string_filters': self.use_byte_string_filters, 'use_byte_string_filters': self.use_byte_string_filters,

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar # Copyright © 2010-2019 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -33,6 +33,7 @@ import six
from rattail.db import model, api from rattail.db import model, api
from rattail.settings import Setting from rattail.settings import Setting
from rattail.util import import_module_path from rattail.util import import_module_path
from rattail.config import parse_bool
import colander import colander
from webhelpers2.html import tags from webhelpers2.html import tags
@ -109,7 +110,7 @@ class AppSettingsView(View):
if form.validate(newstyle=True): if form.validate(newstyle=True):
self.save_form(form) self.save_form(form)
group = self.request.POST.get('settings-group') group = self.request.POST.get('settings-group')
if group: if group is not None:
self.request.session['appsettings.current_group'] = group self.request.session['appsettings.current_group'] = group
self.request.session.flash("App Settings have been saved.") self.request.session.flash("App Settings have been saved.")
return self.redirect(self.request.current_route_url()) return self.redirect(self.request.current_route_url())
@ -120,17 +121,56 @@ class AppSettingsView(View):
if not current_group: if not current_group:
current_group = self.request.session.get('appsettings.current_group') current_group = self.request.session.get('appsettings.current_group')
group_options = [tags.Option(group, group) for group in groups] use_buefy = self.get_use_buefy()
group_options.insert(0, tags.Option("(All)", "(All)")) context = {
return {
'index_title': "App Settings", 'index_title': "App Settings",
'form': form, 'form': form,
'dform': form.make_deform_form(), 'dform': form.make_deform_form(),
'groups': groups, 'groups': groups,
'group_options': group_options,
'current_group': current_group,
'settings': settings, 'settings': settings,
'use_buefy': use_buefy,
} }
if use_buefy:
context['buefy_data'] = self.get_buefy_data(form, groups, settings)
# TODO: this seems hacky, and probably only needed if theme changes?
if current_group == '(All)':
current_group = ''
else:
group_options = [tags.Option(group, group) for group in groups]
group_options.insert(0, tags.Option("(All)", "(All)"))
context['group_options'] = group_options
context['current_group'] = current_group
return context
def get_buefy_data(self, form, groups, settings):
dform = form.make_deform_form()
grouped = dict([(label, [])
for label in groups])
for setting in settings:
field = dform[setting.node_name]
s = {
'field_name': field.name,
'label': form.get_label(field.name),
'data_type': setting.data_type.__name__,
'choices': setting.choices,
'helptext': form.render_helptext(field.name) if form.has_helptext(field.name) else None,
'error': field.error,
}
value = self.get_setting_value(setting)
if setting.data_type is bool:
value = parse_bool(value)
s['value'] = value
if field.error:
s['error_messages'] = field.error_messages()
grouped[setting.group].append(s)
data = []
for label in groups:
group = {'label': label, 'settings': grouped[label]}
data.append(group)
return data
def make_form(self, known_settings): def make_form(self, known_settings):
schema = colander.MappingSchema() schema = colander.MappingSchema()