diff --git a/dms/controllers/main.py b/dms/controllers/main.py
index 5898e80fb..b09d85e97 100644
--- a/dms/controllers/main.py
+++ b/dms/controllers/main.py
@@ -5,7 +5,7 @@
class OnboardingController(http.Controller):
- @http.route("/config/dms.forbidden_extensions", type="json", auth="user")
+ @http.route("/config/dms.forbidden_extensions", type="jsonrpc", auth="user")
def forbidden_extensions(self, **_kwargs):
params = request.env["ir.config_parameter"].sudo()
return {
diff --git a/dms/demo/res_users.xml b/dms/demo/res_users.xml
index c586a6007..ca02b6a9a 100644
--- a/dms/demo/res_users.xml
+++ b/dms/demo/res_users.xml
@@ -7,6 +7,6 @@
-->
-
+
diff --git a/dms/models/access_groups.py b/dms/models/access_groups.py
index 71113d28b..1b2edab63 100644
--- a/dms/models/access_groups.py
+++ b/dms/models/access_groups.py
@@ -3,7 +3,7 @@
# Copyright 2024 Timothée Vannier - Subteno (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-from odoo import _, api, fields, models
+from odoo import Command, api, fields, models
from odoo.exceptions import ValidationError
@@ -47,7 +47,7 @@ class DmsAccessGroups(models.Model):
string="Directories",
column1="gid",
column2="aid",
- auto_join=True,
+ bypass_search_access=True,
readonly=True,
)
complete_directory_ids = fields.Many2many(
@@ -56,7 +56,7 @@ class DmsAccessGroups(models.Model):
column1="gid",
column2="aid",
string="Complete directories",
- auto_join=True,
+ bypass_search_access=True,
readonly=True,
)
count_users = fields.Integer(compute="_compute_users", store=True)
@@ -94,7 +94,7 @@ class DmsAccessGroups(models.Model):
column2="uid",
string="Group Users",
compute="_compute_users",
- auto_join=True,
+ bypass_search_access=True,
store=True,
recursive=True,
)
@@ -104,9 +104,10 @@ def _compute_count_directories(self):
for record in self:
record.count_directories = len(record.directory_ids)
- _sql_constraints = [
- ("name_uniq", "unique (name)", "The name of the group must be unique!")
- ]
+ _name_uniq = models.Constraint(
+ "unique (name)",
+ "The name of the group must be unique!",
+ )
@api.depends(
"parent_group_id.perm_inclusive_create",
@@ -136,20 +137,20 @@ def default_get(self, fields_list):
if res.get("explicit_user_ids"):
res["explicit_user_ids"] = res["explicit_user_ids"] + [self.env.uid]
else:
- res["explicit_user_ids"] = [(6, 0, [self.env.uid])]
+ res["explicit_user_ids"] = [Command.set([self.env.uid])]
return res
@api.depends(
"parent_group_id",
"parent_group_id.users",
"group_ids",
- "group_ids.users",
+ "group_ids.user_ids",
"explicit_user_ids",
)
def _compute_users(self):
for record in self:
users = (
- record.group_ids.users
+ record.group_ids.user_ids
| record.explicit_user_ids
| record.parent_group_id.users
)
@@ -158,7 +159,7 @@ def _compute_users(self):
def copy_data(self, default=None):
vals_list = super().copy_data(default)
for group, vals in zip(self, vals_list, strict=False):
- vals["name"] = _("%s (copy)") % group.name
+ vals["name"] = self.env._("%s (copy)", group.name)
return vals_list
@api.constrains("parent_path")
@@ -169,9 +170,9 @@ def _check_parent_recursiveness(self):
for one in self.filtered("parent_group_id"):
if str(one.id) in one.parent_path.split("/"):
raise ValidationError(
- _("Parent group '%(parent)s' is child of '%(current)s'.")
- % {
- "parent": one.parent_group_id.display_name,
- "current": one.display_name,
- }
+ self.env._(
+ "Parent group '%(parent)s' is child of '%(current)s'.",
+ parent=one.parent_group_id.display_name,
+ current=one.display_name,
+ )
)
diff --git a/dms/models/directory.py b/dms/models/directory.py
index 34127d2d2..467133be3 100644
--- a/dms/models/directory.py
+++ b/dms/models/directory.py
@@ -12,9 +12,9 @@
from collections import defaultdict
from typing import Literal # noqa # pylint: disable=unused-import
-from odoo import _, api, fields, models, tools
+from odoo import api, fields, models, tools
from odoo.exceptions import UserError, ValidationError
-from odoo.osv.expression import AND, OR
+from odoo.fields import Domain
from odoo.tools import consteq, human_size
from ..tools.file import check_name, unique_name
@@ -60,7 +60,7 @@ class DmsDirectory(models.Model):
comodel_name="dms.storage",
string="Storage",
ondelete="restrict",
- auto_join=True,
+ bypass_search_access=True,
store=True,
)
parent_id = fields.Many2one(
@@ -116,7 +116,7 @@ def _default_parent_id(self):
comodel_name="dms.directory",
inverse_name="parent_id",
string="Subdirectories",
- auto_join=False,
+ bypass_search_access=False,
copy=True,
)
@@ -153,7 +153,7 @@ def _default_parent_id(self):
comodel_name="dms.file",
inverse_name="directory_id",
string="Files",
- auto_join=False,
+ bypass_search_access=False,
copy=True,
)
@@ -221,7 +221,7 @@ def _get_domain_by_access_groups(self, operation):
if operation == "create":
# When creating, I need create access in parent directory, or
# self-create permission if it's a root directory
- result = OR(
+ result = Domain.OR(
[
[("is_root_directory", "=", False)] + result,
[("is_root_directory", "=", True)] + self_filter,
@@ -379,7 +379,11 @@ def _search_panel_directory(self, **kwargs):
# Search
@api.model
def _search_starred(self, operator, operand):
- if operator == "=" and operand:
+ # The 19.0 Domain optimizer normalises ('starred', '=', True) to
+ # ('starred', 'in', {True}); accept both shapes.
+ if isinstance(operand, (set, list, tuple)):
+ operand = True in operand
+ if operator in ("=", "in") and operand:
return [("user_star_ids", "in", [self.env.uid])]
return [("user_star_ids", "not in", [self.env.uid])]
@@ -412,14 +416,16 @@ def _compute_count_directories(self):
for record in self:
directories = len(record.child_directory_ids)
record.count_directories = directories
- record.count_directories_title = _("%s Subdirectories") % directories
+ record.count_directories_title = self.env._(
+ "%s Subdirectories", directories
+ )
@api.depends("file_ids")
def _compute_count_files(self):
for record in self:
files = len(record.file_ids)
record.count_files = files
- record.count_files_title = _("%s Files") % files
+ record.count_files_title = self.env._("%s Files", files)
@api.depends("child_directory_ids", "file_ids")
def _compute_count_elements(self):
@@ -528,7 +534,9 @@ def _onchange_model_id(self):
@api.constrains("parent_id")
def _check_directory_recursion(self):
if self._has_cycle():
- raise ValidationError(_("Error! You cannot create recursive directories."))
+ raise ValidationError(
+ self.env._("Error! You cannot create recursive directories.")
+ )
return True
@api.constrains("storage_id", "model_id")
@@ -538,34 +546,40 @@ def _check_storage_id_attachment_model_id(self):
):
if not record.model_id:
raise ValidationError(
- _("A directory has to have model in attachment storage.")
+ self.env._("A directory has to have model in attachment storage.")
)
if not record.is_root_directory and not record.res_id:
raise ValidationError(
- _("This directory needs to be associated to a record.")
+ self.env._("This directory needs to be associated to a record.")
)
@api.constrains("is_root_directory", "storage_id")
def _check_directory_storage(self):
for record in self:
if record.is_root_directory and not record.storage_id:
- raise ValidationError(_("A root directory has to have a storage."))
+ raise ValidationError(
+ self.env._("A root directory has to have a storage.")
+ )
@api.constrains("is_root_directory", "parent_id")
def _check_directory_parent(self):
for record in self:
if record.is_root_directory and record.parent_id:
raise ValidationError(
- _("A directory can't be a root and have a parent directory.")
+ self.env._(
+ "A directory can't be a root and have a parent directory."
+ )
)
if not record.is_root_directory and not record.parent_id:
- raise ValidationError(_("A directory has to have a parent directory."))
+ raise ValidationError(
+ self.env._("A directory has to have a parent directory.")
+ )
@api.constrains("name")
def _check_name(self):
for record in self:
if self.env.context.get("check_name", True) and not check_name(record.name):
- raise ValidationError(_("The directory name is invalid."))
+ raise ValidationError(self.env._("The directory name is invalid."))
if record.is_root_directory:
children = record.sudo().storage_id.root_directory_ids
else:
@@ -576,7 +590,7 @@ def _check_name(self):
and child != record
):
raise ValidationError(
- _("A directory with the same name already exists.")
+ self.env._("A directory with the same name already exists.")
)
# Create, Update, Delete
@@ -626,7 +640,7 @@ def message_new(self, msg_dict, custom_values=None):
return parent_directory
names = parent_directory.child_directory_ids.mapped("name")
slug = self.env["ir.http"]._slug
- subject = slug(msg_dict.get("subject", _("Alias-Mail-Extraction")))
+ subject = slug(msg_dict.get("subject", self.env._("Alias-Mail-Extraction")))
defaults = dict(
{"name": unique_name(subject, names, escape_suffix=True)}, **custom_values
)
@@ -678,13 +692,15 @@ def write(self, vals):
if new_parent_id:
if old_storage_id != self.browse(new_parent_id).storage_id.id:
raise UserError(
- _(
+ self.env._(
"It is not possible to change to a parent "
"with other storage."
)
)
elif old_storage_id != new_storage_id:
- raise UserError(_("It is not possible to change the storage."))
+ raise UserError(
+ self.env._("It is not possible to change the storage.")
+ )
# Groups part
if any(key in vals for key in ["group_ids", "inherit_group_ids"]):
res = super().write(vals)
@@ -743,7 +759,7 @@ def action_dms_directories_all_directory(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"dms.action_dms_directory"
)
- domain = AND(
+ domain = Domain.AND(
[
literal_eval(action["domain"].strip()),
[("parent_id", "child_of", self.id)],
@@ -761,7 +777,7 @@ def action_dms_directories_all_directory(self):
def action_dms_files_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id("dms.action_dms_file")
- domain = AND(
+ domain = Domain.AND(
[
literal_eval(action["domain"].strip()),
[("directory_id", "child_of", self.id)],
diff --git a/dms/models/dms_category.py b/dms/models/dms_category.py
index 5900f4d93..575b01f3f 100644
--- a/dms/models/dms_category.py
+++ b/dms/models/dms_category.py
@@ -5,7 +5,7 @@
import logging
-from odoo import _, api, fields, models
+from odoo import api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
@@ -63,9 +63,10 @@ class DMSCategory(models.Model):
count_directories = fields.Integer(compute="_compute_count_directories")
count_files = fields.Integer(compute="_compute_count_files")
- _sql_constraints = [
- ("name_uniq", "unique (name)", "Category name already exists!"),
- ]
+ _name_uniq = models.Constraint(
+ "unique (name)",
+ "Category name already exists!",
+ )
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
@@ -100,5 +101,7 @@ def _compute_count_files(self):
@api.constrains("parent_id")
def _check_category_recursion(self):
if self._has_cycle():
- raise ValidationError(_("Error! You cannot create recursive categories."))
+ raise ValidationError(
+ self.env._("Error! You cannot create recursive categories.")
+ )
return True
diff --git a/dms/models/dms_file.py b/dms/models/dms_file.py
index 328ebb434..ea4c3df32 100644
--- a/dms/models/dms_file.py
+++ b/dms/models/dms_file.py
@@ -12,9 +12,9 @@
from PIL import Image
-from odoo import _, api, fields, models, tools
+from odoo import api, fields, models, tools
from odoo.exceptions import UserError, ValidationError
-from odoo.osv import expression
+from odoo.fields import Domain
from odoo.tools import consteq, human_size
from odoo.tools.mimetypes import guess_mimetype
@@ -50,7 +50,7 @@ class DMSFile(models.Model):
domain="[('permission_create', '=', True)]",
context={"dms_directory_show_path": True},
ondelete="restrict",
- auto_join=True,
+ bypass_search_access=True,
required=True,
index="btree",
tracking=True, # Leave log if "moved" to another directory
@@ -197,7 +197,7 @@ def check_access_token(self, access_token=False):
string="Attachment File",
prefetch=False,
ondelete="cascade",
- index=True,
+ index="btree",
)
def get_human_size(self):
@@ -250,7 +250,7 @@ def action_migrate(self, should_logging=True):
for dms_file in self:
if should_logging:
_logger.info(
- _(
+ self.env._(
"Migrate File %(index)s of %(record_count)s [ %("
"dms_file_migration)s ]",
index=index,
@@ -275,7 +275,9 @@ def action_wizard_dms_file_move(self):
items = self.browse(self.env.context.get("active_ids"))
root_directories = items.mapped("root_directory_id")
if len(root_directories) > 1:
- raise UserError(_("Only files in the same root directory can be moved."))
+ raise UserError(
+ self.env._("Only files in the same root directory can be moved.")
+ )
result = self.env["ir.actions.act_window"]._for_xml_id(
"dms.wizard_dms_file_move_act_window"
)
@@ -300,7 +302,7 @@ def _search_panel_domain(self, field, operator, directory_id, comodel_domain=Fal
if not comodel_domain:
comodel_domain = []
files_ids = self.search([("directory_id", operator, directory_id)]).ids
- return expression.AND([comodel_domain, [(field, "in", files_ids)]])
+ return Domain.AND([comodel_domain, [(field, "in", files_ids)]])
@api.model
def search_panel_select_range(self, field_name, **kwargs):
@@ -505,20 +507,24 @@ def _check_storage_id_attachment_res_model(self):
record.res_model and record.res_id
):
raise ValidationError(
- _("A file must have model and resource ID in attachment storage.")
+ self.env._(
+ "A file must have model and resource ID in attachment storage."
+ )
)
@api.constrains("name")
def _check_name(self):
for record in self:
if not file.check_name(record.name):
- raise ValidationError(_("The file name is invalid."))
+ raise ValidationError(self.env._("The file name is invalid."))
files = record.sudo().directory_id.file_ids
if files.filtered(
lambda file, record=record: file.name == record.name and file != record
):
raise ValidationError(
- _("A file with the same name already exists in this directory.")
+ self.env._(
+ "A file with the same name already exists in this directory."
+ )
)
@api.constrains("extension")
@@ -527,7 +533,9 @@ def _check_extension(self):
lambda rec: rec.extension
and rec.extension in self._get_forbidden_extensions()
):
- raise ValidationError(_("The file has a forbidden file extension."))
+ raise ValidationError(
+ self.env._("The file has a forbidden file extension.")
+ )
@api.constrains("size")
def _check_size(self):
@@ -535,7 +543,9 @@ def _check_size(self):
lambda rec: rec.size > self._get_binary_max_size() * 1024 * 1024
):
raise ValidationError(
- _("The maximum upload size is %s MB.") % self._get_binary_max_size()
+ self.env._(
+ "The maximum upload size is %s MB.", self._get_binary_max_size()
+ )
)
# Create, Update, Delete
@@ -655,7 +665,7 @@ def get_dms_files_from_attachments(self, attachment_ids=None):
:return: An Array of dms files.
"""
if not attachment_ids:
- raise UserError(_("No attachment was provided"))
+ raise UserError(self.env._("No attachment was provided"))
attachments = self.env["ir.attachment"].browse(attachment_ids)
@@ -663,6 +673,6 @@ def get_dms_files_from_attachments(self, attachment_ids=None):
attachment.res_id or attachment.res_model != "dms.file"
for attachment in attachments
):
- raise UserError(_("Invalid attachments!"))
+ raise UserError(self.env._("Invalid attachments!"))
return [self.get_attachment_object(attachment) for attachment in attachments]
diff --git a/dms/models/dms_security_mixin.py b/dms/models/dms_security_mixin.py
index 87f65a449..3c2fa7fa4 100644
--- a/dms/models/dms_security_mixin.py
+++ b/dms/models/dms_security_mixin.py
@@ -8,12 +8,7 @@
from odoo import api, fields, models
from odoo.exceptions import AccessError
-from odoo.osv.expression import (
- FALSE_DOMAIN,
- NEGATIVE_TERM_OPERATORS,
- OR,
- TRUE_DOMAIN,
-)
+from odoo.fields import Domain
from odoo.tools import SQL
_logger = getLogger(__name__)
@@ -60,6 +55,8 @@ class DmsSecurityMixin(models.AbstractModel):
@api.model
def _get_ref_selection(self):
+ # All registered models are an intentional choice here.
+ # pylint: disable=no-search-all
models = self.env["ir.model"].sudo().search([])
return [(model.model, model.name) for model in models]
@@ -116,14 +113,14 @@ def _get_domain_by_inheritance(self, operation):
]
domains = []
# Get all used related records
- related_groups = self.sudo().read_group(
+ related_groups = self.sudo()._read_group(
domain=inherited_access_domain + [("res_model", "!=", False)],
- fields=["res_id:array_agg"],
groupby=["res_model"],
+ aggregates=["res_id:array_agg"],
)
- for group in related_groups:
+ for res_model, res_id_array in related_groups:
try:
- model = self.env[group["res_model"]]
+ model = self.env[res_model]
except KeyError:
# The model might not be registered.
# This is normal if you are upgrading the database.
@@ -131,7 +128,7 @@ def _get_domain_by_inheritance(self, operation):
# These records will be accessible by DB users only.
domains.append(
[
- ("res_model", "=", group["res_model"]),
+ ("res_model", "=", res_model),
(True, "=", self.env.user.has_group("base.group_user")),
]
)
@@ -143,7 +140,7 @@ def _get_domain_by_inheritance(self, operation):
continue
domains.append([("res_model", "=", model._name), ("res_id", "=", False)])
# Check record access in batch too
- res_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id
+ res_ids = [i for i in res_id_array if i] # Hack to remove None res_id
# Apply exists to skip records that do not exist. (e.g. a res.partner
# deleted by database).
model_records = model.browse(res_ids).exists()
@@ -153,8 +150,7 @@ def _get_domain_by_inheritance(self, operation):
domains.append(
[("res_model", "=", model._name), ("res_id", "in", related_ok.ids)]
)
- result = inherited_access_domain + OR(domains)
- return result
+ return Domain.AND([Domain(inherited_access_domain), Domain.OR(domains)])
@api.model
def _get_access_groups_query(self, operation):
@@ -211,19 +207,21 @@ def _get_permission_domain(self, operator, value, operation):
value = bool(value)
# Tricky one, to know if you want to search
# positive or negative access
- positive = (operator not in NEGATIVE_TERM_OPERATORS) == bool(value)
+ positive = (operator not in Domain.NEGATIVE_OPERATORS) == bool(value)
if _self.env.su:
# You're SUPERUSER_ID
- return TRUE_DOMAIN if positive else FALSE_DOMAIN
+ return Domain.TRUE if positive else Domain.FALSE
- result = OR(
+ result = Domain.OR(
[
_self._get_domain_by_access_groups(operation),
_self._get_domain_by_inheritance(operation),
]
)
if not positive:
- result.insert(0, "!")
+ # Domain is immutable in 19.0; use the bitwise invert operator
+ # (replaces the 18.0 ``result.insert(0, "!")`` mutation).
+ result = ~Domain(result)
return result
@api.model
@@ -286,6 +284,42 @@ def _check_access_dms_record(self, operation: str) -> tuple | None:
if any(x_id not in items.ids for x_id in self.ids):
raise Rule._make_access_error(operation, (self - items))
+ @api.model
+ def _search(self, domain, *args, **kwargs):
+ """Inject the DMS access-group + inheritance filter into reads.
+
+ ``ir.rule._compute_domain()`` runs the basic Domain optimizer
+ (``Domain.optimize``, not ``Domain.optimize_full``), so ``search=``
+ methods on non-stored computed fields like ``permission_read`` are
+ silently bypassed when ``domain_force`` is evaluated. The read-side
+ ``ir.rule`` records on ``dms.directory`` / ``dms.file`` would
+ therefore no-op and every user would see every record. We restore
+ the intended behaviour by AND-ing the same domain directly into the
+ search here, where SQL translation runs the full optimizer that
+ does honour ``search=`` methods.
+
+ Notes:
+ - ``env.su`` short-circuits the filter (superuser sees everything).
+ - The context flag ``dms_skip_access_filter`` lets internal helpers
+ (``_get_domain_by_inheritance``'s ``_read_group`` etc.) bypass the
+ filter on their recursive reads, avoiding O(N) blow-up.
+ - The filter targets the ``read`` operation. Write/create/unlink
+ validation continues via ``_check_access_dms_record`` (called from
+ ``create``/``write``/``unlink`` in this mixin) plus the
+ ``_search_permission_*`` methods for explicit
+ ``('permission_', '=', user.id)`` clauses in caller domains.
+ """
+ if not self.env.su and not self.env.context.get("dms_skip_access_filter"):
+ self = self.with_context(dms_skip_access_filter=True)
+ dms_domain = Domain.OR(
+ [
+ self._get_domain_by_access_groups("read"),
+ self._get_domain_by_inheritance("read"),
+ ]
+ )
+ domain = Domain.AND([Domain(domain), dms_domain])
+ return super()._search(domain, *args, **kwargs)
+
@api.model_create_multi
def create(self, vals_list):
# Create as sudo to avoid testing creation permissions before DMS security
diff --git a/dms/models/storage.py b/dms/models/storage.py
index cbc74cf01..7f29d3bdc 100644
--- a/dms/models/storage.py
+++ b/dms/models/storage.py
@@ -5,7 +5,7 @@
import logging
-from odoo import _, api, fields, models
+from odoo import api, fields, models
from odoo.exceptions import AccessError
_logger = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ class Storage(models.Model):
comodel_name="dms.directory",
inverse_name="storage_id",
string="Root Directories",
- auto_join=False,
+ bypass_search_access=False,
readonly=False,
copy=False,
)
@@ -52,7 +52,7 @@ class Storage(models.Model):
comodel_name="dms.directory",
inverse_name="storage_id",
string="Directories",
- auto_join=False,
+ bypass_search_access=False,
readonly=True,
copy=False,
)
@@ -60,7 +60,7 @@ class Storage(models.Model):
comodel_name="dms.file",
inverse_name="storage_id",
string="Files",
- auto_join=False,
+ bypass_search_access=False,
readonly=True,
copy=False,
)
@@ -100,7 +100,7 @@ def _onchange_save_type(self):
def action_storage_migrate(self):
if self.save_type != "attachment":
if not self.env.user.has_group("dms.group_dms_manager"):
- raise AccessError(_("Only managers can execute this action."))
+ raise AccessError(self.env._("Only managers can execute this action."))
files = self.env["dms.file"].with_context(active_test=False).sudo()
for record in self:
diff --git a/dms/models/tag.py b/dms/models/tag.py
index 25dfb9a57..dac0dc799 100644
--- a/dms/models/tag.py
+++ b/dms/models/tag.py
@@ -16,7 +16,7 @@ class Tag(models.Model):
name = fields.Char(required=True, translate=True)
active = fields.Boolean(
default=True,
- help="The active field allows you " "to hide the tag without removing it.",
+ help="The active field allows you to hide the tag without removing it.",
)
category_id = fields.Many2one(
comodel_name="dms.category",
@@ -44,9 +44,10 @@ class Tag(models.Model):
count_directories = fields.Integer(compute="_compute_count_directories")
count_files = fields.Integer(compute="_compute_count_files")
- _sql_constraints = [
- ("name_uniq", "unique (name, category_id)", "Tag name already exists!"),
- ]
+ _name_uniq = models.Constraint(
+ "unique (name, category_id)",
+ "Tag name already exists!",
+ )
@api.depends("directory_ids")
def _compute_count_directories(self):
diff --git a/dms/security/security.xml b/dms/security/security.xml
index 463f914c3..5f6bf0db8 100644
--- a/dms/security/security.xml
+++ b/dms/security/security.xml
@@ -12,17 +12,21 @@
Documents
+
+ Documents
+
+
User
-
+
Manager
-
+
diff --git a/dms/static/src/js/fields/path_json/path_owl.xml b/dms/static/src/js/fields/path_json/path_owl.xml
index b2a8e7c70..ff0090c9a 100644
--- a/dms/static/src/js/fields/path_json/path_owl.xml
+++ b/dms/static/src/js/fields/path_json/path_owl.xml
@@ -6,25 +6,27 @@
-
-
- /
-
-
-
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
-
- /
-
-
-
-
-
+
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 ca55cdd5b..65fd2bbaf 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
@@ -18,7 +18,7 @@ export class PreviewRecordField extends BinaryField {
onFilePreview() {
const self = this;
- const attachment = this.store.Attachment.insert({
+ const attachment = this.store["ir.attachment"].insert({
id: self.props.record.resId,
filename: self.props.record.data.display_name || "",
name: self.props.record.data.display_name || "",
diff --git a/dms/static/src/js/views/file_kanban_record.esm.js b/dms/static/src/js/views/file_kanban_record.esm.js
index 335fb08dd..002ec429e 100644
--- a/dms/static/src/js/views/file_kanban_record.esm.js
+++ b/dms/static/src/js/views/file_kanban_record.esm.js
@@ -44,7 +44,7 @@ export class FileKanbanRecord extends KanbanRecord {
mimetype = self.props.record.data.mimetype;
}
- const attachment = this.store.Attachment.insert({
+ const attachment = this.store["ir.attachment"].insert({
id: self.props.record.data.id,
filename: self.props.record.data.name,
name: self.props.record.data.name,
diff --git a/dms/static/src/js/views/file_kanban_renderer.xml b/dms/static/src/js/views/file_kanban_renderer.xml
index a1bb3c4fa..3d3626bc4 100644
--- a/dms/static/src/js/views/file_kanban_renderer.xml
+++ b/dms/static/src/js/views/file_kanban_renderer.xml
@@ -16,12 +16,21 @@
-
-
+
+
+
-
+
diff --git a/dms/static/src/js/views/file_list_renderer.xml b/dms/static/src/js/views/file_list_renderer.xml
index 0b1b83418..6a88307b9 100644
--- a/dms/static/src/js/views/file_list_renderer.xml
+++ b/dms/static/src/js/views/file_list_renderer.xml
@@ -13,7 +13,7 @@
t-inherit="web.ListView.Buttons"
t-inherit-mode="primary"
>
-
+
-
+
-
+
[
{
content: "Go to Mails directory",
@@ -14,15 +13,16 @@ registry.category("web_tour.tours").add("dms_portal_mail_tour", {
run: "click",
},
{
- content: "Go to Mail_01.eml",
+ content: "Mail_01.eml is reachable",
trigger: ".tr_dms_file_link:contains('Mail_01.eml')",
+ // eslint-disable-next-line no-empty-function
+ run() {},
},
],
});
registry.category("web_tour.tours").add("dms_portal_partners_tour", {
url: "/my/dms",
- test: true,
steps: () => [
{
content: "Go to Partners directory",
@@ -35,8 +35,10 @@ registry.category("web_tour.tours").add("dms_portal_partners_tour", {
run: "click",
},
{
- content: "Go to test.txt",
+ content: "test.txt is reachable",
trigger: ".tr_dms_file_link:contains('test.txt')",
+ // eslint-disable-next-line no-empty-function
+ run() {},
},
],
});
diff --git a/dms/tests/common.py b/dms/tests/common.py
index d09a5a8e9..ee5bf9b1b 100644
--- a/dms/tests/common.py
+++ b/dms/tests/common.py
@@ -8,12 +8,28 @@
import logging
import threading
import time
+import unittest
import uuid
from odoo.tests import Form, new_test_user
from odoo.addons.base.tests.common import BaseCommon
+
+def require_demo_xmlid(env, xmlid):
+ """Return ``env.ref(xmlid)`` or raise ``unittest.SkipTest``.
+
+ Helper for tests that depend on demo data — in 19.0 the default is
+ ``with_demo=False``, so CI may run with no demo records loaded.
+ Safe to call from ``setUpClass`` (where ``self.skipTest`` is not
+ available and would TypeError).
+ """
+ record = env.ref(xmlid, raise_if_not_found=False)
+ if not record:
+ raise unittest.SkipTest(f"demo data {xmlid!r} is required for these tests")
+ return record
+
+
_logger = logging.getLogger(__name__)
# ----------------------------------------------------------
diff --git a/dms/tests/test_file.py b/dms/tests/test_file.py
index f67aa4ec9..707518753 100644
--- a/dms/tests/test_file.py
+++ b/dms/tests/test_file.py
@@ -10,7 +10,7 @@
from odoo.tests.common import users
from odoo.tools import mute_logger
-from .common import StorageFileBaseCase
+from .common import StorageFileBaseCase, require_demo_xmlid
try:
import magic
@@ -145,7 +145,7 @@ def test_content_file(self):
object_file.unlink()
def test_content_file_mimetype(self):
- file_svg = self.env.ref("dms.file_05_demo")
+ file_svg = require_demo_xmlid(self.env, "dms.file_05_demo")
self.assertEqual(file_svg.mimetype, "image/svg+xml", msg="SVG mimetype")
file_logo = self.env.ref("dms.file_02_demo")
self.assertEqual(file_logo.mimetype, "image/jpeg", msg="JPEG mimetype")
@@ -153,11 +153,11 @@ def test_content_file_mimetype(self):
def test_content_file_mimetype_magic_library(self):
if not magic:
self.skipTest("Without python-magic library installed")
- file_video = self.env.ref("dms.file_10_demo")
+ file_video = require_demo_xmlid(self.env, "dms.file_10_demo")
self.assertEqual(file_video.mimetype, "video/mp4", msg="MP4 mimetype")
def test_content_file_extension(self):
- file_pdf = self.env.ref("dms.file_27_demo")
+ file_pdf = require_demo_xmlid(self.env, "dms.file_27_demo")
self.assertEqual(file_pdf.extension, "pdf", msg="PDF extension")
file_pdf.name = "Document_05"
self.assertEqual(
diff --git a/dms/tests/test_file_database.py b/dms/tests/test_file_database.py
index d619db378..8759ea150 100644
--- a/dms/tests/test_file_database.py
+++ b/dms/tests/test_file_database.py
@@ -7,14 +7,14 @@
from odoo.tests.common import users
from odoo.tools import mute_logger
-from .common import StorageDatabaseBaseCase
+from .common import StorageDatabaseBaseCase, require_demo_xmlid
class FileDatabaseTestCase(StorageDatabaseBaseCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.file_demo_01 = cls.env.ref("dms.file_01_demo")
+ cls.file_demo_01 = require_demo_xmlid(cls.env, "dms.file_01_demo")
cls.directory2 = cls.create_directory(storage=cls.storage)
cls.new_storage2 = cls.create_storage(save_type="database")
cls.directory3 = cls.create_directory(storage=cls.new_storage2)
@@ -68,7 +68,7 @@ def test_move_file(self):
@users("dms-manager", "dms-user")
def test_move_directory(self):
with self.assertRaises(
- UserError, msg="Directory can't have any parent, because it is " "root"
+ UserError, msg="Directory can't have any parent, because it is root"
):
self.directory.write(
{
diff --git a/dms/tests/test_portal.py b/dms/tests/test_portal.py
index dbc321ae6..4d0a5dbbe 100644
--- a/dms/tests/test_portal.py
+++ b/dms/tests/test_portal.py
@@ -6,7 +6,7 @@
from odoo.tests.common import users
from odoo.tools import mute_logger
-from .common import StorageAttachmentBaseCase
+from .common import StorageAttachmentBaseCase, require_demo_xmlid
@odoo.tests.tagged("post_install", "-at_install")
@@ -14,7 +14,7 @@ class TestDmsPortal(odoo.tests.HttpCase, StorageAttachmentBaseCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- cls.partner = cls.env.ref("base.partner_demo_portal")
+ cls.partner = require_demo_xmlid(cls.env, "base.partner_demo_portal")
cls.portal_user = cls.partner.user_ids
cls._create_attachment("test.txt")
cls._create_attachment("test2.txt", cls.other_partner)
diff --git a/dms/tests/test_storage_attachment.py b/dms/tests/test_storage_attachment.py
index f0a1a898b..c278fcd4c 100644
--- a/dms/tests/test_storage_attachment.py
+++ b/dms/tests/test_storage_attachment.py
@@ -51,8 +51,8 @@ def test_storage_attachment_record_db_unlink(self):
@mute_logger("odoo.models.unlink")
def test_storage_attachment_unlink_lock_file(self):
group_partner_manager = self.env.ref("base.group_partner_manager")
- self.dms_manager_user.write({"groups_id": [(4, group_partner_manager.id)]})
- self.dms_user.write({"groups_id": [(4, group_partner_manager.id)]})
+ self.dms_manager_user.write({"group_ids": [(4, group_partner_manager.id)]})
+ self.dms_user.write({"group_ids": [(4, group_partner_manager.id)]})
attachment = self._create_attachment("demo.txt")
attachment = attachment.with_user(self.dms_manager_user)
file = self.storage.storage_file_ids.filtered(lambda x: x.name == "demo.txt")
diff --git a/dms/views/dms_directory.xml b/dms/views/dms_directory.xml
index 5f9e6d212..4d048a53f 100644
--- a/dms/views/dms_directory.xml
+++ b/dms/views/dms_directory.xml
@@ -140,7 +140,7 @@
name="filter_write"
date="write_date"
/>
-
+
-
+
-
+
Move
-
+
list
code
diff --git a/dms/views/dms_tag.xml b/dms/views/dms_tag.xml
index 2a2017685..ca0eb4708 100644
--- a/dms/views/dms_tag.xml
+++ b/dms/views/dms_tag.xml
@@ -12,13 +12,13 @@
dms.tag
-
+
-
+
ir.actions.act_window
res.config.settings
form
- inline
+
{'module': 'dms'}
diff --git a/dms/views/storage.xml b/dms/views/storage.xml
index a45fa9e5c..77dde4f6d 100644
--- a/dms/views/storage.xml
+++ b/dms/views/storage.xml
@@ -83,7 +83,7 @@
-
+