diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website/models/ir_ui_view.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website/models/ir_ui_view.py')
| -rw-r--r-- | addons/website/models/ir_ui_view.py | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py new file mode 100644 index 00000000..c1d3a130 --- /dev/null +++ b/addons/website/models/ir_ui_view.py @@ -0,0 +1,514 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import os +import uuid +import werkzeug + +from odoo import api, fields, models +from odoo import tools +from odoo.addons import website +from odoo.exceptions import AccessError +from odoo.osv import expression +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class View(models.Model): + + _name = "ir.ui.view" + _inherit = ["ir.ui.view", "website.seo.metadata"] + + website_id = fields.Many2one('website', ondelete='cascade', string="Website") + page_ids = fields.One2many('website.page', 'view_id') + first_page_id = fields.Many2one('website.page', string='Website Page', help='First page linked to this view', compute='_compute_first_page_id') + track = fields.Boolean(string='Track', default=False, help="Allow to specify for one page of the website to be trackable or not") + visibility = fields.Selection([('', 'All'), ('connected', 'Signed In'), ('restricted_group', 'Restricted Group'), ('password', 'With Password')], default='') + visibility_password = fields.Char(groups='base.group_system', copy=False) + visibility_password_display = fields.Char(compute='_get_pwd', inverse='_set_pwd', groups='website.group_website_designer') + + @api.depends('visibility_password') + def _get_pwd(self): + for r in self: + r.visibility_password_display = r.sudo().visibility_password and '********' or '' + + def _set_pwd(self): + crypt_context = self.env.user._crypt_context() + for r in self: + if r.type == 'qweb': + r.sudo().visibility_password = r.visibility_password_display and crypt_context.encrypt(r.visibility_password_display) or '' + r.visibility = r.visibility # double check access + + def _compute_first_page_id(self): + for view in self: + view.first_page_id = self.env['website.page'].search([('view_id', '=', view.id)], limit=1) + + def name_get(self): + if (not self._context.get('display_website') and not self.env.user.has_group('website.group_multi_website')) or \ + not self._context.get('display_website'): + return super(View, self).name_get() + + res = [] + for view in self: + view_name = view.name + if view.website_id: + view_name += ' [%s]' % view.website_id.name + res.append((view.id, view_name)) + return res + + def write(self, vals): + '''COW for ir.ui.view. This way editing websites does not impact other + websites. Also this way newly created websites will only + contain the default views. + ''' + current_website_id = self.env.context.get('website_id') + if not current_website_id or self.env.context.get('no_cow'): + return super(View, self).write(vals) + + # We need to consider inactive views when handling multi-website cow + # feature (to copy inactive children views, to search for specific + # views, ...) + for view in self.with_context(active_test=False): + # Make sure views which are written in a website context receive + # a value for their 'key' field + if not view.key and not vals.get('key'): + view.with_context(no_cow=True).key = 'website.key_%s' % str(uuid.uuid4())[:6] + + # No need of COW if the view is already specific + if view.website_id: + super(View, view).write(vals) + continue + + # Ensure the cache of the pages stay consistent when doing COW. + # This is necessary when writing view fields from a page record + # because the generic page will put the given values on its cache + # but in reality the values were only meant to go on the specific + # page. Invalidate all fields and not only those in vals because + # other fields could have been changed implicitly too. + pages = view.page_ids + pages.flush(records=pages) + pages.invalidate_cache(ids=pages.ids) + + # If already a specific view for this generic view, write on it + website_specific_view = view.search([ + ('key', '=', view.key), + ('website_id', '=', current_website_id) + ], limit=1) + if website_specific_view: + super(View, website_specific_view).write(vals) + continue + + # Set key to avoid copy() to generate an unique key as we want the + # specific view to have the same key + copy_vals = {'website_id': current_website_id, 'key': view.key} + # Copy with the 'inherit_id' field value that will be written to + # ensure the copied view's validation works + if vals.get('inherit_id'): + copy_vals['inherit_id'] = vals['inherit_id'] + website_specific_view = view.copy(copy_vals) + + view._create_website_specific_pages_for_view(website_specific_view, + view.env['website'].browse(current_website_id)) + + for inherit_child in view.inherit_children_ids.filter_duplicate().sorted(key=lambda v: (v.priority, v.id)): + if inherit_child.website_id.id == current_website_id: + # In the case the child was already specific to the current + # website, we cannot just reattach it to the new specific + # parent: we have to copy it there and remove it from the + # original tree. Indeed, the order of children 'id' fields + # must remain the same so that the inheritance is applied + # in the same order in the copied tree. + child = inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key}) + inherit_child.inherit_children_ids.write({'inherit_id': child.id}) + inherit_child.unlink() + else: + # Trigger COW on inheriting views + inherit_child.write({'inherit_id': website_specific_view.id}) + + super(View, website_specific_view).write(vals) + + return True + + def _load_records_write_on_cow(self, cow_view, inherit_id, values): + inherit_id = self.search([ + ('key', '=', self.browse(inherit_id).key), + ('website_id', 'in', (False, cow_view.website_id.id)), + ], order='website_id', limit=1).id + values['inherit_id'] = inherit_id + cow_view.with_context(no_cow=True).write(values) + + def _create_all_specific_views(self, processed_modules): + """ When creating a generic child view, we should + also create that view under specific view trees (COW'd). + Top level view (no inherit_id) do not need that behavior as they + will be shared between websites since there is no specific yet. + """ + # Only for the modules being processed + regex = '^(%s)[.]' % '|'.join(processed_modules) + # Retrieve the views through a SQl query to avoid ORM queries inside of for loop + # Retrieves all the views that are missing their specific counterpart with all the + # specific view parent id and their website id in one query + query = """ + SELECT generic.id, ARRAY[array_agg(spec_parent.id), array_agg(spec_parent.website_id)] + FROM ir_ui_view generic + INNER JOIN ir_ui_view generic_parent ON generic_parent.id = generic.inherit_id + INNER JOIN ir_ui_view spec_parent ON spec_parent.key = generic_parent.key + LEFT JOIN ir_ui_view specific ON specific.key = generic.key AND specific.website_id = spec_parent.website_id + WHERE generic.type='qweb' + AND generic.website_id IS NULL + AND generic.key ~ %s + AND spec_parent.website_id IS NOT NULL + AND specific.id IS NULL + GROUP BY generic.id + """ + self.env.cr.execute(query, (regex, )) + result = dict(self.env.cr.fetchall()) + + for record in self.browse(result.keys()): + specific_parent_view_ids, website_ids = result[record.id] + for specific_parent_view_id, website_id in zip(specific_parent_view_ids, website_ids): + record.with_context(website_id=website_id).write({ + 'inherit_id': specific_parent_view_id, + }) + super(View, self)._create_all_specific_views(processed_modules) + + def unlink(self): + '''This implements COU (copy-on-unlink). When deleting a generic page + website-specific pages will be created so only the current + website is affected. + ''' + current_website_id = self._context.get('website_id') + + if current_website_id and not self._context.get('no_cow'): + for view in self.filtered(lambda view: not view.website_id): + for w in self.env['website'].search([('id', '!=', current_website_id)]): + # reuse the COW mechanism to create + # website-specific copies, it will take + # care of creating pages and menus. + view.with_context(website_id=w.id).write({'name': view.name}) + + specific_views = self.env['ir.ui.view'] + if self and self.pool._init: + for view in self.filtered(lambda view: not view.website_id): + specific_views += view._get_specific_views() + + result = super(View, self + specific_views).unlink() + self.clear_caches() + return result + + def _create_website_specific_pages_for_view(self, new_view, website): + for page in self.page_ids: + # create new pages for this view + new_page = page.copy({ + 'view_id': new_view.id, + 'is_published': page.is_published, + }) + page.menu_ids.filtered(lambda m: m.website_id.id == website.id).page_id = new_page.id + + @api.model + def get_related_views(self, key, bundles=False): + '''Make this only return most specific views for website.''' + # get_related_views can be called through website=False routes + # (e.g. /web_editor/get_assets_editor_resources), so website + # dispatch_parameters may not be added. Manually set + # website_id. (It will then always fallback on a website, this + # method should never be called in a generic context, even for + # tests) + self = self.with_context(website_id=self.env['website'].get_current_website().id) + return super(View, self).get_related_views(key, bundles=bundles) + + def filter_duplicate(self): + """ Filter current recordset only keeping the most suitable view per distinct key. + Every non-accessible view will be removed from the set: + * In non website context, every view with a website will be removed + * In a website context, every view from another website + """ + current_website_id = self._context.get('website_id') + most_specific_views = self.env['ir.ui.view'] + if not current_website_id: + return self.filtered(lambda view: not view.website_id) + + for view in self: + # specific view: add it if it's for the current website and ignore + # it if it's for another website + if view.website_id and view.website_id.id == current_website_id: + most_specific_views |= view + # generic view: add it only if, for the current website, there is no + # specific view for this view (based on the same `key` attribute) + elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in self): + most_specific_views |= view + + return most_specific_views + + @api.model + def _view_get_inherited_children(self, view): + extensions = super(View, self)._view_get_inherited_children(view) + return extensions.filter_duplicate() + + @api.model + def _view_obj(self, view_id): + ''' Given an xml_id or a view_id, return the corresponding view record. + In case of website context, return the most specific one. + :param view_id: either a string xml_id or an integer view_id + :return: The view record or empty recordset + ''' + if isinstance(view_id, str) or isinstance(view_id, int): + return self.env['website'].viewref(view_id) + else: + # It can already be a view object when called by '_views_get()' that is calling '_view_obj' + # for it's inherit_children_ids, passing them directly as object record. (Note that it might + # be a view_id from another website but it will be filtered in 'get_related_views()') + return view_id if view_id._name == 'ir.ui.view' else self.env['ir.ui.view'] + + @api.model + def _get_inheriting_views_arch_domain(self, model): + domain = super(View, self)._get_inheriting_views_arch_domain(model) + current_website = self.env['website'].browse(self._context.get('website_id')) + website_views_domain = current_website.website_domain() + # when rendering for the website we have to include inactive views + # we will prefer inactive website-specific views over active generic ones + if current_website: + domain = [leaf for leaf in domain if 'active' not in leaf] + return expression.AND([website_views_domain, domain]) + + @api.model + def get_inheriting_views_arch(self, model): + if not self._context.get('website_id'): + return super(View, self).get_inheriting_views_arch(model) + + views = super(View, self.with_context(active_test=False)).get_inheriting_views_arch(model) + # prefer inactive website-specific views over active generic ones + return views.filter_duplicate().filtered('active') + + @api.model + def _get_filter_xmlid_query(self): + """This method add some specific view that do not have XML ID + """ + if not self._context.get('website_id'): + return super()._get_filter_xmlid_query() + else: + return """SELECT res_id + FROM ir_model_data + WHERE res_id IN %(res_ids)s + AND model = 'ir.ui.view' + AND module IN %(modules)s + UNION + SELECT sview.id + FROM ir_ui_view sview + INNER JOIN ir_ui_view oview USING (key) + INNER JOIN ir_model_data d + ON oview.id = d.res_id + AND d.model = 'ir.ui.view' + AND d.module IN %(modules)s + WHERE sview.id IN %(res_ids)s + AND sview.website_id IS NOT NULL + AND oview.website_id IS NULL; + """ + + @api.model + @tools.ormcache_context('self.env.uid', 'self.env.su', 'xml_id', keys=('website_id',)) + def get_view_id(self, xml_id): + """If a website_id is in the context and the given xml_id is not an int + then try to get the id of the specific view for that website, but + fallback to the id of the generic view if there is no specific. + + If no website_id is in the context, it might randomly return the generic + or the specific view, so it's probably not recommanded to use this + method. `viewref` is probably more suitable. + + Archived views are ignored (unless the active_test context is set, but + then the ormcache_context will not work as expected). + """ + if 'website_id' in self._context and not isinstance(xml_id, int): + current_website = self.env['website'].browse(self._context.get('website_id')) + domain = ['&', ('key', '=', xml_id)] + current_website.website_domain() + + view = self.sudo().search(domain, order='website_id', limit=1) + if not view: + _logger.warning("Could not find view object with xml_id '%s'", xml_id) + raise ValueError('View %r in website %r not found' % (xml_id, self._context['website_id'])) + return view.id + return super(View, self.sudo()).get_view_id(xml_id) + + @api.model + def read_template(self, xml_id): + """ This method is deprecated + """ + view = self._view_obj(self.get_view_id(xml_id)) + if view.visibility and view._handle_visibility(do_raise=False): + self = self.sudo() + return super(View, self).read_template(xml_id) + + def _get_original_view(self): + """Given a view, retrieve the original view it was COW'd from. + The given view might already be the original one. In that case it will + (and should) return itself. + """ + self.ensure_one() + domain = [('key', '=', self.key), ('model_data_id', '!=', None)] + return self.with_context(active_test=False).search(domain, limit=1) # Useless limit has multiple xmlid should not be possible + + def _handle_visibility(self, do_raise=True): + """ Check the visibility set on the main view and raise 403 if you should not have access. + Order is: Public, Connected, Has group, Password + + It only check the visibility on the main content, others views called stay available in rpc. + """ + error = False + + self = self.sudo() + + if self.visibility and not request.env.user.has_group('website.group_website_designer'): + if (self.visibility == 'connected' and request.website.is_public_user()): + error = werkzeug.exceptions.Forbidden() + elif self.visibility == 'password' and \ + (request.website.is_public_user() or self.id not in request.session.get('views_unlock', [])): + pwd = request.params.get('visibility_password') + if pwd and self.env.user._crypt_context().verify( + pwd, self.sudo().visibility_password): + request.session.setdefault('views_unlock', list()).append(self.id) + else: + error = werkzeug.exceptions.Forbidden('website_visibility_password_required') + + if self.visibility not in ('password', 'connected'): + try: + self._check_view_access() + except AccessError: + error = werkzeug.exceptions.Forbidden() + + if error: + if do_raise: + raise error + else: + return False + return True + + def _render(self, values=None, engine='ir.qweb', minimal_qcontext=False): + """ Render the template. If website is enabled on request, then extend rendering context with website values. """ + self._handle_visibility(do_raise=True) + new_context = dict(self._context) + if request and getattr(request, 'is_frontend', False): + + editable = request.website.is_publisher() + translatable = editable and self._context.get('lang') != request.website.default_lang_id.code + editable = not translatable and editable + + # in edit mode ir.ui.view will tag nodes + if not translatable and not self.env.context.get('rendering_bundle'): + if editable: + new_context = dict(self._context, inherit_branding=True) + elif request.env.user.has_group('website.group_website_publisher'): + new_context = dict(self._context, inherit_branding_auto=True) + if values and 'main_object' in values: + if request.env.user.has_group('website.group_website_publisher'): + func = getattr(values['main_object'], 'get_backend_menu_id', False) + values['backend_menu_id'] = func and func() or self.env['ir.model.data'].xmlid_to_res_id('website.menu_website_configuration') + + if self._context != new_context: + self = self.with_context(new_context) + return super(View, self)._render(values, engine=engine, minimal_qcontext=minimal_qcontext) + + @api.model + def _prepare_qcontext(self): + """ Returns the qcontext : rendering context with website specific value (required + to render website layout template) + """ + qcontext = super(View, self)._prepare_qcontext() + + if request and getattr(request, 'is_frontend', False): + Website = self.env['website'] + editable = request.website.is_publisher() + translatable = editable and self._context.get('lang') != request.env['ir.http']._get_default_lang().code + editable = not translatable and editable + + cur = Website.get_current_website() + if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'): + qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur._get_http_domain()} + qcontext['multi_website_websites'] = [ + {'website_id': website.id, 'name': website.name, 'domain': website._get_http_domain()} + for website in Website.search([]) if website != cur + ] + + cur_company = self.env.company + qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} + qcontext['multi_website_companies'] = [ + {'company_id': comp.id, 'name': comp.name} + for comp in self.env.user.company_ids if comp != cur_company + ] + + qcontext.update(dict( + main_object=self, + website=request.website, + is_view_active=request.website.is_view_active, + res_company=request.website.company_id.sudo(), + translatable=translatable, + editable=editable, + )) + + return qcontext + + @api.model + def get_default_lang_code(self): + website_id = self.env.context.get('website_id') + if website_id: + lang_code = self.env['website'].browse(website_id).default_lang_id.code + return lang_code + else: + return super(View, self).get_default_lang_code() + + def redirect_to_page_manager(self): + return { + 'type': 'ir.actions.act_url', + 'url': '/website/pages', + 'target': 'self', + } + + def _read_template_keys(self): + return super(View, self)._read_template_keys() + ['website_id'] + + @api.model + def _save_oe_structure_hook(self): + res = super(View, self)._save_oe_structure_hook() + res['website_id'] = self.env['website'].get_current_website().id + return res + + @api.model + def _set_noupdate(self): + '''If website is installed, any call to `save` from the frontend will + actually write on the specific view (or create it if not exist yet). + In that case, we don't want to flag the generic view as noupdate. + ''' + if not self._context.get('website_id'): + super(View, self)._set_noupdate() + + def save(self, value, xpath=None): + self.ensure_one() + current_website = self.env['website'].get_current_website() + # xpath condition is important to be sure we are editing a view and not + # a field as in that case `self` might not exist (check commit message) + if xpath and self.key and current_website: + # The first time a generic view is edited, if multiple editable parts + # were edited at the same time, multiple call to this method will be + # done but the first one may create a website specific view. So if there + # already is a website specific view, we need to divert the super to it. + website_specific_view = self.env['ir.ui.view'].search([ + ('key', '=', self.key), + ('website_id', '=', current_website.id) + ], limit=1) + if website_specific_view: + self = website_specific_view + super(View, self).save(value, xpath=xpath) + + # -------------------------------------------------------------------------- + # Snippet saving + # -------------------------------------------------------------------------- + + @api.model + def _snippet_save_view_values_hook(self): + res = super()._snippet_save_view_values_hook() + website_id = self.env.context.get('website_id') + if website_id: + res['website_id'] = website_id + return res |
