From 7844e6e65d347e994c6aa3e808b89fcaad362bbe Mon Sep 17 00:00:00 2001 From: jaydeep32 Date: Wed, 14 Feb 2024 11:31:45 -0500 Subject: [PATCH 01/58] [MIG] base_dav: Migration to 17.0 --- base_dav/README.rst | 125 +++++ base_dav/__init__.py | 5 + base_dav/__manifest__.py | 25 + base_dav/controllers/__init__.py | 3 + base_dav/controllers/main.py | 67 +++ base_dav/demo/dav_collection.xml | 34 ++ base_dav/i18n/base_dav.pot | 215 ++++++++ base_dav/models/__init__.py | 4 + base_dav/models/dav_collection.py | 301 +++++++++++ .../models/dav_collection_field_mapping.py | 172 +++++++ base_dav/pyproject.toml | 3 + base_dav/radicale/__init__.py | 5 + base_dav/radicale/auth.py | 20 + base_dav/radicale/collection.py | 130 +++++ base_dav/radicale/rights.py | 33 ++ base_dav/readme/CONFIGURE.md | 8 + base_dav/readme/CONTRIBUTORS.md | 2 + base_dav/readme/CREDITS.md | 3 + base_dav/readme/DESCRIPTION.md | 6 + base_dav/readme/ROADMAP.md | 19 + base_dav/security/ir.model.access.csv | 3 + base_dav/static/description/icon.png | Bin 0 -> 9718 bytes base_dav/static/description/index.html | 472 ++++++++++++++++++ base_dav/tests/__init__.py | 3 + base_dav/tests/test_base_dav.py | 123 +++++ base_dav/tests/test_collection.py | 126 +++++ base_dav/views/dav_collection.xml | 82 +++ requirements.txt | 2 + 28 files changed, 1991 insertions(+) create mode 100644 base_dav/README.rst create mode 100644 base_dav/__init__.py create mode 100644 base_dav/__manifest__.py create mode 100644 base_dav/controllers/__init__.py create mode 100644 base_dav/controllers/main.py create mode 100644 base_dav/demo/dav_collection.xml create mode 100644 base_dav/i18n/base_dav.pot create mode 100644 base_dav/models/__init__.py create mode 100644 base_dav/models/dav_collection.py create mode 100644 base_dav/models/dav_collection_field_mapping.py create mode 100644 base_dav/pyproject.toml create mode 100644 base_dav/radicale/__init__.py create mode 100644 base_dav/radicale/auth.py create mode 100644 base_dav/radicale/collection.py create mode 100644 base_dav/radicale/rights.py create mode 100644 base_dav/readme/CONFIGURE.md create mode 100644 base_dav/readme/CONTRIBUTORS.md create mode 100644 base_dav/readme/CREDITS.md create mode 100644 base_dav/readme/DESCRIPTION.md create mode 100644 base_dav/readme/ROADMAP.md create mode 100644 base_dav/security/ir.model.access.csv create mode 100755 base_dav/static/description/icon.png create mode 100755 base_dav/static/description/index.html create mode 100644 base_dav/tests/__init__.py create mode 100644 base_dav/tests/test_base_dav.py create mode 100644 base_dav/tests/test_collection.py create mode 100644 base_dav/views/dav_collection.xml create mode 100644 requirements.txt diff --git a/base_dav/README.rst b/base_dav/README.rst new file mode 100644 index 000000000..9977eb605 --- /dev/null +++ b/base_dav/README.rst @@ -0,0 +1,125 @@ +========================== +Caldav and Carddav support +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6cc5b91f1cff865b4527a2097534adb50c8bade6eaed9f3b820275b7b8ab19d3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--backend-lightgray.png?logo=github + :target: https://github.com/OCA/server-backend/tree/17.0/base_dav + :alt: OCA/server-backend +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-backend-17-0/server-backend-17-0-base_dav + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-backend&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds WebDAV support to Odoo, specifically CalDAV and +CardDAV. + +You can configure arbitrary objects as a calendar or an address book, +thus make arbitrary information accessible in external systems or your +mobile. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +1. go to Settings / WebDAV Collections and create or edit your + collections. There, you'll also see the URL to point your clients to. + +Note that you need to configure a dbfilter if you use multiple +databases. + +Known issues / Roadmap +====================== + +- much better UX for configuring collections (probably provide a group + that sees the current fully flexible field mappings, and by default + show some dumbed down version where you can select some preselected + vobject fields) +- support todo lists and journals +- support configuring default field mappings per model +- support plain WebDAV collections to make some model's records + accessible as folders, and the records' attachments as files (r/w) +- support configuring lists of calendars so that you can have a + calendar for every project and appointments are tasks, or a calendar + for every sales team and appointments are sale orders. Lots of + possibilities + +Backporting this to <=v10 will be tricky because radicale only supports +python3. Probably it will be quite a hassle to backport the relevant +code, so it might be more sensible to just backport the configuration +part, and implement the rest as radicale auth/storage plugin that talks +to Odoo via odoorpc. It should be possible to recycle most of the code +from this addon, which actually implements those plugins, but then +within Odoo. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* initOS GmbH +* Therp BV + +Contributors +------------ + +- Holger Brunn +- Florian Kantelberg + +Other credits +------------- + +- Odoo Community Association: + `Icon `__ +- All the actual work is done by `Radicale `__ + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/server-backend `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_dav/__init__.py b/base_dav/__init__.py new file mode 100644 index 000000000..2e9e0c3e0 --- /dev/null +++ b/base_dav/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import models +from . import controllers +from . import radicale diff --git a/base_dav/__manifest__.py b/base_dav/__manifest__.py new file mode 100644 index 000000000..a0e5a57e8 --- /dev/null +++ b/base_dav/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2018 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Caldav and Carddav support", + "version": "17.0.1.0.0", + "author": "initOS GmbH,Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Extra Tools", + "summary": "Access Odoo data as calendar or address book", + "website": "https://github.com/OCA/server-backend", + "depends": [ + "base", + ], + "demo": [ + "demo/dav_collection.xml", + ], + "data": [ + "views/dav_collection.xml", + "security/ir.model.access.csv", + ], + "external_dependencies": { + "python": ["radicale"], + }, +} diff --git a/base_dav/controllers/__init__.py b/base_dav/controllers/__init__.py new file mode 100644 index 000000000..665f08cea --- /dev/null +++ b/base_dav/controllers/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import main diff --git a/base_dav/controllers/main.py b/base_dav/controllers/main.py new file mode 100644 index 000000000..967d7970a --- /dev/null +++ b/base_dav/controllers/main.py @@ -0,0 +1,67 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging +from configparser import RawConfigParser as ConfigParser + +import werkzeug + +from odoo import http +from odoo.http import request + +try: + import radicale + # from radicale import config +except ImportError: + radicale = None + +PREFIX = "/.dav" + + +class Main(http.Controller): + @http.route( + ["/.well-known/carddav", "/.well-known/caldav", "/.well-known/webdav"], + type="http", + auth="none", + csrf=False, + ) + def handle_well_known_request(self): + return werkzeug.utils.redirect(PREFIX, 301) + + @http.route( + [PREFIX, "%s/" % PREFIX], + type="http", + auth="none", + csrf=False, + ) + def handle_dav_request(self, davpath=None): + config = ConfigParser() + for section, values in radicale.config.INITIAL_CONFIG.items(): + config.add_section(section) + for key, data in values.items(): + config.set(section, key, data["value"]) + config.set("auth", "type", "odoo.addons.base_dav.radicale.auth") + config.set("storage", "type", "odoo.addons.base_dav.radicale.collection") + config.set("rights", "type", "odoo.addons.base_dav.radicale.rights") + config.set("web", "type", "none") + application = radicale.Application( + config, + logging.getLogger("radicale"), + ) + + response = None + + def start_response(status, headers): + nonlocal response + response = http.Response(status=status, headers=headers) + + result = application( + dict( + request.httprequest.environ, + HTTP_X_SCRIPT_NAME=PREFIX, + PATH_INFO=davpath or "", + ), + start_response, + ) + response.stream.write(result and result[0] or b"") + return response diff --git a/base_dav/demo/dav_collection.xml b/base_dav/demo/dav_collection.xml new file mode 100644 index 000000000..b038d687c --- /dev/null +++ b/base_dav/demo/dav_collection.xml @@ -0,0 +1,34 @@ + + + + Addressbook + addressbook + + [] + + + N + + + + + FN + + + + + photo + + + + + email + + + + + tel + + + + diff --git a/base_dav/i18n/base_dav.pot b/base_dav/i18n/base_dav.pot new file mode 100644 index 000000000..2e0939ca5 --- /dev/null +++ b/base_dav/i18n/base_dav.pot @@ -0,0 +1,215 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_dav +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \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: base_dav +#: model:ir.model,name:base_dav.model_dav_collection +msgid "A collection accessible via WebDAV" +msgstr "" + +#. module: base_dav +#: model:ir.model,name:base_dav.model_dav_collection_field_mapping +msgid "A field mapping for a WebDAV collection" +msgstr "" + +#. module: base_dav +#: model:ir.ui.view,arch_db:base_dav.view_dav_collection_form +msgid "Access" +msgstr "" + +#. module: base_dav +#: model:ir.ui.view,arch_db:base_dav.view_dav_collection_form +msgid "Additional field mapping" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Addressbook" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_name +msgid "Attribute name in the vobject" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Authenticated" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Calendar" +msgstr "" + +#. module: base_dav +#: selection:dav.collection.field_mapping,mapping_type:0 +msgid "Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_export_code +msgid "Code to export the value to a vobject. Use the variable result for the output of the value and record as input" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_import_code +msgid "Code to import the value from a vobject. Use the variable result for the output of the value and item as input" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_collection_id +msgid "Collection" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_create_uid +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_create_uid +msgid "Created by" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_create_date +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_create_date +msgid "Created on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_display_name +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_display_name +msgid "Display Name" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_domain +msgid "Domain" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_export_code +msgid "Export Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_field_id +msgid "Field" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_uuid +msgid "Field Uuid" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_ids +msgid "Field mappings" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_field_id +msgid "Field of the model the values are mapped to" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Files" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_id +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_id +msgid "ID" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_import_code +msgid "Import Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection___last_update +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping___last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_write_uid +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_write_date +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_mapping_type +msgid "Mapping Type" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_model_id +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_model_id +msgid "Model" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_name +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_name +msgid "Name" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Owner Only" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Owner Write Only" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_rights +msgid "Rights" +msgstr "" + +#. module: base_dav +#: selection:dav.collection.field_mapping,mapping_type:0 +msgid "Simple" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_tag +msgid "Tag" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_dav_type +msgid "Type" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_url +msgid "Url" +msgstr "" + +#. module: base_dav +#: model:ir.actions.act_window,name:base_dav.action_dav_collection +#: model:ir.ui.menu,name:base_dav.menu_dav_collection +msgid "WebDAV collections" +msgstr "" + diff --git a/base_dav/models/__init__.py b/base_dav/models/__init__.py new file mode 100644 index 000000000..3e6c8778b --- /dev/null +++ b/base_dav/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import dav_collection +from . import dav_collection_field_mapping diff --git a/base_dav/models/dav_collection.py b/base_dav/models/dav_collection.py new file mode 100644 index 000000000..9009468eb --- /dev/null +++ b/base_dav/models/dav_collection.py @@ -0,0 +1,301 @@ +# Copyright 2019 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import os +import time +from operator import itemgetter +from urllib.parse import quote_plus + +import vobject + +from odoo import SUPERUSER_ID, api, fields, models +from odoo.tools.safe_eval import safe_eval + +from odoo.addons.base_dav.radicale.collection import Collection, FileItem, Item + +# pylint: disable=missing-import-error +from ..controllers.main import PREFIX + + +class DavCollection(models.Model): + _name = "dav.collection" + _description = "A collection accessible via WebDAV" + + name = fields.Char(required=True) + rights = fields.Selection( + [ + ("owner_only", "Owner Only"), + ("owner_write_only", "Owner Write Only"), + ("authenticated", "Authenticated"), + ], + required=True, + default="owner_only", + ) + dav_type = fields.Selection( + [ + ("calendar", "Calendar"), + ("addressbook", "Addressbook"), + ("files", "Files"), + ], + string="Type", + required=True, + default="calendar", + ) + tag = fields.Char(compute="_compute_tag") + model_id = fields.Many2one( + "ir.model", + string="Model", + required=True, + domain=[("transient", "=", False)], + ondelete="cascade", + ) + domain = fields.Char( + required=True, + default="[]", + ) + field_uuid = fields.Many2one("ir.model.fields") + field_mapping_ids = fields.One2many( + "dav.collection.field_mapping", + "collection_id", + string="Field mappings", + ) + url = fields.Char(compute="_compute_url") + + def _compute_tag(self): + for this in self: + if this.dav_type == "calendar": + this.tag = "VCALENDAR" + elif this.dav_type == "addressbook": + this.tag = "VADDRESSBOOK" + + def _compute_url(self): + base_url = self.env["ir.config_parameter"].get_param("web.base.url") + for this in self: + this.url = f"{base_url}{PREFIX}/{self.env.user.login}/{this.id}" + + @api.constrains("domain") + def _check_domain(self): + self._eval_domain() + + @api.model + def _eval_context(self): + return { + "user": self.env.user, + } + + def _eval_domain(self): + self.ensure_one() + return list(safe_eval(self.domain, self._eval_context())) + + def eval(self): + if not self: + return self.env["unknown"] + self.ensure_one() + self = self.with_user(SUPERUSER_ID) + return self.env[self.model_id.model].search(self._eval_domain()) + + def get_record(self, components): + self.ensure_one() + self = self.with_user(SUPERUSER_ID) + collection_model = self.env[self.model_id.model] + + field_name = self.field_uuid.name or "id" + domain = [(field_name, "=", components[-1])] + self._eval_domain() + return collection_model.search(domain, limit=1) + + def from_vobject(self, item): + self.ensure_one() + + result = {} + if self.dav_type == "calendar": + if item.name != "VCALENDAR": + return None + if not hasattr(item, "vevent"): + return None + item = item.vevent + elif self.dav_type == "addressbook" and item.name != "VCARD": + return None + + children = {c.name.lower(): c for c in item.getChildren()} + for mapping in self.field_mapping_ids: + name = mapping.name.lower() + if name not in children: + continue + + if name in children: + value = mapping.from_vobject(children[name]) + if value: + result[mapping.field_id.name] = value + + return result + + def to_vobject(self, record): + self.ensure_one() + self = self.with_user(SUPERUSER_ID) + result = None + vobj = None + if self.dav_type == "calendar": + result = vobject.iCalendar() + vobj = result.add("vevent") + if self.dav_type == "addressbook": + result = vobject.vCard() + vobj = result + for mapping in self.field_mapping_ids: + value = mapping.to_vobject(record) + if value: + vobj.add(mapping.name).value = value + + if "uid" not in vobj.contents: + vobj.add("uid").value = f"{record._name},{record.id}" + if "rev" not in vobj.contents and "write_date" in record._fields: + vobj.add("rev").value = ( + str(record.write_date) + .replace(":", "") + .replace(" ", "T") + .replace(".", "") + + "Z" + ) + return result + + @api.model + def _odoo_to_http_datetime(self, value): + value = str(value).split(".")[0] + return time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", + time.strptime(value, "%Y-%m-%d %H:%M:%S"), + ) + + @api.model + def _split_path(self, path): + return list(filter(None, os.path.normpath(path or "").strip("/").split("/"))) + + def dav_list(self, collection, path_components): + self.ensure_one() + + if self.dav_type == "files": + if len(path_components) == 3: + collection_model = self.env[self.model_id.model] + record = collection_model.browse( + map( + itemgetter(0), + collection_model.name_search( + path_components[2], + operator="=", + limit=1, + ), + ) + ) + return [ + "/" + "/".join(path_components + [quote_plus(attachment.name)]) + for attachment in self.env["ir.attachment"].search( + [ + ("type", "=", "binary"), + ("res_model", "=", record._name), + ("res_id", "=", record.id), + ] + ) + ] + elif len(path_components) == 2: + return [ + "/" + "/".join(path_components + [quote_plus(record.display_name)]) + for record in self.eval() + ] + + if len(path_components) > 2: + return [] + + result = [] + for record in self.eval(): + if self.field_uuid: + uuid = record[self.field_uuid.name] + else: + uuid = str(record.id) + result.append("/" + "/".join(path_components + [uuid])) + return result + + def dav_delete(self, collection, components): + self.ensure_one() + + if self.dav_type == "files": + # TODO: Handle deletion of attachments + pass + else: + self.get_record(components).unlink() + + def dav_upload(self, collection, href, item): + self.ensure_one() + + components = self._split_path(href) + collection_model = self.env[self.model_id.model] + if self.dav_type == "files": + # TODO: Handle upload of attachments + return None + + data = self.from_vobject(item) + record = self.get_record(components) + + if not record: + if self.field_uuid: + data[self.field_uuid.name] = components[-1] + + record = collection_model.create(data) + uuid = components[-1] if self.field_uuid else record.id + href = f"{href}/{uuid}" + else: + record.write(data) + + return Item( + collection, + item=self.to_vobject(record), + href=href, + last_modified=self._odoo_to_http_datetime(record.write_date), + ) + + def dav_get(self, collection, href): + self.ensure_one() + + components = self._split_path(href) + collection_model = self.env[self.model_id.model] + if self.dav_type == "files": + if len(components) == 3: + result = Collection(href) + result.logger = self.logger + return result + if len(components) == 4: + record = collection_model.browse( + map( + itemgetter(0), + collection_model.name_search( + components[2], + operator="=", + limit=1, + ), + ) + ) + attachment = self.env["ir.attachment"].search( + [ + ("type", "=", "binary"), + ("res_model", "=", record._name), + ("res_id", "=", record.id), + ("name", "=", components[3]), + ], + limit=1, + ) + return FileItem( + collection, + item=attachment, + href=href, + last_modified=self._odoo_to_http_datetime(record.write_date), + ) + + record = self.get_record(components) + + if not record: + return None + + return Item( + collection, + item=self.to_vobject(record), + href=href, + last_modified=self._odoo_to_http_datetime(record.write_date), + ) diff --git a/base_dav/models/dav_collection_field_mapping.py b/base_dav/models/dav_collection_field_mapping.py new file mode 100644 index 000000000..48c88c4d1 --- /dev/null +++ b/base_dav/models/dav_collection_field_mapping.py @@ -0,0 +1,172 @@ +# Copyright 2019 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import datetime + +import dateutil +import vobject +from dateutil import tz + +from odoo import api, fields, models, tools + + +class DavCollectionFieldMapping(models.Model): + _name = "dav.collection.field_mapping" + _description = "A field mapping for a WebDAV collection" + + collection_id = fields.Many2one( + "dav.collection", + required=True, + ondelete="cascade", + ) + name = fields.Char( + required=True, + help="Attribute name in the vobject", + ) + mapping_type = fields.Selection( + [ + ("simple", "Simple"), + ("code", "Code"), + ], + default="simple", + required=True, + ) + field_id = fields.Many2one( + "ir.model.fields", + required=True, + help="Field of the model the values are mapped to", + ondelete="cascade", + ) + model_id = fields.Many2one( + "ir.model", related="collection_id.model_id", ondelete="cascade" + ) + import_code = fields.Text( + help="Code to import the value from a vobject. Use the variable " + "result for the output of the value and item as input" + ) + export_code = fields.Text( + help="Code to export the value to a vobject. Use the variable " + "result for the output of the value and record as input" + ) + + def from_vobject(self, child): + self.ensure_one() + if self.mapping_type == "code": + return self._from_vobject_code(child) + return self._from_vobject_simple(child) + + def _from_vobject_code(self, child): + self.ensure_one() + context = { + "datetime": datetime, + "dateutil": dateutil, + "item": child, + "result": None, + "tools": tools, + "tz": tz, + "vobject": vobject, + } + tools.safe_eval(self.import_code, context, mode="exec", nocopy=True) + return context.get("result", {}) + + def _from_vobject_simple(self, child): + self.ensure_one() + name = self.name.lower() + conversion_funcs = [ + f"_from_vobject_{self.field_id.ttype}_{name}", + f"_from_vobject_{self.field_id.ttype}", + ] + + for conversion_func in conversion_funcs: + if hasattr(self, conversion_func): + value = getattr(self, conversion_func)(child) + if value: + return value + + return child.value + + @api.model + def _from_vobject_datetime(self, item): + if isinstance(item.value, datetime.datetime): + value = item.value.astimezone(dateutil.tz.UTC) + return value.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + elif isinstance(item.value, datetime.date): + return item.value.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + return None + + @api.model + def _from_vobject_date(self, item): + if isinstance(item.value, datetime.datetime): + value = item.value.astimezone(dateutil.tz.UTC) + return value.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + elif isinstance(item.value, datetime.date): + return item.value.strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + return None + + @api.model + def _from_vobject_binary(self, item): + return item.value.encode("ascii") + + @api.model + def _from_vobject_char_n(self, item): + return item.family + + def to_vobject(self, record): + self.ensure_one() + if self.mapping_type == "code": + result = self._to_vobject_code(record) + else: + result = self._to_vobject_simple(record) + + if isinstance(result, datetime.datetime) and not result.tzinfo: + return result.replace(tzinfo=tz.UTC) + return result + + def _to_vobject_code(self, record): + self.ensure_one() + context = { + "datetime": datetime, + "dateutil": dateutil, + "record": record, + "result": None, + "tools": tools, + "tz": tz, + "vobject": vobject, + } + tools.safe_eval(self.export_code, context, mode="exec", nocopy=True) + return context.get("result", None) + + def _to_vobject_simple(self, record): + self.ensure_one() + conversion_funcs = [ + f"_to_vobject_{self.field_id.ttype}_{self.name.lower()}", + f"_to_vobject_{self.field_id.ttype}", + ] + value = record[self.field_id.name] + for conversion_func in conversion_funcs: + if hasattr(self, conversion_func): + return getattr(self, conversion_func)(value) + return value + + @api.model + def _to_vobject_datetime(self, value): + result = fields.Datetime.from_string(value) + return result.replace(tzinfo=tz.UTC) + + @api.model + def _to_vobject_datetime_rev(self, value): + return value and value.replace("-", "").replace(" ", "T").replace(":", "") + "Z" + + @api.model + def _to_vobject_date(self, value): + return fields.Date.from_string(value) + + @api.model + def _to_vobject_binary(self, value): + return value and value.decode("ascii") + + @api.model + def _to_vobject_char_n(self, value): + # TODO: how are we going to handle compound types like this? + return vobject.vcard.Name(family=value) diff --git a/base_dav/pyproject.toml b/base_dav/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/base_dav/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_dav/radicale/__init__.py b/base_dav/radicale/__init__.py new file mode 100644 index 000000000..dd8d0f16c --- /dev/null +++ b/base_dav/radicale/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import auth +from . import collection +from . import rights diff --git a/base_dav/radicale/auth.py b/base_dav/radicale/auth.py new file mode 100644 index 000000000..7c855a0d2 --- /dev/null +++ b/base_dav/radicale/auth.py @@ -0,0 +1,20 @@ +# Copyright 2018 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.http import request + +try: + from radicale.auth import BaseAuth +except ImportError: + BaseAuth = None + + +class Auth(BaseAuth): + def is_authenticated2(self, login, user, password): + env = request.env + uid = env["res.users"]._login( + env.cr.dbname, user, password, user_agent_env={"interactive": True} + ) + if uid: + request._env = env(user=uid) + return bool(uid) diff --git a/base_dav/radicale/collection.py b/base_dav/radicale/collection.py new file mode 100644 index 000000000..3be55436b --- /dev/null +++ b/base_dav/radicale/collection.py @@ -0,0 +1,130 @@ +# Copyright 2018 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import base64 +import os +import time +from contextlib import contextmanager + +from odoo.http import request + +try: + from radicale.storage import BaseCollection, Item, get_etag +except ImportError: + BaseCollection = None + Item = None + get_etag = None + + +class BytesPretendingToBeString(bytes): + # radicale expects a string as file content, so we provide the str + # functions needed + def encode(self, encoding): + return self + + +class FileItem(Item): + """this item tricks radicalev into serving a plain file""" + + @property + def name(self): + return "VCARD" + + def serialize(self): + return BytesPretendingToBeString(base64.b64decode(self.item.datas)) + + @property + def etag(self): + return get_etag(self.item.datas.decode("ascii")) + + +class Collection(BaseCollection): + @classmethod + def static_init(cls): + pass + + @classmethod + def _split_path(cls, path): + return list(filter(None, os.path.normpath(path or "").strip("/").split("/"))) + + @classmethod + def discover(cls, path, depth=None): + depth = int(depth or "0") + components = cls._split_path(path) + collection = cls(path) + if len(components) > 2: + # TODO: this probably better should happen in some dav.collection + # function + if collection.collection.dav_type == "files" and depth: + for href in collection.list(): + yield collection.get(href) + return + yield collection.get(path) + return + yield collection + if depth and len(components) == 1: + for collection in request.env["dav.collection"].search([]): + yield cls("/".join(components + ["/%d" % collection.id])) + if depth and len(components) == 2: + for href in collection.list(): + yield collection.get(href) + + @classmethod + @contextmanager + def acquire_lock(cls, mode, user=None): + """We have a database for that""" + yield + + @property + def env(self): + return request.env + + @property + def last_modified(self): + return self._odoo_to_http_datetime(self.collection.create_date) + + def __init__(self, path): + self.path_components = self._split_path(path) + self.path = "/".join(self.path_components) or "/" + self.collection = self.env["dav.collection"] + if len(self.path_components) >= 2 and str(self.path_components[1]).isdigit(): + self.collection = self.env["dav.collection"].browse( + int(self.path_components[1]) + ) + + def _odoo_to_http_datetime(self, value): + value = str(value).split(".")[0] + return time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", + time.strptime(value, "%Y-%m-%d %H:%M:%S"), + ) + + def get_meta(self, key=None): + if key is None: + return {} + elif key == "tag": + return self.collection.tag + elif key == "D:displayname": + return self.collection.display_name + elif key == "C:supported-calendar-component-set": + return "VTODO,VEVENT,VJOURNAL" + elif key == "C:calendar-home-set": + return None + elif key == "D:principal-URL": + return None + elif key == "ICAL:calendar-color": + # TODO: set in dav.collection + return "#48c9f4" + self.logger.warning("unsupported metadata %s", key) + + def get(self, href): + return self.collection.dav_get(self, href) + + def upload(self, href, vobject_item): + return self.collection.dav_upload(self, href, vobject_item) + + def delete(self, href): + return self.collection.dav_delete(self, self._split_path(href)) + + def list(self): + return self.collection.dav_list(self, self.path_components) diff --git a/base_dav/radicale/rights.py b/base_dav/radicale/rights.py new file mode 100644 index 000000000..fb03c19bb --- /dev/null +++ b/base_dav/radicale/rights.py @@ -0,0 +1,33 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from .collection import Collection + +try: + from radicale.rights import ( + AuthenticatedRights, + OwnerOnlyRights, + OwnerWriteRights, + ) +except ImportError: + AuthenticatedRights = OwnerOnlyRights = OwnerWriteRights = None + + +class Rights(OwnerOnlyRights, OwnerWriteRights, AuthenticatedRights): + def authorized(self, user, path, perm): + if path == "/": + return True + + collection = Collection(path) + if not collection.collection: + return False + + rights = collection.collection.sudo().rights + cls = { + "owner_only": OwnerOnlyRights, + "owner_write_only": OwnerWriteRights, + "authenticated": AuthenticatedRights, + }.get(rights) + if not cls: + return False + return cls.authorized(self, user, path, perm) diff --git a/base_dav/readme/CONFIGURE.md b/base_dav/readme/CONFIGURE.md new file mode 100644 index 000000000..4857fccff --- /dev/null +++ b/base_dav/readme/CONFIGURE.md @@ -0,0 +1,8 @@ +To configure this module, you need to: + +1. go to Settings / WebDAV Collections and create or edit your + collections. There, you'll also see the URL to point your clients + to. + +Note that you need to configure a dbfilter if you use multiple +databases. diff --git a/base_dav/readme/CONTRIBUTORS.md b/base_dav/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..647f55bc2 --- /dev/null +++ b/base_dav/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Holger Brunn \<\> +- Florian Kantelberg \<\> diff --git a/base_dav/readme/CREDITS.md b/base_dav/readme/CREDITS.md new file mode 100644 index 000000000..af1341db0 --- /dev/null +++ b/base_dav/readme/CREDITS.md @@ -0,0 +1,3 @@ +- Odoo Community Association: + [Icon](https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg) +- All the actual work is done by [Radicale](https://radicale.org) diff --git a/base_dav/readme/DESCRIPTION.md b/base_dav/readme/DESCRIPTION.md new file mode 100644 index 000000000..3615c5ea7 --- /dev/null +++ b/base_dav/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module adds WebDAV support to Odoo, specifically CalDAV and +CardDAV. + +You can configure arbitrary objects as a calendar or an address book, +thus make arbitrary information accessible in external systems or your +mobile. diff --git a/base_dav/readme/ROADMAP.md b/base_dav/readme/ROADMAP.md new file mode 100644 index 000000000..a8273c508 --- /dev/null +++ b/base_dav/readme/ROADMAP.md @@ -0,0 +1,19 @@ +- much better UX for configuring collections (probably provide a group + that sees the current fully flexible field mappings, and by default + show some dumbed down version where you can select some preselected + vobject fields) +- support todo lists and journals +- support configuring default field mappings per model +- support plain WebDAV collections to make some model's records + accessible as folders, and the records' attachments as files (r/w) +- support configuring lists of calendars so that you can have a calendar + for every project and appointments are tasks, or a calendar for every + sales team and appointments are sale orders. Lots of possibilities + +Backporting this to \<=v10 will be tricky because radicale only supports +python3. Probably it will be quite a hassle to backport the relevant +code, so it might be more sensible to just backport the configuration +part, and implement the rest as radicale auth/storage plugin that talks +to Odoo via odoorpc. It should be possible to recycle most of the code +from this addon, which actually implements those plugins, but then +within Odoo. diff --git a/base_dav/security/ir.model.access.csv b/base_dav/security/ir.model.access.csv new file mode 100644 index 000000000..3d2d4d57b --- /dev/null +++ b/base_dav/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_dav_collection,access_dav_collection,model_dav_collection,base.group_user,1,0,0,0 +access_dav_collection_field_mapping,access_dav_collection_field_mapping,model_dav_collection_field_mapping,base.group_user,1,0,0,0 diff --git a/base_dav/static/description/icon.png b/base_dav/static/description/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..f0eebfd17427b74c00e59bdafdd785abf7e8dcc7 GIT binary patch literal 9718 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 YooIC{JT?$`LJLDxNmH>#-ZK3E0V&OZSpWb4 literal 0 HcmV?d00001 diff --git a/base_dav/static/description/index.html b/base_dav/static/description/index.html new file mode 100755 index 000000000..d83347b4b --- /dev/null +++ b/base_dav/static/description/index.html @@ -0,0 +1,472 @@ + + + + + + +Caldav and Carddav support + + + +

