From 3546f26770ac3df8be1044edf36a5f74d29738c1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 16 Feb 2017 13:15:26 +0100 Subject: [PATCH 01/31] [add] cms_form + cms_form_example --- .coveragerc | 24 ++ cms_form/README.rst | 183 ++++++++ cms_form/__init__.py | 2 + cms_form/__openerp__.py | 24 ++ cms_form/controllers/__init__.py | 1 + cms_form/controllers/main.py | 155 +++++++ cms_form/i18n/de.po | 170 ++++++++ .../cms_form_example_create_partner.png | Bin 0 -> 12328 bytes .../images/cms_form_example_edit_partner.png | Bin 0 -> 13320 bytes cms_form/images/cms_form_example_search.png | Bin 0 -> 12899 bytes cms_form/models/__init__.py | 5 + cms_form/models/cms_form.py | 223 ++++++++++ cms_form/models/cms_form_mixin.py | 395 ++++++++++++++++++ cms_form/models/cms_search_form.py | 126 ++++++ cms_form/models/test_models.py | 75 ++++ cms_form/models/website_mixin.py | 28 ++ cms_form/security/cms_form.xml | 20 + cms_form/static/description/icon.png | Bin 0 -> 9455 bytes cms_form/static/src/js/date_widget.js | 27 ++ cms_form/static/src/js/select2widgets.js | 86 ++++ cms_form/static/src/js/textarea_widget.js | 23 + cms_form/static/src/less/cms_form.less | 15 + cms_form/templates/assets.xml | 15 + cms_form/templates/form.xml | 134 ++++++ cms_form/templates/widgets.xml | 124 ++++++ cms_form/tests/__init__.py | 6 + cms_form/tests/common.py | 75 ++++ cms_form/tests/test_controllers.py | 92 ++++ cms_form/tests/test_extractors.py | 5 + cms_form/tests/test_form_base.py | 224 ++++++++++ cms_form/tests/test_form_cms.py | 95 +++++ cms_form/tests/test_form_render.py | 42 ++ cms_form/tests/test_form_search.py | 67 +++ cms_form/tests/test_loaders.py | 5 + cms_form/tests/test_widgets.py | 5 + cms_form/utils.py | 143 +++++++ cms_form/widgets.py | 82 ++++ cms_form_example/README.rst | 52 +++ cms_form_example/__init__.py | 2 + cms_form_example/__openerp__.py | 19 + cms_form_example/controllers/__init__.py | 1 + cms_form_example/controllers/main.py | 32 ++ cms_form_example/models/__init__.py | 1 + cms_form_example/models/partner.py | 38 ++ cms_form_example/static/description/icon.png | Bin 0 -> 9455 bytes .../static/src/js/cms_form_example.tour.js | 38 ++ cms_form_example/templates/assets.xml | 12 + 47 files changed, 2891 insertions(+) create mode 100644 .coveragerc create mode 100644 cms_form/README.rst create mode 100644 cms_form/__init__.py create mode 100644 cms_form/__openerp__.py create mode 100644 cms_form/controllers/__init__.py create mode 100644 cms_form/controllers/main.py create mode 100644 cms_form/i18n/de.po create mode 100644 cms_form/images/cms_form_example_create_partner.png create mode 100644 cms_form/images/cms_form_example_edit_partner.png create mode 100644 cms_form/images/cms_form_example_search.png create mode 100644 cms_form/models/__init__.py create mode 100644 cms_form/models/cms_form.py create mode 100644 cms_form/models/cms_form_mixin.py create mode 100644 cms_form/models/cms_search_form.py create mode 100644 cms_form/models/test_models.py create mode 100644 cms_form/models/website_mixin.py create mode 100644 cms_form/security/cms_form.xml create mode 100644 cms_form/static/description/icon.png create mode 100644 cms_form/static/src/js/date_widget.js create mode 100644 cms_form/static/src/js/select2widgets.js create mode 100644 cms_form/static/src/js/textarea_widget.js create mode 100644 cms_form/static/src/less/cms_form.less create mode 100644 cms_form/templates/assets.xml create mode 100644 cms_form/templates/form.xml create mode 100644 cms_form/templates/widgets.xml create mode 100644 cms_form/tests/__init__.py create mode 100644 cms_form/tests/common.py create mode 100644 cms_form/tests/test_controllers.py create mode 100644 cms_form/tests/test_extractors.py create mode 100644 cms_form/tests/test_form_base.py create mode 100644 cms_form/tests/test_form_cms.py create mode 100644 cms_form/tests/test_form_render.py create mode 100644 cms_form/tests/test_form_search.py create mode 100644 cms_form/tests/test_loaders.py create mode 100644 cms_form/tests/test_widgets.py create mode 100644 cms_form/utils.py create mode 100644 cms_form/widgets.py create mode 100644 cms_form_example/README.rst create mode 100644 cms_form_example/__init__.py create mode 100644 cms_form_example/__openerp__.py create mode 100644 cms_form_example/controllers/__init__.py create mode 100644 cms_form_example/controllers/main.py create mode 100644 cms_form_example/models/__init__.py create mode 100644 cms_form_example/models/partner.py create mode 100644 cms_form_example/static/description/icon.png create mode 100644 cms_form_example/static/src/js/cms_form_example.tour.js create mode 100644 cms_form_example/templates/assets.xml diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..d3c19d43 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,24 @@ +[run] + +branch = True + +[report] + +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + raise NotImplementedError + if __name__ == .__main__.: + +ignore_errors = True + +include = + */cms_form/* + */cms_status_message/* + +omit = + # omit anything in cms_form_example/ + */cms_form_example/* + +output = coverage.xml diff --git a/cms_form/README.rst b/cms_form/README.rst new file mode 100644 index 00000000..3644b07e --- /dev/null +++ b/cms_form/README.rst @@ -0,0 +1,183 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +CMS Form +======== + +Basic website contents form framework. Allows to define front-end forms for every models in a simple way. + +If you are tired of re-defining every time an edit form or a search form for your odoo website, +this is the module you are looking for. + +Features +======== + +* automatic form generation (create, write, search) +* automatic route generation (create, write, search) +* automatic machinery based on fields' type: + * widget rendering + * field value load (from existing instance or from request) + * field value extraction (from request) + * field value write (to existing instance) + +* highly customizable +* works with every odoo model +* works also without any model +* add handy attributes to models inheriting from ``website.published.mixin``: + * ``cms_add_url``: lead to create form view. By default ``/cms/form/create/my.model`` + * ``cms_edit_url``: lead to edit form view. By default ``/cms/form/edit/my.model/model_id`` + * ``cms_search_url``: lead to search form view. By default ``/cms/form/search/my.model`` + +Usage +===== + +Create / Edit form +------------------ + +Just inherit from ``cms.form`` to add a form for your model. Quick example for partner: + +.. code-block:: python + + class PartnerForm(models.AbstractModel): + + _name = 'cms.form.res.partner' + _inherit = 'cms.form' + _form_model = 'res.partner' + _form_model_fields = ('name', 'country_id') + _form_required_fields = ('name', 'country_id') + + +In this case you'll have form with the following characteristics: + +* works with ``res.partner`` model +* have only ``name`` and ``country_id`` fields +* both fields are required (is not possible to submit the form w/out one of those values) + +Here's the result: + +|preview_create| +|preview_edit| + +The form will be automatically available on these routes: + +* ``/cms/form/create/res.partner`` to create new partners +* ``/cms/form/edit/res.partner/1`` edit existing partners (partner id=1 in this case) + +NOTE: default generic routes work if the form's name is ``cms.form.`` + model name, like ``cms.form.res.partner``. +If you want you can easily define your own controller and give your form a different name, +and have more elegant routes like ```/partner/edit/partner-slug-1``. +Take a look at `cms_form_example <../cms_form_example>`_. + +By default, the form is rendered as an horizontal twitter bootstrap form, but you can provide your own templates of course. +By default, fields are ordered by their order in the model's schema. You can tweak it using ``_form_fields_order``. + + +Form with extra control fields +------------------------------ + +Imagine you want to notify the partner after its creation but only if you really need it. + +The form above can be extended with extra fields that are not part of the ``_form_model`` schema: + +.. code-block:: python + + class PartnerForm(models.AbstractModel): + + _name = 'cms.form.res.partner' + _inherit = 'cms.form' + _form_model = 'res.partner' + _form_model_fields = ('name', 'country_id', 'email') + _form_required_fields = ('name', 'country_id', 'email') + + notify_partner = fields.Boolean() + + def form_after_create_or_update(self, values, extra_values): + if extra_values.get('notify_partner'): + # do what you want here... + +``notify_partner`` will be included into the form but it will be discarded on create and write. +Nevertheless you can use it as a control flag before and after the record has been created or updated +using the hook ``form_after_create_or_update``, as you see in this example. + + +Search form +----------- + +Just inherit from ``cms.form.search`` to add a form for your model. Quick example for partner: + +.. code-block:: python + + class PartnerSearchForm(models.AbstractModel): + """Partner model search form.""" + + _name = 'cms.form.search.res.partner' + _inherit = 'cms.form.search' + _form_model = 'res.partner' + _form_model_fields = ('name', 'country_id', ) + _form_fields_order = ('country_id', 'name', ) + + +|preview_search| + +The form will be automatically available at: ``/cms/form/search/res.partner``. + +NOTE: default generic routes work if the form's name is ```cms.form.search`` + model name, like ``cms.form.search.res.partner``. +If you want you can easily define your own controller and give your form a different name, +and have more elegant routes like ``/partners``. +Take a look at `cms_form_example <../cms_form_example>`_. + + +Known issues / Roadmap +====================== + +* add more tests, especially per each widget and type of field +* provide better widgets for image and file fields in general +* o2m fields: to be tested at all +* move widgets to abstract models too +* search form: generate default search domain in a clever way +* add easy way to switch from horizontal to vertical form +* provide more examples +* x2x fields: allow sub-items creation +* handle api onchanges + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Sponsor +------- + +* `Fluxdock.io `_. + +Contributors +------------ + +* Simone Orsi + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. + +.. |preview_create| image:: ./images/cms_form_example_create_partner.png +.. |preview_edit| image:: ./images/cms_form_example_edit_partner.png +.. |preview_search| image:: ./images/cms_form_example_search.png diff --git a/cms_form/__init__.py b/cms_form/__init__.py new file mode 100644 index 00000000..f7209b17 --- /dev/null +++ b/cms_form/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/cms_form/__openerp__.py b/cms_form/__openerp__.py new file mode 100644 index 00000000..50797847 --- /dev/null +++ b/cms_form/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'CMS Form', + 'summary': """ + Basic content type form""", + 'version': '9.0.1.0.0', + 'license': 'AGPL-3', + 'author': 'Camptocamp SA, Odoo Community Association (OCA)', + 'depends': [ + 'website', + 'cms_status_message', + ], + 'data': [ + 'security/cms_form.xml', + 'templates/assets.xml', + 'templates/form.xml', + 'templates/widgets.xml', + ], + 'demo': [ + ], +} diff --git a/cms_form/controllers/__init__.py b/cms_form/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/cms_form/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/cms_form/controllers/main.py b/cms_form/controllers/main.py new file mode 100644 index 00000000..4adee3c0 --- /dev/null +++ b/cms_form/controllers/main.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +import werkzeug + +from openerp import http, _ +from openerp.http import request + + +class FormControllerMixin(object): + + # default template + template = 'cms_form.form_wrapper' + + def get_template(self, form, **kw): + """Retrieve rendering template. + + Defaults to `template` attribute. + Can be overridden straight in the form + using the attribute `form_wrapper_template`. + """ + template = self.template + + if getattr(form, 'form_wrapper_template', None): + template = form.form_wrapper_template + + if not template: + raise NotImplementedError("You must provide a template!") + return template + + def get_render_values(self, main_object, **kw): + """Retrieve rendering values. + + You can override this to inject more values. + """ + parent = None + if getattr(main_object, 'parent_id', None): + # get the parent if any + parent = main_object.parent_id + + kw.update({ + 'main_object': main_object, + 'parent': parent, + 'controller': self, + }) + return kw + + def _can_create(self, model, raise_exception=True): + """Check that current user can create instances of given model.""" + return request.env[model].check_access_rights( + 'create', raise_exception=raise_exception) + + def _can_edit(self, main_object, raise_exception=True): + """Check that current user can edit given main object.""" + return main_object.check_access_rights( + 'write', raise_exception=raise_exception) + + def form_model_key(self, model): + """Return a valid form model.""" + return 'cms.form.' + model + + def get_form(self, model, main_object=None, **kw): + """Retrieve form for given model or object and initialize it.""" + form_model_key = self.form_model_key(model) + if form_model_key in request.env: + form = request.env[form_model_key].form_init( + request, main_object=main_object) + else: + # TODO: enable form by default? + # How? with a flag on ir.model.model? + # And which fields to include automatically? + raise NotImplementedError( + _('%s model has no CMS form registered.') % model + ) + return form + + def check_permission(self, model, main_object): + """Check permission on current model and main object.""" + if main_object: + self._can_edit(main_object) + else: + self._can_create(model) + + def make_response(self, model, model_id=None, page=0, **kw): + """Prepare and return form response. + + :param model: an odoo model's name + :param model_id: an odoo record's id + :param page: current page if any (mostly for search forms) + :param kw: extra parameters + + How it works: + * retrieve current main object if any + * check permission on model and/or main object + * retrieve the form + * make the form process current request + * if the form is successful and has `form_redirect` attribute + it redirects to it. + * otherwise it just renders the form + """ + main_object = None + if model_id: + main_object = request.env[model].browse(model_id) + self.check_permission(model, main_object) + form = self.get_form(model, main_object=main_object) + # pass only specific extra args, to not pollute form render values + form.form_process(extra_args={'page': page}) + # search forms do not need these attrs + if getattr(form, 'form_success', None) \ + and getattr(form, 'form_redirect', None): + # anything went fine, redirect to next url + return werkzeug.utils.redirect(form.form_next_url()) + # render form wrapper + values = self.get_render_values(main_object, **kw) + values['form'] = form + return request.render( + self.get_template(form, **kw), + values, + ) + + +class CMSFormController(http.Controller, FormControllerMixin): + """CMS form controller.""" + + @http.route([ + '/cms/form/create/', + '/cms/form/edit//', + ], type='http', auth='user', website=True) + def cms_form(self, model, model_id=None, **kw): + """Handle a `form` route. + """ + return self.make_response(model, model_id=model_id, **kw) + + +class SearchFormControllerMixin(FormControllerMixin): + + template = 'cms_form.search_form_wrapper' + + def form_model_key(self, model): + return 'cms.form.search.' + model + + def check_permission(self, model, main_object): + pass + + +class CMSSearchFormController(http.Controller, SearchFormControllerMixin): + """CMS form controller.""" + + @http.route([ + '/cms/form/search/', + '/cms/form/search//page/', + ], type='http', auth='public', website=True) + def cms_form(self, model, **kw): + """Handle a search `form` route. + """ + return self.make_response(model, **kw) diff --git a/cms_form/i18n/de.po b/cms_form/i18n/de.po new file mode 100644 index 00000000..74827b5b --- /dev/null +++ b/cms_form/i18n/de.po @@ -0,0 +1,170 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cms_form +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-01-10 14:08+0000\n" +"PO-Revision-Date: 2017-01-10 14:08+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_char +msgid "A char" +msgstr "A char" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_float +msgid "A float" +msgstr "A float" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_many2many +msgid "A many2many" +msgstr "A many2many" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_many2one +msgid "A many2one" +msgstr "A many2one" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_number +msgid "A number" +msgstr "A number" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_a_one2many +msgid "A one2many" +msgstr "A one2many" + +#. module: cms_form +#: model:ir.model,name:cms_form.model_cms_form +#: model:ir.model,name:cms_form.model_cms_form_mixin +#: model:ir.model,name:cms_form.model_cms_form_res_partner +#: model:ir.model,name:cms_form.model_cms_form_test_fields +#: model:ir.model,name:cms_form.model_cms_form_test_partner +#: model:ir.model,name:cms_form.model_cms_search_form +msgid "CMS Form mixin" +msgstr "CMS Form mixin" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_website_published_mixin_cms_edit_url +msgid "CMS edit URL" +msgstr "CMS edit URL" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.base_form +msgid "Cancel" +msgstr "Abbrechen" + +#. module: cms_form +#: code:addons/cms_form/models/cms_form.py:285 +#: code:addons/external-src/website-cms/cms_form/models/cms_form.py:285 +#, python-format +msgid "Create %s" +msgstr "Anlegen %s" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_partner_custom +msgid "Custom" +msgstr "Custom" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_display_name +#: model:ir.model.fields,field_description:cms_form.field_cms_form_mixin_display_name +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_display_name +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_partner_display_name +msgid "Display Name" +msgstr "Angezeigter Name" + +#. module: cms_form +#: code:addons/cms_form/models/cms_form.py:283 +#: code:addons/external-src/website-cms/cms_form/models/cms_form.py:283 +#, python-format +msgid "Edit \"%s\"" +msgstr "Bearbeiten \"%s\"" + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form_id +#: model:ir.model.fields,field_description:cms_form.field_cms_form_mixin_id +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields_id +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_partner_id +msgid "ID" +msgstr "ID" + +#. module: cms_form +#: code:addons/cms_form/models/cms_form.py:336 +#: code:addons/external-src/website-cms/cms_form/models/cms_form.py:336 +#, python-format +msgid "Item created." +msgstr "Objekt wurde erstellt." + +#. module: cms_form +#: code:addons/cms_form/models/cms_form.py:341 +#: code:addons/external-src/website-cms/cms_form/models/cms_form.py:341 +#, python-format +msgid "Item updated." +msgstr "Objekt wurde aktualisiert." + +#. module: cms_form +#: model:ir.model.fields,field_description:cms_form.field_cms_form___last_update +#: model:ir.model.fields,field_description:cms_form.field_cms_form_mixin___last_update +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_fields___last_update +#: model:ir.model.fields,field_description:cms_form.field_cms_form_test_partner___last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.form_horizontal_field_wrapper +msgid "Name" +msgstr "Bezeichnung" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.base_form +msgid "Required fields" +msgstr "Pflichtfelder" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.field_widget_m2o +msgid "Select..." +msgstr "Select..." + +#. module: cms_form +#: code:addons/cms_form/models/cms_form.py:345 +#: code:addons/external-src/website-cms/cms_form/models/cms_form.py:345 +#, python-format +msgid "Some required fields are empty." +msgstr "Bitte füllen Sie alle Pflichtfelder aus." + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.base_form +msgid "Submit" +msgstr "Absenden" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.field_widget_date +msgid "YYYY-MM-DD" +msgstr "JJJJ-MM-TT" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.field_widget_image +msgid "Keep current image" +msgstr "Aktuelles Bild beibehalten" + +#. module: cms_form +#: model:ir.ui.view,arch_db:cms_form.field_widget_image +msgid "Replace current image" +msgstr "Aktuelles Bild ersetzen" + +#. module: cms_form +#: model:ir.model,name:cms_form.model_website_published_mixin +msgid "website.published.mixin" +msgstr "website.published.mixin" diff --git a/cms_form/images/cms_form_example_create_partner.png b/cms_form/images/cms_form_example_create_partner.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5f33c0181d9199b7cf469b484ae6e675bbd9c7 GIT binary patch literal 12328 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 literal 0 HcmV?d00001 diff --git a/cms_form/images/cms_form_example_edit_partner.png b/cms_form/images/cms_form_example_edit_partner.png new file mode 100644 index 0000000000000000000000000000000000000000..73be6518d1a4161a0f0f51a06c134cbd7db797b4 GIT binary patch literal 13320 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 `_form_fields_attributes` + """ + form = self.new() + form.o_request = request # odoo wrapped request + form.request = request.httprequest # werkzeug request, the "real" one + form.main_object = main_object + # override `_form_` parameters + for k, v in kw.iteritems(): + if not inspect.ismethod(getattr(form, '_form_' + k)): + setattr(form, '_form_' + k, v) + return form + + @property + def form_title(self): + return '' + + @property + def form_description(self): + return '' + + @property + def form_mode(self): + if self._form_mode: + # forced mode + return self._form_mode + if self.main_object: + return 'edit' + return 'create' + + @property + def form_model(self): + return self.env[self._form_model] + + # TODO: cache fields per form instance? + # if we do it we must take into account + # some fields attributes (like widgets) + # that may vary on a per-request base. + def form_fields(self): + """Retrieve form fields ready to be used. + + Fields lookup: + * model's fields + * form's fields + + Blacklisted fields are skipped. + Whitelisted fields are loaded only. + """ + _all_fields = OrderedDict() + # load model fields + _model_fields = {} + if self._form_model: + _model_fields = self.form_model.fields_get( + self._form_model_fields, + attributes=self._form_fields_attributes) + # load form fields + _form_fields = self.fields_get(attributes=self._form_fields_attributes) + _all_fields.update(_model_fields) + # form fields override model fields + _all_fields.update(_form_fields) + # exclude blacklisted + for fname in self._form_fields_blacklist: + # make it fail if passing wrong field name + _all_fields.pop(fname) + # include whitelisted + _all_whitelisted = {} + for fname in self._form_fields_whitelist: + _all_whitelisted[fname] = _all_fields[fname] + _all_fields = _all_whitelisted or _all_fields + # remove unwanted fields + self._form_remove_uwanted(_all_fields) + # remove non-stored fields to exclude computed + _all_fields = {k: v for k, v in _all_fields.iteritems() if v['store']} + # update fields attributes + self.form_update_fields_attributes(_all_fields) + # update fields order + if self._form_fields_order: + _sorted_all_fields = OrderedDict() + for fname in self._form_fields_order: + _sorted_all_fields[fname] = _all_fields[fname] + _all_fields = _sorted_all_fields + return _all_fields + + def _form_remove_uwanted(self, _all_fields): + """Remove fields from form fields.""" + for fname in self.__form_fields_ignore: + _all_fields.pop(fname, None) + + def form_update_fields_attributes(self, _fields): + """Manipulate fields attributes.""" + for fname, field in _fields.iteritems(): + if fname in self._form_required_fields: + _fields[fname]['required'] = True + field['widget'] = widgets.CharWidget(self, fname, field) + for key in (field['type'], fname): + if key in self._form_widgets: + widget = self._form_widgets[key] + field['widget'] = widget(self, fname, field) + + @property + def form_file_fields(self): + """File fields.""" + return { + k: v for k, v in self.form_fields().iteritems() + if v['type'] == 'binary' + } + + def form_get_request_values(self): + """Retrieve fields values from current request.""" + # on POST requests values are in `form` attr + # on GET requests values are in `args` attr + _values = self.request.args or self.request.form + # normal fields + values = { + k: v for k, v in _values.iteritems() + if k not in ('csrf_token', ) + } + # file fields + values.update( + {k: v for k, v in self.request.files.iteritems()} + ) + return values + + def form_load_defaults(self, main_object=None, request_values=None): + """Load default values. + + Values lookup order: + + 1. `main_object` fields' values (if an existing main_object is passed) + 2. request parameters (only parameters matching form fields names) + """ + main_object = main_object or self.main_object + request_values = request_values or self.form_get_request_values() + defaults = request_values.copy() + form_fields = self.form_fields() + for fname, field in form_fields.iteritems(): + value = None + # we could have form-only fields (like `custom` in test form below) + if main_object and fname in main_object: + value = main_object[fname] + # maybe a POST request with new values: override item value + value = request_values.get(fname, value) + loader = self.form_get_loader( + fname, field, + main_object=main_object, value=value, **request_values) + if loader: + value = loader( + self, main_object, fname, value, **request_values) + defaults[fname] = value + return defaults + + def form_get_loader(self, fname, field, + main_object=None, value=None, **req_values): + """Retrieve form value loader. + + :param fname: field name + :param field: field description as `fields_get` + :param main_object: current main object if any + :param value: current field value if any + :param req_values: custom request valuess + """ + # 1nd lookup for a default type / name loader + loader = self._form_loaders.get( + field['type'], self._form_loaders.get(fname, None)) + if loader: + value = loader( + self, main_object, fname, value, **req_values) + # 2nd lookup for a specific type loader method + loader = getattr( + self, '_form_load_' + field['type'], loader) + # 3rd lookup and override by named loader if any + loader = getattr( + self, '_form_load_' + fname, loader) + return loader + + def form_extract_values(self, **request_values): + """Extract values from request form.""" + request_values = request_values or self.form_get_request_values() + values = {} + for fname, field in self.form_fields().iteritems(): + value = request_values.get(fname) + extractor = self.form_get_extractor( + fname, field, value=value, **request_values) + if extractor: + value = extractor(self, fname, value, **request_values) + if value is None: + # we assume we do not want to override the field value. + # a typical example is an image field. + # If you have an existing image + # you cannot set the default value on the file input + # for standard HTML security restrictions. + # If you want to flush a value on a field just return "False". + continue + values[fname] = value + return values + + def form_get_extractor(self, fname, field, value=None, **req_values): + """Retrieve form value extractor. + + :param fname: field name + :param field: field description as `fields_get` + :param value: current field value if any + :param req_values: custom request valuess + """ + # 1nd lookup for a default type or name handler + extractor = self._form_extractors.get( + field['type'], self._form_extractors.get(fname, None)) + # 2nd lookup for a specific type handler + extractor = getattr( + self, '_form_extract_' + field['type'], extractor) + # 3rd lookup and override by named handler if any + extractor = getattr( + self, '_form_extract_' + fname, extractor) + return extractor + + __form_render_values = {} + + @property + def form_render_values(self): + """Values used to render the form.""" + if not self.__form_render_values: + # default render values + self.__form_render_values = { + 'main_object': self.main_object, + 'form': self, + 'form_data': {}, + 'errors': {}, + 'errors_messages': {}, + } + return self.__form_render_values + + @form_render_values.setter + def form_render_values(self, value): + self.__form_render_values = value + + def form_render(self, **kw): + """Renders form template declared in `form_template`. + + To render the form simply do: + + + """ + values = self.form_render_values.copy() + values.update(kw) + return self.env.ref(self.form_template).render(values) + + def form_process(self, **kw): + """Process current request. + + :param kw: inject custom / extra rendering values. + + Lookup correct request handler by request method + and call it with rendering values. + The handler can perform any action (like creating objects) + and then return final rendering form values + and store them into `form_render_values`. + """ + render_values = self.form_render_values + render_values.update(kw) + render_values['form_data'] = self.form_load_defaults() + handler = getattr(self, 'form_process_' + self.request.method.upper()) + render_values.update(handler(render_values)) + self.form_render_values = render_values + + def form_process_GET(self, render_values): + """Process GET requests.""" + return render_values + + def form_process_POST(self, render_values): + """Process POST requests.""" + raise NotImplementedError() + + @property + def form_wrapper_css_klass(self): + """Return form wrapper css klass. + + By default the form markup is wrapped + into a `cms_form_wrapper` element. + You can use this set of klasses to customize form styles. + + Included by default: + * `cms_form_wrapper` marker + * form model name normalized (res.partner -> res_partner) + * `_form_wrapper_extra_css_klass` extra klasses from form attribute + * `mode_` + form mode (ie: 'mode_write') + """ + parts = [ + 'cms_form_wrapper', + self._form_model.replace('.', '_').lower(), + self._form_wrapper_extra_css_klass, + 'mode_' + self.form_mode, + ] + return ' '.join([x.strip() for x in parts if x.strip()]) + + @property + def form_css_klass(self): + """Return `
` element css klasses. + + By default you can provide extra klasses via `_form_extra_css_klass`. + """ + return self._form_extra_css_klass diff --git a/cms_form/models/cms_search_form.py b/cms_form/models/cms_search_form.py new file mode 100644 index 00000000..eeef6414 --- /dev/null +++ b/cms_form/models/cms_search_form.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, _ + + +class CMSFormSearch(models.AbstractModel): + _name = 'cms.form.search' + _inherit = 'cms.form.mixin' + + form_buttons_template = 'cms_form.search_form_buttons' + form_search_results_template = 'cms_form.search_results' + form_action = '' + form_method = 'GET' + _form_mode = 'search' + _form_extract_value_mode = 'read' + # show results if no query has been submitted? + _form_show_results_no_submit = 1 + _form_results_per_page = 10 + # sort by this param, defaults to model's `_order` + _form_results_orderby = '' + + def form_update_fields_attributes(self, _fields): + """No field should be mandatory.""" + super(CMSFormSearch, self).form_update_fields_attributes(_fields) + for fname, field in _fields.iteritems(): + field['required'] = False + + __form_search_results = {} + + @property + def form_search_results(self): + """Return search results.""" + return self.__form_search_results + + @form_search_results.setter + def form_search_results(self, value): + self.__form_search_results = value + + @property + def form_title(self): + title = _('Search') + if self._form_model: + model = self.env['ir.model'].search( + [('model', '=', self._form_model)]) + name = model and model.name or '' + title = _('Search %s') % name + return title + + def form_process_GET(self, render_values): + self.form_search(render_values) + return render_values + + def form_search(self, render_values): + """Produce search results.""" + search_values = self.form_extract_values() + if not search_values and not self._form_show_results_no_submit: + return self.form_search_results + domain = self.form_search_domain(search_values) + count = self.form_model.search_count(domain) + page = render_values.get('extra_args', {}).get('page', 0) + url = render_values.get('extra_args', {}).get('pager_url', '') + if self._form_model: + url = getattr(self.form_model, 'cms_search_url', url) + pager = self._form_results_pager(count=count, page=page, url=url) + order = self._form_results_orderby or None + results = self.form_model.search( + domain, + limit=self._form_results_per_page, + offset=pager['offset'], + order=order + ) + self.form_search_results = { + 'results': results, + 'count': count, + 'pager': pager, + } + return self.form_search_results + + def pager(self, **kw): + return self.env['website'].pager(**kw) + + def _form_results_pager(self, count=None, page=0, url='', url_args=None): + """Prepare pager for current search.""" + url_args = url_args or self.request.args.to_dict() + count = count + return self.pager( + url=url, + total=count, + page=page, + step=self._form_results_per_page, + scope=self._form_results_per_page, + url_args=url_args + ) + + def form_search_domain(self, search_values): + """Build search domain.""" + domain = [] + for fname, field in self.form_fields().iteritems(): + value = search_values.get(fname) + if value is None: + continue + if field['type'] in ('many2one', ) and value < 1: + # we need an existing ID here ( > 0) + continue + # TODO: find the way to properly handle this. + # It would be nice to guess leafs in a clever way. + operator = '=' + if field['type'] in ('char', 'text'): + operator = 'ilike' + value = '%{}%'.format(value) + elif field['type'] in ('integer', 'float', 'many2one'): + operator = '=' + elif field['type'] in ('one2many', 'many2many'): + if not value: + continue + operator = 'in' + elif field['type'] in ('many2one', ) and not value: + # we need an existing ID here ( > 0) + continue + elif field['type'] in ('boolean', ): + value = value == 'on' and True + leaf = (fname, operator, value) + domain.append(leaf) + return domain diff --git a/cms_form/models/test_models.py b/cms_form/models/test_models.py new file mode 100644 index 00000000..81904874 --- /dev/null +++ b/cms_form/models/test_models.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import fields, models, tools + +testing = tools.config.get('test_enable') + + +if testing: + class TestPartnerForm(models.AbstractModel): + """A test model form.""" + + _name = 'cms.form.res.partner' + _inherit = 'cms.form' + _form_model = 'res.partner' + _form_model_fields = ('name', 'country_id') + _form_required_fields = ('name', 'country_id') + + custom = fields.Char() + + def _form_load_custom( + self, form, main_object, fname, value, **req_values): + return req_values.get('custom', 'oh yeah!') + + class TestSearchPartnerForm(models.AbstractModel): + """A test model search form.""" + + _name = 'cms.form.search.res.partner' + _inherit = 'cms.form.search' + _form_model = 'res.partner' + _form_model_fields = ('name', 'country_id') + + def form_search_domain(self, search_values): + """Force domain to include only test-created records.""" + domain = super( + TestSearchPartnerForm, self + ).form_search_domain(search_values) + # we use this attr in tests to limit search results + # to test records' scope + include_only_ids = getattr(self, 'test_record_ids', []) + if include_only_ids: + domain.append(('id', 'in', include_only_ids)) + return domain + + class TestFieldsForm(models.AbstractModel): + """A test model form.""" + + _name = 'cms.form.test_fields' + _inherit = 'cms.form' + + a_char = fields.Char() + a_number = fields.Integer() + a_float = fields.Float() + # fake relation fields + a_many2one = fields.Char() + a_one2many = fields.Char() + a_many2many = fields.Char() + + def form_fields(self): + _fields = super(TestFieldsForm, self).form_fields() + # fake fields' types + _fields['a_many2one']['type'] = 'many2one' + _fields['a_many2many']['type'] = 'many2many' + _fields['a_one2many']['type'] = 'one2many' + return _fields + + def _form_validate_a_float(self, value, **request_values): + """Specific validator for `a_float` field.""" + value = float(value or '0') + return not value > 5, 'Must be greater than 5!' + + def _form_validate_char(self, value, **request_values): + """Specific validator for all `char` fields.""" + return not len(value) > 8, 'Text lenght must be greater than 8!' diff --git a/cms_form/models/website_mixin.py b/cms_form/models/website_mixin.py new file mode 100644 index 00000000..6eb82c1c --- /dev/null +++ b/cms_form/models/website_mixin.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from openerp import api, fields, models + + +class WebsitePublishedMixin(models.AbstractModel): + _inherit = "website.published.mixin" + + @property + def cms_add_url(self): + return '/cms/form/create/{}'.format(self._name) + + @property + def cms_search_url(self): + return '/cms/form/search/{}'.format(self._name) + + cms_edit_url = fields.Char( + string='CMS edit URL', + compute='_compute_cms_edit_url', + readonly=True, + ) + + @api.multi + def _compute_cms_edit_url(self): + for item in self: + item.cms_edit_url = \ + '/cms/form/edit/{}/{}'.format(item._name, item.id) diff --git a/cms_form/security/cms_form.xml b/cms_form/security/cms_form.xml new file mode 100644 index 00000000..96f8afe7 --- /dev/null +++ b/cms_form/security/cms_form.xml @@ -0,0 +1,20 @@ + + + + + + + + cms.form access name + + + + + + + + + + + diff --git a/cms_form/static/description/icon.png b/cms_form/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/cms_form/static/src/js/date_widget.js b/cms_form/static/src/js/date_widget.js new file mode 100644 index 00000000..4b87e153 --- /dev/null +++ b/cms_form/static/src/js/date_widget.js @@ -0,0 +1,27 @@ +odoo.define('cms_form.date_widget', function (require) { + 'use strict'; + + $(document).ready(function () { + $("input.js_datepicker").each(function(){ + var $input = $(this); + $input.datepicker({ + useSeconds: false, + icons : { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + up: 'fa fa-chevron-up', + down: 'fa fa-chevron-down' + }, + dateFormat : $input.data('format') + }).datepicker("setDate", new Date()); + $input + .closest('.input-group') + .find('.js_datepicker_trigger').click(function(){ + $input.datepicker('show'); + }) + }) + + + }); + +}); diff --git a/cms_form/static/src/js/select2widgets.js b/cms_form/static/src/js/select2widgets.js new file mode 100644 index 00000000..a8562893 --- /dev/null +++ b/cms_form/static/src/js/select2widgets.js @@ -0,0 +1,86 @@ +odoo.define('cms_form.select2widgets', function (require) { + 'use strict'; + + var Model = require('web.Model'); + var ajax = require('web.ajax'); + var core = require('web.core'); + var base = require('web_editor.base'); + + var _t = core._t; + + + if(!$('.js_select2_m2m_widget').length) { + return $.Deferred().reject("DOM doesn't contain '.js_select2_m2m_widget'"); + } + + $(document).ready(function () { + + $('input.js_select2_m2m_widget').each(function(){ + var $input = $(this); + var lastsearch = []; + $input.select2({ + multiple: true, + tags: true, + tokenSeparators: [",", " ", "_"], + formatResult: function(term) { + if (term.isNew) { + return 'New ' + _.escape(term.text); + } else { + return _.escape(term.text); + } + }, + query: function(options) { + var domain = []; + if (options.term){ + domain.push([ + $input.data('search_field') || 'name', 'ilike', '%' + options.term + '%' + ]) + } + // TODO: use data.CompundDomain to build domain + // ATM it's just in backend assets + // and requires both data.js and pyeval.js + domain = _.union(domain, $input.data('domain')); + ajax.jsonRpc("/web/dataset/call_kw", 'call', { + model: $input.data('model'), + method: 'search_read', + args: [domain], + kwargs: { + fields: $input.data('fields'), + context: base.get_context() + } + }).then(function(data) { + var display_name = $input.data('display_name'); + data.sort(function(a, b) { + return a[display_name].localeCompare(b[display_name]); + }); + var res = { + results: [] + }; + _.each(data, function(x) { + res.results.push({ + id: x.id, + text: x[display_name], + isNew: false + }); + }); + options.callback(res); + }); + }, + // Default tags from the input value + initSelection: function(element, callback) { + var data = []; + _.each(element.data('init-value'), function(x) { + data.push({ + id: x.id, + text: x.name, + isNew: false + }); + }); + element.val(''); + callback(data); + }, + }); + }); + + }); +}); diff --git a/cms_form/static/src/js/textarea_widget.js b/cms_form/static/src/js/textarea_widget.js new file mode 100644 index 00000000..97e160b9 --- /dev/null +++ b/cms_form/static/src/js/textarea_widget.js @@ -0,0 +1,23 @@ +odoo.define('cms_form.textarea_widget', function (require) { + 'use strict'; + + var ajax = require('web.ajax'); + + $(document).ready(function () { + $('textarea[maxlength]').bind('input propertychange', function(){ + var $self = $(this), + maxlength = parseInt($self.attr('maxlength')), + length = $self.val().length, + left = maxlength - length, + $counter = $self.siblings('.text-counter'); + if ($self.data('counter')) { + $counter = $($self.data('counter')); + } + if (left < 0) { + left = 0; + } + $counter.val(left); + }).trigger('input'); + }); + +}); diff --git a/cms_form/static/src/less/cms_form.less b/cms_form/static/src/less/cms_form.less new file mode 100644 index 00000000..03227bc4 --- /dev/null +++ b/cms_form/static/src/less/cms_form.less @@ -0,0 +1,15 @@ +.cms_form_wrapper{ + form{ + .field-required{ + .control-label::after{ + content: '*'; + font-weight: bold; + } + } + .text-counter{ + width: auto; + float: right; + margin-top: 0.2em; + } + } +} diff --git a/cms_form/templates/assets.xml b/cms_form/templates/assets.xml new file mode 100644 index 00000000..990ae090 --- /dev/null +++ b/cms_form/templates/assets.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/cms_form/templates/form.xml b/cms_form/templates/form.xml new file mode 100644 index 00000000..018b4f21 --- /dev/null +++ b/cms_form/templates/form.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cms_form/templates/widgets.xml b/cms_form/templates/widgets.xml new file mode 100644 index 00000000..db3fb28e --- /dev/null +++ b/cms_form/templates/widgets.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + +