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'}:

Reply via email to