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 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(' '))
|
||||||
|
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>
|
# 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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue