[MERGE] gamification: track karma changes on users

PURPOSE

Allow karma gain tracking enabling notably display of top users based on
weekly / monthly gain in website profile.

SPECIFICATIONS

Each time a user gains karma a record is created in the gamification karma
tracking model. Scheduled activity runs to consolidate the records into
monthly gain records to avoid having crowdy table and unnecessary noise
in karma gain.

This model is made private and only accessible through some dedicated
compute methods / controllers used in website profile.

In website profile module buttons are added to see users ranking based
on their total karma (like before) but also by last week and last month
gains (using the newly introduced tracking model).

Some fixes are provided in this merge as well as tests.

LINKS

Task ID 2003505
PR #34594

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2019-11-05 19:22:38 +01:00
committed by GitHub
14 changed files with 743 additions and 187 deletions
+3 -1
View File
@@ -33,9 +33,11 @@ Both goals and badges are flexibles and can be adapted to a large range of modul
'data/gamification_karma_rank_data.xml',
'views/gamification.xml',
'views/gamification_karma_rank_views.xml',
'views/gamification_karma_tracking_views.xml',
'views/mail_templates.xml'
],
'demo': [
'data/gamification_karma_rank_demo.xml'
'data/gamification_karma_rank_demo.xml',
'data/gamification_karma_tracking_demo.xml',
],
}
+13
View File
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_check_challenge" model="ir.cron">
@@ -10,5 +11,17 @@
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
</record>
<record id="ir_cron_consolidate_last_month" model="ir.cron">
<field name="name">Gamification: Karma tracking consolidation</field>
<field name="model_id" ref="model_gamification_karma_tracking"/>
<field name="state">code</field>
<field name="code">model._consolidate_last_month()</field>
<field name="active" eval="True"/>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field>
<field name="nextcall" eval="(DateTime.now() + relativedelta(day=1, months=1)).strftime('%Y-%m-%d 04:00:00')" />
</record>
</data>
</odoo>
@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- Set demo karma for test tour as demo user-->
<record id="base.user_demo" model="res.users">
<field name="karma">20</field>
</record>
<record id="base.demo_user0" model="res.users">
<field name="karma">10</field>
</record>
<!--Ranks-->
<record id="rank_student" model="gamification.karma.rank">
<field name="description_motivational" type="html">
@@ -44,5 +36,4 @@
</div>
</field>
</record>
</data></odoo>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!--base.user_demo-->
<record id="karma_tracking_user_demo_1st_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_demo"/>
<field name="old_value">0</field>
<field name="new_value">1000</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=1, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_demo_2nd_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_demo"/>
<field name="old_value">1000</field>
<field name="new_value">1500</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=2, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_demo_5th_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_demo"/>
<field name="old_value">1500</field>
<field name="new_value">2000</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=5, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_demo_20th_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_demo"/>
<field name="old_value">2000</field>
<field name="new_value">2050</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=20, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="base.user_demo" model="res.users">
<field name="karma">2500</field>
</record>
<!--base.demo_user0 -->
<record id="karma_tracking_user_portal_2nd_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.demo_user0"/>
<field name="old_value">0</field>
<field name="new_value">5</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=2, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_portal_3rd_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.demo_user0"/>
<field name="old_value">5</field>
<field name="new_value">10</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=3, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_portal_10th_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.demo_user0"/>
<field name="old_value">10</field>
<field name="new_value">20</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=10, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_portal_today_0" model="gamification.karma.tracking">
<field name="user_id" ref="base.demo_user0"/>
<field name="old_value">20</field>
<field name="new_value">25</field>
<field name="tracking_date" eval="(DateTime.now()).strftime('%Y-%m-%d')"/>
</record>
<record id="base.demo_user0" model="res.users">
<field name="karma">30</field>
</record>
<!--base.user_admin (already have a tracking to 2500)-->
<record id="karma_tracking_user_admin_1st_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_admin"/>
<field name="old_value">0</field>
<field name="new_value">2000</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=1, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_admin_5th_day_last_month" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_admin"/>
<field name="old_value">2000</field>
<field name="new_value">2250</field>
<field name="tracking_date" eval="(DateTime.now() - relativedelta(day=5, months=1)).strftime('%Y-%m-%d')"/>
</record>
<record id="karma_tracking_user_admin_today" model="gamification.karma.tracking">
<field name="user_id" ref="base.user_admin"/>
<field name="old_value">2250</field>
<field name="new_value">2500</field>
<field name="tracking_date" eval="(DateTime.now()).strftime('%Y-%m-%d')"/>
</record>
</data>
</odoo>
+1
View File
@@ -5,4 +5,5 @@ from . import goal
from . import challenge
from . import badge
from . import gamification_karma_rank
from . import gamification_karma_tracking
from . import res_users
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class KarmaTracking(models.Model):
_name = 'gamification.karma.tracking'
_description = 'Track Karma Changes'
_rec_name = 'user_id'
_order = 'tracking_date DESC'
user_id = fields.Many2one('res.users', 'User', index=True, readonly=True, required=True, ondelete='cascade')
old_value = fields.Integer('Old Karma Value', required=True, readonly=True)
new_value = fields.Integer('New Karma Value', required=True, readonly=True)
consolidated = fields.Boolean('Consolidated')
tracking_date = fields.Date(default=fields.Date.context_today)
@api.model
def _consolidate_last_month(self):
""" Consolidate last month. Used by a cron to cleanup tracking records. """
previous_month_start = fields.Date.today() + relativedelta(months=-1, day=1)
return self._process_consolidate(previous_month_start)
def _process_consolidate(self, from_date):
""" Consolidate trackings into a single record for a given month, starting
at a from_date (included). End date is set to last day of current month
using a smart calendar.monthrange construction. """
end_date = from_date + relativedelta(day=calendar.monthrange(from_date.year, from_date.month)[1])
select_query = """
SELECT user_id,
(
SELECT old_value from gamification_karma_tracking old_tracking
WHERE old_tracking.user_id = gamification_karma_tracking.user_id
AND tracking_date::timestamp BETWEEN %(from_date)s AND %(to_date)s
AND consolidated IS NOT TRUE
ORDER BY tracking_date ASC LIMIT 1
), (
SELECT new_value from gamification_karma_tracking new_tracking
WHERE new_tracking.user_id = gamification_karma_tracking.user_id
AND tracking_date::timestamp BETWEEN %(from_date)s AND %(to_date)s
AND consolidated IS NOT TRUE
ORDER BY tracking_date DESC LIMIT 1
)
FROM gamification_karma_tracking
WHERE tracking_date::timestamp BETWEEN %(from_date)s AND %(to_date)s
AND consolidated IS NOT TRUE
GROUP BY user_id """
self.env.cr.execute(select_query, {
'from_date': from_date,
'to_date': end_date,
})
results = self.env.cr.dictfetchall()
if results:
for result in results:
result['consolidated'] = True
result['tracking_date'] = fields.Date.to_string(from_date)
self.create(results)
self.search([
('tracking_date', '>=', from_date),
('tracking_date', '<=', end_date),
('consolidated', '!=', True)]
).unlink()
return True
+122 -7
View File
@@ -3,16 +3,12 @@
from odoo import api, fields, models
import logging
_logger = logging.getLogger(__name__)
class Users(models.Model):
_inherit = 'res.users'
karma = fields.Integer('Karma', default=0)
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")
@@ -46,11 +42,28 @@ class Users(models.Model):
@api.model_create_multi
def create(self, values_list):
res = super(Users, self).create(values_list)
karma_trackings = []
for user in res:
if user.karma:
karma_trackings.append({'user_id': user.id, 'old_value': 0, 'new_value': user.karma})
if karma_trackings:
self.env['gamification.karma.tracking'].sudo().create(karma_trackings)
res._recompute_rank()
return res
def write(self, vals):
karma_trackings = []
if 'karma' in vals:
for user in self:
if user.karma != vals['karma']:
karma_trackings.append({'user_id': user.id, 'old_value': user.karma, 'new_value': vals['karma']})
result = super(Users, self).write(vals)
if karma_trackings:
self.env['gamification.karma.tracking'].sudo().create(karma_trackings)
if 'karma' in vals:
self._recompute_rank()
return result
@@ -60,6 +73,109 @@ class Users(models.Model):
user.karma += karma
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::timestamp >= timestamp %s'
params.append(from_date)
if to_date:
date_to_condition = 'AND tracking.tracking_date::timestamp <= timestamp %s'
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
@@ -68,7 +184,7 @@ class Users(models.Model):
if template:
for u in self:
if u.rank_id.karma_min > 0:
template.send_mail(u.id, force_send=len(self) == 1, notif_layout='mail.mail_notification_light')
template.send_mail(u.id, force_send=False, notif_layout='mail.mail_notification_light')
def _recompute_rank(self):
"""
@@ -161,7 +277,6 @@ class Users(models.Model):
'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
@@ -27,4 +27,6 @@ badge_user_portal,"Badge-user Portal",gamification.model_gamification_badge_user
badge_user_public,"Badge-user Public",gamification.model_gamification_badge_user,base.group_public,1,0,0,0
gamification_karma_rank_access_all,gamification.karma.rank.access.all,gamification.model_gamification_karma_rank,,1,0,0,0
gamification_karma_rank_access_user_manager,gamification.karma.rank.access.user.manager,gamification.model_gamification_karma_rank,base.group_erp_manager,1,1,1,1
gamification_karma_rank_access_user_manager,gamification.karma.rank.access.user.manager,gamification.model_gamification_karma_rank,base.group_system,1,1,1,1
gamification_karma_tracking_access_all,gamification.karma.tracking.access.all,gamification.model_gamification_karma_tracking,,0,0,0,0
gamification_karma_tracking_access_user_manager,gamification.karma.tracking.access.user.manager,gamification.model_gamification_karma_tracking,base.group_system,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
27
28
29
30
31
32
+1 -1
View File
@@ -2,4 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_challenge
from . import test_recompute_rank
from . import test_karma_tracking
@@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
from odoo import exceptions, fields
from odoo.tests import common
class TestKarmaTrackingCommon(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestKarmaTrackingCommon, cls).setUpClass()
cls.test_user = cls.env['res.users'].with_context(no_reset_password=True, mail_create_nosubscribe=True).create({
'name': 'Test User',
'login': 'test',
'email': 'test@example.com',
'karma': 0,
'groups_id': [(4, cls.env.ref('base.group_user').id)]
})
cls.test_user_2 = cls.env['res.users'].with_context(no_reset_password=True, mail_create_nosubscribe=True).create({
'name': 'Test User 2',
'login': 'test2',
'email': 'test2@example.com',
'karma': 0,
'groups_id': [(4, cls.env.ref('base.group_user').id)]
})
cls.env['gamification.karma.tracking'].search([]).unlink()
cls.test_date = fields.Date.today() + relativedelta(month=4, day=1)
@classmethod
def _create_trackings(cls, user, karma, steps, date, days_delta=1):
old_value = user.karma
for step in range(steps):
new_value = old_value + karma
cls.env['gamification.karma.tracking'].create([{
'user_id': user.id,
'old_value': old_value,
'new_value': new_value,
'consolidated': False,
'tracking_date': fields.Date.to_string(date)
}])
old_value = new_value
date = date + relativedelta(days=days_delta)
def test_computation_gain(self):
self._create_trackings(self.test_user, 20, 2, self.test_date, days_delta=30)
self._create_trackings(self.test_user_2, 10, 20, self.test_date, days_delta=2)
results = (self.test_user | self.test_user_2)._get_tracking_karma_gain_position([])
self.assertEqual(results[0]['user_id'], self.test_user_2.id)
self.assertEqual(results[0]['karma_gain_total'], 200)
self.assertEqual(results[0]['karma_position'], 1)
self.assertEqual(results[1]['user_id'], self.test_user.id)
self.assertEqual(results[1]['karma_gain_total'], 40)
self.assertEqual(results[1]['karma_position'], 2)
results = (self.test_user | self.test_user_2)._get_tracking_karma_gain_position([], to_date=self.test_date + relativedelta(day=2))
self.assertEqual(results[0]['user_id'], self.test_user.id)
self.assertEqual(results[0]['karma_gain_total'], 20)
self.assertEqual(results[0]['karma_position'], 1)
self.assertEqual(results[1]['user_id'], self.test_user_2.id)
self.assertEqual(results[1]['karma_gain_total'], 10)
self.assertEqual(results[1]['karma_position'], 2)
results = (self.test_user | self.test_user_2)._get_tracking_karma_gain_position([], from_date=self.test_date + relativedelta(months=1, day=1))
self.assertEqual(results[0]['user_id'], self.test_user_2.id)
self.assertEqual(results[0]['karma_gain_total'], 50)
self.assertEqual(results[0]['karma_position'], 1)
self.assertEqual(results[1]['user_id'], self.test_user.id)
self.assertEqual(results[1]['karma_gain_total'], 20)
self.assertEqual(results[1]['karma_position'], 2)
results = self.env['res.users']._get_tracking_karma_gain_position([])
self.assertEqual(len(results), 0)
def test_consolidation_cron(self):
self.patcher = patch('odoo.addons.gamification.models.gamification_karma_tracking.fields.Date', wraps=fields.Date)
self.mock_datetime = self.patcher.start()
self.mock_datetime.today.return_value = date(self.test_date.year, self.test_date.month + 1, self.test_date.day)
self._create_trackings(self.test_user, 20, 2, self.test_date, days_delta=30)
self._create_trackings(self.test_user_2, 10, 20, self.test_date, days_delta=2)
self.env['gamification.karma.tracking']._consolidate_last_month()
consolidated = self.env['gamification.karma.tracking'].search([
('user_id', 'in', (self.test_user | self.test_user_2).ids),
('consolidated', '=', True),
('tracking_date', '=', self.test_date)
])
self.assertEqual(len(consolidated), 2)
unconsolidated = self.env['gamification.karma.tracking'].search([
('user_id', 'in', (self.test_user | self.test_user_2).ids),
('consolidated', '=', False),
])
self.assertEqual(len(unconsolidated), 6) # 5 for test user 2, 1 for test user
self.patcher.stop()
def test_consolidation_monthly(self):
Tracking = self.env['gamification.karma.tracking']
base_test_user_karma = self.test_user.karma
base_test_user_2_karma = self.test_user_2.karma
self._create_trackings(self.test_user, 20, 2, self.test_date, days_delta=30)
self._create_trackings(self.test_user_2, 10, 20, self.test_date, days_delta=2)
Tracking._process_consolidate(self.test_date)
consolidated = Tracking.search([
('user_id', '=', self.test_user_2.id),
('consolidated', '=', True),
('tracking_date', '=', self.test_date)
])
self.assertEqual(len(consolidated), 1)
self.assertEqual(consolidated.old_value, base_test_user_2_karma) # 15 2-days span, from 1 to 29 included = 15 steps -> 150 karma
self.assertEqual(consolidated.new_value, base_test_user_2_karma + 150) # 15 2-days span, from 1 to 29 included = 15 steps -> 150 karma
remaining = Tracking.search([
('user_id', '=', self.test_user_2.id),
('consolidated', '=', False)
])
self.assertEqual(len(remaining), 5) # 15 steps consolidated, remaining 5
self.assertEqual(remaining[0].tracking_date, self.test_date + relativedelta(months=1, day=9)) # ordering: last first
self.assertEqual(remaining[-1].tracking_date, self.test_date + relativedelta(months=1, day=1))
Tracking._process_consolidate(self.test_date + relativedelta(months=1))
consolidated = Tracking.search([
('user_id', '=', self.test_user_2.id),
('consolidated', '=', True),
])
self.assertEqual(len(consolidated), 2)
self.assertEqual(consolidated[0].new_value, base_test_user_2_karma + 200) # 5 remaining 2-days span, from 1 to 9 included = 5 steps -> 50 karma
self.assertEqual(consolidated[0].old_value, base_test_user_2_karma + 150) # coming from previous iteration
self.assertEqual(consolidated[0].tracking_date, self.test_date + relativedelta(months=1)) # tracking set at beginning of month
self.assertEqual(consolidated[-1].new_value, base_test_user_2_karma + 150) # previously created one still present
self.assertEqual(consolidated[-1].old_value, base_test_user_2_karma) # previously created one still present
remaining = Tracking.search([
('user_id', '=', self.test_user_2.id),
('consolidated', '=', False)
])
self.assertFalse(remaining)
# current user not-in-details tests
current_user_trackings = Tracking.search([
('user_id', '=', self.test_user.id),
])
self.assertEqual(len(current_user_trackings), 2)
self.assertEqual(current_user_trackings[0].new_value, base_test_user_karma + 40)
self.assertEqual(current_user_trackings[-1].old_value, base_test_user_karma)
def test_user_as_erp_manager(self):
self.test_user.write({'groups_id': [
(4, self.env.ref('base.group_partner_manager').id),
(4, self.env.ref('base.group_erp_manager').id)
]})
user = self.env['res.users'].with_user(self.test_user).create({
'name': 'Test Ostérone', 'karma': '32',
'login': 'dummy', 'email': 'dummy@example.com',
})
with self.assertRaises(exceptions.AccessError):
user.read(['karma_tracking_ids'])
user.write({'karma': 60})
user.add_karma(10)
self.assertEqual(user.karma, 70)
trackings = self.env['gamification.karma.tracking'].sudo().search([('user_id', '=', user.id)])
self.assertEqual(len(trackings), 3) # create + write + add_karma
def test_user_tracking(self):
self.test_user.write({'groups_id': [
(4, self.env.ref('base.group_partner_manager').id),
(4, self.env.ref('base.group_system').id)
]})
user = self.env['res.users'].with_user(self.test_user).create({
'name': 'Test Ostérone', 'karma': '32',
'login': 'dummy', 'email': 'dummy@example.com',
})
self.assertEqual(user.karma, 32)
self.assertEqual(len(user.karma_tracking_ids), 1)
self.assertEqual(user.karma_tracking_ids.old_value, 0)
self.assertEqual(user.karma_tracking_ids.new_value, 32)
user.write({'karma': 60})
user.add_karma(10)
self.assertEqual(user.karma, 70)
self.assertEqual(len(user.karma_tracking_ids), 3)
self.assertEqual(user.karma_tracking_ids[2].old_value, 60)
self.assertEqual(user.karma_tracking_ids[2].new_value, 70)
self.assertEqual(user.karma_tracking_ids[1].old_value, 32)
self.assertEqual(user.karma_tracking_ids[1].new_value, 60)
self.assertEqual(user.karma_tracking_ids[0].old_value, 0)
self.assertEqual(user.karma_tracking_ids[0].new_value, 32)
class TestComputeRankCommon(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestComputeRankCommon, cls).setUpClass()
def _patched_send_mail(*args, **kwargs):
pass
patch_email = patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail)
patch_email.start()
cls.users = cls.env['res.users']
for k in range(-5, 1030, 30):
cls.users += cls.env['res.users'].with_context(no_reset_password=True, mail_create_nosubscribe=True).create({
'name': str(k),
'login': "test_recompute_rank_%s" % k,
'karma': k,
})
cls.env['gamification.karma.rank'].search([]).unlink()
cls.rank_1 = cls.env['gamification.karma.rank'].create({
'name': 'rank 1',
'karma_min': 0,
})
cls.rank_2 = cls.env['gamification.karma.rank'].create({
'name': 'rank 2',
'karma_min': 250,
})
cls.rank_3 = cls.env['gamification.karma.rank'].create({
'name': 'rank 3',
'karma_min': 500,
})
cls.rank_4 = cls.env['gamification.karma.rank'].create({
'name': 'rank 4',
'karma_min': 1000,
})
patch_email.stop()
def test_00_initial_compute(self):
self.assertEqual(len(self.users), 35)
self.assertEqual(
len(self.rank_1.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_1.karma_min and u.karma < self.rank_2.karma_min])
)
self.assertEqual(
len(self.rank_2.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_2.karma_min and u.karma < self.rank_3.karma_min])
)
self.assertEqual(
len(self.rank_3.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_3.karma_min and u.karma < self.rank_4.karma_min])
)
self.assertEqual(
len(self.rank_4.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_4.karma_min])
)
def test_01_switch_rank(self):
self.assertEqual(len(self.users), 35)
self.rank_3.karma_min = 100
# rank_1 -> rank_3 -> rank_2 -> rank_4
self.assertEqual(
len(self.rank_1.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_1.karma_min and u.karma < self.rank_3.karma_min])
)
self.assertEqual(
len(self.rank_3.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_3.karma_min and u.karma < self.rank_2.karma_min])
)
self.assertEqual(
len(self.rank_2.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_2.karma_min and u.karma < self.rank_4.karma_min])
)
self.assertEqual(
len(self.rank_4.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_4.karma_min])
)
def test_02_update_rank_without_switch(self):
number_of_users = False
def _patched_recompute_rank(_self, *args, **kwargs):
nonlocal number_of_users
number_of_users = len(_self & self.users)
patch_bulk = patch('odoo.addons.gamification.models.res_users.Users._recompute_rank', _patched_recompute_rank)
patch_bulk.start()
self.rank_3.karma_min = 700
self.assertEqual(number_of_users, 7, "Should just recompute for the 7 users between 500 and 700")
patch_bulk.stop()
def test_03_test_bulk_call(self):
self.assertEqual(len(self.users), 35)
def _patched_check_in_bulk(*args, **kwargs):
raise
patch_bulk = patch('odoo.addons.gamification.models.res_users.Users._recompute_rank_bulk', _patched_check_in_bulk)
patch_bulk.start()
# call on 5 users should not trigger the bulk function
self.users[0:5]._recompute_rank()
# call on 50 users should trigger the bulk function
with self.assertRaises(Exception):
self.users[0:50]._recompute_rank()
patch_bulk.stop()
@@ -1,128 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from unittest.mock import patch
class TestComputeRankCommon(common.TransactionCase):
def setUp(self):
super(TestComputeRankCommon, self).setUp()
def _patched_send_mail(*args, **kwargs):
pass
self.User = self.env['res.users'].with_context(no_reset_password=True, mail_create_nosubscribe=True)
self.users = self.User
patch_email = patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail)
patch_email.start()
for k in range(-5, 1030, 30):
self.users += self.User.create({
'name': str(k),
'login': "test_recompute_rank_%s" % k,
'karma': k,
})
self.env['gamification.karma.rank'].search([]).unlink()
self.rank_1 = self.env['gamification.karma.rank'].create({
'name': 'rank 1',
'karma_min': 0,
})
self.rank_2 = self.env['gamification.karma.rank'].create({
'name': 'rank 2',
'karma_min': 250,
})
self.rank_3 = self.env['gamification.karma.rank'].create({
'name': 'rank 3',
'karma_min': 500,
})
self.rank_4 = self.env['gamification.karma.rank'].create({
'name': 'rank 4',
'karma_min': 1000,
})
patch_email.stop()
class test_recompute(TestComputeRankCommon):
def test_00_initial_compute(self):
self.assertEqual(len(self.users), 35)
self.assertEqual(
len(self.rank_1.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_1.karma_min and u.karma < self.rank_2.karma_min])
)
self.assertEqual(
len(self.rank_2.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_2.karma_min and u.karma < self.rank_3.karma_min])
)
self.assertEqual(
len(self.rank_3.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_3.karma_min and u.karma < self.rank_4.karma_min])
)
self.assertEqual(
len(self.rank_4.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_4.karma_min])
)
def test_01_switch_rank(self):
self.assertEqual(len(self.users), 35)
self.rank_3.karma_min = 100
# rank_1 -> rank_3 -> rank_2 -> rank_4
self.assertEqual(
len(self.rank_1.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_1.karma_min and u.karma < self.rank_3.karma_min])
)
self.assertEqual(
len(self.rank_3.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_3.karma_min and u.karma < self.rank_2.karma_min])
)
self.assertEqual(
len(self.rank_2.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_2.karma_min and u.karma < self.rank_4.karma_min])
)
self.assertEqual(
len(self.rank_4.user_ids & self.users),
len([u for u in self.users if u.karma >= self.rank_4.karma_min])
)
def test_02_update_rank_without_switch(self):
number_of_users = False
def _patched_recompute_rank(_self, *args, **kwargs):
nonlocal number_of_users
number_of_users = len(_self & self.users)
patch_bulk = patch('odoo.addons.gamification.models.res_users.Users._recompute_rank', _patched_recompute_rank)
patch_bulk.start()
self.rank_3.karma_min = 700
self.assertEqual(number_of_users, 7, "Should just recompute for the 7 users between 500 and 700")
patch_bulk.stop()
def test_03_test_bulk_call(self):
self.assertEqual(len(self.users), 35)
def _patched_check_in_bulk(*args, **kwargs):
raise
patch_bulk = patch('odoo.addons.gamification.models.res_users.Users._recompute_rank_bulk', _patched_check_in_bulk)
patch_bulk.start()
# call on 5 users should not trigger the bulk function
self.users[0:5]._recompute_rank()
# call on 50 users should trigger the bulk function
with self.assertRaises(Exception):
self.users[0:50]._recompute_rank()
patch_bulk.stop()
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="gamification_karma_tracking_view_search" model="ir.ui.view">
<field name="name">gamification.karma.tracking.view.search</field>
<field name="model">gamification.karma.tracking</field>
<field name="arch" type="xml">
<search string="Search Trackings">
<field name="user_id"/>
<field name="tracking_date"/>
<filter string="Consolidated" name="filter_consolidated"
domain="[('consolidated', '=', True)]"/>
<group string="Group By" expand="1">
<filter string="User" name="group_by_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record id="gamification_karma_tracking_view_tree" model="ir.ui.view">
<field name="name">gamification.karma.tracking.view.tree</field>
<field name="model">gamification.karma.tracking</field>
<field name="arch" type="xml">
<tree string="Trackings">
<field name="user_id"/>
<field name="tracking_date"/>
<field name="old_value"/>
<field name="new_value"/>
<field name="consolidated"/>
</tree>
</field>
</record>
<record id="gamification_karma_tracking_view_form" model="ir.ui.view">
<field name="name">gamification.karma.tracking.view.form</field>
<field name="model">gamification.karma.tracking</field>
<field name="arch" type="xml">
<form string="Tracking">
<sheet>
<group>
<field name="user_id"/>
<field name="tracking_date"/>
<field name="old_value"/>
<field name="new_value"/>
<field name="consolidated"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="gamification_karma_tracking_action" model="ir.actions.act_window">
<field name="name">Trackings</field>
<field name="res_model">gamification.karma.tracking</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="gamification_karma_tracking_menu"
parent="gamification_menu"
action="gamification_karma_tracking_action"
sequence="50"/>
</data>
</odoo>
+41 -36
View File
@@ -8,7 +8,10 @@ import werkzeug.urls
import werkzeug.wrappers
import math
from odoo import http, modules, tools
from dateutil.relativedelta import relativedelta
from operator import itemgetter
from odoo import fields, http, modules, tools
from odoo.http import request
from odoo.osv import expression
@@ -205,17 +208,21 @@ class WebsiteProfile(http.Controller):
@http.route(['/profile/users',
'/profile/users/page/<int:page>'], type='http', auth="public", website=True, sitemap=True)
def view_all_users_page(self, page=1, **searches):
def view_all_users_page(self, page=1, **kwargs):
User = request.env['res.users']
dom = [('karma', '>', 1), ('website_published', '=', True)]
# Searches
search_term = searches.get('search')
search_term = kwargs.get('search')
group_by = kwargs.get('group_by', False)
render_values = {
'search': search_term,
'group_by': group_by or 'all',
}
if search_term:
dom = expression.AND([['|', ('name', 'ilike', search_term), ('company_id.name', 'ilike', search_term)], dom])
user_count = User.sudo().search_count(dom)
if user_count:
page_count = math.ceil(user_count / self._users_per_page)
pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=self._users_per_page,
@@ -226,43 +233,41 @@ class WebsiteProfile(http.Controller):
# Get karma position for users (only website_published)
position_domain = [('karma', '>', 1), ('website_published', '=', True)]
position_map = self._get_users_karma_position(position_domain, users.ids)
if group_by:
position_map = self._get_user_tracking_karma_gain_position(position_domain, users.ids, group_by)
else:
position_results = request.env['res.users'].browse(users.ids)._get_karma_position(position_domain)
position_map = dict((user_data['user_id'], dict(user_data)) for user_data in position_results)
max_position = max([user_data['karma_position'] for user_data in position_map.values()], default=1)
for user in user_values:
user['position'] = position_map.get(user['id'], 0)
user_data = position_map.get(user['id'], dict())
user['position'] = user_data.get('karma_position', max_position + 1)
user['karma_gain'] = user_data.get('karma_gain_total', 0)
user_values.sort(key=itemgetter('position'))
values = {
'top3_users': user_values[:3] if not search_term and page == 1 else None,
'users': user_values[3:] if not search_term and page == 1 else user_values,
'pager': pager
}
else:
values = {'top3_users': [], 'users': [], 'search': search_term, 'pager': dict(page_count=0)}
user_values = []
pager = {'page_count': 0}
return request.render("website_profile.users_page_main", values)
render_values.update({
'top3_users': user_values[:3] if not search_term and page == 1 else [],
'users': user_values,
'pager': pager})
return request.render("website_profile.users_page_main", render_values)
def _get_users_karma_position(self, domain, user_ids):
if not user_ids:
return {}
Users = request.env['res.users']
where_query = Users._where_calc(domain)
from_clause, 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.id, sub.karma_position
FROM (
SELECT "res_users"."id", row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position
FROM {from_clause}
WHERE {where_clause}
) sub
WHERE sub.id IN %s
""".format(from_clause=from_clause, where_clause=where_clause)
request.env.cr.execute(query, where_clause_params + [tuple(user_ids)])
return {item['id']: item['karma_position'] for item in request.env.cr.dictfetchall()}
def _get_user_tracking_karma_gain_position(self, domain, user_ids, group_by):
""" Helper method computing boundaries to give to _get_tracking_karma_gain_position.
See that method for more details. """
to_date = fields.Date.today()
if group_by == 'week':
from_date = to_date - relativedelta(weeks=1)
elif group_by == 'month':
from_date = to_date - relativedelta(months=1)
else:
from_date = None
results = request.env['res.users'].browse(user_ids)._get_tracking_karma_gain_position(domain, from_date=from_date, to_date=to_date)
return dict((item['user_id'], dict(item)) for item in results)
# User and validation
# --------------------------------------------------
@@ -481,7 +481,18 @@
<div class="o_wprofile_all_users_header o_wprofile_gradient mb-n5 pb-5">
<t t-call="website_profile.user_profile_sub_nav"/>
<div class="container">
<h1 class="py-4 text-white">All Users</h1>
<h1 class="py-4 text-white d-inline-block">All Users</h1>
<div class="form-inline py-4 float-right">
<strong class="form-group text-white mr-2">Rank by :</strong>
<div class="form-group btn-group">
<a t-attf-class="btn btn-secondary #{ 'active' if group_by == 'week' else ''}"
t-att-href="'/profile/users?' + keep_query('search', group_by='week')">This week</a>
<a t-attf-class="btn btn-secondary #{ 'active' if group_by == 'month' else ''}"
t-att-href="'/profile/users?' + keep_query('search', group_by='month')">This month</a>
<a t-attf-class="btn btn-secondary #{ 'active' if group_by == 'all' else ''}"
t-att-href="'/profile/users?' + keep_query('search')">All time</a>
</div>
</div>
</div>
</div>
</template>
@@ -489,7 +500,7 @@
<template id="users_page_content">
<div class="container mb32">
<div class="row mb-3">
<div class="col-md-4" t-foreach="top3_users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';">
<div class="col-md-4 d-flex flex-grow-1" t-foreach="top3_users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';">
<t t-call="website_profile.top3_user_card"></t>
</div>
</div>
@@ -498,7 +509,7 @@
<t t-call="website_profile.all_user_card"/>
</tr>
</table>
<t t-if='search'>
<t t-if='search and not users'>
<div class='alert alert-warning'>No user found for <strong><t t-esc="search"/></strong>. Try another search.</div>
</t>
<div class="form-inline justify-content-center">
@@ -508,7 +519,7 @@
</template>
<template id="top3_user_card" name="Top 3 User Card">
<div class="card text-center mb-2 border-bottom-0 o_wprofile_pointer">
<div class="card w-100 text-center mb-2 border-bottom-0 o_wprofile_pointer">
<div class="card-body">
<div class="d-inline-block position-relative">
<img class="rounded-circle img-fluid"
@@ -519,6 +530,9 @@
<h3 class="mt-2 mb-0" t-esc="user['name']"></h3>
<span class="badge badge-danger font-weight-normal px-2" t-if="not user['website_published']">Unpublished</span>
<strong class="text-muted" t-esc="user['rank']"/>
<div class="h3 my-2" t-if="user['karma_gain']">
<span class="badge badge-pill badge-success px-3 py-2" >+ <t t-esc="user['karma_gain']"/> XP</span>
</div>
</div>
<div class="row mx-0 o_wprofile_top3_card_footer text-nowrap">
<div class="col py-3"><b t-esc="user['karma']"/> <span class="text-muted">XP</span></div>
@@ -537,6 +551,17 @@
<td class="align-middle w-md-75">
<span class="font-weight-bold" t-esc="user['name']"/><br/>
<span class="text-muted font-weight-bold" t-esc="user['rank']"></span>
</td>
<td class="align-middle text-nowrap">
<t t-if="user['karma_gain']">
<span class="badge badge-pill badge-success d-inline">+ <t t-esc="user['karma_gain']"/> XP</span>
<span class="text-muted pl-2 pr-3">
<t t-if="group_by == 'week'">Weekly </t>
<t t-elif="group_by == 'month'">Monthly </t>
<t t-else="">All time </t>
gain
</span>
</t>
</td>
<td t-if="not user['website_published']" class="align-middle font-weight-bold text-right text-nowrap">
<span class="badge badge-danger font-weight-normal px-2 py-1 m-1">Unpublished</span>