summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Hoste <pko@odoo.com>2019-06-27 07:54:04 +0000
committerThibault Delavallée <tde@odoo.com>2019-11-05 15:08:34 +0000
commitbfbc7c6a184b2b507b1bf4ad778430bc2f3a0e7f (patch)
tree76cc91bd7b3b2011c7930e084ed77cc12dcac980
parentc27ba2af1c640d91c6d7605412228687cc99cbd0 (diff)
[IMP] gamification: track karma change on users
PURPOSE Allow karma gain tracking enabling notably display of top users based on weekly / monthly gain in website profile. SPECIFCIATIONS 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). LINKS Task ID 2003505 PR #34594
-rw-r--r--addons/gamification/__manifest__.py4
-rw-r--r--addons/gamification/data/cron.xml13
-rw-r--r--addons/gamification/data/gamification_karma_rank_demo.xml9
-rw-r--r--addons/gamification/data/gamification_karma_tracking_demo.xml83
-rw-r--r--addons/gamification/models/__init__.py1
-rw-r--r--addons/gamification/models/gamification_karma_tracking.py69
-rw-r--r--addons/gamification/models/res_users.py88
-rw-r--r--addons/gamification/security/ir.model.access.csv4
-rw-r--r--addons/gamification/tests/__init__.py1
-rw-r--r--addons/gamification/tests/test_karma_tracking.py195
-rw-r--r--addons/gamification/views/gamification_karma_tracking_views.xml63
-rw-r--r--addons/website_profile/controllers/main.py46
-rw-r--r--addons/website_profile/views/website_profile.xml24
13 files changed, 572 insertions, 28 deletions
diff --git a/addons/gamification/__manifest__.py b/addons/gamification/__manifest__.py
index 0270a25a56b..314dcf070a2 100644
--- a/addons/gamification/__manifest__.py
+++ b/addons/gamification/__manifest__.py
@@ -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',
],
}
diff --git a/addons/gamification/data/cron.xml b/addons/gamification/data/cron.xml
index 0ed59d39e11..4e59205647b 100644
--- a/addons/gamification/data/cron.xml
+++ b/addons/gamification/data/cron.xml
@@ -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>
diff --git a/addons/gamification/data/gamification_karma_rank_demo.xml b/addons/gamification/data/gamification_karma_rank_demo.xml
index 839e8750047..848e7fd68d0 100644
--- a/addons/gamification/data/gamification_karma_rank_demo.xml
+++ b/addons/gamification/data/gamification_karma_rank_demo.xml
@@ -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>
diff --git a/addons/gamification/data/gamification_karma_tracking_demo.xml b/addons/gamification/data/gamification_karma_tracking_demo.xml
new file mode 100644
index 00000000000..01f4d8ac929
--- /dev/null
+++ b/addons/gamification/data/gamification_karma_tracking_demo.xml
@@ -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>
diff --git a/addons/gamification/models/__init__.py b/addons/gamification/models/__init__.py
index 926a5af2a07..659fcff70a1 100644
--- a/addons/gamification/models/__init__.py
+++ b/addons/gamification/models/__init__.py
@@ -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
diff --git a/addons/gamification/models/gamification_karma_tracking.py b/addons/gamification/models/gamification_karma_tracking.py
new file mode 100644
index 00000000000..f7b184ad24d
--- /dev/null
+++ b/addons/gamification/models/gamification_karma_tracking.py
@@ -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
diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py
index 3e8b6d8e1cd..3dc95de74a1 100644
--- a/addons/gamification/models/res_users.py
+++ b/addons/gamification/models/res_users.py
@@ -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,68 @@ 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 _rank_changed(self):
"""
Method that can be called on a batch of users with the same new rank
@@ -68,7 +143,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 +236,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
diff --git a/addons/gamification/security/ir.model.access.csv b/addons/gamification/security/ir.model.access.csv
index 41f36a87a5f..728360ab386 100644
--- a/addons/gamification/security/ir.model.access.csv
+++ b/addons/gamification/security/ir.model.access.csv
@@ -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
diff --git a/addons/gamification/tests/__init__.py b/addons/gamification/tests/__init__.py
index a6a599bf6c8..963887eb670 100644
--- a/addons/gamification/tests/__init__.py
+++ b/addons/gamification/tests/__init__.py
@@ -3,3 +3,4 @@
from . import test_challenge
from . import test_recompute_rank
+from . import test_karma_tracking
diff --git a/addons/gamification/tests/test_karma_tracking.py b/addons/gamification/tests/test_karma_tracking.py
new file mode 100644
index 00000000000..145b745b436
--- /dev/null
+++ b/addons/gamification/tests/test_karma_tracking.py
@@ -0,0 +1,195 @@
+# -*- 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)
diff --git a/addons/gamification/views/gamification_karma_tracking_views.xml b/addons/gamification/views/gamification_karma_tracking_views.xml
new file mode 100644
index 00000000000..998d0ff91ec
--- /dev/null
+++ b/addons/gamification/views/gamification_karma_tracking_views.xml
@@ -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>
diff --git a/addons/website_profile/controllers/main.py b/addons/website_profile/controllers/main.py
index eef5d1d4dc8..703fc250f96 100644
--- a/addons/website_profile/controllers/main.py
+++ b/addons/website_profile/controllers/main.py
@@ -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,12 +208,13 @@ 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)
if search_term:
dom = expression.AND([['|', ('name', 'ilike', search_term), ('company_id.name', 'ilike', search_term)], dom])
@@ -225,12 +229,24 @@ class WebsiteProfile(http.Controller):
user_values = self._prepare_all_users_values(users)
# 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)
- for user in user_values:
- user['position'] = position_map.get(user['id'], 0)
+ if group_by:
+ position_domain = [('karma', '>', 1), ('website_published', '=', True)]
+ position_map = self._get_user_tracking_karma_gain_position(position_domain, users.ids, group_by)
+ max_position = max([user_data['karma_position'] for user_data in position_map.values()], default=1)
+ for user in user_values:
+ 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'))
+ else:
+ position_domain = [('karma', '>', 1), ('website_published', '=', True)]
+ position_map = self._get_users_karma_position(position_domain, users.ids)
+ for user in user_values:
+ user['position'] = position_map.get(user['id'], 0)
+ user['karma_gain'] = False
values = {
+ 'group_by': group_by or 'all',
'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
@@ -244,8 +260,7 @@ class WebsiteProfile(http.Controller):
if not user_ids:
return {}
- Users = request.env['res.users']
- where_query = Users._where_calc(domain)
+ where_query = request.env['res.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)
@@ -264,6 +279,19 @@ class WebsiteProfile(http.Controller):
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
# --------------------------------------------------
diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml
index 629119b98d0..9a8dacb8e2a 100644
--- a/addons/website_profile/views/website_profile.xml
+++ b/addons/website_profile/views/website_profile.xml
@@ -481,7 +481,15 @@
<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">
+ <button type="button" t-attf-class="btn btn-secondary #{ 'active' if group_by == 'week' else ''}" onclick="location.href='/profile/users?group_by=week'">This week</button>
+ <button type="button" t-attf-class="btn btn-secondary #{ 'active' if group_by == 'month' else ''}" onclick="location.href='/profile/users?group_by=month'">This month</button>
+ <button type="button" t-attf-class="btn btn-secondary #{ 'active' if group_by == 'all' else ''}" onclick="location.href='/profile/users'">All time</button>
+ </div>
+ </div>
</div>
</div>
</template>
@@ -519,6 +527,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>
@@ -538,6 +549,17 @@
<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>
</td>