Add basic support for editing page help info

site admin should be able to point help wherever they want
This commit is contained in:
Lance Edgar 2022-12-24 14:56:12 -06:00
parent ed54092268
commit 9fe9983bf9
8 changed files with 315 additions and 28 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar # Copyright © 2010-2022 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -40,6 +40,7 @@ from webhelpers2.html.tags import *
from tailbone.util import (csrf_token, get_csrf_token, from tailbone.util import (csrf_token, get_csrf_token,
pretty_datetime, raw_datetime, pretty_datetime, raw_datetime,
render_markdown,
route_exists) route_exists)

View file

@ -112,6 +112,19 @@ header .level .theme-picker {
margin-top: 1em; margin-top: 1em;
} }
/******************************
* markdown
******************************/
.rendered-markdown p,
.rendered-markdown ul {
margin-bottom: 1rem;
}
.rendered-markdown .codehilite {
margin-bottom: 2rem;
}
/****************************** /******************************
* fix datepicker within modals * fix datepicker within modals
* TODO: someday this may not be necessary? cf. * TODO: someday this may not be necessary? cf.

View file

@ -5,21 +5,6 @@
<%def name="content_title()"></%def> <%def name="content_title()"></%def>
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.content.result p {
margin-bottom: 1rem;
}
.content.result .codehilite {
margin-bottom: 2rem;
}
</style>
</%def>
<%def name="page_content()"> <%def name="page_content()">
<b-field horizontal label="App Prefix" <b-field horizontal label="App Prefix"
@ -290,7 +275,7 @@
</p> </p>
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="content result">${rendered_result or ""|n}</div> <div class="content result rendered-markdown">${rendered_result or ""|n}</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,204 @@
## -*- coding: utf-8; -*-
<%def name="render_template()">
<script type="text/x-template" id="page-help-template">
<div>
% if help_url or help_markdown:
<b-field>
<p class="control">
<b-button icon-pack="fas"
icon-left="question-circle"
% if help_markdown:
@click="displayInit()"
% elif help_url:
tag="a" href="${help_url}"
target="_blank"
% endif
>
Help
</b-button>
</p>
% if can_edit_help:
<p class="control">
<b-button @click="configureInit()">
<span><i class="fa fa-cog"></i></span>
</b-button>
</p>
% endif
</b-field>
% elif can_edit_help:
<b-field>
<p class="control">
<b-button @click="configureInit()">
<span><i class="fa fa-question-circle"></i></span>
<span><i class="fa fa-cog"></i></span>
</b-button>
</p>
</b-field>
% endif
% if help_markdown:
<b-modal has-modal-card
:active.sync="displayShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">${index_title}</p>
</header>
<section class="modal-card-body">
${h.render_markdown(help_markdown)}
</section>
<footer class="modal-card-foot">
% if help_url:
<b-button type="is-primary"
icon-pack="fas"
icon-left="external-link-alt"
tag="a" href="${help_url}"
target="_blank">
More Info
</b-button>
% endif
<b-button @click="displayShowDialog = false">
Close
</b-button>
</footer>
</div>
</b-modal>
% endif
% if can_edit_help:
<b-modal has-modal-card
:active.sync="configureShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Configure Help</p>
</header>
<section class="modal-card-body">
<p class="block">
This help info applies to all views with the current
route prefix.
</p>
<b-field grouped>
<b-field label="Route Prefix">
<span>${route_prefix}</span>
</b-field>
<b-field label="URL Prefix">
<span>${master.get_url_prefix()}</span>
</b-field>
</b-field>
<b-field label="Help Link (URL)">
<b-input v-model="helpURL"
ref="helpURL">
</b-input>
</b-field>
<b-field label="Markdown Text">
<b-input v-model="markdownText"
type="textarea" rows="8">
</b-input>
</b-field>
</section>
<footer class="modal-card-foot">
<b-button @click="configureShowDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
@click="configureSave()"
:disabled="configureSaving"
icon-pack="fas"
icon-left="save">
{{ configureSaving ? "Working, please wait..." : "Save" }}
</b-button>
</footer>
</div>
</b-modal>
% endif
</div>
</script>
</%def>
<%def name="declare_vars()">
<script type="text/javascript">
let PageHelp = {
template: '#page-help-template',
mixins: [FormPosterMixin],
methods: {
displayInit() {
this.displayShowDialog = true
},
configureInit() {
this.configureShowDialog = true
this.$nextTick(() => {
this.$refs.helpURL.focus()
})
},
% if can_edit_help:
configureSave() {
this.configureSaving = true
let url = '${url('{}.edit_help'.format(route_prefix))}'
let params = {
help_url: this.helpURL,
markdown_text: this.markdownText,
}
this.submitForm(url, params, response => {
this.configureShowDialog = false
this.$buefy.toast.open({
message: "Info was saved; please refresh page to see changes.",
type: 'is-info',
duration: 4000, // 4 seconds
})
this.configureSaving = false
}, response => {
this.configureSaving = false
})
},
% endif
},
}
let PageHelpData = {
displayShowDialog: false,
configureShowDialog: false,
configureSaving: false,
helpURL: ${json.dumps(help_url or None)|n},
markdownText: ${json.dumps(help_markdown or None)|n},
}
</script>
</%def>
<%def name="make_component()">
<script type="text/javascript">
PageHelp.data = function() { return PageHelpData }
Vue.component('page-help', PageHelp)
</script>
</%def>

View file

@ -3,6 +3,7 @@
<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" />
<%namespace name="base_meta" file="/base_meta.mako" /> <%namespace name="base_meta" file="/base_meta.mako" />
<%namespace file="/formposter.mako" import="declare_formposter_mixin" /> <%namespace file="/formposter.mako" import="declare_formposter_mixin" />
<%namespace name="page_help" file="/page_help.mako" />
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -383,17 +384,9 @@
</div> </div>
% endif % endif
## Help Button
% if help_url is not Undefined and help_url:
<div class="level-item"> <div class="level-item">
<b-button tag="a" href="${help_url}" <page-help></page-help>
target="_blank"
icon-pack="fas"
icon-left="fas fa-question-circle">
Help
</b-button>
</div> </div>
% endif
## Feedback Button / Dialog ## Feedback Button / Dialog
% if request.has_perm('common.feedback'): % if request.has_perm('common.feedback'):
@ -466,6 +459,8 @@
</div> </div>
</script> </script>
${page_help.render_template()}
<script type="text/x-template" id="feedback-template"> <script type="text/x-template" id="feedback-template">
<div> <div>
@ -736,6 +731,7 @@
</%def> </%def>
<%def name="declare_whole_page_vars()"> <%def name="declare_whole_page_vars()">
${page_help.declare_vars()}
${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))} ${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
<script type="text/javascript"> <script type="text/javascript">
@ -811,6 +807,8 @@
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
${page_help.make_component()}
<script type="text/javascript"> <script type="text/javascript">
FeedbackForm.data = function() { return FeedbackFormData } FeedbackForm.data = function() { return FeedbackFormData }

View file

@ -33,6 +33,8 @@ import pytz
import humanize import humanize
import logging import logging
import markdown
from rattail.time import timezone, make_utc from rattail.time import timezone, make_utc
from rattail.files import resource_path from rattail.files import resource_path
@ -181,6 +183,16 @@ 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):
"""
Render the given markdown text as HTML.
"""
kwargs.setdefault('extensions', ['fenced_code', 'codehilite'])
md = markdown.markdown(text, **kwargs)
md = HTML.literal(md)
return HTML.tag('div', class_='rendered-markdown', c=[md])
def set_app_theme(request, theme, session=None): def set_app_theme(request, theme, session=None):
""" """
Set the app theme. This modifies the *global* Mako template lookup Set the app theme. This modifies the *global* Mako template lookup

View file

@ -287,6 +287,8 @@ class CommonView(View):
# permissions # permissions
config.add_tailbone_permission_group('common', "(common)", overwrite=False) config.add_tailbone_permission_group('common', "(common)", overwrite=False)
config.add_tailbone_permission('common', 'common.edit_help',
"Edit help info for *any* page")
# home # home
config.add_route('home', '/') config.add_route('home', '/')

View file

@ -2267,6 +2267,16 @@ class MasterView(View):
so if you like you can return a different help URL depending on which so if you like you can return a different help URL depending on which
type of CRUD view is in effect, etc. type of CRUD view is in effect, etc.
""" """
model = self.model
route_prefix = self.get_route_prefix()
# nb. self.Session may differ, so use tailbone.db.Session
info = Session.query(model.TailbonePageHelp)\
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
.first()
if info and info.help_url:
return info.help_url
if self.help_url: if self.help_url:
return self.help_url return self.help_url
@ -2275,6 +2285,54 @@ class MasterView(View):
return global_help_url(self.rattail_config) return global_help_url(self.rattail_config)
def get_help_markdown(self):
"""
Return the markdown help text for current page, if defined.
"""
model = self.model
route_prefix = self.get_route_prefix()
# nb. self.Session may differ, so use tailbone.db.Session
info = Session.query(model.TailbonePageHelp)\
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
.first()
if info and info.markdown_text:
return info.markdown_text
def edit_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='help_url',
missing=None))
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.TailbonePageHelp)\
.filter(model.TailbonePageHelp.route_prefix == route_prefix)\
.first()
if not info:
info = model.TailbonePageHelp(route_prefix=route_prefix)
Session.add(info)
info.help_url = form.validated['help_url']
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.
@ -2296,6 +2354,9 @@ class MasterView(View):
'action_url': self.get_action_url, 'action_url': self.get_action_url,
'grid_index': self.grid_index, 'grid_index': self.grid_index,
'help_url': self.get_help_url(), 'help_url': self.get_help_url(),
'help_markdown': self.get_help_markdown(),
'can_edit_help': (self.has_perm('edit_help')
or self.request.has_perm('common.edit_help')),
'quickie': None, 'quickie': None,
} }
@ -4761,6 +4822,17 @@ class MasterView(View):
'prevent_cache_for_index_views', 'prevent_cache_for_index_views',
default=True) default=True)
# edit help info
config.add_tailbone_permission(permission_prefix,
'{}.edit_help'.format(permission_prefix),
"Edit help info for {}".format(model_title_plural))
config.add_route('{}.edit_help'.format(route_prefix),
'{}/edit-help'.format(url_prefix),
request_method='POST')
config.add_view(cls, attr='edit_help',
route_name='{}.edit_help'.format(route_prefix),
renderer='json')
# list/search # list/search
if cls.listable: if cls.listable:
config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix), config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix),