Add basic support for editing field help info

This commit is contained in:
Lance Edgar 2022-12-24 21:46:02 -06:00
parent 9fe9983bf9
commit 3befdc09e3
6 changed files with 218 additions and 7 deletions

View file

@ -48,7 +48,8 @@ from pyramid_deform import SessionFileUploadTempStore
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML
from tailbone.util import raw_datetime, get_form_data from tailbone.db import Session
from tailbone.util import raw_datetime, get_form_data, render_markdown
from . import types from . import types
from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
from tailbone.exceptions import TailboneJSONFieldError from tailbone.exceptions import TailboneJSONFieldError
@ -337,6 +338,8 @@ class Form(object):
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None, hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form', action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form',
vuejs_field_converters={}, vuejs_field_converters={},
# TODO: ugh this is getting out hand!
can_edit_help=False, edit_help_url=None, route_prefix=None,
): ):
self.fields = None self.fields = None
if fields is not None: if fields is not None:
@ -375,6 +378,9 @@ class Form(object):
self.use_buefy = use_buefy self.use_buefy = use_buefy
self.component = component self.component = component
self.vuejs_field_converters = vuejs_field_converters or {} self.vuejs_field_converters = vuejs_field_converters or {}
self.can_edit_help = can_edit_help
self.edit_help_url = edit_help_url
self.route_prefix = route_prefix
def __iter__(self): def __iter__(self):
return iter(self.fields) return iter(self.fields)
@ -800,6 +806,11 @@ class Form(object):
context = kwargs context = kwargs
context['form'] = self context['form'] = self
context['dform'] = dform context['dform'] = dform
context.setdefault('can_edit_help', self.can_edit_help)
if context['can_edit_help']:
context.setdefault('edit_help_url', self.edit_help_url)
context['field_labels'] = self.get_field_labels()
context['field_markdowns'] = self.get_field_markdowns()
context.setdefault('form_kwargs', {}) context.setdefault('form_kwargs', {})
# TODO: deprecate / remove the latter option here # TODO: deprecate / remove the latter option here
if self.auto_disable_save or self.auto_disable: if self.auto_disable_save or self.auto_disable:
@ -815,6 +826,22 @@ class Form(object):
context['render_field_readonly'] = self.render_field_readonly context['render_field_readonly'] = self.render_field_readonly
return render(template, context) return render(template, context)
def get_field_labels(self):
return dict([(field, self.get_label(field))
for field in self])
def get_field_markdowns(self):
model = self.request.rattail_config.get_model()
if not hasattr(self, 'field_markdowns'):
infos = Session.query(model.TailboneFieldInfo)\
.filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\
.all()
self.field_markdowns = dict([(info.field_name, info.markdown_text)
for info in infos])
return self.field_markdowns
def get_vuejs_model_value(self, field): def get_vuejs_model_value(self, field):
""" """
This method must return "raw" JS which will be assigned as the initial This method must return "raw" JS which will be assigned as the initial
@ -874,13 +901,14 @@ class Form(object):
""" """
dform = self.make_deform_form() dform = self.make_deform_form()
field = dform[fieldname] field = dform[fieldname]
label = self.get_label(fieldname)
markdowns = self.get_field_markdowns()
if self.field_visible(fieldname): if self.field_visible(fieldname):
# these attrs will be for the <b-field> (*not* the widget) # these attrs will be for the <b-field> (*not* the widget)
attrs = { attrs = {
':horizontal': 'true', ':horizontal': 'true',
'label': self.get_label(fieldname),
} }
# add some magic for file input fields # add some magic for file input fields
@ -915,11 +943,50 @@ class Form(object):
# render the field widget or whatever # render the field widget or whatever
html = field.serialize(use_buefy=True, html = field.serialize(use_buefy=True,
**self.get_renderer_kwargs(fieldname)) **self.get_renderer_kwargs(fieldname))
# TODO: why do we not get HTML literal from serialize() ?
html = HTML.literal(html) html = HTML.literal(html)
# may need a complex label
label_contents = [label]
# add 'configure' icon if allowed
if self.can_edit_help:
icon = HTML.tag('b-icon', size='is-small', pack='fas',
icon='cog')
icon = HTML.tag('a', title="Configure field", c=[icon],
**{'@click.prevent': "configureFieldInit('{}')".format(fieldname)})
label_contents.append(HTML.literal('&nbsp; &nbsp;'))
label_contents.append(icon)
# add 'help' icon/tooltip if defined
if markdowns.get(fieldname):
icon = HTML.tag('b-icon', size='is-small', pack='fas',
icon='question-circle')
tooltip = render_markdown(markdowns[fieldname])
# nb. must apply hack to get <template #content> as final result
tooltip_template = HTML.tag('template', c=[tooltip],
**{'#content': 1})
tooltip_template = tooltip_template.replace(
HTML.literal('<template #content="1"'),
HTML.literal('<template #content'))
tooltip = HTML.tag('b-tooltip',
type='is-white',
size='is-large',
multilined='multilined',
c=[icon, tooltip_template])
label_contents.append(HTML.literal('&nbsp; &nbsp;'))
label_contents.append(tooltip)
# nb. must apply hack to get <template #label> as final result
label_template = HTML.tag('template', c=label_contents,
**{'#label': 1})
label_template = label_template.replace(
HTML.literal('<template #label="1"'),
HTML.literal('<template #label'))
# and finally wrap it all in a <b-field> # and finally wrap it all in a <b-field>
return HTML.tag('b-field', c=[html], **attrs) return HTML.tag('b-field', c=[label_template, html], **attrs)
else: # hidden field else: # hidden field

View file

@ -58,6 +58,25 @@
${parent.render_this_page_template()} ${parent.render_this_page_template()}
</%def> </%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
% if can_edit_help:
<script type="text/javascript">
${form.component_studly}.methods.configureFieldInit = function(fieldname) {
this.configureFieldName = fieldname
this.configureFieldLabel = this.fieldLabels[fieldname]
this.configureFieldMarkdown = this.fieldMarkdowns[fieldname]
this.configureFieldShowDialog = true
this.$nextTick(() => {
this.$refs.configureFieldMarkdown.focus()
})
}
</script>
% endif
</%def>
<%def name="finalize_this_page_vars()"> <%def name="finalize_this_page_vars()">
${parent.finalize_this_page_vars()} ${parent.finalize_this_page_vars()}
% if form is not Undefined: % if form is not Undefined:

View file

@ -66,6 +66,47 @@
% if not form.readonly: % if not form.readonly:
${h.end_form()} ${h.end_form()}
% endif % endif
% if can_edit_help:
<b-modal has-modal-card
:active.sync="configureFieldShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Field: {{ configureFieldName }}</p>
</header>
<section class="modal-card-body">
<b-field label="Label">
<b-input v-model="configureFieldLabel" disabled></b-input>
</b-field>
<b-field label="Help Text (Markdown)">
<b-input v-model="configureFieldMarkdown"
type="textarea" rows="8"
ref="configureFieldMarkdown">
</b-input>
</b-field>
</section>
<footer class="modal-card-foot">
<b-button @click="configureFieldShowDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
@click="configureFieldSave()"
:disabled="configureFieldSaving"
icon-pack="fas"
icon-left="save">
{{ configureFieldSaving ? "Working, please wait..." : "Save" }}
</b-button>
</footer>
</div>
</b-modal>
% endif
</div> </div>
</script> </script>
@ -85,7 +126,29 @@
submit${form.component_studly}() { submit${form.component_studly}() {
this.${form.component_studly}Submitting = true this.${form.component_studly}Submitting = true
this.${form.component_studly}ButtonText = "Working, please wait..." this.${form.component_studly}ButtonText = "Working, please wait..."
} },
% endif
% if can_edit_help:
configureFieldSave() {
this.configureFieldSaving = true
let url = '${edit_help_url}'
let params = {
field_name: this.configureFieldName,
markdown_text: this.configureFieldMarkdown,
}
this.submitForm(url, params, response => {
this.configureFieldShowDialog = false
this.$buefy.toast.open({
message: "Info was saved; please refresh page to see changes.",
type: 'is-info',
duration: 4000, // 4 seconds
})
this.configureFieldSaving = false
}, response => {
this.configureFieldSaving = false
})
},
% endif % endif
} }
} }
@ -95,6 +158,16 @@
## TODO: should find a better way to handle CSRF token ## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
% if can_edit_help:
fieldLabels: ${json.dumps(field_labels)|n},
fieldMarkdowns: ${json.dumps(field_markdowns)|n},
configureFieldShowDialog: false,
configureFieldSaving: false,
configureFieldName: null,
configureFieldLabel: null,
configureFieldMarkdown: null,
% endif
## TODO: ugh, this seems pretty hacky. need to declare some data models ## TODO: ugh, this seems pretty hacky. need to declare some data models
## for various field components to bind to... ## for various field components to bind to...
% if not form.readonly: % if not form.readonly:

View file

