diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index bf508a6f..f4d5f14f 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -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 (*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 as final result + tooltip_template = HTML.tag('template', c=[tooltip], + **{'#content': 1}) + tooltip_template = tooltip_template.replace( + HTML.literal(' as final result + label_template = HTML.tag('template', c=label_contents, + **{'#label': 1}) + label_template = label_template.replace( + HTML.literal(' - return HTML.tag('b-field', c=[html], **attrs) + return HTML.tag('b-field', c=[label_template, html], **attrs) else: # hidden field diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index 11d4d6ae..c04dce21 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -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: + + % endif +%def> + <%def name="finalize_this_page_vars()"> ${parent.finalize_this_page_vars()} % if form is not Undefined: diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index c387d965..0b1e8d90 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -66,6 +66,47 @@ % if not form.readonly: ${h.end_form()} % endif + + % if can_edit_help: + + + + + Field: {{ configureFieldName }} + + + + + + + + + + + + + + + + + + + % endif + @@ -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: diff --git a/tailbone/templates/page_help.mako b/tailbone/templates/page_help.mako index cd13011e..b745965a 100644 --- a/tailbone/templates/page_help.mako +++ b/tailbone/templates/page_help.mako @@ -108,7 +108,7 @@ - + diff --git a/tailbone/util.py b/tailbone/util.py index f5457149..ca8d0933 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -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]) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 396c953e..2431b437 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -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:
Field: {{ configureFieldName }}