[FIX] website: properly filter website.page records on list/kanban view
Note: This fw-port commit cherry-picked and squashed commit [2] directly
as it was fixing this original commit before it had the chance to
be forward ported.
Since commit [1], which adapted the website pages list view to OWL, the
records listed on screen are filtered according to the active website
filter. However, the full list of records is still used behind the
scenes for all potential actions.
Steps to reproduce:
1. Go to the pages list view.
2. Select a specific website (if it's not already the case).
=> The total of records in the upper right corner does not match the
number of pages on that specific website.
3. Click on the "Select all" checkbox.
=> All the pages are selected, including those that do not appear on
screen.
This is because the records were just visually hidden with a `t-if`.
[1]: https://github.com/odoo/odoo/commit/940f4ee875332dafa1f379970a7683be6b3ee606
[2]: https://github.com/odoo/odoo/commit/db670f64f4c2190f1655f9077ea62885049a3c84
Courtesy of @robinlej and @detrouxdev
Related to task-3676124
opw-3554064
opw-3658648
closes odoo/odoo#149547
X-original-commit: 8d78a916dc8c0eca82e8a6a2c6f5541938709930
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
@@ -17,8 +17,12 @@ models which only purpose is to run tests.""",
|
||||
'website',
|
||||
'theme_default',
|
||||
],
|
||||
'demo': [
|
||||
'data/test_website_demo.xml',
|
||||
],
|
||||
'data': [
|
||||
'views/templates.xml',
|
||||
'views/test_model_views.xml',
|
||||
'data/test_website_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
<field name="perm_read" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- SOME DEFAULT TEST.MODEL RECORDS WITH DIFFERENT WEBSITE_ID -->
|
||||
<record id="test_model_generic" model="test.model">
|
||||
<field name="name">Test Model Generic</field>
|
||||
</record>
|
||||
<record id="test_model_website_1" model="test.model">
|
||||
<field name="name">Test Model Website 1</field>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
</record>
|
||||
|
||||
<!-- RECORDS FOR RESET VIEWS TESTS -->
|
||||
<record id="test_view" model="ir.ui.view">
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="test_model_website_2" model="test.model">
|
||||
<field name="name">Test Model Website 2</field>
|
||||
<field name="website_id" ref="website.website2"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -10,12 +10,17 @@ class TestModel(models.Model):
|
||||
_name = 'test.model'
|
||||
_inherit = [
|
||||
'website.seo.metadata',
|
||||
'website.published.mixin',
|
||||
'website.published.multi.mixin',
|
||||
'website.searchable.mixin',
|
||||
]
|
||||
_description = 'Website Model Test'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
# `cascade` is needed as there is demo data for this model which are bound
|
||||
# to website 2 (demo website). But some tests are unlinking the website 2,
|
||||
# which would fail if the `cascade` is not set. Note that the website 2 is
|
||||
# never set on any records in all other modules.
|
||||
website_id = fields.Many2one('website', string='Website', ondelete='cascade')
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add('test_website_page_manager', {
|
||||
test: true,
|
||||
url: '/web#action=test_website.action_test_model',
|
||||
steps: () => [
|
||||
// Part 1: check that the website filter is working
|
||||
{
|
||||
content: "Check that we see records from My Website",
|
||||
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Model Website 1') " +
|
||||
"~ .o_data_cell[name=website_id]:contains('My Website')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Check that there is only 2 records in the pager",
|
||||
trigger: ".o_pager .o_pager_value:contains('1-2')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Click on the 'Select all records' checkbox",
|
||||
trigger: "thead .o_list_record_selector",
|
||||
}, {
|
||||
content: "Check that there is only 2 records selected",
|
||||
trigger: ".o_list_selection_box:contains('2 selected')",
|
||||
run: () => null, // it's a check
|
||||
}, {
|
||||
content: "Click on the 'Select all records' checkbox again to unselect all records and see the search bar",
|
||||
trigger: "thead .o_list_record_selector",
|
||||
}, {
|
||||
content: "Click on the search options",
|
||||
trigger: ".o_searchview_dropdown_toggler",
|
||||
}, {
|
||||
content: "Select My Website 2",
|
||||
trigger: ".o_dropdown_container.o_website_menu > .dropdown-item:contains('My Website 2')",
|
||||
}, {
|
||||
// This step is just here to ensure there is more records than the 2
|
||||
// available on website 1, to ensure the test is actually testing something.
|
||||
content: "Check that we see records from My Website 2",
|
||||
trigger: ".o_list_table .o_data_row .o_data_cell[name=name]:contains('Test Model Website 2') " +
|
||||
"~ .o_data_cell[name=website_id]:contains('My Website 2')",
|
||||
run: () => null, // it's a check
|
||||
},
|
||||
// Part 2: ensure Kanban View is working / not crashing
|
||||
{
|
||||
content: "Click on Kanban View",
|
||||
trigger: '.o_cp_switch_buttons .o_kanban',
|
||||
}, {
|
||||
content: "Click on List View",
|
||||
extra_trigger: '.o_kanban_renderer',
|
||||
trigger: '.o_cp_switch_buttons .o_list',
|
||||
}, {
|
||||
content: "Wait for List View to be loaded",
|
||||
trigger: '.o_list_renderer',
|
||||
run: () => null, // it's a check
|
||||
}]
|
||||
});
|
||||
|
||||
registry.category("web_tour.tours").add('test_website_page_manager_js_class_bug', {
|
||||
test: true,
|
||||
url: '/web#action=test_website.action_test_model_js_class_bug',
|
||||
steps: () => [
|
||||
{
|
||||
content: "Click on Kanban View",
|
||||
trigger: '.o_cp_switch_buttons .o_kanban',
|
||||
}, {
|
||||
content: "Wait for Kanban View to be loaded",
|
||||
trigger: '.o_kanban_renderer',
|
||||
run: () => null, // it's a check
|
||||
}]
|
||||
});
|
||||
@@ -9,6 +9,7 @@ from . import test_image_upload_progress
|
||||
from . import test_is_multilang
|
||||
from . import test_media
|
||||
from . import test_multi_company
|
||||
from . import test_page_manager
|
||||
from . import test_page
|
||||
from . import test_performance
|
||||
from . import test_qweb
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsitePageManager(odoo.tests.HttpCase):
|
||||
def test_page_manager_test_model(self):
|
||||
if self.env['website'].search_count([]) == 1:
|
||||
website2 = self.env['website'].create({
|
||||
'name': 'My Website 2',
|
||||
'domain': '',
|
||||
'sequence': 20,
|
||||
})
|
||||
else:
|
||||
website2 = self.env['website'].search([], order='id desc', limit=1)
|
||||
self.env['test.model'].create({'name': 'Test Model Website 2', 'website_id': website2.id})
|
||||
self.assertTrue(
|
||||
len(set([t.website_id.id for t in self.env['test.model'].search([])])) >= 3,
|
||||
"There should at least be one record without website_id and one for 2 different websites",
|
||||
)
|
||||
self.start_tour('/web#action=test_website.action_test_model', 'test_website_page_manager', login="admin")
|
||||
# This second test is about ensuring that you can switch from a list
|
||||
# view which has no `website_pages_list` js_class to its kanban view
|
||||
self.start_tour('/web#action=test_website.action_test_model_js_class_bug', 'test_website_page_manager_js_class_bug', login="admin")
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- test.model views -->
|
||||
<record id="test_model_view_kanban" model="ir.ui.view">
|
||||
<field name="name">test.model.kanban</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban js_class="website_pages_kanban" class="o_kanban_mobile" action="open_website_url" type="object" sample="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click d-flex flex-column">
|
||||
<div class="row mb-auto">
|
||||
<strong class="col-8">
|
||||
<span class="o_text_overflow" t-esc="record.name.value"/>
|
||||
<div class="text-muted" t-if="record.website_id.value" groups="website.group_multi_website">
|
||||
<i class="fa fa-globe me-1" title="Website"/>
|
||||
<field name="website_id"/>
|
||||
</div>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="border-top mt-2 pt-2">
|
||||
<field name="is_published" widget="boolean_toggle"/>
|
||||
<t t-if="record.is_published.raw_value">Published</t>
|
||||
<t t-else="">Not Published</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_model_view_list" model="ir.ui.view">
|
||||
<field name="name">Test Model Pages Tree</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="website_pages_list" type="object" action="open_website_url" multi_edit="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_test_model" model="ir.actions.act_window">
|
||||
<field name="name">Test Model Pages</field>
|
||||
<field name="res_model">test.model</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_view_list')}),
|
||||
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_view_kanban')}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<!-- js_class bug records -->
|
||||
<record id="test_model_view_list_js_class_bug" model="ir.ui.view">
|
||||
<field name="name">Test Model Pages Tree js_class bug</field>
|
||||
<field name="model">test.model</field>
|
||||
<field name="priority">99</field>
|
||||
<!-- Omitting `website_pages_list` on purpose to test it does not crash -->
|
||||
<field name="arch" type="xml">
|
||||
<tree type="object" action="open_website_url" multi_edit="1">
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_test_model_js_class_bug" model="ir.actions.act_window">
|
||||
<field name="name">Test Model Pages js_class bug</field>
|
||||
<field name="res_model">test.model</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('test_model_view_list_js_class_bug')}),
|
||||
(0, 0, {'view_mode': 'kanban', 'view_id': ref('test_model_view_kanban')}),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1367,6 +1367,25 @@ class Website(models.Model):
|
||||
record['lastmod'] = page['write_date'].date()
|
||||
yield record
|
||||
|
||||
def get_website_page_ids(self):
|
||||
if not self.env.user.has_group('website.group_website_restricted_editor'):
|
||||
# Note that `website.pages` have `0,0,0,0` ACL rights by default for
|
||||
# everyone except for the website designer which receive `1,0,0,0`.
|
||||
# So the "Website/Site/Content/Pages" menu to reach the page manager
|
||||
# is not shown to the restricted users, as the action linked model
|
||||
# (website.page) can't be access. It's how the Odoo framework works.
|
||||
# Still, we let the restricted editor access this resource for
|
||||
# custos granting them read and/or write access on page.
|
||||
raise AccessError(_("Access Denied"))
|
||||
|
||||
domain = [('url', '!=', False)]
|
||||
if self:
|
||||
domain = AND([domain, self.website_domain()])
|
||||
pages = self.env['website.page'].sudo().search(domain)
|
||||
if self:
|
||||
pages = pages._get_most_specific_pages()
|
||||
return pages.ids
|
||||
|
||||
def _get_website_pages(self, domain=None, order='name', limit=None):
|
||||
if domain is None:
|
||||
domain = []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {PageControllerMixin, PageRendererMixin} from "./page_views_mixin";
|
||||
import {PageSearchModel} from "./page_search_model";
|
||||
import {registry} from '@web/core/registry';
|
||||
import {kanbanView} from "@web/views/kanban/kanban_view";
|
||||
import {CheckboxItem} from "@web/core/dropdown/checkbox_item";
|
||||
@@ -19,6 +20,7 @@ PageKanbanController.components = {
|
||||
CheckboxItem,
|
||||
};
|
||||
|
||||
// TODO master: remove `PageRendererMixin` extend, props override and template
|
||||
export class PageKanbanRenderer extends PageRendererMixin(kanbanView.Renderer) {}
|
||||
PageKanbanRenderer.props = [
|
||||
...kanbanView.Renderer.props,
|
||||
@@ -30,6 +32,7 @@ export const PageKanbanView = {
|
||||
...kanbanView,
|
||||
Renderer: PageKanbanRenderer,
|
||||
Controller: PageKanbanController,
|
||||
SearchModel: PageSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("website_pages_kanban", PageKanbanView);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- TODO master: remove this template -->
|
||||
<t t-name="website.PageKanbanRenderer" t-inherit="web.KanbanRenderer">
|
||||
<xpath expr="//t[@t-if='groupOrRecord.group']//KanbanRecord" position="attributes">
|
||||
<attribute name="t-if">recordFilter(record, props.list.records)</attribute>
|
||||
<attribute name="t-if"/><!-- stable way to cancel the above `t-if` -->
|
||||
</xpath>
|
||||
<xpath expr="//div/t/t[@t-else='']/KanbanRecord" position="attributes">
|
||||
<attribute name="t-if">recordFilter(groupOrRecord.record, props.list.records)</attribute>
|
||||
<attribute name="t-if"/><!-- stable way to cancel the above `t-if` -->
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<!-- TODO master: remove this xpath -->
|
||||
<t t-name="website.PageKanbanView" t-inherit="web.KanbanView">
|
||||
<xpath expr="//t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="activeWebsite">state.activeWebsite</attribute>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import {PageControllerMixin, PageRendererMixin} from "./page_views_mixin";
|
||||
import {PageSearchModel} from "./page_search_model";
|
||||
import {registry} from '@web/core/registry';
|
||||
import {listView} from '@web/views/list/list_view';
|
||||
import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
@@ -94,6 +95,7 @@ PageListController.components = {
|
||||
CheckboxItem,
|
||||
};
|
||||
|
||||
// TODO master: remove `PageRendererMixin` extend and props override
|
||||
export class PageListRenderer extends PageRendererMixin(listView.Renderer) {}
|
||||
PageListRenderer.props = [
|
||||
...listView.Renderer.props,
|
||||
@@ -105,6 +107,7 @@ export const PageListView = {
|
||||
...listView,
|
||||
Renderer: PageListRenderer,
|
||||
Controller: PageListController,
|
||||
SearchModel: PageSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("website_pages_list", PageListView);
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
<i class="fa fa-fw fa-home fa-lg" title="Home page of the current website"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<!-- TODO master: remove this xpath -->
|
||||
<xpath expr="//tr[@class='o_data_row']" position="attributes">
|
||||
<attribute name="t-if">recordFilter(record, list.records)</attribute>
|
||||
<attribute name="t-if"/><!-- stable way to cancel the above `t-if` -->
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="website.PageListView" t-inherit="web.ListView">
|
||||
<!-- TODO master: remove this xpath -->
|
||||
<xpath expr="//t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="activeWebsite">state.activeWebsite</attribute>
|
||||
</xpath>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Domain } from '@web/core/domain';
|
||||
import { SearchModel } from '@web/search/search_model';
|
||||
import { onWillStart, useState } from "@odoo/owl";
|
||||
|
||||
export class PageSearchModel extends SearchModel {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.website = useService('website');
|
||||
|
||||
this.rpc = useService('rpc');
|
||||
this.pagesState = useState({
|
||||
websiteDomain: false,
|
||||
});
|
||||
onWillStart(async () => {
|
||||
// Before the searchModel performs its DB search call, append the
|
||||
// website domain to the search domain.
|
||||
await this.website.fetchWebsites();
|
||||
const website = this.website.currentWebsite || this.website.websites[0];
|
||||
this.notifyWebsiteChange(website.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
exportState() {
|
||||
const state = super.exportState();
|
||||
state.websiteDomain = this.pagesState.websiteDomain;
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_importState(state) {
|
||||
super._importState(...arguments);
|
||||
|
||||
if (state.websiteDomain) {
|
||||
this.pagesState.websiteDomain = state.websiteDomain;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getDomain(params = {}) {
|
||||
let domain = super._getDomain(params);
|
||||
if (!this.pagesState.websiteDomain) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
domain = Domain.and([
|
||||
domain,
|
||||
this.pagesState.websiteDomain,
|
||||
]);
|
||||
return params.raw ? domain : domain.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the website domain state and notifies the change. That domain
|
||||
* state will be appended to the base SearchModel domain.
|
||||
*
|
||||
* @param {number} websiteId - The ID of the website.
|
||||
*/
|
||||
async notifyWebsiteChange(websiteId) {
|
||||
let websiteDomain = [];
|
||||
if (websiteId) {
|
||||
if (this.resModel === 'website.page') {
|
||||
// In case of `website.page`, we can't find the website pages
|
||||
// with a regular domain (because we need to filter duplicates).
|
||||
const pageIds = await this.orm.call(
|
||||
"website",
|
||||
"get_website_page_ids",
|
||||
[websiteId],
|
||||
);
|
||||
websiteDomain = [['id', 'in', pageIds]];
|
||||
} else {
|
||||
websiteDomain = [['website_id', 'in', [false, websiteId]]];
|
||||
}
|
||||
}
|
||||
this.pagesState.websiteDomain = websiteDomain;
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,11 @@ export const PageControllerMixin = (component) => class extends component {
|
||||
|
||||
onSelectWebsite(website) {
|
||||
this.state.activeWebsite = website;
|
||||
this.env.searchModel.notifyWebsiteChange(website.id);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Remove in master, records are not hidden through `t-if` anymore.
|
||||
export const PageRendererMixin = (component) => class extends component {
|
||||
/**
|
||||
* The goal here is to tweak the renderer to display records following some
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
<field name="write_date" optional="hide"/>
|
||||
<field name="track" optional="hide"/>
|
||||
|
||||
<!-- website_id should be shown only in multi website environment
|
||||
<!-- TODO: Remove in master, `PageRendererMixin` not needed anymore
|
||||
website_id should be shown only in multi website environment
|
||||
when the group is enabled, but we need the field to be there all the
|
||||
time for `PageRendererMixin`'s `recordFilter' to be able to filter
|
||||
correctly. -->
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<field name="model">forum.post</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="false" type="object" action="go_to_website">
|
||||
<tree js_class="website_pages_list" create="false" type="object" action="go_to_website" multi_edit="1">
|
||||
<field name="active" column_invisible="True"/>
|
||||
<field name="name"/>
|
||||
<field name="website_url"/>
|
||||
|
||||
Reference in New Issue
Block a user