From d4879ed7fe45861a56cd73c211e3deb4ed81e43f Mon Sep 17 00:00:00 2001 From: MonsieurB Date: Tue, 22 Nov 2016 16:13:12 +0100 Subject: [PATCH 01/77] Gestion d url dans product & category --- base_url/README.rst | 45 +++++++++ base_url/__init__.py | 2 + base_url/__openerp__.py | 28 ++++++ base_url/models/__init__.py | 6 ++ base_url/models/base_url.py | 135 ++++++++++++++++++++++++++ base_url/security/ir.model.access.csv | 3 + base_url/views/url_view.xml | 39 ++++++++ 7 files changed, 258 insertions(+) create mode 100644 base_url/README.rst create mode 100644 base_url/__init__.py create mode 100644 base_url/__openerp__.py create mode 100644 base_url/models/__init__.py create mode 100644 base_url/models/base_url.py create mode 100644 base_url/security/ir.model.access.csv create mode 100644 base_url/views/url_view.xml diff --git a/base_url/README.rst b/base_url/README.rst new file mode 100644 index 00000000000..fcf11bae593 --- /dev/null +++ b/base_url/README.rst @@ -0,0 +1,45 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============== + Base URL +============== + +This module helps to set url list and history of it + + +Installation +============ + +To install this module, you need to: + +#. (root) pip install slugify + + + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Monsieur B +* Sebastien Beau + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/base_url/__init__.py b/base_url/__init__.py new file mode 100644 index 00000000000..a0fdc10fe11 --- /dev/null +++ b/base_url/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/base_url/__openerp__.py b/base_url/__openerp__.py new file mode 100644 index 00000000000..5af4be6a603 --- /dev/null +++ b/base_url/__openerp__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Rewrite url Base module for OpenERP +# Copyright (C) 2013 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Base Url', + 'version': '8.0.0.1.0', + 'category': 'tools', + 'license': 'AGPL-3', + 'summary': 'keep history of url for products & categories ', + + 'author': 'Akretion', + 'website': 'http://www.akretion.com/fr', + # any module necessary for this one to work correctly + 'depends': [], + 'external_dependencies': { + 'python': ['slugify'], + }, + 'data': ['views/url_view.xml', + 'security/ir.model.access.csv'], + + 'demo': [], + + 'url': '', + 'installable': True, +} diff --git a/base_url/models/__init__.py b/base_url/models/__init__.py new file mode 100644 index 00000000000..b69f282e0bf --- /dev/null +++ b/base_url/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import base_url diff --git a/base_url/models/base_url.py b/base_url/models/base_url.py new file mode 100644 index 00000000000..ec821150904 --- /dev/null +++ b/base_url/models/base_url.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, api, fields, _ +import logging +from openerp.exceptions import Warning as UserError +_logger = logging.getLogger(__name__) +try: + from slugify import slugify +except ImportError: + _logger.debug('Cannot `import slugify`.') + + +class UrlUrl(models.Model): + + _name = "url.url" + + url_key = fields.Char(string="Url Id") + model_id = fields.Reference(selection='_reference_models', + help="The id of product or category.", + readonly=True, string="Model") + redirect = fields.Boolean('Redirect', help="this url is active or has" + " to redirect to an other") + + _sql_constraints = [('urlurl unique key', + 'unique(url_key)', + 'Already exists in database')] + + @api.model + def _reference_models(self): + return [] + + @api.multi + def _get_object(self, url): + """ + :return: return object attach to the url + """ + return self.search([('url_key', "=", url)]).model_id + + +class AbstractUrl(models.AbstractModel): + _name = 'abstract.url' + + url_key = fields.Char(compute='_compute_url', inverse='_inverse_set_url', + string='Url Key', help='parts of url to get it') + redirect_url_key_ids = fields.One2many(compute='_compute_redirect_url', + comodel_name='url.url') + + def _get_model_id_reference(self): + return "%s,%s" % (self._name, self.id) + + def _prepare_url(self): + return slugify(self.name) + + @api.multi + def _inverse_set_url(self): + """ + backup old url + + 1 find url redirect true and same model_id + if other model id refuse + 2 if exists set to False + + 3 write the new one + """ + + model_ref = self._get_model_id_reference() + + # key exist ? + exist_url = self.env['url.url'].search( + [('url_key', '=', self.url_key)]) + + if exist_url: + # existing key in wich object ? + exist_url.ensure_one() + if model_ref != exist_url.model_id._get_model_id_reference(): + # existing key for other model + raise UserError( + _("Url_key already exist in other model" + " %s" % (exist_url.model_id.name))) + else: # existing key for same object toggle redirect to False + exist_url.redirect = False + else: + # no existing key creating one if not empty + if self.url_key: + vals = {'url_key': self.url_key, + 'model_id': model_ref, + 'redirect': False} + self.env['url.url'].create(vals) + # other url of object set redirect to True + redirect_urls = self.env['url.url'].search([ + ('model_id', '=', model_ref), + ('url_key', '!=', self.url_key), + ('redirect', '=', False)]) + + for exist_url in redirect_urls: + exist_url.redirect = True + + @api.multi + def _compute_url(self): + for record in self: + model_ref = record._get_model_id_reference() + _logger.info("used model : %s ", model_ref) + url = record.env["url.url"].search([('model_id', '=', model_ref), + ('redirect', '=', False)]) + record.url_key = url.url_key + + @api.multi + def _compute_redirect_url(self): + for record in self: + model_ref = record._get_model_id_reference() + record.redirect_url_key_ids = record.env["url.url"].search( + [('model_id', '=', model_ref), ('redirect', '=', True)]) + + @api.onchange('name') + def on_name_change(self): + for record in self: + if record.name: + record.url_key = record._prepare_url() + + @api.onchange('url_key') + def on_url_key_change(self): + + for record in self: + if record.url_key: + url = slugify(record.url_key) + if url != record.url_key: + record.url_key = url + return {'value': {}, + 'warning': { + 'title': 'Adapt text rules', + 'message': 'it will be adapted to %s' % + (url)}} diff --git a/base_url/security/ir.model.access.csv b/base_url/security/ir.model.access.csv new file mode 100644 index 00000000000..339818b4420 --- /dev/null +++ b/base_url/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_url_url,access_url_url,model_url_url,,1,0,0,0 + diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml new file mode 100644 index 00000000000..0449dcbdf4a --- /dev/null +++ b/base_url/views/url_view.xml @@ -0,0 +1,39 @@ + + + + + url.url + +
+ + + + + + + +
+
+
+ + + url.url + + + + + + + + + + + url list Viewer + url.url + tree,form + + + + +
+
From c89691d6bbcdd95518a298ed528c348a2669b0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Wed, 25 Jan 2017 18:07:30 +0100 Subject: [PATCH 02/77] [REF] refactor base_url module --- base_url/models/base_url.py | 194 +++++++++++++++++++++++------------- base_url/views/url_view.xml | 4 +- 2 files changed, 127 insertions(+), 71 deletions(-) diff --git a/base_url/models/base_url.py b/base_url/models/base_url.py index ec821150904..f469155ca72 100644 --- a/base_url/models/base_url.py +++ b/base_url/models/base_url.py @@ -13,21 +13,45 @@ _logger.debug('Cannot `import slugify`.') +def get_model_ref(record): + return "%s,%s" % (record._name, record.id) + + class UrlUrl(models.Model): _name = "url.url" - url_key = fields.Char(string="Url Id") - model_id = fields.Reference(selection='_reference_models', - help="The id of product or category.", - readonly=True, string="Model") - redirect = fields.Boolean('Redirect', help="this url is active or has" - " to redirect to an other") - - _sql_constraints = [('urlurl unique key', - 'unique(url_key)', + url_key = fields.Char(required=True) + model_id = fields.Reference( + selection=[], + help="The id of content linked to the url.", + readonly=True, + string="Model", + required=True) + redirect = fields.Boolean( + help="If tick this url is a redirection to the new url") + backend_id = fields.Reference( + selection=[], + compute='_compute_related_fields', + store=True, + help="Backend linked to this URL", + string="Backend") + lang_id = fields.Many2one( + 'res.lang', + 'Lang', + compute='_compute_related_fields', + store=True) + + _sql_constraints = [('unique_key_per_backend_per_lang', + 'unique(url_key, backend_id, lang_id)', 'Already exists in database')] + @api.depends('model_id') + def _compute_related_fields(self): + for record in self: + record.backend_id = get_model_ref(record.model_id.backend_id) + record.lang_id = record.model_id.lang_id + @api.model def _reference_models(self): return [] @@ -43,19 +67,32 @@ def _get_object(self, url): class AbstractUrl(models.AbstractModel): _name = 'abstract.url' - url_key = fields.Char(compute='_compute_url', inverse='_inverse_set_url', - string='Url Key', help='parts of url to get it') - redirect_url_key_ids = fields.One2many(compute='_compute_redirect_url', - comodel_name='url.url') - - def _get_model_id_reference(self): - return "%s,%s" % (self._name, self.id) - - def _prepare_url(self): - return slugify(self.name) + url_builder = fields.Selection( + selection=[ + ('auto', 'Automatic'), + ('manual', 'Manual'), + ], default='auto') + manual_url_key = fields.Char() + url_key = fields.Char( + compute='_compute_url', + store=True) + redirect_url_key_ids = fields.One2many( + compute='_compute_redirect_url', + comodel_name='url.url') + + def _build_url_key(self): + return slugify(self.record_id.with_context( + lang=self.lang_id.code).name) + + def _prepare_url(self, url_key): + return { + 'url_key': url_key, + 'redirect': False, + 'model_id': get_model_ref(self), + } @api.multi - def _inverse_set_url(self): + def set_url(self, url_key): """ backup old url @@ -65,71 +102,88 @@ def _inverse_set_url(self): 3 write the new one """ - - model_ref = self._get_model_id_reference() - - # key exist ? - exist_url = self.env['url.url'].search( - [('url_key', '=', self.url_key)]) - - if exist_url: - # existing key in wich object ? - exist_url.ensure_one() - if model_ref != exist_url.model_id._get_model_id_reference(): - # existing key for other model + self.ensure_one() + + existing_url = self.env['url.url'].search([ + ('url_key', '=', url_key), + ('backend_id', '=', get_model_ref(self.backend_id)), + ('lang_id', '=', self.lang_id.id), + ]) + if existing_url: + if self != existing_url.model_id: raise UserError( _("Url_key already exist in other model" - " %s" % (exist_url.model_id.name))) - else: # existing key for same object toggle redirect to False - exist_url.redirect = False + "\n- name: %s\n - id: %s\n" + "- url_key: %s\n - url_key_id %s") % ( + existing_url.model_id.name, + existing_url.model_id.record_id.id, + existing_url.url_key, + existing_url.id, + )) + else: + existing_url.write({'redirect': False}) else: # no existing key creating one if not empty - if self.url_key: - vals = {'url_key': self.url_key, - 'model_id': model_ref, - 'redirect': False} - self.env['url.url'].create(vals) - # other url of object set redirect to True + self.env['url.url'].create(self._prepare_url(url_key)) + # other url of object set redirect to True redirect_urls = self.env['url.url'].search([ - ('model_id', '=', model_ref), - ('url_key', '!=', self.url_key), + ('model_id', '=', get_model_ref(self)), + ('url_key', '!=', url_key), ('redirect', '=', False)]) - - for exist_url in redirect_urls: - exist_url.redirect = True + redirect_urls.write({'redirect': True}) @api.multi + @api.depends('url_builder') def _compute_url(self): + # We need a clear env before call the set_url method + # indeed set_url will call a create that will trigger + # the recomputation of computed field + # This avoid useless recomputation of field + # TODO we should see with odoo how we can improve the ORM + todo = self.env.all.todo + self.env.all.todo = {} for record in self: - model_ref = record._get_model_id_reference() - _logger.info("used model : %s ", model_ref) - url = record.env["url.url"].search([('model_id', '=', model_ref), - ('redirect', '=', False)]) - record.url_key = url.url_key + if type(record.record_id.id) == models.NewId\ + or type(record.id) == models.NewId: + # Do not update field value on onchange + # as with_context is broken on NewId + continue + if record.url_builder == 'manual': + new_url = record.manual_url_key + else: + new_url = record._build_url_key() + if new_url: + record.set_url(new_url) + record.url_key = new_url + self.env.all.todo = todo @api.multi def _compute_redirect_url(self): for record in self: - model_ref = record._get_model_id_reference() - record.redirect_url_key_ids = record.env["url.url"].search( - [('model_id', '=', model_ref), ('redirect', '=', True)]) + record.redirect_url_key_ids = record.env["url.url"].search([ + ('model_id', '=', get_model_ref(record)), + ('redirect', '=', True), + ]) - @api.onchange('name') - def on_name_change(self): - for record in self: - if record.name: - record.url_key = record._prepare_url() - - @api.onchange('url_key') + @api.onchange('manual_url_key') def on_url_key_change(self): + self.ensure_one() + if self.manual_url_key: + url = slugify(self.manual_url_key) + if url != self.manual_url_key: + self.manual_url_key = url + return { + 'warning': { + 'title': 'Adapt text rules', + 'message': 'it will be adapted to %s' % url, + }} + @api.multi + def unlink(self): for record in self: - if record.url_key: - url = slugify(record.url_key) - if url != record.url_key: - record.url_key = url - return {'value': {}, - 'warning': { - 'title': 'Adapt text rules', - 'message': 'it will be adapted to %s' % - (url)}} + # TODO we should propose to redirect the old url + urls = record.env["url.url"].search([ + ('model_id', '=', get_model_ref(record)), + ]) + urls.unlink() + return super(AbstractUrl, self).unlink() diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index 0449dcbdf4a..079aed3a5e6 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -7,8 +7,10 @@
+ + - + From 61aa45a0d592c6c11e8b2536f801bcc201033dd8 Mon Sep 17 00:00:00 2001 From: David Beal Date: Mon, 2 Oct 2017 12:12:51 +0200 Subject: [PATCH 03/77] [MIG] PORT base_url to 10.0 --- base_url/{__openerp__.py => __manifest__.py} | 3 +-- base_url/models/__init__.py | 3 --- base_url/models/base_url.py | 11 ++++------- base_url/views/url_view.xml | 12 ++++++------ 4 files changed, 11 insertions(+), 18 deletions(-) rename base_url/{__openerp__.py => __manifest__.py} (96%) diff --git a/base_url/__openerp__.py b/base_url/__manifest__.py similarity index 96% rename from base_url/__openerp__.py rename to base_url/__manifest__.py index 5af4be6a603..a304b2d0a6e 100644 --- a/base_url/__openerp__.py +++ b/base_url/__manifest__.py @@ -6,11 +6,10 @@ { 'name': 'Base Url', - 'version': '8.0.0.1.0', + 'version': '10.0.0.1.0', 'category': 'tools', 'license': 'AGPL-3', 'summary': 'keep history of url for products & categories ', - 'author': 'Akretion', 'website': 'http://www.akretion.com/fr', # any module necessary for this one to work correctly diff --git a/base_url/models/__init__.py b/base_url/models/__init__.py index b69f282e0bf..c2c73a353fb 100644 --- a/base_url/models/__init__.py +++ b/base_url/models/__init__.py @@ -1,6 +1,3 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2016 Akretion (http://www.akretion.com) -# @author EBII MonsieurB -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import base_url diff --git a/base_url/models/base_url.py b/base_url/models/base_url.py index f469155ca72..1ab836917a2 100644 --- a/base_url/models/base_url.py +++ b/base_url/models/base_url.py @@ -3,9 +3,9 @@ # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api, fields, _ +from odoo import models, api, fields, _ import logging -from openerp.exceptions import Warning as UserError +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) try: from slugify import slugify @@ -56,7 +56,6 @@ def _compute_related_fields(self): def _reference_models(self): return [] - @api.multi def _get_object(self, url): """ :return: return object attach to the url @@ -81,9 +80,11 @@ class AbstractUrl(models.AbstractModel): comodel_name='url.url') def _build_url_key(self): + self.ensure_one() return slugify(self.record_id.with_context( lang=self.lang_id.code).name) + @api.model def _prepare_url(self, url_key): return { 'url_key': url_key, @@ -91,7 +92,6 @@ def _prepare_url(self, url_key): 'model_id': get_model_ref(self), } - @api.multi def set_url(self, url_key): """ backup old url @@ -132,7 +132,6 @@ def set_url(self, url_key): ('redirect', '=', False)]) redirect_urls.write({'redirect': True}) - @api.multi @api.depends('url_builder') def _compute_url(self): # We need a clear env before call the set_url method @@ -157,7 +156,6 @@ def _compute_url(self): record.url_key = new_url self.env.all.todo = todo - @api.multi def _compute_redirect_url(self): for record in self: record.redirect_url_key_ids = record.env["url.url"].search([ @@ -178,7 +176,6 @@ def on_url_key_change(self): 'message': 'it will be adapted to %s' % url, }} - @api.multi def unlink(self): for record in self: # TODO we should propose to redirect the old url diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index 079aed3a5e6..1c2e21df4a0 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -1,6 +1,6 @@ - - + + url.url @@ -16,7 +16,7 @@ - + url.url @@ -35,7 +35,7 @@ tree,form - + - - + From ee2bf1bbcaed7b1e4b5a98a66dd388044eba1c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Beau?= Date: Mon, 2 Oct 2017 17:15:37 +0200 Subject: [PATCH 04/77] REFactor move url model to another file --- .../models/{base_url.py => abstract_url.py} | 0 base_url/models/url_url.py | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+) rename base_url/models/{base_url.py => abstract_url.py} (100%) create mode 100644 base_url/models/url_url.py diff --git a/base_url/models/base_url.py b/base_url/models/abstract_url.py similarity index 100% rename from base_url/models/base_url.py rename to base_url/models/abstract_url.py diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py new file mode 100644 index 00000000000..bd3c43b8ce8 --- /dev/null +++ b/base_url/models/url_url.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +import logging + +_logger = logging.getLogger(__name__) + + +def get_model_ref(record): + return "%s,%s" % (record._name, record.id) + + +class UrlUrl(models.Model): + + _name = "url.url" + + url_key = fields.Char(required=True) + model_id = fields.Reference( + selection=[], + help="The id of content linked to the url.", + readonly=True, + string="Model", + required=True) + redirect = fields.Boolean( + help="If tick this url is a redirection to the new url") + backend_id = fields.Reference( + selection=[], + compute='_compute_related_fields', + store=True, + help="Backend linked to this URL", + string="Backend") + lang_id = fields.Many2one( + 'res.lang', + 'Lang', + compute='_compute_related_fields', + store=True) + + _sql_constraints = [('unique_key_per_backend_per_lang', + 'unique(url_key, backend_id, lang_id)', + 'Already exists in database')] + + @api.depends('model_id') + def _compute_related_fields(self): + for record in self: + record.backend_id = get_model_ref(record.model_id.backend_id) + record.lang_id = record.model_id.lang_id + + @api.model + def _reference_models(self): + return [] + + def _get_object(self, url): + """ + :return: return object attach to the url + """ + return self.search([('url_key', "=", url)]).model_id From 3b7729aaf5292add7fe3e2d2260c32951fae0bbc Mon Sep 17 00:00:00 2001 From: David Beal Date: Mon, 2 Oct 2017 17:16:28 +0200 Subject: [PATCH 05/77] FIX file organization --- base_url/README.rst | 34 +++++++++++------------ base_url/__manifest__.py | 2 +- base_url/models/__init__.py | 3 ++- base_url/models/abstract_url.py | 48 +-------------------------------- 4 files changed, 21 insertions(+), 66 deletions(-) diff --git a/base_url/README.rst b/base_url/README.rst index fcf11bae593..af56fc7888f 100644 --- a/base_url/README.rst +++ b/base_url/README.rst @@ -2,11 +2,16 @@ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 -============== +========= Base URL -============== +========= -This module helps to set url list and history of it +This module helps to manage url with these features: +* automatic builder according to models +* slugify string to get url +* redirection +* store url by backends depending on your connections apps +* languages Installation @@ -17,11 +22,15 @@ To install this module, you need to: #. (root) pip install slugify +Known issues / Roadmap +====================== -Images ------- +Update README with the last model of README when migration to v11 in OCA + + +Credits +======= -* Odoo Community Association: `Icon `_. Contributors ------------ @@ -29,17 +38,8 @@ Contributors * Monsieur B * Sebastien Beau + Maintainer ---------- -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -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. - -To contribute to this module, please visit https://odoo-community.org. +* Akretion diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index a304b2d0a6e..5cbed705bc8 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -13,7 +13,7 @@ 'author': 'Akretion', 'website': 'http://www.akretion.com/fr', # any module necessary for this one to work correctly - 'depends': [], + 'depends': ['base'], 'external_dependencies': { 'python': ['slugify'], }, diff --git a/base_url/models/__init__.py b/base_url/models/__init__.py index c2c73a353fb..bbcf6833595 100644 --- a/base_url/models/__init__.py +++ b/base_url/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import base_url +from . import abstract_url +from . import url_url diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 1ab836917a2..fe2f2f2e6c3 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -3,7 +3,7 @@ # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, api, fields, _ +from odoo import api, fields, models, _ import logging from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -17,52 +17,6 @@ def get_model_ref(record): return "%s,%s" % (record._name, record.id) -class UrlUrl(models.Model): - - _name = "url.url" - - url_key = fields.Char(required=True) - model_id = fields.Reference( - selection=[], - help="The id of content linked to the url.", - readonly=True, - string="Model", - required=True) - redirect = fields.Boolean( - help="If tick this url is a redirection to the new url") - backend_id = fields.Reference( - selection=[], - compute='_compute_related_fields', - store=True, - help="Backend linked to this URL", - string="Backend") - lang_id = fields.Many2one( - 'res.lang', - 'Lang', - compute='_compute_related_fields', - store=True) - - _sql_constraints = [('unique_key_per_backend_per_lang', - 'unique(url_key, backend_id, lang_id)', - 'Already exists in database')] - - @api.depends('model_id') - def _compute_related_fields(self): - for record in self: - record.backend_id = get_model_ref(record.model_id.backend_id) - record.lang_id = record.model_id.lang_id - - @api.model - def _reference_models(self): - return [] - - def _get_object(self, url): - """ - :return: return object attach to the url - """ - return self.search([('url_key', "=", url)]).model_id - - class AbstractUrl(models.AbstractModel): _name = 'abstract.url' From 1a976b20ce58550cd573c0f25327e94131bba6f9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 12 Jan 2018 18:32:46 +0100 Subject: [PATCH 06/77] [IMP] shopinvader: product binding wizard --- base_url/models/abstract_url.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index fe2f2f2e6c3..cb54dda7208 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -7,6 +7,7 @@ import logging from odoo.exceptions import UserError _logger = logging.getLogger(__name__) + try: from slugify import slugify except ImportError: @@ -27,11 +28,20 @@ class AbstractUrl(models.AbstractModel): ], default='auto') manual_url_key = fields.Char() url_key = fields.Char( + string='Url key', compute='_compute_url', - store=True) + store=True, + ) + url_url_ids = fields.One2many( + compute='_compute_url_url_id', + comodel_name='url.url') redirect_url_key_ids = fields.One2many( compute='_compute_redirect_url', comodel_name='url.url') + lang_id = fields.Many2one( + 'res.lang', + string='Lang', + required=True) def _build_url_key(self): self.ensure_one() @@ -57,7 +67,6 @@ def set_url(self, url_key): 3 write the new one """ self.ensure_one() - existing_url = self.env['url.url'].search([ ('url_key', '=', url_key), ('backend_id', '=', get_model_ref(self.backend_id)), @@ -117,6 +126,12 @@ def _compute_redirect_url(self): ('redirect', '=', True), ]) + def _compute_url_url_ids(self): + for record in self: + record.redirect_url_key_ids = record.env["url.url"].search([ + ('model_id', '=', get_model_ref(record)), + ]) + @api.onchange('manual_url_key') def on_url_key_change(self): self.ensure_one() From ebb2f8404d31fb0d352f59cec86ea92dd305cfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 28 May 2018 17:33:03 +0200 Subject: [PATCH 07/77] [FIX] remove optimisation code as it seem not necessary anymore. Moreover this generate a random issue. If you want to reproduce this random issue you can make it not random by forcing the processing of the todo. You can take the following code diff --git a/odoo/api.py b/odoo/api.py index 413a6b8..bb63ba8 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -937,6 +937,11 @@ class Environment(Mapping): The field is such that none of its dependencies must be recomputed. """ field = min(self.all.todo, key=self.registry.field_sequence) + if str(field) == 'shopinvader.category.index_id': + if len(self.all.todo) > 1: + for f in self.all.todo: + if str(f) != 'shopinvader.category.index_id': + field = f return field, self.all.todo[field][0] def check_cache(self): --- base_url/models/abstract_url.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index cb54dda7208..9b54ef170a2 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -97,13 +97,6 @@ def set_url(self, url_key): @api.depends('url_builder') def _compute_url(self): - # We need a clear env before call the set_url method - # indeed set_url will call a create that will trigger - # the recomputation of computed field - # This avoid useless recomputation of field - # TODO we should see with odoo how we can improve the ORM - todo = self.env.all.todo - self.env.all.todo = {} for record in self: if type(record.record_id.id) == models.NewId\ or type(record.id) == models.NewId: @@ -117,7 +110,6 @@ def _compute_url(self): if new_url: record.set_url(new_url) record.url_key = new_url - self.env.all.todo = todo def _compute_redirect_url(self): for record in self: From e802217e83ff2bca360719325363875b8f08f023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Honor=C3=A9?= Date: Mon, 2 Jul 2018 09:24:48 +0200 Subject: [PATCH 08/77] [10.0][FIX] Fix function name for computed field --- base_url/models/abstract_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 9b54ef170a2..6ac6c24104c 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -33,7 +33,7 @@ class AbstractUrl(models.AbstractModel): store=True, ) url_url_ids = fields.One2many( - compute='_compute_url_url_id', + compute='_compute_url_url_ids', comodel_name='url.url') redirect_url_key_ids = fields.One2many( compute='_compute_redirect_url', From 83bc54a591db06757a1bb26a780eb022416a1af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 31 Jul 2018 23:32:46 +0200 Subject: [PATCH 09/77] [FIX] fix access right issue, a user who have the right to modify the product should be able to do it even if the product is online --- base_url/models/abstract_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 6ac6c24104c..c13bb0377bf 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -108,7 +108,7 @@ def _compute_url(self): else: new_url = record._build_url_key() if new_url: - record.set_url(new_url) + record.sudo().set_url(new_url) record.url_key = new_url def _compute_redirect_url(self): From 39d87a7be79b7b827aecdaf61d6218227dd219e5 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Mon, 6 Aug 2018 10:47:35 +0200 Subject: [PATCH 10/77] [FIX] base_url: Remove unneeded sudo() --- base_url/models/abstract_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index c13bb0377bf..6ac6c24104c 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -108,7 +108,7 @@ def _compute_url(self): else: new_url = record._build_url_key() if new_url: - record.sudo().set_url(new_url) + record.set_url(new_url) record.url_key = new_url def _compute_redirect_url(self): From a11dc68464649412cca782800d33e12e66594fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Honor=C3=A9?= Date: Wed, 7 Nov 2018 15:56:48 +0100 Subject: [PATCH 11/77] [10.0][ADD] Add URL unbind for shopinvader.variant --- base_url/models/abstract_url.py | 2 +- base_url/models/url_url.py | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 6ac6c24104c..2762963734c 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -120,7 +120,7 @@ def _compute_redirect_url(self): def _compute_url_url_ids(self): for record in self: - record.redirect_url_key_ids = record.env["url.url"].search([ + record.url_url_ids = record.env["url.url"].search([ ('model_id', '=', get_model_ref(record)), ]) diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index bd3c43b8ce8..dc74a7d3981 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -52,6 +52,40 @@ def _compute_related_fields(self): def _reference_models(self): return [] + @api.model + def create(self, vals): + """ + Inherit the create to remove existing url in some case + :param vals: dict + :return: self recordset + """ + url = vals.get('url_key') + backend_id = vals.get('backend_id') + lang_id = vals.get('lang_id') + self._clean_existing_redirect(url, backend_id, lang_id) + return super(UrlUrl, self).create(vals) + + @api.model + def _clean_existing_redirect(self, url, backend_id, lang_id): + """ + If a new URL is created (related to the same backend) and the existing + is a redirect, we have to remove them to give priority to the new url. + :param url: str + :param backend_id: str (because it's a reference field) + :param lang_id: int + :return: bool + """ + domain = [ + ('url_key', '=', url), + ('redirect', '=', True), + ('backend_id', '=', backend_id), + ('lang_id', '=', lang_id), + ] + old_url = self.search(domain) + if old_url: + old_url.unlink() + return True + def _get_object(self, url): """ :return: return object attach to the url From c9cb0e34c9de11534142090432287fd52eee2dd3 Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 15 Feb 2019 12:38:31 +0100 Subject: [PATCH 12/77] [FIX] unbind url --- base_url/models/abstract_url.py | 50 ++++++++++++++++++++++----------- base_url/models/url_url.py | 39 +------------------------ 2 files changed, 35 insertions(+), 54 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 2762963734c..8dc38b8d3a9 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -42,6 +42,7 @@ class AbstractUrl(models.AbstractModel): 'res.lang', string='Lang', required=True) + active = fields.Boolean(string="Active", default=True) def _build_url_key(self): self.ensure_one() @@ -56,6 +57,13 @@ def _prepare_url(self, url_key): 'model_id': get_model_ref(self), } + def _reuse_url(self, existing_url): + # TODO add user notification in the futur SEO dashboard + existing_url.write({ + 'model_id': get_model_ref(self), + 'redirect': False, + }) + def set_url(self, url_key): """ backup old url @@ -74,15 +82,18 @@ def set_url(self, url_key): ]) if existing_url: if self != existing_url.model_id: - raise UserError( - _("Url_key already exist in other model" - "\n- name: %s\n - id: %s\n" - "- url_key: %s\n - url_key_id %s") % ( - existing_url.model_id.name, - existing_url.model_id.record_id.id, - existing_url.url_key, - existing_url.id, - )) + if existing_url.redirect: + self._reuse_url(existing_url) + else: + raise UserError( + _("Url_key already exist in other model" + "\n- name: %s\n - id: %s\n" + "- url_key: %s\n - url_key_id %s") % ( + existing_url.model_id.name, + existing_url.model_id.record_id.id, + existing_url.url_key, + existing_url.id, + )) else: existing_url.write({'redirect': False}) else: @@ -95,7 +106,10 @@ def set_url(self, url_key): ('redirect', '=', False)]) redirect_urls.write({'redirect': True}) - @api.depends('url_builder') + def _redirect_existing_url(self): + return True + + @api.depends('url_builder', 'manual_url_key', 'active') def _compute_url(self): for record in self: if type(record.record_id.id) == models.NewId\ @@ -103,13 +117,17 @@ def _compute_url(self): # Do not update field value on onchange # as with_context is broken on NewId continue - if record.url_builder == 'manual': - new_url = record.manual_url_key + if not record.active: + record._redirect_existing_url() + record.url_key = '' else: - new_url = record._build_url_key() - if new_url: - record.set_url(new_url) - record.url_key = new_url + if record.url_builder == 'manual': + new_url = record.manual_url_key + else: + new_url = record._build_url_key() + if new_url: + record.set_url(new_url) + record.url_key = new_url def _compute_redirect_url(self): for record in self: diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index dc74a7d3981..1313830c8aa 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -5,14 +5,11 @@ from odoo import api, fields, models import logging +from .abstract_url import get_model_ref _logger = logging.getLogger(__name__) -def get_model_ref(record): - return "%s,%s" % (record._name, record.id) - - class UrlUrl(models.Model): _name = "url.url" @@ -52,40 +49,6 @@ def _compute_related_fields(self): def _reference_models(self): return [] - @api.model - def create(self, vals): - """ - Inherit the create to remove existing url in some case - :param vals: dict - :return: self recordset - """ - url = vals.get('url_key') - backend_id = vals.get('backend_id') - lang_id = vals.get('lang_id') - self._clean_existing_redirect(url, backend_id, lang_id) - return super(UrlUrl, self).create(vals) - - @api.model - def _clean_existing_redirect(self, url, backend_id, lang_id): - """ - If a new URL is created (related to the same backend) and the existing - is a redirect, we have to remove them to give priority to the new url. - :param url: str - :param backend_id: str (because it's a reference field) - :param lang_id: int - :return: bool - """ - domain = [ - ('url_key', '=', url), - ('redirect', '=', True), - ('backend_id', '=', backend_id), - ('lang_id', '=', lang_id), - ] - old_url = self.search(domain) - if old_url: - old_url.unlink() - return True - def _get_object(self, url): """ :return: return object attach to the url From ac1f207cfa867ce18a41a337b10beb57f8def018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Honor=C3=A9?= Date: Mon, 25 Mar 2019 11:20:45 +0100 Subject: [PATCH 13/77] [FIX] Fix flake8 --- base_url/models/abstract_url.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 8dc38b8d3a9..233a52fa6c5 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -60,9 +60,9 @@ def _prepare_url(self, url_key): def _reuse_url(self, existing_url): # TODO add user notification in the futur SEO dashboard existing_url.write({ - 'model_id': get_model_ref(self), - 'redirect': False, - }) + 'model_id': get_model_ref(self), + 'redirect': False, + }) def set_url(self, url_key): """ From d5c602f45e6a23fefc630b0aacdf5cc1289a3c90 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 4 Apr 2019 18:32:51 +0200 Subject: [PATCH 14/77] [FIX] base_url: Never call write/unlink/create within a compute method --- base_url/models/abstract_url.py | 34 ++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 233a52fa6c5..6722d737860 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -42,6 +42,10 @@ class AbstractUrl(models.AbstractModel): 'res.lang', string='Lang', required=True) + is_urls_sync_required = fields.Boolean( + compute="_compute_is_urls_sync_required", + store=True, + ) active = fields.Boolean(string="Active", default=True) def _build_url_key(self): @@ -109,6 +113,11 @@ def set_url(self, url_key): def _redirect_existing_url(self): return True + @api.depends('url_builder', 'manual_url_key', 'active') + def _compute_is_urls_sync_required(self): + for record in self: + record.is_urls_sync_required = True + @api.depends('url_builder', 'manual_url_key', 'active') def _compute_url(self): for record in self: @@ -118,15 +127,12 @@ def _compute_url(self): # as with_context is broken on NewId continue if not record.active: - record._redirect_existing_url() record.url_key = '' else: if record.url_builder == 'manual': new_url = record.manual_url_key else: new_url = record._build_url_key() - if new_url: - record.set_url(new_url) record.url_key = new_url def _compute_redirect_url(self): @@ -155,6 +161,28 @@ def on_url_key_change(self): 'message': 'it will be adapted to %s' % url, }} + @api.multi + def _sync_urls(self): + """ + This method is in charge of syncing the url.url object related to + the current model + """ + records = self.filtered("is_urls_sync_required") + for record in records: + if not record.active: + record._redirect_existing_url() + else: + record.set_url(record.url_key) + return records + + @api.multi + def write(self, value): + res = super(AbstractUrl, self).write(value) + synced = self._sync_urls() + super(AbstractUrl, synced).write({"is_urls_sync_required": False}) + return res + + @api.multi def unlink(self): for record in self: # TODO we should propose to redirect the old url From 3df427706caad7c779da41f10e057d3205d71ee1 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 5 Apr 2019 13:24:55 +0200 Subject: [PATCH 15/77] [IMP] base_url: Refactor base url to no more depends on record_id and safely allows the triggeer of field computation by concrete models --- base_url/__manifest__.py | 2 +- base_url/models/abstract_url.py | 138 ++++++++++++++++------------ base_url/tests/__init__.py | 1 + base_url/tests/models.py | 40 ++++++++ base_url/tests/models_mixin.py | 114 +++++++++++++++++++++++ base_url/tests/test_abstract_url.py | 120 ++++++++++++++++++++++++ 6 files changed, 357 insertions(+), 58 deletions(-) create mode 100644 base_url/tests/__init__.py create mode 100644 base_url/tests/models.py create mode 100644 base_url/tests/models_mixin.py create mode 100644 base_url/tests/test_abstract_url.py diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 5cbed705bc8..99defd7be0f 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -10,7 +10,7 @@ 'category': 'tools', 'license': 'AGPL-3', 'summary': 'keep history of url for products & categories ', - 'author': 'Akretion', + 'author': 'Akretion, ACSONE SA/NV', 'website': 'http://www.akretion.com/fr', # any module necessary for this one to work correctly 'depends': ['base'], diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 6722d737860..cc971dc93b1 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -5,7 +5,7 @@ from odoo import api, fields, models, _ import logging -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError _logger = logging.getLogger(__name__) try: @@ -26,17 +26,21 @@ class AbstractUrl(models.AbstractModel): ('auto', 'Automatic'), ('manual', 'Manual'), ], default='auto') + automatic_url_key = fields.Char( + compute="_compute_automatic_url_key", + store=True + ) manual_url_key = fields.Char() url_key = fields.Char( string='Url key', - compute='_compute_url', + compute='_compute_url_key', store=True, ) url_url_ids = fields.One2many( compute='_compute_url_url_ids', comodel_name='url.url') - redirect_url_key_ids = fields.One2many( - compute='_compute_redirect_url', + redirect_url_url_ids = fields.One2many( + compute='_compute_redirect_url_url_ids', comodel_name='url.url') lang_id = fields.Many2one( 'res.lang', @@ -48,10 +52,67 @@ class AbstractUrl(models.AbstractModel): ) active = fields.Boolean(string="Active", default=True) - def _build_url_key(self): + @api.constrains("url_builder", "manual_url_key") + def _check_manual_url_key(self): + for rec in self: + if rec.url_builder == "manual" and not rec.manual_url_key: + raise ValidationError( + _("Manual url key is required if builder is set to manual") + ) + + @api.onchange('manual_url_key') + def on_url_key_change(self): self.ensure_one() - return slugify(self.record_id.with_context( - lang=self.lang_id.code).name) + if self.manual_url_key: + url = slugify(self.manual_url_key) + if url != self.manual_url_key: + self.manual_url_key = url + return { + 'warning': { + 'title': 'Adapt text rules', + 'message': 'it will be adapted to %s' % url, + }} + + @api.multi + @api.depends("url_key") + def _compute_is_urls_sync_required(self): + for record in self: + record.is_urls_sync_required = True + + @api.multi + def _compute_automatic_url_key(self): + raise NotImplemented( + "Automatic url key must be computed in concrete model" + ) + + @api.multi + @api.depends("manual_url_key", "automatic_url_key", "url_builder", + "active") + def _compute_url_key(self): + for record in self: + if not record.active: + record.url_key = '' + else: + if record.url_builder == 'manual': + new_url = record.manual_url_key + else: + new_url = record.automatic_url_key + record.url_key = new_url + + @api.multi + def _compute_redirect_url_url_ids(self): + for record in self: + record.redirect_url_url_ids = record.env["url.url"].search([ + ('model_id', '=', get_model_ref(record)), + ('redirect', '=', True), + ]) + + @api.multi + def _compute_url_url_ids(self): + for record in self: + record.url_url_ids = record.env["url.url"].search([ + ('model_id', '=', get_model_ref(record)), + ]) @api.model def _prepare_url(self, url_key): @@ -69,7 +130,7 @@ def _reuse_url(self, existing_url): }) def set_url(self, url_key): - """ + """ Se a new url backup old url 1 find url redirect true and same model_id @@ -94,7 +155,7 @@ def set_url(self, url_key): "\n- name: %s\n - id: %s\n" "- url_key: %s\n - url_key_id %s") % ( existing_url.model_id.name, - existing_url.model_id.record_id.id, + existing_url.model_id.id, existing_url.url_key, existing_url.id, )) @@ -111,56 +172,12 @@ def set_url(self, url_key): redirect_urls.write({'redirect': True}) def _redirect_existing_url(self): + """ + This method is called when the record is deactivated to give a chance + to the concrete model to implement a redirect strategy + """ return True - @api.depends('url_builder', 'manual_url_key', 'active') - def _compute_is_urls_sync_required(self): - for record in self: - record.is_urls_sync_required = True - - @api.depends('url_builder', 'manual_url_key', 'active') - def _compute_url(self): - for record in self: - if type(record.record_id.id) == models.NewId\ - or type(record.id) == models.NewId: - # Do not update field value on onchange - # as with_context is broken on NewId - continue - if not record.active: - record.url_key = '' - else: - if record.url_builder == 'manual': - new_url = record.manual_url_key - else: - new_url = record._build_url_key() - record.url_key = new_url - - def _compute_redirect_url(self): - for record in self: - record.redirect_url_key_ids = record.env["url.url"].search([ - ('model_id', '=', get_model_ref(record)), - ('redirect', '=', True), - ]) - - def _compute_url_url_ids(self): - for record in self: - record.url_url_ids = record.env["url.url"].search([ - ('model_id', '=', get_model_ref(record)), - ]) - - @api.onchange('manual_url_key') - def on_url_key_change(self): - self.ensure_one() - if self.manual_url_key: - url = slugify(self.manual_url_key) - if url != self.manual_url_key: - self.manual_url_key = url - return { - 'warning': { - 'title': 'Adapt text rules', - 'message': 'it will be adapted to %s' % url, - }} - @api.multi def _sync_urls(self): """ @@ -175,6 +192,13 @@ def _sync_urls(self): record.set_url(record.url_key) return records + @api.model + def create(self, value): + res = super(AbstractUrl, self).create(value) + synced = res._sync_urls() + super(AbstractUrl, synced).write({"is_urls_sync_required": False}) + return res + @api.multi def write(self, value): res = super(AbstractUrl, self).write(value) diff --git a/base_url/tests/__init__.py b/base_url/tests/__init__.py new file mode 100644 index 00000000000..68bd2af57f7 --- /dev/null +++ b/base_url/tests/__init__.py @@ -0,0 +1 @@ +from . import test_abstract_url diff --git a/base_url/tests/models.py b/base_url/tests/models.py new file mode 100644 index 00000000000..91c99582c56 --- /dev/null +++ b/base_url/tests/models.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging + +from odoo import api, fields, models +from .models_mixin import TestMixin + +_logger = logging.getLogger(__name__) + +try: + from slugify import slugify +except ImportError: + _logger.debug('Cannot `import slugify`.') + + +class UrlBackendFake(models.Model, TestMixin): + + _name = "url.backend.fake" + _description = "Url Backend" + + name = fields.Char(required=True) + + +class ResPartnerAddressableFake(models.Model, TestMixin): + _name = "res.partner.addressable.fake" + _inherit = "abstract.url" + _inherits = {'res.partner': 'record_id'} + _description = "Fake partner addressable" + + backend_id = fields.Many2one(comodel_name="url.backend.fake") + + @api.multi + @api.depends("lang_id", "record_id.name") + def _compute_automatic_url_key(self): + key_by_id = {} + for record in self.with_context(lang=self.lang_id.code): + key_by_id[record.id] = slugify(record.name) + for record in self: + record.automatic_url_key = key_by_id[record.id] diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py new file mode 100644 index 00000000000..d4b36bcd105 --- /dev/null +++ b/base_url/tests/models_mixin.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Simone Orsi - Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from operator import attrgetter + + +class TestMixin(object): + """Mixin to setup fake models for tests. + + Usage - the model: + + class FakeModel(models.Model, TestMixin): + _name = 'fake.model' + + name = fields.Char() + + Usage - the test klass: + + @classmethod + def setUpClass(cls): + super().setUpClass() + FakeModel._test_setup_model(cls.env) + + @classmethod + def tearDownClass(cls): + FakeModel._test_teardown_model(cls.env) + super().tearDownClass() + """ + + # Generate xmlids + # This is needed if you want to load data tied to a test model via xid. + _test_setup_gen_xid = False + # If you extend a real model (ie: res.partner) you must enable this + # to not delete the model on tear down. + _test_teardown_no_delete = False + # You can add custom fields to real models (eg: res.partner). + # In this case you must delete them to leave registry and model clean. + # This is mandatory for relational fields that link a fake model. + _test_purge_fields = [] + + @classmethod + def _test_setup_model(cls, env): + """Initialize it.""" + cls._build_model(env.registry, env.cr) + env.registry.setup_models(env.cr) + ctx = dict(env.context, update_custom_fields=True) + if cls._test_setup_gen_xid: + ctx["module"] = cls._module + env.registry.init_models(env.cr, [cls._name], ctx) + + @classmethod + def _test_teardown_model(cls, env): + """Cleanup registry and real models.""" + + for fname in cls._test_purge_fields: + model = env[cls._name] + if fname in model: + model._pop_field(fname) + + if not getattr(cls, "_test_teardown_no_delete", False): + del env.registry.models[cls._name] + # here we must remove the model from list of children of inherited + # models + parents = cls._inherit + parents = ( + [parents] + if isinstance(parents, basestring) + else (parents or []) + ) + # kepp a copy to be sure to not modify the original _inherit + parents = list(parents) + parents.extend(cls._inherits.keys()) + parents.append("base") + funcs = [ + attrgetter(kind + "_children") + for kind in ["_inherits", "_inherit"] + ] + for parent in parents: + for func in funcs: + children = func(env.registry[parent]) + if cls._name in children: + # at this stage our cls is referenced as children of + # parent -> must un reference it + children.remove(cls._name) + + def _test_get_model_id(self): + self.env.cr.execute( + "SELECT id FROM ir_model WHERE model = %s", (self._name,) + ) + res = self.env.cr.fetchone() + return res[0] if res else None + + def _test_create_ACL(self, **kw): + model_id = self._test_get_model_id() + if not model_id: + self._reflect() + model_id = self._test_get_model_id() + if model_id: + vals = self._test_ACL_values(model_id) + vals.update(kw) + self.env["ir.model.access"].create(vals) + + def _test_ACL_values(self, model_id): + values = { + "name": "Fake ACL for %s" % self._name, + "model_id": model_id, + "perm_read": 1, + "perm_create": 1, + "perm_write": 1, + "perm_unlink": 1, + "active": True, + } + return values diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py new file mode 100644 index 00000000000..146c17f5f65 --- /dev/null +++ b/base_url/tests/test_abstract_url.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import mock + +from odoo.exceptions import ValidationError +from odoo.tests import SavepointCase +from .models import UrlBackendFake, ResPartnerAddressableFake + + +class TestAbstractUrl(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestAbstractUrl, cls).setUpClass() + UrlBackendFake._test_setup_model(cls.env) + ResPartnerAddressableFake._test_setup_model(cls.env) + cls.lang = cls.env.ref("base.lang_en") + cls.UrlUrl = cls.env["url.url"] + cls.ResPartnerAddressable = cls.env["res.partner.addressable.fake"] + cls.url_backend = cls.env["url.backend.fake"].create( + {"name": "fake backend"} + ) + cls.name = "partner name" + cls.auto_key = "partner-name" + + @classmethod + def tearDownClass(cls): + ResPartnerAddressableFake._test_teardown_model(cls.env) + UrlBackendFake._test_teardown_model(cls.env) + super(TestAbstractUrl, cls).tearDownClass() + + def _get_default_partner_value(self): + return { + "name": self.name, + "lang_id": self.lang.id, + "url_builder": "auto", + "backend_id": self.url_backend.id, + } + + def _create_auto(self): + return self.ResPartnerAddressable.create( + self._get_default_partner_value() + ) + + def _check_url_key(self, partner_addressable, key_name): + self.assertFalse(partner_addressable.is_urls_sync_required) + self.assertEqual(partner_addressable.url_key, key_name) + self.assertEqual(len(partner_addressable.url_url_ids), 1) + url_url = partner_addressable.url_url_ids + self.assertEqual(url_url.url_key, key_name) + self.assertEqual(url_url.lang_id, self.lang) + self.assertEqual(url_url.model_id, partner_addressable) + + def test_create_auto_url(self): + my_partner = self.ResPartnerAddressable.create( + self._get_default_partner_value() + ) + self._check_url_key(my_partner, self.auto_key) + + def test_create_manual_url_contrains(self): + value = self._get_default_partner_value() + value["url_builder"] = "manual" + with self.assertRaises(ValidationError): + self.ResPartnerAddressable.create(value) + value["manual_url_key"] = "my url key" + res = self.ResPartnerAddressable.create(value) + self.assertTrue(res) + + def test_create_manual_url(self): + value = self._get_default_partner_value() + manual_url_key = "manual-url key" + value.update( + {"url_builder": "manual", "manual_url_key": manual_url_key} + ) + my_partner = self.ResPartnerAddressable.create(value) + self.assertTrue(my_partner) + self._check_url_key(my_partner, manual_url_key) + + def test_write_url_builder_constrains(self): + my_partner = self._create_auto() + with self.assertRaises(ValidationError): + my_partner.url_builder = "manual" + my_partner.write( + {"url_builder": "manual", "manual_url_key": "manual url key"} + ) + + def test_write_url_builder(self): + # tests that a new url is created we update the url builder + my_partner = self._create_auto() + self._check_url_key(my_partner, "partner-name") + manual_url_key = "manual-url key" + my_partner.write( + {"url_builder": "manual", "manual_url_key": manual_url_key} + ) + my_partner.refresh() + url_keys = set(my_partner.mapped("url_url_ids.url_key")) + self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) + # if we reset the auto key, no new url.url should be created + my_partner.write({"url_builder": "auto"}) + my_partner.refresh() + self.assertEqual(2, len(my_partner.url_url_ids)) + url_keys = set(my_partner.mapped("url_url_ids.url_key")) + self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) + + def test_write_launching_automatic_url_key(self): + my_partner = self._create_auto() + my_partner.name = "my new name" + my_partner.refresh() + self.assertEqual(2, len(my_partner.url_url_ids)) + url_keys = set(my_partner.mapped("url_url_ids.url_key")) + self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) + + def test_write_inactive(self): + my_partner = self._create_auto() + # when we deactivate a record, the redirect method should be called + with mock.patch.object( + self.ResPartnerAddressable.__class__, "_redirect_existing_url" + ) as mocked_redirect: + my_partner.active = False + mocked_redirect.assert_called_once() From 901592ca8a4f6e9a3e463b510097e47468768fcc Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 6 May 2019 17:40:28 +0200 Subject: [PATCH 16/77] [IMP] Add pre-commit hook and reformat code thanks to black --- base_url/__manifest__.py | 31 +++--- base_url/models/abstract_url.py | 147 +++++++++++++------------- base_url/models/url_url.py | 39 ++++--- base_url/security/ir.model.access.csv | 1 - base_url/tests/models.py | 5 +- base_url/tests/test_abstract_url.py | 4 +- 6 files changed, 117 insertions(+), 110 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 99defd7be0f..2b52a2b7349 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,23 +5,18 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Base Url', - 'version': '10.0.0.1.0', - 'category': 'tools', - 'license': 'AGPL-3', - 'summary': 'keep history of url for products & categories ', - 'author': 'Akretion, ACSONE SA/NV', - 'website': 'http://www.akretion.com/fr', + "name": "Base Url", + "version": "10.0.0.1.0", + "category": "tools", + "license": "AGPL-3", + "summary": "keep history of url for products & categories ", + "author": "Akretion, ACSONE SA/NV", + "website": "http://www.akretion.com/fr", # any module necessary for this one to work correctly - 'depends': ['base'], - 'external_dependencies': { - 'python': ['slugify'], - }, - 'data': ['views/url_view.xml', - 'security/ir.model.access.csv'], - - 'demo': [], - - 'url': '', - 'installable': True, + "depends": ["base"], + "external_dependencies": {"python": ["slugify"]}, + "data": ["views/url_view.xml", "security/ir.model.access.csv"], + "demo": [], + "url": "", + "installable": True, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index cc971dc93b1..bd09981da42 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -3,52 +3,45 @@ # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ import logging + +from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError + _logger = logging.getLogger(__name__) try: from slugify import slugify except ImportError: - _logger.debug('Cannot `import slugify`.') + _logger.debug("Cannot `import slugify`.") def get_model_ref(record): - return "%s,%s" % (record._name, record.id) + return "{},{}".format(record._name, record.id) class AbstractUrl(models.AbstractModel): - _name = 'abstract.url' + _name = "abstract.url" url_builder = fields.Selection( - selection=[ - ('auto', 'Automatic'), - ('manual', 'Manual'), - ], default='auto') + selection=[("auto", "Automatic"), ("manual", "Manual")], default="auto" + ) automatic_url_key = fields.Char( - compute="_compute_automatic_url_key", - store=True + compute="_compute_automatic_url_key", store=True ) manual_url_key = fields.Char() url_key = fields.Char( - string='Url key', - compute='_compute_url_key', - store=True, + string="Url key", compute="_compute_url_key", store=True ) url_url_ids = fields.One2many( - compute='_compute_url_url_ids', - comodel_name='url.url') + compute="_compute_url_url_ids", comodel_name="url.url" + ) redirect_url_url_ids = fields.One2many( - compute='_compute_redirect_url_url_ids', - comodel_name='url.url') - lang_id = fields.Many2one( - 'res.lang', - string='Lang', - required=True) + compute="_compute_redirect_url_url_ids", comodel_name="url.url" + ) + lang_id = fields.Many2one("res.lang", string="Lang", required=True) is_urls_sync_required = fields.Boolean( - compute="_compute_is_urls_sync_required", - store=True, + compute="_compute_is_urls_sync_required", store=True ) active = fields.Boolean(string="Active", default=True) @@ -60,7 +53,7 @@ def _check_manual_url_key(self): _("Manual url key is required if builder is set to manual") ) - @api.onchange('manual_url_key') + @api.onchange("manual_url_key") def on_url_key_change(self): self.ensure_one() if self.manual_url_key: @@ -68,10 +61,11 @@ def on_url_key_change(self): if url != self.manual_url_key: self.manual_url_key = url return { - 'warning': { - 'title': 'Adapt text rules', - 'message': 'it will be adapted to %s' % url, - }} + "warning": { + "title": "Adapt text rules", + "message": "it will be adapted to %s" % url, + } + } @api.multi @api.depends("url_key") @@ -81,19 +75,20 @@ def _compute_is_urls_sync_required(self): @api.multi def _compute_automatic_url_key(self): - raise NotImplemented( + raise NotImplementedError( "Automatic url key must be computed in concrete model" ) @api.multi - @api.depends("manual_url_key", "automatic_url_key", "url_builder", - "active") + @api.depends( + "manual_url_key", "automatic_url_key", "url_builder", "active" + ) def _compute_url_key(self): for record in self: if not record.active: - record.url_key = '' + record.url_key = "" else: - if record.url_builder == 'manual': + if record.url_builder == "manual": new_url = record.manual_url_key else: new_url = record.automatic_url_key @@ -102,32 +97,33 @@ def _compute_url_key(self): @api.multi def _compute_redirect_url_url_ids(self): for record in self: - record.redirect_url_url_ids = record.env["url.url"].search([ - ('model_id', '=', get_model_ref(record)), - ('redirect', '=', True), - ]) + record.redirect_url_url_ids = record.env["url.url"].search( + [ + ("model_id", "=", get_model_ref(record)), + ("redirect", "=", True), + ] + ) @api.multi def _compute_url_url_ids(self): for record in self: - record.url_url_ids = record.env["url.url"].search([ - ('model_id', '=', get_model_ref(record)), - ]) + record.url_url_ids = record.env["url.url"].search( + [("model_id", "=", get_model_ref(record))] + ) @api.model def _prepare_url(self, url_key): return { - 'url_key': url_key, - 'redirect': False, - 'model_id': get_model_ref(self), - } + "url_key": url_key, + "redirect": False, + "model_id": get_model_ref(self), + } def _reuse_url(self, existing_url): # TODO add user notification in the futur SEO dashboard - existing_url.write({ - 'model_id': get_model_ref(self), - 'redirect': False, - }) + existing_url.write( + {"model_id": get_model_ref(self), "redirect": False} + ) def set_url(self, url_key): """ Se a new url @@ -140,36 +136,45 @@ def set_url(self, url_key): 3 write the new one """ self.ensure_one() - existing_url = self.env['url.url'].search([ - ('url_key', '=', url_key), - ('backend_id', '=', get_model_ref(self.backend_id)), - ('lang_id', '=', self.lang_id.id), - ]) + existing_url = self.env["url.url"].search( + [ + ("url_key", "=", url_key), + ("backend_id", "=", get_model_ref(self.backend_id)), + ("lang_id", "=", self.lang_id.id), + ] + ) if existing_url: if self != existing_url.model_id: if existing_url.redirect: self._reuse_url(existing_url) else: raise UserError( - _("Url_key already exist in other model" - "\n- name: %s\n - id: %s\n" - "- url_key: %s\n - url_key_id %s") % ( - existing_url.model_id.name, - existing_url.model_id.id, - existing_url.url_key, - existing_url.id, - )) + _( + "Url_key already exist in other model" + "\n- name: %s\n - id: %s\n" + "- url_key: %s\n - url_key_id %s" + ) + % ( + existing_url.model_id.name, + existing_url.model_id.id, + existing_url.url_key, + existing_url.id, + ) + ) else: - existing_url.write({'redirect': False}) + existing_url.write({"redirect": False}) else: # no existing key creating one if not empty - self.env['url.url'].create(self._prepare_url(url_key)) + self.env["url.url"].create(self._prepare_url(url_key)) # other url of object set redirect to True - redirect_urls = self.env['url.url'].search([ - ('model_id', '=', get_model_ref(self)), - ('url_key', '!=', url_key), - ('redirect', '=', False)]) - redirect_urls.write({'redirect': True}) + redirect_urls = self.env["url.url"].search( + [ + ("model_id", "=", get_model_ref(self)), + ("url_key", "!=", url_key), + ("redirect", "=", False), + ] + ) + redirect_urls.write({"redirect": True}) def _redirect_existing_url(self): """ @@ -210,8 +215,8 @@ def write(self, value): def unlink(self): for record in self: # TODO we should propose to redirect the old url - urls = record.env["url.url"].search([ - ('model_id', '=', get_model_ref(record)), - ]) + urls = record.env["url.url"].search( + [("model_id", "=", get_model_ref(record))] + ) urls.unlink() return super(AbstractUrl, self).unlink() diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 1313830c8aa..11e3e55338d 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -3,8 +3,10 @@ # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models import logging + +from odoo import api, fields, models + from .abstract_url import get_model_ref _logger = logging.getLogger(__name__) @@ -20,26 +22,31 @@ class UrlUrl(models.Model): help="The id of content linked to the url.", readonly=True, string="Model", - required=True) + required=True, + ) redirect = fields.Boolean( - help="If tick this url is a redirection to the new url") + help="If tick this url is a redirection to the new url" + ) backend_id = fields.Reference( selection=[], - compute='_compute_related_fields', + compute="_compute_related_fields", store=True, help="Backend linked to this URL", - string="Backend") + string="Backend", + ) lang_id = fields.Many2one( - 'res.lang', - 'Lang', - compute='_compute_related_fields', - store=True) - - _sql_constraints = [('unique_key_per_backend_per_lang', - 'unique(url_key, backend_id, lang_id)', - 'Already exists in database')] - - @api.depends('model_id') + "res.lang", "Lang", compute="_compute_related_fields", store=True + ) + + _sql_constraints = [ + ( + "unique_key_per_backend_per_lang", + "unique(url_key, backend_id, lang_id)", + "Already exists in database", + ) + ] + + @api.depends("model_id") def _compute_related_fields(self): for record in self: record.backend_id = get_model_ref(record.model_id.backend_id) @@ -53,4 +60,4 @@ def _get_object(self, url): """ :return: return object attach to the url """ - return self.search([('url_key', "=", url)]).model_id + return self.search([("url_key", "=", url)]).model_id diff --git a/base_url/security/ir.model.access.csv b/base_url/security/ir.model.access.csv index 339818b4420..7f57c8c626d 100644 --- a/base_url/security/ir.model.access.csv +++ b/base_url/security/ir.model.access.csv @@ -1,3 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_url_url,access_url_url,model_url_url,,1,0,0,0 - diff --git a/base_url/tests/models.py b/base_url/tests/models.py index 91c99582c56..01abe5db498 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -4,6 +4,7 @@ import logging from odoo import api, fields, models + from .models_mixin import TestMixin _logger = logging.getLogger(__name__) @@ -11,7 +12,7 @@ try: from slugify import slugify except ImportError: - _logger.debug('Cannot `import slugify`.') + _logger.debug("Cannot `import slugify`.") class UrlBackendFake(models.Model, TestMixin): @@ -25,7 +26,7 @@ class UrlBackendFake(models.Model, TestMixin): class ResPartnerAddressableFake(models.Model, TestMixin): _name = "res.partner.addressable.fake" _inherit = "abstract.url" - _inherits = {'res.partner': 'record_id'} + _inherits = {"res.partner": "record_id"} _description = "Fake partner addressable" backend_id = fields.Many2one(comodel_name="url.backend.fake") diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 146c17f5f65..e7e370303b1 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -2,10 +2,10 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock - from odoo.exceptions import ValidationError from odoo.tests import SavepointCase -from .models import UrlBackendFake, ResPartnerAddressableFake + +from .models import ResPartnerAddressableFake, UrlBackendFake class TestAbstractUrl(SavepointCase): From 4b8b7d4d923c7f7c0ff9af29972bc9e38586bfdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Honor=C3=A9?= Date: Tue, 7 May 2019 11:20:35 +0200 Subject: [PATCH 17/77] [10.0][FIX] Fix unit test (avoid commit) --- base_url/tests/models_mixin.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py index d4b36bcd105..214a6dbbaf0 100644 --- a/base_url/tests/models_mixin.py +++ b/base_url/tests/models_mixin.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Copyright 2018 Simone Orsi - Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - from operator import attrgetter +import mock + class TestMixin(object): """Mixin to setup fake models for tests. @@ -42,12 +43,13 @@ def tearDownClass(cls): @classmethod def _test_setup_model(cls, env): """Initialize it.""" - cls._build_model(env.registry, env.cr) - env.registry.setup_models(env.cr) - ctx = dict(env.context, update_custom_fields=True) - if cls._test_setup_gen_xid: - ctx["module"] = cls._module - env.registry.init_models(env.cr, [cls._name], ctx) + with mock.patch.object(env.cr, "commit"): + cls._build_model(env.registry, env.cr) + env.registry.setup_models(env.cr) + ctx = dict(env.context, update_custom_fields=True) + if cls._test_setup_gen_xid: + ctx["module"] = cls._module + env.registry.init_models(env.cr, [cls._name], ctx) @classmethod def _test_teardown_model(cls, env): From 1c4f5c2d9fc570e68e55a46d0da222d48cee8624 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Fri, 10 May 2019 11:28:27 +0200 Subject: [PATCH 18/77] [12.0] Set installable False to all modules --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 2b52a2b7349..21c1d78ed13 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -18,5 +18,5 @@ "data": ["views/url_view.xml", "security/ir.model.access.csv"], "demo": [], "url": "", - "installable": True, + "installable": False, } From e1b2a61dad598002ad871effbe88661a2e45921d Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Fri, 10 May 2019 19:05:20 +0200 Subject: [PATCH 19/77] [12.0][MIG] base_url --- base_url/README.rst | 1 + base_url/__init__.py | 1 - base_url/__manifest__.py | 8 +++----- base_url/models/__init__.py | 2 -- base_url/models/abstract_url.py | 2 +- base_url/models/url_url.py | 2 +- base_url/tests/models.py | 1 - base_url/tests/models_mixin.py | 3 +-- base_url/tests/test_abstract_url.py | 1 - 9 files changed, 7 insertions(+), 14 deletions(-) diff --git a/base_url/README.rst b/base_url/README.rst index af56fc7888f..a8d1a873f9f 100644 --- a/base_url/README.rst +++ b/base_url/README.rst @@ -7,6 +7,7 @@ ========= This module helps to manage url with these features: + * automatic builder according to models * slugify string to get url * redirection diff --git a/base_url/__init__.py b/base_url/__init__.py index a0fdc10fe11..0650744f6bc 100644 --- a/base_url/__init__.py +++ b/base_url/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- from . import models diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 21c1d78ed13..ad83431f06e 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Rewrite url Base module for OpenERP # Copyright (C) 2013 Akretion (http://www.akretion.com) # @author EBII MonsieurB @@ -6,17 +5,16 @@ { "name": "Base Url", - "version": "10.0.0.1.0", + "version": "12.0.0.1.0", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", "author": "Akretion, ACSONE SA/NV", - "website": "http://www.akretion.com/fr", + "website": "https://github.com/shopinvader", # any module necessary for this one to work correctly "depends": ["base"], "external_dependencies": {"python": ["slugify"]}, "data": ["views/url_view.xml", "security/ir.model.access.csv"], - "demo": [], "url": "", - "installable": False, + "installable": True, } diff --git a/base_url/models/__init__.py b/base_url/models/__init__.py index bbcf6833595..bc2f6141e74 100644 --- a/base_url/models/__init__.py +++ b/base_url/models/__init__.py @@ -1,4 +1,2 @@ -# -*- coding: utf-8 -*- - from . import abstract_url from . import url_url diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index bd09981da42..833cd9fd665 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2016 Akretion (http://www.akretion.com) # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -22,6 +21,7 @@ def get_model_ref(record): class AbstractUrl(models.AbstractModel): _name = "abstract.url" + _description = "Abstract Url" url_builder = fields.Selection( selection=[("auto", "Automatic"), ("manual", "Manual")], default="auto" diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 11e3e55338d..81b59c0f4b3 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2016 Akretion (http://www.akretion.com) # @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -15,6 +14,7 @@ class UrlUrl(models.Model): _name = "url.url" + _description = "Url" url_key = fields.Char(required=True) model_id = fields.Reference( diff --git a/base_url/tests/models.py b/base_url/tests/models.py index 01abe5db498..81ccc7c32fb 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import logging diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py index 214a6dbbaf0..fa1555ba40a 100644 --- a/base_url/tests/models_mixin.py +++ b/base_url/tests/models_mixin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 Simone Orsi - Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from operator import attrgetter @@ -67,7 +66,7 @@ def _test_teardown_model(cls, env): parents = cls._inherit parents = ( [parents] - if isinstance(parents, basestring) + if isinstance(parents, (str, bytes)) else (parents or []) ) # kepp a copy to be sure to not modify the original _inherit diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index e7e370303b1..cd741089f3e 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock From 714b116f99a24a9b3a1e7278d520274501634f47 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 29 Oct 2019 11:16:38 +0100 Subject: [PATCH 20/77] [MIG] Create branch 13.0 --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index ad83431f06e..ec7b8263267 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -16,5 +16,5 @@ "external_dependencies": {"python": ["slugify"]}, "data": ["views/url_view.xml", "security/ir.model.access.csv"], "url": "", - "installable": True, + "installable": False, } From 05b11ab4a359e738d0a45443a0380db0bec287e9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 29 Oct 2019 12:19:39 +0100 Subject: [PATCH 21/77] [MIG] base_url: Migration to 13.0 --- base_url/__manifest__.py | 6 +++--- base_url/models/abstract_url.py | 12 ++++-------- base_url/models/url_url.py | 9 +++++++-- base_url/tests/models.py | 1 - 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index ec7b8263267..cdc2bc3de61 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "12.0.0.1.0", + "version": "13.0.1.0.0", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", @@ -13,8 +13,8 @@ "website": "https://github.com/shopinvader", # any module necessary for this one to work correctly "depends": ["base"], - "external_dependencies": {"python": ["slugify"]}, + "external_dependencies": {"python": ["python-slugify"]}, "data": ["views/url_view.xml", "security/ir.model.access.csv"], "url": "", - "installable": False, + "installable": True, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 833cd9fd665..bcd48ac9c5e 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -67,19 +67,16 @@ def on_url_key_change(self): } } - @api.multi @api.depends("url_key") def _compute_is_urls_sync_required(self): for record in self: record.is_urls_sync_required = True - @api.multi def _compute_automatic_url_key(self): raise NotImplementedError( "Automatic url key must be computed in concrete model" ) - @api.multi @api.depends( "manual_url_key", "automatic_url_key", "url_builder", "active" ) @@ -94,7 +91,6 @@ def _compute_url_key(self): new_url = record.automatic_url_key record.url_key = new_url - @api.multi def _compute_redirect_url_url_ids(self): for record in self: record.redirect_url_url_ids = record.env["url.url"].search( @@ -104,7 +100,6 @@ def _compute_redirect_url_url_ids(self): ] ) - @api.multi def _compute_url_url_ids(self): for record in self: record.url_url_ids = record.env["url.url"].search( @@ -175,6 +170,10 @@ def set_url(self, url_key): ] ) redirect_urls.write({"redirect": True}) + # we must explicitly invalidate the cache since there is no depends + # defined on this computed fields and this field could have already + # been loaded into the cache + self.invalidate_cache(fnames=["url_url_ids"], ids=self.ids) def _redirect_existing_url(self): """ @@ -183,7 +182,6 @@ def _redirect_existing_url(self): """ return True - @api.multi def _sync_urls(self): """ This method is in charge of syncing the url.url object related to @@ -204,14 +202,12 @@ def create(self, value): super(AbstractUrl, synced).write({"is_urls_sync_required": False}) return res - @api.multi def write(self, value): res = super(AbstractUrl, self).write(value) synced = self._sync_urls() super(AbstractUrl, synced).write({"is_urls_sync_required": False}) return res - @api.multi def unlink(self): for record in self: # TODO we should propose to redirect the old url diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 81b59c0f4b3..5b22c8557ad 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -18,7 +18,7 @@ class UrlUrl(models.Model): url_key = fields.Char(required=True) model_id = fields.Reference( - selection=[], + selection="_selection_target_model", help="The id of content linked to the url.", readonly=True, string="Model", @@ -28,7 +28,7 @@ class UrlUrl(models.Model): help="If tick this url is a redirection to the new url" ) backend_id = fields.Reference( - selection=[], + selection="_selection_target_model", compute="_compute_related_fields", store=True, help="Backend linked to this URL", @@ -46,6 +46,11 @@ class UrlUrl(models.Model): ) ] + @api.model + def _selection_target_model(self): + models = self.env["ir.model"].search([]) + return [(model.model, model.name) for model in models] + @api.depends("model_id") def _compute_related_fields(self): for record in self: diff --git a/base_url/tests/models.py b/base_url/tests/models.py index 81ccc7c32fb..9f0ae71ce9d 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -30,7 +30,6 @@ class ResPartnerAddressableFake(models.Model, TestMixin): backend_id = fields.Many2one(comodel_name="url.backend.fake") - @api.multi @api.depends("lang_id", "record_id.name") def _compute_automatic_url_key(self): key_by_id = {} From 84850e96a1e2be96ebad1a2406cada636137e6ef Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 8 Nov 2019 19:05:47 +0100 Subject: [PATCH 22/77] [MIG] shopinvader: Migration to 13.0 --- base_url/models/abstract_url.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index bcd48ac9c5e..2b6c1272b2b 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -198,12 +198,16 @@ def _sync_urls(self): @api.model def create(self, value): res = super(AbstractUrl, self).create(value) + # without this flush we have cases where the url_key is not stored + self.flush() synced = res._sync_urls() super(AbstractUrl, synced).write({"is_urls_sync_required": False}) return res def write(self, value): res = super(AbstractUrl, self).write(value) + # without this flush we have cases where the url_key is not stored + self.flush() synced = self._sync_urls() super(AbstractUrl, synced).write({"is_urls_sync_required": False}) return res @@ -215,4 +219,5 @@ def unlink(self): [("model_id", "=", get_model_ref(record))] ) urls.unlink() + self.flush() return super(AbstractUrl, self).unlink() From f44eae0753080db9f2167a2111cf3815615b5e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 3 Apr 2020 22:33:21 +0200 Subject: [PATCH 23/77] fix issue when creating binding from the main record Indeed the implementation of write on one2many (see odoo/fields.py:2235) call the method create/write in mode norecompute This mean that the field is_urls_sync_required is not computed after the call to super of write/create of the binding but later during the create/write of the main record. To solve the issue we sync the url after calling the method "_recompute_done". This also simplify the code \o/ --- base_url/models/abstract_url.py | 21 ++++------------ base_url/tests/models.py | 24 +++++++++++++----- base_url/tests/models_mixin.py | 1 + base_url/tests/test_abstract_url.py | 39 ++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 2b6c1272b2b..a72e910e584 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -195,22 +195,11 @@ def _sync_urls(self): record.set_url(record.url_key) return records - @api.model - def create(self, value): - res = super(AbstractUrl, self).create(value) - # without this flush we have cases where the url_key is not stored - self.flush() - synced = res._sync_urls() - super(AbstractUrl, synced).write({"is_urls_sync_required": False}) - return res - - def write(self, value): - res = super(AbstractUrl, self).write(value) - # without this flush we have cases where the url_key is not stored - self.flush() - synced = self._sync_urls() - super(AbstractUrl, synced).write({"is_urls_sync_required": False}) - return res + def _recompute_done(self, field): + super(AbstractUrl, self)._recompute_done(field) + if field.name == "is_urls_sync_required": + synced = self.exists()._sync_urls() + synced.write({"is_urls_sync_required": False}) def unlink(self): for record in self: diff --git a/base_url/tests/models.py b/base_url/tests/models.py index 9f0ae71ce9d..eb714a8c053 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -22,6 +22,15 @@ class UrlBackendFake(models.Model, TestMixin): name = fields.Char(required=True) +class ResPartner(models.Model, TestMixin): + _name = "res.partner" + _inherit = "res.partner" + _test_teardown_no_delete = True + _test_purge_fields = ["binding_ids"] + + binding_ids = fields.One2many("res.partner.addressable.fake", "record_id") + + class ResPartnerAddressableFake(models.Model, TestMixin): _name = "res.partner.addressable.fake" _inherit = "abstract.url" @@ -29,11 +38,14 @@ class ResPartnerAddressableFake(models.Model, TestMixin): _description = "Fake partner addressable" backend_id = fields.Many2one(comodel_name="url.backend.fake") + special_code = fields.Char() - @api.depends("lang_id", "record_id.name") + @api.depends("lang_id", "special_code", "record_id.name") def _compute_automatic_url_key(self): - key_by_id = {} - for record in self.with_context(lang=self.lang_id.code): - key_by_id[record.id] = slugify(record.name) - for record in self: - record.automatic_url_key = key_by_id[record.id] + self._generic_compute_automatic_url_key() + + def _get_url_keywords(self): + res = super(ResPartnerAddressableFake, self)._get_url_keywords() + if self.special_code: + res += [self.special_code] + return res diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py index fa1555ba40a..80d86160af9 100644 --- a/base_url/tests/models_mixin.py +++ b/base_url/tests/models_mixin.py @@ -58,6 +58,7 @@ def _test_teardown_model(cls, env): model = env[cls._name] if fname in model: model._pop_field(fname) + model._proper_fields.remove(fname) if not getattr(cls, "_test_teardown_no_delete", False): del env.registry.models[cls._name] diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index cd741089f3e..ce03b27f055 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -4,7 +4,7 @@ from odoo.exceptions import ValidationError from odoo.tests import SavepointCase -from .models import ResPartnerAddressableFake, UrlBackendFake +from .models import ResPartner, ResPartnerAddressableFake, UrlBackendFake class TestAbstractUrl(SavepointCase): @@ -13,6 +13,7 @@ def setUpClass(cls): super(TestAbstractUrl, cls).setUpClass() UrlBackendFake._test_setup_model(cls.env) ResPartnerAddressableFake._test_setup_model(cls.env) + ResPartner._test_setup_model(cls.env) cls.lang = cls.env.ref("base.lang_en") cls.UrlUrl = cls.env["url.url"] cls.ResPartnerAddressable = cls.env["res.partner.addressable.fake"] @@ -26,6 +27,7 @@ def setUpClass(cls): def tearDownClass(cls): ResPartnerAddressableFake._test_teardown_model(cls.env) UrlBackendFake._test_teardown_model(cls.env) + ResPartner._test_teardown_model(cls.env) super(TestAbstractUrl, cls).tearDownClass() def _get_default_partner_value(self): @@ -117,3 +119,38 @@ def test_write_inactive(self): ) as mocked_redirect: my_partner.active = False mocked_redirect.assert_called_once() + + def test_onchange_do_nothing(self): + # calling onchange should not fail and should not return an url + result = self.ResPartnerAddressable.onchange( + {}, ["automatic_url_key"], {} + ) + self.assertEqual(result, {"value": {}}) + + def test_create_from_main(self): + partner = self.env["res.partner"].create({"name": self.name}) + partner.write( + { + "binding_ids": [ + ( + 0, + 0, + { + "lang_id": self.lang.id, + "url_builder": "auto", + "backend_id": self.url_backend.id, + }, + ) + ] + } + ) + self._check_url_key(partner.binding_ids, "partner-name") + + def test_write_from_main(self): + my_partner = self._create_auto() + my_partner.record_id.write( + {"binding_ids": [(1, my_partner.id, {"special_code": "foo"})]} + ) + self.assertEqual(2, len(my_partner.url_url_ids)) + url_keys = set(my_partner.mapped("url_url_ids.url_key")) + self.assertSetEqual(url_keys, {"partner-name-foo", "partner-name"}) From 4a5dafa588bb092df2dbcb8b69fc7785424641a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Sun, 5 Apr 2020 00:36:22 +0200 Subject: [PATCH 24/77] fix test and use use odoo-test-helper lib \o/ --- base_url/tests/models.py | 17 +++-------------- base_url/tests/test_abstract_url.py | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/base_url/tests/models.py b/base_url/tests/models.py index eb714a8c053..d7330369aec 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -4,34 +4,23 @@ from odoo import api, fields, models -from .models_mixin import TestMixin - _logger = logging.getLogger(__name__) -try: - from slugify import slugify -except ImportError: - _logger.debug("Cannot `import slugify`.") - - -class UrlBackendFake(models.Model, TestMixin): +class UrlBackendFake(models.Model): _name = "url.backend.fake" _description = "Url Backend" name = fields.Char(required=True) -class ResPartner(models.Model, TestMixin): - _name = "res.partner" +class ResPartner(models.Model): _inherit = "res.partner" - _test_teardown_no_delete = True - _test_purge_fields = ["binding_ids"] binding_ids = fields.One2many("res.partner.addressable.fake", "record_id") -class ResPartnerAddressableFake(models.Model, TestMixin): +class ResPartnerAddressableFake(models.Model): _name = "res.partner.addressable.fake" _inherit = "abstract.url" _inherits = {"res.partner": "record_id"} diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index ce03b27f055..6f057a3f0d4 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -3,17 +3,25 @@ import mock from odoo.exceptions import ValidationError from odoo.tests import SavepointCase +from odoo_test_helper import FakeModelLoader -from .models import ResPartner, ResPartnerAddressableFake, UrlBackendFake - -class TestAbstractUrl(SavepointCase): +class TestAbstractUrl(SavepointCase, FakeModelLoader): @classmethod def setUpClass(cls): super(TestAbstractUrl, cls).setUpClass() - UrlBackendFake._test_setup_model(cls.env) - ResPartnerAddressableFake._test_setup_model(cls.env) - ResPartner._test_setup_model(cls.env) + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import ( + UrlBackendFake, + ResPartner, + ResPartnerAddressableFake, + ) + + cls.loader.update_registry( + (UrlBackendFake, ResPartner, ResPartnerAddressableFake) + ) + cls.lang = cls.env.ref("base.lang_en") cls.UrlUrl = cls.env["url.url"] cls.ResPartnerAddressable = cls.env["res.partner.addressable.fake"] @@ -25,9 +33,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - ResPartnerAddressableFake._test_teardown_model(cls.env) - UrlBackendFake._test_teardown_model(cls.env) - ResPartner._test_teardown_model(cls.env) + cls.loader.restore_registry() super(TestAbstractUrl, cls).tearDownClass() def _get_default_partner_value(self): From 795ad60b66fcf123c731c40d0d0a725277591c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Thu, 6 Jun 2019 22:58:15 +0200 Subject: [PATCH 25/77] [REF] refactor url generator - add generic method - split the method to be able to customize easily the url key with the possibility to enjoy all the work that process record per lang and so enjoying the prefetching - fix onchange issue by skipping compute when calling from an onchange --- base_url/models/abstract_url.py | 61 ++++++++++++++++++++++++++--- base_url/tests/models.py | 6 --- base_url/tests/test_abstract_url.py | 35 ----------------- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index a72e910e584..5b25b58f953 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -3,6 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging +from collections import defaultdict from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -72,6 +73,45 @@ def _compute_is_urls_sync_required(self): for record in self: record.is_urls_sync_required = True + def _get_url_keywords(self): + """This method return a list of keyword that will be concatenated + with '-' to generate the url + Ex: if you return ['foo', '42'] the url will be foo-42 + + Note the self already include in the context the lang of the record + """ + self.ensure_one() + return [self.name] + + def _post_process_url_key(self, key): + """This method allow you to customized the url key. + you can use it to build full path be adding the url of parent record + Ex: key is 42 you can prefix it with "foo" and so return "foo/42" + + Note: the self do not include in the context the lang of the record + """ + self.ensure_one() + return key + + def _generic_compute_automatic_url_key(self): + records_by_lang = defaultdict(self.browse) + for record in self: + records_by_lang[record.lang_id] |= record + + key_by_id = {} + for lang_id, records in records_by_lang.items(): + for record in records.with_context(lang=lang_id.code): + if not isinstance(record.id, models.NewId): + key_by_id[record.id] = slugify( + "-".join(record._get_url_keywords()) + ) + + for record in self: + if not isinstance(record.id, models.NewId): + record.automatic_url_key = record._post_process_url_key( + key_by_id[record.id] + ) + def _compute_automatic_url_key(self): raise NotImplementedError( "Automatic url key must be computed in concrete model" @@ -195,11 +235,22 @@ def _sync_urls(self): record.set_url(record.url_key) return records - def _recompute_done(self, field): - super(AbstractUrl, self)._recompute_done(field) - if field.name == "is_urls_sync_required": - synced = self.exists()._sync_urls() - synced.write({"is_urls_sync_required": False}) + @api.model + def create(self, value): + res = super(AbstractUrl, self).create(value) + # without this flush we have cases where the url_key is not stored + self.flush() + synced = res._sync_urls() + super(AbstractUrl, synced).write({"is_urls_sync_required": False}) + return res + + def write(self, value): + res = super(AbstractUrl, self).write(value) + # without this flush we have cases where the url_key is not stored + self.flush() + synced = self._sync_urls() + super(AbstractUrl, synced).write({"is_urls_sync_required": False}) + return res def unlink(self): for record in self: diff --git a/base_url/tests/models.py b/base_url/tests/models.py index d7330369aec..dc9c25176e0 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -32,9 +32,3 @@ class ResPartnerAddressableFake(models.Model): @api.depends("lang_id", "special_code", "record_id.name") def _compute_automatic_url_key(self): self._generic_compute_automatic_url_key() - - def _get_url_keywords(self): - res = super(ResPartnerAddressableFake, self)._get_url_keywords() - if self.special_code: - res += [self.special_code] - return res diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 6f057a3f0d4..beefa0fa55b 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -125,38 +125,3 @@ def test_write_inactive(self): ) as mocked_redirect: my_partner.active = False mocked_redirect.assert_called_once() - - def test_onchange_do_nothing(self): - # calling onchange should not fail and should not return an url - result = self.ResPartnerAddressable.onchange( - {}, ["automatic_url_key"], {} - ) - self.assertEqual(result, {"value": {}}) - - def test_create_from_main(self): - partner = self.env["res.partner"].create({"name": self.name}) - partner.write( - { - "binding_ids": [ - ( - 0, - 0, - { - "lang_id": self.lang.id, - "url_builder": "auto", - "backend_id": self.url_backend.id, - }, - ) - ] - } - ) - self._check_url_key(partner.binding_ids, "partner-name") - - def test_write_from_main(self): - my_partner = self._create_auto() - my_partner.record_id.write( - {"binding_ids": [(1, my_partner.id, {"special_code": "foo"})]} - ) - self.assertEqual(2, len(my_partner.url_url_ids)) - url_keys = set(my_partner.mapped("url_url_ids.url_key")) - self.assertSetEqual(url_keys, {"partner-name-foo", "partner-name"}) From 9c339ccab1cfdc84e4fcea46ba6d5f8e22bfdebf Mon Sep 17 00:00:00 2001 From: HviorForgeFlow Date: Thu, 28 May 2020 15:14:15 +0200 Subject: [PATCH 26/77] fixup compute --- base_url/models/abstract_url.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 5b25b58f953..50e3d2c687a 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -111,6 +111,8 @@ def _generic_compute_automatic_url_key(self): record.automatic_url_key = record._post_process_url_key( key_by_id[record.id] ) + else: + record.automatic_url_key = False def _compute_automatic_url_key(self): raise NotImplementedError( From dadd374947c0b434391d0aaa2d1ea4abab6076cc Mon Sep 17 00:00:00 2001 From: shopinvader-git-bot Date: Fri, 5 Jun 2020 10:18:02 +0000 Subject: [PATCH 27/77] base_url 13.0.1.1.0 --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index cdc2bc3de61..389df25bd40 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "13.0.1.0.0", + "version": "13.0.1.1.0", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", From 441f501b61b1aa3c7f24f48e7b3e13febacf3144 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 29 Jun 2020 10:58:32 +0200 Subject: [PATCH 28/77] Add some TODO comments --- base_url/models/abstract_url.py | 1 + 1 file changed, 1 insertion(+) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 50e3d2c687a..865a6efa0a6 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -81,6 +81,7 @@ def _get_url_keywords(self): Note the self already include in the context the lang of the record """ self.ensure_one() + # TODO: IMO we should add the ID here by default to make sure the URL is always unique return [self.name] def _post_process_url_key(self, key): From a3103958ff1a434bc3b54f5434a0f81ffc217929 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 29 Jun 2020 10:58:10 +0200 Subject: [PATCH 29/77] shopinvader: fix category automatic url compute Computation was broken when the parent binding was archived. Moreover this commit eases overrides of depends for the computation. --- base_url/models/abstract_url.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 865a6efa0a6..db993247073 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -115,6 +115,10 @@ def _generic_compute_automatic_url_key(self): else: record.automatic_url_key = False + def _compute_automatic_url_key_depends(self): + return ["lang_id", "record_id.name"] + + @api.depends(lambda self: self._compute_automatic_url_key_depends()) def _compute_automatic_url_key(self): raise NotImplementedError( "Automatic url key must be computed in concrete model" From ad226324a75a6aa4aa6f1397de09149a8d1e4e4c Mon Sep 17 00:00:00 2001 From: shopinvader-git-bot Date: Thu, 23 Jul 2020 11:05:06 +0000 Subject: [PATCH 30/77] base_url 13.0.1.1.1 --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 389df25bd40..3a16aef623f 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "13.0.1.1.0", + "version": "13.0.1.1.1", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", From 907af44d0e382bc4b30a9c7778e7870cf61d992b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 8 Dec 2020 10:42:00 +0100 Subject: [PATCH 31/77] .pre-commit-config.yaml align w/ OCA and Fix linting --- base_url/models/abstract_url.py | 3 ++- base_url/tests/test_abstract_url.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index db993247073..db89cf1fceb 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -81,7 +81,8 @@ def _get_url_keywords(self): Note the self already include in the context the lang of the record """ self.ensure_one() - # TODO: IMO we should add the ID here by default to make sure the URL is always unique + # TODO: IMO we should add the ID here by default + # to make sure the URL is always unique return [self.name] def _post_process_url_key(self, key): diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index beefa0fa55b..33a4eb44e42 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -13,9 +13,9 @@ def setUpClass(cls): cls.loader = FakeModelLoader(cls.env, cls.__module__) cls.loader.backup_registry() from .models import ( - UrlBackendFake, ResPartner, ResPartnerAddressableFake, + UrlBackendFake, ) cls.loader.update_registry( From 4394f9c67b5b18540151b02d71f5f2cc754b8dd6 Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Thu, 21 Jan 2021 14:17:20 +0100 Subject: [PATCH 32/77] [IMP] base_url: black, isort, prettier --- base_url/__manifest__.py | 4 ++-- base_url/models/abstract_url.py | 22 ++++++---------------- base_url/models/url_url.py | 4 +--- base_url/tests/models_mixin.py | 11 +++-------- base_url/tests/test_abstract_url.py | 29 ++++++++--------------------- base_url/views/url_view.xml | 24 ++++++++++++++---------- 6 files changed, 34 insertions(+), 60 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 3a16aef623f..daf9cc24fe5 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -10,11 +10,11 @@ "license": "AGPL-3", "summary": "keep history of url for products & categories ", "author": "Akretion, ACSONE SA/NV", - "website": "https://github.com/shopinvader", + "website": "https://github.com/shopinvader/odoo-shopinvader", # any module necessary for this one to work correctly "depends": ["base"], "external_dependencies": {"python": ["python-slugify"]}, "data": ["views/url_view.xml", "security/ir.model.access.csv"], "url": "", - "installable": True, + "installable": False, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index db89cf1fceb..9d9e3e52870 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -27,13 +27,9 @@ class AbstractUrl(models.AbstractModel): url_builder = fields.Selection( selection=[("auto", "Automatic"), ("manual", "Manual")], default="auto" ) - automatic_url_key = fields.Char( - compute="_compute_automatic_url_key", store=True - ) + automatic_url_key = fields.Char(compute="_compute_automatic_url_key", store=True) manual_url_key = fields.Char() - url_key = fields.Char( - string="Url key", compute="_compute_url_key", store=True - ) + url_key = fields.Char(string="Url key", compute="_compute_url_key", store=True) url_url_ids = fields.One2many( compute="_compute_url_url_ids", comodel_name="url.url" ) @@ -104,9 +100,7 @@ def _generic_compute_automatic_url_key(self): for lang_id, records in records_by_lang.items(): for record in records.with_context(lang=lang_id.code): if not isinstance(record.id, models.NewId): - key_by_id[record.id] = slugify( - "-".join(record._get_url_keywords()) - ) + key_by_id[record.id] = slugify("-".join(record._get_url_keywords())) for record in self: if not isinstance(record.id, models.NewId): @@ -125,9 +119,7 @@ def _compute_automatic_url_key(self): "Automatic url key must be computed in concrete model" ) - @api.depends( - "manual_url_key", "automatic_url_key", "url_builder", "active" - ) + @api.depends("manual_url_key", "automatic_url_key", "url_builder", "active") def _compute_url_key(self): for record in self: if not record.active: @@ -164,12 +156,10 @@ def _prepare_url(self, url_key): def _reuse_url(self, existing_url): # TODO add user notification in the futur SEO dashboard - existing_url.write( - {"model_id": get_model_ref(self), "redirect": False} - ) + existing_url.write({"model_id": get_model_ref(self), "redirect": False}) def set_url(self, url_key): - """ Se a new url + """Se a new url backup old url 1 find url redirect true and same model_id diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 5b22c8557ad..0b55f7e3448 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -24,9 +24,7 @@ class UrlUrl(models.Model): string="Model", required=True, ) - redirect = fields.Boolean( - help="If tick this url is a redirection to the new url" - ) + redirect = fields.Boolean(help="If tick this url is a redirection to the new url") backend_id = fields.Reference( selection="_selection_target_model", compute="_compute_related_fields", diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py index 80d86160af9..6ef504673bd 100644 --- a/base_url/tests/models_mixin.py +++ b/base_url/tests/models_mixin.py @@ -66,17 +66,14 @@ def _test_teardown_model(cls, env): # models parents = cls._inherit parents = ( - [parents] - if isinstance(parents, (str, bytes)) - else (parents or []) + [parents] if isinstance(parents, (str, bytes)) else (parents or []) ) # kepp a copy to be sure to not modify the original _inherit parents = list(parents) parents.extend(cls._inherits.keys()) parents.append("base") funcs = [ - attrgetter(kind + "_children") - for kind in ["_inherits", "_inherit"] + attrgetter(kind + "_children") for kind in ["_inherits", "_inherit"] ] for parent in parents: for func in funcs: @@ -87,9 +84,7 @@ def _test_teardown_model(cls, env): children.remove(cls._name) def _test_get_model_id(self): - self.env.cr.execute( - "SELECT id FROM ir_model WHERE model = %s", (self._name,) - ) + self.env.cr.execute("SELECT id FROM ir_model WHERE model = %s", (self._name,)) res = self.env.cr.fetchone() return res[0] if res else None diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 33a4eb44e42..9fdb9b91e3d 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -1,9 +1,10 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock +from odoo_test_helper import FakeModelLoader + from odoo.exceptions import ValidationError from odoo.tests import SavepointCase -from odoo_test_helper import FakeModelLoader class TestAbstractUrl(SavepointCase, FakeModelLoader): @@ -12,11 +13,7 @@ def setUpClass(cls): super(TestAbstractUrl, cls).setUpClass() cls.loader = FakeModelLoader(cls.env, cls.__module__) cls.loader.backup_registry() - from .models import ( - ResPartner, - ResPartnerAddressableFake, - UrlBackendFake, - ) + from .models import ResPartner, ResPartnerAddressableFake, UrlBackendFake cls.loader.update_registry( (UrlBackendFake, ResPartner, ResPartnerAddressableFake) @@ -25,9 +22,7 @@ def setUpClass(cls): cls.lang = cls.env.ref("base.lang_en") cls.UrlUrl = cls.env["url.url"] cls.ResPartnerAddressable = cls.env["res.partner.addressable.fake"] - cls.url_backend = cls.env["url.backend.fake"].create( - {"name": "fake backend"} - ) + cls.url_backend = cls.env["url.backend.fake"].create({"name": "fake backend"}) cls.name = "partner name" cls.auto_key = "partner-name" @@ -45,9 +40,7 @@ def _get_default_partner_value(self): } def _create_auto(self): - return self.ResPartnerAddressable.create( - self._get_default_partner_value() - ) + return self.ResPartnerAddressable.create(self._get_default_partner_value()) def _check_url_key(self, partner_addressable, key_name): self.assertFalse(partner_addressable.is_urls_sync_required) @@ -76,9 +69,7 @@ def test_create_manual_url_contrains(self): def test_create_manual_url(self): value = self._get_default_partner_value() manual_url_key = "manual-url key" - value.update( - {"url_builder": "manual", "manual_url_key": manual_url_key} - ) + value.update({"url_builder": "manual", "manual_url_key": manual_url_key}) my_partner = self.ResPartnerAddressable.create(value) self.assertTrue(my_partner) self._check_url_key(my_partner, manual_url_key) @@ -87,18 +78,14 @@ def test_write_url_builder_constrains(self): my_partner = self._create_auto() with self.assertRaises(ValidationError): my_partner.url_builder = "manual" - my_partner.write( - {"url_builder": "manual", "manual_url_key": "manual url key"} - ) + my_partner.write({"url_builder": "manual", "manual_url_key": "manual url key"}) def test_write_url_builder(self): # tests that a new url is created we update the url builder my_partner = self._create_auto() self._check_url_key(my_partner, "partner-name") manual_url_key = "manual-url key" - my_partner.write( - {"url_builder": "manual", "manual_url_key": manual_url_key} - ) + my_partner.write({"url_builder": "manual", "manual_url_key": manual_url_key}) my_partner.refresh() url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index 1c2e21df4a0..3767cceed85 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -1,4 +1,4 @@ - + @@ -7,11 +7,11 @@
- - + + - - + +
@@ -22,9 +22,9 @@ url.url - - - + + +
@@ -35,7 +35,11 @@ tree,form - +
From e767f6c93436bd17090394bfde0ae1dd33b2a88e Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Thu, 21 Jan 2021 14:17:20 +0100 Subject: [PATCH 33/77] [MIG] base_url: Migration to 14.0 --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index daf9cc24fe5..97fda75e885 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "13.0.1.1.1", + "version": "14.0.1.0.0", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", From 1c1ec770e27fbcfd9e2eab4bb884bebd8b6ace4f Mon Sep 17 00:00:00 2001 From: oca-ci Date: Sun, 21 Mar 2021 15:38:24 +0000 Subject: [PATCH 34/77] [UPD] Update base_url.pot --- base_url/i18n/base_url.pot | 190 +++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 base_url/i18n/base_url.pot diff --git a/base_url/i18n/base_url.pot b/base_url/i18n/base_url.pot new file mode 100644 index 00000000000..76e94552b5a --- /dev/null +++ b/base_url/i18n/base_url.pot @@ -0,0 +1,190 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_url +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_url +#: model:ir.model,name:base_url.model_abstract_url +msgid "Abstract Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__active +msgid "Active" +msgstr "" + +#. module: base_url +#: model:ir.model.constraint,message:base_url.constraint_url_url_unique_key_per_backend_per_lang +msgid "Already exists in database" +msgstr "" + +#. module: base_url +#: model:ir.model.fields.selection,name:base_url.selection__abstract_url__url_builder__auto +msgid "Automatic" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__automatic_url_key +msgid "Automatic Url Key" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__backend_id +msgid "Backend" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__backend_id +msgid "Backend linked to this URL" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_uid +msgid "Created by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__create_date +msgid "Created on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__display_name +#: model:ir.model.fields,field_description:base_url.field_url_url__display_name +msgid "Display Name" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__id +#: model:ir.model.fields,field_description:base_url.field_url_url__id +msgid "ID" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__redirect +msgid "If tick this url is a redirection to the new url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__is_urls_sync_required +msgid "Is Urls Sync Required" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__lang_id +#: model:ir.model.fields,field_description:base_url.field_url_url__lang_id +msgid "Lang" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url____last_update +#: model:ir.model.fields,field_description:base_url.field_url_url____last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_url +#: model:ir.model.fields.selection,name:base_url.selection__abstract_url__url_builder__manual +msgid "Manual" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__manual_url_key +msgid "Manual Url Key" +msgstr "" + +#. module: base_url +#: code:addons/base_url/models/abstract_url.py:0 +#, python-format +msgid "Manual url key is required if builder is set to manual" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__model_id +msgid "Model" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__redirect +msgid "Redirect" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__redirect_url_url_ids +msgid "Redirect Url Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__model_id +msgid "The id of content linked to the url." +msgstr "" + +#. module: base_url +#: model:ir.model,name:base_url.model_url_url +msgid "Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_builder +msgid "Url Builder" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_url_url__url_key +msgid "Url Key" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_url_ids +msgid "Url Url" +msgstr "" + +#. module: base_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_key +msgid "Url key" +msgstr "" + +#. module: base_url +#: model_terms:ir.ui.view,arch_db:base_url.url_view_form +msgid "Url rewriting edit form" +msgstr "" + +#. module: base_url +#: code:addons/base_url/models/abstract_url.py:0 +#, python-format +msgid "" +"Url_key already exist in other model\n" +"- name: %s\n" +" - id: %s\n" +"- url_key: %s\n" +" - url_key_id %s" +msgstr "" + +#. module: base_url +#: model:ir.ui.menu,name:base_url.url_menu_view +msgid "Url_rewrite" +msgstr "" + +#. module: base_url +#: model:ir.actions.act_window,name:base_url.base_url_action_view +msgid "url list Viewer" +msgstr "" From f58e7bdb9ae73ae2976a993ea13b3c9d45bd1a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Sat, 17 Jul 2021 00:10:20 +0200 Subject: [PATCH 35/77] [IMP] add broken test for missing redirect url on categ --- base_url/tests/test_abstract_url.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 9fdb9b91e3d..8e77e16b4ac 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -104,6 +104,14 @@ def test_write_launching_automatic_url_key(self): url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) + def test_write_on_related_record_launching_automatic_url_key(self): + my_partner = self._create_auto() + my_partner.record_id.name = "my new name" + my_partner.refresh() + self.assertEqual(2, len(my_partner.url_url_ids)) + url_keys = set(my_partner.mapped("url_url_ids.url_key")) + self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) + def test_write_inactive(self): my_partner = self._create_auto() # when we deactivate a record, the redirect method should be called From edc206fc9e34725d5340e272adad432b2beedd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Sat, 17 Jul 2021 01:28:03 +0200 Subject: [PATCH 36/77] [REF] base_url refactor and fix url redirection --- base_url/__manifest__.py | 2 +- base_url/i18n/base_url.pot | 5 --- base_url/models/abstract_url.py | 47 +++++------------------------ base_url/tests/test_abstract_url.py | 11 ++++--- 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 97fda75e885..4c5086a8661 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", diff --git a/base_url/i18n/base_url.pot b/base_url/i18n/base_url.pot index 76e94552b5a..b71f6f58c45 100644 --- a/base_url/i18n/base_url.pot +++ b/base_url/i18n/base_url.pot @@ -75,11 +75,6 @@ msgstr "" msgid "If tick this url is a redirection to the new url" msgstr "" -#. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__is_urls_sync_required -msgid "Is Urls Sync Required" -msgstr "" - #. module: base_url #: model:ir.model.fields,field_description:base_url.field_abstract_url__lang_id #: model:ir.model.fields,field_description:base_url.field_url_url__lang_id diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 9d9e3e52870..a553f2328c0 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -37,9 +37,6 @@ class AbstractUrl(models.AbstractModel): compute="_compute_redirect_url_url_ids", comodel_name="url.url" ) lang_id = fields.Many2one("res.lang", string="Lang", required=True) - is_urls_sync_required = fields.Boolean( - compute="_compute_is_urls_sync_required", store=True - ) active = fields.Boolean(string="Active", default=True) @api.constrains("url_builder", "manual_url_key") @@ -64,11 +61,6 @@ def on_url_key_change(self): } } - @api.depends("url_key") - def _compute_is_urls_sync_required(self): - for record in self: - record.is_urls_sync_required = True - def _get_url_keywords(self): """This method return a list of keyword that will be concatenated with '-' to generate the url @@ -124,14 +116,19 @@ def _compute_url_key(self): for record in self: if not record.active: record.url_key = "" + record._redirect_existing_url() else: if record.url_builder == "manual": new_url = record.manual_url_key else: new_url = record.automatic_url_key - record.url_key = new_url + if record.url_key != new_url: + record.url_key = new_url + record.set_url(record.url_key) + @api.depends("url_key") def _compute_redirect_url_url_ids(self): + self.flush() for record in self: record.redirect_url_url_ids = record.env["url.url"].search( [ @@ -140,7 +137,9 @@ def _compute_redirect_url_url_ids(self): ] ) + @api.depends("url_key") def _compute_url_url_ids(self): + self.flush() for record in self: record.url_url_ids = record.env["url.url"].search( [("model_id", "=", get_model_ref(record))] @@ -220,36 +219,6 @@ def _redirect_existing_url(self): """ return True - def _sync_urls(self): - """ - This method is in charge of syncing the url.url object related to - the current model - """ - records = self.filtered("is_urls_sync_required") - for record in records: - if not record.active: - record._redirect_existing_url() - else: - record.set_url(record.url_key) - return records - - @api.model - def create(self, value): - res = super(AbstractUrl, self).create(value) - # without this flush we have cases where the url_key is not stored - self.flush() - synced = res._sync_urls() - super(AbstractUrl, synced).write({"is_urls_sync_required": False}) - return res - - def write(self, value): - res = super(AbstractUrl, self).write(value) - # without this flush we have cases where the url_key is not stored - self.flush() - synced = self._sync_urls() - super(AbstractUrl, synced).write({"is_urls_sync_required": False}) - return res - def unlink(self): for record in self: # TODO we should propose to redirect the old url diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 8e77e16b4ac..397b6cb2c95 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -43,7 +43,6 @@ def _create_auto(self): return self.ResPartnerAddressable.create(self._get_default_partner_value()) def _check_url_key(self, partner_addressable, key_name): - self.assertFalse(partner_addressable.is_urls_sync_required) self.assertEqual(partner_addressable.url_key, key_name) self.assertEqual(len(partner_addressable.url_url_ids), 1) url_url = partner_addressable.url_url_ids @@ -86,28 +85,28 @@ def test_write_url_builder(self): self._check_url_key(my_partner, "partner-name") manual_url_key = "manual-url key" my_partner.write({"url_builder": "manual", "manual_url_key": manual_url_key}) - my_partner.refresh() url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) # if we reset the auto key, no new url.url should be created my_partner.write({"url_builder": "auto"}) - my_partner.refresh() self.assertEqual(2, len(my_partner.url_url_ids)) url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) def test_write_launching_automatic_url_key(self): my_partner = self._create_auto() + # call flush to force to apply the recompute + my_partner.flush() my_partner.name = "my new name" - my_partner.refresh() self.assertEqual(2, len(my_partner.url_url_ids)) url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) def test_write_on_related_record_launching_automatic_url_key(self): my_partner = self._create_auto() + # call flush to force to apply the recompute + my_partner.flush() my_partner.record_id.name = "my new name" - my_partner.refresh() self.assertEqual(2, len(my_partner.url_url_ids)) url_keys = set(my_partner.mapped("url_url_ids.url_key")) self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) @@ -119,4 +118,6 @@ def test_write_inactive(self): self.ResPartnerAddressable.__class__, "_redirect_existing_url" ) as mocked_redirect: my_partner.active = False + # call flush to force to apply the recompute + my_partner.flush() mocked_redirect.assert_called_once() From 264a70d3606534005872996dae63bc544a010c70 Mon Sep 17 00:00:00 2001 From: Xavier Bouquiaux Date: Wed, 5 Apr 2023 15:53:31 +0200 Subject: [PATCH 37/77] [IMP] base_url : add index performance issue, add index --- base_url/__manifest__.py | 2 +- base_url/models/url_url.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 4c5086a8661..89512475cbc 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "14.0.1.0.1", + "version": "14.0.1.0.2", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 0b55f7e3448..407843cf85c 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -23,6 +23,7 @@ class UrlUrl(models.Model): readonly=True, string="Model", required=True, + index=True, ) redirect = fields.Boolean(help="If tick this url is a redirection to the new url") backend_id = fields.Reference( From 6832dd2f8017c7be8d9c7737570c7b241a180a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 23 Jun 2023 21:49:33 +0200 Subject: [PATCH 38/77] base_url: pre-commit stuff, remove deprecated file --- base_url/__manifest__.py | 4 +- base_url/models/abstract_url.py | 16 ++--- base_url/tests/models_mixin.py | 111 -------------------------------- base_url/views/url_view.xml | 2 +- 4 files changed, 11 insertions(+), 122 deletions(-) delete mode 100644 base_url/tests/models_mixin.py diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 89512475cbc..b39459b91e9 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "14.0.1.0.2", + "version": "16.0.1.0.2", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", @@ -16,5 +16,5 @@ "external_dependencies": {"python": ["python-slugify"]}, "data": ["views/url_view.xml", "security/ir.model.access.csv"], "url": "", - "installable": False, + "installable": True, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index a553f2328c0..3ec3857f6a9 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -37,7 +37,7 @@ class AbstractUrl(models.AbstractModel): compute="_compute_redirect_url_url_ids", comodel_name="url.url" ) lang_id = fields.Many2one("res.lang", string="Lang", required=True) - active = fields.Boolean(string="Active", default=True) + active = fields.Boolean(default=True) @api.constrains("url_builder", "manual_url_key") def _check_manual_url_key(self): @@ -183,14 +183,14 @@ def set_url(self, url_key): raise UserError( _( "Url_key already exist in other model" - "\n- name: %s\n - id: %s\n" - "- url_key: %s\n - url_key_id %s" + "\n- name: %(model_name)s\n - id: %(model_id)s\n" + "- url_key: %(url_key)s\n - url_key_id %(url_id)s" ) - % ( - existing_url.model_id.name, - existing_url.model_id.id, - existing_url.url_key, - existing_url.id, + % dict( + model_name=existing_url.model_id.name, + model_id=existing_url.model_id.id, + url_key=existing_url.url_key, + url_id=existing_url.id, ) ) else: diff --git a/base_url/tests/models_mixin.py b/base_url/tests/models_mixin.py deleted file mode 100644 index 6ef504673bd..00000000000 --- a/base_url/tests/models_mixin.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2018 Simone Orsi - Camptocamp SA -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from operator import attrgetter - -import mock - - -class TestMixin(object): - """Mixin to setup fake models for tests. - - Usage - the model: - - class FakeModel(models.Model, TestMixin): - _name = 'fake.model' - - name = fields.Char() - - Usage - the test klass: - - @classmethod - def setUpClass(cls): - super().setUpClass() - FakeModel._test_setup_model(cls.env) - - @classmethod - def tearDownClass(cls): - FakeModel._test_teardown_model(cls.env) - super().tearDownClass() - """ - - # Generate xmlids - # This is needed if you want to load data tied to a test model via xid. - _test_setup_gen_xid = False - # If you extend a real model (ie: res.partner) you must enable this - # to not delete the model on tear down. - _test_teardown_no_delete = False - # You can add custom fields to real models (eg: res.partner). - # In this case you must delete them to leave registry and model clean. - # This is mandatory for relational fields that link a fake model. - _test_purge_fields = [] - - @classmethod - def _test_setup_model(cls, env): - """Initialize it.""" - with mock.patch.object(env.cr, "commit"): - cls._build_model(env.registry, env.cr) - env.registry.setup_models(env.cr) - ctx = dict(env.context, update_custom_fields=True) - if cls._test_setup_gen_xid: - ctx["module"] = cls._module - env.registry.init_models(env.cr, [cls._name], ctx) - - @classmethod - def _test_teardown_model(cls, env): - """Cleanup registry and real models.""" - - for fname in cls._test_purge_fields: - model = env[cls._name] - if fname in model: - model._pop_field(fname) - model._proper_fields.remove(fname) - - if not getattr(cls, "_test_teardown_no_delete", False): - del env.registry.models[cls._name] - # here we must remove the model from list of children of inherited - # models - parents = cls._inherit - parents = ( - [parents] if isinstance(parents, (str, bytes)) else (parents or []) - ) - # kepp a copy to be sure to not modify the original _inherit - parents = list(parents) - parents.extend(cls._inherits.keys()) - parents.append("base") - funcs = [ - attrgetter(kind + "_children") for kind in ["_inherits", "_inherit"] - ] - for parent in parents: - for func in funcs: - children = func(env.registry[parent]) - if cls._name in children: - # at this stage our cls is referenced as children of - # parent -> must un reference it - children.remove(cls._name) - - def _test_get_model_id(self): - self.env.cr.execute("SELECT id FROM ir_model WHERE model = %s", (self._name,)) - res = self.env.cr.fetchone() - return res[0] if res else None - - def _test_create_ACL(self, **kw): - model_id = self._test_get_model_id() - if not model_id: - self._reflect() - model_id = self._test_get_model_id() - if model_id: - vals = self._test_ACL_values(model_id) - vals.update(kw) - self.env["ir.model.access"].create(vals) - - def _test_ACL_values(self, model_id): - values = { - "name": "Fake ACL for %s" % self._name, - "model_id": model_id, - "perm_read": 1, - "perm_create": 1, - "perm_write": 1, - "perm_unlink": 1, - "active": True, - } - return values diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index 3767cceed85..a681a1d6a27 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -21,7 +21,7 @@ url.url - + From 9df3e6d042049ce121ba9fe2dbfe8d32aec8686f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 26 Jun 2023 23:42:16 +0200 Subject: [PATCH 39/77] base_url: Refactor implementation Now we do not have binding anymore, need a new implementation --- base_url/__manifest__.py | 6 +- base_url/models/abstract_url.py | 336 +++++++++++++------------- base_url/models/url_url.py | 80 +++--- base_url/security/ir.model.access.csv | 3 +- base_url/security/res_groups.xml | 12 + base_url/tests/models.py | 37 +-- base_url/tests/test_abstract_url.py | 196 +++++++-------- base_url/views/url_view.xml | 19 +- 8 files changed, 355 insertions(+), 334 deletions(-) create mode 100644 base_url/security/res_groups.xml diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index b39459b91e9..88bc7ee0dd1 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -14,7 +14,11 @@ # any module necessary for this one to work correctly "depends": ["base"], "external_dependencies": {"python": ["python-slugify"]}, - "data": ["views/url_view.xml", "security/ir.model.access.csv"], + "data": [ + "views/url_view.xml", + "security/res_groups.xml", + "security/ir.model.access.csv", + ], "url": "", "installable": True, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 3ec3857f6a9..ac92bbe57f5 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -3,10 +3,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from collections import defaultdict + +from lxml import etree from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import UserError + +DEFAULT_LANG = "en_US" _logger = logging.getLogger(__name__) @@ -16,215 +19,212 @@ _logger.debug("Cannot `import slugify`.") -def get_model_ref(record): - return "{},{}".format(record._name, record.id) +SMART_BUTTON = """ +""" class AbstractUrl(models.AbstractModel): _name = "abstract.url" _description = "Abstract Url" + _have_url = True - url_builder = fields.Selection( - selection=[("auto", "Automatic"), ("manual", "Manual")], default="auto" - ) - automatic_url_key = fields.Char(compute="_compute_automatic_url_key", store=True) - manual_url_key = fields.Char() - url_key = fields.Char(string="Url key", compute="_compute_url_key", store=True) - url_url_ids = fields.One2many( - compute="_compute_url_url_ids", comodel_name="url.url" - ) - redirect_url_url_ids = fields.One2many( - compute="_compute_redirect_url_url_ids", comodel_name="url.url" + url_ids = fields.One2many("url.url", "res_id") + url_need_refresh = fields.Boolean( + compute="_compute_url_need_refresh", store=True, readonly=False ) - lang_id = fields.Many2one("res.lang", string="Lang", required=True) - active = fields.Boolean(default=True) - - @api.constrains("url_builder", "manual_url_key") - def _check_manual_url_key(self): - for rec in self: - if rec.url_builder == "manual" and not rec.manual_url_key: - raise ValidationError( - _("Manual url key is required if builder is set to manual") - ) + count_url = fields.Integer(compute="_compute_count_url") + + def _compute_count_url(self): + res = self.env["url.url"].read_group( + domain=[ + ("res_id", "in", self.ids), + ("res_model", "=", self._name), + ], + fields=["res_id"], + groupby=["res_id"], + ) + id2count = {item["res_id"]: item["res_id_count"] for item in res} + for record in self: + record.count_url = id2count.get(record.id, 0) - @api.onchange("manual_url_key") - def on_url_key_change(self): - self.ensure_one() - if self.manual_url_key: - url = slugify(self.manual_url_key) - if url != self.manual_url_key: - self.manual_url_key = url - return { - "warning": { - "title": "Adapt text rules", - "message": "it will be adapted to %s" % url, - } - } - - def _get_url_keywords(self): - """This method return a list of keyword that will be concatenated + def _compute_url_need_refresh_depends(self): + return self._get_keyword_fields() + + @api.depends(lambda self: self._compute_url_need_refresh_depends()) + def _compute_url_need_refresh(self): + for record in self: + record.url_need_refresh = True + + def _get_keyword_fields(self): + """This method return a list of field that will be concatenated with '-' to generate the url - Ex: if you return ['foo', '42'] the url will be foo-42 + Ex: if you return ['name', 'code'] the url will be f"{record.name}-{record.code}" - Note the self already include in the context the lang of the record + Note: the self already include in the context the lang of the record + Note: you can return key like in depends ex: ["categ_id.name", "code"] """ - self.ensure_one() # TODO: IMO we should add the ID here by default # to make sure the URL is always unique - return [self.name] - - def _post_process_url_key(self, key): - """This method allow you to customized the url key. - you can use it to build full path be adding the url of parent record - Ex: key is 42 you can prefix it with "foo" and so return "foo/42" + # seb.beau: not sure, most of site do not have id in url + # the url have an seo impact so it's better to only put meaning information + # moreover for unicity you can put the default code of the product or ean13 + return ["name"] + + def _generate_url_key(self, referential, lang): + def get(self, key_path): + value = self + for key_field in key_path.split("."): + value = value[key_field] + return value - Note: the self do not include in the context the lang of the record - """ self.ensure_one() - return key - - def _generic_compute_automatic_url_key(self): - records_by_lang = defaultdict(self.browse) - for record in self: - records_by_lang[record.lang_id] |= record - - key_by_id = {} - for lang_id, records in records_by_lang.items(): - for record in records.with_context(lang=lang_id.code): - if not isinstance(record.id, models.NewId): - key_by_id[record.id] = slugify("-".join(record._get_url_keywords())) - - for record in self: - if not isinstance(record.id, models.NewId): - record.automatic_url_key = record._post_process_url_key( - key_by_id[record.id] - ) - else: - record.automatic_url_key = False - - def _compute_automatic_url_key_depends(self): - return ["lang_id", "record_id.name"] - - @api.depends(lambda self: self._compute_automatic_url_key_depends()) - def _compute_automatic_url_key(self): - raise NotImplementedError( - "Automatic url key must be computed in concrete model" + return slugify( + "-".join([get(self, k) for k in self._get_keyword_fields() if get(self, k)]) ) - @api.depends("manual_url_key", "automatic_url_key", "url_builder", "active") - def _compute_url_key(self): - for record in self: - if not record.active: - record.url_key = "" - record._redirect_existing_url() - else: - if record.url_builder == "manual": - new_url = record.manual_url_key - else: - new_url = record.automatic_url_key - if record.url_key != new_url: - record.url_key = new_url - record.set_url(record.url_key) - - @api.depends("url_key") - def _compute_redirect_url_url_ids(self): - self.flush() - for record in self: - record.redirect_url_url_ids = record.env["url.url"].search( - [ - ("model_id", "=", get_model_ref(record)), - ("redirect", "=", True), - ] + def _get_redirect_urls(self, referential, lang): + self.ensure_one() + return self.url_ids.filtered( + lambda s: ( + s.lang_id.code == lang and s.referential == referential and s.redirect ) + ) - @api.depends("url_key") - def _compute_url_url_ids(self): - self.flush() - for record in self: - record.url_url_ids = record.env["url.url"].search( - [("model_id", "=", get_model_ref(record))] + def _get_main_url(self, referential, lang): + self.ensure_one() + return self.url_ids.filtered( + lambda s: ( + s.lang_id.code == lang + and s.referential == referential + and not s.redirect ) + ) @api.model - def _prepare_url(self, url_key): + def _prepare_url(self, referential, lang, url_key): return { - "url_key": url_key, + "key": url_key, "redirect": False, - "model_id": get_model_ref(self), + "res_model": self._name, + "res_id": self.id, + "referential": referential, + "lang_id": self.env["res.lang"]._lang_get_id(lang), + "manual": False, } def _reuse_url(self, existing_url): # TODO add user notification in the futur SEO dashboard - existing_url.write({"model_id": get_model_ref(self), "redirect": False}) - - def set_url(self, url_key): - """Se a new url - backup old url - - 1 find url redirect true and same model_id - if other model id refuse - 2 if exists set to False + existing_url.write( + { + "res_model": self._name, + "res_id": self.id, + "redirect": False, + } + ) - 3 write the new one - """ + def _update_url_key(self, referential="global", lang=DEFAULT_LANG): + for record in self.with_context(lang=lang): + # TODO maybe we should have a computed field that flag the + # current url if the key used for building the url have changed + # so we can skip this check if nothing have changed + current_url = record._get_main_url(referential, lang) + if not current_url.manual: + # Updating an url is done for a specific context + # a lang and a referential + # if something have changed on the record the url_need_refresh + # is flagged. + # Before updating one specific url (referential + lang) + # the flag is propagated on all valid url + if record.url_need_refresh: + record.url_ids.filtered( + lambda s: not s.redirect and not s.manual + ).write({"need_refresh": True}) + if not current_url or current_url.need_refresh: + current_url.need_refresh = False + url_key = record._generate_url_key(referential, lang) + # maybe some change have been done but the url is the same + # so check it + if current_url.key != url_key: + current_url.redirect = True + record._add_url(referential, lang, url_key) + record.url_need_refresh = False + + def _add_url(self, referential, lang, url_key): self.ensure_one() existing_url = self.env["url.url"].search( [ - ("url_key", "=", url_key), - ("backend_id", "=", get_model_ref(self.backend_id)), - ("lang_id", "=", self.lang_id.id), + ("referential", "=", referential), + ("lang_id.code", "=", lang), + ("key", "=", url_key), ] ) if existing_url: - if self != existing_url.model_id: - if existing_url.redirect: - self._reuse_url(existing_url) - else: - raise UserError( - _( - "Url_key already exist in other model" - "\n- name: %(model_name)s\n - id: %(model_id)s\n" - "- url_key: %(url_key)s\n - url_key_id %(url_id)s" - ) - % dict( - model_name=existing_url.model_id.name, - model_id=existing_url.model_id.id, - url_key=existing_url.url_key, - url_id=existing_url.id, - ) - ) + if existing_url.redirect: + self._reuse_url(existing_url) else: - existing_url.write({"redirect": False}) + raise UserError( + _( + "Url_key already exist in other model" + "\n- name: %(model_name)s\n - id: %(model_id)s\n" + "- url_key: %(url_key)s\n - url_key_id %(url_id)s" + ) + % dict( + model_name=existing_url.model_id.name, + model_id=existing_url.model_id.id, + url_key=existing_url.url_key, + url_id=existing_url.id, + ) + ) + else: - # no existing key creating one if not empty - self.env["url.url"].create(self._prepare_url(url_key)) - # other url of object set redirect to True - redirect_urls = self.env["url.url"].search( - [ - ("model_id", "=", get_model_ref(self)), - ("url_key", "!=", url_key), - ("redirect", "=", False), - ] - ) - redirect_urls.write({"redirect": True}) - # we must explicitly invalidate the cache since there is no depends - # defined on this computed fields and this field could have already - # been loaded into the cache - self.invalidate_cache(fnames=["url_url_ids"], ids=self.ids) + vals = self._prepare_url(referential, lang, url_key) + self.env["url.url"].create(vals) - def _redirect_existing_url(self): + def _redirect_existing_url(self, action): """ This method is called when the record is deactivated to give a chance to the concrete model to implement a redirect strategy + action can be "archived" or "unlink" """ return True def unlink(self): for record in self: - # TODO we should propose to redirect the old url - urls = record.env["url.url"].search( - [("model_id", "=", get_model_ref(record))] - ) - urls.unlink() - self.flush() - return super(AbstractUrl, self).unlink() + record._redirect_existing_url("unlink") + # Remove dead url that have been not redirected + record.url_ids.unlink() + return super().unlink() + + def write(self, vals): + res = super().write(vals) + if "active" in vals and not vals["active"]: + self._redirect_existing_url("archived") + return res + + @api.model + def _get_view(self, view_id=None, view_type="form", **options): + arch, view = super()._get_view(view_id=view_id, view_type=view_type, **options) + button_box = arch.xpath("//div[@name='button_box']") + if button_box: + button_box[0].append(etree.fromstring(SMART_BUTTON)) + return arch, view + + def open_url(self): + self.ensure_one() + action = self.env.ref("shopinvader_base_url.base_url_action_view").read()[0] + action["domain"] = [("res_model", "=", self._name), ("res_id", "in", self.ids)] + action["context"] = { + "hide_res_model": True, + "hide_res_id": True, + "default_res_model": self._name, + "default_res_id": self.id, + } + return action diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index 407843cf85c..e7f2c72b380 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -4,64 +4,70 @@ import logging -from odoo import api, fields, models - -from .abstract_url import get_model_ref +from odoo import api, fields, models, tools _logger = logging.getLogger(__name__) class UrlUrl(models.Model): - _name = "url.url" _description = "Url" + _order = "res_model,res_id,redirect desc" - url_key = fields.Char(required=True) - model_id = fields.Reference( - selection="_selection_target_model", - help="The id of content linked to the url.", + manual = fields.Boolean(default=True, readonly=True) + key = fields.Char(required=True, index=True) + res_id = fields.Many2oneReference( + string="Record ID", + help="ID of the target record in the database", + model_field="res_model", readonly=True, - string="Model", - required=True, index=True, ) - redirect = fields.Boolean(help="If tick this url is a redirection to the new url") - backend_id = fields.Reference( - selection="_selection_target_model", - compute="_compute_related_fields", - store=True, - help="Backend linked to this URL", - string="Backend", + res_model = fields.Selection( + selection=lambda s: s._get_model_with_url_selection(), readonly=True, index=True ) - lang_id = fields.Many2one( - "res.lang", "Lang", compute="_compute_related_fields", store=True + redirect = fields.Boolean(help="If tick this url is a redirection to the new url") + referential = fields.Selection( + selection=lambda s: s._get_all_referential(), + index=True, + default="global", + required=True, ) + lang_id = fields.Many2one("res.lang", "Lang", index=True, required=True) + need_refresh = fields.Boolean() _sql_constraints = [ ( - "unique_key_per_backend_per_lang", - "unique(url_key, backend_id, lang_id)", + "unique_key_per_referential_per_lang", + "unique(key, referential, lang_id)", "Already exists in database", ) ] - @api.model - def _selection_target_model(self): - models = self.env["ir.model"].search([]) - return [(model.model, model.name) for model in models] - - @api.depends("model_id") - def _compute_related_fields(self): - for record in self: - record.backend_id = get_model_ref(record.model_id.backend_id) - record.lang_id = record.model_id.lang_id + def init(self): + self.env.cr.execute( + f"""CREATE UNIQUE INDEX IF NOT EXISTS main_url_uniq + ON {self._table} (referential, lang_id, res_id, res_model) + WHERE redirect = False""" + ) + return super().init() + @tools.ormcache() @api.model - def _reference_models(self): - return [] + def _get_model_with_url_selection(self): + return [ + (model, self.env[model]._description) + for model in self.env + if ( + hasattr(self.env[model], "_have_url") + and not self.env[model]._abstract + and not self.env[model]._transient + ) + ] - def _get_object(self, url): - """ - :return: return object attach to the url + def _get_all_referential(self): + """Return the list of referential for your url, by default it's global + but you can do your own implementation to have url per search engine + index for example """ - return self.search([("url_key", "=", url)]).model_id + return [("global", "Global")] diff --git a/base_url/security/ir.model.access.csv b/base_url/security/ir.model.access.csv index 7f57c8c626d..5396b2a22a6 100644 --- a/base_url/security/ir.model.access.csv +++ b/base_url/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_url_url,access_url_url,model_url_url,,1,0,0,0 +access_read_url_url,access_url_url,model_url_url,,1,0,0,0 +access_edit_url_url,access_url_url,model_url_url,group_edit_url,1,1,1,1 diff --git a/base_url/security/res_groups.xml b/base_url/security/res_groups.xml new file mode 100644 index 00000000000..cee814fc7dc --- /dev/null +++ b/base_url/security/res_groups.xml @@ -0,0 +1,12 @@ + + + + + Edit url + + + + diff --git a/base_url/tests/models.py b/base_url/tests/models.py index dc9c25176e0..7407fc65b3b 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -1,34 +1,23 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import logging -from odoo import api, fields, models +from odoo import fields, models -_logger = logging.getLogger(__name__) +class FakeProduct(models.Model): + _inherit = ["abstract.url"] + _name = "fake.product" -class UrlBackendFake(models.Model): - _name = "url.backend.fake" - _description = "Url Backend" + code = fields.Char() + name = fields.Char(translate=True) + active = fields.Boolean(default=True) + categ_id = fields.Many2one("fake.categ") - name = fields.Char(required=True) + def _get_keyword_fields(self): + return ["categ_id.name"] + super()._get_keyword_fields() + ["code"] -class ResPartner(models.Model): - _inherit = "res.partner" +class FakeCateg(models.Model): + _name = "fake.categ" - binding_ids = fields.One2many("res.partner.addressable.fake", "record_id") - - -class ResPartnerAddressableFake(models.Model): - _name = "res.partner.addressable.fake" - _inherit = "abstract.url" - _inherits = {"res.partner": "record_id"} - _description = "Fake partner addressable" - - backend_id = fields.Many2one(comodel_name="url.backend.fake") - special_code = fields.Char() - - @api.depends("lang_id", "special_code", "record_id.name") - def _compute_automatic_url_key(self): - self._generic_compute_automatic_url_key() + name = fields.Char() diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 397b6cb2c95..53ac4fec37f 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -1,123 +1,123 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import mock +from unittest import mock + from odoo_test_helper import FakeModelLoader -from odoo.exceptions import ValidationError -from odoo.tests import SavepointCase +from odoo.tests import TransactionCase -class TestAbstractUrl(SavepointCase, FakeModelLoader): +class TestAbstractUrl(TransactionCase, FakeModelLoader): @classmethod def setUpClass(cls): - super(TestAbstractUrl, cls).setUpClass() + super().setUpClass() cls.loader = FakeModelLoader(cls.env, cls.__module__) cls.loader.backup_registry() - from .models import ResPartner, ResPartnerAddressableFake, UrlBackendFake + from .models import FakeCateg, FakeProduct + + cls.loader.update_registry([FakeProduct, FakeCateg]) - cls.loader.update_registry( - (UrlBackendFake, ResPartner, ResPartnerAddressableFake) + cls.lang_en = cls.env.ref("base.lang_en") + cls.lang_fr = cls.env.ref("base.lang_fr") + cls.lang_fr.active = True + cls.product = ( + cls.env["fake.product"] + .with_context(lang="en_US") + .create({"name": "My Product"}) ) + cls.product.with_context(lang="fr_FR").name = "Mon Produit" - cls.lang = cls.env.ref("base.lang_en") - cls.UrlUrl = cls.env["url.url"] - cls.ResPartnerAddressable = cls.env["res.partner.addressable.fake"] - cls.url_backend = cls.env["url.backend.fake"].create({"name": "fake backend"}) - cls.name = "partner name" - cls.auto_key = "partner-name" + def _expect_url_for_lang(self, lang, url_key): + self.assertEqual(self.product._get_main_url("global", lang).key, url_key) @classmethod def tearDownClass(cls): cls.loader.restore_registry() - super(TestAbstractUrl, cls).tearDownClass() - - def _get_default_partner_value(self): - return { - "name": self.name, - "lang_id": self.lang.id, - "url_builder": "auto", - "backend_id": self.url_backend.id, - } - - def _create_auto(self): - return self.ResPartnerAddressable.create(self._get_default_partner_value()) - - def _check_url_key(self, partner_addressable, key_name): - self.assertEqual(partner_addressable.url_key, key_name) - self.assertEqual(len(partner_addressable.url_url_ids), 1) - url_url = partner_addressable.url_url_ids - self.assertEqual(url_url.url_key, key_name) - self.assertEqual(url_url.lang_id, self.lang) - self.assertEqual(url_url.model_id, partner_addressable) - - def test_create_auto_url(self): - my_partner = self.ResPartnerAddressable.create( - self._get_default_partner_value() - ) - self._check_url_key(my_partner, self.auto_key) - - def test_create_manual_url_contrains(self): - value = self._get_default_partner_value() - value["url_builder"] = "manual" - with self.assertRaises(ValidationError): - self.ResPartnerAddressable.create(value) - value["manual_url_key"] = "my url key" - res = self.ResPartnerAddressable.create(value) - self.assertTrue(res) + super().tearDownClass() + + def test_update_url_key(self): + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-product") + url = self.product.url_ids.filtered(lambda s: s.lang_id == self.lang_en) + self.assertEqual(len(url), 1) + self.assertFalse(url.manual) + self.assertFalse(url.redirect) + + def test_update_url_2_lang(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self._expect_url_for_lang("en_US", "my-product") + self._expect_url_for_lang("fr_FR", "mon-produit") + self.assertEqual(len(self.product.url_ids), 2) + + url = self.product.url_ids.filtered(lambda s: s.lang_id == self.lang_fr) + self.assertEqual(len(url), 1) + self.assertFalse(url.manual) + self.assertFalse(url.redirect) + + def test_update_no_translatable_field(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.product.write({"code": "1234"}) + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.assertEqual(len(self.product.url_ids), 4) + redirects = self.product._get_redirect_urls("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-1234") + self.assertEqual(len(redirects), 1) + redirects = self.product._get_redirect_urls("global", "fr_FR") + self.assertEqual(len(redirects), 1) + self._expect_url_for_lang("fr_FR", "mon-produit-1234") + + def test_update_translatable_field(self): + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.product.name = "My Product With Redirect" + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "fr_FR") + self.assertEqual(len(self.product.url_ids), 3) + redirects = self.product._get_redirect_urls("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-with-redirect") + self.assertEqual(len(redirects), 1) + + def test_update_product_never_generated(self): + self.product.name = "My product never had url generated" + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-product-never-had-url-generated") + self.assertEqual(len(self.product.url_ids), 1) + + def test_update_with_relation(self): + self.product._update_url_key("global", "en_US") + categ = self.env["fake.categ"].create({"name": "Foo"}) + self.product.write({"categ_id": categ.id}) + self.product._update_url_key("global", "en_US") + self.assertEqual(len(self.product.url_ids), 2) + redirects = self.product._get_redirect_urls("global", "en_US") + self.assertEqual(len(redirects), 1) + self._expect_url_for_lang("en_US", "foo-my-product") def test_create_manual_url(self): - value = self._get_default_partner_value() - manual_url_key = "manual-url key" - value.update({"url_builder": "manual", "manual_url_key": manual_url_key}) - my_partner = self.ResPartnerAddressable.create(value) - self.assertTrue(my_partner) - self._check_url_key(my_partner, manual_url_key) - - def test_write_url_builder_constrains(self): - my_partner = self._create_auto() - with self.assertRaises(ValidationError): - my_partner.url_builder = "manual" - my_partner.write({"url_builder": "manual", "manual_url_key": "manual url key"}) - - def test_write_url_builder(self): - # tests that a new url is created we update the url builder - my_partner = self._create_auto() - self._check_url_key(my_partner, "partner-name") - manual_url_key = "manual-url key" - my_partner.write({"url_builder": "manual", "manual_url_key": manual_url_key}) - url_keys = set(my_partner.mapped("url_url_ids.url_key")) - self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) - # if we reset the auto key, no new url.url should be created - my_partner.write({"url_builder": "auto"}) - self.assertEqual(2, len(my_partner.url_url_ids)) - url_keys = set(my_partner.mapped("url_url_ids.url_key")) - self.assertSetEqual(url_keys, {manual_url_key, self.auto_key}) - - def test_write_launching_automatic_url_key(self): - my_partner = self._create_auto() - # call flush to force to apply the recompute - my_partner.flush() - my_partner.name = "my new name" - self.assertEqual(2, len(my_partner.url_url_ids)) - url_keys = set(my_partner.mapped("url_url_ids.url_key")) - self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) - - def test_write_on_related_record_launching_automatic_url_key(self): - my_partner = self._create_auto() - # call flush to force to apply the recompute - my_partner.flush() - my_partner.record_id.name = "my new name" - self.assertEqual(2, len(my_partner.url_url_ids)) - url_keys = set(my_partner.mapped("url_url_ids.url_key")) - self.assertSetEqual(url_keys, {"my-new-name", self.auto_key}) + self.env["url.url"].create( + { + "manual": True, + "key": "my-custom-key", + "lang_id": self.lang_en.id, + "res_id": self.product.id, + "res_model": "fake.product", + "referential": "global", + } + ) + self._expect_url_for_lang("en_US", "my-custom-key") + self.assertEqual(len(self.product.url_ids), 1) + + self.product.name = "My name have change but my url is the same" + self.product._update_url_key("global", "en_US") + self._expect_url_for_lang("en_US", "my-custom-key") def test_write_inactive(self): - my_partner = self._create_auto() # when we deactivate a record, the redirect method should be called with mock.patch.object( - self.ResPartnerAddressable.__class__, "_redirect_existing_url" + self.product.__class__, "_redirect_existing_url" ) as mocked_redirect: - my_partner.active = False - # call flush to force to apply the recompute - my_partner.flush() + self.product.active = False mocked_redirect.assert_called_once() diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index a681a1d6a27..ac42ba5dc19 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -7,10 +7,15 @@
- + - - + + + + @@ -22,8 +27,12 @@ url.url - - + + + + + + From c31fb22771f34970be45706ae7d12c1c35b42dbf Mon Sep 17 00:00:00 2001 From: Quentin Groulard Date: Tue, 29 Aug 2023 11:22:02 +0200 Subject: [PATCH 40/77] base_url: add fields url_key and redirect_url_key, shopinvader_product_url add fields to schemas --- base_url/models/abstract_url.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index ac92bbe57f5..91e3c5efaf6 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -41,6 +41,10 @@ class AbstractUrl(models.AbstractModel): compute="_compute_url_need_refresh", store=True, readonly=False ) count_url = fields.Integer(compute="_compute_count_url") + url_key = fields.Char(compute="_compute_url_key") + redirect_url_key = fields.Serialized( + compute="_compute_url_key", string="Redirect Url Keys" + ) def _compute_count_url(self): res = self.env["url.url"].read_group( @@ -63,6 +67,17 @@ def _compute_url_need_refresh(self): for record in self: record.url_need_refresh = True + @api.depends("url_ids") + @api.depends_context("lang", "referential") + def _compute_url_key(self): + referential = self._context.get("referential", "global") + lang = self._context.get("lang", "en_US") + for record in self: + record.url_key = record._get_main_url(referential, lang).key + record.redirect_url_key = record._get_redirect_urls( + referential, lang + ).mapped("key") + def _get_keyword_fields(self): """This method return a list of field that will be concatenated with '-' to generate the url From e335aad4830a717e096797ef3e37ce5a3f58a59a Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 10 Oct 2023 18:54:09 +0200 Subject: [PATCH 41/77] [FIX] base_url: Add missing dependency The dependency on base_sparse_field_list_support is required since we use a Serialized field to store list of values --- base_url/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 88bc7ee0dd1..2fe7fede2ea 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -12,7 +12,7 @@ "author": "Akretion, ACSONE SA/NV", "website": "https://github.com/shopinvader/odoo-shopinvader", # any module necessary for this one to work correctly - "depends": ["base"], + "depends": ["base", "base_sparse_field_list_support"], "external_dependencies": {"python": ["python-slugify"]}, "data": [ "views/url_view.xml", From 52f131a2d700db57880cd26639ad57d5d411e221 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 10 Oct 2023 18:56:13 +0200 Subject: [PATCH 42/77] [FIX] base_url: Import Serialized from its module This is required to avoid race condition when running tests if the base_sparse_field addon has not yet patch the odoo.fields namespace to register the Serialized field class --- base_url/models/abstract_url.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 91e3c5efaf6..a00b2aacb6b 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -9,6 +9,8 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError +from odoo.addons.base_sparse_field.models.fields import Serialized + DEFAULT_LANG = "en_US" _logger = logging.getLogger(__name__) @@ -42,7 +44,7 @@ class AbstractUrl(models.AbstractModel): ) count_url = fields.Integer(compute="_compute_count_url") url_key = fields.Char(compute="_compute_url_key") - redirect_url_key = fields.Serialized( + redirect_url_key = Serialized( compute="_compute_url_key", string="Redirect Url Keys" ) @@ -145,7 +147,8 @@ def _reuse_url(self, existing_url): } ) - def _update_url_key(self, referential="global", lang=DEFAULT_LANG): + def _update_url_key(self, referential="global", lang=None): + lang = lang or self.env.context.get("lang", DEFAULT_LANG) for record in self.with_context(lang=lang): # TODO maybe we should have a computed field that flag the # current url if the key used for building the url have changed From 2999686d5818068dcf7e0d85e4dc4ea7b70b2ed8 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 13 Oct 2023 10:11:56 +0000 Subject: [PATCH 43/77] [UPD] Update shopinvader_base_url.pot --- base_url/i18n/base_url.pot | 100 +++++++++++++++---------------------- 1 file changed, 41 insertions(+), 59 deletions(-) diff --git a/base_url/i18n/base_url.pot b/base_url/i18n/base_url.pot index b71f6f58c45..6dcba9084e8 100644 --- a/base_url/i18n/base_url.pot +++ b/base_url/i18n/base_url.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" +"Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -19,33 +19,13 @@ msgid "Abstract Url" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__active -msgid "Active" -msgstr "" - -#. module: base_url -#: model:ir.model.constraint,message:base_url.constraint_url_url_unique_key_per_backend_per_lang +#: model:ir.model.constraint,message:base_url.constraint_url_url_unique_key_per_referential_per_lang msgid "Already exists in database" msgstr "" #. module: base_url -#: model:ir.model.fields.selection,name:base_url.selection__abstract_url__url_builder__auto -msgid "Automatic" -msgstr "" - -#. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__automatic_url_key -msgid "Automatic Url Key" -msgstr "" - -#. module: base_url -#: model:ir.model.fields,field_description:base_url.field_url_url__backend_id -msgid "Backend" -msgstr "" - -#. module: base_url -#: model:ir.model.fields,help:base_url.field_url_url__backend_id -msgid "Backend linked to this URL" +#: model:ir.model.fields,field_description:base_url.field_abstract_url__count_url +msgid "Count Url" msgstr "" #. module: base_url @@ -59,30 +39,41 @@ msgid "Created on" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__display_name #: model:ir.model.fields,field_description:base_url.field_url_url__display_name msgid "Display Name" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__id +#: model:res.groups,name:base_url.group_edit_url +msgid "Edit url" +msgstr "" + +#. module: base_url #: model:ir.model.fields,field_description:base_url.field_url_url__id msgid "ID" msgstr "" +#. module: base_url +#: model:ir.model.fields,help:base_url.field_url_url__res_id +msgid "ID of the target record in the database" +msgstr "" + #. module: base_url #: model:ir.model.fields,help:base_url.field_url_url__redirect msgid "If tick this url is a redirection to the new url" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__lang_id +#: model:ir.model.fields,field_description:base_url.field_url_url__key +msgid "Key" +msgstr "" + +#. module: base_url #: model:ir.model.fields,field_description:base_url.field_url_url__lang_id msgid "Lang" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url____last_update #: model:ir.model.fields,field_description:base_url.field_url_url____last_update msgid "Last Modified on" msgstr "" @@ -98,24 +89,18 @@ msgid "Last Updated on" msgstr "" #. module: base_url -#: model:ir.model.fields.selection,name:base_url.selection__abstract_url__url_builder__manual +#: model:ir.model.fields,field_description:base_url.field_url_url__manual msgid "Manual" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__manual_url_key -msgid "Manual Url Key" +#: model:ir.model.fields,field_description:base_url.field_url_url__need_refresh +msgid "Need Refresh" msgstr "" #. module: base_url -#: code:addons/base_url/models/abstract_url.py:0 -#, python-format -msgid "Manual url key is required if builder is set to manual" -msgstr "" - -#. module: base_url -#: model:ir.model.fields,field_description:base_url.field_url_url__model_id -msgid "Model" +#: model:ir.model.fields,field_description:base_url.field_url_url__res_id +msgid "Record ID" msgstr "" #. module: base_url @@ -124,38 +109,34 @@ msgid "Redirect" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__redirect_url_url_ids -msgid "Redirect Url Url" +#: model:ir.model.fields,field_description:base_url.field_abstract_url__redirect_url_key +msgid "Redirect Url Keys" msgstr "" #. module: base_url -#: model:ir.model.fields,help:base_url.field_url_url__model_id -msgid "The id of content linked to the url." +#: model:ir.model.fields,field_description:base_url.field_url_url__referential +msgid "Referential" msgstr "" #. module: base_url -#: model:ir.model,name:base_url.model_url_url -msgid "Url" +#: model:ir.model.fields,field_description:base_url.field_url_url__res_model +msgid "Res Model" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_builder -msgid "Url Builder" +#: model:ir.model,name:base_url.model_url_url +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_ids +msgid "Url" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_url_url__url_key +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_key msgid "Url Key" msgstr "" #. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_url_ids -msgid "Url Url" -msgstr "" - -#. module: base_url -#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_key -msgid "Url key" +#: model:ir.model.fields,field_description:base_url.field_abstract_url__url_need_refresh +msgid "Url Need Refresh" msgstr "" #. module: base_url @@ -164,14 +145,15 @@ msgid "Url rewriting edit form" msgstr "" #. module: base_url +#. odoo-python #: code:addons/base_url/models/abstract_url.py:0 #, python-format msgid "" "Url_key already exist in other model\n" -"- name: %s\n" -" - id: %s\n" -"- url_key: %s\n" -" - url_key_id %s" +"- name: %(model_name)s\n" +" - id: %(model_id)s\n" +"- url_key: %(url_key)s\n" +" - url_key_id %(url_id)s" msgstr "" #. module: base_url From 86156950e44b858b6b6967c6650656119e929d13 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 28 Nov 2023 14:53:26 +0100 Subject: [PATCH 44/77] [FIX] base_url: No assign value if not changed In a compute method, we must assign a value to the computed field only if it's different from the original one. Otherwise, the write method will be called every time. --- base_url/__manifest__.py | 2 +- base_url/models/abstract_url.py | 3 ++- base_url/tests/test_abstract_url.py | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 2fe7fede2ea..d9b6f7e94a9 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base Url", - "version": "16.0.1.0.2", + "version": "16.0.1.0.3", "category": "tools", "license": "AGPL-3", "summary": "keep history of url for products & categories ", diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index a00b2aacb6b..9a71527711a 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -173,7 +173,8 @@ def _update_url_key(self, referential="global", lang=None): if current_url.key != url_key: current_url.redirect = True record._add_url(referential, lang, url_key) - record.url_need_refresh = False + if record.url_need_refresh: + record.url_need_refresh = False def _add_url(self, referential, lang, url_key): self.ensure_one() diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 53ac4fec37f..e26bd93bc22 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -121,3 +121,25 @@ def test_write_inactive(self): ) as mocked_redirect: self.product.active = False mocked_redirect.assert_called_once() + + def test_update_twice_write_once(self): + """" + When we update twice the same record, the write method should be called + only once. This is important because for example, by default, in + shopinvader_search_engine_update, when the method write is called, + it mark the record as to_recompute. The recompute will then call the + serializer method on each binding one by one and set the state to done. + Unfortunately, if a record has 2 bindings, the serializer will call 2 + times the _update_url_key method. The first time, if every time the + method make a write on the record, the record will end up with the + state to_recompute. + """ "" + + # we mock the write method to check the number of call but we want the + # method to be executed + original_write = self.product.write + with mock.patch.object(type(self.product), "write") as mocked_write: + mocked_write.side_effect = original_write + self.product._update_url_key("global", "en_US") + self.product._update_url_key("global", "en_US") + mocked_write.assert_called_once() From 2ceb00af85cb1c4ff2829d163693bec5add6026c Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 1 Apr 2026 23:53:19 +0200 Subject: [PATCH 45/77] [IMP] base_url: pre-commit auto fixes --- base_url/__manifest__.py | 10 ++++------ base_url/models/abstract_url.py | 4 ++-- base_url/pyproject.toml | 3 +++ base_url/security/res_groups.xml | 2 -- base_url/views/url_view.xml | 26 ++++++++++++-------------- requirements.txt | 1 + 6 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 base_url/pyproject.toml diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index d9b6f7e94a9..56067460385 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -5,13 +5,12 @@ { "name": "Base Url", - "version": "16.0.1.0.3", + "version": "18.0.1.0.0", "category": "tools", "license": "AGPL-3", - "summary": "keep history of url for products & categories ", - "author": "Akretion, ACSONE SA/NV", - "website": "https://github.com/shopinvader/odoo-shopinvader", - # any module necessary for this one to work correctly + "summary": "Keep history of url for products & categories ", + "author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", "depends": ["base", "base_sparse_field_list_support"], "external_dependencies": {"python": ["python-slugify"]}, "data": [ @@ -19,6 +18,5 @@ "security/res_groups.xml", "security/ir.model.access.csv", ], - "url": "", "installable": True, } diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 9a71527711a..9a7d0e07a36 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -49,13 +49,13 @@ class AbstractUrl(models.AbstractModel): ) def _compute_count_url(self): - res = self.env["url.url"].read_group( + res = self.env["url.url"]._read_group( domain=[ ("res_id", "in", self.ids), ("res_model", "=", self._name), ], - fields=["res_id"], groupby=["res_id"], + aggregates=["__count"], ) id2count = {item["res_id"]: item["res_id_count"] for item in res} for record in self: diff --git a/base_url/pyproject.toml b/base_url/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/base_url/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_url/security/res_groups.xml b/base_url/security/res_groups.xml index cee814fc7dc..921e2663656 100644 --- a/base_url/security/res_groups.xml +++ b/base_url/security/res_groups.xml @@ -1,6 +1,5 @@ - Edit url - diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index ac42ba5dc19..2bc41b4617d 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -1,6 +1,5 @@ - url.url @@ -26,29 +25,28 @@ url.url - - - - - - - - - + + + + + + + + + url list Viewer url.url - tree,form - - + list,form + + - diff --git a/requirements.txt b/requirements.txt index 5d1fefa6f23..77104f85f24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,6 @@ openupgradelib paramiko<4.0.0 pygount pysftp +python-slugify sentry_sdk>=2.0.0,<=2.22.0 unittest-xml-reporting From c9c1b8e940cfe051a35120a7433c64a83fc39544 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 1 Apr 2026 23:55:12 +0200 Subject: [PATCH 46/77] [MIG] base_url --- base_url/__manifest__.py | 2 +- base_url/models/abstract_url.py | 9 ++++--- base_url/models/url_url.py | 15 +++++++++-- base_url/security/ir.model.access.csv | 2 +- base_url/tests/test_abstract_url.py | 37 ++++++++++++++------------- base_url/views/url_view.xml | 6 +++-- 6 files changed, 43 insertions(+), 28 deletions(-) diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 56067460385..fdd5f92f62a 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -8,7 +8,7 @@ "version": "18.0.1.0.0", "category": "tools", "license": "AGPL-3", - "summary": "Keep history of url for products & categories ", + "summary": "Keep history of url for products & categories", "author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-tools", "depends": ["base", "base_sparse_field_list_support"], diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 9a7d0e07a36..deb55e32101 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -57,9 +57,9 @@ def _compute_count_url(self): groupby=["res_id"], aggregates=["__count"], ) - id2count = {item["res_id"]: item["res_id_count"] for item in res} + mapped_data = {item: count for item, count in res} for record in self: - record.count_url = id2count.get(record.id, 0) + record.count_url = mapped_data.get(record.id, 0) def _compute_url_need_refresh_depends(self): return self._get_keyword_fields() @@ -83,7 +83,8 @@ def _compute_url_key(self): def _get_keyword_fields(self): """This method return a list of field that will be concatenated with '-' to generate the url - Ex: if you return ['name', 'code'] the url will be f"{record.name}-{record.code}" + Ex: if you return ['name', 'code'] the url will be + f"{record.name}-{record.code}" Note: the self already include in the context the lang of the record Note: you can return key like in depends ex: ["categ_id.name", "code"] @@ -133,7 +134,7 @@ def _prepare_url(self, referential, lang, url_key): "res_model": self._name, "res_id": self.id, "referential": referential, - "lang_id": self.env["res.lang"]._lang_get_id(lang), + "lang_id": self.env["res.lang"]._lang_get(lang).id, "manual": False, } diff --git a/base_url/models/url_url.py b/base_url/models/url_url.py index e7f2c72b380..6b0acc9cf55 100644 --- a/base_url/models/url_url.py +++ b/base_url/models/url_url.py @@ -19,13 +19,19 @@ class UrlUrl(models.Model): res_id = fields.Many2oneReference( string="Record ID", help="ID of the target record in the database", - model_field="res_model", + model_field="res_model_field", readonly=True, index=True, ) res_model = fields.Selection( selection=lambda s: s._get_model_with_url_selection(), readonly=True, index=True ) + res_model_field = fields.Char( + string="Model Name", + compute="_compute_res_model_field", + readonly=True, + store=True, + ) redirect = fields.Boolean(help="If tick this url is a redirection to the new url") referential = fields.Selection( selection=lambda s: s._get_all_referential(), @@ -33,7 +39,7 @@ class UrlUrl(models.Model): default="global", required=True, ) - lang_id = fields.Many2one("res.lang", "Lang", index=True, required=True) + lang_id = fields.Many2one("res.lang", string="Lang", index=True, required=True) need_refresh = fields.Boolean() _sql_constraints = [ @@ -65,6 +71,11 @@ def _get_model_with_url_selection(self): ) ] + @api.depends("res_model") + def _compute_res_model_field(self): + for record in self: + record.res_model_field = record.res_model + def _get_all_referential(self): """Return the list of referential for your url, by default it's global but you can do your own implementation to have url per search engine diff --git a/base_url/security/ir.model.access.csv b/base_url/security/ir.model.access.csv index 5396b2a22a6..cd039fa1c30 100644 --- a/base_url/security/ir.model.access.csv +++ b/base_url/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_read_url_url,access_url_url,model_url_url,,1,0,0,0 +access_read_url_url,access_url_url,model_url_url,base.group_user,1,0,0,0 access_edit_url_url,access_url_url,model_url_url,group_edit_url,1,1,1,1 diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index e26bd93bc22..0350bc5338e 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -8,32 +8,30 @@ class TestAbstractUrl(TransactionCase, FakeModelLoader): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() + def setUp(self): + super().setUp() + self.loader = FakeModelLoader(self.env, self.__module__) + self.loader.backup_registry() from .models import FakeCateg, FakeProduct - cls.loader.update_registry([FakeProduct, FakeCateg]) + self.loader.update_registry([FakeProduct, FakeCateg]) - cls.lang_en = cls.env.ref("base.lang_en") - cls.lang_fr = cls.env.ref("base.lang_fr") - cls.lang_fr.active = True - cls.product = ( - cls.env["fake.product"] + self.lang_en = self.env.ref("base.lang_en") + self.lang_fr = self.env.ref("base.lang_fr") + self.lang_fr.active = True + self.product = ( + self.env["fake.product"] .with_context(lang="en_US") .create({"name": "My Product"}) ) - cls.product.with_context(lang="fr_FR").name = "Mon Produit" + self.product.with_context(lang="fr_FR").name = "Mon Produit" def _expect_url_for_lang(self, lang, url_key): self.assertEqual(self.product._get_main_url("global", lang).key, url_key) - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() + def tearDown(self): + self.loader.restore_registry() + super().tearDown() def test_update_url_key(self): self.product._update_url_key("global", "en_US") @@ -123,7 +121,8 @@ def test_write_inactive(self): mocked_redirect.assert_called_once() def test_update_twice_write_once(self): - """" + ( + """" When we update twice the same record, the write method should be called only once. This is important because for example, by default, in shopinvader_search_engine_update, when the method write is called, @@ -133,7 +132,9 @@ def test_update_twice_write_once(self): times the _update_url_key method. The first time, if every time the method make a write on the record, the record will end up with the state to_recompute. - """ "" + """ + "" + ) # we mock the write method to check the number of call but we want the # method to be executed diff --git a/base_url/views/url_view.xml b/base_url/views/url_view.xml index 2bc41b4617d..30b0e3205f1 100644 --- a/base_url/views/url_view.xml +++ b/base_url/views/url_view.xml @@ -12,6 +12,7 @@ name="res_model" invisible="context.get('hide_res_model')" /> + @@ -30,6 +31,7 @@ + @@ -38,14 +40,14 @@ - url list Viewer + Url List url.url list,form From 5cf55c44bfdda2da56632f9e1f9caad5f33307fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Thu, 2 Apr 2026 00:38:00 +0200 Subject: [PATCH 47/77] base_url: Update readme and manifest --- base_url/README.rst | 114 +++++++++++++++++++++++--------- base_url/__manifest__.py | 3 +- base_url/readme/CONTRIBUTORS.md | 3 + base_url/readme/DESCRIPTION.md | 10 +++ 4 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 base_url/readme/CONTRIBUTORS.md create mode 100644 base_url/readme/DESCRIPTION.md diff --git a/base_url/README.rst b/base_url/README.rst index a8d1a873f9f..44d7872fb2d 100644 --- a/base_url/README.rst +++ b/base_url/README.rst @@ -1,46 +1,96 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +======== +Base Url +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cf83149625b6626854e0441d1b4e095b6796d8707d85a8ff6710cb5b78fd3909 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/18.0/base_url + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-base_url + :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/server-tools&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +The aim of the module is to provide a generic mixin for building url on +record. + +Natively odoo implementation of url is based on having an "id" in the +url, this module provide an alternative way of doing. Url are generated +and stored in an uniq table (to ensure unicity). Redirection are also +managed. + +This project was initial build for shopinvader as we need to have uniq +url for the connected webshop. But this concept can be reuse in other +case and even replace the odoo url implementation. + +**Table of contents** + +.. contents:: + :local: + +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 `_. + +Do not contact contributors directly about support or help with technical issues. -========= - Base URL -========= - -This module helps to manage url with these features: - -* automatic builder according to models -* slugify string to get url -* redirection -* store url by backends depending on your connections apps -* languages - - -Installation -============ +Credits +======= -To install this module, you need to: +Authors +------- -#. (root) pip install slugify +* Akretion +* ACSONE SA/NV +Contributors +------------ -Known issues / Roadmap -====================== +- Sebastien Beau sebastien.beau@akretion.com +- Laurent Mignon laurent.mignon@acsone.eu -Update README with the last model of README when migration to v11 in OCA +Maintainers +----------- +This module is maintained by the OCA. -Credits -======= +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org +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. -Contributors ------------- +.. |maintainer-sebastienbeau| image:: https://github.com/sebastienbeau.png?size=40px + :target: https://github.com/sebastienbeau + :alt: sebastienbeau -* Monsieur B -* Sebastien Beau +Current `maintainer `__: +|maintainer-sebastienbeau| -Maintainer ----------- +This module is part of the `OCA/server-tools `_ project on GitHub. -* Akretion +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index fdd5f92f62a..066c2fd513c 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -8,10 +8,11 @@ "version": "18.0.1.0.0", "category": "tools", "license": "AGPL-3", - "summary": "Keep history of url for products & categories", + "summary": "Abstract module/mixin to manage clean url on odoo record", "author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-tools", "depends": ["base", "base_sparse_field_list_support"], + "maintainers": ["sebastienbeau"], "external_dependencies": {"python": ["python-slugify"]}, "data": [ "views/url_view.xml", diff --git a/base_url/readme/CONTRIBUTORS.md b/base_url/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..8b4480916ab --- /dev/null +++ b/base_url/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ + +- Sebastien Beau +- Laurent Mignon diff --git a/base_url/readme/DESCRIPTION.md b/base_url/readme/DESCRIPTION.md new file mode 100644 index 00000000000..b5058493803 --- /dev/null +++ b/base_url/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +The aim of the module is to provide a generic mixin for building url on record. + +Natively odoo implementation of url is based on having an "id" in the url, this module +provide an alternative way of doing. Url are generated and stored in an uniq table (to ensure unicity). +Redirection are also managed. + +This project was initial build for shopinvader as we need to have uniq url for the connected webshop. +But this concept can be reuse in other case and even replace the odoo url implementation. + + From 2a0bb37861a97c2c2da1da126ce5dba25646b86e Mon Sep 17 00:00:00 2001 From: Arnaud LAYEC Date: Mon, 13 Apr 2026 16:46:39 +0200 Subject: [PATCH 48/77] [FIX] base_url: fix typo during refactoring --- base_url/models/abstract_url.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index deb55e32101..86e034583aa 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -192,14 +192,16 @@ def _add_url(self, referential, lang, url_key): else: raise UserError( _( - "Url_key already exist in other model" - "\n- name: %(model_name)s\n - id: %(model_id)s\n" - "- url_key: %(url_key)s\n - url_key_id %(url_id)s" + "Url_key already exists in other model\n" + "- Model name: %(model_name)s\n" + "- Record ID: %(record_id)s\n" + "- Url Key: %(url_key)s\n" + "- Url ID: %(url_id)s" ) % dict( - model_name=existing_url.model_id.name, - model_id=existing_url.model_id.id, - url_key=existing_url.url_key, + model_name=existing_url.res_model, + record_id=existing_url.res_id, + url_key=existing_url.key, url_id=existing_url.id, ) ) From b2ef53d164ca324d342fc4dd3d5a9f8a84078e17 Mon Sep 17 00:00:00 2001 From: Arnaud LAYEC Date: Mon, 13 Apr 2026 16:54:02 +0200 Subject: [PATCH 49/77] [FIX] open_url: remove shopinvader --- base_url/models/abstract_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 86e034583aa..5008c92d665 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -241,7 +241,7 @@ def _get_view(self, view_id=None, view_type="form", **options): def open_url(self): self.ensure_one() - action = self.env.ref("shopinvader_base_url.base_url_action_view").read()[0] + action = self.env.ref("base_url.base_url_action_view").read()[0] action["domain"] = [("res_model", "=", self._name), ("res_id", "in", self.ids)] action["context"] = { "hide_res_model": True, From 9b2d3607d5f6221da3cfe9818dc95d1b916da37c Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 11 May 2026 09:59:45 +0200 Subject: [PATCH 50/77] [IMP] base_url: Add error coverage in unit test --- base_url/tests/models.py | 1 + base_url/tests/test_abstract_url.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/base_url/tests/models.py b/base_url/tests/models.py index 7407fc65b3b..128aff2112e 100644 --- a/base_url/tests/models.py +++ b/base_url/tests/models.py @@ -7,6 +7,7 @@ class FakeProduct(models.Model): _inherit = ["abstract.url"] _name = "fake.product" + _description = "Fake Product" code = fields.Char() name = fields.Char(translate=True) diff --git a/base_url/tests/test_abstract_url.py b/base_url/tests/test_abstract_url.py index 0350bc5338e..7fa0cf33f03 100644 --- a/base_url/tests/test_abstract_url.py +++ b/base_url/tests/test_abstract_url.py @@ -4,6 +4,7 @@ from odoo_test_helper import FakeModelLoader +from odoo.exceptions import UserError from odoo.tests import TransactionCase @@ -121,8 +122,7 @@ def test_write_inactive(self): mocked_redirect.assert_called_once() def test_update_twice_write_once(self): - ( - """" + """ When we update twice the same record, the write method should be called only once. This is important because for example, by default, in shopinvader_search_engine_update, when the method write is called, @@ -133,8 +133,6 @@ def test_update_twice_write_once(self): method make a write on the record, the record will end up with the state to_recompute. """ - "" - ) # we mock the write method to check the number of call but we want the # method to be executed @@ -144,3 +142,17 @@ def test_update_twice_write_once(self): self.product._update_url_key("global", "en_US") self.product._update_url_key("global", "en_US") mocked_write.assert_called_once() + + def test_update_url_key_not_existing(self): + self.env["url.url"].create( + { + "manual": True, + "key": "my-product", + "lang_id": self.lang_en.id, + "res_id": self.env.ref("base.partner_demo").id, + "res_model": "res.partner", + "referential": "global", + } + ) + with self.assertRaisesRegex(UserError, "Url_key already exists in other model"): + self.product._update_url_key("global", "en_US") From f2ba676c16eca16a6ac55701b2af32daf0cd8f3b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 11 May 2026 11:46:38 +0200 Subject: [PATCH 51/77] [IMP] base_url: Dropped base_sparse_field_list_support dependency. Use standard odoo slugify. Tests the computed fields. Documentation --- base_url/README.rst | 45 ++- base_url/__manifest__.py | 9 +- base_url/models/abstract_url.py | 24 +- base_url/models/url_url.py | 4 +- base_url/readme/CONTRIBUTORS.md | 2 +- base_url/readme/DESCRIPTION.md | 15 +- base_url/readme/DEVELOP.md | 20 ++ base_url/static/description/index.html | 458 +++++++++++++++++++++++++ base_url/tests/models.py | 1 + base_url/tests/test_abstract_url.py | 15 +- requirements.txt | 1 - 11 files changed, 552 insertions(+), 42 deletions(-) create mode 100644 base_url/readme/DEVELOP.md create mode 100644 base_url/static/description/index.html diff --git a/base_url/README.rst b/base_url/README.rst index 44d7872fb2d..7531252a289 100644 --- a/base_url/README.rst +++ b/base_url/README.rst @@ -28,23 +28,49 @@ Base Url |badge1| |badge2| |badge3| |badge4| |badge5| -The aim of the module is to provide a generic mixin for building url on -record. +The aim of this module is to provide a generic mixin for building urls +on records. -Natively odoo implementation of url is based on having an "id" in the -url, this module provide an alternative way of doing. Url are generated -and stored in an uniq table (to ensure unicity). Redirection are also -managed. +The native odoo implementation of a record url is based on having an +"id" in the url, this module provides an alternative way of doing it. +Urls are generated and stored in an unique table (to ensure unicity). +Redirections are also managed in case of url changes. -This project was initial build for shopinvader as we need to have uniq -url for the connected webshop. But this concept can be reuse in other -case and even replace the odoo url implementation. +This project was initial build for shopinvader as we need to have unique +url for the connected webshop. But this concept can be reused in other +cases and even replace the odoo url implementation. **Table of contents** .. contents:: :local: +Development +=========== + +To use this module, you need to inherit the ``abstract.url`` model and +override the ``_get_keyword_fields`` method to include your custom url +fields. By default, the ``_get_keyword_fields`` method returns the +record ``name``. + +Here is an example of how to override the ``_get_keyword_fields`` method +to include the ``default_code`` field for ``product.template`` records: + +.. code:: python + + + class ProductTemplate(models.Model): + _inherit = ["product.template", "abstract.url"] + _name = "product.template" + + def _get_keyword_fields(self): + return super()._get_keyword_fields() + ["default_code"] + +Your product template records will now have an ``url_key`` field that is +generated from the ``name`` and ``default_code`` fields and a +``redirect_url_key`` field that will contain a list of redirect URLs +(old ``url_key`` field values). + Bug Tracker =========== @@ -69,6 +95,7 @@ Contributors - Sebastien Beau sebastien.beau@akretion.com - Laurent Mignon laurent.mignon@acsone.eu +- Florian Mounier florian.mounier@akretion.com Maintainers ----------- diff --git a/base_url/__manifest__.py b/base_url/__manifest__.py index 066c2fd513c..a1cb1dfc5e3 100644 --- a/base_url/__manifest__.py +++ b/base_url/__manifest__.py @@ -1,6 +1,6 @@ -# Rewrite url Base module for OpenERP -# Copyright (C) 2013 Akretion (http://www.akretion.com) -# @author EBII MonsieurB +# Rewrite url Base module for OpenERP +# Copyright (C) 2013 Akretion (http://www.akretion.com) +# @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { @@ -11,9 +11,8 @@ "summary": "Abstract module/mixin to manage clean url on odoo record", "author": "Akretion, ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-tools", - "depends": ["base", "base_sparse_field_list_support"], + "depends": ["base"], "maintainers": ["sebastienbeau"], - "external_dependencies": {"python": ["python-slugify"]}, "data": [ "views/url_view.xml", "security/res_groups.xml", diff --git a/base_url/models/abstract_url.py b/base_url/models/abstract_url.py index 5008c92d665..79a4b12775f 100644 --- a/base_url/models/abstract_url.py +++ b/base_url/models/abstract_url.py @@ -1,25 +1,18 @@ -# Copyright (C) 2016 Akretion (http://www.akretion.com) -# @author EBII MonsieurB +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author EBII MonsieurB # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from lxml import etree -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import UserError -from odoo.addons.base_sparse_field.models.fields import Serialized - DEFAULT_LANG = "en_US" _logger = logging.getLogger(__name__) -try: - from slugify import slugify -except ImportError: - _logger.debug("Cannot `import slugify`.") - SMART_BUTTON = """