From 2ef7e788263b4742a8e0788f47673b7b31da3726 Mon Sep 17 00:00:00 2001 From: xO-Tx Date: Wed, 22 Jun 2022 14:03:26 +0200 Subject: [PATCH] [IMP] website: add page properties dialog The goal of this commit is to add the 'Page Properties' dialog using the 'website.page' form view. The PagePropertiesDialogWrapper was updated to work on pages listView (to clone/remove pages). A boolean field 'is_in_menu' was added to control if page is in menu on page properties dialog. This should be refactored later as a wizard. See merge commit for more information. task-2687506 --- addons/website/__manifest__.py | 2 + addons/website/models/website.py | 21 +- addons/website/models/website_page.py | 154 ++++------ .../src/components/dialog/page_properties.js | 269 ++++++++++++++++++ .../components/dialog/page_properties.scss | 6 + .../src/components/dialog/page_properties.xml | 230 ++++----------- .../static/src/components/fields/fields.js | 95 +++++++ .../static/src/components/fields/fields.xml | 35 +++ .../static/src/components/navbar/navbar.js | 16 +- .../static/src/components/navbar/navbar.xml | 1 + addons/website/views/website_views.xml | 48 ++++ addons/website_blog/models/website.py | 6 +- 12 files changed, 609 insertions(+), 274 deletions(-) create mode 100644 addons/website/static/src/components/dialog/page_properties.js create mode 100644 addons/website/static/src/components/dialog/page_properties.scss create mode 100644 addons/website/static/src/components/fields/fields.js create mode 100644 addons/website/static/src/components/fields/fields.xml diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index 6071e4584a3..cc83eacb7b3 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -176,6 +176,7 @@ 'website/static/src/components/ace_editor/ace_editor.scss', 'website/static/src/components/dialog/*.js', 'website/static/src/components/dialog/*.scss', + 'website/static/src/components/fields/fields.js', 'website/static/src/components/switch/switch.js', 'website/static/src/components/switch/switch.scss', 'website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js', @@ -278,6 +279,7 @@ 'website/static/src/xml/website_widget.xml', 'website/static/src/xml/theme_preview.xml', 'website/static/src/components/dialog/*', + 'website/static/src/components/fields/fields.xml', 'website/static/src/components/editor/editor.xml', 'website/static/src/client_actions/*/*.xml', 'website/static/src/systray_items/*.xml', diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 0552b19ee8f..3f78bcbd838 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -13,6 +13,7 @@ from psycopg2 import sql from werkzeug import urls from werkzeug.datastructures import OrderedMultiDict from werkzeug.exceptions import NotFound +from markupsafe import Markup from odoo import api, fields, models, tools, http, release, registry from odoo.addons.http_routing.models.ir_http import RequestUID, slugify, _guess_mimetype, url_for @@ -722,7 +723,8 @@ class Website(models.Model): # we only want a unique_path for website specific. # we need to be able to have /url for website=False, and /url for website=1 # in case of duplicate, page manager will allow you to manage this case - domain_static = [('website_id', '=', self.get_current_website().id)] # .website_domain() + website_id = self.env.context.get('website_id', False) or self.get_current_website().id + domain_static = [('website_id', '=', website_id)] # .website_domain() page_temp = page_url while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static): inc += 1 @@ -761,6 +763,9 @@ class Website(models.Model): key_copy = string inc = 0 domain_static = self.get_current_website().website_domain() + website_id = self.env.context.get('website_id', False) + if website_id: + domain_static = [('website_id', 'in', (False, website_id))] while self.env['website.page'].with_context(active_test=False).sudo().search([('key', '=', key_copy)] + domain_static): inc += 1 key_copy = string + (inc and "-%s" % inc or "") @@ -779,7 +784,7 @@ class Website(models.Model): return dependencies page = self.env['website.page'].browse(int(page_id)) - website = self.env['website'].browse(self._context.get('website_id')) + website = page.website_id or self.get_current_website() url = page.url # search for website_page with link @@ -792,7 +797,7 @@ class Website(models.Model): for page in pages: dependencies.setdefault(page_key, []) dependencies[page_key].append({ - 'text': _('Page %s contains a link to this page', page.url), + 'content': Markup(_("Page %s contains a link to this page")) % page.url, 'item': page.name, 'link': page.url, }) @@ -807,7 +812,7 @@ class Website(models.Model): for view in views: dependencies.setdefault(view_key, []) dependencies[view_key].append({ - 'text': _('Template %s (id:%s) contains a link to this page') % (view.key or view.name, view.id), + 'content': Markup(_('Template %s (id:%s) contains a link to this page')) % (view.key or view.name, view.id), 'link': '/web#id=%s&view_type=form&model=ir.ui.view' % view.id, 'item': _('%s (id:%s)') % (view.key or view.name, view.id), }) @@ -820,7 +825,7 @@ class Website(models.Model): menu_key = _('Menus') for menu in menus: dependencies.setdefault(menu_key, []).append({ - 'text': _('This page is in the menu %s', menu.name), + 'content': Markup(_('This page is in the menu %s')) % menu.name, 'link': '/web#id=%s&view_type=form&model=website.menu' % menu.id, 'item': menu.name, }) @@ -840,7 +845,7 @@ class Website(models.Model): return dependencies page = self.env['website.page'].browse(int(page_id)) - website = self.env['website'].browse(self._context.get('website_id')) + website = page.website_id or self.get_current_website() key = page.key # search for website_page with link @@ -856,7 +861,7 @@ class Website(models.Model): for p in pages: dependencies.setdefault(page_key, []) dependencies[page_key].append({ - 'text': _('Page %s is calling this file', p.url), + 'content': Markup(_('Page %s is calling this file')) % p.url, 'item': p.name, 'link': p.url, }) @@ -874,7 +879,7 @@ class Website(models.Model): for view in views: dependencies.setdefault(view_key, []) dependencies[view_key].append({ - 'text': _('Template %s (id:%s) is calling this file') % (view.key or view.name, view.id), + 'content': Markup(_('Template %s (id:%s) is calling this file')) % (view.key or view.name, view.id), 'item': _('%s (id:%s)') % (view.key or view.name, view.id), 'link': '/web#id=%s&view_type=form&model=ir.ui.view' % view.id, }) diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 539022dfca7..7633ad0c6fc 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -26,8 +26,10 @@ class Page(models.Model): view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade") website_indexed = fields.Boolean('Is Indexed', default=True) date_publish = fields.Datetime('Publishing Date') - # This is needed to be able to display if page is a menu in /website/pages menu_ids = fields.One2many('website.menu', 'page_id', 'Related Menus') + # This is needed to be able to control if page is a menu in page properties. + # TODO this should be reviewed entirely so that we use a transient model. + is_in_menu = fields.Boolean(compute='_compute_website_menu', inverse='_inverse_website_menu') is_homepage = fields.Boolean(compute='_compute_homepage', inverse='_set_homepage', string='Homepage') is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') @@ -61,6 +63,26 @@ class Page(models.Model): not page.date_publish or page.date_publish < fields.Datetime.now() ) + @api.depends('menu_ids') + def _compute_website_menu(self): + for page in self: + page.is_in_menu = bool(page.menu_ids) + + def _inverse_website_menu(self): + for page in self: + if page.is_in_menu: + if not page.menu_ids: + self.env['website.menu'].create({ + 'name': page.name, + 'url': page.url, + 'page_id': page.id, + 'parent_id': page.website_id.menu_id.id, + 'website_id': page.website_id.id, + }) + elif page.menu_ids: + # If the page is no longer in menu, we should remove its website_menu + page.menu_ids.unlink() + def _get_most_specific_pages(self): ''' Returns the most specific pages in self. ''' ids = [] @@ -72,93 +94,6 @@ class Page(models.Model): previous_page = page return self.filtered(lambda page: page.id in ids) - def get_page_properties(self): - self.ensure_one() - res = self.read([ - 'id', 'view_id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', - 'menu_ids', 'is_homepage', 'website_id', 'visibility', 'groups_id' - ])[0] - if not res['groups_id']: - res['group_id'] = self.env.ref('base.group_user').name_get()[0] - elif len(res['groups_id']) == 1: - res['group_id'] = self.env['res.groups'].browse(res['groups_id']).name_get()[0] - del res['groups_id'] - - res['visibility_password'] = res['visibility'] == 'password' and self.visibility_password_display or '' - return res - - @api.model - def save_page_info(self, website_id, data): - website = self.env['website'].browse(website_id) - page = self.browse(int(data['id'])) - - # If URL has been edited, slug it - original_url = page.url - url = data['url'] - if not url.startswith('/'): - url = '/' + url - if page.url != url: - url = '/' + slugify(url, max_length=1024, path=True) - url = self.env['website'].get_unique_path(url) - - # If name has changed, check for key uniqueness - if page.name != data['name']: - page_key = self.env['website'].get_unique_key(slugify(data['name'])) - else: - page_key = page.key - - menu = self.env['website.menu'].search([('page_id', '=', int(data['id']))]) - if not data['is_menu']: - # If the page is no longer in menu, we should remove its website_menu - if menu: - menu.unlink() - else: - # The page is now a menu, check if has already one - if menu: - menu.write({'url': url}) - else: - self.env['website.menu'].create({ - 'name': data['name'], - 'url': url, - 'page_id': data['id'], - 'parent_id': website.menu_id.id, - 'website_id': website.id, - }) - - # Edits via the page manager shouldn't trigger the COW - # mechanism and generate new pages. The user manages page - # visibility manually with is_published here. - w_vals = { - 'key': page_key, - 'name': data['name'], - 'url': url, - 'is_published': data['website_published'], - 'website_indexed': data['website_indexed'], - 'date_publish': data['date_publish'] or None, - 'is_homepage': data['is_homepage'], - 'visibility': data['visibility'], - } - if page.visibility == 'restricted_group' and data['visibility'] != "restricted_group": - w_vals['groups_id'] = False - elif 'group_id' in data: - w_vals['groups_id'] = [data['group_id']] - if 'visibility_pwd' in data: - w_vals['visibility_password_display'] = data['visibility_pwd'] or '' - - page.with_context(no_cow=True).write(w_vals) - - # Create redirect if needed - if data['create_redirect']: - self.env['website.rewrite'].create({ - 'name': data['name'], - 'redirect_type': data['redirect_type'], - 'url_from': original_url, - 'url_to': url, - 'website_id': website.id, - }) - - return url - @api.returns('self', lambda value: value.id) def copy(self, default=None): if default: @@ -178,8 +113,7 @@ class Page(models.Model): page = self.browse(int(page_id)) copy_param = dict(name=page_name or page.name, website_id=self.env['website'].get_current_website().id) if page_name: - page_url = '/' + slugify(page_name, max_length=1024, path=True) - copy_param['url'] = self.env['website'].get_unique_path(page_url) + copy_param['url'] = self.get_valid_page_url(page_name) new_page = page.copy(copy_param) # Should not clone menu if the page was cloned from one website to another @@ -209,8 +143,41 @@ class Page(models.Model): return super(Page, self).unlink() def write(self, vals): - if 'url' in vals and not vals['url'].startswith('/'): - vals['url'] = '/' + vals['url'] + for page in self: + website_id = False + if vals.get('website_id') or page.website_id: + website_id = vals.get('website_id') or page.website_id.id + + # If URL has been edited, slug it + if 'url' in vals: + url = vals['url'] + redirect_old_url = redirect_type = None + # TODO This should be done another way after the backend/frontend merge + if isinstance(url, dict): + redirect_old_url = url.get('redirect_old_url') + redirect_type = url.get('redirect_type') + url = url.get('url') + if not url.startswith('/'): + url = '/' + url + if page.url != url: + url = self.get_valid_page_url(url, website_id) + page.menu_ids.write({'url': url}) + if redirect_old_url: + self.env['website.rewrite'].create({ + 'name': vals.get('name') or page.name, + 'redirect_type': redirect_type, + 'url_from': page.url, + 'url_to': url, + 'website_id': website_id, + }) + vals['url'] = url + + # If name has changed, check for key uniqueness + if 'name' in vals and page.name != vals['name']: + vals['key'] = self.env['website'].with_context(website_id=website_id).get_unique_key(slugify(vals['name'])) + if 'visibility' in vals: + if vals['visibility'] != 'restricted_group': + vals['groups_id'] = False self.clear_caches() # write on page == write on view that invalid cache return super(Page, self).write(vals) @@ -306,6 +273,9 @@ class Page(models.Model): results = results.filtered(lambda result: filter_page(search, result, results)) return results, count + def get_valid_page_url(self, page_url, website_id=False): + url = '/' + slugify(page_url, max_length=1024, path=True) + return self.env['website'].with_context(website_id=website_id).get_unique_path(url) # this is just a dummy function to be used as ormcache key def _cached_response(): diff --git a/addons/website/static/src/components/dialog/page_properties.js b/addons/website/static/src/components/dialog/page_properties.js new file mode 100644 index 00000000000..2144c4bda96 --- /dev/null +++ b/addons/website/static/src/components/dialog/page_properties.js @@ -0,0 +1,269 @@ +/** @odoo-module **/ + +import {CheckBox} from '@web/core/checkbox/checkbox'; +import {useService} from "@web/core/utils/hooks"; +import {useWowlService} from '@web/legacy/utils'; +import {WebsiteDialog} from './dialog'; +import {FormViewDialog} from 'web.view_dialogs'; +import {standaloneAdapter} from 'web.OwlCompatibility'; +import {qweb} from 'web.core'; + +const {Component, onWillStart, useState, xml, useRef, markup, onWillRender} = owl; + +export class PageDependencies extends Component { + setup() { + super.setup(); + try { + this.orm = useService('orm'); + } catch { + // We are in a legacy environment. + // TODO check with framework team to know if this is really needed. + this.orm = useWowlService('orm'); + } + + this.dependencies = {}; + this.depText = '...'; + this.action = useRef('action'); + + onWillStart(() => this.onWillStart()); + } + + async onWillStart() { + this.dependencies = await this.orm.call( + 'website', + this.props.type === 'key' ? + 'page_search_key_dependencies' : + 'page_search_dependencies', + [this.props.pageId], + ); + if (this.props.mode === 'popover') { + this.depText = Object.entries(this.dependencies) + .map(dependency => `${dependency[1].length} ${dependency[0].toLowerCase()}`) + .join(', '); + } else { + for (const key of Object.keys(this.dependencies)) { + this.dependencies[key] = this.dependencies[key].map(item => { + // TODO probably need to refactor this feature so that the + // client side is in charge of what the sentences look like + // (not server-side HTML). + item.contentToDisplay = markup(item.content); + return item; + }); + } + } + } + + showDependencies() { + $(this.action.el).popover({ + title: this.env._t('Dependencies'), + boundary: 'viewport', + placement: 'right', + trigger: 'focus', + content: qweb.render('website.PageDependencies.Tooltip', {dependencies: this.dependencies}), + }).popover('toggle'); + } +} +PageDependencies.popoverTemplate = xml` +