From 0cede3fa14d1ce59f8a6c3bfa92facb08f2b3f2f Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Tue, 26 Jan 2021 12:49:11 +0000 Subject: [PATCH 1/4] [IMP] website: display sample data for dynamic snippets Before this commit when a dynamic snippet was fully configured but returned no data, the rendered section remained empty in edit mode. After this commit when a dynamic snippet is fully configured but has no data to display, some sample data is generated in edit mode to give a feel of how the page will look like when data will be available. task-2446024 https://github.com/odoo/odoo/pull/65176 --- addons/website/controllers/main.py | 4 +- addons/website/data/website_data.xml | 2 + .../website/models/website_snippet_filter.py | 104 +++++++++++++++++- .../src/snippets/s_dynamic_snippet/000.js | 3 +- .../models/website_snippet_filter.py | 27 +++++ 5 files changed, 135 insertions(+), 5 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 33a8d18d04f..85f32fc00cd 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -270,11 +270,11 @@ class Website(Home): } @http.route('/website/snippet/filters', type='json', auth='public', website=True) - def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None): + def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None, with_sample=False): dynamic_filter = request.env['website.snippet.filter'].sudo().search( [('id', '=', filter_id)] + request.website.website_domain() ) - return dynamic_filter and dynamic_filter.render(template_key, limit, search_domain) or '' + return dynamic_filter and dynamic_filter.render(template_key, limit, search_domain, with_sample) or '' @http.route('/website/snippet/options_filters', type='json', auth='user', website=True) def get_dynamic_snippet_filters(self): diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index a0252169ec4..e16d1754bd8 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -727,6 +727,7 @@
+

Sample

@@ -752,6 +753,7 @@
+

Sample

diff --git a/addons/website/models/website_snippet_filter.py b/addons/website/models/website_snippet_filter.py index a2a8c82e86c..447581d7996 100644 --- a/addons/website/models/website_snippet_filter.py +++ b/addons/website/models/website_snippet_filter.py @@ -8,6 +8,7 @@ from odoo.osv import expression from odoo.tools import html_escape as escape from lxml import etree as ET import logging +from random import randint _logger = logging.getLogger(__name__) @@ -49,17 +50,22 @@ class WebsiteSnippetFilter(models.Model): if not field_name.strip(): raise ValidationError(_("Empty field name in %r") % (record.field_names)) - def render(self, template_key, limit, search_domain=[]): + def render(self, template_key, limit, search_domain=None, with_sample=False): """Renders the website dynamic snippet items""" self.ensure_one() assert '.dynamic_filter_template_' in template_key, _("You can only use template prefixed by dynamic_filter_template_ ") + if search_domain is None: + search_domain = [] if self.website_id and self.env['website'].get_current_website() != self.website_id: return '' records = self._prepare_values(limit, search_domain) + is_sample = with_sample and not records + if is_sample: + records = self._prepare_sample() View = self.env['ir.ui.view'].sudo().with_context(inherit_branding=False) - content = View._render_template(template_key, dict(records=records)).decode('utf-8') + content = View._render_template(template_key, dict(records=records, is_sample=is_sample)).decode('utf-8') return [ET.tostring(el) for el in ET.fromstring('%s' % content).getchildren()] def _prepare_values(self, limit=None, search_domain=None): @@ -95,6 +101,100 @@ class WebsiteSnippetFilter(models.Model): _logger.warning("The provided domain %s in 'ir.actions.server' generated a MissingError in '%s'", search_domain, self._name) return [] + def _get_field_name_and_type(self, model, field_name): + """ + Separates the name and the widget type + + @param model: Model to which the field belongs, without it type is deduced from field_name + @param field_name: Name of the field possibly followed by a colon and a forced field type + + @return Tuple containing the field name and the field type + """ + field_name, _, field_widget = field_name.partition(":") + field = model._fields.get(field_name) if model else None + if field: + field_type = field.type + elif 'image' in field_name: + field_type = 'image' + elif 'price' in field_name: + field_type = 'monetary' + else: + field_type = 'text' + return field_name, field_widget or field_type + + def _prepare_sample(self, length=4): + """ + Generates sample data and returns it the right format for render. + + @param length: Number of sample records to generate + + @return Array of objets with a value associated to each name in field_names + """ + if not length: + return [] + sample = [] + model = self.env[self.filter_id.model_id] if self.filter_id else ( + self.action_server_id.model_id if self.action_server_id else None) + sample_data = self._get_hardcoded_sample(model) + for index in range(0, length): + single_sample_data = sample_data[index % len(sample_data)].copy() + self._fill_sample(single_sample_data, model, index) + data = self._get_rendering_data_structure() + for field_name in self.field_names.split(","): + field_name, field_widget = self._get_field_name_and_type(model, field_name) + value = single_sample_data[field_name] + if field_widget == 'binary': + data['image_fields'][field_name] = self.escape_falsy_as_empty(value) + elif field_widget == 'image': + data['image_fields'][field_name] = value + elif field_widget == 'monetary': + FieldMonetary = self.env['ir.qweb.field.monetary'] + website_currency = self._get_website_currency() + data['fields'][field_name] = FieldMonetary.value_to_html( + value, + {'display_currency': website_currency} + ) + elif 'ir.qweb.field.%s' % field_widget in self.env: + data['fields'][field_name] = self.env['ir.qweb.field.%s' % field_widget].value_to_html( + value, {}) + else: + data['fields'][field_name] = self.escape_falsy_as_empty(value) + data['fields']['call_to_action_url'] = '' + sample.append(data) + return sample + + def _fill_sample(self, sample, model, index): + """ + Fills the sample for the given model + + @param sample: Data structure to fill with values for each name in field_names + @param model: Model to which the sample belongs + @param index: Index of the sample within the dataset + """ + for field_name in self.field_names.split(","): + field_name, field_widget = self._get_field_name_and_type(model, field_name) + if field_name not in sample: + if field_widget == 'binary': + sample[field_name] = None + elif field_widget == 'image': + sample[field_name] = '/web/image' + elif field_widget == 'monetary': + sample[field_name] = randint(100, 10000) / 10.0 + elif field_widget in ('integer', 'float'): + sample[field_name] = index + else: + sample[field_name] = _('Sample %s', index + 1) + + def _get_hardcoded_sample(self, model): + """ + Returns a hard-coded sample + + @param model: Model of the currently rendered view + + @return Sample data records with field values + """ + return [{}] + @api.model def _get_rendering_data_structure(self): return { diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.js b/addons/website/static/src/snippets/s_dynamic_snippet/000.js index d6a3e0ffca4..53ae9bf7dca 100644 --- a/addons/website/static/src/snippets/s_dynamic_snippet/000.js +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.js @@ -110,7 +110,8 @@ const DynamicSnippet = publicWidget.Widget.extend({ 'filter_id': parseInt(this.$el.get(0).dataset.filterId), 'template_key': this.$el.get(0).dataset.templateKey, 'limit': parseInt(this.$el.get(0).dataset.numberOfRecords), - 'search_domain': this._getSearchDomain() + 'search_domain': this._getSearchDomain(), + 'with_sample': this.editableMode, }, }) .then( diff --git a/addons/website_sale/models/website_snippet_filter.py b/addons/website_sale/models/website_snippet_filter.py index 7b8206b1928..d528e175d75 100644 --- a/addons/website_sale/models/website_snippet_filter.py +++ b/addons/website_sale/models/website_snippet_filter.py @@ -10,3 +10,30 @@ class WebsiteSnippetFilter(models.Model): def _get_website_currency(self): pricelist = self.env['website'].get_current_website().get_current_pricelist() return pricelist.currency_id + + def _get_hardcoded_sample(self, model): + samples = super()._get_hardcoded_sample(model) + if model and model.model == 'product.product': + data = [{ + 'image_512': '/product/static/img/product_chair.png', + 'display_name': _('Chair'), + 'description_sale': _('Sit comfortably'), + }, { + 'image_512': '/product/static/img/product_lamp.png', + 'display_name': _('Lamp'), + 'description_sale': _('Lightbulb sold separately'), + }, { + 'image_512': '/product/static/img/product_product_20-image.png', + 'display_name': _('Whiteboard'), + 'description_sale': _('With three feet'), + }, { + 'image_512': '/product/static/img/product_product_27-image.png', + 'display_name': _('Drawer'), + 'description_sale': _('On wheels'), + }] + merged = [] + for index in range(0, max(len(samples), len(data))): + merged.append({**samples[index % len(samples)], **data[index % len(data)]}) + # merge definitions + samples = merged + return samples From df7c77fd7a7617206645f742242069e3bcd14aa7 Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Mon, 1 Feb 2021 14:18:26 +0000 Subject: [PATCH 2/4] [REF] website: make the rendering code common to real data and sample Before this commit the rendering data structure had to be handled both when rendering a data sample and when rendering actual records. After this commit the rendering data structure is generated in the same way in both situations. task-2446024 https://github.com/odoo/odoo/pull/65176 --- .../website/models/website_snippet_filter.py | 126 +++++++++++------- 1 file changed, 77 insertions(+), 49 deletions(-) diff --git a/addons/website/models/website_snippet_filter.py b/addons/website/models/website_snippet_filter.py index 447581d7996..e77cdbd5d8e 100644 --- a/addons/website/models/website_snippet_filter.py +++ b/addons/website/models/website_snippet_filter.py @@ -101,6 +101,49 @@ class WebsiteSnippetFilter(models.Model): _logger.warning("The provided domain %s in 'ir.actions.server' generated a MissingError in '%s'", search_domain, self._name) return [] + def _record_to_dict_values(self, meta_data, records): + """Converts a list of dictionary records into the structure used by the template for rendering: + + [{ + 'fields': + OrderedDict([ + ('name', 'Afghanistan'), + ('code', 'AF'), + ]), + 'image_fields': + OrderedDict([ + ('image', '/web/image/res.country/3/image?unique=5d9b44e') + ]), + }, ... , ...] + + @param meta_data: OrderedDict result of _get_filter_meta_data + @param records: records as dictionaries of non-escaped values associated to field names + + @return List of Dict matching the rendering structure + """ + result = [] + for record in records: + data = self._get_rendering_data_structure() + for field_name, field_widget in meta_data.items(): + value = record[field_name] + if field_widget in ('binary', 'image'): + data['image_fields'][field_name] = self.escape_falsy_as_empty(value) + elif field_widget == 'monetary': + FieldMonetary = self.env['ir.qweb.field.monetary'] + website_currency = self._get_website_currency() + data['fields'][field_name] = FieldMonetary.value_to_html( + value, + {'display_currency': website_currency} + ) + elif 'ir.qweb.field.%s' % field_widget in self.env: + data['fields'][field_name] = self.env['ir.qweb.field.%s' % field_widget].value_to_html( + value, {}) + else: + data['fields'][field_name] = self.escape_falsy_as_empty(value) + data['fields']['call_to_action_url'] = record['call_to_action_url'] + result.append(data) + return result + def _get_field_name_and_type(self, model, field_name): """ Separates the name and the widget type @@ -122,6 +165,20 @@ class WebsiteSnippetFilter(models.Model): field_type = 'text' return field_name, field_widget or field_type + def _get_filter_meta_data(self): + """ + Extracts the meta data of each field + + @return OrderedDict containing the widget type for each field name + """ + model = self.env[self.filter_id.model_id] if self.filter_id else ( + self.action_server_id.model_id if self.action_server_id else None) + meta_data = OrderedDict({}) + for field_name in self.field_names.split(","): + field_name, field_widget = self._get_field_name_and_type(model, field_name) + meta_data[field_name] = field_widget + return meta_data + def _prepare_sample(self, length=4): """ Generates sample data and returns it the right format for render. @@ -133,37 +190,17 @@ class WebsiteSnippetFilter(models.Model): if not length: return [] sample = [] + meta_data = self._get_filter_meta_data() model = self.env[self.filter_id.model_id] if self.filter_id else ( self.action_server_id.model_id if self.action_server_id else None) sample_data = self._get_hardcoded_sample(model) for index in range(0, length): single_sample_data = sample_data[index % len(sample_data)].copy() - self._fill_sample(single_sample_data, model, index) - data = self._get_rendering_data_structure() - for field_name in self.field_names.split(","): - field_name, field_widget = self._get_field_name_and_type(model, field_name) - value = single_sample_data[field_name] - if field_widget == 'binary': - data['image_fields'][field_name] = self.escape_falsy_as_empty(value) - elif field_widget == 'image': - data['image_fields'][field_name] = value - elif field_widget == 'monetary': - FieldMonetary = self.env['ir.qweb.field.monetary'] - website_currency = self._get_website_currency() - data['fields'][field_name] = FieldMonetary.value_to_html( - value, - {'display_currency': website_currency} - ) - elif 'ir.qweb.field.%s' % field_widget in self.env: - data['fields'][field_name] = self.env['ir.qweb.field.%s' % field_widget].value_to_html( - value, {}) - else: - data['fields'][field_name] = self.escape_falsy_as_empty(value) - data['fields']['call_to_action_url'] = '' - sample.append(data) - return sample + self._fill_sample(single_sample_data, meta_data, index) + sample.append(single_sample_data) + return self._record_to_dict_values(meta_data, sample) - def _fill_sample(self, sample, model, index): + def _fill_sample(self, sample, meta_data, index): """ Fills the sample for the given model @@ -171,8 +208,7 @@ class WebsiteSnippetFilter(models.Model): @param model: Model to which the sample belongs @param index: Index of the sample within the dataset """ - for field_name in self.field_names.split(","): - field_name, field_widget = self._get_field_name_and_type(model, field_name) + for field_name, field_widget in meta_data.items(): if field_name not in sample: if field_widget == 'binary': sample[field_name] = None @@ -184,6 +220,7 @@ class WebsiteSnippetFilter(models.Model): sample[field_name] = index else: sample[field_name] = _('Sample %s', index + 1) + sample['call_to_action_url'] = '' def _get_hardcoded_sample(self, model): """ @@ -222,19 +259,15 @@ class WebsiteSnippetFilter(models.Model): self.ensure_one() values = [] model = self.env[self.filter_id.model_id] + meta_data = self._get_filter_meta_data() Website = self.env['website'] for record in records: - data = self._get_rendering_data_structure() - for field_name in self.field_names.split(","): - field_name, _, field_widget = field_name.partition(":") + data = {} + for field_name, field_widget in meta_data.items(): field = model._fields.get(field_name) - field_widget = field_widget or field.type if field.type == 'binary': - data['image_fields'][field_name] = self.escape_falsy_as_empty(Website.image_url(record, field_name)) - elif field_widget == 'image': - data['image_fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) + data[field_name] = Website.image_url(record, field_name) elif field_widget == 'monetary': - FieldMonetary = self.env['ir.qweb.field.monetary'] model_currency = None if field.type == 'monetary': model_currency = record[record[field_name].currency_field] @@ -242,25 +275,20 @@ class WebsiteSnippetFilter(models.Model): model_currency = record['currency_id'] if model_currency: website_currency = self._get_website_currency() - data['fields'][field_name] = FieldMonetary.value_to_html( - model_currency._convert( - record[field_name], - website_currency, - Website.get_current_website().company_id, - fields.Date.today() - ), - {'display_currency': website_currency} + data[field_name] = model_currency._convert( + record[field_name], + website_currency, + Website.get_current_website().company_id, + fields.Date.today() ) else: - data['fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) - elif ('ir.qweb.field.%s' % field_widget) in self.env: - data['fields'][field_name] = self.env[('ir.qweb.field.%s' % field_widget)].record_to_html(record, field_name, {}) + data[field_name] = record[field_name] else: - data['fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) + data[field_name] = record[field_name] - data['fields']['call_to_action_url'] = 'website_url' in record and record['website_url'] + data['call_to_action_url'] = 'website_url' in record and record['website_url'] values.append(data) - return values + return self._record_to_dict_values(meta_data, values) @api.model def _get_website_currency(self): From 1d2dd3f76fbbb3e8d05d323da4f9c0645dc48022 Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Tue, 2 Feb 2021 10:05:10 +0000 Subject: [PATCH 3/4] [IMP] website, website_sale: remove most escaping from Dynamic Products Before this commit the filtering code for the Dynamic Products snippet was rendering all its values. After this commit the filtering code for the Dynamic Products snippet mostly returns the values to be rendered. Because the list_price still needs to be rendered with the correct currency, the general rendering mechanism is being informed through the field meta-data that the escaping was already done. task-2446024 https://github.com/odoo/odoo/pull/65176 --- addons/website/data/website_data.xml | 12 ++- .../website/models/website_snippet_filter.py | 99 ++++++++++++------- addons/website_sale/data/data.xml | 22 ++--- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index e16d1754bd8..21b49b0a11d 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -723,10 +723,11 @@