Skip to content
Snippets Groups Projects
Commit a740989b authored by std-odoo's avatar std-odoo Committed by Raphael Collet
Browse files

[MOV] web: move the domain selector component to web

Purpose
=======

For security reason access on ir.model is not granted to internal suers. Due
to this constraint a component exists in spreadsheet to be able to select
the models for which we have a read access on the records of this model.

This is required for the properties fields feature, hence moving its code
to web.

Some renaming is performed to make it generic. This generates some changes
in other addons, notably some class renaming.el.

Task-2852259

Part-of: odoo/odoo#95184
parent 3961038d
No related branches found
No related tags found
Loading
......@@ -3,6 +3,7 @@
from . import ir_qweb_fields
from . import ir_http
from . import ir_model
from . import ir_ui_menu
from . import models
from . import base_document_layout
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api
class IrModel(models.Model):
_inherit = "ir.model"
@api.model
def display_name_for(self, models):
"""
Returns the display names from provided models which the current user can access.
The result is the same whether someone tries to access an inexistent model or a model they cannot access.
:models list(str): list of technical model names to lookup (e.g. `["res.partner"]`)
:return: list of dicts of the form `{ "model", "display_name" }` (e.g. `{ "model": "res_partner", "display_name": "Contact"}`)
"""
# Store accessible models in a temporary list in order to execute only one SQL query
accessible_models = []
not_accessible_models = []
for model in models:
if self._check_model_access(model):
accessible_models.append(model)
else:
not_accessible_models.append({"display_name": model, "model": model})
records = self.env["ir.model"].sudo().search_read([("model", "in", accessible_models)], ["name", "model"])
return [{
"display_name": model["name"],
"model": model["model"],
} for model in records] + not_accessible_models
@api.model
def _check_model_access(self, model):
return self.env.user._is_internal() and model in self.env and self.env[model].check_access_rights("read", raise_exception=False)
/** @odoo-module **/
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { useService } from "@web/core/utils/hooks";
import { fuzzyLookup } from "@web/core/utils/search";
import { _t } from "@web/core/l10n/translation";
const { Component, onWillStart } = owl;
export class ModelSelector extends Component {
setup() {
this.orm = useService("orm");
onWillStart(async () => {
this.models = await this.orm.call("ir.model", "display_name_for", [this.props.models]);
this.models = this.models.map((record) => ({
label: record.display_name,
technical: record.model,
classList: {
[`o_model_selector_${record.model}`]: 1,
},
}));
});
}
get placeholder() {
return _t("Start typing to search more...");
}
get sources() {
return [this.optionsSource];
}
get optionsSource() {
return {
placeholder: _t("Loading..."),
options: this.loadOptionsSource.bind(this),
};
}
onSelect(option) {
this.props.onModelSelected({
label: option.label,
technical: option.technical,
});
}
filterModels(name) {
if (!name) {
return this.models.slice(0, 8);
}
return fuzzyLookup(name, this.models, (model) => model.technical + model.label).slice(0, 8);
}
loadOptionsSource(request) {
const options = this.filterModels(request);
if (!options.length) {
options.push({
label: _t("No records"),
classList: "o_m2o_no_result",
unselectable: true,
});
}
return options;
}
}
ModelSelector.template = "web.ModelSelector";
ModelSelector.components = { AutoComplete };
ModelSelector.props = {
onModelSelected: Function,
value: { type: String, optional: true },
models: Array,
};
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="web.ModelSelector" owl="1">
<div class="o_sp_input_dropdown" t-ref="autocomplete_container">
<input t-if="env.isSmall"
type="text"
class="o_input"
readonly=""
t-att-value="props.value"
/>
<AutoComplete t-else=""
value="props.value || ''"
sources="sources"
placeholder="placeholder"
autoSelect="props.autoSelect"
onSelect.bind="onSelect"
/>
<a role="button" class="o_dropdown_button" draggable="false" />
</div>
</t>
</templates>
/** @odoo-module */
import { browser } from "@web/core/browser/browser";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { registry } from "@web/core/registry";
import { ormService } from "@web/core/orm_service";
import { ModelSelector } from "@web/core/model_selector/model_selector";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { click, editInput, getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils";
registry
.category("mock_server")
.add("ir.model/display_name_for", function (route, args) {
const models = args.args[0];
const records = this.models["ir.model"].records.filter((record) =>
models.includes(record.model)
);
return records.map((record) => ({
model: record.model,
display_name: record.name,
}));
});
const serviceRegistry = registry.category("services");
let env;
let fixture;
async function mountModelSelector(models = [], value = undefined, onModelSelected = () => {}) {
await mount(ModelSelector, fixture, {
env,
props: {
models,
value,
onModelSelected,
},
});
}
async function openAutocomplete(search = undefined) {
await click(fixture, ".o-autocomplete--input");
}
async function beforeEach() {
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("orm", ormService);
env = await makeTestEnv({
serverData: {
models: {
"ir.model": {
fields: {
name: { string: "Model Name", type: "char" },
model: { string: "Model", type: "char" },
},
records: [
{
id: 1,
name: "Model 1",
model: "model_1",
},
{
id: 2,
name: "Model 2",
model: "model_2",
},
{
id: 3,
name: "Model 3",
model: "model_3",
},
{
id: 4,
name: "Model 4",
model: "model_4",
},
{
id: 5,
name: "Model 5",
model: "model_5",
},
{
id: 6,
name: "Model 6",
model: "model_6",
},
{
id: 7,
name: "Model 7",
model: "model_7",
},
{
id: 8,
name: "Model 8",
model: "model_8",
},
{
id: 9,
name: "Model 9",
model: "model_9",
},
{
id: 10,
name: "Model 10",
model: "model_10",
},
],
},
},
},
});
fixture = getFixture();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
}
QUnit.module("web > model_selector", { beforeEach });
QUnit.test("model_selector: with no model", async function (assert) {
await mountModelSelector();
await openAutocomplete();
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
assert.strictEqual(
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
"No records"
);
});
QUnit.test("model_selector: displays model display names", async function (assert) {
await mountModelSelector(["model_1", "model_2", "model_3"]);
await openAutocomplete();
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 3);
const items = fixture.querySelectorAll("li.o-autocomplete--dropdown-item");
assert.strictEqual(items[0].innerText, "Model 1");
assert.strictEqual(items[1].innerText, "Model 2");
assert.strictEqual(items[2].innerText, "Model 3");
});
QUnit.test("model_selector: with 8 models", async function (assert) {
await mountModelSelector([
"model_1",
"model_2",
"model_3",
"model_4",
"model_5",
"model_6",
"model_7",
"model_8",
]);
await openAutocomplete();
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 8);
});
QUnit.test("model_selector: with more than 8 models", async function (assert) {
await mountModelSelector([
"model_1",
"model_2",
"model_3",
"model_4",
"model_5",
"model_6",
"model_7",
"model_8",
"model_9",
"model_10",
]);
await openAutocomplete();
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 8);
});
QUnit.test("model_selector: search content is not applied when opening the autocomplete", async function (assert) {
await mountModelSelector(["model_1", "model_2"], "_2");
await openAutocomplete();
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 2);
});
QUnit.test("model_selector: with search matching some records on technical name", async function (assert) {
await mountModelSelector(["model_1", "model_2"]);
await openAutocomplete();
await editInput(fixture, ".o-autocomplete--input", "_2");
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
assert.strictEqual(
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
"Model 2"
);
});
QUnit.test("model_selector: with search matching some records on business name", async function (assert) {
await mountModelSelector(["model_1", "model_2"]);
await openAutocomplete();
await editInput(fixture, ".o-autocomplete--input", " 2");
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
assert.strictEqual(
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
"Model 2"
);
});
QUnit.test("model_selector: with search matching no record", async function (assert) {
await mountModelSelector(["model_1", "model_2"]);
await openAutocomplete("a random search query");
await editInput(fixture, ".o-autocomplete--input", "a random search query");
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
assert.strictEqual(
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
"No records"
);
});
QUnit.test("model_selector: select a model", async function (assert) {
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1", (selected) => {
assert.step("model selected");
assert.deepEqual(selected, {
label: "Model 2",
technical: "model_2",
});
});
await openAutocomplete();
await click(fixture.querySelector(".o_model_selector_model_2"));
assert.verifySteps(["model selected"]);
});
QUnit.test("model_selector: with an initial value", async function (assert) {
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1");
assert.equal(fixture.querySelector(".o-autocomplete--input").value, "Model 1");
});
......@@ -3,6 +3,7 @@
from . import test_db_manager
from . import test_health
from . import test_image
from . import test_ir_model
from . import test_js
from . import test_menu
from . import test_serving_base
......
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
from odoo.tests.common import new_test_user
@tagged("post_install", "-at_install")
class IrModelAccessTest(TransactionCase):
@classmethod
def setUpClass(cls):
super(IrModelAccessTest, cls).setUpClass()
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_public").id,
'perm_read': False,
})
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_portal").id,
'perm_read': True,
})
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_user").id,
'perm_read': True,
})
cls.portal_user = new_test_user(
cls.env, login="portalDude", groups="base.group_portal"
)
cls.public_user = new_test_user(
cls.env, login="publicDude", groups="base.group_public"
)
cls.spreadsheet_user = new_test_user(
cls.env, login="spreadsheetDude", groups="base.group_user"
)
def test_display_name_for(self):
# Internal User with access rights can access the business name
result = self.env['ir.model'].with_user(self.spreadsheet_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}])
# external user with access rights cannot access business name
result = self.env['ir.model'].with_user(self.portal_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "res.company", "model": "res.company"}])
# external user without access rights cannot access business name
result = self.env['ir.model'].with_user(self.public_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "res.company", "model": "res.company"}])
# admin has all rights
result = self.env['ir.model'].display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}])
# non existent model yields same result as a lack of access rights
result = self.env['ir.model'].display_name_for(["unexistent"])
self.assertEqual(result, [{"display_name": "unexistent", "model": "unexistent"}])
# non existent model comes after existent model
result = self.env['ir.model'].display_name_for(["res.company", "unexistent"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "unexistent", "model": "unexistent"}])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment