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