[IMP] gamification, website_forum, website_slides: karma tracking improvement
Purpose ======= Some users found a way to farm karma points, so we want to be able to track the source of the karma gain / loss. Specifications ============== Now, we added a reference field `origin_ref` which store the record responsible for the karma gain / loss (e.g. a slide we just completed). In addition to this origin, we also have a new field to store the reason (e.g. "Slide completed") so we know exactly what happened and how the user gains his karma. Before, the `old_value` of the karma tracking has to be set manually, but now it's done automatically based on the value of the previous tracking of the same user. That way, it will simplify other part of the code. Add the karma reason in the website modules. Adapt those modules due to the changes in gamification. Task-2234179 closes odoo/odoo#76430 Related: odoo/enterprise#23702 Related: odoo/upgrade#3299 Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com> Co-authored-by: std-odoo <std@odoo.com>
This commit is contained in:
committed by
std-odoo
co-authored by
std-odoo
parent
f76e65118e
commit
0ffa2d64ba
@@ -1,11 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<!--Karma-->
|
||||
<record id="base.user_root" model="res.users">
|
||||
<field name="karma">2500</field>
|
||||
<record id="karma_tracking_user_root" model="gamification.karma.tracking" forcecreate="0">
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="new_value">2500</field>
|
||||
<field name="reason">I am the Root!</field>
|
||||
</record>
|
||||
<record id="base.user_admin" model="res.users">
|
||||
<field name="karma">2500</field>
|
||||
<record id="karma_tracking_user_admin" model="gamification.karma.tracking" forcecreate="0">
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="new_value">2500</field>
|
||||
<field name="reason">I am the Admin!</field>
|
||||
</record>
|
||||
|
||||
<!--Ranks-->
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
<field name="new_value">2500</field>
|
||||
<field name="tracking_date" eval="(DateTime.now()).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="karma">2500</field>
|
||||
</record>
|
||||
<function model="gamification.karma.tracking" name="unlink">
|
||||
<value model="gamification.karma.tracking" eval="obj().search([
|
||||
('user_id', '=', ref('base.user_demo')),
|
||||
@@ -74,9 +71,6 @@
|
||||
<field name="new_value">30</field>
|
||||
<field name="tracking_date" eval="DateTime.now()"/>
|
||||
</record>
|
||||
<record id="base.demo_user0" model="res.users">
|
||||
<field name="karma">30</field>
|
||||
</record>
|
||||
<function model="gamification.karma.tracking" name="unlink">
|
||||
<value model="gamification.karma.tracking" eval="obj().search([
|
||||
('user_id', '=', ref('base.demo_user0')),
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
<field eval="False" name="doall" />
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_consolidate_last_month" model="ir.cron">
|
||||
<record id="ir_cron_consolidate" 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="code">model._consolidate_cron()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">months</field>
|
||||
|
||||
@@ -1,69 +1,135 @@
|
||||
# -*- 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
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import date_utils
|
||||
|
||||
|
||||
class KarmaTracking(models.Model):
|
||||
_name = 'gamification.karma.tracking'
|
||||
_description = 'Track Karma Changes'
|
||||
_rec_name = 'user_id'
|
||||
_order = 'tracking_date DESC'
|
||||
_order = 'tracking_date desc, id 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)
|
||||
def _get_origin_selection_values(self):
|
||||
return [('res.users', _('User'))]
|
||||
|
||||
user_id = fields.Many2one('res.users', 'User', index=True, required=True, ondelete='cascade')
|
||||
old_value = fields.Integer('Old Karma Value', readonly=True)
|
||||
new_value = fields.Integer('New Karma Value', required=True)
|
||||
gain = fields.Integer('Gain', compute='_compute_gain', readonly=False)
|
||||
consolidated = fields.Boolean('Consolidated')
|
||||
tracking_date = fields.Date(default=fields.Date.context_today)
|
||||
|
||||
tracking_date = fields.Datetime(default=fields.Datetime.now, readonly=True, index=True)
|
||||
reason = fields.Text(default=lambda self: _('Add Manually'), string='Description')
|
||||
origin_ref = fields.Reference(
|
||||
string='Source',
|
||||
selection=lambda self: self._get_origin_selection_values(),
|
||||
default=lambda self: f'res.users,{self.env.user.id}',
|
||||
)
|
||||
origin_ref_model_name = fields.Selection(
|
||||
string='Source Type', selection=lambda self: self._get_origin_selection_values(),
|
||||
compute='_compute_origin_ref_model_name', store=True)
|
||||
|
||||
@api.depends('old_value', 'new_value')
|
||||
def _compute_gain(self):
|
||||
for karma in self:
|
||||
karma.gain = karma.new_value - (karma.old_value or 0)
|
||||
|
||||
@api.depends('origin_ref')
|
||||
def _compute_origin_ref_model_name(self):
|
||||
for karma in self:
|
||||
if not karma.origin_ref:
|
||||
karma.origin_ref_model_name = False
|
||||
continue
|
||||
|
||||
karma.origin_ref_model_name = karma.origin_ref._name
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
# fill missing old value with current user karma
|
||||
users = self.env['res.users'].browse([
|
||||
values['user_id']
|
||||
for values in values_list
|
||||
if 'old_value' not in values and values.get('user_id')
|
||||
])
|
||||
karma_per_users = {user.id: user.karma for user in users}
|
||||
|
||||
for values in values_list:
|
||||
if 'old_value' not in values and values.get('user_id'):
|
||||
values['old_value'] = karma_per_users[values['user_id']]
|
||||
|
||||
if 'gain' in values and 'old_value' in values:
|
||||
values['new_value'] = values['old_value'] + values['gain']
|
||||
del values['gain']
|
||||
|
||||
return super().create(values_list)
|
||||
|
||||
@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 _consolidate_cron(self):
|
||||
"""Consolidate the trackings 2 months ago. Used by a cron to cleanup tracking records."""
|
||||
from_date = date_utils.start_of(fields.Datetime.today(), 'month') - relativedelta(months=2)
|
||||
return self._process_consolidate(from_date)
|
||||
|
||||
def _process_consolidate(self, from_date, end_date=None):
|
||||
"""Consolidate the karma trackings.
|
||||
|
||||
The consolidation keeps, for each user, the oldest "old_value" and the most recent
|
||||
"new_value", creates a new karma tracking with those values and removes all karma
|
||||
trackings between those dates. The origin / reason is changed on the consolidated
|
||||
records, so this information is lost in the process.
|
||||
"""
|
||||
self.env['gamification.karma.tracking'].flush_model()
|
||||
|
||||
if not end_date:
|
||||
end_date = date_utils.end_of(date_utils.end_of(from_date, 'month'), 'day')
|
||||
|
||||
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 """
|
||||
WITH old_tracking AS (
|
||||
SELECT DISTINCT ON (user_id) user_id, old_value, tracking_date
|
||||
FROM gamification_karma_tracking
|
||||
WHERE tracking_date BETWEEN %(from_date)s
|
||||
AND %(end_date)s
|
||||
AND consolidated IS NOT TRUE
|
||||
ORDER BY user_id, tracking_date ASC, id ASC
|
||||
)
|
||||
INSERT INTO gamification_karma_tracking (
|
||||
user_id,
|
||||
old_value,
|
||||
new_value,
|
||||
tracking_date,
|
||||
origin_ref,
|
||||
consolidated,
|
||||
reason)
|
||||
SELECT DISTINCT ON (nt.user_id)
|
||||
nt.user_id,
|
||||
ot.old_value AS old_value,
|
||||
nt.new_value AS new_value,
|
||||
ot.tracking_date AS from_tracking_date,
|
||||
%(origin_ref)s AS origin_ref,
|
||||
TRUE,
|
||||
%(reason)s
|
||||
FROM gamification_karma_tracking AS nt
|
||||
JOIN old_tracking AS ot
|
||||
ON ot.user_id = nt.user_id
|
||||
WHERE nt.tracking_date BETWEEN %(from_date)s
|
||||
AND %(end_date)s
|
||||
AND nt.consolidated IS NOT TRUE
|
||||
ORDER BY nt.user_id, nt.tracking_date DESC, id DESC
|
||||
"""
|
||||
|
||||
self.env.cr.execute(select_query, {
|
||||
'from_date': from_date,
|
||||
'to_date': end_date,
|
||||
'end_date': end_date,
|
||||
'origin_ref': f'res.users,{self.env.user.id}',
|
||||
'reason': _('Consolidation from %s to %s', from_date.date(), end_date.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()
|
||||
self.search([
|
||||
('tracking_date', '>=', from_date),
|
||||
('tracking_date', '<=', end_date),
|
||||
('consolidated', '!=', True)]
|
||||
).with_context(skip_karma_computation=True).unlink()
|
||||
return True
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
karma = fields.Integer('Karma', default=0)
|
||||
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")
|
||||
@@ -16,6 +16,33 @@ class Users(models.Model):
|
||||
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
|
||||
@@ -43,34 +70,56 @@ class Users(models.Model):
|
||||
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)
|
||||
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')
|
||||
})
|
||||
|
||||
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']})
|
||||
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)
|
||||
|
||||
result = super(Users, self).write(vals)
|
||||
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})
|
||||
|
||||
if karma_trackings:
|
||||
self.env['gamification.karma.tracking'].sudo().create(karma_trackings)
|
||||
if 'karma' in vals:
|
||||
self._recompute_rank()
|
||||
return result
|
||||
def _add_karma_batch(self, values_per_user):
|
||||
if not values_per_user:
|
||||
return
|
||||
|
||||
def add_karma(self, karma):
|
||||
for user in self:
|
||||
user.karma += karma
|
||||
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):
|
||||
@@ -104,10 +153,10 @@ class Users(models.Model):
|
||||
|
||||
params = []
|
||||
if from_date:
|
||||
date_from_condition = 'AND tracking.tracking_date::timestamp >= timestamp %s'
|
||||
date_from_condition = 'AND tracking.tracking_date::DATE >= %s::DATE'
|
||||
params.append(from_date)
|
||||
if to_date:
|
||||
date_to_condition = 'AND tracking.tracking_date::timestamp <= timestamp %s'
|
||||
date_to_condition = 'AND tracking.tracking_date::DATE <= %s::DATE'
|
||||
params.append(to_date)
|
||||
params.append(tuple(self.ids))
|
||||
|
||||
@@ -303,3 +352,18 @@ WHERE sub.user_id IN %%s""" % {
|
||||
"""
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import exceptions, fields
|
||||
from odoo import exceptions, fields, _
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.tests import common
|
||||
|
||||
@@ -29,7 +30,9 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
)
|
||||
cls.env['gamification.karma.tracking'].search([]).unlink()
|
||||
|
||||
cls.test_date = fields.Date.today() + relativedelta(month=4, day=1)
|
||||
cls.test_date = datetime(2021, 6, 1)
|
||||
cls.first_day_of_test_date_month = '2021-06-01'
|
||||
cls.first_day_of_test_date_next_month = '2021-07-01'
|
||||
|
||||
@classmethod
|
||||
def _create_trackings(cls, user, karma, steps, track_date, days_delta=1):
|
||||
@@ -41,7 +44,7 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
'old_value': old_value,
|
||||
'new_value': new_value,
|
||||
'consolidated': False,
|
||||
'tracking_date': fields.Date.to_string(track_date)
|
||||
'tracking_date': fields.Datetime.to_string(track_date)
|
||||
}])
|
||||
old_value = new_value
|
||||
track_date = track_date + relativedelta(days=days_delta)
|
||||
@@ -77,25 +80,67 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
results = self.env['res.users']._get_tracking_karma_gain_position([])
|
||||
self.assertEqual(len(results), 0)
|
||||
|
||||
@freeze_time('2021-02-02')
|
||||
def test_consolidation_cron(self):
|
||||
self.patcher = patch('odoo.addons.gamification.models.gamification_karma_tracking.fields.Date', wraps=fields.Date)
|
||||
self.mock_datetime = self.startPatcher(self.patcher)
|
||||
self.mock_datetime.today.return_value = date(self.test_date.year, self.test_date.month + 1, self.test_date.day)
|
||||
Tracking = self.env['gamification.karma.tracking']
|
||||
|
||||
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)
|
||||
# Sanity check
|
||||
self.assertFalse(Tracking.search_count([('user_id', 'in', (self.test_user | self.test_user_2).ids)]))
|
||||
|
||||
test_date = datetime(2020, 12, 15)
|
||||
first_day_of_test_date_month = '2020-12-01'
|
||||
first_day_of_test_date_next_month = '2021-01-01'
|
||||
|
||||
self._create_trackings(self.test_user, karma=20, steps=2, track_date=test_date, days_delta=30)
|
||||
self._create_trackings(self.test_user_2, karma=10, steps=20, track_date=test_date, days_delta=2)
|
||||
|
||||
# Sanity check
|
||||
self.assertEqual(Tracking.search_count([('user_id', '=', self.test_user.id)]), 2)
|
||||
self.assertEqual(Tracking.search_count([('user_id', '=', self.test_user_2.id)]), 20)
|
||||
self.assertEqual(self.test_user.karma, 40)
|
||||
self.assertEqual(self.test_user_2.karma, 200)
|
||||
|
||||
with self.assertQueryCount(8), patch.object(type(self.env['res.users']), 'write') as patched_user_write:
|
||||
Tracking._consolidate_cron()
|
||||
|
||||
# consolidation should not change user karma
|
||||
self.assertFalse(patched_user_write.called, "User karma didn't change during consolidation, it should not be updated")
|
||||
self.assertEqual(self.test_user.karma, 40)
|
||||
self.assertEqual(self.test_user_2.karma, 200)
|
||||
|
||||
consolidated_1 = Tracking.search([
|
||||
('user_id', '=', self.test_user.id),
|
||||
('tracking_date', '>=', first_day_of_test_date_month),
|
||||
('tracking_date', '<', first_day_of_test_date_next_month),
|
||||
])
|
||||
self.assertEqual(len(consolidated), 2)
|
||||
unconsolidated = self.env['gamification.karma.tracking'].search([
|
||||
('user_id', 'in', (self.test_user | self.test_user_2).ids),
|
||||
self.assertEqual(len(consolidated_1), 1)
|
||||
self.assertTrue(consolidated_1.consolidated)
|
||||
self.assertEqual(consolidated_1.old_value, 0)
|
||||
self.assertEqual(consolidated_1.new_value, 20)
|
||||
self.assertEqual(consolidated_1.reason, 'Consolidation from 2020-12-01 to 2020-12-31')
|
||||
|
||||
consolidated_2 = Tracking.search([
|
||||
('user_id', '=', self.test_user_2.id),
|
||||
('tracking_date', '>=', first_day_of_test_date_month),
|
||||
('tracking_date', '<', first_day_of_test_date_next_month),
|
||||
])
|
||||
self.assertEqual(len(consolidated_2), 1)
|
||||
self.assertTrue(consolidated_2.consolidated)
|
||||
self.assertEqual(consolidated_2.old_value, 0)
|
||||
self.assertEqual(consolidated_2.new_value, 10 * 9) # 9 records have been consolidated
|
||||
self.assertEqual(consolidated_2.reason, 'Consolidation from 2020-12-01 to 2020-12-31')
|
||||
|
||||
unconsolidated_1 = Tracking.search_count([
|
||||
('user_id', '=', self.test_user.id),
|
||||
('consolidated', '=', False),
|
||||
])
|
||||
self.assertEqual(len(unconsolidated), 6) # 5 for test user 2, 1 for test user
|
||||
self.assertEqual(unconsolidated_1, 1)
|
||||
|
||||
unconsolidated_2 = Tracking.search_count([
|
||||
('user_id', '=', self.test_user_2.id),
|
||||
('consolidated', '=', False),
|
||||
])
|
||||
self.assertEqual(unconsolidated_2, 11)
|
||||
|
||||
def test_consolidation_monthly(self):
|
||||
Tracking = self.env['gamification.karma.tracking']
|
||||
@@ -107,11 +152,12 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
Tracking._process_consolidate(self.test_date)
|
||||
consolidated = Tracking.search([
|
||||
('user_id', '=', self.test_user_2.id),
|
||||
('consolidated', '=', True),
|
||||
('tracking_date', '=', self.test_date)
|
||||
('tracking_date', '>=', self.first_day_of_test_date_month),
|
||||
('tracking_date', '<', self.first_day_of_test_date_next_month),
|
||||
])
|
||||
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.assertTrue(consolidated.consolidated)
|
||||
self.assertEqual(consolidated.old_value, base_test_user_2_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([
|
||||
@@ -130,7 +176,7 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
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[0].tracking_date.date(), self.test_date.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
|
||||
|
||||
@@ -160,11 +206,17 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
with self.assertRaises(exceptions.AccessError):
|
||||
user.read(['karma_tracking_ids'])
|
||||
|
||||
user.write({'karma': 60})
|
||||
user.add_karma(10)
|
||||
user._add_karma(38, source=self.test_user_2)
|
||||
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
|
||||
trackings = self.env['gamification.karma.tracking'].sudo().search(
|
||||
[('user_id', '=', user.id)], order="create_date ASC, id ASC")
|
||||
self.assertEqual(len(trackings), 2) # create + add_karma
|
||||
self.assertEqual(trackings[0].origin_ref, self.test_user)
|
||||
self.assertEqual(trackings[0].reason, "User Creation (Test User #%i)" % self.test_user.id)
|
||||
self.assertEqual(trackings[1].origin_ref, self.test_user_2)
|
||||
self.assertIn("Add Manually", trackings[1].reason)
|
||||
self.assertIn(self.test_user_2.display_name, trackings[1].reason)
|
||||
self.assertIn(str(self.test_user_2.id), trackings[1].reason)
|
||||
|
||||
def test_user_tracking(self):
|
||||
self.test_user.write({'groups_id': [
|
||||
@@ -180,17 +232,61 @@ class TestKarmaTrackingCommon(common.TransactionCase):
|
||||
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)
|
||||
user._add_karma(38)
|
||||
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(len(user.karma_tracking_ids), 2)
|
||||
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[1].new_value, 70)
|
||||
self.assertIn(_('Add Manually'), user.karma_tracking_ids[1].reason)
|
||||
self.assertIn(self.test_user.display_name, user.karma_tracking_ids[1].reason)
|
||||
self.assertIn(str(self.test_user.id), user.karma_tracking_ids[1].reason)
|
||||
self.assertEqual(user.karma_tracking_ids[0].old_value, 0)
|
||||
self.assertEqual(user.karma_tracking_ids[0].new_value, 32)
|
||||
|
||||
user._add_karma(69, user, _('Test Reason'))
|
||||
self.assertEqual(len(user.karma_tracking_ids), 3)
|
||||
self.assertIn(_('Test Reason'), user.karma_tracking_ids[2].reason)
|
||||
self.assertEqual(user.karma, 139)
|
||||
|
||||
# add manually karma to a user (e.g. from the technical view)
|
||||
tracking = self.env['gamification.karma.tracking'].create({
|
||||
'user_id': user.id,
|
||||
'new_value': 150,
|
||||
'consolidated': False,
|
||||
})
|
||||
self.assertEqual(tracking.old_value, 139)
|
||||
self.assertEqual(tracking.gain, 11)
|
||||
self.assertEqual(user.karma, 150)
|
||||
|
||||
# write directly on the karma field, should generate <gamification.karma.tracking>
|
||||
self.test_user_2.karma = 100 # won't change
|
||||
last_tracking_3 = self.test_user_2.karma_tracking_ids[-1]
|
||||
|
||||
users = (user | self.test_user | self.test_user_2).with_user(self.test_user)
|
||||
with self.assertQueryCount(12):
|
||||
users.karma = 100
|
||||
|
||||
tracking_1 = user.karma_tracking_ids[-1]
|
||||
tracking_2 = self.test_user.karma_tracking_ids[-1]
|
||||
tracking_3 = self.test_user_2.karma_tracking_ids[-1]
|
||||
|
||||
self.assertEqual(user.karma, 100)
|
||||
self.assertEqual(self.test_user.karma, 100)
|
||||
self.assertEqual(tracking_1.new_value, 100)
|
||||
self.assertEqual(tracking_1.old_value, 150)
|
||||
self.assertEqual(tracking_1.gain, -50)
|
||||
self.assertEqual(tracking_1.reason, "Add Manually (Test User #%i)" % self.test_user.id)
|
||||
self.assertEqual(tracking_1.origin_ref, self.test_user)
|
||||
self.assertEqual(tracking_2.new_value, 100)
|
||||
self.assertEqual(tracking_2.old_value, 0)
|
||||
self.assertEqual(tracking_2.gain, 100)
|
||||
self.assertEqual(tracking_2.reason, "Add Manually (Test User #%i)" % self.test_user.id)
|
||||
self.assertEqual(tracking_2.origin_ref, self.test_user)
|
||||
self.assertEqual(last_tracking_3, tracking_3, "Shouldn't have created a new tracking for the third user")
|
||||
self.assertEqual(tracking_3.new_value, 100)
|
||||
self.assertEqual(tracking_3.old_value, 0)
|
||||
self.assertEqual(tracking_3.gain, 100)
|
||||
|
||||
|
||||
class TestComputeRankCommon(common.TransactionCase):
|
||||
|
||||
|
||||
@@ -5,12 +5,22 @@
|
||||
<field name="model">gamification.karma.tracking</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Trackings">
|
||||
<field name="user_id"/>
|
||||
<field name="user_id" string="Karma Owner"/>
|
||||
<field name="tracking_date"/>
|
||||
<field name="origin_ref_model_name"/>
|
||||
<filter string="Consolidated" name="filter_consolidated"
|
||||
domain="[('consolidated', '=', True)]"/>
|
||||
<filter string="My Karma" name="filter_user_id"
|
||||
domain="[('user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Manual" name="filter_res_users"
|
||||
domain="[('origin_ref', 'ilike', 'res.users,')]"/>
|
||||
<group string="Group By" expand="1">
|
||||
<filter string="User" name="group_by_user_id"
|
||||
<filter string="Granted By" name="group_by_create_uid"
|
||||
context="{'group_by': 'create_uid'}"/>
|
||||
<filter string="Source Type" name="group_by_origin_ref_model_name"
|
||||
context="{'group_by': 'origin_ref_model_name'}"/>
|
||||
<filter string="Karma Owner" name="group_by_user_id"
|
||||
context="{'group_by': 'user_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
@@ -21,12 +31,15 @@
|
||||
<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 string="Trackings" editable="top" delete="0" sample="1">
|
||||
<field name="tracking_date" string="Date"/>
|
||||
<field name="origin_ref" options="{'no_create': True}" required="1"/>
|
||||
<field name="reason"/>
|
||||
<field name="user_id" widget="many2one_avatar_user" string="Karma Owner"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="old_value" string="Previous Total" optional="hidden"/>
|
||||
<field name="gain" attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="new_value" string="Total" optional="show" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -35,14 +48,18 @@
|
||||
<field name="name">gamification.karma.tracking.view.form</field>
|
||||
<field name="model">gamification.karma.tracking</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Tracking">
|
||||
<form string="Tracking" delete="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="user_id" widget="many2one_avatar_user" string="Karma Owner"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="tracking_date"/>
|
||||
<field name="gain" attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="old_value"/>
|
||||
<field name="new_value"/>
|
||||
<field name="new_value" readonly="1"/>
|
||||
<field name="consolidated"/>
|
||||
<field name="origin_ref" options="{'no_create': True}" required="1"/>
|
||||
<field name="reason"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@@ -50,9 +67,17 @@
|
||||
</record>
|
||||
|
||||
<record id="gamification_karma_tracking_action" model="ir.actions.act_window">
|
||||
<field name="name">Trackings</field>
|
||||
<field name="name">Karma Tracking</field>
|
||||
<field name="res_model">gamification.karma.tracking</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Karma Tracking
|
||||
</p>
|
||||
<p>
|
||||
Track the sources of the users karma and monitor
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="messaging" position="inside">
|
||||
<field name="karma"/>
|
||||
</group>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button type="object" class="oe_stat_button" groups="base.group_no_one"
|
||||
name="action_karma_report" icon="fa-certificate">
|
||||
<field string="Karma" name="karma" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Ask questions, get answers, no distractions
|
||||
'views/snippets/snippets.xml',
|
||||
'views/website_pages_views.xml',
|
||||
'views/forum_forum_add.xml',
|
||||
'views/gamification_karma_tracking_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_forum_security.xml',
|
||||
'data/badges_question.xml',
|
||||
|
||||
@@ -45,6 +45,7 @@ In simple words does Odoo became CMS+ERP platform?</p>]]></field>
|
||||
|
||||
<!-- Answer -->
|
||||
<record id="answer_0" model="forum.post">
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
<field name="name">Re: How to configure alerts for employee contract expiration</field>
|
||||
<field name="content"><![CDATA[<p>Just for posterity so other can see. Here are the steps to set automatic alerts on any contract.. i.e. HR Employee, or Fleet for example. I will use fleet as an example.</p>
|
||||
@@ -57,6 +58,7 @@ In simple words does Odoo became CMS+ERP platform?</p>]]></field>
|
||||
<field name="parent_id" ref="question_0" />
|
||||
</record>
|
||||
<record id="answer_1" model="forum.post">
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
<field name="name">Re: CMS replacement for ERP and eCommerce</field>
|
||||
<field name="content"><![CDATA[
|
||||
@@ -69,11 +71,13 @@ The CMS editor in Odoo web is nice but I prefer Drupal for customization and the
|
||||
<!-- Post Vote -->
|
||||
<record id="post_vote_0" model="forum.post.vote">
|
||||
<field name="post_id" ref="question_0"/>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="vote">1</field>
|
||||
</record>
|
||||
<record id="post_vote_1" model="forum.post.vote">
|
||||
<field name="post_id" ref="answer_0"/>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="vote">1</field>
|
||||
</record>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from . import forum
|
||||
from . import gamification
|
||||
from . import gamification_karma_tracking
|
||||
from . import ir_attachment
|
||||
from . import res_users
|
||||
from . import website
|
||||
|
||||
@@ -550,7 +550,7 @@ class Post(models.Model):
|
||||
|
||||
# add karma for posting new questions
|
||||
if not post.parent_id and post.state == 'active':
|
||||
self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new)
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_question_new, post, _('Ask a new question'))
|
||||
posts.post_notification()
|
||||
return posts
|
||||
|
||||
@@ -592,8 +592,10 @@ class Post(models.Model):
|
||||
# update karma except for self-acceptance
|
||||
mult = 1 if vals['is_correct'] else -1
|
||||
if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
|
||||
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult)
|
||||
self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * mult, post,
|
||||
_('User answer accepted') if mult > 0 else _('Accepted answer removed'))
|
||||
self.env.user.sudo()._add_karma(post.forum_id.karma_gen_answer_accept * mult, post,
|
||||
_('Validate an answer') if mult > 0 else _('Remove validated answer'))
|
||||
if tag_ids:
|
||||
if set(post.tag_ids.ids) != tag_ids and self.env.user.karma < post.forum_id.karma_edit_retag:
|
||||
raise AccessError(_('%d karma required to retag.', post.forum_id.karma_edit_retag))
|
||||
@@ -667,7 +669,7 @@ class Post(models.Model):
|
||||
count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
|
||||
if count_post == 1:
|
||||
karma *= 10
|
||||
post.create_uid.sudo().add_karma(karma * -1)
|
||||
post.create_uid.sudo()._add_karma(karma * -1, post, _('Reopen a banned question'))
|
||||
|
||||
self.sudo().write({'state': 'active'})
|
||||
|
||||
@@ -687,7 +689,12 @@ class Post(models.Model):
|
||||
count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
|
||||
if count_post == 1:
|
||||
karma *= 10
|
||||
post.create_uid.sudo().add_karma(karma)
|
||||
message = (
|
||||
_('Post is closed and marked as spam')
|
||||
if reason_id == reason_spam else
|
||||
_('Post is closed and marked as offensive content')
|
||||
)
|
||||
post.create_uid.sudo()._add_karma(karma, post, message)
|
||||
|
||||
self.write({
|
||||
'state': 'close',
|
||||
@@ -703,7 +710,11 @@ class Post(models.Model):
|
||||
raise AccessError(_('%d karma required to validate a post.', post.forum_id.karma_moderate))
|
||||
# if state == pending, no karma previously added for the new question
|
||||
if post.state == 'pending':
|
||||
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_new)
|
||||
post.create_uid.sudo()._add_karma(
|
||||
post.forum_id.karma_gen_question_new,
|
||||
post,
|
||||
_('Ask a question'),
|
||||
)
|
||||
post.write({
|
||||
'state': 'active',
|
||||
'active': True,
|
||||
@@ -747,7 +758,7 @@ class Post(models.Model):
|
||||
raise AccessError(_('%d karma required to mark a post as offensive.', post.forum_id.karma_moderate))
|
||||
# remove some karma
|
||||
_logger.info('Downvoting user <%s> for posting spam/offensive contents', post.create_uid)
|
||||
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_flagged)
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_flagged, post, _('Downvote for posting offensive contents'))
|
||||
# TODO: potential bottleneck, could be done in batch
|
||||
post.write({
|
||||
'state': 'offensive',
|
||||
@@ -781,8 +792,8 @@ class Post(models.Model):
|
||||
# if unlinking an answer with accepted answer: remove provided karma
|
||||
for post in self:
|
||||
if post.is_correct:
|
||||
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
|
||||
self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * -1, post, _('The accepted answer is deleted'))
|
||||
self.env.user.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * -1, post, _('Delete the accepted answer'))
|
||||
return super(Post, self).unlink()
|
||||
|
||||
def bump(self):
|
||||
@@ -1074,12 +1085,22 @@ class Vote(models.Model):
|
||||
]
|
||||
|
||||
def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma):
|
||||
_karma_upd = {
|
||||
'-1': {'-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma},
|
||||
'0': {'-1': 1 * down_karma, '0': 0, '1': up_karma},
|
||||
'1': {'-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0}
|
||||
}
|
||||
return _karma_upd[old_vote][new_vote]
|
||||
"""Return the karma to add / remove based on the old vote and on the new vote."""
|
||||
karma_values = {'-1': down_karma, '0': 0, '1': up_karma}
|
||||
karma = karma_values[new_vote] - karma_values[old_vote]
|
||||
|
||||
if old_vote == new_vote:
|
||||
reason = _('no changes')
|
||||
elif new_vote == '1':
|
||||
reason = _('upvoted')
|
||||
elif new_vote == '-1':
|
||||
reason = _('downvoted')
|
||||
elif old_vote == '1':
|
||||
reason = _('no more upvoted')
|
||||
else:
|
||||
reason = _('no more downvoted')
|
||||
|
||||
return karma, reason
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
@@ -1139,10 +1160,20 @@ class Vote(models.Model):
|
||||
|
||||
def _vote_update_karma(self, old_vote, new_vote):
|
||||
if self.post_id.parent_id:
|
||||
karma_value = self._get_karma_value(old_vote, new_vote, self.forum_id.karma_gen_answer_upvote, self.forum_id.karma_gen_answer_downvote)
|
||||
karma, reason = self._get_karma_value(
|
||||
old_vote,
|
||||
new_vote,
|
||||
self.forum_id.karma_gen_answer_upvote,
|
||||
self.forum_id.karma_gen_answer_downvote)
|
||||
source = _('Answer %s', reason)
|
||||
else:
|
||||
karma_value = self._get_karma_value(old_vote, new_vote, self.forum_id.karma_gen_question_upvote, self.forum_id.karma_gen_question_downvote)
|
||||
self.recipient_id.sudo().add_karma(karma_value)
|
||||
karma, reason = self._get_karma_value(
|
||||
old_vote,
|
||||
new_vote,
|
||||
self.forum_id.karma_gen_question_upvote,
|
||||
self.forum_id.karma_gen_question_downvote)
|
||||
source = _('Question %s', reason)
|
||||
self.recipient_id.sudo()._add_karma(karma, self.post_id, source)
|
||||
|
||||
|
||||
class Tags(models.Model):
|
||||
@@ -1170,6 +1201,6 @@ class Tags(models.Model):
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
forum = self.env['forum.forum'].browse(vals.get('forum_id'))
|
||||
if self.env.user.karma < forum.karma_tag_create:
|
||||
if self.env.user.karma < forum.karma_tag_create and not self.env.is_admin():
|
||||
raise AccessError(_('%d karma required to create a new Tag.', forum.karma_tag_create))
|
||||
return super(Tags, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals_list)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class KarmaTracking(models.Model):
|
||||
_inherit = 'gamification.karma.tracking'
|
||||
|
||||
def _get_origin_selection_values(self):
|
||||
return super()._get_origin_selection_values() + [('forum.post', self.env['ir.model']._get('forum.post').display_name)]
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="gamification_karma_tracking_view_search" model="ir.ui.view">
|
||||
<field name="name">gamification.karma.tracking.view.search.inherit.website.forum</field>
|
||||
<field name="model">gamification.karma.tracking</field>
|
||||
<field name="inherit_id" ref="gamification.gamification_karma_tracking_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='filter_res_users']" position='after'>
|
||||
<filter string="Forum" name="filter_forum_post"
|
||||
domain="[('origin_ref', 'ilike', 'forum.post,')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -28,6 +28,7 @@ Featuring
|
||||
'data': [
|
||||
'security/website_slides_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/gamification_karma_tracking_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/rating_rating_views.xml',
|
||||
@@ -64,6 +65,7 @@ Featuring
|
||||
'data/slide_channel_demo.xml',
|
||||
'data/slide_slide_demo.xml',
|
||||
'data/slide_user_demo.xml',
|
||||
'data/slide_user_gamification_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
|
||||
@@ -5,7 +5,7 @@ import werkzeug
|
||||
|
||||
from werkzeug.exceptions import NotFound, Forbidden
|
||||
|
||||
from odoo import http
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.mail import _check_special_access, PortalChatter
|
||||
from odoo.tools import plaintext2html, html2plaintext
|
||||
@@ -28,7 +28,7 @@ class SlidesPortalChatter(PortalChatter):
|
||||
slide_channel = request.env[res_model].sudo().browse(int(res_id))
|
||||
if rating_value and slide_channel and request.env.user.partner_id.id == int(kw.get('pid')):
|
||||
# apply karma gain rule only once
|
||||
request.env.user.add_karma(slide_channel.karma_gen_channel_rank)
|
||||
request.env.user._add_karma(slide_channel.karma_gen_channel_rank, slide_channel, _('Course Ranked'))
|
||||
result.update({
|
||||
'default_rating_value': rating_value,
|
||||
'rating_avg': slide_channel.rating_avg,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Completion of Quiz and Karma Generation -->
|
||||
<record id="slide_slide_1_4_partner_admin" model="slide.slide.partner">
|
||||
<field name="slide_id" ref="website_slides.slide_slide_demo_1_4"/>
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="quiz_attempts_count">1</field>
|
||||
<field name="completed" eval="True"/>
|
||||
</record>
|
||||
<function model="res.users" name="_add_karma">
|
||||
<value eval="[ref('base.user_admin')]"/>
|
||||
<value eval="20"/>
|
||||
<value model="slide.slide" eval="obj().env.ref('website_slides.slide_slide_demo_1_4')"/>
|
||||
<value>Quiz completed</value>
|
||||
</function>
|
||||
<!-- Completion of Course (Karma Generation Triggered in Python) -->
|
||||
<record id="slide_slide_1_3_partner_admin" model="slide.slide.partner">
|
||||
<field name="slide_id" ref="website_slides.slide_slide_demo_1_3"/>
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="completed" eval="True"/>
|
||||
</record>
|
||||
<record id="slide_slide_1_5_partner_admin" model="slide.slide.partner">
|
||||
<field name="slide_id" ref="website_slides.slide_slide_demo_1_5"/>
|
||||
<field name="partner_id" ref="base.partner_admin"/>
|
||||
<field name="completed" eval="True"/>
|
||||
</record>
|
||||
<!-- Demo join and leave a course -->
|
||||
<function model="res.users" name="_add_karma">
|
||||
<value eval="[ref('base.user_demo')]"/>
|
||||
<value eval="30"/>
|
||||
<value model="slide.channel" eval="obj().env.ref('website_slides.slide_channel_demo_1_gard1')"/>
|
||||
<value>Course Completed</value>
|
||||
</function>
|
||||
<function model="res.users" name="_add_karma">
|
||||
<value eval="[ref('base.user_demo')]"/>
|
||||
<value eval="-30"/>
|
||||
<value model="slide.channel" eval="obj().env.ref('website_slides.slide_channel_demo_1_gard1')"/>
|
||||
<value>Membership Removed</value>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,6 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import gamification_challenge
|
||||
from . import gamification_karma_tracking
|
||||
from . import slide_slide
|
||||
from . import slide_question
|
||||
from . import slide_embed
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, models
|
||||
|
||||
|
||||
class KarmaTracking(models.Model):
|
||||
_inherit = 'gamification.karma.tracking'
|
||||
|
||||
def _get_origin_selection_values(self):
|
||||
return (
|
||||
super(KarmaTracking, self)._get_origin_selection_values()
|
||||
+ [('slide.slide', _('Course Quiz')), ('slide.channel', self.env['ir.model']._get('slide.channel').display_name)]
|
||||
)
|
||||
@@ -4,7 +4,6 @@
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import ast
|
||||
|
||||
@@ -104,24 +103,21 @@ class ChannelUsersRelation(models.Model):
|
||||
True if we make the slide as completed
|
||||
False if we remove user completion
|
||||
"""
|
||||
partner_karma = dict.fromkeys(self.mapped('partner_id').ids, 0)
|
||||
for record in self:
|
||||
record.completed = completed
|
||||
partner_karma[record.partner_id.id] += record.channel_id.karma_gen_channel_finish
|
||||
for channel, memberships in self.grouped("channel_id").items():
|
||||
memberships.completed = completed
|
||||
karma = channel.karma_gen_channel_finish
|
||||
if karma <= 0:
|
||||
continue
|
||||
|
||||
partner_karma = {
|
||||
partner_id: karma_to_add
|
||||
for partner_id, karma_to_add in partner_karma.items() if karma_to_add > 0
|
||||
}
|
||||
karma_per_users = {}
|
||||
for user in memberships.sudo().partner_id.user_ids:
|
||||
karma_per_users[user] = {
|
||||
'gain': karma if completed else karma * -1,
|
||||
'source': channel,
|
||||
'reason': _('Course Finished') if completed else _('Course Set Uncompleted'),
|
||||
}
|
||||
|
||||
if partner_karma:
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_karma.keys()))])
|
||||
for user in users:
|
||||
karma = partner_karma[user.partner_id.id]
|
||||
if not completed:
|
||||
# Mark the channel as not-completed, we remove the gained karma
|
||||
karma *= -1
|
||||
users.add_karma(karma)
|
||||
self.env['res.users']._add_karma_batch(karma_per_users)
|
||||
|
||||
def _send_completed_mail(self):
|
||||
""" Send an email to the attendee when they have successfully completed a course. """
|
||||
@@ -745,7 +741,7 @@ class Channel(models.Model):
|
||||
Warning: this count will not be accurate if the configuration has been
|
||||
modified after the completion of a course!
|
||||
"""
|
||||
total_karma = defaultdict(int)
|
||||
total_karma = defaultdict(list)
|
||||
|
||||
slide_completed = self.env['slide.slide.partner'].sudo().search([
|
||||
('partner_id', 'in', partner_ids),
|
||||
@@ -757,21 +753,29 @@ class Channel(models.Model):
|
||||
slide = partner_slide.slide_id
|
||||
if not slide.question_ids:
|
||||
continue
|
||||
gains = [slide.quiz_first_attempt_reward,
|
||||
slide.quiz_second_attempt_reward,
|
||||
slide.quiz_third_attempt_reward,
|
||||
slide.quiz_fourth_attempt_reward]
|
||||
attempts = min(partner_slide.quiz_attempts_count - 1, 3)
|
||||
total_karma[partner_slide.partner_id.id] += gains[attempts]
|
||||
gains = [
|
||||
slide.quiz_first_attempt_reward,
|
||||
slide.quiz_second_attempt_reward,
|
||||
slide.quiz_third_attempt_reward,
|
||||
slide.quiz_fourth_attempt_reward,
|
||||
]
|
||||
attempts = min(partner_slide.quiz_attempts_count, len(gains))
|
||||
total_karma[partner_slide.partner_id.id].append({
|
||||
'karma': gains[attempts - 1],
|
||||
'channel_id': slide.channel_id,
|
||||
})
|
||||
|
||||
channel_completed = self.env['slide.channel.partner'].sudo().search([
|
||||
('partner_id', 'in', partner_ids),
|
||||
('channel_id', 'in', self.ids),
|
||||
('completed', '=', True)
|
||||
('completed', '=', True),
|
||||
])
|
||||
for partner_channel in channel_completed:
|
||||
channel = partner_channel.channel_id
|
||||
total_karma[partner_channel.partner_id.id] += channel.karma_gen_channel_finish
|
||||
total_karma[partner_channel.partner_id.id].append({
|
||||
'karma': channel.karma_gen_channel_finish,
|
||||
'channel_id': channel,
|
||||
})
|
||||
|
||||
return total_karma
|
||||
|
||||
@@ -782,23 +786,29 @@ class Channel(models.Model):
|
||||
if not partner_ids:
|
||||
raise ValueError("Do not use this method with an empty partner_id recordset")
|
||||
|
||||
earned_karma = self._get_earned_karma(partner_ids)
|
||||
users = self.env['res.users'].sudo().search([
|
||||
('partner_id', 'in', list(earned_karma)),
|
||||
])
|
||||
for user in users:
|
||||
if earned_karma[user.partner_id.id]:
|
||||
user.add_karma(-1 * earned_karma[user.partner_id.id])
|
||||
|
||||
removed_channel_partner_domain = []
|
||||
for channel in self:
|
||||
earned_karma = channel._get_earned_karma(partner_ids)
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(earned_karma))])
|
||||
|
||||
karma_values = {}
|
||||
for user in users:
|
||||
karma = sum(values['karma'] for values in earned_karma[user.partner_id.id])
|
||||
if karma:
|
||||
karma_values[user] = {
|
||||
'gain': karma * -1,
|
||||
'source': channel,
|
||||
'reason': _('Membership Removed'),
|
||||
}
|
||||
self.env['res.users']._add_karma_batch(karma_values)
|
||||
|
||||
removed_channel_partner_domain = expression.OR([
|
||||
removed_channel_partner_domain,
|
||||
[('partner_id', 'in', partner_ids),
|
||||
('channel_id', '=', channel.id)]
|
||||
])
|
||||
self.message_unsubscribe(partner_ids=partner_ids)
|
||||
|
||||
self.message_unsubscribe(partner_ids=partner_ids)
|
||||
if removed_channel_partner_domain:
|
||||
self.env['slide.channel.partner'].sudo().search(removed_channel_partner_domain).unlink()
|
||||
|
||||
|
||||
@@ -955,12 +955,16 @@ class Slide(models.Model):
|
||||
slide.quiz_second_attempt_reward,
|
||||
slide.quiz_third_attempt_reward,
|
||||
slide.quiz_fourth_attempt_reward]
|
||||
points += gains[user_membership_sudo.quiz_attempts_count - 1] if user_membership_sudo.quiz_attempts_count <= len(gains) else gains[-1]
|
||||
points = gains[min(user_membership_sudo.quiz_attempts_count, len(gains)) - 1]
|
||||
if points:
|
||||
if completed:
|
||||
reason = _('Quiz Completed')
|
||||
else:
|
||||
points *= -1
|
||||
reason = _('Quiz Set Uncompleted')
|
||||
self.env.user.sudo()._add_karma(points, slide, reason)
|
||||
|
||||
if not completed:
|
||||
points *= -1
|
||||
|
||||
return self.env.user.sudo().add_karma(points)
|
||||
return True
|
||||
|
||||
def action_view_embeds(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -115,3 +115,47 @@ class TestKarmaGain(common.SlidesCase):
|
||||
computed_karma += self.channel.karma_gen_channel_finish + self.channel_2.karma_gen_channel_finish
|
||||
(self.slide | self.slide_2 | self.slide_3 | self.slide_2_0 | self.slide_2_1).with_user(user)._action_mark_completed()
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
def test_karma_gain_multiple_course_multiple_users(self):
|
||||
users = self.user_emp | self.user_portal
|
||||
users.write({'karma': 0})
|
||||
|
||||
(self.channel | self.channel_2)._action_add_members(users.partner_id)
|
||||
channel_partners = self.env['slide.channel.partner'].sudo().search([('partner_id', 'in', users.partner_id.ids)])
|
||||
self.assertEqual(len(channel_partners), 4)
|
||||
|
||||
with self.assertQueryCount(53):
|
||||
channel_partners._set_as_completed()
|
||||
|
||||
computed_karma = self.channel.karma_gen_channel_finish + self.channel_2.karma_gen_channel_finish
|
||||
|
||||
for user in users:
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
user_trackings = user.karma_tracking_ids
|
||||
self.assertEqual(len(user_trackings), 2)
|
||||
|
||||
self.assertEqual(user_trackings[0].new_value, computed_karma)
|
||||
self.assertEqual(user_trackings[0].old_value, self.channel_2.karma_gen_channel_finish)
|
||||
self.assertEqual(user_trackings[0].origin_ref, self.channel_2)
|
||||
|
||||
self.assertEqual(user_trackings[1].new_value, self.channel.karma_gen_channel_finish)
|
||||
self.assertEqual(user_trackings[1].old_value, 0)
|
||||
self.assertEqual(user_trackings[1].origin_ref, self.channel)
|
||||
|
||||
# now, remove the membership in batch, on multiple users
|
||||
with self.assertQueryCount(43):
|
||||
(self.channel | self.channel_2)._remove_membership(users.partner_id.ids)
|
||||
|
||||
for user in users:
|
||||
self.assertEqual(user.karma, 0)
|
||||
user_trackings = user.karma_tracking_ids
|
||||
self.assertEqual(len(user_trackings), 4)
|
||||
|
||||
self.assertEqual(user_trackings[0].new_value, 0)
|
||||
self.assertEqual(user_trackings[0].old_value, self.channel.karma_gen_channel_finish)
|
||||
self.assertEqual(user_trackings[0].origin_ref, self.channel_2)
|
||||
|
||||
self.assertEqual(user_trackings[1].new_value, self.channel.karma_gen_channel_finish)
|
||||
self.assertEqual(user_trackings[1].old_value, computed_karma)
|
||||
self.assertEqual(user_trackings[1].origin_ref, self.channel)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="gamification_karma_tracking_view_search" model="ir.ui.view">
|
||||
<field name="name">gamification.karma.tracking.view.search.inherit.website.slides</field>
|
||||
<field name="model">gamification.karma.tracking</field>
|
||||
<field name="inherit_id" ref="gamification.gamification_karma_tracking_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='filter_res_users']" position='after'>
|
||||
<filter string="Course" name="filter_slide_channel"
|
||||
domain="[('origin_ref', 'ilike', 'slide.channel,')]"/>
|
||||
<filter string="Quiz" name="filter_slide_slide"
|
||||
domain="[('origin_ref', 'ilike', 'slide.slide,')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user