changeset 0c6cf104c008 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=0c6cf104c008
description:
Add HTML editor
issue8004
review54481002
diffstat:
CHANGELOG | 1 +
doc/topics/configuration.rst | 38 ++++++++
trytond/ir/__init__.py | 3 +
trytond/ir/action.py | 22 ++++
trytond/ir/message.xml | 3 +
trytond/ir/routes.py | 156 +++++++++++++++++++++++++++++++++
trytond/ir/ui/form.rnc | 47 +++++----
trytond/ir/ui/form.rng | 41 ++++----
trytond/ir/view/action_report_form.xml | 5 +-
trytond/protocols/wrappers.py | 13 ++-
trytond/wsgi.py | 5 +-
11 files changed, 286 insertions(+), 48 deletions(-)
diffs (485 lines):
diff -r bbae9fd78f9e -r 0c6cf104c008 CHANGELOG
--- a/CHANGELOG Thu Mar 28 19:22:49 2019 +0100
+++ b/CHANGELOG Sat Mar 30 16:04:45 2019 +0100
@@ -1,3 +1,4 @@
+* Add HTML widget
* Support import of native numeric, date and datetime data
* Add day view on calendar
* Raise NotImplementedError when setting Function field without setter
diff -r bbae9fd78f9e -r 0c6cf104c008 doc/topics/configuration.rst
--- a/doc/topics/configuration.rst Thu Mar 28 19:22:49 2019 +0100
+++ b/doc/topics/configuration.rst Sat Mar 30 16:04:45 2019 +0100
@@ -425,6 +425,44 @@
Default: `5`
+html
+----
+
+src
+~~~
+
+The URL pointing to `TinyMCE <https://www.tiny.cloud/>`_ editor.
+
+Default: `https://cloud.tinymce.com/stable/tinymce.min.js`
+
+plugins
+~~~~~~~
+
+The space separated list of TinyMCE plugins to load.
+It can be overridden for specific models and fields using the names:
+`plugins-<model>-<field>` or `plugins-<model>`.
+
+Default: ``
+
+css
+~~~
+
+The JSON list of CSS files to load.
+It can be overridden for specific models and fields using the names:
+`css-<model>-<field>` or `css-<model>`.
+
+Default: `[]`
+
+class
+~~~~~
+
+The class to add on the body.
+It can be overridden for specific models and fields using the names:
+`class-<model>-<field>` or `class-<model>`.
+
+Default: `''`
+
+
.. _JSON-RPC: http://en.wikipedia.org/wiki/JSON-RPC
.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
.. _RFC-3986: http://tools.ietf.org/html/rfc3986
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/__init__.py
--- a/trytond/ir/__init__.py Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/__init__.py Sat Mar 30 16:04:45 2019 +0100
@@ -23,6 +23,9 @@
from . import queue
from . import calendar_
from . import message
+from . import routes
+
+__all__ = ['register', 'routes']
def register():
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/action.py
--- a/trytond/ir/action.py Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/action.py Sat Mar 30 16:04:45 2019 +0100
@@ -7,6 +7,7 @@
from sql import Null
+from trytond.config import config
from trytond.i18n import gettext
from trytond.model.exceptions import ValidationError
from ..model import (
@@ -25,6 +26,10 @@
'ActionWizard', 'ActionURL',
]
+if not config.get('html', 'plugins-ir.action.report-report_content_html'):
+ config.set(
+ 'html', 'plugins-ir.action.report-report_content_html', 'fullpage')
+
class WizardModelError(ValidationError):
pass
@@ -419,6 +424,14 @@
'get_report_content', setter='set_report_content')
report_content_name = fields.Function(fields.Char('Content Name'),
'on_change_with_report_content_name')
+ report_content_html = fields.Function(fields.Binary(
+ "Content HTML",
+ states={
+ 'invisible': ~Eval('template_extension').in_(
+ ['html', 'xhtml']),
+ },
+ depends=['template_extension']),
+ 'get_report_content_html', setter='set_report_content_html')
action = fields.Many2One('ir.action', 'Action', required=True,
ondelete='CASCADE')
direct_print = fields.Boolean('Direct Print')
@@ -615,6 +628,15 @@
def set_report_content(cls, records, name, value):
cls.write(records, {'%s_custom' % name: value})
+ @classmethod
+ def get_report_content_html(cls, reports, name):
+ return cls.get_report_content(reports, name[:-5])
+
+ @classmethod
+ def set_report_content_html(cls, reports, name, value):
+ value = value.encode('utf-8')
+ cls.set_report_content(reports, name[:-5], value)
+
@fields.depends('name', 'template_extension')
def on_change_with_report_content_name(self, name=None):
if not self.name:
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/message.xml
--- a/trytond/ir/message.xml Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/message.xml Sat Mar 30 16:04:45 2019 +0100
@@ -203,5 +203,8 @@
<record model="ir.message" id="msg_trigger_invalid_condition">
<field name="text">Condition "%(condition)s" is not a valid PYSON
expression for trigger "%(trigger)s".</field>
</record>
+ <record model="ir.message" id="msg_html_editor_save_fail">
+ <field name="text">Failed to save, please retry.</field>
+ </record>
</data>
</tryton>
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/routes.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/ir/routes.py Sat Mar 30 16:04:45 2019 +0100
@@ -0,0 +1,156 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+try:
+ from http import HTTPStatus
+except ImportError:
+ from http import client as HTTPStatus
+
+from werkzeug.utils import redirect
+from werkzeug.wrappers import Response
+
+from trytond.i18n import gettext
+from trytond.config import config
+from trytond.wsgi import app
+from trytond.protocols.wrappers import with_pool, with_transaction
+from trytond.transaction import Transaction
+
+SOURCE = config.get(
+ 'html', 'src', default='https://cloud.tinymce.com/stable/tinymce.min.js')
+
+
+def get_token(record):
+ return str((record.write_date or record.create_date).timestamp())
+
+
+def get_config(names, section='html', default=None):
+ names = names[:]
+ while names:
+ value = config.get(section, '-'.join(names))
+ if value is not None:
+ return value
+ names = names[:-1]
+ return default
+
+
[email protected]('/<database_name>/ir/html/<model>/<int:record>/<field>',
+ methods={'GET', 'POST'})
[email protected]_required
+@with_pool
+@with_transaction(
+ user='request', context=dict(_check_access=True, fuzzy_translation=True))
+def html_editor(request, pool, model, record, field):
+ Field = pool.get('ir.model.field')
+ field, = Field.search([
+ ('name', '=', field),
+ ('model.model', '=', model),
+ ])
+
+ transaction = Transaction()
+ language = request.args.get('language', transaction.language)
+ with transaction.set_context(language=language):
+ Model = pool.get(model)
+ record = Model(record)
+
+ status = HTTPStatus.OK
+ error = ''
+ if request.method == 'POST':
+ setattr(record, field.name, request.form['text'])
+ if request.form['_csrf_token'] == get_token(record):
+ record.save()
+ return redirect(request.url)
+ else:
+ status = HTTPStatus.BAD_REQUEST
+ error = gettext('ir.msg_html_editor_save_fail')
+
+ csrf_token = get_token(record)
+ text = getattr(record, field.name)
+ if isinstance(text, bytes):
+ text = text.decode('utf-8')
+ title = '%(model)s "%(name)s" %(field)s - %(title)s' % {
+ 'model': field.model.name,
+ 'name': record.rec_name,
+ 'field': field.field_description,
+ 'title': request.args.get('title', "Tryton"),
+ }
+
+ return Response(TEMPLATE % {
+ 'source': SOURCE,
+ 'plugins': get_config(
+ ['plugins', model, field.name], default=''),
+ 'css': get_config(
+ ['css', model, field.name], default='[]'),
+ 'class': get_config(
+ ['class', model, field.name], default="''"),
+ 'language': transaction.language,
+ 'title': title,
+ 'text': text,
+ 'csrf_token': csrf_token,
+ 'error': error,
+ }, status, content_type='text/html')
+
+
+TEMPLATE = '''<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <script src="%(source)s"></script>
+ <script>
+ tinymce.init({
+ selector: '#text',
+ language: '%(language)s',
+ plugins: 'fullscreen autosave %(plugins)s',
+ removed_menuitems: 'newdocument',
+ toolbar: 'save | undo redo | styleselect | bold italic | ' +
+ 'alignleft aligncenter alignright alignjustify | ' +
+ 'bullist numlist outdent indent | link image | close',
+ content_css: %(css)s,
+ body_class: %(class)s,
+ setup: function(editor) {
+ editor.addMenuItem('save', {
+ text: 'Save',
+ icon: 'save',
+ context: 'file',
+ cmd: 'save',
+ });
+ editor.addButton('save', {
+ title: 'Save',
+ icon: 'save',
+ cmd: 'save',
+ });
+ editor.addShortcut('ctrl+s', 'save', 'save');
+ editor.addCommand('save', function() {
+ document.form.submit();
+ });
+ editor.addButton('close', {
+ title: 'Close',
+ icon: 'remove',
+ onclick: function() {
+ window.location =
+ window.location.protocol + '//_@' +
+ window.location.host +
+ window.location.pathname;
+ window.close();
+ },
+ });
+ },
+ init_instance_callback: function(editor) {
+ editor.execCommand('mceFullScreen');
+ var error = '%(error)s';
+ if (error) {
+ editor.notificationManager.open({
+ text: error,
+ type: 'error',
+ });
+ }
+ },
+ });
+ </script>
+ <title>%(title)s</title>
+</head>
+<body>
+ <form name="form" method="post" style="display: block; text-align: center">
+ <textarea id="text" name="text">%(text)s</textarea>
+ <input name="_csrf_token" type="hidden" value="%(csrf_token)s">
+ </form>
+</body>
+</html>'''
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/ui/form.rnc
--- a/trytond/ir/ui/form.rnc Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/ui/form.rnc Sat Mar 30 16:04:45 2019 +0100
@@ -38,34 +38,35 @@
attlist.field &= [ a:defaultValue = "1" ] attribute colspan { text }?
attlist.field &=
attribute widget {
- "date"
- | "datetime"
- | "time"
- | "float"
- | "numeric"
- | "integer"
- | "selection"
+ "binary"
+ | "boolean"
+ | "callto"
| "char"
- | "password"
- | "timedelta"
- | "boolean"
- | "reference"
- | "binary"
- | "text"
- | "one2many"
+ | "date"
+ | "datetime"
+ | "dict"
+ | "email"
+ | "float"
+ | "html"
+ | "image"
+ | "integer"
| "many2many"
| "many2one"
- | "email"
- | "url"
- | "callto"
- | "sip"
- | "image"
+ | "multiselection"
+ | "numeric"
+ | "one2many"
+ | "one2one"
+ | "password"
| "progressbar"
- | "one2one"
+ | "pyson"
+ | "reference"
| "richtext"
- | "dict"
- | "multiselection"
- | "pyson"
+ | "selection"
+ | "sip"
+ | "text"
+ | "time"
+ | "timedelta"
+ | "url"
}?
attlist.field &= attribute fill { "0" | "1" }?
attlist.field &= attribute yexpand { "0" | "1" }?
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/ui/form.rng
--- a/trytond/ir/ui/form.rng Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/ui/form.rng Sat Mar 30 16:04:45 2019 +0100
@@ -182,34 +182,35 @@
<attribute>
<name ns="">widget</name>
<choice>
+ <value>binary</value>
+ <value>boolean</value>
+ <value>callto</value>
+ <value>char</value>
<value>date</value>
<value>datetime</value>
- <value>time</value>
+ <value>dict</value>
+ <value>email</value>
<value>float</value>
- <value>numeric</value>
+ <value>html</value>
+ <value>image</value>
<value>integer</value>
- <value>selection</value>
- <value>char</value>
- <value>password</value>
- <value>timedelta</value>
- <value>boolean</value>
- <value>reference</value>
- <value>binary</value>
- <value>text</value>
- <value>one2many</value>
<value>many2many</value>
<value>many2one</value>
- <value>email</value>
- <value>url</value>
- <value>callto</value>
- <value>sip</value>
- <value>image</value>
+ <value>multiselection</value>
+ <value>numeric</value>
+ <value>one2many</value>
+ <value>one2one</value>
+ <value>password</value>
<value>progressbar</value>
- <value>one2one</value>
+ <value>pyson</value>
+ <value>reference</value>
<value>richtext</value>
- <value>dict</value>
- <value>multiselection</value>
- <value>pyson</value>
+ <value>selection</value>
+ <value>sip</value>
+ <value>text</value>
+ <value>time</value>
+ <value>timedelta</value>
+ <value>url</value>
</choice>
</attribute>
</optional>
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/ir/view/action_report_form.xml
--- a/trytond/ir/view/action_report_form.xml Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/ir/view/action_report_form.xml Sat Mar 30 16:04:45 2019 +0100
@@ -24,7 +24,10 @@
<field name="icon"/>
<newline/>
<label name="report_content"/>
- <field name="report_content"/>
+ <group col="-1" id="report_content">
+ <field name="report_content" xexpand="0"/>
+ <field name="report_content_html" string="Edit" widget="html"
xexpand="0"/>
+ </group>
<label name="report"/>
<field name="report"/>
<label name="template_extension"/>
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/protocols/wrappers.py
--- a/trytond/protocols/wrappers.py Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/protocols/wrappers.py Sat Mar 30 16:04:45 2019 +0100
@@ -132,12 +132,13 @@
return wrapper
-def with_transaction(readonly=None):
+def with_transaction(readonly=None, user=0, context=None):
from trytond.worker import run_task
def decorator(func):
@wraps(func)
def wrapper(request, pool, *args, **kwargs):
+ nonlocal user, context
DatabaseOperationalError = backend.get('DatabaseOperationalError')
readonly_ = readonly # can not modify non local
if readonly_ is None:
@@ -145,13 +146,19 @@
readonly_ = False
else:
readonly_ = True
- context = {'_request': request.context}
+ if context is None:
+ context = {}
+ else:
+ context = context.copy()
+ context['_request'] = request.context
+ if user == 'request':
+ user = request.user_id
retry = config.getint('database', 'retry')
for count in range(retry, -1, -1):
if count != retry:
time.sleep(0.02 * (retry - count))
with Transaction().start(
- pool.database_name, 0, readonly=readonly_,
+ pool.database_name, user, readonly=readonly_,
context=context) as transaction:
try:
result = func(request, pool, *args, **kwargs)
diff -r bbae9fd78f9e -r 0c6cf104c008 trytond/wsgi.py
--- a/trytond/wsgi.py Thu Mar 28 19:22:49 2019 +0100
+++ b/trytond/wsgi.py Sat Mar 30 16:04:45 2019 +0100
@@ -48,7 +48,10 @@
if request.user_id:
return wrapped(*args, **kwargs)
else:
- abort(http.client.UNAUTHORIZED)
+ response = Response(
+ None, http.client.UNAUTHORIZED,
+ {'WWW-Authenticate': 'Basic realm="Tryton"'})
+ abort(http.client.UNAUTHORIZED, response=response)
def check_request_size(self, request, size=None):
if request.method not in {'POST', 'PUT', 'PATCH'}: