From 0cede3fa14d1ce59f8a6c3bfa92facb08f2b3f2f Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Tue, 26 Jan 2021 12:49:11 +0000 Subject: [PATCH] [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