1# -*- coding: utf-8 -*- 2 3from ast import literal_eval 4from collections import OrderedDict 5from odoo import models, fields, api, _ 6from odoo.exceptions import ValidationError, MissingError 7from odoo.osv import expression 8from odoo.tools import html_escape as escape 9from lxml import etree as ET 10import logging 11 12_logger = logging.getLogger(__name__) 13 14 15class WebsiteSnippetFilter(models.Model): 16 _name = 'website.snippet.filter' 17 _inherit = ['website.published.multi.mixin'] 18 _description = 'Website Snippet Filter' 19 _order = 'name ASC' 20 21 name = fields.Char(required=True) 22 action_server_id = fields.Many2one('ir.actions.server', 'Server Action', ondelete='cascade') 23 field_names = fields.Char(help="A list of comma-separated field names", required=True) 24 filter_id = fields.Many2one('ir.filters', 'Filter', ondelete='cascade') 25 limit = fields.Integer(help='The limit is the maximum number of records retrieved', required=True) 26 website_id = fields.Many2one('website', string='Website', ondelete='cascade', required=True) 27 28 @api.model 29 def escape_falsy_as_empty(self, s): 30 return escape(s) if s else '' 31 32 @api.constrains('action_server_id', 'filter_id') 33 def _check_data_source_is_provided(self): 34 for record in self: 35 if bool(record.action_server_id) == bool(record.filter_id): 36 raise ValidationError(_("Either action_server_id or filter_id must be provided.")) 37 38 @api.constrains('limit') 39 def _check_limit(self): 40 """Limit must be between 1 and 16.""" 41 for record in self: 42 if not 0 < record.limit <= 16: 43 raise ValidationError(_("The limit must be between 1 and 16.")) 44 45 @api.constrains('field_names') 46 def _check_field_names(self): 47 for record in self: 48 for field_name in record.field_names.split(","): 49 if not field_name.strip(): 50 raise ValidationError(_("Empty field name in %r") % (record.field_names)) 51 52 def render(self, template_key, limit, search_domain=[]): 53 """Renders the website dynamic snippet items""" 54 self.ensure_one() 55 assert '.dynamic_filter_template_' in template_key, _("You can only use template prefixed by dynamic_filter_template_ ") 56 57 if self.env['website'].get_current_website() != self.website_id: 58 return '' 59 60 records = self._prepare_values(limit, search_domain) 61 View = self.env['ir.ui.view'].sudo().with_context(inherit_branding=False) 62 content = View._render_template(template_key, dict(records=records)).decode('utf-8') 63 return [ET.tostring(el, encoding='utf-8') for el in ET.fromstring('<root>%s</root>' % content).getchildren()] 64 65 def _prepare_values(self, limit=None, search_domain=None): 66 """Gets the data and returns it the right format for render.""" 67 self.ensure_one() 68 limit = limit and min(limit, self.limit) or self.limit 69 if self.filter_id: 70 filter_sudo = self.filter_id.sudo() 71 domain = filter_sudo._get_eval_domain() 72 if 'is_published' in self.env[filter_sudo.model_id]: 73 domain = expression.AND([domain, [('is_published', '=', True)]]) 74 if search_domain: 75 domain = expression.AND([domain, search_domain]) 76 try: 77 records = self.env[filter_sudo.model_id].search( 78 domain, 79 order=','.join(literal_eval(filter_sudo.sort)) or None, 80 limit=limit 81 ) 82 return self._filter_records_to_dict_values(records) 83 except MissingError: 84 _logger.warning("The provided domain %s in 'ir.filters' generated a MissingError in '%s'", domain, self._name) 85 return [] 86 elif self.action_server_id: 87 try: 88 return self.action_server_id.with_context( 89 dynamic_filter=self, 90 limit=limit, 91 search_domain=search_domain, 92 get_rendering_data_structure=self._get_rendering_data_structure, 93 ).sudo().run() 94 except MissingError: 95 _logger.warning("The provided domain %s in 'ir.actions.server' generated a MissingError in '%s'", search_domain, self._name) 96 return [] 97 98 @api.model 99 def _get_rendering_data_structure(self): 100 return { 101 'fields': OrderedDict({}), 102 'image_fields': OrderedDict({}), 103 } 104 105 def _filter_records_to_dict_values(self, records): 106 """Extract the fields from the data source and put them into a dictionary of values 107 108 [{ 109 'fields': 110 OrderedDict([ 111 ('name', 'Afghanistan'), 112 ('code', 'AF'), 113 ]), 114 'image_fields': 115 OrderedDict([ 116 ('image', '/web/image/res.country/3/image?unique=5d9b44e') 117 ]), 118 }, ... , ...] 119 120 """ 121 122 self.ensure_one() 123 values = [] 124 model = self.env[self.filter_id.model_id] 125 Website = self.env['website'] 126 for record in records: 127 data = self._get_rendering_data_structure() 128 for field_name in self.field_names.split(","): 129 field_name, _, field_widget = field_name.partition(":") 130 field = model._fields.get(field_name) 131 field_widget = field_widget or field.type 132 if field.type == 'binary': 133 data['image_fields'][field_name] = self.escape_falsy_as_empty(Website.image_url(record, field_name)) 134 elif field_widget == 'image': 135 data['image_fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) 136 elif field_widget == 'monetary': 137 FieldMonetary = self.env['ir.qweb.field.monetary'] 138 model_currency = None 139 if field.type == 'monetary': 140 model_currency = record[record[field_name].currency_field] 141 elif 'currency_id' in model._fields: 142 model_currency = record['currency_id'] 143 if model_currency: 144 website_currency = self._get_website_currency() 145 data['fields'][field_name] = FieldMonetary.value_to_html( 146 model_currency._convert( 147 record[field_name], 148 website_currency, 149 Website.get_current_website().company_id, 150 fields.Date.today() 151 ), 152 {'display_currency': website_currency} 153 ) 154 else: 155 data['fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) 156 elif ('ir.qweb.field.%s' % field_widget) in self.env: 157 data['fields'][field_name] = self.env[('ir.qweb.field.%s' % field_widget)].record_to_html(record, field_name, {}) 158 else: 159 data['fields'][field_name] = self.escape_falsy_as_empty(record[field_name]) 160 161 data['fields']['call_to_action_url'] = 'website_url' in record and record['website_url'] 162 values.append(data) 163 return values 164 165 @api.model 166 def _get_website_currency(self): 167 company = self.env['website'].get_current_website().company_id 168 return company.currency_id 169