mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 10:32:02 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -1,47 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.fields import Domain
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
rating_ids = fields.One2many('rating.rating', 'message_id', groups='base.group_user', string='Related ratings')
|
||||
rating_ids = fields.One2many("rating.rating", "message_id", string="Related ratings")
|
||||
rating_id = fields.Many2one("rating.rating", compute="_compute_rating_id")
|
||||
rating_value = fields.Float(
|
||||
'Rating Value', compute='_compute_rating_value', compute_sudo=True,
|
||||
store=False, search='_search_rating_value')
|
||||
|
||||
@api.depends("rating_ids.consumed")
|
||||
def _compute_rating_id(self):
|
||||
for message in self:
|
||||
message.rating_id = message.rating_ids.filtered(lambda rating: rating.consumed).sorted(
|
||||
"create_date", reverse=True
|
||||
)[:1]
|
||||
|
||||
@api.depends('rating_ids', 'rating_ids.rating')
|
||||
def _compute_rating_value(self):
|
||||
ratings = self.env['rating.rating'].search([('message_id', 'in', self.ids), ('consumed', '=', True)], order='create_date DESC')
|
||||
mapping = dict((r.message_id.id, r.rating) for r in ratings)
|
||||
for message in self:
|
||||
message.rating_value = mapping.get(message.id, 0.0)
|
||||
message.rating_value = message.rating_id.rating if message.rating_id else 0.0
|
||||
|
||||
def _search_rating_value(self, operator, operand):
|
||||
ratings = self.env['rating.rating'].sudo().search([
|
||||
if operator in Domain.NEGATIVE_OPERATORS:
|
||||
return NotImplemented
|
||||
ratings = self.env['rating.rating'].sudo()._search([
|
||||
('rating', operator, operand),
|
||||
('message_id', '!=', False)
|
||||
('message_id', '!=', False),
|
||||
('consumed', '=', True),
|
||||
])
|
||||
return [('id', 'in', ratings.mapped('message_id').ids)]
|
||||
domain = Domain("id", "in", ratings.subselect("message_id"))
|
||||
if operator == "in" and 0 in operand:
|
||||
return domain | Domain("rating_ids", "=", False)
|
||||
return domain
|
||||
|
||||
def message_format(self, format_reply=True):
|
||||
message_values = super().message_format(format_reply=format_reply)
|
||||
rating_mixin_messages = self.filtered(lambda message:
|
||||
message.model
|
||||
and message.res_id
|
||||
and issubclass(self.pool[message.model], self.pool['rating.mixin'])
|
||||
)
|
||||
if rating_mixin_messages:
|
||||
ratings = self.env['rating.rating'].sudo().search([('message_id', 'in', rating_mixin_messages.ids), ('consumed', '=', True)])
|
||||
rating_by_message_id = dict((r.message_id.id, r) for r in ratings)
|
||||
for vals in message_values:
|
||||
if vals['id'] in rating_by_message_id:
|
||||
rating = rating_by_message_id[vals['id']]
|
||||
vals['rating'] = {
|
||||
'id': rating.id,
|
||||
'ratingImageUrl': rating.rating_image_url,
|
||||
'ratingText': rating.rating_text,
|
||||
}
|
||||
return message_values
|
||||
def _to_store_defaults(self, target):
|
||||
# sudo: mail.message - guest and portal user can receive rating of accessible message
|
||||
return super()._to_store_defaults(target) + [
|
||||
Store.One("rating_id", sudo=True),
|
||||
"record_rating",
|
||||
]
|
||||
|
||||
def _to_store(self, store: Store, fields, **kwargs):
|
||||
super()._to_store(store, [f for f in fields if f != "record_rating"], **kwargs)
|
||||
if "record_rating" in fields:
|
||||
for records in self._records_by_model_name().values():
|
||||
if (
|
||||
issubclass(self.pool[records._name], self.pool["rating.mixin"])
|
||||
and records._has_field_access(records._fields["rating_avg"], 'read')
|
||||
):
|
||||
all_stats = {}
|
||||
if records._allow_publish_rating_stats():
|
||||
all_stats = records._rating_get_stats_per_record()
|
||||
record_fields = [
|
||||
"rating_avg",
|
||||
"rating_count",
|
||||
Store.Attr(
|
||||
"rating_stats",
|
||||
lambda record, all_stats=all_stats: all_stats.get(record.id),
|
||||
predicate=lambda record: record._allow_publish_rating_stats(),
|
||||
),
|
||||
]
|
||||
store.add(records, record_fields, as_thread=True)
|
||||
|
||||
def _is_empty(self):
|
||||
return super()._is_empty() and not self.rating_id
|
||||
|
|
|
|||
|
|
@ -1,42 +1,205 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
import datetime
|
||||
import markupsafe
|
||||
|
||||
from odoo import _, fields, models, tools
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
rating_ids = fields.One2many('rating.rating', 'res_id', string='Ratings', groups='base.group_user',
|
||||
domain=lambda self: [('res_model', '=', self._name)], bypass_search_access=True)
|
||||
|
||||
# MAIL OVERRIDES
|
||||
# --------------------------------------------------
|
||||
|
||||
def unlink(self):
|
||||
""" When removing a record, its rating should be deleted too. """
|
||||
record_ids = self.ids
|
||||
result = super().unlink()
|
||||
self.env['rating.rating'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', record_ids)]).unlink()
|
||||
return result
|
||||
|
||||
def _get_message_create_ignore_field_names(self):
|
||||
return super()._get_message_create_ignore_field_names() | {"rating_id"}
|
||||
|
||||
# RATING CONFIGURATION
|
||||
# --------------------------------------------------
|
||||
|
||||
def _rating_apply_get_default_subtype_id(self):
|
||||
return self.env['ir.model.data']._xmlid_to_res_id("mail.mt_comment")
|
||||
|
||||
def _rating_get_operator(self):
|
||||
""" Return the operator (partner) that is the person who is rated.
|
||||
|
||||
:return: res.partner singleton
|
||||
"""
|
||||
if 'user_id' in self and self.user_id.partner_id:
|
||||
return self.user_id.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
def _rating_get_partner(self):
|
||||
""" Return the customer (partner) that performs the rating.
|
||||
|
||||
:return: res.partner singleton
|
||||
"""
|
||||
if 'partner_id' in self and self.partner_id:
|
||||
return self.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
# RATING SUPPORT
|
||||
# --------------------------------------------------
|
||||
|
||||
def _rating_get_access_token(self, partner=None):
|
||||
""" Return access token linked to existing ratings, or create a new rating
|
||||
that will create the asked token. An explicit call to access rights is
|
||||
performed as sudo is used afterwards as this method could be used from
|
||||
different sources, notably templates. """
|
||||
self.check_access('read')
|
||||
if not partner:
|
||||
partner = self._rating_get_partner()
|
||||
rated_partner = self._rating_get_operator()
|
||||
rating = next(
|
||||
(r for r in self.rating_ids.sudo()
|
||||
if r.partner_id.id == partner.id and not r.consumed),
|
||||
None)
|
||||
if not rating:
|
||||
rating = self.env['rating.rating'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'rated_partner_id': rated_partner.id,
|
||||
'res_model_id': self.env['ir.model']._get_id(self._name),
|
||||
'res_id': self.id,
|
||||
'is_internal': False,
|
||||
})
|
||||
return rating.access_token
|
||||
|
||||
# EXPOSED API
|
||||
# --------------------------------------------------
|
||||
|
||||
def rating_send_request(self, template, lang=False, force_send=True):
|
||||
""" This method send rating request by email, using a template given in parameter.
|
||||
|
||||
:param record template: a mail.template record used to compute the message body;
|
||||
:param str lang: optional lang; it can also be specified directly on the template
|
||||
itself in the lang field;
|
||||
:param bool force_send: whether to send the request directly or use the mail
|
||||
queue cron (preferred option);
|
||||
"""
|
||||
if lang:
|
||||
template = template.with_context(lang=lang)
|
||||
self.with_context(mail_notify_force_send=force_send).message_post_with_source(
|
||||
template,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
force_send=force_send,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def rating_apply(self, rate, token=None, rating=None, feedback=None,
|
||||
subtype_xmlid=None, notify_delay_send=False):
|
||||
""" Apply a rating to the record. This rating can either be linked to a
|
||||
token (customer flow) or directly a rating record (code flow).
|
||||
|
||||
If the current model inherits from mail.thread mixin a message is posted
|
||||
on its chatter. User going through this method should have at least
|
||||
employee rights as well as rights on the current record because of rating
|
||||
manipulation and chatter post (either employee, either sudo-ed in public
|
||||
controllers after security check granting access).
|
||||
|
||||
:param float rate: the rating value to apply (from 0 to 5);
|
||||
:param string token: access token to fetch the rating to apply (optional);
|
||||
:param record rating: rating.rating to apply (if no token);
|
||||
:param string feedback: additional feedback (plaintext);
|
||||
:param string subtype_xmlid: xml id of a valid mail.message.subtype used
|
||||
to post the message (if it applies). If not given a classic comment is
|
||||
posted;
|
||||
:param notify_delay_send: Delay the sending by 2 hours of the email so the user
|
||||
can still change his feedback. If False, the email will be sent immediately.
|
||||
|
||||
:returns: rating.rating record
|
||||
"""
|
||||
if rate < 0 or rate > 5:
|
||||
raise ValueError(_('Wrong rating value. A rate should be between 0 and 5 (received %d).', rate))
|
||||
if token:
|
||||
rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1)
|
||||
if not rating:
|
||||
raise ValueError(_('Invalid token or rating.'))
|
||||
|
||||
rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
|
||||
if isinstance(self, self.env.registry['mail.thread']):
|
||||
if subtype_xmlid is None:
|
||||
subtype_id = self._rating_apply_get_default_subtype_id()
|
||||
else:
|
||||
subtype_id = False
|
||||
feedback = tools.plaintext2html(feedback or '', with_paragraph=False)
|
||||
|
||||
scheduled_datetime = (
|
||||
fields.Datetime.now() + datetime.timedelta(hours=2)
|
||||
if notify_delay_send else None
|
||||
)
|
||||
rating_body = (
|
||||
markupsafe.Markup(
|
||||
"<img src='%s' alt=':%s/5' style='width:18px;height:18px;float:left;margin-right: 5px;'/>%s"
|
||||
) % (rating.rating_image_url, rate, feedback)
|
||||
)
|
||||
|
||||
if rating.message_id:
|
||||
self._message_update_content(
|
||||
rating.message_id,
|
||||
body=rating_body,
|
||||
scheduled_date=scheduled_datetime,
|
||||
strict=False,
|
||||
)
|
||||
else:
|
||||
self.message_post(
|
||||
author_id=rating.partner_id.id or None, # None will set the default author in mail/mail_thread.py
|
||||
body=rating_body,
|
||||
rating_id=rating.id,
|
||||
scheduled_date=scheduled_datetime,
|
||||
subtype_id=subtype_id,
|
||||
subtype_xmlid=subtype_xmlid,
|
||||
)
|
||||
return rating
|
||||
|
||||
def message_post(self, **kwargs):
|
||||
rating_id = kwargs.pop('rating_id', False)
|
||||
rating_value = kwargs.pop('rating_value', False)
|
||||
rating_feedback = kwargs.pop('rating_feedback', False)
|
||||
message = super(MailThread, self).message_post(**kwargs)
|
||||
|
||||
# create rating.rating record linked to given rating_value. Using sudo as portal users may have
|
||||
# rights to create messages and therefore ratings (security should be checked beforehand)
|
||||
if rating_value:
|
||||
self.env['rating.rating'].sudo().create({
|
||||
rating_vals = {
|
||||
'rating': float(rating_value) if rating_value is not None else False,
|
||||
'feedback': rating_feedback,
|
||||
'feedback': tools.html2plaintext(kwargs.get('body', '')),
|
||||
'res_model_id': self.env['ir.model']._get_id(self._name),
|
||||
'res_id': self.id,
|
||||
'message_id': message.id,
|
||||
'consumed': True,
|
||||
'partner_id': self.env.user.partner_id.id,
|
||||
})
|
||||
elif rating_id:
|
||||
self.env['rating.rating'].browse(rating_id).write({'message_id': message.id})
|
||||
}
|
||||
rating_id = self.env["rating.rating"].sudo().create(rating_vals).id
|
||||
if rating_id:
|
||||
kwargs["rating_id"] = rating_id
|
||||
return super().message_post(**kwargs)
|
||||
|
||||
return message
|
||||
def _message_post_after_hook(self, message, msg_values):
|
||||
"""Override to link rating to message as sudo. This is done in
|
||||
_message_post_after_hook to be before _notify_thread."""
|
||||
# sudo: rating.rating - can link rating to message from same author and thread
|
||||
rating = self.env["rating.rating"].browse(msg_values.get("rating_id")).sudo()
|
||||
same_author = rating.partner_id and rating.partner_id == message.author_id
|
||||
if same_author and rating.res_model == message.model and rating.res_id == message.res_id:
|
||||
rating.message_id = message.id
|
||||
super()._message_post_after_hook(message, msg_values)
|
||||
|
||||
def _message_create(self, values_list):
|
||||
""" Force usage of rating-specific methods and API allowing to delegate
|
||||
computation to records. Keep methods optimized and skip rating_ids
|
||||
support to simplify MailThrad main API. """
|
||||
if not isinstance(values_list, (list)):
|
||||
values_list = [values_list]
|
||||
if any(values.get('rating_ids') for values in values_list):
|
||||
raise ValueError(_("Posting a rating should be done using message post API."))
|
||||
return super()._message_create(values_list)
|
||||
def _get_allowed_message_params(self):
|
||||
return super()._get_allowed_message_params() | {"rating_value"}
|
||||
|
||||
def _message_update_content(self, message, /, *, body, rating_value=None, **kwargs):
|
||||
if rating_value:
|
||||
message.rating_id.rating = rating_value
|
||||
message.rating_id.feedback = tools.html2plaintext(body)
|
||||
elif rating_value is False:
|
||||
rating_ids = message.rating_ids
|
||||
rating_ids.message_id = False
|
||||
rating_ids.unlink()
|
||||
return super()._message_update_content(message, body=body, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import uuid
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
from odoo.addons.rating.models import rating_data
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
_name = "rating.rating"
|
||||
class RatingRating(models.Model):
|
||||
_name = 'rating.rating'
|
||||
_description = "Rating"
|
||||
_order = 'write_date desc, id desc'
|
||||
_rec_name = 'res_name'
|
||||
|
|
@ -40,7 +41,7 @@ class Rating(models.Model):
|
|||
rated_partner_id = fields.Many2one('res.partner', string="Rated Operator")
|
||||
rated_partner_name = fields.Char(related="rated_partner_id.name")
|
||||
partner_id = fields.Many2one('res.partner', string='Customer')
|
||||
rating = fields.Float(string="Rating Value", group_operator="avg", default=0)
|
||||
rating = fields.Float(string="Rating Value", aggregator="avg", default=0)
|
||||
rating_image = fields.Binary('Image', compute='_compute_rating_image')
|
||||
rating_image_url = fields.Char('Image URL', compute='_compute_rating_image')
|
||||
rating_text = fields.Selection(rating_data.RATING_TEXT, string='Rating', store=True, compute='_compute_rating_text', readonly=True)
|
||||
|
|
@ -51,16 +52,24 @@ class Rating(models.Model):
|
|||
is_internal = fields.Boolean('Visible Internally Only', readonly=False, related='message_id.is_internal', store=True)
|
||||
access_token = fields.Char('Security Token', default=_default_access_token)
|
||||
consumed = fields.Boolean(string="Filled Rating")
|
||||
rated_on = fields.Datetime(string="Rated On")
|
||||
|
||||
_sql_constraints = [
|
||||
('rating_range', 'check(rating >= 0 and rating <= 5)', 'Rating should be between 0 and 5'),
|
||||
]
|
||||
_rating_range = models.Constraint(
|
||||
'check(rating >= 0 and rating <= 5)',
|
||||
'Rating should be between 0 and 5',
|
||||
)
|
||||
|
||||
_consumed_idx = models.Index('(res_model, res_id, write_date) WHERE consumed IS TRUE')
|
||||
_parent_consumed_idx = models.Index('(parent_res_model, parent_res_id, write_date) WHERE consumed IS TRUE')
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_res_name(self):
|
||||
for rating in self:
|
||||
name = self.env[rating.res_model].sudo().browse(rating.res_id).name_get()
|
||||
rating.res_name = name and name[0][1] or ('%s/%s') % (rating.res_model, rating.res_id)
|
||||
if rating.res_model and rating.res_id:
|
||||
name = self.env[rating.res_model].sudo().browse(rating.res_id).display_name
|
||||
else:
|
||||
name = False
|
||||
rating.res_name = name or f'{rating.res_model}/{rating.res_id}'
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_resource_ref(self):
|
||||
|
|
@ -83,8 +92,8 @@ class Rating(models.Model):
|
|||
for rating in self:
|
||||
name = False
|
||||
if rating.parent_res_model and rating.parent_res_id:
|
||||
name = self.env[rating.parent_res_model].sudo().browse(rating.parent_res_id).name_get()
|
||||
name = name and name[0][1] or ('%s/%s') % (rating.parent_res_model, rating.parent_res_id)
|
||||
name = self.env[rating.parent_res_model].sudo().browse(rating.parent_res_id).display_name
|
||||
name = name or f'{rating.parent_res_model}/{rating.parent_res_id}'
|
||||
rating.parent_res_name = name
|
||||
|
||||
def _get_rating_image_filename(self):
|
||||
|
|
@ -96,34 +105,43 @@ class Rating(models.Model):
|
|||
self.rating_image_url = False
|
||||
self.rating_image = False
|
||||
for rating in self:
|
||||
image_path = f'rating/static/src/img/{rating._get_rating_image_filename()}'
|
||||
rating.rating_image_url = f'/{image_path}'
|
||||
try:
|
||||
image_path = get_resource_path('rating', 'static/src/img', rating._get_rating_image_filename())
|
||||
rating.rating_image_url = '/rating/static/src/img/%s' % rating._get_rating_image_filename()
|
||||
rating.rating_image = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
with file_open(image_path, 'rb', filter_ext=('.png',)) as f:
|
||||
rating.rating_image = base64.b64encode(f.read())
|
||||
except OSError:
|
||||
rating.rating_image = False
|
||||
|
||||
@api.depends('rating')
|
||||
def _compute_rating_text(self):
|
||||
for rating in self:
|
||||
rating.rating_text = rating_data._rating_to_text(rating.rating)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if values.get('res_model_id') and values.get('res_id'):
|
||||
values.update(self._find_parent_data(values))
|
||||
if 'rating' in values or 'feedback' in values:
|
||||
values['rated_on'] = fields.Datetime.now()
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if values.get('res_model_id') and values.get('res_id'):
|
||||
values.update(self._find_parent_data(values))
|
||||
return super(Rating, self).write(values)
|
||||
def write(self, vals):
|
||||
if vals.get('res_model_id') and vals.get('res_id'):
|
||||
vals.update(self._find_parent_data(vals))
|
||||
if 'rating' in vals or 'feedback' in vals:
|
||||
vals['rated_on'] = fields.Datetime.now()
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
# OPW-2181568: Delete the chatter message too
|
||||
self.env['mail.message'].search([('rating_ids', 'in', self.ids)]).unlink()
|
||||
return super(Rating, self).unlink()
|
||||
return super().unlink()
|
||||
|
||||
def _find_parent_data(self, values):
|
||||
""" Determine the parent res_model/res_id, based on the values to create or write """
|
||||
|
|
@ -141,6 +159,10 @@ class Rating(models.Model):
|
|||
data['parent_res_id'] = parent_res_model.id
|
||||
return data
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def reset(self):
|
||||
for record in self:
|
||||
record.write({
|
||||
|
|
@ -158,3 +180,33 @@ class Rating(models.Model):
|
|||
'res_id': self.res_id,
|
||||
'views': [[False, 'form']]
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _classify_by_model(self):
|
||||
""" To ease batch computation of various ratings related methods they
|
||||
are classified by model. Ratings not linked to a valid record through
|
||||
res_model / res_id are ignored.
|
||||
|
||||
:returns: for each model having at least one rating in self, have
|
||||
a sub-dict containing
|
||||
* ratings: ratings related to that model;
|
||||
* record IDs: records linked to the ratings of that model, in same
|
||||
order;
|
||||
:rtype: dict
|
||||
"""
|
||||
data_by_model = {}
|
||||
for rating in self.filtered(lambda act: act.res_model and act.res_id):
|
||||
if rating.res_model not in data_by_model:
|
||||
data_by_model[rating.res_model] = {
|
||||
'ratings': self.env['rating.rating'],
|
||||
'record_ids': [],
|
||||
}
|
||||
data_by_model[rating.res_model]['ratings'] += rating
|
||||
data_by_model[rating.res_model]['record_ids'].append(rating.res_id)
|
||||
return data_by_model
|
||||
|
||||
def _to_store_defaults(self, target):
|
||||
return ["rating", "rating_image_url", "rating_text"]
|
||||
|
|
|
|||
|
|
@ -12,16 +12,21 @@ RATING_AVG_MIN = 1
|
|||
RATING_LIMIT_SATISFIED = 4
|
||||
RATING_LIMIT_OK = 3
|
||||
RATING_LIMIT_MIN = 1
|
||||
RATING_HAPPY_VALUE = 5
|
||||
RATING_NEUTRAL_VALUE = 3
|
||||
RATING_UNHAPPY_VALUE = 1
|
||||
RATING_NONE_VALUE = 0
|
||||
|
||||
RATING_TEXT = [
|
||||
('top', 'Satisfied'),
|
||||
('ok', 'Okay'),
|
||||
('ko', 'Dissatisfied'),
|
||||
('none', 'No Rating yet'),
|
||||
('top', 'Happy'),
|
||||
('ok', 'Neutral'),
|
||||
('ko', 'Unhappy'),
|
||||
('none', 'Not Rated yet'),
|
||||
]
|
||||
|
||||
OPERATOR_MAPPING = {
|
||||
'=': operator.eq,
|
||||
'!=': operator.ne,
|
||||
'in': lambda elem, container: elem in container,
|
||||
'not in': lambda elem, container: elem not in container,
|
||||
'<': operator.lt,
|
||||
'<=': operator.le,
|
||||
'>': operator.gt,
|
||||
|
|
@ -38,7 +43,7 @@ def _rating_avg_to_text(rating_avg):
|
|||
return 'none'
|
||||
|
||||
def _rating_assert_value(rating_value):
|
||||
assert 0 <= rating_value <= 5
|
||||
assert RATING_NONE_VALUE <= rating_value <= RATING_HAPPY_VALUE
|
||||
|
||||
def _rating_to_grade(rating_value):
|
||||
""" From a rating value give a text-based mean value. """
|
||||
|
|
@ -65,9 +70,9 @@ def _rating_to_threshold(rating_value):
|
|||
notably for images. """
|
||||
_rating_assert_value(rating_value)
|
||||
if rating_value >= RATING_LIMIT_SATISFIED:
|
||||
return 5
|
||||
return RATING_HAPPY_VALUE
|
||||
if rating_value >= RATING_LIMIT_OK:
|
||||
return 3
|
||||
return RATING_NEUTRAL_VALUE
|
||||
if rating_value >= RATING_LIMIT_MIN:
|
||||
return 1
|
||||
return 0
|
||||
return RATING_UNHAPPY_VALUE
|
||||
return RATING_NONE_VALUE
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import markupsafe
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.rating.models import rating_data
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools.float_utils import float_compare, float_round
|
||||
|
||||
|
||||
class RatingMixin(models.AbstractModel):
|
||||
"""This mixin adds rating statistics to mail.thread that already support ratings."""
|
||||
_name = 'rating.mixin'
|
||||
_description = "Rating Mixin"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
rating_ids = fields.One2many('rating.rating', 'res_id', string='Rating', groups='base.group_user', domain=lambda self: [('res_model', '=', self._name)], auto_join=True)
|
||||
rating_last_value = fields.Float('Rating Last Value', groups='base.group_user', compute='_compute_rating_last_value', compute_sudo=True, store=True)
|
||||
rating_last_value = fields.Float('Rating Last Value', groups='base.group_user', compute='_compute_rating_last_value', compute_sudo=True, store=True, aggregator="avg")
|
||||
rating_last_feedback = fields.Text('Rating Last Feedback', groups='base.group_user', related='rating_ids.feedback')
|
||||
rating_last_image = fields.Binary('Rating Last Image', groups='base.group_user', related='rating_ids.rating_image')
|
||||
rating_count = fields.Integer('Rating count', compute="_compute_rating_stats", compute_sudo=True)
|
||||
|
|
@ -52,23 +51,24 @@ class RatingMixin(models.AbstractModel):
|
|||
@api.depends('rating_ids.res_id', 'rating_ids.rating')
|
||||
def _compute_rating_stats(self):
|
||||
""" Compute avg and count in one query, as thoses fields will be used together most of the time. """
|
||||
domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]])
|
||||
read_group_res = self.env['rating.rating'].read_group(domain, ['rating:avg'], groupby=['res_id'], lazy=False) # force average on rating column
|
||||
mapping = {item['res_id']: {'rating_count': item['__count'], 'rating_avg': item['rating']} for item in read_group_res}
|
||||
domain = self._rating_domain() & Domain('rating', '>=', rating_data.RATING_LIMIT_MIN)
|
||||
read_group_res = self.env['rating.rating']._read_group(domain, ['res_id'], aggregates=['__count', 'rating:avg']) # force average on rating column
|
||||
mapping = {res_id: {'rating_count': count, 'rating_avg': rating_avg} for res_id, count, rating_avg in read_group_res}
|
||||
for record in self:
|
||||
record.rating_count = mapping.get(record.id, {}).get('rating_count', 0)
|
||||
record.rating_avg = mapping.get(record.id, {}).get('rating_avg', 0)
|
||||
|
||||
def _search_rating_avg(self, operator, value):
|
||||
if operator not in rating_data.OPERATOR_MAPPING:
|
||||
raise NotImplementedError('This operator %s is not supported in this search method.' % operator)
|
||||
rating_read_group = self.env['rating.rating'].sudo().read_group(
|
||||
op = rating_data.OPERATOR_MAPPING.get(operator)
|
||||
if not op:
|
||||
return NotImplemented
|
||||
rating_read_group = self.env['rating.rating'].sudo()._read_group(
|
||||
[('res_model', '=', self._name), ('consumed', '=', True), ('rating', '>=', rating_data.RATING_LIMIT_MIN)],
|
||||
['res_id', 'rating_avg:avg(rating)'], ['res_id'])
|
||||
['res_id'], ['rating:avg'])
|
||||
res_ids = [
|
||||
res['res_id']
|
||||
for res in rating_read_group
|
||||
if rating_data.OPERATOR_MAPPING[operator](float_compare(res['rating_avg'], value, 2), 0)
|
||||
res_id
|
||||
for res_id, rating_avg in rating_read_group
|
||||
if op(float_compare(rating_avg, value, 2), 0)
|
||||
]
|
||||
return [('id', 'in', res_ids)]
|
||||
|
||||
|
|
@ -81,190 +81,43 @@ class RatingMixin(models.AbstractModel):
|
|||
def _compute_rating_satisfaction(self):
|
||||
""" Compute the rating satisfaction percentage, this is done separately from rating_count and rating_avg
|
||||
since the query is different, to avoid computing if it is not necessary"""
|
||||
domain = expression.AND([self._rating_domain(), [('rating', '>=', rating_data.RATING_LIMIT_MIN)]])
|
||||
domain = self._rating_domain() & Domain('rating', '>=', rating_data.RATING_LIMIT_MIN)
|
||||
# See `_compute_rating_percentage_satisfaction` above
|
||||
read_group_res = self.env['rating.rating']._read_group(domain, ['res_id', 'rating'], groupby=['res_id', 'rating'], lazy=False)
|
||||
read_group_res = self.env['rating.rating']._read_group(domain, ['res_id', 'rating'], aggregates=['__count'])
|
||||
default_grades = {'great': 0, 'okay': 0, 'bad': 0}
|
||||
grades_per_record = {record_id: default_grades.copy() for record_id in self.ids}
|
||||
|
||||
for group in read_group_res:
|
||||
record_id = group['res_id']
|
||||
grade = rating_data._rating_to_grade(group['rating'])
|
||||
grades_per_record[record_id][grade] += group['__count']
|
||||
for record_id, rating, count in read_group_res:
|
||||
grade = rating_data._rating_to_grade(rating)
|
||||
grades_per_record[record_id][grade] += count
|
||||
|
||||
for record in self:
|
||||
grade_repartition = grades_per_record.get(record.id, default_grades)
|
||||
grade_count = sum(grade_repartition.values())
|
||||
record.rating_percentage_satisfaction = grade_repartition['great'] * 100 / grade_count if grade_count else -1
|
||||
|
||||
def write(self, values):
|
||||
def write(self, vals):
|
||||
""" If the rated ressource name is modified, we should update the rating res_name too.
|
||||
If the rated ressource parent is changed we should update the parent_res_id too"""
|
||||
with self.env.norecompute():
|
||||
result = super(RatingMixin, self).write(values)
|
||||
for record in self:
|
||||
if record._rec_name in values: # set the res_name of ratings to be recomputed
|
||||
res_name_field = self.env['rating.rating']._fields['res_name']
|
||||
self.env.add_to_compute(res_name_field, record.rating_ids)
|
||||
if record._rating_get_parent_field_name() in values:
|
||||
record.rating_ids.sudo().write({'parent_res_id': record[record._rating_get_parent_field_name()].id})
|
||||
result = super().write(vals)
|
||||
for record in self.sudo(): # ratings may be inaccessible
|
||||
if record._rec_name in vals: # set the res_name of ratings to be recomputed
|
||||
res_name_field = self.env['rating.rating']._fields['res_name']
|
||||
self.env.add_to_compute(res_name_field, record.rating_ids)
|
||||
if record._rating_get_parent_field_name() in vals:
|
||||
record.rating_ids.write({'parent_res_id': record[record._rating_get_parent_field_name()].id})
|
||||
|
||||
return result
|
||||
|
||||
def unlink(self):
|
||||
""" When removing a record, its rating should be deleted too. """
|
||||
record_ids = self.ids
|
||||
result = super(RatingMixin, self).unlink()
|
||||
self.env['rating.rating'].sudo().search([('res_model', '=', self._name), ('res_id', 'in', record_ids)]).unlink()
|
||||
return result
|
||||
|
||||
def _rating_get_parent_field_name(self):
|
||||
"""Return the parent relation field name
|
||||
Should return a Many2One"""
|
||||
"""Return the parent relation field name. Should return a Many2One"""
|
||||
return None
|
||||
|
||||
def _rating_domain(self):
|
||||
""" Returns a normalized domain on rating.rating to select the records to
|
||||
include in count, avg, ... computation of current model.
|
||||
"""
|
||||
return ['&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)]
|
||||
|
||||
def _rating_get_partner(self):
|
||||
""" Return the customer (partner) that performs the rating.
|
||||
|
||||
:return record: res.partner singleton
|
||||
"""
|
||||
if hasattr(self, 'partner_id') and self.partner_id:
|
||||
return self.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
def _rating_get_operator(self):
|
||||
""" Return the operator (partner) that is the person who is rated.
|
||||
|
||||
:return record: res.partner singleton
|
||||
"""
|
||||
if hasattr(self, 'user_id') and self.user_id.partner_id:
|
||||
return self.user_id.partner_id
|
||||
return self.env['res.partner']
|
||||
|
||||
def _rating_get_access_token(self, partner=None):
|
||||
""" Return access token linked to existing ratings, or create a new rating
|
||||
that will create the asked token. An explicit call to access rights is
|
||||
performed as sudo is used afterwards as this method could be used from
|
||||
different sources, notably templates. """
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
if not partner:
|
||||
partner = self._rating_get_partner()
|
||||
rated_partner = self._rating_get_operator()
|
||||
ratings = self.rating_ids.sudo().filtered(lambda x: x.partner_id.id == partner.id and not x.consumed)
|
||||
if not ratings:
|
||||
rating = self.env['rating.rating'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'rated_partner_id': rated_partner.id,
|
||||
'res_model_id': self.env['ir.model']._get_id(self._name),
|
||||
'res_id': self.id,
|
||||
'is_internal': False,
|
||||
})
|
||||
else:
|
||||
rating = ratings[0]
|
||||
return rating.access_token
|
||||
|
||||
def rating_send_request(self, template, lang=False, subtype_id=False, force_send=True, composition_mode='comment',
|
||||
email_layout_xmlid=None):
|
||||
""" This method send rating request by email, using a template given
|
||||
in parameter.
|
||||
|
||||
:param record template: a mail.template record used to compute the message body;
|
||||
:param str lang: optional lang; it can also be specified directly on the template
|
||||
itself in the lang field;
|
||||
:param int subtype_id: optional subtype to use when creating the message; is
|
||||
a note by default to avoid spamming followers;
|
||||
:param bool force_send: whether to send the request directly or use the mail
|
||||
queue cron (preferred option);
|
||||
:param str composition_mode: comment (message_post) or mass_mail (template.send_mail);
|
||||
:param str email_layout_xmlid: layout used to encapsulate the content when sending email;
|
||||
"""
|
||||
if lang:
|
||||
template = template.with_context(lang=lang)
|
||||
if subtype_id is False:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
if force_send:
|
||||
self = self.with_context(mail_notify_force_send=True) # default value is True, should be set to false if not?
|
||||
for record in self:
|
||||
record.message_post_with_template(
|
||||
template.id,
|
||||
composition_mode=composition_mode,
|
||||
email_layout_xmlid=email_layout_xmlid if email_layout_xmlid is not None else 'mail.mail_notification_light',
|
||||
subtype_id=subtype_id
|
||||
)
|
||||
|
||||
def rating_apply(self, rate, token=None, rating=None, feedback=None,
|
||||
subtype_xmlid=None, notify_delay_send=False):
|
||||
""" Apply a rating to the record. This rating can either be linked to a
|
||||
token (customer flow) or directly a rating record (code flow).
|
||||
|
||||
If the current model inherits from mail.thread mixin a message is posted
|
||||
on its chatter. User going through this method should have at least
|
||||
employee rights as well as rights on the current record because of rating
|
||||
manipulation and chatter post (either employee, either sudo-ed in public
|
||||
controllers after security check granting access).
|
||||
|
||||
:param float rate: the rating value to apply (from 0 to 5);
|
||||
:param string token: access token to fetch the rating to apply (optional);
|
||||
:param record rating: rating.rating to apply (if no token);
|
||||
:param string feedback: additional feedback (plaintext);
|
||||
:param string subtype_xmlid: xml id of a valid mail.message.subtype used
|
||||
to post the message (if it applies). If not given a classic comment is
|
||||
posted;
|
||||
:param notify_delay_send: Delay the sending by 2 hours of the email so the user
|
||||
can still change his feedback. If False, the email will be sent immediately.
|
||||
|
||||
:returns rating: rating.rating record
|
||||
"""
|
||||
if rate < 0 or rate > 5:
|
||||
raise ValueError('Wrong rating value. A rate should be between 0 and 5 (received %d).' % rate)
|
||||
if token:
|
||||
rating = self.env['rating.rating'].search([('access_token', '=', token)], limit=1)
|
||||
if not rating:
|
||||
raise ValueError('Invalid token or rating.')
|
||||
|
||||
rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
|
||||
if isinstance(self, self.env.registry['mail.thread']):
|
||||
if subtype_xmlid is None:
|
||||
subtype_id = self._rating_apply_get_default_subtype_id()
|
||||
else:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid)
|
||||
feedback = tools.plaintext2html(feedback or '')
|
||||
|
||||
scheduled_datetime = (
|
||||
fields.Datetime.now() + datetime.timedelta(hours=2)
|
||||
if notify_delay_send else None
|
||||
)
|
||||
rating_body = (
|
||||
markupsafe.Markup(
|
||||
"<img src='%s' alt=':%s/5' style='width:18px;height:18px;float:left;margin-right: 5px;'/>%s"
|
||||
) % (rating.rating_image_url, rate, feedback)
|
||||
)
|
||||
|
||||
if rating.message_id:
|
||||
self._message_update_content(
|
||||
rating.message_id, rating_body,
|
||||
scheduled_date=scheduled_datetime,
|
||||
strict=False
|
||||
)
|
||||
else:
|
||||
self.message_post(
|
||||
author_id=rating.partner_id.id or None, # None will set the default author in mail_thread.py
|
||||
body=rating_body,
|
||||
rating_id=rating.id,
|
||||
scheduled_date=scheduled_datetime,
|
||||
subtype_id=subtype_id,
|
||||
)
|
||||
return rating
|
||||
|
||||
def _rating_apply_get_default_subtype_id(self):
|
||||
return self.env['ir.model.data']._xmlid_to_res_id("mail.mt_comment")
|
||||
return Domain([('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)])
|
||||
|
||||
def _rating_get_repartition(self, add_stats=False, domain=None):
|
||||
""" get the repatition of rating grade for the given res_ids.
|
||||
|
|
@ -278,33 +131,39 @@ class RatingMixin(models.AbstractModel):
|
|||
otherwise, key is the value of the information (string) : either stat name (avg, total, ...) or 'repartition'
|
||||
containing the same dict if add_stats was False.
|
||||
"""
|
||||
base_domain = expression.AND([self._rating_domain(), [('rating', '>=', 1)]])
|
||||
base_domain = self._rating_domain() & Domain('rating', '>=', 1)
|
||||
if domain:
|
||||
base_domain += domain
|
||||
rg_data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id'])
|
||||
# init dict with all posible rate value, except 0 (no value for the rating)
|
||||
base_domain &= Domain(domain)
|
||||
rg_data = self.env['rating.rating']._read_group(base_domain, ['rating'], ['__count'])
|
||||
# init dict with all possible rate value, except 0 (no value for the rating)
|
||||
values = dict.fromkeys(range(1, 6), 0)
|
||||
for rating_rg in rg_data:
|
||||
rating_val_round = float_round(rating_rg['rating'], precision_digits=1)
|
||||
values[rating_val_round] = values.get(rating_val_round, 0) + rating_rg['rating_count']
|
||||
for rating, count in rg_data:
|
||||
rating_val_round = float_round(rating, precision_digits=1)
|
||||
values[rating_val_round] = values.get(rating_val_round, 0) + count
|
||||
# add other stats
|
||||
if add_stats:
|
||||
rating_number = sum(values.values())
|
||||
result = {
|
||||
return {
|
||||
'repartition': values,
|
||||
'avg': sum(float(key * values[key]) for key in values) / rating_number if rating_number > 0 else 0,
|
||||
'total': sum(it['rating_count'] for it in rg_data),
|
||||
'total': sum(count for __, count in rg_data),
|
||||
}
|
||||
return result
|
||||
return values
|
||||
|
||||
def rating_get_grades(self, domain=None):
|
||||
""" get the repatition of rating grade for the given res_ids.
|
||||
:param domain : optional domain of the rating to include/exclude in grades computation
|
||||
:return dictionnary where the key is the grade (great, okay, bad), and the value, the number of object (res_model, res_id) having the grade
|
||||
the grade are compute as 0-30% : Bad
|
||||
31-69%: Okay
|
||||
70-100%: Great
|
||||
""" Get the repartitions of rating grade for the given res_ids.
|
||||
:param domain: Optional domain of the rating to include/exclude
|
||||
in the grades computation.
|
||||
:returns: A dictionary where the key is the rating and the value
|
||||
is the count of unique ``(res_model, res_id)`` pairs whose
|
||||
grades are associated with that rating.
|
||||
|
||||
The rates are:
|
||||
|
||||
* ``"great"``, graded between 70 and 100
|
||||
* ``"okay"``, graded between 31 and 69
|
||||
* ``"bad"``, graded between 0 and 30
|
||||
:rtype: dict[typing.Literal["great", "okay", "bad"], int]
|
||||
"""
|
||||
data = self._rating_get_repartition(domain=domain)
|
||||
res = dict.fromkeys(['great', 'okay', 'bad'], 0)
|
||||
|
|
@ -314,12 +173,14 @@ class RatingMixin(models.AbstractModel):
|
|||
return res
|
||||
|
||||
def rating_get_stats(self, domain=None):
|
||||
""" get the statistics of the rating repatition
|
||||
:param domain : optional domain of the rating to include/exclude in statistic computation
|
||||
:return dictionnary where
|
||||
- key is the name of the information (stat name)
|
||||
- value is statistic value : 'percent' contains the repartition in percentage, 'avg' is the average rate
|
||||
and 'total' is the number of rating
|
||||
"""Get the statistics of the rating repartitions
|
||||
|
||||
:param domain : optional domain of the rating to include/exclude in statistic computation
|
||||
:returns: A dictionnary where:
|
||||
|
||||
- key is the name of the information (stat name)
|
||||
- value is statistic value : 'percent' contains the repartition in percentage, 'avg' is the average rate
|
||||
and 'total' is the number of rating
|
||||
"""
|
||||
data = self._rating_get_repartition(domain=domain, add_stats=True)
|
||||
result = {
|
||||
|
|
@ -330,3 +191,46 @@ class RatingMixin(models.AbstractModel):
|
|||
for rate in data['repartition']:
|
||||
result['percent'][rate] = (data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0
|
||||
return result
|
||||
|
||||
def _rating_get_stats_per_record(self, domain=None):
|
||||
"""
|
||||
Computes rating statistics for each record individually.
|
||||
|
||||
:param domain: Optional domain to apply on the ratings.
|
||||
:return: A dictionary mapping each record ID to its statistics dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
base_domain = self._rating_domain() & Domain("rating", ">=", 1)
|
||||
if domain:
|
||||
base_domain &= Domain(domain)
|
||||
rg_data = self.env["rating.rating"]._read_group(
|
||||
base_domain,
|
||||
groupby=["res_id", "rating"],
|
||||
aggregates=["__count"],
|
||||
)
|
||||
stats_per_record = defaultdict(
|
||||
lambda: {"total": 0, "weighted_sum": 0.0, "counts": defaultdict(int), "percent": {}}
|
||||
)
|
||||
for res_id, rating, count in rg_data:
|
||||
stats = stats_per_record[res_id]
|
||||
stats["total"] += count
|
||||
stats["weighted_sum"] += rating * count
|
||||
stats["counts"][int(rating)] = count
|
||||
for stats in stats_per_record.values():
|
||||
total = stats["total"]
|
||||
if total > 0:
|
||||
stats["avg"] = stats["weighted_sum"] / total
|
||||
stats["percent"] = {
|
||||
rate: (stats["counts"].get(rate, 0) * 100) / total for rate in range(1, 6)
|
||||
}
|
||||
else:
|
||||
stats["avg"] = 0
|
||||
stats["percent"] = dict.fromkeys(range(1, 6), 0.0)
|
||||
del stats["weighted_sum"]
|
||||
del stats["counts"]
|
||||
return stats_per_record
|
||||
|
||||
@api.model
|
||||
def _allow_publish_rating_stats(self):
|
||||
"""Override to allow the rating stats to be demonstrated."""
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
|
@ -6,7 +5,7 @@ from datetime import timedelta
|
|||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.rating.models import rating_data
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
|
|
@ -17,7 +16,7 @@ class RatingParentMixin(models.AbstractModel):
|
|||
|
||||
rating_ids = fields.One2many(
|
||||
'rating.rating', 'parent_res_id', string='Ratings',
|
||||
auto_join=True, groups='base.group_user',
|
||||
bypass_search_access=True, groups='base.group_user',
|
||||
domain=lambda self: [('parent_res_model', '=', self._name)])
|
||||
rating_percentage_satisfaction = fields.Integer(
|
||||
"Rating Satisfaction",
|
||||
|
|
@ -34,18 +33,17 @@ class RatingParentMixin(models.AbstractModel):
|
|||
# build domain and fetch data
|
||||
domain = [('parent_res_model', '=', self._name), ('parent_res_id', 'in', self.ids), ('rating', '>=', rating_data.RATING_LIMIT_MIN), ('consumed', '=', True)]
|
||||
if self._rating_satisfaction_days:
|
||||
domain += [('write_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=self._rating_satisfaction_days)))]
|
||||
data = self.env['rating.rating'].read_group(domain, ['parent_res_id', 'rating'], ['parent_res_id', 'rating'], lazy=False)
|
||||
domain += [('write_date', '>=', fields.Datetime.to_string(fields.Datetime.now() - timedelta(days=self._rating_satisfaction_days)))]
|
||||
data = self.env['rating.rating']._read_group(domain, ['parent_res_id', 'rating'], ['__count'])
|
||||
|
||||
# get repartition of grades per parent id
|
||||
default_grades = {'great': 0, 'okay': 0, 'bad': 0}
|
||||
grades_per_parent = dict((parent_id, dict(default_grades)) for parent_id in self.ids) # map: {parent_id: {'great': 0, 'bad': 0, 'ok': 0}}
|
||||
rating_scores_per_parent = defaultdict(int) # contains the total of the rating values per record
|
||||
for item in data:
|
||||
parent_id = item['parent_res_id']
|
||||
grade = rating_data._rating_to_grade(item['rating'])
|
||||
grades_per_parent[parent_id][grade] += item['__count']
|
||||
rating_scores_per_parent[parent_id] += item['rating'] * item['__count']
|
||||
for parent_id, rating, count in data:
|
||||
grade = rating_data._rating_to_grade(rating)
|
||||
grades_per_parent[parent_id][grade] += count
|
||||
rating_scores_per_parent[parent_id] += rating * count
|
||||
|
||||
# compute percentage per parent
|
||||
for record in self:
|
||||
|
|
@ -57,16 +55,17 @@ class RatingParentMixin(models.AbstractModel):
|
|||
record.rating_avg_percentage = record.rating_avg / 5
|
||||
|
||||
def _search_rating_avg(self, operator, value):
|
||||
if operator not in rating_data.OPERATOR_MAPPING:
|
||||
raise NotImplementedError('This operator %s is not supported in this search method.' % operator)
|
||||
domain = [('parent_res_model', '=', self._name), ('consumed', '=', True), ('rating', '>=', rating_data.RATING_LIMIT_MIN)]
|
||||
op = rating_data.OPERATOR_MAPPING.get(operator)
|
||||
if not op:
|
||||
return NotImplemented
|
||||
domain = Domain([('parent_res_model', '=', self._name), ('consumed', '=', True), ('rating', '>=', rating_data.RATING_LIMIT_MIN)])
|
||||
if self._rating_satisfaction_days:
|
||||
min_date = fields.datetime.now() - timedelta(days=self._rating_satisfaction_days)
|
||||
domain = expression.AND([domain, [('write_date', '>=', fields.Datetime.to_string(min_date))]])
|
||||
rating_read_group = self.env['rating.rating'].sudo().read_group(domain, ['parent_res_id', 'rating_avg:avg(rating)'], ['parent_res_id'])
|
||||
min_date = fields.Datetime.now() - timedelta(days=self._rating_satisfaction_days)
|
||||
domain &= Domain('write_date', '>=', fields.Datetime.to_string(min_date))
|
||||
rating_read_group = self.env['rating.rating'].sudo()._read_group(domain, ['parent_res_id'], ['rating:avg'])
|
||||
parent_res_ids = [
|
||||
res['parent_res_id']
|
||||
for res in rating_read_group
|
||||
if rating_data.OPERATOR_MAPPING[operator](float_compare(res['rating_avg'], value, 2), 0)
|
||||
parent_res_id
|
||||
for parent_res_id, rating_avg in rating_read_group
|
||||
if op(float_compare(rating_avg, value, 2), 0)
|
||||
]
|
||||
return [('id', 'in', parent_res_ids)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue