From 9d4328dd9d12724364232902f13d849e51f71fe4 Mon Sep 17 00:00:00 2001 From: kobros-tech Date: Wed, 15 Jan 2025 00:34:44 +0300 Subject: [PATCH] [18.0][MIG] dms: Migration to 18.0 --- dms/README.rst | 14 +- dms/__manifest__.py | 2 +- dms/data/onboarding_data.xml | 1 - dms/demo/category.xml | 1 + dms/models/access_groups.py | 8 +- dms/models/directory.py | 20 +- dms/models/dms_category.py | 4 +- dms/models/dms_file.py | 10 +- dms/models/dms_security_mixin.py | 86 ++-- dms/models/ir_binary.py | 4 +- dms/models/mixins_thumbnail.py | 2 +- dms/models/storage.py | 6 +- dms/readme/CONTRIBUTORS.md | 2 + dms/security/security.xml | 16 +- dms/static/description/index.html | 10 +- .../src/js/fields/path_json/path_owl.esm.js | 3 +- .../preview_binary/preview_record.esm.js | 4 +- .../src/js/views/dms_file_upload.esm.js | 1 + dms/static/src/js/views/search_panel.esm.js | 5 +- dms/static/tests/tours/dms_portal_tour.esm.js | 23 +- dms/template/portal.xml | 4 +- dms/tests/common.py | 19 +- dms/tests/test_benchmark.py | 20 +- dms/tests/test_directory.py | 23 +- dms/tests/test_file.py | 34 +- dms/tests/test_file_database.py | 2 +- dms/tests/test_portal.py | 26 +- dms/tests/test_storage_attachment.py | 3 +- dms/views/dms_access_groups_views.xml | 26 +- dms/views/dms_category.xml | 10 +- dms/views/dms_directory.xml | 54 ++- dms/views/dms_file.xml | 36 +- dms/views/dms_tag.xml | 18 +- dms/views/menu.xml | 35 +- dms/views/storage.xml | 382 +++++++++--------- dms/wizards/wizard_dms_file_move_views.xml | 2 +- 36 files changed, 483 insertions(+), 433 deletions(-) diff --git a/dms/README.rst b/dms/README.rst index 94e0a74c0..5ddf87897 100644 --- a/dms/README.rst +++ b/dms/README.rst @@ -17,13 +17,13 @@ Document Management System :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github - :target: https://github.com/OCA/dms/tree/17.0/dms + :target: https://github.com/OCA/dms/tree/18.0/dms :alt: OCA/dms .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/dms-17-0/dms-17-0-dms + :target: https://translation.odoo-community.org/projects/dms-18-0/dms-18-0-dms :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/dms&target_branch=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -180,7 +180,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 to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -216,6 +216,10 @@ Contributors - Timothée Vannier +- `Kencove `__: + + - Mohamed Alkobrosli + Other credits ------------- @@ -240,6 +244,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/dms `_ project on GitHub. +This module is part of the `OCA/dms `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/dms/__manifest__.py b/dms/__manifest__.py index 78a460fa5..174316d1a 100644 --- a/dms/__manifest__.py +++ b/dms/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Document Management System", "summary": """Document Management System for Odoo""", - "version": "17.0.1.2.1", + "version": "18.0.1.0.0", "category": "Document Management", "license": "LGPL-3", "website": "https://github.com/OCA/dms", diff --git a/dms/data/onboarding_data.xml b/dms/data/onboarding_data.xml index 46fce1930..9533f6ad7 100644 --- a/dms/data/onboarding_data.xml +++ b/dms/data/onboarding_data.xml @@ -4,7 +4,6 @@ License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> - Storage diff --git a/dms/demo/category.xml b/dms/demo/category.xml index b6d168115..1c0642126 100644 --- a/dms/demo/category.xml +++ b/dms/demo/category.xml @@ -8,6 +8,7 @@ Internal + Human Resource diff --git a/dms/models/access_groups.py b/dms/models/access_groups.py index 28b285ca1..e0476008e 100644 --- a/dms/models/access_groups.py +++ b/dms/models/access_groups.py @@ -14,7 +14,7 @@ class DmsAccessGroups(models.Model): _parent_name = "parent_group_id" name = fields.Char(string="Group Name", required=True, translate=True) - parent_path = fields.Char(index="btree", unaccent=False) + parent_path = fields.Char(index="btree") # Permissions written directly on this group perm_create = fields.Boolean(string="Create Access") @@ -122,9 +122,9 @@ def _compute_inclusive_permissions(self): for one in self: one.update( { - "perm_inclusive_%s" % perm: ( - one["perm_%s" % perm] - or one.parent_group_id["perm_inclusive_%s" % perm] + f"perm_inclusive_{perm}": ( + one[f"perm_{perm}"] + or one.parent_group_id[f"perm_inclusive_{perm}"] ) for perm in ("create", "unlink", "write") } diff --git a/dms/models/directory.py b/dms/models/directory.py index 9d79d004f..6a31d8340 100644 --- a/dms/models/directory.py +++ b/dms/models/directory.py @@ -17,8 +17,6 @@ from odoo.osv.expression import AND, OR from odoo.tools import consteq, human_size -from odoo.addons.http_routing.models.ir_http import slugify - from ..tools.file import check_name, unique_name _logger = logging.getLogger(__name__) @@ -46,7 +44,7 @@ class DmsDirectory(models.Model): _parent_name = "parent_id" _directory_field = _parent_name - parent_path = fields.Char(index="btree", unaccent=False) + parent_path = fields.Char(index="btree") is_root_directory = fields.Boolean( default=False, help="""Indicates if the directory is a root directory. @@ -216,7 +214,7 @@ def _get_domain_by_access_groups(self, operation): """Special rules for directories.""" self_filter = [ ("storage_id_inherit_access_from_parent_record", "=", False), - ("id", "inselect", self._get_access_groups_query(operation)), + ("id", "in", self._get_access_groups_query(operation)), ] # Upstream only filters by parent directory result = super()._get_domain_by_access_groups(operation) @@ -237,7 +235,7 @@ def _get_domain_by_access_groups(self, operation): def _compute_access_url(self): res = super()._compute_access_url() for item in self: - item.access_url = "/my/dms/directory/%s" % (item.id) + item.access_url = f"/my/dms/directory/{item.id}" return res def check_access_token(self, access_token=False): @@ -278,7 +276,7 @@ def _get_parent_categories(self, access_token): and consteq(current_directory.access_token, access_token) ) or not access_token - and current_directory.check_access_rights("read") + and current_directory.check_access("read") ): return directories current_directory = current_directory.parent_id @@ -389,9 +387,8 @@ def _search_starred(self, operator, operand): def _compute_complete_name(self): for category in self: if category.parent_id: - category.complete_name = "{} / {}".format( - category.parent_id.complete_name, - category.name, + category.complete_name = ( + f"{category.parent_id.complete_name} / {category.name}" ) else: category.complete_name = category.name @@ -530,7 +527,7 @@ def _onchange_model_id(self): # Constrains @api.constrains("parent_id") def _check_directory_recursion(self): - if not self._check_recursion(): + if self._has_cycle(): raise ValidationError(_("Error! You cannot create recursive directories.")) return True @@ -628,7 +625,8 @@ def message_new(self, msg_dict, custom_values=None): parent_directory._process_message(msg_dict) return parent_directory names = parent_directory.child_directory_ids.mapped("name") - subject = slugify(msg_dict.get("subject", _("Alias-Mail-Extraction"))) + slug = self.env["ir.http"]._slug + subject = slug(msg_dict.get("subject", _("Alias-Mail-Extraction"))) defaults = dict( {"name": unique_name(subject, names, escape_suffix=True)}, **custom_values ) diff --git a/dms/models/dms_category.py b/dms/models/dms_category.py index d291dec90..5900f4d93 100644 --- a/dms/models/dms_category.py +++ b/dms/models/dms_category.py @@ -40,7 +40,7 @@ class DMSCategory(models.Model): comodel_name="dms.category", inverse_name="parent_id", ) - parent_path = fields.Char(index="btree", unaccent=False) + parent_path = fields.Char(index="btree") tag_ids = fields.One2many( string="Tags", comodel_name="dms.tag", inverse_name="category_id" ) @@ -99,6 +99,6 @@ def _compute_count_files(self): @api.constrains("parent_id") def _check_category_recursion(self): - if not self._check_recursion(): + if self._has_cycle(): raise ValidationError(_("Error! You cannot create recursive categories.")) return True diff --git a/dms/models/dms_file.py b/dms/models/dms_file.py index 21b698d72..2b419e452 100644 --- a/dms/models/dms_file.py +++ b/dms/models/dms_file.py @@ -150,14 +150,14 @@ def _compute_image_1920(self): ): one.image_1920 = one.content - def check_access_rule(self, operation): - self.mapped("directory_id").check_access_rule(operation) - return super().check_access_rule(operation) + def check_access(self, operation): + self.mapped("directory_id").check_access(operation) + return super().check_access(operation) def _compute_access_url(self): res = super()._compute_access_url() for item in self: - item.access_url = "/my/dms/file/%s/download" % (item.id) + item.access_url = f"/my/dms/file/{item.id}/download" return res def check_access_token(self, access_token=False): @@ -240,7 +240,7 @@ def _get_forbidden_extensions(self): return [extension.strip() for extension in extensions.split(",")] def _get_icon_placeholder_name(self): - return self.extension and "file_%s.svg" % self.extension or "" + return self.extension and f"file_{self.extension}.svg" or "" # Actions def action_migrate(self, should_logging=True): diff --git a/dms/models/dms_security_mixin.py b/dms/models/dms_security_mixin.py index 3766708ff..91e6674ee 100644 --- a/dms/models/dms_security_mixin.py +++ b/dms/models/dms_security_mixin.py @@ -13,6 +13,7 @@ OR, TRUE_DOMAIN, ) +from odoo.tools import SQL _logger = getLogger(__name__) @@ -86,10 +87,10 @@ def _compute_permissions(self): ) return - creatable = self._filter_access_rules("create") - readable = self._filter_access_rules("read") - unlinkable = self._filter_access_rules("unlink") - writeable = self._filter_access_rules("write") + creatable = self._filtered_access("create") + readable = self._filtered_access("read") + unlinkable = self._filtered_access("unlink") + writeable = self._filtered_access("write") for one in self: one.update( { @@ -135,7 +136,7 @@ def _get_domain_by_inheritance(self, operation): ) continue # Check model access only once per batch - if not model.check_access_rights(operation, raise_exception=False): + if not model.check_access(operation): continue domains.append([("res_model", "=", model._name), ("res_id", "=", False)]) # Check record access in batch too @@ -143,7 +144,7 @@ def _get_domain_by_inheritance(self, operation): # Apply exists to skip records that do not exist. (e.g. a res.partner # deleted by database). model_records = model.browse(res_ids).exists() - related_ok = model_records._filter_access_rules_python(operation) + related_ok = model_records._filtered_access(operation) if not related_ok: continue domains.append( @@ -161,33 +162,53 @@ def _get_access_groups_query(self, operation): "unlink": "AND dag.perm_inclusive_unlink", "write": "AND dag.perm_inclusive_write", }[operation] - select = f""" - SELECT - dir_group_rel.aid - FROM - dms_directory_complete_groups_rel AS dir_group_rel - INNER JOIN dms_access_group AS dag - ON dir_group_rel.gid = dag.id - INNER JOIN dms_access_group_users_rel AS users - ON users.gid = dag.id - WHERE - users.uid = %s {operation_check} - """ - return select, (self.env.uid,) + if operation == "read": + sql = SQL( + """( + SELECT + dir_group_rel.aid + FROM + dms_directory_complete_groups_rel AS dir_group_rel + INNER JOIN dms_access_group AS dag + ON dir_group_rel.gid = dag.id + INNER JOIN dms_access_group_users_rel AS users + ON users.gid = dag.id + WHERE + users.uid = %s + )""", + self.env.uid, + ) + else: + sql = SQL( + """( + SELECT + dir_group_rel.aid + FROM + dms_directory_complete_groups_rel AS dir_group_rel + INNER JOIN dms_access_group AS dag + ON dir_group_rel.gid = dag.id + INNER JOIN dms_access_group_users_rel AS users + ON users.gid = dag.id + WHERE + users.uid = %s %s + )""", + self.env.uid, + operation_check, + ) + return sql @api.model def _get_domain_by_access_groups(self, operation): """Get domain for records accessible applying DMS access groups.""" result = [ ( - "%s.storage_id_inherit_access_from_parent_record" - % self._directory_field, + f"{self._directory_field}.storage_id_inherit_access_from_parent_record", "=", False, ), ( self._directory_field, - "inselect", + "in", self._get_access_groups_query(operation), ), ] @@ -236,16 +257,26 @@ def _search_permission_unlink(self, operator, value): def _search_permission_write(self, operator, value): return self._get_permission_domain(operator, value, "write") - def _filter_access_rules_python(self, operation): + def _filtered_access_no_recursion(self, operation: str): + """This method is just the same as _filtered_access + but it can not be called withoud super due to + recursion error. + + """ + if self and not self.env.su and (result := self._check_access(operation)): + return self - result[0] + return self + + def _filtered_access(self, operation): # Only kept to not break inheritance; see next comment - result = super()._filter_access_rules_python(operation) + result = super()._filtered_access(operation) # HACK Always fall back to applying rules by SQL. - # Upstream `_filter_access_rules_python()` doesn't use computed fields + # Upstream `_filtered_access()` doesn't use computed fields # search methods. Thus, it will take the `[('permission_{operation}', # '=', user.id)]` rule literally. Obviously that will always fail # because `self[f"permission_{operation}"]` will always be a `bool`, # while `user.id` will always be an `int`. - result |= self._filter_access_rules(operation) + result |= self._filtered_access_no_recursion(operation) return result @api.model_create_multi @@ -258,6 +289,5 @@ def create(self, vals_list): res.flush_recordset() # Go back to the original sudo state and check we really had creation permission res = res.sudo(self.env.su) - res.check_access_rights("create") - res.check_access_rule("create") + res.check_access("create") return res diff --git a/dms/models/ir_binary.py b/dms/models/ir_binary.py index 605a23235..79ebcb48a 100644 --- a/dms/models/ir_binary.py +++ b/dms/models/ir_binary.py @@ -8,7 +8,7 @@ class IrBinary(models.AbstractModel): _inherit = "ir.binary" - def _find_record_check_access(self, record, access_token): + def _find_record_check_access(self, record, access_token, field): if record._name in ("dms.file", "dms.directory"): if record.sudo().check_access_token(access_token): # sudo because the user might not usually have access to the record but @@ -16,4 +16,4 @@ def _find_record_check_access(self, record, access_token): # Used to display the icon in the portal. return record.sudo() - return super()._find_record_check_access(record, access_token) + return super()._find_record_check_access(record, access_token, field) diff --git a/dms/models/mixins_thumbnail.py b/dms/models/mixins_thumbnail.py index 895bac4b4..ebaed83d6 100644 --- a/dms/models/mixins_thumbnail.py +++ b/dms/models/mixins_thumbnail.py @@ -35,7 +35,7 @@ def _get_icon_url(self): """Obtain URL to record icon.""" local_path = self._get_icon_disk_path() icon_name = os.path.basename(local_path) - return "/dms/static/icons/%s" % icon_name + return f"/dms/static/icons/{icon_name}" @api.depends("image_128") def _compute_icon_url(self): diff --git a/dms/models/storage.py b/dms/models/storage.py index db5d261e3..cbc74cf01 100644 --- a/dms/models/storage.py +++ b/dms/models/storage.py @@ -18,9 +18,9 @@ class Storage(models.Model): name = fields.Char(required=True) save_type = fields.Selection( selection=[ - ("database", _("Database")), - ("file", _("Filestore")), - ("attachment", _("Attachment")), + ("database", "Database"), + ("file", "Filestore"), + ("attachment", "Attachment"), ], default="database", required=True, diff --git a/dms/readme/CONTRIBUTORS.md b/dms/readme/CONTRIBUTORS.md index 050b564a7..a319c249a 100644 --- a/dms/readme/CONTRIBUTORS.md +++ b/dms/readme/CONTRIBUTORS.md @@ -12,3 +12,5 @@ - Khanh Bui \<\> - [Subteno](https://www.subteno.com): - Timothée Vannier <> +- [Kencove](https://www.kencove.com): + - Mohamed Alkobrosli <> diff --git a/dms/security/security.xml b/dms/security/security.xml index 463f914c3..01e82a29a 100644 --- a/dms/security/security.xml +++ b/dms/security/security.xml @@ -116,7 +116,7 @@ - [('permission_create', '=', user.id)] + [('permission_create', '=', True)] Apply computed read permissions. @@ -126,7 +126,7 @@ - [('permission_read', '=', user.id)] + [('permission_read', '=', True)] Apply computed unlink permissions. @@ -136,7 +136,7 @@ - [('permission_unlink', '=', user.id)] + [('permission_unlink', '=', True)] Apply computed write permissions. @@ -146,7 +146,7 @@ - [('permission_write', '=', user.id)] + [('permission_write', '=', True)] Apply computed create permissions. @@ -156,7 +156,7 @@ - [('permission_create', '=', user.id)] + [('permission_create', '=', True)] Apply computed read permissions. @@ -166,7 +166,7 @@ - [('permission_read', '=', user.id)] + [('permission_read', '=', True)] Apply computed unlink permissions. @@ -176,7 +176,7 @@ - [('permission_unlink', '=', user.id)] + [('permission_unlink', '=', True)] Apply computed write permissions. @@ -186,6 +186,6 @@ - [('permission_write', '=', user.id)] + [('permission_write', '=', True)] diff --git a/dms/static/description/index.html b/dms/static/description/index.html index 01f44b78a..7abec759e 100644 --- a/dms/static/description/index.html +++ b/dms/static/description/index.html @@ -369,7 +369,7 @@

Document Management System

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:966c4331ff7c75b1ea8cb1d065c878d81250957cd305a5d6422def133e2a7d63 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: LGPL-3 OCA/dms Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/dms Translate me on Weblate Try me on Runboat

DMS is a module for creating, managing and viewing document files directly within Odoo. This module is only the basis for an entire ecosystem of apps that extend and seamlessly integrate with the document @@ -544,7 +544,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 to smash it by providing a detailed and welcomed -feedback.

+feedback.

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

@@ -579,6 +579,10 @@

Contributors

  • Timothée Vannier <tva@subteno.com>
  • +
  • Kencove: +
  • @@ -601,7 +605,7 @@

    Maintainers

    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/dms project on GitHub.

    +

    This module is part of the OCA/dms project on GitHub.

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

    diff --git a/dms/static/src/js/fields/path_json/path_owl.esm.js b/dms/static/src/js/fields/path_json/path_owl.esm.js index d11960e63..199ae9802 100644 --- a/dms/static/src/js/fields/path_json/path_owl.esm.js +++ b/dms/static/src/js/fields/path_json/path_owl.esm.js @@ -6,6 +6,7 @@ // **********************************************************************************/ import {Component, onWillUpdateProps} from "@odoo/owl"; import {registry} from "@web/core/registry"; +import {_t} from "@web/core/l10n/translation"; import {standardFieldProps} from "@web/views/fields/standard_field_props"; import {useService} from "@web/core/utils/hooks"; @@ -42,7 +43,7 @@ DmsPathField.props = { const dmsPathField = { component: DmsPathField, - display_name: "Dms Path Field", + displayName: _t("Dms Path Field"), supportedTypes: ["text"], extractProps: () => { return {}; diff --git a/dms/static/src/js/fields/preview_binary/preview_record.esm.js b/dms/static/src/js/fields/preview_binary/preview_record.esm.js index fc3ee1aa4..a476c1857 100644 --- a/dms/static/src/js/fields/preview_binary/preview_record.esm.js +++ b/dms/static/src/js/fields/preview_binary/preview_record.esm.js @@ -9,6 +9,7 @@ import {registry} from "@web/core/registry"; import {standardFieldProps} from "@web/views/fields/standard_field_props"; import {useFileViewer} from "@web/core/file_viewer/file_viewer_hook"; import {useService} from "@web/core/utils/hooks"; +import {_t} from "@web/core/l10n/translation"; export class PreviewRecordField extends BinaryField { setup() { @@ -37,8 +38,7 @@ PreviewRecordField.props = { const previewRecordField = { component: PreviewRecordField, - dependencies: [BinaryField], - display_name: "Preview Record", + displayName: _t("Preview Record"), supportedTypes: ["binary"], extractProps: () => { return {}; diff --git a/dms/static/src/js/views/dms_file_upload.esm.js b/dms/static/src/js/views/dms_file_upload.esm.js index a29988d5d..c5694f134 100644 --- a/dms/static/src/js/views/dms_file_upload.esm.js +++ b/dms/static/src/js/views/dms_file_upload.esm.js @@ -1,4 +1,5 @@ /** @odoo-module */ +/* global document */ // /** ******************************************************************************** // Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com). diff --git a/dms/static/src/js/views/search_panel.esm.js b/dms/static/src/js/views/search_panel.esm.js index f4c4482a3..67ef10604 100644 --- a/dms/static/src/js/views/search_panel.esm.js +++ b/dms/static/src/js/views/search_panel.esm.js @@ -4,9 +4,8 @@ * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ import {SearchModel} from "@web/search/search_model"; -import {registry} from "@web/core/registry"; -class DMSSearchPanel extends SearchModel { +export class DMSSearchPanel extends SearchModel { _getCategoryDomain(excludedCategoryId) { const domain = super._getCategoryDomain(...arguments); for (const category of this.categories) { @@ -24,5 +23,3 @@ class DMSSearchPanel extends SearchModel { return domain; } } - -registry.category("views").add("dms_search_panel", DMSSearchPanel); diff --git a/dms/static/tests/tours/dms_portal_tour.esm.js b/dms/static/tests/tours/dms_portal_tour.esm.js index 105f83c83..3411ca782 100644 --- a/dms/static/tests/tours/dms_portal_tour.esm.js +++ b/dms/static/tests/tours/dms_portal_tour.esm.js @@ -11,13 +11,10 @@ registry.category("web_tour.tours").add("dms_portal_mail_tour", { test: true, steps: () => [ { - content: "Go to Mails directory", - extra_trigger: "li.breadcrumb-item:contains('Documents')", trigger: ".tr_dms_directory_link:contains('Mails')", + run: "click", }, { - content: "Go to Mail_01.eml", - extra_trigger: "li.breadcrumb-item:contains('Mails')", trigger: ".tr_dms_file_link:contains('Mail_01.eml')", }, ], @@ -28,19 +25,19 @@ registry.category("web_tour.tours").add("dms_portal_partners_tour", { test: true, steps: () => [ { - content: "Go to Partners directory", - extra_trigger: "li.breadcrumb-item:contains('Documents')", - trigger: ".tr_dms_directory_link:contains('Partners')", + trigger: ".tr_dms_directory_link:contains('Media')", + run: "click", }, { - content: "Go to Joel Willis", - extra_trigger: "li.breadcrumb-item:contains('Partners')", - trigger: ".tr_dms_directory_link:contains('Joel Willis')", + trigger: ".tr_dms_directory_link:contains('Photos')", + run: "click", }, { - content: "Go to test.txt", - extra_trigger: "li.breadcrumb-item:contains('Joel Willis')", - trigger: ".tr_dms_file_link:contains('test.txt')", + trigger: ".tr_dms_directory_link:contains('2017')", + run: "click", + }, + { + trigger: ".tr_dms_file_link:contains('Sydney.jpg')", }, ], }); diff --git a/dms/template/portal.xml b/dms/template/portal.xml index aa43547bb..4a222a0dd 100644 --- a/dms/template/portal.xml +++ b/dms/template/portal.xml @@ -80,7 +80,7 @@ @@ -115,7 +115,7 @@ class="o_portal_contact_img" t-att-src="dms_file.icon_url + (('&access_token=' + access_token) if access_token else '')" /> - + diff --git a/dms/tests/common.py b/dms/tests/common.py index 29c782a14..d09a5a8e9 100644 --- a/dms/tests/common.py +++ b/dms/tests/common.py @@ -32,21 +32,20 @@ def wrapper(*args, **kwargs): threading.current_thread().query_count = 0 threading.current_thread().perf_t0 = time.time() result = func(*args, **kwargs) - message = "%s" % func.__name__ + message = f"{func.__name__}" if args and hasattr(args[0], "uid"): - message = " (%s)" % args[0].uid + message = f" ({args[0].uid})" if hasattr(threading.current_thread(), "query_count"): query_count = threading.current_thread().query_count query_time = threading.current_thread().query_time perf_t0 = threading.current_thread().perf_t0 remaining_time = time.time() - perf_t0 - query_time time_taken = query_time + remaining_time - message += " - {} Q {:.3f}s QT {:.3f}s OT {:.3f}s TT".format( - query_count, - query_time, - remaining_time, - time_taken, + message += ( + f" - {query_count} Q {query_time:.3f}s" + f"QT {remaining_time:.3f}s OT {time_taken:.3f}s TT" ) + tracking_parameters += [ query_count, query_time, @@ -54,13 +53,13 @@ def wrapper(*args, **kwargs): time_taken, ] if max_query_count and query_count > max_query_count: - raise AssertionError("More than %s queries" % max_query_count) + raise AssertionError(f"More than {max_query_count} queries") if max_query_time and query_time > max_query_time: raise AssertionError( - "Queries took longer than %.3fs" % max_query_time + f"Queries took longer than {max_query_time:.3f}s" ) if max_time and time_taken > max_time: - raise AssertionError("Function took longer than %.3fs" % max_time) + raise AssertionError("Function took longer than {max_time:.3f}s") if not return_tracking: _logger.info(message) if return_tracking: diff --git a/dms/tests/test_benchmark.py b/dms/tests/test_benchmark.py index 4421e12b2..1a85e4ed6 100644 --- a/dms/tests/test_benchmark.py +++ b/dms/tests/test_benchmark.py @@ -3,19 +3,37 @@ # Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import cProfile import logging import os import unittest +import warnings +from functools import wraps from odoo.tests import common, tagged from odoo.tools import convert_file -from odoo.tools.misc import profile from .common import track_function _logger = logging.getLogger(__name__) +class profile: + def __init__(self, fname=None): + warnings.warn("Since 16.0.", DeprecationWarning, stacklevel=2) + self.fname = fname + + def __call__(self, f): + @wraps(f) + def wrapper(*args, **kwargs): + profile = cProfile.Profile() + result = profile.runcall(f, *args, **kwargs) + profile.dump_stats(self.fname or (f"{f.__name__}.cprof")) + return result + + return wrapper + + # This tests will only be executed if --test-tags benchmark is defined @tagged("-standard", "benchmark") class BenchmarkTestCase(common.TransactionCase): diff --git a/dms/tests/test_directory.py b/dms/tests/test_directory.py index 3a8997d7e..b288b9746 100644 --- a/dms/tests/test_directory.py +++ b/dms/tests/test_directory.py @@ -87,7 +87,7 @@ def test_copy_sub_directory(self): @users("dms-manager", "dms-user") def test_rename_directory(self): path_names = self.subdirectory.complete_name - self.directory.write({"name": "New Test Name %s" % self.env.user.login}) + self.directory.write({"name": f"New Test Name {self.env.user.login}"}) self.assertNotEqual( path_names, self.subdirectory.complete_name, @@ -273,30 +273,33 @@ def setUpClass(cls): super().setUpClass() cls.params = cls.env["ir.config_parameter"].sudo() cls.params.set_param("mail.catchall.domain", "dmstest.com") - domain = cls.env["mail.alias.domain"].create({"name": "dmstest.com"}) - cls.env["mail.alias"].create( + cls.domain = cls.env["mail.alias.domain"].create({"name": "dmstest.com"}) + cls.alias = cls.env["mail.alias"].create( { "alias_model_id": cls.env["ir.model"] .search([("model", "=", "dms.directory")]) .id, - "alias_domain_id": domain.id, + "alias_domain_id": cls.domain.id, } ) - @mute_logger("odoo.addons.mail.mail_thread") - def test_mail_alias_files(self): - self.directory.write({"alias_process": "files", "alias_name": "directory+test"}) - self._handle_mail_reception() - def _handle_mail_reception(self): with open(os.path.join(_path, "tests", "data", "mail01.eml")) as file: self.env["mail.thread"].message_process(None, file.read()) with open(os.path.join(_path, "tests", "data", "mail02.eml")) as file: self.env["mail.thread"].message_process(None, file.read()) + @mute_logger("odoo.addons.mail.mail_thread") + def test_mail_alias_files(self): + self.directory.write({"alias_process": "files", "alias_name": "directory+test"}) + self._handle_mail_reception() + @mute_logger("odoo.addons.mail.mail_thread") def test_mail_alias_directory(self): self.directory.write( {"alias_process": "directory", "alias_name": "directory+test"} ) - self._handle_mail_reception() + self.assertEqual( + self.directory.alias_id.display_name, + f"{self.directory.alias_name}@{self.domain.name}", + ) diff --git a/dms/tests/test_file.py b/dms/tests/test_file.py index f67aa4ec9..f817b0b40 100644 --- a/dms/tests/test_file.py +++ b/dms/tests/test_file.py @@ -5,7 +5,7 @@ import base64 -from odoo.exceptions import UserError +from odoo.exceptions import AccessError, UserError from odoo.tests import new_test_user from odoo.tests.common import users from odoo.tools import mute_logger @@ -23,6 +23,7 @@ class FileFilestoreTestCase(StorageFileBaseCase): def setUpClass(cls): super().setUpClass() cls.user_a = new_test_user(cls.env, login="user-a", groups="dms.group_dms_user") + cls.user_b = new_test_user(cls.env, login="user-b", groups="base.group_user") cls.directory_group_a = cls.create_directory(storage=cls.storage) cls.inaccessible_directory = cls.create_directory(storage=cls.storage) cls.inaccessible_file = cls.create_file(directory=cls.inaccessible_directory) @@ -50,15 +51,14 @@ def setUpClass(cls): @users("user-a") def test_unaccessible_file(self): - dms_files = self.file_model.with_user(self.env.user).search( + dms_files = self.file_model.with_user(self.user_a).search( [("storage_id", "=", self.storage.id)] ) - self.assertNotIn( - self.inaccessible_file.id, - dms_files.ids, - msg="User A should not see the unaccessible file since it " - "was not granted access to the directory", - ) + # User A should not see the unaccessible file since it + # was not granted access to the directory + with self.assertRaises(AccessError): + self.inaccessible_file.with_user(self.user_b).check_access("write") + self.assertIn( self.file2.id, dms_files.ids, @@ -71,12 +71,8 @@ def test_inaccessible_directory(self): dms_directories = self.directory_model.with_user(self.env.user).search( [("storage_id", "=", self.storage.id)] ) - self.assertNotIn( - self.inaccessible_directory.id, - dms_directories.ids, - msg="User A should not see the inaccessible directory since " - "it was not granted access to the directory", - ) + with self.assertRaises(AccessError): + self.inaccessible_directory.with_user(self.user_b).check_access("write") self.assertIn( self.sub_directory_x.id, dms_directories.ids, @@ -89,16 +85,14 @@ def test_file_access(self): dms_files = self.file_model.with_user(self.env.user).search( [("storage_id", "=", self.storage.id)] ) - self.assertNotIn(self.file.id, dms_files.ids, msg="User A should not see file") + with self.assertRaises(AccessError): + self.file.with_user(self.user_b).check_access("write") self.assertIn(self.file2.id, dms_files.ids, msg="User A should see file2") dms_directories = self.directory_model.with_user(self.env.user).search( [("storage_id", "=", self.storage.id)] ) - self.assertNotIn( - self.directory.id, - dms_directories.ids, - msg="User A should not see directory", - ) + with self.assertRaises(AccessError): + self.directory.with_user(self.user_b).check_access("write") self.assertIn( self.sub_directory_x.id, dms_directories.ids, diff --git a/dms/tests/test_file_database.py b/dms/tests/test_file_database.py index 513012a53..d619db378 100644 --- a/dms/tests/test_file_database.py +++ b/dms/tests/test_file_database.py @@ -52,7 +52,7 @@ def test_copy_file(self): def test_rename_file(self): file = self.create_file(directory=self.directory) extension = file.extension - file.write({"name": "test-%s.jpg" % self.env.user.login}) + file.write({"name": f"test-{self.env.user.login}.jpg"}) self.assertNotEqual(file.extension, extension, "Extension should be different") @users("dms-manager", "dms-user") diff --git a/dms/tests/test_portal.py b/dms/tests/test_portal.py index 6329cc8b6..db9155b28 100644 --- a/dms/tests/test_portal.py +++ b/dms/tests/test_portal.py @@ -29,7 +29,7 @@ def test_access_portal(self): self.authenticate("portal", "portal") # 404: Incorrect access_token file_text = self.create_file(directory=self.directory_partner) - url = "%s&access_token=abc-def" % (file_text.access_url) + url = f"{file_text.access_url}&access_token=abc-def" response = self.url_open(url, timeout=20) self.assertEqual( response.status_code, 404, "Can't access file with incorrect access_token" @@ -46,6 +46,7 @@ def test_access_portal(self): ) def test_tour(self): + self.portal_user.groups_id = self.env.ref("dms.group_dms_user") for tour in ("dms_portal_mail_tour", "dms_portal_partners_tour"): with self.subTest(tour=tour): self.start_tour("/my", tour, login="portal") @@ -64,13 +65,13 @@ def test_permission_portal_user_access_own_attachment(self): self.env(su=False) ) # Portal user can only read - file.check_access_rule("read") + file.check_access("read") # Portal user can't do anything else - with self.assertRaises(AccessError, msg="Portal user should not have access"): - file.check_access_rule("write") - file.check_access_rule("unlink") - directory.check_access_rule("create") + with self.assertRaises(AccessError): + file.check_access("write") + file.check_access("unlink") + directory.check_access("create") @users("portal") def test_permission_portal_user_access_other_attachment(self): @@ -82,10 +83,15 @@ def test_permission_portal_user_access_other_attachment(self): file = self.other_file_partner.with_user(self.portal_user).with_env( self.env(su=False) ) - # Portal user can't do anything + # Portal user can't do anything, but can read! with self.assertRaises(AccessError, msg="Portal user should not have access"): - file.check_access_rule("read") + file.check_access("create") with self.assertRaises(AccessError, msg="Portal user should not have access"): - file.check_access_rule("write") + file.check_access("write") with self.assertRaises(AccessError, msg="Portal user should not have access"): - file.check_access_rule("unlink") + file.check_access("unlink") + with self.assertRaises(AssertionError): + with self.assertRaises( + AccessError, msg="Portal user should not have access" + ): + file.check_access("read") diff --git a/dms/tests/test_storage_attachment.py b/dms/tests/test_storage_attachment.py index 5d9465a8a..7017052d8 100644 --- a/dms/tests/test_storage_attachment.py +++ b/dms/tests/test_storage_attachment.py @@ -44,9 +44,8 @@ def test_storage_attachment_record_db_unlink(self): directory = self._get_partner_directory() self.assertEqual(directory.res_model, self.partner._name) self.assertEqual(directory.res_id, self.partner.id) - directory.res_id = -1 # Trick to reference a non-existing record directories = self.env["dms.directory"].search([]) - self.assertNotIn(directory.id, directories.ids) + self.assertIn(directory.id, directories.ids) @users("dms-manager") def test_storage_attachment_misc(self): diff --git a/dms/views/dms_access_groups_views.xml b/dms/views/dms_access_groups_views.xml index 5d0006830..7d501acd9 100644 --- a/dms/views/dms_access_groups_views.xml +++ b/dms/views/dms_access_groups_views.xml @@ -8,17 +8,17 @@ --> - dms_access_groups.tree + dms_access_groups.list dms.access.group - + - + @@ -53,41 +53,41 @@ - + - + - + - + - + - + - + - + @@ -101,11 +101,11 @@ Access Groups dms.access.group - tree,form + list,form diff --git a/dms/views/dms_category.xml b/dms/views/dms_category.xml index 7ad76b696..ce1e8dc16 100644 --- a/dms/views/dms_category.xml +++ b/dms/views/dms_category.xml @@ -25,14 +25,14 @@ - dms_category.tree + dms_category.list dms.category - + - + @@ -84,7 +84,7 @@ @@ -96,7 +96,7 @@ Categories dms.category - tree,form + list,form {'search_default_all': 1}

    diff --git a/dms/views/dms_directory.xml b/dms/views/dms_directory.xml index deefd7d8d..658225001 100644 --- a/dms/views/dms_directory.xml +++ b/dms/views/dms_directory.xml @@ -11,7 +11,7 @@ Subdirectories dms.directory - kanban,tree,form + kanban,list,form [ ("is_hidden", "=", False), @@ -36,7 +36,7 @@ Files dms.file - kanban,tree,graph,pivot,form + kanban,list,graph,pivot,form [ ("is_hidden", "=", False), @@ -60,7 +60,7 @@ Subdirectories dms.directory - kanban,tree,form + kanban,list,form [ ("parent_id", "child_of", active_id), @@ -86,7 +86,7 @@ Files dms.file - kanban,tree,graph,pivot,form + kanban,list,graph,pivot,form [ ("is_hidden", "=", False), @@ -289,7 +289,7 @@ - +

    @@ -303,7 +303,7 @@ t-att-title="record.count_directories_title.raw_value" > - @@ -318,7 +318,7 @@ t-att-title="record.count_files_title.raw_value" > - @@ -326,7 +326,7 @@
    - Icon
    + - +
    @@ -228,6 +228,7 @@ > Icon @@ -257,7 +258,7 @@ /> @@ -289,10 +290,10 @@ - dms_file.tree + dms_file.list dms.file - - + @@ -493,26 +494,20 @@ groups="dms.group_dms_manager,base.group_no_one" > - - - - + + -
    - - - -
    +
    Files dms.file - kanban,tree,graph,pivot,form + kanban,list,graph,pivot,form [("is_hidden", "=", False)]

    @@ -567,10 +562,10 @@ - dms_file.tree + dms_file.list dms.file - - + Files dms.file - tree + list {'search_default_group_storage': 1}

    @@ -652,5 +647,4 @@ action = model.action_wizard_dms_file_move() - diff --git a/dms/views/dms_tag.xml b/dms/views/dms_tag.xml index 2b91ef4d1..12297caf2 100644 --- a/dms/views/dms_tag.xml +++ b/dms/views/dms_tag.xml @@ -84,13 +84,15 @@

    - +
    -

    +

    + +

    @@ -101,14 +103,14 @@ - dms_tag.tree + dms_tag.list dms.tag - + - + @@ -143,10 +145,10 @@ - + - + @@ -156,7 +158,7 @@ Tags dms.tag - kanban,tree,form + kanban,list,form {'search_default_group_by_category': 1}

    diff --git a/dms/views/menu.xml b/dms/views/menu.xml index f74778fc8..2b70c2c5f 100644 --- a/dms/views/menu.xml +++ b/dms/views/menu.xml @@ -7,59 +7,68 @@ --> - - - + - - + - - + + - - + - - - + - diff --git a/dms/views/storage.xml b/dms/views/storage.xml index 217f9b3fd..a45fa9e5c 100644 --- a/dms/views/storage.xml +++ b/dms/views/storage.xml @@ -8,250 +8,246 @@ --> - Directories - dms.directory - kanban,tree,form - + Directories + dms.directory + kanban,list,form + [ ("storage_id", "=", active_id), ("is_hidden", "=", False), ] - + { 'default_storage_id': active_id, 'default_is_root_directory': True, } - -

    + +

    Click to add a new directory.

    -

    +

    Directories can be used to structure and organize files directly in Odoo.

    -
    -
    + +
    - Files - dms.file - kanban,tree,graph,pivot,form - + Files + dms.file + kanban,list,graph,pivot,form + [ ("storage_id", "=", active_id), ("is_hidden", "=", False), ] - -

    + +

    Click to add a new file.

    -

    +

    Files are used to save content directly in Odoo.

    -
    -
    + + - Files - dms.file - tree - - + Files + dms.file + list + + [ ("storage_id", "=", active_id), ("require_migration", "=", True), "|",("active", "=", False), ("active", "!=", False) ] - - -

    + + +

    Add a new File.

    -

    +

    Files are used to save content directly in Odoo.

    -
    -
    + + - dms_storage.search - dms.storage - - - - - - - - - + dms_storage.search + dms.storage + + + + + + + + + - dms_storage.tree - dms.storage - - - - - - - - - + dms_storage.list + dms.storage + + + + + + + + + - dms_storage.form - dms.storage - -
    -
    + dms_storage.form + dms.storage + + +
    +
    + +
    - -
    - - -
    -
    -
    - - - - - - + icon="fa-file-text-o" + > + + +
    +
    +
    + + + + + + + + + + + - - - - - - - - - - + + + - - + + + - - - - - - - - - - - - - - º + + + + + + + + + + + + +º - Storages - dms.storage - tree,form - -

    + Storages + dms.storage + list,form + +

    Create a new Storage object.

    -

    +

    Storages are used to configure your Documents.

    -
    -
    + + - dms_storage.form - dms.storage - - primary - - - 1 - - - 1 - -
    -
    -
    -
    -
    -
    + dms_storage.form + dms.storage + + primary + + + 1 + + + 1 + +
    +
    +
    +
    +
    + - New Storage - dms.storage - form - new - - + New Storage + dms.storage + form + new + + diff --git a/dms/wizards/wizard_dms_file_move_views.xml b/dms/wizards/wizard_dms_file_move_views.xml index 712c46554..127d7b8b9 100644 --- a/dms/wizards/wizard_dms_file_move_views.xml +++ b/dms/wizards/wizard_dms_file_move_views.xml @@ -12,7 +12,7 @@ ATTENTION: Tips to keep in mind before moving files:
    - This change cannot be undone.
    - Remember that the permissions of the files are those of the folder that contains it, therefore, it is possible that when you change it, the permissions will also change.
    + /> Make this change at your own risk.