+

Caldav and Carddav support

+ + +

Beta License: AGPL-3 OCA/server-backend Translate me on Weblate Try me on Runboat

+

This module adds WebDAV support to Odoo, specifically CalDAV and +CardDAV.

+

You can configure arbitrary objects as a calendar or an address book, +thus make arbitrary information accessible in external systems or your +mobile.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. go to Settings / WebDAV Collections and create or edit your +collections. There, you’ll also see the URL to point your clients to.
  2. +
+

Note that you need to configure a dbfilter if you use multiple +databases.

+
+
+

Known issues / Roadmap

+
    +
  • much better UX for configuring collections (probably provide a group +that sees the current fully flexible field mappings, and by default +show some dumbed down version where you can select some preselected +vobject fields)
  • +
  • support todo lists and journals
  • +
  • support configuring default field mappings per model
  • +
  • support plain WebDAV collections to make some model’s records +accessible as folders, and the records’ attachments as files (r/w)
  • +
  • support configuring lists of calendars so that you can have a +calendar for every project and appointments are tasks, or a calendar +for every sales team and appointments are sale orders. Lots of +possibilities
  • +
+

Backporting this to <=v10 will be tricky because radicale only supports +python3. Probably it will be quite a hassle to backport the relevant +code, so it might be more sensible to just backport the configuration +part, and implement the rest as radicale auth/storage plugin that talks +to Odoo via odoorpc. It should be possible to recycle most of the code +from this addon, which actually implements those plugins, but then +within Odoo.

+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+
    +
  • Odoo Community Association: +Icon
  • +
  • All the actual work is done by Radicale
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/server-backend project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_dav/tests/__init__.py b/base_dav/tests/__init__.py new file mode 100644 index 000000000..09e6f8403 --- /dev/null +++ b/base_dav/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import test_base_dav, test_collection diff --git a/base_dav/tests/test_base_dav.py b/base_dav/tests/test_base_dav.py new file mode 100644 index 000000000..ac00c0a24 --- /dev/null +++ b/base_dav/tests/test_base_dav.py @@ -0,0 +1,123 @@ +# Copyright 2018 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from base64 import b64encode +from unittest import mock +from urllib.parse import urlparse + +from odoo.tests.common import TransactionCase +from odoo.tools import mute_logger + +from ..controllers.main import PREFIX +from ..controllers.main import Main as Controller + +MODULE_PATH = "odoo.addons.base_dav" +CONTROLLER_PATH = MODULE_PATH + ".controllers.main" +RADICALE_PATH = MODULE_PATH + ".radicale" + +ADMIN_PASSWORD = "RadicalePa$$word" + + +@mute_logger("radicale") +@mock.patch(CONTROLLER_PATH + ".request") +@mock.patch(RADICALE_PATH + ".auth.request") +@mock.patch(RADICALE_PATH + ".collection.request") +class TestBaseDav(TransactionCase): + def setUp(self): + super().setUp() + + self.collection = self.env["dav.collection"].create( + { + "name": "Test Collection", + "dav_type": "calendar", + "model_id": self.env.ref("base.model_res_users").id, + "domain": "[]", + } + ) + + self.dav_path = urlparse(self.collection.url).path.replace(PREFIX, "") + + self.controller = Controller() + self.env.user.password_crypt = ADMIN_PASSWORD + + self.test_user = self.env["res.users"].create( + { + "login": "tester", + "name": "tester", + } + ) + + self.auth_owner = self.auth_string(self.env.user, ADMIN_PASSWORD) + self.auth_tester = self.auth_string(self.test_user, ADMIN_PASSWORD) + + patcher = mock.patch("odoo.http.request") + self.addCleanup(patcher.stop) + patcher.start() + + def auth_string(self, user, password): + return b64encode((f"{user.login}:{password}").encode()).decode() + + def init_mocks(self, coll_mock, auth_mock, req_mock): + req_mock.env = self.env + req_mock.httprequest.environ = { + "HTTP_AUTHORIZATION": "Basic %s" % self.auth_owner, + "REQUEST_METHOD": "PROPFIND", + "HTTP_X_SCRIPT_NAME": PREFIX, + } + + auth_mock.env["res.users"]._login.return_value = self.env.uid + coll_mock.env = self.env + + def check_status_code(self, response, forbidden): + if forbidden: + self.assertNotEqual(response.status_code, 403) + else: + self.assertEqual(response.status_code, 403) + + def check_access(self, environ, auth_string, read, write): + environ.update( + { + "REQUEST_METHOD": "PROPFIND", + "HTTP_AUTHORIZATION": "Basic %s" % auth_string, + } + ) + response = self.controller.handle_dav_request(self.dav_path) + self.check_status_code(response, read) + + environ["REQUEST_METHOD"] = "PUT" + response = self.controller.handle_dav_request(self.dav_path) + self.check_status_code(response, write) + + def test_well_known(self, coll_mock, auth_mock, req_mock): + req_mock.env = self.env + + response = self.controller.handle_well_known_request() + self.assertEqual(response.status_code, 301) + + def test_authenticated(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "authenticated" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=True, write=True) + + def test_owner_only(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "owner_only" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=False, write=False) + + def test_owner_write_only(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "owner_write_only" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=True, write=False) diff --git a/base_dav/tests/test_collection.py b/base_dav/tests/test_collection.py new file mode 100644 index 000000000..97f9b9ed0 --- /dev/null +++ b/base_dav/tests/test_collection.py @@ -0,0 +1,126 @@ +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime, timedelta +from unittest import mock + +from odoo.tests.common import TransactionCase +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +from ..radicale.collection import Collection + + +class TestCalendar(TransactionCase): + def setUp(self): + super().setUp() + + self.collection = self.env["dav.collection"].create( + { + "name": "Test Collection", + "dav_type": "calendar", + "model_id": self.env.ref("base.model_res_users").id, + "domain": "[]", + } + ) + + self.create_field_mapping( + "login", + "base.field_res_users_login", + excode="result = record.login", + imcode="result = item.value", + ) + self.create_field_mapping( + "name", + "base.field_res_users_name", + ) + self.create_field_mapping( + "dtstart", + "base.field_res_users_create_date", + ) + self.create_field_mapping( + "dtend", + "base.field_res_users_write_date", + ) + + start = datetime.now() + stop = start + timedelta(hours=1) + self.record = self.env["res.users"].create( + { + "login": "tester", + "name": "Test User", + "create_date": start.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + "write_date": stop.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + } + ) + + def create_field_mapping(self, name, field_ref, imcode=None, excode=None): + return self.env["dav.collection.field_mapping"].create( + { + "collection_id": self.collection.id, + "name": name, + "field_id": self.env.ref(field_ref).id, + "mapping_type": "code" if imcode or excode else "simple", + "import_code": imcode, + "export_code": excode, + } + ) + + def compare_record(self, vobj, rec=None): + tmp = self.collection.from_vobject(vobj) + + self.assertEqual((rec or self.record).login, tmp["login"]) + self.assertEqual((rec or self.record).name, tmp["name"]) + self.assertEqual((rec or self.record).create_date, tmp["create_date"]) + self.assertEqual((rec or self.record).write_date, tmp["write_date"]) + + def test_import_export(self): + # Exporting and importing should result in the same record + vobj = self.collection.to_vobject(self.record) + self.compare_record(vobj) + + def test_get_record(self): + rec = self.collection.get_record([self.record.id]) + self.assertEqual(rec, self.record) + + self.collection.field_uuid = self.env.ref( + "base.field_res_users_login", + ).id + rec = self.collection.get_record([self.record.login]) + self.assertEqual(rec, self.record) + + @mock.patch("odoo.addons.base_dav.radicale.collection.request") + def test_collection(self, request_mock): + request_mock.env = self.env + collection_url = f"/{self.env.user.login}/{self.collection.id}" + collection = list(Collection.discover(collection_url))[0] + + # Try to get the test record + record_url = f"{collection_url}/{self.record.id}" + self.assertIn(record_url, collection.list()) + + # Get the test record using the URL and compare it + item = collection.get(record_url) + self.compare_record(item.item) + self.assertEqual(item.href, record_url) + + # Get a non-existing record + self.assertFalse(collection.get(record_url + "0")) + + # Get the record and alter it later + item = self.collection.to_vobject(self.record) + self.record.login = "different" + with self.assertRaises(AssertionError): + self.compare_record(item) + + # Restore the record + item = collection.upload(record_url, item) + self.compare_record(item.item) + + # Delete an record + collection.delete(item.href) + self.assertFalse(self.record.exists()) + + # Create a new record + item = collection.upload(record_url + "0", item) + record = self.collection.get_record(collection._split_path(item.href)) + self.assertNotEqual(record, self.record) + self.compare_record(item.item, record) diff --git a/base_dav/views/dav_collection.xml b/base_dav/views/dav_collection.xml new file mode 100644 index 000000000..945b79911 --- /dev/null +++ b/base_dav/views/dav_collection.xml @@ -0,0 +1,82 @@ + + + + dav.collection + + + + + + + + + + + dav.collection + +
+ + + + + + + + + + + + + + + + +
+ + + dav.collection.field_mapping + + + + + + + + + + + dav.collection.field_mapping + +
+ + + + + + + + + +
+
+
+ + + WebDAV collections + ir.actions.act_window + dav.collection + tree,form + + + + +
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..f24d62b80 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +radicale From 5e3179ebe134d3b05923ab6365611bda75ac4082 Mon Sep 17 00:00:00 2001 From: jaydeep32 Date: Wed, 14 Feb 2024 13:08:17 -0500 Subject: [PATCH 02/58] [IMP] base_dav: Some test cases fixes,odoo tools safe_eval fixes. --- .../models/dav_collection_field_mapping.py | 26 +++++++++---------- base_dav/readme/CONFIGURE.rst | 5 ++++ base_dav/readme/CONTRIBUTORS.rst | 2 ++ base_dav/readme/CREDITS.rst | 2 ++ base_dav/readme/DESCRIPTION.rst | 3 +++ base_dav/readme/ROADMAP.rst | 7 +++++ base_dav/static/description/index.html | 1 - base_dav/tests/test_collection.py | 24 +++++++++++------ requirements.txt | 2 +- 9 files changed, 49 insertions(+), 23 deletions(-) create mode 100755 base_dav/readme/CONFIGURE.rst create mode 100755 base_dav/readme/CONTRIBUTORS.rst create mode 100755 base_dav/readme/CREDITS.rst create mode 100755 base_dav/readme/DESCRIPTION.rst create mode 100755 base_dav/readme/ROADMAP.rst diff --git a/base_dav/models/dav_collection_field_mapping.py b/base_dav/models/dav_collection_field_mapping.py index 48c88c4d1..fe006baf6 100644 --- a/base_dav/models/dav_collection_field_mapping.py +++ b/base_dav/models/dav_collection_field_mapping.py @@ -9,7 +9,7 @@ from dateutil import tz from odoo import api, fields, models, tools - +from odoo.tools import safe_eval class DavCollectionFieldMapping(models.Model): _name = "dav.collection.field_mapping" @@ -59,15 +59,15 @@ def from_vobject(self, child): def _from_vobject_code(self, child): self.ensure_one() context = { - "datetime": datetime, - "dateutil": dateutil, + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, "item": child, "result": None, - "tools": tools, - "tz": tz, - "vobject": vobject, + # "tools": tools, + # "tz": tz, + # "vobject": vobject, } - tools.safe_eval(self.import_code, context, mode="exec", nocopy=True) + safe_eval.safe_eval(self.import_code, context, mode="exec", nocopy=True) return context.get("result", {}) def _from_vobject_simple(self, child): @@ -126,15 +126,15 @@ def to_vobject(self, record): def _to_vobject_code(self, record): self.ensure_one() context = { - "datetime": datetime, - "dateutil": dateutil, + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, "record": record, "result": None, - "tools": tools, - "tz": tz, - "vobject": vobject, + # "tools": tools, + # "tz": tz, + # "vobject": vobject, } - tools.safe_eval(self.export_code, context, mode="exec", nocopy=True) + safe_eval.safe_eval(self.export_code, context, mode="exec", nocopy=True) return context.get("result", None) def _to_vobject_simple(self, record): diff --git a/base_dav/readme/CONFIGURE.rst b/base_dav/readme/CONFIGURE.rst new file mode 100755 index 000000000..dc57f0e19 --- /dev/null +++ b/base_dav/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +To configure this module, you need to: + +#. go to `Settings / WebDAV Collections` and create or edit your collections. There, you'll also see the URL to point your clients to. + +Note that you need to configure a dbfilter if you use multiple databases. diff --git a/base_dav/readme/CONTRIBUTORS.rst b/base_dav/readme/CONTRIBUTORS.rst new file mode 100755 index 000000000..9c7447f2a --- /dev/null +++ b/base_dav/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Holger Brunn +* Florian Kantelberg diff --git a/base_dav/readme/CREDITS.rst b/base_dav/readme/CREDITS.rst new file mode 100755 index 000000000..b83250bd3 --- /dev/null +++ b/base_dav/readme/CREDITS.rst @@ -0,0 +1,2 @@ +* Odoo Community Association: `Icon `_ +* All the actual work is done by `Radicale `_ diff --git a/base_dav/readme/DESCRIPTION.rst b/base_dav/readme/DESCRIPTION.rst new file mode 100755 index 000000000..5b4aad0b7 --- /dev/null +++ b/base_dav/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module adds WebDAV support to Odoo, specifically CalDAV and CardDAV. + +You can configure arbitrary objects as a calendar or an address book, thus make arbitrary information accessible in external systems or your mobile. diff --git a/base_dav/readme/ROADMAP.rst b/base_dav/readme/ROADMAP.rst new file mode 100755 index 000000000..9897f6237 --- /dev/null +++ b/base_dav/readme/ROADMAP.rst @@ -0,0 +1,7 @@ +* much better UX for configuring collections (probably provide a group that sees the current fully flexible field mappings, and by default show some dumbed down version where you can select some preselected vobject fields) +* support todo lists and journals +* support configuring default field mappings per model +* support plain WebDAV collections to make some model's records accessible as folders, and the records' attachments as files (r/w) +* support configuring lists of calendars so that you can have a calendar for every project and appointments are tasks, or a calendar for every sales team and appointments are sale orders. Lots of possibilities + +Backporting this to <=v10 will be tricky because radicale only supports python3. Probably it will be quite a hassle to backport the relevant code, so it might be more sensible to just backport the configuration part, and implement the rest as radicale auth/storage plugin that talks to Odoo via odoorpc. It should be possible to recycle most of the code from this addon, which actually implements those plugins, but then within Odoo. diff --git a/base_dav/static/description/index.html b/base_dav/static/description/index.html index d83347b4b..7cf5816ad 100755 --- a/base_dav/static/description/index.html +++ b/base_dav/static/description/index.html @@ -1,4 +1,3 @@ - diff --git a/base_dav/tests/test_collection.py b/base_dav/tests/test_collection.py index 97f9b9ed0..06f1123be 100644 --- a/base_dav/tests/test_collection.py +++ b/base_dav/tests/test_collection.py @@ -4,11 +4,12 @@ from unittest import mock from odoo.tests.common import TransactionCase -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, mute_logger from ..radicale.collection import Collection +@mute_logger("radicale") class TestCalendar(TransactionCase): def setUp(self): super().setUp() @@ -24,21 +25,21 @@ def setUp(self): self.create_field_mapping( "login", - "base.field_res_users_login", + "base.field_res_users__login", excode="result = record.login", imcode="result = item.value", ) self.create_field_mapping( "name", - "base.field_res_users_name", + "base.field_res_users__name", ) self.create_field_mapping( "dtstart", - "base.field_res_users_create_date", + "base.field_res_users__create_date", ) self.create_field_mapping( "dtend", - "base.field_res_users_write_date", + "base.field_res_users__write_date", ) start = datetime.now() @@ -51,6 +52,9 @@ def setUp(self): "write_date": stop.strftime(DEFAULT_SERVER_DATETIME_FORMAT), } ) + patcher = mock.patch("odoo.http.request") + self.addCleanup(patcher.stop) + patcher.start() def create_field_mapping(self, name, field_ref, imcode=None, excode=None): return self.env["dav.collection.field_mapping"].create( @@ -69,8 +73,12 @@ def compare_record(self, vobj, rec=None): self.assertEqual((rec or self.record).login, tmp["login"]) self.assertEqual((rec or self.record).name, tmp["name"]) - self.assertEqual((rec or self.record).create_date, tmp["create_date"]) - self.assertEqual((rec or self.record).write_date, tmp["write_date"]) + create_date = (rec or self.record).create_date + self.assertEqual(create_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + tmp["create_date"]) + write_date = (rec or self.record).write_date + self.assertEqual(write_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + tmp["write_date"]) def test_import_export(self): # Exporting and importing should result in the same record @@ -82,7 +90,7 @@ def test_get_record(self): self.assertEqual(rec, self.record) self.collection.field_uuid = self.env.ref( - "base.field_res_users_login", + "base.field_res_users__login", ).id rec = self.collection.get_record([self.record.login]) self.assertEqual(rec, self.record) diff --git a/requirements.txt b/requirements.txt index f24d62b80..c123808a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ # generated from manifests external_dependencies -radicale +radicale==2.1.12 From a244bd53d14e92bda5f398dcf1d48593fadc9556 Mon Sep 17 00:00:00 2001 From: jaydeep32 Date: Wed, 14 Feb 2024 13:08:50 -0500 Subject: [PATCH 03/58] [IMP] base_dav: pre-commit auto fixes --- base_dav/models/dav_collection_field_mapping.py | 1 + base_dav/tests/test_collection.py | 10 ++++++---- requirements.txt | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/base_dav/models/dav_collection_field_mapping.py b/base_dav/models/dav_collection_field_mapping.py index fe006baf6..17ee8fc6c 100644 --- a/base_dav/models/dav_collection_field_mapping.py +++ b/base_dav/models/dav_collection_field_mapping.py @@ -11,6 +11,7 @@ from odoo import api, fields, models, tools from odoo.tools import safe_eval + class DavCollectionFieldMapping(models.Model): _name = "dav.collection.field_mapping" _description = "A field mapping for a WebDAV collection" diff --git a/base_dav/tests/test_collection.py b/base_dav/tests/test_collection.py index 06f1123be..155cb8a82 100644 --- a/base_dav/tests/test_collection.py +++ b/base_dav/tests/test_collection.py @@ -74,11 +74,13 @@ def compare_record(self, vobj, rec=None): self.assertEqual((rec or self.record).login, tmp["login"]) self.assertEqual((rec or self.record).name, tmp["name"]) create_date = (rec or self.record).create_date - self.assertEqual(create_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - tmp["create_date"]) + self.assertEqual( + create_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), tmp["create_date"] + ) write_date = (rec or self.record).write_date - self.assertEqual(write_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - tmp["write_date"]) + self.assertEqual( + write_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), tmp["write_date"] + ) def test_import_export(self): # Exporting and importing should result in the same record diff --git a/requirements.txt b/requirements.txt index c123808a5..f24d62b80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ # generated from manifests external_dependencies -radicale==2.1.12 +radicale From 10d533d0a768ae64ab86b351b9c8d0b0fbefff7b Mon Sep 17 00:00:00 2001 From: jaydeep32 Date: Wed, 14 Feb 2024 13:13:47 -0500 Subject: [PATCH 04/58] [UPD] base_dav: python package version --- base_dav/README.rst | 3 +++ requirements.txt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/base_dav/README.rst b/base_dav/README.rst index 9977eb605..7f22e96a3 100644 --- a/base_dav/README.rst +++ b/base_dav/README.rst @@ -75,6 +75,9 @@ to Odoo via odoorpc. It should be possible to recycle most of the code from this addon, which actually implements those plugins, but then within Odoo. +In order to install this module and use it make sure you use right version of `radicale==2.1.12` library. + + Bug Tracker =========== diff --git a/requirements.txt b/requirements.txt index f24d62b80..c123808a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ # generated from manifests external_dependencies -radicale +radicale==2.1.12 From 804cce3a693bcc709a11ab1e9a2c4504f231fd1c Mon Sep 17 00:00:00 2001 From: jaydeep32 Date: Wed, 14 Feb 2024 13:28:20 -0500 Subject: [PATCH 05/58] [UPD] base_dav: Module external_dependencies with version. --- base_dav/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_dav/__manifest__.py b/base_dav/__manifest__.py index a0e5a57e8..9e6075592 100644 --- a/base_dav/__manifest__.py +++ b/base_dav/__manifest__.py @@ -20,6 +20,6 @@ "security/ir.model.access.csv", ], "external_dependencies": { - "python": ["radicale"], + "python": ["radicale==2.1.12"], }, } From a14edea011c414e5736ff2fc2a2449f3bd370362 Mon Sep 17 00:00:00 2001 From: Ivorra78 Date: Wed, 14 Feb 2024 17:35:58 +0000 Subject: [PATCH 06/58] Translated using Weblate (Spanish) Currently translated at 100.0% (62 of 62 strings) Translation: server-backend-17.0/server-backend-17.0-base_user_role Translate-URL: https://translation.odoo-community.org/projects/server-backend-17-0/server-backend-17-0-base_user_role/es/ --- base_user_role/i18n/es.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base_user_role/i18n/es.po b/base_user_role/i18n/es.po index bfba5b704..3b825cf36 100644 --- a/base_user_role/i18n/es.po +++ b/base_user_role/i18n/es.po @@ -12,7 +12,7 @@ msgstr "" "Project-Id-Version: Odoo Server 10.0c\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-12-17 02:07+0000\n" -"PO-Revision-Date: 2023-10-09 09:15+0000\n" +"PO-Revision-Date: 2024-02-14 20:36+0000\n" "Last-Translator: Ivorra78 \n" "Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" "Language: es\n" @@ -89,7 +89,7 @@ msgstr "Categoría del grupo asociado" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.group_groups_into_role_wiz_view msgid "Cancel" -msgstr "" +msgstr "Cancelar" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.create_from_user_wizard_view @@ -115,7 +115,7 @@ msgstr "Crear" #. module: base_user_role #: model:ir.actions.act_window,name:base_user_role.action_wizard_groups_into_role msgid "Create Role" -msgstr "" +msgstr "Crear Función" #. module: base_user_role #: model:ir.actions.act_window,name:base_user_role.create_from_user_wizard_action @@ -177,12 +177,12 @@ msgstr "" #. module: base_user_role #: model:ir.model,name:base_user_role.model_wizard_groups_into_role msgid "Group groups into a role" -msgstr "" +msgstr "Agrupar a los grupos en un rol" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_wizard_groups_into_role__name msgid "Group groups into a role and specify a name for this role" -msgstr "" +msgstr "Agrupar grupos en un rol y especificar un nombre para este rol" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_form From 2efc7ea4e9c3aa80c41496deee0da5559b82cd1a Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 15 Feb 2024 11:37:59 +0000 Subject: [PATCH 07/58] Translated using Weblate (Italian) Currently translated at 100.0% (62 of 62 strings) Translation: server-backend-17.0/server-backend-17.0-base_user_role Translate-URL: https://translation.odoo-community.org/projects/server-backend-17-0/server-backend-17-0-base_user_role/it/ --- base_user_role/i18n/it.po | 99 ++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/base_user_role/i18n/it.po b/base_user_role/i18n/it.po index ca749ca9d..06f2599d0 100644 --- a/base_user_role/i18n/it.po +++ b/base_user_role/i18n/it.po @@ -12,8 +12,8 @@ msgstr "" "Project-Id-Version: Odoo Server 10.0c\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-12-17 02:07+0000\n" -"PO-Revision-Date: 2023-09-07 17:36+0000\n" -"Last-Translator: Francesco Foresti \n" +"PO-Revision-Date: 2024-02-15 15:34+0000\n" +"Last-Translator: mymage \n" "Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" "Language: it\n" "MIME-Version: 1.0\n" @@ -26,80 +26,80 @@ msgstr "" #: model:ir.model.fields,field_description:base_user_role.field_res_groups__role_count #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__role_count msgid "# Roles" -msgstr "" +msgstr "N° ruoli" #. module: base_user_role #. odoo-python #: code:addons/base_user_role/models/role.py:0 #, python-format msgid "%s (copy)" -msgstr "" +msgstr "%s (copia)" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__model_access msgid "Access Controls" -msgstr "" +msgstr "Controlli di accesso" #. module: base_user_role #: model:ir.model,name:base_user_role.model_res_groups msgid "Access Groups" -msgstr "" +msgstr "Gruppi di accesso" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__menu_access msgid "Access Menu" -msgstr "" +msgstr "Menu d'accesso" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__model_access_ids #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_form msgid "Access Rights" -msgstr "" +msgstr "Diritti di accesso" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role_line__active msgid "Active" -msgstr "" +msgstr "Attivo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__category_id msgid "Application" -msgstr "" +msgstr "Applicazione" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_wizard_create_role_from_user__assign_to_user msgid "Assign to user" -msgstr "" +msgstr "Assegna a utente" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__group_category_id msgid "Associated category" -msgstr "" +msgstr "Categoria associata" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__group_id msgid "Associated group" -msgstr "" +msgstr "Gruppo associato" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_users_role__group_category_id msgid "Associated group's category" -msgstr "" +msgstr "Categoria del gruppo associato" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.group_groups_into_role_wiz_view msgid "Cancel" -msgstr "" +msgstr "Annulla" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.create_from_user_wizard_view msgid "Close" -msgstr "" +msgstr "Chiudi" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__color msgid "Color Index" -msgstr "" +msgstr "Indice colore" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__comment @@ -110,23 +110,23 @@ msgstr "Coomento" #: model_terms:ir.ui.view,arch_db:base_user_role.create_from_user_wizard_view #: model_terms:ir.ui.view,arch_db:base_user_role.group_groups_into_role_wiz_view msgid "Create" -msgstr "" +msgstr "Crea" #. module: base_user_role #: model:ir.actions.act_window,name:base_user_role.action_wizard_groups_into_role msgid "Create Role" -msgstr "" +msgstr "Crea ruolo" #. module: base_user_role #: model:ir.actions.act_window,name:base_user_role.create_from_user_wizard_action #: model_terms:ir.ui.view,arch_db:base_user_role.create_from_user_wizard_view msgid "Create role from user" -msgstr "" +msgstr "Crea ruolo dall'utente" #. module: base_user_role #: model:ir.model,name:base_user_role.model_wizard_create_role_from_user msgid "Create role from user wizard" -msgstr "" +msgstr "Procedura guidata creazione ruolo dall'utente" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__create_uid @@ -165,22 +165,24 @@ msgstr "Da" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__full_name msgid "Group Name" -msgstr "" +msgstr "Nome gruppo" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_users_role__share msgid "Group created to set access rights for sharing data with some users." msgstr "" +"Gruppo creato per impostare i diritti di accesso per la condivisione dei " +"dati con alcuni utenti." #. module: base_user_role #: model:ir.model,name:base_user_role.model_wizard_groups_into_role msgid "Group groups into a role" -msgstr "" +msgstr "Raggruppa gruppi in un ruolo" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_wizard_groups_into_role__name msgid "Group groups into a role and specify a name for this role" -msgstr "" +msgstr "Raggruppa gruppi in un ruolo e indica un nume per il ruolo" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_form @@ -198,12 +200,12 @@ msgstr "ID" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__implied_ids msgid "Inherits" -msgstr "" +msgstr "Eredita" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_form msgid "Internal Notes" -msgstr "" +msgstr "Note interne" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_groups__parent_ids @@ -212,6 +214,8 @@ msgid "" "Inverse relation for the Inherits field. The groups from which this group is " "inheriting" msgstr "" +"Relazione inversa per il campo ereditato. I gruppi che ereditano da questo " +"gruppo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__write_uid @@ -232,7 +236,7 @@ msgstr "Ultimo aggiornamento il" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__model_access_count msgid "Model Access Count" -msgstr "" +msgstr "Conteggio accesso modello" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__name @@ -245,38 +249,38 @@ msgstr "Nome" #: model:ir.model.fields,field_description:base_user_role.field_res_groups__trans_parent_ids #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__trans_parent_ids msgid "Parent Groups" -msgstr "" +msgstr "Gruppi padre" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_groups__parent_ids #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__parent_ids msgid "Parents" -msgstr "" +msgstr "Padri" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__rule_ids #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_form msgid "Record Rules" -msgstr "" +msgstr "Regole record" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_groups__role_id #: model:ir.model.fields,help:base_user_role.field_res_users_role__role_id msgid "Relation for the groups that represents a role" -msgstr "" +msgstr "Relazione per i gruppi che rappresentano un ruolo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_groups__role_id #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__role_id #: model:ir.model.fields,field_description:base_user_role.field_res_users_role_line__role_id msgid "Role" -msgstr "" +msgstr "Ruolo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users__role_line_ids #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__line_ids msgid "Role lines" -msgstr "" +msgstr "Righe del ruolo" #. module: base_user_role #: model:ir.actions.act_window,name:base_user_role.action_res_users_role_tree @@ -288,33 +292,33 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_form_inherit #: model_terms:ir.ui.view,arch_db:base_user_role.view_res_users_role_search msgid "Roles" -msgstr "" +msgstr "Ruoli" #. module: base_user_role #: model:ir.model.constraint,message:base_user_role.constraint_res_users_role_line_user_role_uniq msgid "Roles can be assigned to a user only once at a time" -msgstr "" +msgstr "I ruoli possono essere assegnati all'utente solo uno alla volta" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_groups__role_ids #: model:ir.model.fields,help:base_user_role.field_res_users_role__role_ids msgid "Roles in which the group is involved" -msgstr "" +msgstr "Ruoli nei quali il gruppo è coinvolo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__rule_groups msgid "Rules" -msgstr "" +msgstr "Regole" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__rules_count msgid "Rules Count" -msgstr "" +msgstr "Numero regole" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__share msgid "Share Group" -msgstr "" +msgstr "Gruppo condivisione" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role_line__date_to @@ -324,12 +328,12 @@ msgstr "A" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__trans_implied_ids msgid "Transitively inherits" -msgstr "" +msgstr "Eredita transitivamente" #. module: base_user_role #: model:ir.actions.server,name:base_user_role.cron_update_users_ir_actions_server msgid "Update user roles" -msgstr "" +msgstr "Aggiorna i ruoli dell'utente" #. module: base_user_role #: model:ir.model,name:base_user_role.model_res_users @@ -340,12 +344,12 @@ msgstr "Utente" #. module: base_user_role #: model:ir.model,name:base_user_role.model_res_users_role msgid "User role" -msgstr "" +msgstr "Ruolo utente" #. module: base_user_role #: model:ir.module.category,name:base_user_role.ir_module_category_role msgid "User roles" -msgstr "" +msgstr "Ruoli utente" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__users @@ -356,18 +360,17 @@ msgstr "Utenti" #. module: base_user_role #: model:ir.model,name:base_user_role.model_res_users_role_line msgid "Users associated to a role" -msgstr "" +msgstr "Utenti associati al ruolo" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_users_role__user_ids -#, fuzzy msgid "Users list" -msgstr "Utenti" +msgstr "Elenco utenti" #. module: base_user_role #: model:ir.model.fields,help:base_user_role.field_res_users_role__implied_ids msgid "Users of this group automatically inherit those groups" -msgstr "" +msgstr "Gli utenti di questo gruppo ereditano automaticamente quei gruppi" #. module: base_user_role #: model:ir.model.fields,field_description:base_user_role.field_res_groups__view_access @@ -378,7 +381,7 @@ msgstr "Viste" #. module: base_user_role #: model_terms:ir.ui.view,arch_db:base_user_role.create_from_user_wizard_view msgid "or" -msgstr "" +msgstr "o" #~ msgid "Last Modified on" #~ msgstr "Ultima modifica il" From a6546d7fe54ba63bc1d9ac8eec0619caa57b1a62 Mon Sep 17 00:00:00 2001 From: Chandresh Thakkar OSI Date: Tue, 4 May 2021 02:06:01 +0530 Subject: [PATCH 08/58] [ADD] base_user_role_company: add per company user roles --- base_user_role_company/__init__.py | 4 ++ base_user_role_company/__manifest__.py | 19 ++++++ base_user_role_company/models/__init__.py | 5 ++ base_user_role_company/models/ir_http.py | 23 +++++++ base_user_role_company/models/role.py | 65 +++++++++++++++++++ base_user_role_company/readme/CONFIGURE.rst | 6 ++ .../readme/CONTRIBUTORS.rst | 4 ++ base_user_role_company/readme/DESCRIPTION.rst | 9 +++ base_user_role_company/readme/USAGE.rst | 17 +++++ base_user_role_company/tests/__init__.py | 1 + .../tests/test_role_per_company.py | 62 ++++++++++++++++++ base_user_role_company/views/role.xml | 14 ++++ 12 files changed, 229 insertions(+) create mode 100644 base_user_role_company/__init__.py create mode 100644 base_user_role_company/__manifest__.py create mode 100644 base_user_role_company/models/__init__.py create mode 100644 base_user_role_company/models/ir_http.py create mode 100644 base_user_role_company/models/role.py create mode 100644 base_user_role_company/readme/CONFIGURE.rst create mode 100644 base_user_role_company/readme/CONTRIBUTORS.rst create mode 100644 base_user_role_company/readme/DESCRIPTION.rst create mode 100644 base_user_role_company/readme/USAGE.rst create mode 100644 base_user_role_company/tests/__init__.py create mode 100644 base_user_role_company/tests/test_role_per_company.py create mode 100644 base_user_role_company/views/role.xml diff --git a/base_user_role_company/__init__.py b/base_user_role_company/__init__.py new file mode 100644 index 000000000..bb83730e9 --- /dev/null +++ b/base_user_role_company/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/base_user_role_company/__manifest__.py b/base_user_role_company/__manifest__.py new file mode 100644 index 000000000..5b1af0981 --- /dev/null +++ b/base_user_role_company/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "User roles by company", + "version": "14.0.1.0.0", + "category": "Tools", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/server-backend", + "depends": ["base_user_role"], + "data": [ + "views/role.xml", + ], + "installable": True, + "auto_install": True, + "maintainer": "dreispt", + "development_status": "Beta", +} diff --git a/base_user_role_company/models/__init__.py b/base_user_role_company/models/__init__.py new file mode 100644 index 000000000..d38a0acf7 --- /dev/null +++ b/base_user_role_company/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import role +from . import ir_http diff --git a/base_user_role_company/models/ir_http.py b/base_user_role_company/models/ir_http.py new file mode 100644 index 000000000..7eaca704a --- /dev/null +++ b/base_user_role_company/models/ir_http.py @@ -0,0 +1,23 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.http import request + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + def session_info(self): + """ + Based on the selected companies (cids), + calculate the roles to enable. + A role should be enabled only when it applies to all selected companies. + """ + result = super(IrHttp, self).session_info() + if self.env.user.role_line_ids: + cids_str = request.httprequest.cookies.get("cids", str(self.env.company.id)) + cids = [int(cid) for cid in cids_str.split(",")] + self.env.user._set_session_active_roles(cids) + self.env.user.set_groups_from_roles() + return result diff --git a/base_user_role_company/models/role.py b/base_user_role_company/models/role.py new file mode 100644 index 000000000..c6702494c --- /dev/null +++ b/base_user_role_company/models/role.py @@ -0,0 +1,65 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResUsersRoleLine(models.Model): + _inherit = "res.users.role.line" + + company_id = fields.Many2one( + "res.company", + "Company", + help="If set, this role only applies when this is the main company selected." + " Otherwise it applies to all companies.", + ) + active_role = fields.Boolean(string="Active Role", default=True) + + @api.constrains("user_id", "company_id") + def _check_company(self): + for record in self: + if ( + record.company_id + and record.company_id != record.user_id.company_id + and record.company_id not in record.user_id.company_ids + ): + raise ValidationError( + _('User "{}" does not have access to the company "{}"').format( + record.user_id.name, record.company_id.name + ) + ) + + _sql_constraints = [ + ( + "user_role_uniq", + "unique (user_id,role_id,company_id)", + "Roles can be assigned to a user only once at a time", + ) + ] + + +class ResUsers(models.Model): + _inherit = "res.users" + + def _get_enabled_roles(self): + res = super()._get_enabled_roles() + return res.filtered("active_role") + + @api.model + def _set_session_active_roles(self, cids): + """ + Based on the selected companies (cids), + calculate the roles to enable. + A role should be enabled only when it applies to all selected companies. + """ + for role_line in self.env.user.role_line_ids: + if not role_line.company_id: + role_line.active_role = True + elif role_line.company_id.id in cids: + is_on_companies = self.env.user.role_line_ids.filtered( + lambda x: x.role_id == role_line.role_id and x.company_id.id in cids + ) + role_line.active_role = len(is_on_companies) == len(cids) + else: + role_line.active_role = False diff --git a/base_user_role_company/readme/CONFIGURE.rst b/base_user_role_company/readme/CONFIGURE.rst new file mode 100644 index 000000000..c50bf9fec --- /dev/null +++ b/base_user_role_company/readme/CONFIGURE.rst @@ -0,0 +1,6 @@ +Roles are set on the User form. + +The "Company" additional column allows to set a Role as only valid for specific companies. + +There is also a "Active Role" techincal field, only visible in developer mode. +It shows what roles are active, after applying the company selection rules. diff --git a/base_user_role_company/readme/CONTRIBUTORS.rst b/base_user_role_company/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..c259741d2 --- /dev/null +++ b/base_user_role_company/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +`Open Source Integrators `_ + + * Daniel Reis + * Chandresh Thakkar diff --git a/base_user_role_company/readme/DESCRIPTION.rst b/base_user_role_company/readme/DESCRIPTION.rst new file mode 100644 index 000000000..68507f1e4 --- /dev/null +++ b/base_user_role_company/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +Enable User Roles depending on the Companies selected. + +A company specific Role will only be enabled +if it is set for **all** the currently selected companies. + +For example, if a user is "Sales Manager" only for Company A, +it will see that role enabled only if Company A is selected. +If the user selects Company A and Company B, +then the "Sales Manager" role won't be enabled. diff --git a/base_user_role_company/readme/USAGE.rst b/base_user_role_company/readme/USAGE.rst new file mode 100644 index 000000000..88dce6bc7 --- /dev/null +++ b/base_user_role_company/readme/USAGE.rst @@ -0,0 +1,17 @@ +Select the active companies from the web client widget, near the top right corner. +When doing so, the User's security Groups are recomputed, based on the Roles. + +When the user changes the company selection, only the groups available to all active companies will be activated. + +For example: + +* A "SALES PERSON" and a "SALES MANAGER" roles are created. + +* A user is assigned to the roles: + * "SALES PERSON", with no specific company assigned (meaning all) + * "SALES MANAGER" only to "My Company (Chicago)" + +* When selecting active companies from the UI widget: + * If only "My Company (San Francisco)" is active, "SALES PERSON" will be active. + * If only "My Company (Chicago)" is active, "SALES PERSON" and "SALES MANAGER" will be active. + * If both "My Company (San Francisco)" and "My Company (Chicago)" is active, "SALES PERSON" will be active. diff --git a/base_user_role_company/tests/__init__.py b/base_user_role_company/tests/__init__.py new file mode 100644 index 000000000..cd7f7833f --- /dev/null +++ b/base_user_role_company/tests/__init__.py @@ -0,0 +1 @@ +from . import test_role_per_company diff --git a/base_user_role_company/tests/test_role_per_company.py b/base_user_role_company/tests/test_role_per_company.py new file mode 100644 index 000000000..14cb869bf --- /dev/null +++ b/base_user_role_company/tests/test_role_per_company.py @@ -0,0 +1,62 @@ +# Copyright 2021 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.tests.common import TransactionCase + + +class TestUserRoleCompany(TransactionCase): + def setUp(self): + super().setUp() + # COMPANIES + self.Company = self.env["res.company"] + self.company1 = self.env.ref("base.main_company") + self.company2 = self.Company.create({"name": "company2"}) + # ROLES + self.Role = self.env["res.users.role"] + self.roleA = self.Role.create({"name": "ROLE All Companies"}) + self.roleB = self.Role.create({"name": "ROLE Company 1"}) + self.roleC = self.Role.create({"name": "ROLE Company 1 and 2"}) + # USER + # ==Role=== ==Company== C1 C2 C1+C2 + # Role A Yes Yes Yes + # Role B Company1 Yes + # Role C Company1 Yes Yes + # Role C Company2 Yes Yes + self.User = self.env["res.users"] + user_vals = { + "name": "ROLES TEST USER", + "login": "test_user", + "company_ids": [(6, 0, [self.company1.id, self.company2.id])], + "role_line_ids": [ + (0, 0, {"role_id": self.roleA.id}), + (0, 0, {"role_id": self.roleB.id, "company_id": self.company1.id}), + (0, 0, {"role_id": self.roleC.id, "company_id": self.company1.id}), + (0, 0, {"role_id": self.roleC.id, "company_id": self.company2.id}), + ], + } + self.test_user = self.User.create(user_vals) + self.User = self.User.with_user(self.test_user) + + def test_110_company_1(self): + "Company 1 selected: Tech and Settings roles are activated" + self.User._set_session_active_roles([self.company1.id]) + active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( + "role_id" + ) + self.assertEqual(active_roles, self.roleA | self.roleB | self.roleC) + + def test_120_company_2(self): + "Company 2 selected: only Tech role enabled" + self.User._set_session_active_roles([self.company2.id]) + active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( + "role_id" + ) + self.assertEqual(active_roles, self.roleA | self.roleC) + + def test_130_company_1_2(self): + "Settings Role enabled for Company 1 and 2" + self.User._set_session_active_roles([self.company1.id, self.company2.id]) + active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( + "role_id" + ) + self.assertEqual(active_roles, self.roleA | self.roleC) diff --git a/base_user_role_company/views/role.xml b/base_user_role_company/views/role.xml new file mode 100644 index 000000000..bfcb01e8f --- /dev/null +++ b/base_user_role_company/views/role.xml @@ -0,0 +1,14 @@ + + + + res.users.form.inherit.company + res.users + + + + + + + + + From e0635079448d7dff1748ad7e7c27123425fe7d5e Mon Sep 17 00:00:00 2001 From: oca-travis Date: Wed, 19 May 2021 18:13:39 +0000 Subject: [PATCH 09/58] [UPD] Update base_user_role_company.pot --- .../i18n/base_user_role_company.pot | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 base_user_role_company/i18n/base_user_role_company.pot diff --git a/base_user_role_company/i18n/base_user_role_company.pot b/base_user_role_company/i18n/base_user_role_company.pot new file mode 100644 index 000000000..776c3cb0a --- /dev/null +++ b/base_user_role_company/i18n/base_user_role_company.pot @@ -0,0 +1,78 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_user_role_company +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \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: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__active_role +msgid "Active Role" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__company_id +msgid "Company" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__display_name +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users__display_name +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__display_name +msgid "Display Name" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__id +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users__id +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__id +msgid "ID" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,help:base_user_role_company.field_res_users_role_line__company_id +msgid "" +"If set, this role only applies when this is the main company selected. " +"Otherwise it applies to all companies." +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http____last_update +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users____last_update +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.constraint,message:base_user_role_company.constraint_res_users_role_line_user_role_uniq +msgid "Roles can be assigned to a user only once at a time" +msgstr "" + +#. module: base_user_role_company +#: code:addons/base_user_role_company/models/role.py:0 +#, python-format +msgid "User \"{}\" does not have access to the company \"{}\"" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_res_users +msgid "Users" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_res_users_role_line +msgid "Users associated to a role" +msgstr "" From 0fba5262253d270e24965a5e7ea8a8fe18ba9698 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 May 2021 18:27:49 +0000 Subject: [PATCH 10/58] [UPD] README.rst --- base_user_role_company/README.rst | 115 +++++ .../static/description/index.html | 467 ++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 base_user_role_company/README.rst create mode 100644 base_user_role_company/static/description/index.html diff --git a/base_user_role_company/README.rst b/base_user_role_company/README.rst new file mode 100644 index 000000000..5b7d12252 --- /dev/null +++ b/base_user_role_company/README.rst @@ -0,0 +1,115 @@ +===================== +User roles by company +===================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--backend-lightgray.png?logo=github + :target: https://github.com/OCA/server-backend/tree/14.0/base_user_role_company + :alt: OCA/server-backend +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-backend-14-0/server-backend-14-0-base_user_role_company + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/253/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Enable User Roles depending on the Companies selected. + +A company specific Role will only be enabled +if it is set for **all** the currently selected companies. + +For example, if a user is "Sales Manager" only for Company A, +it will see that role enabled only if Company A is selected. +If the user selects Company A and Company B, +then the "Sales Manager" role won't be enabled. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Roles are set on the User form. + +The "Company" additional column allows to set a Role as only valid for specific companies. + +There is also a "Active Role" techincal field, only visible in developer mode. +It shows what roles are active, after applying the company selection rules. + +Usage +===== + +Select the active companies from the web client widget, near the top right corner. +When doing so, the User's security Groups are recomputed, based on the Roles. + +When the user changes the company selection, only the groups available to all active companies will be activated. + +For example: + +* A "SALES PERSON" and a "SALES MANAGER" roles are created. + +* A user is assigned to the roles: + * "SALES PERSON", with no specific company assigned (meaning all) + * "SALES MANAGER" only to "My Company (Chicago)" + +* When selecting active companies from the UI widget: + * If only "My Company (San Francisco)" is active, "SALES PERSON" will be active. + * If only "My Company (Chicago)" is active, "SALES PERSON" and "SALES MANAGER" will be active. + * If both "My Company (San Francisco)" and "My Company (Chicago)" is active, "SALES PERSON" will be active. + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Open Source Integrators + +Contributors +~~~~~~~~~~~~ + +`Open Source Integrators `_ + + * Daniel Reis + * Chandresh Thakkar + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/server-backend `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_user_role_company/static/description/index.html b/base_user_role_company/static/description/index.html new file mode 100644 index 000000000..9eb4a6fcc --- /dev/null +++ b/base_user_role_company/static/description/index.html @@ -0,0 +1,467 @@ + + + + + + +User roles by company + + + +
+

User roles by company

+ + +

Beta License: AGPL-3 OCA/server-backend Translate me on Weblate Try me on Runbot

+

Enable User Roles depending on the Companies selected.

+

A company specific Role will only be enabled +if it is set for all the currently selected companies.

+

For example, if a user is “Sales Manager” only for Company A, +it will see that role enabled only if Company A is selected. +If the user selects Company A and Company B, +then the “Sales Manager” role won’t be enabled.

+

Table of contents

+ +
+

Configuration

+

Roles are set on the User form.

+

The “Company” additional column allows to set a Role as only valid for specific companies.

+

There is also a “Active Role” techincal field, only visible in developer mode. +It shows what roles are active, after applying the company selection rules.

+
+
+

Usage

+

Select the active companies from the web client widget, near the top right corner. +When doing so, the User’s security Groups are recomputed, based on the Roles.

+

When the user changes the company selection, only the groups available to all active companies will be activated.

+

For example:

+
    +
  • A “SALES PERSON” and a “SALES MANAGER” roles are created.
  • +
  • +
    A user is assigned to the roles:
    +
      +
    • “SALES PERSON”, with no specific company assigned (meaning all)
    • +
    • “SALES MANAGER” only to “My Company (Chicago)”
    • +
    +
    +
    +
  • +
  • +
    When selecting active companies from the UI widget:
    +
      +
    • If only “My Company (San Francisco)” is active, “SALES PERSON” will be active.
    • +
    • If only “My Company (Chicago)” is active, “SALES PERSON” and “SALES MANAGER” will be active.
    • +
    • If both “My Company (San Francisco)” and “My Company (Chicago)” is active, “SALES PERSON” will be active.
    • +
    +
    +
    +
  • +
+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Open Source Integrators
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/server-backend project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From 5a153103e19521f50d426f09001458f6d9178574 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 May 2021 18:27:49 +0000 Subject: [PATCH 11/58] [ADD] icon.png --- .../static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 base_user_role_company/static/description/icon.png diff --git a/base_user_role_company/static/description/icon.png b/base_user_role_company/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 From 2976844caeed2d6641d29b5282ebebc67e97acde Mon Sep 17 00:00:00 2001 From: Andrii Skrypka Date: Wed, 3 Nov 2021 10:04:09 +0200 Subject: [PATCH 12/58] [FIX] base_user_role_company: wrong xpath --- base_user_role_company/views/role.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/base_user_role_company/views/role.xml b/base_user_role_company/views/role.xml index bfcb01e8f..89bd80ad7 100644 --- a/base_user_role_company/views/role.xml +++ b/base_user_role_company/views/role.xml @@ -5,10 +5,13 @@ res.users - + - + From 9752e9cf499bd507898da9184f7ef284bebd9afb Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 4 Nov 2021 18:50:53 +0000 Subject: [PATCH 13/58] base_user_role_company 14.0.1.1.0 --- base_user_role_company/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_user_role_company/__manifest__.py b/base_user_role_company/__manifest__.py index 5b1af0981..8e306b5c8 100644 --- a/base_user_role_company/__manifest__.py +++ b/base_user_role_company/__manifest__.py @@ -3,7 +3,7 @@ { "name": "User roles by company", - "version": "14.0.1.0.0", + "version": "14.0.1.1.0", "category": "Tools", "author": "Open Source Integrators, Odoo Community Association (OCA)", "license": "AGPL-3", From b7c3aa688d00d80a98fa2f26f003542bdf45ce3a Mon Sep 17 00:00:00 2001 From: Riccardo Bellanova Date: Tue, 15 Feb 2022 12:15:48 +0000 Subject: [PATCH 14/58] Added translation using Weblate (Italian) --- base_user_role_company/i18n/it.po | 79 +++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 base_user_role_company/i18n/it.po diff --git a/base_user_role_company/i18n/it.po b/base_user_role_company/i18n/it.po new file mode 100644 index 000000000..b48538f39 --- /dev/null +++ b/base_user_role_company/i18n/it.po @@ -0,0 +1,79 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_user_role_company +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__active_role +msgid "Active Role" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__company_id +msgid "Company" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__display_name +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users__display_name +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__display_name +msgid "Display Name" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__id +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users__id +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__id +msgid "ID" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,help:base_user_role_company.field_res_users_role_line__company_id +msgid "" +"If set, this role only applies when this is the main company selected. " +"Otherwise it applies to all companies." +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.fields,field_description:base_user_role_company.field_ir_http____last_update +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users____last_update +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model.constraint,message:base_user_role_company.constraint_res_users_role_line_user_role_uniq +msgid "Roles can be assigned to a user only once at a time" +msgstr "" + +#. module: base_user_role_company +#: code:addons/base_user_role_company/models/role.py:0 +#, python-format +msgid "User \"{}\" does not have access to the company \"{}\"" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_res_users +msgid "Users" +msgstr "" + +#. module: base_user_role_company +#: model:ir.model,name:base_user_role_company.model_res_users_role_line +msgid "Users associated to a role" +msgstr "" From 31b92c7de9319063635249e2c0d3d3d1ec93f235 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 15 Feb 2022 12:34:49 +0000 Subject: [PATCH 15/58] [FIX] base_user_role_company: roles not properly applied on login --- base_user_role_company/models/__init__.py | 1 + base_user_role_company/models/ir_http.py | 4 +-- base_user_role_company/models/role.py | 29 ++---------------- base_user_role_company/models/user.py | 36 +++++++++++++++++++++++ base_user_role_company/views/role.xml | 2 +- 5 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 base_user_role_company/models/user.py diff --git a/base_user_role_company/models/__init__.py b/base_user_role_company/models/__init__.py index d38a0acf7..32004dfac 100644 --- a/base_user_role_company/models/__init__.py +++ b/base_user_role_company/models/__init__.py @@ -2,4 +2,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import role +from . import user from . import ir_http diff --git a/base_user_role_company/models/ir_http.py b/base_user_role_company/models/ir_http.py index 7eaca704a..65a6b3b91 100644 --- a/base_user_role_company/models/ir_http.py +++ b/base_user_role_company/models/ir_http.py @@ -18,6 +18,6 @@ def session_info(self): if self.env.user.role_line_ids: cids_str = request.httprequest.cookies.get("cids", str(self.env.company.id)) cids = [int(cid) for cid in cids_str.split(",")] - self.env.user._set_session_active_roles(cids) - self.env.user.set_groups_from_roles() + # The first element of cids is the currently selected company + self.env.user.set_groups_from_roles(company_id=cids[0]) return result diff --git a/base_user_role_company/models/role.py b/base_user_role_company/models/role.py index c6702494c..0fdd234d8 100644 --- a/base_user_role_company/models/role.py +++ b/base_user_role_company/models/role.py @@ -8,13 +8,14 @@ class ResUsersRoleLine(models.Model): _inherit = "res.users.role.line" + allowed_company_ids = fields.Many2many(related="user_id.company_ids") company_id = fields.Many2one( "res.company", "Company", + domain="[('id', 'in', allowed_company_ids)]", help="If set, this role only applies when this is the main company selected." " Otherwise it applies to all companies.", ) - active_role = fields.Boolean(string="Active Role", default=True) @api.constrains("user_id", "company_id") def _check_company(self): @@ -37,29 +38,3 @@ def _check_company(self): "Roles can be assigned to a user only once at a time", ) ] - - -class ResUsers(models.Model): - _inherit = "res.users" - - def _get_enabled_roles(self): - res = super()._get_enabled_roles() - return res.filtered("active_role") - - @api.model - def _set_session_active_roles(self, cids): - """ - Based on the selected companies (cids), - calculate the roles to enable. - A role should be enabled only when it applies to all selected companies. - """ - for role_line in self.env.user.role_line_ids: - if not role_line.company_id: - role_line.active_role = True - elif role_line.company_id.id in cids: - is_on_companies = self.env.user.role_line_ids.filtered( - lambda x: x.role_id == role_line.role_id and x.company_id.id in cids - ) - role_line.active_role = len(is_on_companies) == len(cids) - else: - role_line.active_role = False diff --git a/base_user_role_company/models/user.py b/base_user_role_company/models/user.py new file mode 100644 index 000000000..edc9ee0d2 --- /dev/null +++ b/base_user_role_company/models/user.py @@ -0,0 +1,36 @@ +# Copyright (C) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + @classmethod + def authenticate(cls, db, login, password, user_agent_env): + uid = super().authenticate(db, login, password, user_agent_env) + # On login, ensure the proper roles are applied + # The last Role applied may not be the correct one, + # sonce the new session current company can be different + with cls.pool.cursor() as cr: + env = api.Environment(cr, uid, {}) + if env.user.role_line_ids: + env.user.set_groups_from_roles() + return uid + + def _get_enabled_roles(self): + res = super()._get_enabled_roles() + # Enable only the Roles corresponing to the currently selected company + if self.env.user.role_line_ids: + curr_company = self.env.company + res = res.filtered( + lambda x: not x.company_id or x.company_id == curr_company + ) + return res + + def set_groups_from_roles(self, force=False, company_id=False): + # When using the Company Switcher widget, the self.env.company is not yet set + if company_id: + self = self.with_company(company_id) + return super().set_groups_from_roles(force=force) diff --git a/base_user_role_company/views/role.xml b/base_user_role_company/views/role.xml index 89bd80ad7..9cb06b7e9 100644 --- a/base_user_role_company/views/role.xml +++ b/base_user_role_company/views/role.xml @@ -9,8 +9,8 @@ expr="//field[@name='role_line_ids']//field[@name='role_id']" position="after" > + - From c6ef4fa23cac0c2c12bb22daa69c027dfe3f0385 Mon Sep 17 00:00:00 2001 From: Riccardo Bellanova Date: Tue, 15 Feb 2022 13:51:05 +0000 Subject: [PATCH 16/58] Translated using Weblate (Italian) Currently translated at 100.0% (11 of 11 strings) Translation: server-backend-14.0/server-backend-14.0-base_user_role_company Translate-URL: https://translation.odoo-community.org/projects/server-backend-14-0/server-backend-14-0-base_user_role_company/it/ --- base_user_role_company/i18n/it.po | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/base_user_role_company/i18n/it.po b/base_user_role_company/i18n/it.po index b48538f39..f28ab71a1 100644 --- a/base_user_role_company/i18n/it.po +++ b/base_user_role_company/i18n/it.po @@ -6,42 +6,44 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2022-02-15 16:16+0000\n" +"Last-Translator: Riccardo Bellanova \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.3.2\n" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__active_role msgid "Active Role" -msgstr "" +msgstr "Ruolo attivo" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__company_id msgid "Company" -msgstr "" +msgstr "Azienda" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__display_name #: model:ir.model.fields,field_description:base_user_role_company.field_res_users__display_name #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: base_user_role_company #: model:ir.model,name:base_user_role_company.model_ir_http msgid "HTTP Routing" -msgstr "" +msgstr "Rotta HTTP" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__id #: model:ir.model.fields,field_description:base_user_role_company.field_res_users__id #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__id msgid "ID" -msgstr "" +msgstr "ID" #. module: base_user_role_company #: model:ir.model.fields,help:base_user_role_company.field_res_users_role_line__company_id @@ -49,31 +51,33 @@ msgid "" "If set, this role only applies when this is the main company selected. " "Otherwise it applies to all companies." msgstr "" +"Se impostato, questo ruolo si applica solo quando questa è l'azienda " +"principale selezionata. Altrimenti vale per tutte le aziende." #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_ir_http____last_update #: model:ir.model.fields,field_description:base_user_role_company.field_res_users____last_update #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: base_user_role_company #: model:ir.model.constraint,message:base_user_role_company.constraint_res_users_role_line_user_role_uniq msgid "Roles can be assigned to a user only once at a time" -msgstr "" +msgstr "I ruoli possono essere assegnati all'utente solo uno alla volta" #. module: base_user_role_company #: code:addons/base_user_role_company/models/role.py:0 #, python-format msgid "User \"{}\" does not have access to the company \"{}\"" -msgstr "" +msgstr "L'utente \"{}\" non ha l'accesso all'azienda \"{}\"" #. module: base_user_role_company #: model:ir.model,name:base_user_role_company.model_res_users msgid "Users" -msgstr "" +msgstr "Utenti" #. module: base_user_role_company #: model:ir.model,name:base_user_role_company.model_res_users_role_line msgid "Users associated to a role" -msgstr "" +msgstr "Utenti associati al ruolo" From b0b99e039d865f0f1a9e4e0d403bce33605f870d Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 2 Mar 2022 12:42:30 +0000 Subject: [PATCH 17/58] [FIX] base_user_role_company: wrong menus on re-login Issue found on logout / relogin. The user groups were applied correctly, but the main menu showed apps the user did not have access to. This was related to the menu caching mechanisn, that was disabled here. --- base_user_role_company/__init__.py | 1 + base_user_role_company/controllers/__init__.py | 1 + base_user_role_company/controllers/main.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 base_user_role_company/controllers/__init__.py create mode 100644 base_user_role_company/controllers/main.py diff --git a/base_user_role_company/__init__.py b/base_user_role_company/__init__.py index bb83730e9..efc2a3f33 100644 --- a/base_user_role_company/__init__.py +++ b/base_user_role_company/__init__.py @@ -1,4 +1,5 @@ # Copyright (C) 2021 Open Source Integrators # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import controllers from . import models diff --git a/base_user_role_company/controllers/__init__.py b/base_user_role_company/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/base_user_role_company/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/base_user_role_company/controllers/main.py b/base_user_role_company/controllers/main.py new file mode 100644 index 000000000..7df62870e --- /dev/null +++ b/base_user_role_company/controllers/main.py @@ -0,0 +1,16 @@ +# Copyright (C) 2022 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import http + +from odoo.addons.web.controllers.main import Home + + +class HomeExtended(Home): + @http.route() + def web_load_menus(self, unique): + response = super().web_load_menus(unique) + # On logout & re-login we could see wrong menus being rendered + # To avoid this, menu http cache must be disabled + response.headers.remove("Cache-Control") + return response From e81489f52f6ce6b8e6a08f2487d7c087c9fd1506 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 3 Mar 2022 11:40:58 +0000 Subject: [PATCH 18/58] [FIX] base_user_role_company: fix tests --- base_user_role_company/models/user.py | 5 +-- .../tests/test_role_per_company.py | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/base_user_role_company/models/user.py b/base_user_role_company/models/user.py index edc9ee0d2..1a2f6296b 100644 --- a/base_user_role_company/models/user.py +++ b/base_user_role_company/models/user.py @@ -22,10 +22,9 @@ def authenticate(cls, db, login, password, user_agent_env): def _get_enabled_roles(self): res = super()._get_enabled_roles() # Enable only the Roles corresponing to the currently selected company - if self.env.user.role_line_ids: - curr_company = self.env.company + if self.role_line_ids: res = res.filtered( - lambda x: not x.company_id or x.company_id == curr_company + lambda x: not x.company_id or x.company_id == self.env.company ) return res diff --git a/base_user_role_company/tests/test_role_per_company.py b/base_user_role_company/tests/test_role_per_company.py index 14cb869bf..9b1ac1fcc 100644 --- a/base_user_role_company/tests/test_role_per_company.py +++ b/base_user_role_company/tests/test_role_per_company.py @@ -11,11 +11,18 @@ def setUp(self): self.Company = self.env["res.company"] self.company1 = self.env.ref("base.main_company") self.company2 = self.Company.create({"name": "company2"}) + # GROUPS for roles + self.groupA = self.env.ref("base.group_user") + self.groupB = self.env.ref("base.group_system") + self.groupC = self.env.ref("base.group_partner_manager") # ROLES self.Role = self.env["res.users.role"] self.roleA = self.Role.create({"name": "ROLE All Companies"}) + self.roleA.implied_ids |= self.groupA self.roleB = self.Role.create({"name": "ROLE Company 1"}) + self.roleB.implied_ids |= self.groupB self.roleC = self.Role.create({"name": "ROLE Company 1 and 2"}) + self.roleC.implied_ids |= self.groupC # USER # ==Role=== ==Company== C1 C2 C1+C2 # Role A Yes Yes Yes @@ -35,28 +42,22 @@ def setUp(self): ], } self.test_user = self.User.create(user_vals) - self.User = self.User.with_user(self.test_user) def test_110_company_1(self): - "Company 1 selected: Tech and Settings roles are activated" - self.User._set_session_active_roles([self.company1.id]) - active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( - "role_id" - ) - self.assertEqual(active_roles, self.roleA | self.roleB | self.roleC) + "Company 1 selected: Roles A, B and C are enabled" + self.test_user.set_groups_from_roles(company_id=self.company1.id) + expected = self.groupA | self.groupB | self.groupC + found = self.test_user.groups_id.filtered(lambda x: x in expected) + self.assertEqual(expected, found) def test_120_company_2(self): - "Company 2 selected: only Tech role enabled" - self.User._set_session_active_roles([self.company2.id]) - active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( - "role_id" - ) - self.assertEqual(active_roles, self.roleA | self.roleC) + "Company 2 selected: Roles A and C are enabled" + self.test_user.set_groups_from_roles(company_id=self.company2.id) + enabled = self.test_user.groups_id + expected = self.groupA | self.groupC + found = enabled.filtered(lambda x: x in expected) + self.assertEqual(expected, found) - def test_130_company_1_2(self): - "Settings Role enabled for Company 1 and 2" - self.User._set_session_active_roles([self.company1.id, self.company2.id]) - active_roles = self.test_user.role_line_ids.filtered("active_role").mapped( - "role_id" - ) - self.assertEqual(active_roles, self.roleA | self.roleC) + not_expected = self.groupB + found = enabled.filtered(lambda x: x in not_expected) + self.assertFalse(found) From 8e0628dc90c00283016cd0c04f65269926974ad8 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 3 Mar 2022 15:29:25 +0000 Subject: [PATCH 19/58] [UPD] Update base_user_role_company.pot --- base_user_role_company/i18n/base_user_role_company.pot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base_user_role_company/i18n/base_user_role_company.pot b/base_user_role_company/i18n/base_user_role_company.pot index 776c3cb0a..3b1fe1508 100644 --- a/base_user_role_company/i18n/base_user_role_company.pot +++ b/base_user_role_company/i18n/base_user_role_company.pot @@ -14,8 +14,8 @@ msgstr "" "Plural-Forms: \n" #. module: base_user_role_company -#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__active_role -msgid "Active Role" +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__allowed_company_ids +msgid "Companies" msgstr "" #. module: base_user_role_company From 677b85344943c2487bc73222cfb3c0970d2f4871 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 3 Mar 2022 15:33:16 +0000 Subject: [PATCH 20/58] base_user_role_company 14.0.2.0.0 --- base_user_role_company/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_user_role_company/__manifest__.py b/base_user_role_company/__manifest__.py index 8e306b5c8..b5716f984 100644 --- a/base_user_role_company/__manifest__.py +++ b/base_user_role_company/__manifest__.py @@ -3,7 +3,7 @@ { "name": "User roles by company", - "version": "14.0.1.1.0", + "version": "14.0.2.0.0", "category": "Tools", "author": "Open Source Integrators, Odoo Community Association (OCA)", "license": "AGPL-3", From 2aa2b91ebb5060867fab6983fab1a6035581d4c0 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Thu, 3 Mar 2022 15:33:27 +0000 Subject: [PATCH 21/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-backend-14.0/server-backend-14.0-base_user_role_company Translate-URL: https://translation.odoo-community.org/projects/server-backend-14-0/server-backend-14-0-base_user_role_company/ --- base_user_role_company/i18n/it.po | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/base_user_role_company/i18n/it.po b/base_user_role_company/i18n/it.po index f28ab71a1..c587735df 100644 --- a/base_user_role_company/i18n/it.po +++ b/base_user_role_company/i18n/it.po @@ -17,9 +17,9 @@ msgstr "" "X-Generator: Weblate 4.3.2\n" #. module: base_user_role_company -#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__active_role -msgid "Active Role" -msgstr "Ruolo attivo" +#: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__allowed_company_ids +msgid "Companies" +msgstr "" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__company_id @@ -81,3 +81,6 @@ msgstr "Utenti" #: model:ir.model,name:base_user_role_company.model_res_users_role_line msgid "Users associated to a role" msgstr "Utenti associati al ruolo" + +#~ msgid "Active Role" +#~ msgstr "Ruolo attivo" From 20abc76f9355dbd4deb95fc47e5ae0295a70e00b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 3 Mar 2022 15:45:46 +0000 Subject: [PATCH 22/58] base_user_role_company 14.0.2.0.1 --- base_user_role_company/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_user_role_company/__manifest__.py b/base_user_role_company/__manifest__.py index b5716f984..d39f645a7 100644 --- a/base_user_role_company/__manifest__.py +++ b/base_user_role_company/__manifest__.py @@ -3,7 +3,7 @@ { "name": "User roles by company", - "version": "14.0.2.0.0", + "version": "14.0.2.0.1", "category": "Tools", "author": "Open Source Integrators, Odoo Community Association (OCA)", "license": "AGPL-3", From 51e54c060168135968bc69acdb15f2fe32a24c49 Mon Sep 17 00:00:00 2001 From: Francesco Foresti Date: Tue, 7 Mar 2023 17:07:28 +0000 Subject: [PATCH 23/58] Translated using Weblate (Italian) Currently translated at 90.9% (10 of 11 strings) Translation: server-backend-14.0/server-backend-14.0-base_user_role_company Translate-URL: https://translation.odoo-community.org/projects/server-backend-14-0/server-backend-14-0-base_user_role_company/it/ --- base_user_role_company/i18n/it.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base_user_role_company/i18n/it.po b/base_user_role_company/i18n/it.po index c587735df..90e9e0b20 100644 --- a/base_user_role_company/i18n/it.po +++ b/base_user_role_company/i18n/it.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 14.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2022-02-15 16:16+0000\n" -"Last-Translator: Riccardo Bellanova \n" +"PO-Revision-Date: 2023-03-07 19:24+0000\n" +"Last-Translator: Francesco Foresti \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.3.2\n" +"X-Generator: Weblate 4.14.1\n" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_res_users_role_line__allowed_company_ids @@ -36,7 +36,7 @@ msgstr "Nome visualizzato" #. module: base_user_role_company #: model:ir.model,name:base_user_role_company.model_ir_http msgid "HTTP Routing" -msgstr "Rotta HTTP" +msgstr "Instradamento HTTP" #. module: base_user_role_company #: model:ir.model.fields,field_description:base_user_role_company.field_ir_http__id From 581fb4662de11915c9ef0c6ffb7b90f56b0c32a0 Mon Sep 17 00:00:00 2001 From: Urvisha-OSI Date: Wed, 5 Apr 2023 12:43:55 +0530 Subject: [PATCH 24/58] [IMP] base_user_role_company: pre-commit stuff --- base_user_role_company/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_user_role_company/__manifest__.py b/base_user_role_company/__manifest__.py index d39f645a7..4b225993f 100644 --- a/base_user_role_company/__manifest__.py +++ b/base_user_role_company/__manifest__.py @@ -3,7 +3,7 @@ { "name": "User roles by company", - "version": "14.0.2.0.1", + "version": "16.0.1.0.0", "category": "Tools", "author": "Open Source Integrators, Odoo Community Association (OCA)", "license": "AGPL-3", From 81827d880bd02bb5aa202d115a4a93c0b8304f47 Mon Sep 17 00:00:00 2001 From: Urvisha-OSI Date: Wed, 5 Apr 2023 13:30:38 +0530 Subject: [PATCH 25/58] [MIG] base_user_role_company: Migration to 16.0 --- base_user_role_company/README.rst | 15 ++++++++------- base_user_role_company/controllers/main.py | 2 +- base_user_role_company/readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 9 +++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/base_user_role_company/README.rst b/base_user_role_company/README.rst index 5b7d12252..a75e13635 100644 --- a/base_user_role_company/README.rst +++ b/base_user_role_company/README.rst @@ -14,14 +14,14 @@ User roles by company :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--backend-lightgray.png?logo=github - :target: https://github.com/OCA/server-backend/tree/14.0/base_user_role_company + :target: https://github.com/OCA/server-backend/tree/16.0/base_user_role_company :alt: OCA/server-backend .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-backend-14-0/server-backend-14-0-base_user_role_company + :target: https://translation.odoo-community.org/projects/server-backend-16-0/server-backend-16-0-base_user_role_company :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/253/14.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/server-backend&target_branch=16.0 + :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -77,7 +77,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -96,6 +96,7 @@ Contributors * Daniel Reis * Chandresh Thakkar + * Urvisha Desai Maintainers ~~~~~~~~~~~ @@ -110,6 +111,6 @@ 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. -This module is part of the `OCA/server-backend `_ project on GitHub. +This module is part of the `OCA/server-backend `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_user_role_company/controllers/main.py b/base_user_role_company/controllers/main.py index 7df62870e..7e75d6954 100644 --- a/base_user_role_company/controllers/main.py +++ b/base_user_role_company/controllers/main.py @@ -3,7 +3,7 @@ from odoo import http -from odoo.addons.web.controllers.main import Home +from odoo.addons.web.controllers.home import Home class HomeExtended(Home): diff --git a/base_user_role_company/readme/CONTRIBUTORS.rst b/base_user_role_company/readme/CONTRIBUTORS.rst index c259741d2..3b275b35c 100644 --- a/base_user_role_company/readme/CONTRIBUTORS.rst +++ b/base_user_role_company/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Daniel Reis * Chandresh Thakkar + * Urvisha Desai diff --git a/base_user_role_company/static/description/index.html b/base_user_role_company/static/description/index.html index 9eb4a6fcc..4eef8c25c 100644 --- a/base_user_role_company/static/description/index.html +++ b/base_user_role_company/static/description/index.html @@ -3,7 +3,7 @@ - + User roles by company + + +
+

Caldav and Carddav support

+ + +

Beta License: AGPL-3 OCA/server-backend Translate me on Weblate Try me on Runbot

+

This module adds WebDAV support to Odoo, specifically CalDAV and CardDAV.

+

You can configure arbitrary objects as a calendar or an address book, thus make arbitrary information accessible in external systems or your mobile.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. go to Settings / WebDAV Collections and create or edit your collections. There, you’ll also see the URL to point your clients to.
  2. +
+

Note that you need to configure a dbfilter if you use multiple databases.

+
+
+

Known issues / Roadmap

+
    +
  • better UX for configuring collections
  • +
  • support writing
  • +
  • support address books
  • +
  • support todo lists and journals
  • +
  • support configuring default field mappings per model
  • +
  • support plain WebDAV collections to make some model’s records accessible as folders, and the records’ attachments as files (r/w)
  • +
  • support configuring lists of calendars so that you can have a calendar for every project and appointments are tasks, or a calendar for every sales team and appointments are sale orders. Lots of possibilities
  • +
+

Backporting this to <=v10 will be tricky because radicale only supports python3. Probably it will be quite a hassle to backport the relevant code, so it might be more sensible to just backport the configuration part, and implement the rest as radicale auth/storage plugin that talks to Odoo via odoorpc. It should be possible to recycle most of the code from this addon, which actually implements those plugins, but then within Odoo.

+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+
    +
  • Odoo Community Association: Icon
  • +
  • All the actual work is done by Radicale
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/server-backend project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_dav/tests/__init__.py b/base_dav/tests/__init__.py new file mode 100644 index 000000000..09e6f8403 --- /dev/null +++ b/base_dav/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import test_base_dav, test_collection diff --git a/base_dav/tests/test_base_dav.py b/base_dav/tests/test_base_dav.py new file mode 100644 index 000000000..b76cec111 --- /dev/null +++ b/base_dav/tests/test_base_dav.py @@ -0,0 +1,117 @@ +# Copyright 2018 Therp BV +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from base64 import b64encode +from unittest import mock +from urllib.parse import urlparse + +from odoo.tests.common import TransactionCase + +from ..controllers.main import PREFIX +from ..controllers.main import Main as Controller + +MODULE_PATH = "odoo.addons.base_dav" +CONTROLLER_PATH = MODULE_PATH + ".controllers.main" +RADICALE_PATH = MODULE_PATH + ".radicale" + +ADMIN_PASSWORD = "RadicalePa$$word" + + +@mock.patch(CONTROLLER_PATH + ".request") +@mock.patch(RADICALE_PATH + ".auth.request") +@mock.patch(RADICALE_PATH + ".collection.request") +class TestBaseDav(TransactionCase): + def setUp(self): + super().setUp() + + self.collection = self.env["dav.collection"].create({ + "name": "Test Collection", + "dav_type": "calendar", + "model_id": self.env.ref("base.model_res_users").id, + "domain": "[]", + }) + + self.dav_path = urlparse(self.collection.url).path.replace(PREFIX, '') + + self.controller = Controller() + self.env.user.password_crypt = ADMIN_PASSWORD + + self.test_user = self.env["res.users"].create({ + "login": "tester", + "name": "tester", + }) + + self.auth_owner = self.auth_string(self.env.user, ADMIN_PASSWORD) + self.auth_tester = self.auth_string(self.test_user, ADMIN_PASSWORD) + + patcher = mock.patch('odoo.http.request') + self.addCleanup(patcher.stop) + patcher.start() + + def auth_string(self, user, password): + return b64encode( + ("%s:%s" % (user.login, password)).encode() + ).decode() + + def init_mocks(self, coll_mock, auth_mock, req_mock): + req_mock.env = self.env + req_mock.httprequest.environ = { + "HTTP_AUTHORIZATION": "Basic %s" % self.auth_owner, + "REQUEST_METHOD": "PROPFIND", + "HTTP_X_SCRIPT_NAME": PREFIX, + } + + auth_mock.env["res.users"]._login.return_value = self.env.uid + coll_mock.env = self.env + + def check_status_code(self, response, forbidden): + if forbidden: + self.assertNotEqual(response.status_code, 403) + else: + self.assertEqual(response.status_code, 403) + + def check_access(self, environ, auth_string, read, write): + environ.update({ + "REQUEST_METHOD": "PROPFIND", + "HTTP_AUTHORIZATION": "Basic %s" % auth_string, + }) + response = self.controller.handle_dav_request(self.dav_path) + self.check_status_code(response, read) + + environ["REQUEST_METHOD"] = "PUT" + response = self.controller.handle_dav_request(self.dav_path) + self.check_status_code(response, write) + + def test_well_known(self, coll_mock, auth_mock, req_mock): + req_mock.env = self.env + + response = self.controller.handle_well_known_request() + self.assertEqual(response.status_code, 301) + + def test_authenticated(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "authenticated" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=True, write=True) + + def test_owner_only(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "owner_only" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=False, write=False) + + def test_owner_write_only(self, coll_mock, auth_mock, req_mock): + self.init_mocks(coll_mock, auth_mock, req_mock) + environ = req_mock.httprequest.environ + + self.collection.rights = "owner_write_only" + + self.check_access(environ, self.auth_owner, read=True, write=True) + self.check_access(environ, self.auth_tester, read=True, write=False) diff --git a/base_dav/tests/test_collection.py b/base_dav/tests/test_collection.py new file mode 100644 index 000000000..47e9cbfcb --- /dev/null +++ b/base_dav/tests/test_collection.py @@ -0,0 +1,118 @@ +# Copyright 2019-2020 initOS GmbH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime, timedelta +from unittest import mock + +from odoo.exceptions import MissingError +from odoo.tests.common import TransactionCase +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +from ..radicale.collection import Collection + + +class TestCalendar(TransactionCase): + def setUp(self): + super().setUp() + + self.collection = self.env["dav.collection"].create({ + "name": "Test Collection", + "dav_type": "calendar", + "model_id": self.env.ref("base.model_res_users").id, + "domain": "[]", + }) + + self.create_field_mapping( + "login", "base.field_res_users_login", + excode="result = record.login", + imcode="result = item.value", + ) + self.create_field_mapping( + "name", "base.field_res_users_name", + ) + self.create_field_mapping( + "dtstart", "base.field_res_users_create_date", + ) + self.create_field_mapping( + "dtend", "base.field_res_users_write_date", + ) + + start = datetime.now() + stop = start + timedelta(hours=1) + self.record = self.env["res.users"].create({ + "login": "tester", + "name": "Test User", + "create_date": start.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + "write_date": stop.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + }) + + def create_field_mapping(self, name, field_ref, imcode=None, excode=None): + return self.env["dav.collection.field_mapping"].create({ + "collection_id": self.collection.id, + "name": name, + "field_id": self.env.ref(field_ref).id, + "mapping_type": "code" if imcode or excode else "simple", + "import_code": imcode, + "export_code": excode, + }) + + def compare_record(self, vobj, rec=None): + tmp = self.collection.from_vobject(vobj) + + self.assertEqual((rec or self.record).login, tmp["login"]) + self.assertEqual((rec or self.record).name, tmp["name"]) + self.assertEqual((rec or self.record).create_date, tmp["create_date"]) + self.assertEqual((rec or self.record).write_date, tmp["write_date"]) + + def test_import_export(self): + # Exporting and importing should result in the same record + vobj = self.collection.to_vobject(self.record) + self.compare_record(vobj) + + def test_get_record(self): + rec = self.collection.get_record([self.record.id]) + self.assertEqual(rec, self.record) + + self.collection.field_uuid = self.env.ref( + "base.field_res_users_login", + ).id + rec = self.collection.get_record([self.record.login]) + self.assertEqual(rec, self.record) + + @mock.patch("odoo.addons.base_dav.radicale.collection.request") + def test_collection(self, request_mock): + request_mock.env = self.env + collection_url = "/%s/%s" % (self.env.user.login, self.collection.id) + collection = list(Collection.discover(collection_url))[0] + + # Try to get the test record + record_url = "%s/%s" % (collection_url, self.record.id) + self.assertIn(record_url, collection.list()) + + # Get the test record using the URL and compare it + item = collection.get(record_url) + self.compare_record(item.item) + self.assertEqual(item.href, record_url) + + # Get a non-existing record + self.assertFalse(collection.get(record_url + "0")) + + # Get the record and alter it later + item = self.collection.to_vobject(self.record) + self.record.login = "different" + with self.assertRaises(AssertionError): + self.compare_record(item) + + # Restore the record + item = collection.upload(record_url, item) + self.compare_record(item.item) + + # Delete an record + collection.delete(item.href) + with self.assertRaises(MissingError): + self.record.name + + # Create a new record + item = collection.upload(record_url + "0", item) + record = self.collection.get_record(collection._split_path(item.href)) + self.assertNotEqual(record, self.record) + self.compare_record(item.item, record) diff --git a/base_dav/views/dav_collection.xml b/base_dav/views/dav_collection.xml new file mode 100644 index 000000000..602b50748 --- /dev/null +++ b/base_dav/views/dav_collection.xml @@ -0,0 +1,78 @@ + + + + dav.collection + + + + + + + + + + + dav.collection + +
+ + + + + + + + + + + + + + + +
+
+
+ + + dav.collection.field_mapping + + + + + + + + + + dav.collection.field_mapping + +
+ + + + + + + + +
+
+
+ + + + +
From 54b7a7f4e1518c4f18681498604d3c652285d954 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Sun, 12 Jan 2020 21:39:32 +0100 Subject: [PATCH 48/58] Fix CI errors --- base_dav/radicale/collection.py | 4 ++++ base_dav/tests/test_collection.py | 4 +--- base_dav/views/dav_collection.xml | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/base_dav/radicale/collection.py b/base_dav/radicale/collection.py index 9a37c5bfb..5fd10cf5d 100644 --- a/base_dav/radicale/collection.py +++ b/base_dav/radicale/collection.py @@ -130,3 +130,7 @@ def delete(self, href): def list(self): return self.collection.dav_list(self.path_components) + + @classmethod + def create_collection(cls, href, collection=None, props=None): + return None diff --git a/base_dav/tests/test_collection.py b/base_dav/tests/test_collection.py index 47e9cbfcb..6839515a4 100644 --- a/base_dav/tests/test_collection.py +++ b/base_dav/tests/test_collection.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from unittest import mock -from odoo.exceptions import MissingError from odoo.tests.common import TransactionCase from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT @@ -108,8 +107,7 @@ def test_collection(self, request_mock): # Delete an record collection.delete(item.href) - with self.assertRaises(MissingError): - self.record.name + self.assertFalse(self.record.exists()) # Create a new record item = collection.upload(record_url + "0", item) diff --git a/base_dav/views/dav_collection.xml b/base_dav/views/dav_collection.xml index 602b50748..d0cbfb218 100644 --- a/base_dav/views/dav_collection.xml +++ b/base_dav/views/dav_collection.xml @@ -39,6 +39,7 @@ + @@ -52,9 +53,7 @@ - + From dd23c30c8c7a355104c8bcb6766464bcb8d65ebd Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Sun, 12 Jan 2020 22:16:13 +0100 Subject: [PATCH 49/58] Mute radicale logger for testing --- base_dav/radicale/collection.py | 4 ---- base_dav/tests/test_base_dav.py | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/base_dav/radicale/collection.py b/base_dav/radicale/collection.py index 5fd10cf5d..9a37c5bfb 100644 --- a/base_dav/radicale/collection.py +++ b/base_dav/radicale/collection.py @@ -130,7 +130,3 @@ def delete(self, href): def list(self): return self.collection.dav_list(self.path_components) - - @classmethod - def create_collection(cls, href, collection=None, props=None): - return None diff --git a/base_dav/tests/test_base_dav.py b/base_dav/tests/test_base_dav.py index b76cec111..6ed014f94 100644 --- a/base_dav/tests/test_base_dav.py +++ b/base_dav/tests/test_base_dav.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from odoo.tests.common import TransactionCase +from odoo.tools import mute_logger from ..controllers.main import PREFIX from ..controllers.main import Main as Controller @@ -18,6 +19,7 @@ ADMIN_PASSWORD = "RadicalePa$$word" +@mute_logger("radicale") @mock.patch(CONTROLLER_PATH + ".request") @mock.patch(RADICALE_PATH + ".auth.request") @mock.patch(RADICALE_PATH + ".collection.request") From e194c4a7fb257c5d0c5d60c22d7e6fae12a6a0da Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Thu, 16 Jan 2020 14:35:05 +0100 Subject: [PATCH 50/58] Use the collection as first argument in the creation of the items --- base_dav/models/dav_collection.py | 14 +++++++------- base_dav/radicale/collection.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/base_dav/models/dav_collection.py b/base_dav/models/dav_collection.py index 2e0ee1aa2..c98e53202 100644 --- a/base_dav/models/dav_collection.py +++ b/base_dav/models/dav_collection.py @@ -172,7 +172,7 @@ def _split_path(self, path): )) @api.multi - def dav_list(self, path_components): + def dav_list(self, collection, path_components): self.ensure_one() if self.dav_type == 'files': @@ -215,7 +215,7 @@ def dav_list(self, path_components): return result @api.multi - def dav_delete(self, components): + def dav_delete(self, collection, components): self.ensure_one() if self.dav_type == "files": @@ -225,7 +225,7 @@ def dav_delete(self, components): self.get_record(components).unlink() @api.multi - def dav_upload(self, href, item): + def dav_upload(self, collection, href, item): self.ensure_one() components = self._split_path(href) @@ -248,14 +248,14 @@ def dav_upload(self, href, item): record.write(data) return Item( - self, + collection, item=self.to_vobject(record), href=href, last_modified=self._odoo_to_http_datetime(record.write_date), ) @api.multi - def dav_get(self, href): + def dav_get(self, collection, href): self.ensure_one() components = self._split_path(href) @@ -279,7 +279,7 @@ def dav_get(self, href): ('name', '=', components[3]), ], limit=1) return FileItem( - self, + collection, item=attachment, href=href, last_modified=self._odoo_to_http_datetime( @@ -293,7 +293,7 @@ def dav_get(self, href): return None return Item( - self, + collection, item=self.to_vobject(record), href=href, last_modified=self._odoo_to_http_datetime(record.write_date), diff --git a/base_dav/radicale/collection.py b/base_dav/radicale/collection.py index 9a37c5bfb..0fe39abe8 100644 --- a/base_dav/radicale/collection.py +++ b/base_dav/radicale/collection.py @@ -120,13 +120,13 @@ def get_meta(self, key=None): self.logger.warning('unsupported metadata %s', key) def get(self, href): - return self.collection.dav_get(href) + return self.collection.dav_get(self, href) def upload(self, href, vobject_item): - return self.collection.dav_upload(href, vobject_item) + return self.collection.dav_upload(self, href, vobject_item) def delete(self, href): - return self.collection.dav_delete(self._split_path(href)) + return self.collection.dav_delete(self, self._split_path(href)) def list(self): - return self.collection.dav_list(self.path_components) + return self.collection.dav_list(self, self.path_components) From f1a711cefce0065963338a06a8acd1a30fd63076 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Mon, 13 Jul 2020 08:52:44 +0200 Subject: [PATCH 51/58] Use related model_id field to build domain --- base_dav/models/dav_collection.py | 3 ++- base_dav/models/dav_collection_field_mapping.py | 7 ++++++- base_dav/views/dav_collection.xml | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/base_dav/models/dav_collection.py b/base_dav/models/dav_collection.py index c98e53202..f7a4d96d9 100644 --- a/base_dav/models/dav_collection.py +++ b/base_dav/models/dav_collection.py @@ -7,9 +7,10 @@ from operator import itemgetter from urllib.parse import quote_plus -import vobject from odoo import api, fields, models, tools +import vobject + # pylint: disable=missing-import-error from ..controllers.main import PREFIX from ..radicale.collection import Collection, FileItem, Item diff --git a/base_dav/models/dav_collection_field_mapping.py b/base_dav/models/dav_collection_field_mapping.py index 8ef7e3047..9f90715e3 100644 --- a/base_dav/models/dav_collection_field_mapping.py +++ b/base_dav/models/dav_collection_field_mapping.py @@ -4,10 +4,11 @@ import datetime +from odoo import api, fields, models, tools + import dateutil import vobject from dateutil import tz -from odoo import api, fields, models, tools class DavCollectionFieldMapping(models.Model): @@ -34,6 +35,10 @@ class DavCollectionFieldMapping(models.Model): required=True, help="Field of the model the values are mapped to", ) + model_id = fields.Many2one( + 'ir.model', + related='collection_id.model_id', + ) import_code = fields.Text( help="Code to import the value from a vobject. Use the variable " "result for the output of the value and item as input" diff --git a/base_dav/views/dav_collection.xml b/base_dav/views/dav_collection.xml index d0cbfb218..dcea34147 100644 --- a/base_dav/views/dav_collection.xml +++ b/base_dav/views/dav_collection.xml @@ -51,9 +51,10 @@
+ - + @@ -73,5 +74,4 @@ action="action_dav_collection" sequence="100" /> - From f9b597319b5b7d319259c3941d029c7c0009e712 Mon Sep 17 00:00:00 2001 From: fkantelberg <39951254+fkantelberg@users.noreply.github.com> Date: Wed, 29 Jul 2020 11:54:36 +0200 Subject: [PATCH 52/58] Set default id for fields Co-authored-by: Yannick Vaucher --- base_dav/views/dav_collection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_dav/views/dav_collection.xml b/base_dav/views/dav_collection.xml index dcea34147..94d56f341 100644 --- a/base_dav/views/dav_collection.xml +++ b/base_dav/views/dav_collection.xml @@ -28,7 +28,7 @@ - + - From b5b9e2060f2f003f7b0589726ab9dc86e35a6082 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 11 Feb 2021 10:52:18 +0000 Subject: [PATCH 54/58] [UPD] Update base_dav.pot --- base_dav/i18n/base_dav.pot | 215 +++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 base_dav/i18n/base_dav.pot diff --git a/base_dav/i18n/base_dav.pot b/base_dav/i18n/base_dav.pot new file mode 100644 index 000000000..2e0939ca5 --- /dev/null +++ b/base_dav/i18n/base_dav.pot @@ -0,0 +1,215 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_dav +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \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: base_dav +#: model:ir.model,name:base_dav.model_dav_collection +msgid "A collection accessible via WebDAV" +msgstr "" + +#. module: base_dav +#: model:ir.model,name:base_dav.model_dav_collection_field_mapping +msgid "A field mapping for a WebDAV collection" +msgstr "" + +#. module: base_dav +#: model:ir.ui.view,arch_db:base_dav.view_dav_collection_form +msgid "Access" +msgstr "" + +#. module: base_dav +#: model:ir.ui.view,arch_db:base_dav.view_dav_collection_form +msgid "Additional field mapping" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Addressbook" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_name +msgid "Attribute name in the vobject" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Authenticated" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Calendar" +msgstr "" + +#. module: base_dav +#: selection:dav.collection.field_mapping,mapping_type:0 +msgid "Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_export_code +msgid "Code to export the value to a vobject. Use the variable result for the output of the value and record as input" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_import_code +msgid "Code to import the value from a vobject. Use the variable result for the output of the value and item as input" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_collection_id +msgid "Collection" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_create_uid +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_create_uid +msgid "Created by" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_create_date +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_create_date +msgid "Created on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_display_name +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_display_name +msgid "Display Name" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_domain +msgid "Domain" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_export_code +msgid "Export Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_field_id +msgid "Field" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_uuid +msgid "Field Uuid" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_ids +msgid "Field mappings" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,help:base_dav.field_dav_collection_field_mapping_field_id +msgid "Field of the model the values are mapped to" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,dav_type:0 +msgid "Files" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_id +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_id +msgid "ID" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_import_code +msgid "Import Code" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection___last_update +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping___last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_write_uid +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_write_date +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_mapping_type +msgid "Mapping Type" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_model_id +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_model_id +msgid "Model" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_field_mapping_name +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_name +msgid "Name" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Owner Only" +msgstr "" + +#. module: base_dav +#: selection:dav.collection,rights:0 +msgid "Owner Write Only" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_rights +msgid "Rights" +msgstr "" + +#. module: base_dav +#: selection:dav.collection.field_mapping,mapping_type:0 +msgid "Simple" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_tag +msgid "Tag" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_dav_type +msgid "Type" +msgstr "" + +#. module: base_dav +#: model:ir.model.fields,field_description:base_dav.field_dav_collection_url +msgid "Url" +msgstr "" + +#. module: base_dav +#: model:ir.actions.act_window,name:base_dav.action_dav_collection +#: model:ir.ui.menu,name:base_dav.menu_dav_collection +msgid "WebDAV collections" +msgstr "" + From 8b51c3ef68695ac85a4d84955c209aafa9761510 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 11 Feb 2021 11:24:31 +0000 Subject: [PATCH 55/58] [UPD] README.rst --- base_dav/README.rst | 6 ++--- base_dav/static/description/index.html | 36 +++++++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/base_dav/README.rst b/base_dav/README.rst index 8270a8937..130b62c77 100644 --- a/base_dav/README.rst +++ b/base_dav/README.rst @@ -46,9 +46,7 @@ Note that you need to configure a dbfilter if you use multiple databases. Known issues / Roadmap ====================== -* better UX for configuring collections -* support writing -* support address books +* much better UX for configuring collections (probably provide a group that sees the current fully flexible field mappings, and by default show some dumbed down version where you can select some preselected vobject fields) * support todo lists and journals * support configuring default field mappings per model * support plain WebDAV collections to make some model's records accessible as folders, and the records' attachments as files (r/w) @@ -72,12 +70,14 @@ Credits Authors ~~~~~~~ +* initOS GmbH * Therp BV Contributors ~~~~~~~~~~~~ * Holger Brunn +* Florian Kantelberg Other credits ~~~~~~~~~~~~~ diff --git a/base_dav/static/description/index.html b/base_dav/static/description/index.html index 3c659e7f0..b2966d4fb 100644 --- a/base_dav/static/description/index.html +++ b/base_dav/static/description/index.html @@ -3,13 +3,13 @@ - + Caldav and Carddav support