Add basic support for editing field help info
This commit is contained in:
parent
9fe9983bf9
commit
3befdc09e3
|
@ -48,7 +48,8 @@ from pyramid_deform import SessionFileUploadTempStore
|
|||
from pyramid.renderers import render
|
||||
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 .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
|
||||
from tailbone.exceptions import TailboneJSONFieldError
|
||||
|
@ -337,6 +338,8 @@ class Form(object):
|
|||
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
|
||||
action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form',
|
||||
vuejs_field_converters={},
|
||||
# TODO: ugh this is getting out hand!
|
||||
can_edit_help=False, edit_help_url=None, route_prefix=None,
|
||||
):
|
||||
self.fields = None
|
||||
if fields is not None:
|
||||
|
@ -375,6 +378,9 @@ class Form(object):
|
|||
self.use_buefy = use_buefy
|
||||
self.component = component
|
||||
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):
|
||||
return iter(self.fields)
|
||||
|
@ -800,6 +806,11 @@ class Form(object):
|
|||
context = kwargs
|
||||
context['form'] = self
|
||||
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', {})
|
||||
# TODO: deprecate / remove the latter option here
|
||||
if self.auto_disable_save or self.auto_disable:
|
||||
|
@ -815,6 +826,22 @@ class Form(object):
|
|||
context['render_field_readonly'] = self.render_field_readonly
|
||||
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):
|
||||
"""
|
||||
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()
|
||||
field = dform[fieldname]
|
||||
label = self.get_label(fieldname)
|
||||
markdowns = self.get_field_markdowns()
|
||||
|
||||
if self.field_visible(fieldname):
|
||||
|
||||
# these attrs will be for the <b-field> (*not* the widget)
|
||||
attrs = {
|
||||
':horizontal': 'true',
|
||||
'label': self.get_label(fieldname),
|
||||
}
|
||||
|
||||
# add some magic for file input fields
|
||||
|
@ -915,11 +943,50 @@ class Form(object):
|
|||
# render the field widget or whatever
|
||||
html = field.serialize(use_buefy=True,
|
||||
**self.get_renderer_kwargs(fieldname))
|
||||
# TODO: why do we not get HTML literal from serialize() ?
|
||||
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(' '))
|
||||
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(' '))
|
||||
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>
|
||||
return HTML.tag('b-field', c=[html], **attrs)
|
||||
return HTML.tag('b-field', c=[label_template, html], **attrs)
|
||||
|
||||
else: # hidden field
|
||||
|
||||
|
|
|
@ -58,6 +58,25 @@
|
|||
${parent.render_this_page_template()}
|
||||
</%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()">
|
||||
${parent.finalize_this_page_vars()}
|
||||
% if form is not Undefined:
|
||||
|
|
|
@ -66,6 +66,47 @@
|
|||
% if not form.readonly:
|
||||
${h.end_form()}
|
||||
% 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>
|
||||
</script>
|
||||
|
||||
|
@ -85,7 +126,29 @@
|
|||
submit${form.component_studly}() {
|
||||
this.${form.component_studly}Submitting = true
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +158,16 @@
|
|||
## 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},
|
||||
|
||||
% 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
|
||||
## for various field components to bind to...
|
||||
% if not form.readonly:
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Markdown Text">
|
||||
<b-field label="Help Text (Markdown)">
|
||||
<b-input v-model="markdownText"
|
||||
type="textarea" rows="8">
|
||||
</b-input>
|
||||
|
|
|
@ -183,12 +183,14 @@ def raw_datetime(config, value, verbose=False, as_date=False):
|
|||
return HTML.tag('span', **kwargs)
|
||||
|
||||
|
||||
def render_markdown(text, **kwargs):
|
||||
def render_markdown(text, raw=False, **kwargs):
|
||||
"""
|
||||
Render the given markdown text as HTML.
|
||||
"""
|
||||
kwargs.setdefault('extensions', ['fenced_code', 'codehilite'])
|
||||
md = markdown.markdown(text, **kwargs)
|
||||
if raw:
|
||||
return md
|
||||
md = HTML.literal(md)
|
||||
return HTML.tag('div', class_='rendered-markdown', c=[md])
|
||||
|
||||
|
|
|
@ -2333,6 +2333,40 @@ class MasterView(View):
|
|||
info.markdown_text = form.validated['markdown_text']
|
||||
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):
|
||||
"""
|
||||
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
|
||||
new form instances.
|
||||
"""
|
||||
route_prefix = self.get_route_prefix()
|
||||
defaults = {
|
||||
'request': self.request,
|
||||
'readonly': self.viewing,
|
||||
|
@ -3951,12 +3986,21 @@ class MasterView(View):
|
|||
'action_url': self.request.current_route_url(_query=None),
|
||||
'use_buefy': self.get_use_buefy(),
|
||||
'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:
|
||||
kwargs.setdefault('cancel_url', self.get_index_url())
|
||||
else:
|
||||
instance = kwargs['model_instance']
|
||||
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
|
||||
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
|
@ -4832,6 +4876,12 @@ class MasterView(View):
|
|||
config.add_view(cls, attr='edit_help',
|
||||
route_name='{}.edit_help'.format(route_prefix),
|
||||
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
|
||||
if cls.listable:
|
||||
|
|
Loading…
Reference in a new issue