@ -108,7 +108,7 @@
</b-input> </b-input>
</b-field> </b-field>
<b-field label="Markdown Text"> <b-field label="Help Text (Markdown)">
<b-input v-model="markdownText" <b-input v-model="markdownText"
type="textarea" rows="8"> type="textarea" rows="8">
</b-input> </b-input>

View file

@ -183,12 +183,14 @@ def raw_datetime(config, value, verbose=False, as_date=False):
return HTML.tag('span', **kwargs) return HTML.tag('span', **kwargs)
def render_markdown(text, **kwargs): def render_markdown(text, raw=False, **kwargs):
""" """
Render the given markdown text as HTML. Render the given markdown text as HTML.
""" """
kwargs.setdefault('extensions', ['fenced_code', 'codehilite']) kwargs.setdefault('extensions', ['fenced_code', 'codehilite'])
md = markdown.markdown(text, **kwargs) md = markdown.markdown(text, **kwargs)
if raw:
return md
md = HTML.literal(md) md = HTML.literal(md)
return HTML.tag('div', class_='rendered-markdown', c=[md]) return HTML.tag('div', class_='rendered-markdown', c=[md])

View file

@ -2333,6 +2333,40 @@ class MasterView(View):
info.markdown_text = form.validated['markdown_text'] info.markdown_text = form.validated['markdown_text']
return {'ok': True} return {'ok': True}
def edit_field_help(self):
if (not self.has_perm('edit_help')
and not self.request.has_perm('common.edit_help')):
raise self.forbidden()
model = self.model
route_prefix = self.get_route_prefix()
schema = colander.Schema()
schema.add(colander.SchemaNode(colander.String(),
name='field_name'))
schema.add(colander.SchemaNode(colander.String(),
name='markdown_text',
missing=None))
factory = self.get_form_factory()
form = factory(schema=schema, request=self.request)
if not form.validate(newstyle=True):
return {'error': "Form did not validate"}
# nb. self.Session may differ, so use tailbone.db.Session
info = Session.query(model.TailboneFieldInfo)\
.filter(model.TailboneFieldInfo.route_prefix == route_prefix)\
.filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\
.first()
if not info:
info = model.TailboneFieldInfo(route_prefix=route_prefix,
field_name=form.validated['field_name'])
Session.add(info)
info.markdown_text = form.validated['markdown_text']
return {'ok': True}
def render_to_response(self, template, data, **kwargs): def render_to_response(self, template, data, **kwargs):
""" """
Return a response with the given template rendered with the given data. Return a response with the given template rendered with the given data.
@ -3944,6 +3978,7 @@ class MasterView(View):
Return a dictionary of kwargs to be passed to the factory when creating Return a dictionary of kwargs to be passed to the factory when creating
new form instances. new form instances.
""" """
route_prefix = self.get_route_prefix()
defaults = { defaults = {
'request': self.request, 'request': self.request,
'readonly': self.viewing, 'readonly': self.viewing,
@ -3951,12 +3986,21 @@ class MasterView(View):
'action_url': self.request.current_route_url(_query=None), 'action_url': self.request.current_route_url(_query=None),
'use_buefy': self.get_use_buefy(), 'use_buefy': self.get_use_buefy(),
'assume_local_times': self.has_local_times, 'assume_local_times': self.has_local_times,
'route_prefix': route_prefix,
'can_edit_help': (self.has_perm('edit_help')
or self.request.has_perm('common.edit_help')),
} }
if defaults['can_edit_help']:
defaults['edit_help_url'] = self.request.route_url(
'{}.edit_field_help'.format(route_prefix))
if self.creating: if self.creating:
kwargs.setdefault('cancel_url', self.get_index_url()) kwargs.setdefault('cancel_url', self.get_index_url())
else: else:
instance = kwargs['model_instance'] instance = kwargs['model_instance']
kwargs.setdefault('cancel_url', self.get_action_url('view', instance)) kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
defaults.update(kwargs) defaults.update(kwargs)
return defaults return defaults
@ -4832,6 +4876,12 @@ class MasterView(View):
config.add_view(cls, attr='edit_help', config.add_view(cls, attr='edit_help',
route_name='{}.edit_help'.format(route_prefix), route_name='{}.edit_help'.format(route_prefix),
renderer='json') renderer='json')
config.add_route('{}.edit_field_help'.format(route_prefix),
'{}/edit-field-help'.format(url_prefix),
request_method='POST')
config.add_view(cls, attr='edit_field_help',
route_name='{}.edit_field_help'.format(route_prefix),
renderer='json')
# list/search # list/search
if cls.listable: if cls.listable: