370 lines
15 KiB
Python
370 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import _, api, fields, models
|
|
|
|
|
|
class Users(models.Model):
|
|
_inherit = 'res.users'
|
|
|
|
karma = fields.Integer('Karma', compute='_compute_karma', store=True, readonly=False)
|
|
karma_tracking_ids = fields.One2many('gamification.karma.tracking', 'user_id', string='Karma Changes', groups="base.group_system")
|
|
badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False)
|
|
gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level")
|
|
silver_badge = fields.Integer('Silver badges count', compute="_get_user_badge_level")
|
|
bronze_badge = fields.Integer('Bronze badges count', compute="_get_user_badge_level")
|
|
rank_id = fields.Many2one('gamification.karma.rank', 'Rank')
|
|
next_rank_id = fields.Many2one('gamification.karma.rank', 'Next Rank')
|
|
|
|
@api.depends('karma_tracking_ids.new_value')
|
|
def _compute_karma(self):
|
|
if self.env.context.get('skip_karma_computation'):
|
|
# do not need to update the user karma
|
|
# e.g. during the tracking consolidation
|
|
return
|
|
|
|
self.env['gamification.karma.tracking'].flush_model()
|
|
|
|
select_query = """
|
|
SELECT DISTINCT ON (user_id) user_id, new_value
|
|
FROM gamification_karma_tracking
|
|
WHERE user_id = ANY(%(user_ids)s)
|
|
ORDER BY user_id, tracking_date DESC, id DESC
|
|
"""
|
|
self.env.cr.execute(select_query, {'user_ids': self.ids})
|
|
|
|
user_karma_map = {
|
|
values['user_id']: values['new_value']
|
|
for values in self.env.cr.dictfetchall()
|
|
}
|
|
|
|
for user in self:
|
|
user.karma = user_karma_map.get(user.id, 0)
|
|
|
|
self.sudo()._recompute_rank()
|
|
|
|
@api.depends('badge_ids')
|
|
def _get_user_badge_level(self):
|
|
""" Return total badge per level of users
|
|
TDE CLEANME: shouldn't check type is forum ? """
|
|
for user in self:
|
|
user.gold_badge = 0
|
|
user.silver_badge = 0
|
|
user.bronze_badge = 0
|
|
|
|
self.env.cr.execute("""
|
|
SELECT bu.user_id, b.level, count(1)
|
|
FROM gamification_badge_user bu, gamification_badge b
|
|
WHERE bu.user_id IN %s
|
|
AND bu.badge_id = b.id
|
|
AND b.level IS NOT NULL
|
|
GROUP BY bu.user_id, b.level
|
|
ORDER BY bu.user_id;
|
|
""", [tuple(self.ids)])
|
|
|
|
for (user_id, level, count) in self.env.cr.fetchall():
|
|
# levels are gold, silver, bronze but fields have _badge postfix
|
|
self.browse(user_id)['{}_badge'.format(level)] = count
|
|
|
|
@api.model_create_multi
|
|
def create(self, values_list):
|
|
res = super(Users, self).create(values_list)
|
|
|
|
self._add_karma_batch({
|
|
user: {
|
|
'gain': int(vals['karma']),
|
|
'old_value': 0,
|
|
'origin_ref': f'res.users,{self.env.uid}',
|
|
'reason': _('User Creation'),
|
|
}
|
|
for user, vals in zip(res, values_list)
|
|
if vals.get('karma')
|
|
})
|
|
|
|
return res
|
|
|
|
def write(self, values):
|
|
if 'karma' in values:
|
|
self._add_karma_batch({
|
|
user: {
|
|
'gain': int(values['karma']) - user.karma,
|
|
'origin_ref': f'res.users,{self.env.uid}',
|
|
}
|
|
for user in self
|
|
if int(values['karma']) != user.karma
|
|
})
|
|
return super().write(values)
|
|
|
|
def _add_karma(self, gain, source=None, reason=None):
|
|
self.ensure_one()
|
|
values = {'gain': gain, 'source': source, 'reason': reason}
|
|
return self._add_karma_batch({self: values})
|
|
|
|
def _add_karma_batch(self, values_per_user):
|
|
if not values_per_user:
|
|
return
|
|
|
|
create_values = []
|
|
for user, values in values_per_user.items():
|
|
origin = values.get('source') or self.env.user
|
|
reason = values.get('reason') or _('Add Manually')
|
|
origin_description = f'{origin.display_name} #{origin.id}'
|
|
old_value = values.get('old_value', user.karma)
|
|
|
|
create_values.append({
|
|
'new_value': old_value + values['gain'],
|
|
'old_value': old_value,
|
|
'origin_ref': f'{origin._name},{origin.id}',
|
|
'reason': f'{reason} ({origin_description})',
|
|
'user_id': user.id,
|
|
})
|
|
|
|
self.env['gamification.karma.tracking'].sudo().create(create_values)
|
|
return True
|
|
|
|
def _get_tracking_karma_gain_position(self, user_domain, from_date=None, to_date=None):
|
|
""" Get absolute position in term of gained karma for users. First a ranking
|
|
of all users is done given a user_domain; then the position of each user
|
|
belonging to the current record set is extracted.
|
|
|
|
Example: in website profile, search users with name containing Norbert. Their
|
|
positions should not be 1 to 4 (assuming 4 results), but their actual position
|
|
in the karma gain ranking (with example user_domain being karma > 1,
|
|
website published True).
|
|
|
|
:param user_domain: general domain (i.e. active, karma > 1, website, ...)
|
|
to compute the absolute position of the current record set
|
|
:param from_date: compute karma gained after this date (included) or from
|
|
beginning of time;
|
|
:param to_date: compute karma gained before this date (included) or until
|
|
end of time;
|
|
|
|
:return list: [{
|
|
'user_id': user_id (belonging to current record set),
|
|
'karma_gain_total': integer, karma gained in the given timeframe,
|
|
'karma_position': integer, ranking position
|
|
}, {..}] ordered by karma_position desc
|
|
"""
|
|
if not self:
|
|
return []
|
|
|
|
where_query = self.env['res.users']._where_calc(user_domain)
|
|
user_from_clause, user_where_clause, where_clause_params = where_query.get_sql()
|
|
|
|
params = []
|
|
if from_date:
|
|
date_from_condition = 'AND tracking.tracking_date::DATE >= %s::DATE'
|
|
params.append(from_date)
|
|
if to_date:
|
|
date_to_condition = 'AND tracking.tracking_date::DATE <= %s::DATE'
|
|
params.append(to_date)
|
|
params.append(tuple(self.ids))
|
|
|
|
query = """
|
|
SELECT final.user_id, final.karma_gain_total, final.karma_position
|
|
FROM (
|
|
SELECT intermediate.user_id, intermediate.karma_gain_total, row_number() OVER (ORDER BY intermediate.karma_gain_total DESC) AS karma_position
|
|
FROM (
|
|
SELECT "res_users".id as user_id, COALESCE(SUM("tracking".new_value - "tracking".old_value), 0) as karma_gain_total
|
|
FROM %(user_from_clause)s
|
|
LEFT JOIN "gamification_karma_tracking" as "tracking"
|
|
ON "res_users".id = "tracking".user_id AND "res_users"."active" = TRUE
|
|
WHERE %(user_where_clause)s %(date_from_condition)s %(date_to_condition)s
|
|
GROUP BY "res_users".id
|
|
ORDER BY karma_gain_total DESC
|
|
) intermediate
|
|
) final
|
|
WHERE final.user_id IN %%s""" % {
|
|
'user_from_clause': user_from_clause,
|
|
'user_where_clause': user_where_clause or (not from_date and not to_date and 'TRUE') or '',
|
|
'date_from_condition': date_from_condition if from_date else '',
|
|
'date_to_condition': date_to_condition if to_date else ''
|
|
}
|
|
|
|
self.env.cr.execute(query, tuple(where_clause_params + params))
|
|
return self.env.cr.dictfetchall()
|
|
|
|
def _get_karma_position(self, user_domain):
|
|
""" Get absolute position in term of total karma for users. First a ranking
|
|
of all users is done given a user_domain; then the position of each user
|
|
belonging to the current record set is extracted.
|
|
|
|
Example: in website profile, search users with name containing Norbert. Their
|
|
positions should not be 1 to 4 (assuming 4 results), but their actual position
|
|
in the total karma ranking (with example user_domain being karma > 1,
|
|
website published True).
|
|
|
|
:param user_domain: general domain (i.e. active, karma > 1, website, ...)
|
|
to compute the absolute position of the current record set
|
|
|
|
:return list: [{
|
|
'user_id': user_id (belonging to current record set),
|
|
'karma_position': integer, ranking position
|
|
}, {..}] ordered by karma_position desc
|
|
"""
|
|
if not self:
|
|
return {}
|
|
|
|
where_query = self.env['res.users']._where_calc(user_domain)
|
|
user_from_clause, user_where_clause, where_clause_params = where_query.get_sql()
|
|
|
|
# we search on every user in the DB to get the real positioning (not the one inside the subset)
|
|
# then, we filter to get only the subset.
|
|
query = """
|
|
SELECT sub.user_id, sub.karma_position
|
|
FROM (
|
|
SELECT "res_users"."id" as user_id, row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position
|
|
FROM %(user_from_clause)s
|
|
WHERE %(user_where_clause)s
|
|
) sub
|
|
WHERE sub.user_id IN %%s""" % {
|
|
'user_from_clause': user_from_clause,
|
|
'user_where_clause': user_where_clause or 'TRUE',
|
|
}
|
|
|
|
self.env.cr.execute(query, tuple(where_clause_params + [tuple(self.ids)]))
|
|
return self.env.cr.dictfetchall()
|
|
|
|
def _rank_changed(self):
|
|
"""
|
|
Method that can be called on a batch of users with the same new rank
|
|
"""
|
|
if self.env.context.get('install_mode', False):
|
|
# avoid sending emails in install mode (prevents spamming users when creating data ranks)
|
|
return
|
|
|
|
template = self.env.ref('gamification.mail_template_data_new_rank_reached', raise_if_not_found=False)
|
|
if template:
|
|
for u in self:
|
|
if u.rank_id.karma_min > 0:
|
|
template.send_mail(u.id, force_send=False, email_layout_xmlid='mail.mail_notification_light')
|
|
|
|
def _recompute_rank(self):
|
|
"""
|
|
The caller should filter the users on karma > 0 before calling this method
|
|
to avoid looping on every single users
|
|
|
|
Compute rank of each user by user.
|
|
For each user, check the rank of this user
|
|
"""
|
|
|
|
ranks = [{'rank': rank, 'karma_min': rank.karma_min} for rank in
|
|
self.env['gamification.karma.rank'].search([], order="karma_min DESC")]
|
|
|
|
# 3 is the number of search/requests used by rank in _recompute_rank_bulk()
|
|
if len(self) > len(ranks) * 3:
|
|
self._recompute_rank_bulk()
|
|
return
|
|
|
|
for user in self:
|
|
old_rank = user.rank_id
|
|
if user.karma == 0 and ranks:
|
|
user.write({'next_rank_id': ranks[-1]['rank'].id})
|
|
else:
|
|
for i in range(0, len(ranks)):
|
|
if user.karma >= ranks[i]['karma_min']:
|
|
user.write({
|
|
'rank_id': ranks[i]['rank'].id,
|
|
'next_rank_id': ranks[i - 1]['rank'].id if 0 < i else False
|
|
})
|
|
break
|
|
if old_rank != user.rank_id:
|
|
user._rank_changed()
|
|
|
|
def _recompute_rank_bulk(self):
|
|
"""
|
|
Compute rank of each user by rank.
|
|
For each rank, check which users need to be ranked
|
|
|
|
"""
|
|
ranks = [{'rank': rank, 'karma_min': rank.karma_min} for rank in
|
|
self.env['gamification.karma.rank'].search([], order="karma_min DESC")]
|
|
|
|
users_todo = self
|
|
|
|
next_rank_id = False
|
|
# wtf, next_rank_id should be a related on rank_id.next_rank_id and life might get easier.
|
|
# And we only need to recompute next_rank_id on write with min_karma or in the create on rank model.
|
|
for r in ranks:
|
|
rank_id = r['rank'].id
|
|
dom = [
|
|
('karma', '>=', r['karma_min']),
|
|
('id', 'in', users_todo.ids),
|
|
'|', # noqa
|
|
'|', ('rank_id', '!=', rank_id), ('rank_id', '=', False),
|
|
'|', ('next_rank_id', '!=', next_rank_id), ('next_rank_id', '=', False if next_rank_id else -1),
|
|
]
|
|
users = self.env['res.users'].search(dom)
|
|
if users:
|
|
users_to_notify = self.env['res.users'].search([
|
|
('karma', '>=', r['karma_min']),
|
|
'|', ('rank_id', '!=', rank_id), ('rank_id', '=', False),
|
|
('id', 'in', users.ids),
|
|
])
|
|
users.write({
|
|
'rank_id': rank_id,
|
|
'next_rank_id': next_rank_id,
|
|
})
|
|
users_to_notify._rank_changed()
|
|
users_todo -= users
|
|
|
|
nothing_to_do_users = self.env['res.users'].search([
|
|
('karma', '>=', r['karma_min']),
|
|
'|', ('rank_id', '=', rank_id), ('next_rank_id', '=', next_rank_id),
|
|
('id', 'in', users_todo.ids),
|
|
])
|
|
users_todo -= nothing_to_do_users
|
|
next_rank_id = r['rank'].id
|
|
|
|
if ranks:
|
|
lower_rank = ranks[-1]['rank']
|
|
users = self.env['res.users'].search([
|
|
('karma', '>=', 0),
|
|
('karma', '<', lower_rank.karma_min),
|
|
'|', ('rank_id', '!=', False), ('next_rank_id', '!=', lower_rank.id),
|
|
('id', 'in', users_todo.ids),
|
|
])
|
|
if users:
|
|
users.write({
|
|
'rank_id': False,
|
|
'next_rank_id': lower_rank.id,
|
|
})
|
|
|
|
def _get_next_rank(self):
|
|
""" For fresh users with 0 karma that don't have a rank_id and next_rank_id yet
|
|
this method returns the first karma rank (by karma ascending). This acts as a
|
|
default value in related views.
|
|
|
|
TDE FIXME in post-12.4: make next_rank_id a non-stored computed field correctly computed """
|
|
|
|
if self.next_rank_id:
|
|
return self.next_rank_id
|
|
elif not self.rank_id:
|
|
return self.env['gamification.karma.rank'].search([], order="karma_min ASC", limit=1)
|
|
else:
|
|
return self.env['gamification.karma.rank']
|
|
|
|
def get_gamification_redirection_data(self):
|
|
"""
|
|
Hook for other modules to add redirect button(s) in new rank reached mail
|
|
Must return a list of dictionnary including url and label.
|
|
E.g. return [{'url': '/forum', label: 'Go to Forum'}]
|
|
"""
|
|
self.ensure_one()
|
|
return []
|
|
|
|
def action_karma_report(self):
|
|
self.ensure_one()
|
|
|
|
return {
|
|
'name': _('Karma Updates'),
|
|
'res_model': 'gamification.karma.tracking',
|
|
'target': 'current',
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'tree',
|
|
'context': {
|
|
'default_user_id': self.id,
|
|
'search_default_user_id': self.id,
|
|
},
|
|
}
|