[MERGE] gamification: improve challenge views and reorder module

PURPOSE

Gamification display is slow when being heavily used. Indeed the m2m towards
users is used in kanban and form views, leading to a huge data fetch when
being used in production environment with lots of users.

SPECIFICATIONS

Remove user_ids m2m from views. Indeed in some heavily used DBs you may have
more than 10K records in user_ids, leading to a huge m2m tags to display.

Replace it by an optimized count, used in

  * form view: as a stat button redirecting to participants of a challenge;
  * kanban view: a line (replacing the 11 first avatars) redirecting to
    participants of a challenge;

Also of this commit is to rename and reorder views, data and models by main
model. It allows to better understand module organization and find views
one may have to update.

Task-2678295

closes odoo/odoo#79599

Related: odoo/upgrade#3020
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2021-11-10 15:30:51 +01:00
committed by GitHub
21 changed files with 509 additions and 429 deletions
+15 -11
View File
@@ -22,19 +22,23 @@ Both goals and badges are flexibles and can be adapted to a large range of modul
'data': [
'wizard/update_goal.xml',
'wizard/grant_badge.xml',
'views/badge.xml',
'views/challenge.xml',
'views/goal.xml',
'data/cron.xml',
'security/gamification_security.xml',
'security/ir.model.access.csv',
'data/mail_template_data.xml',
'data/goal_base.xml',
'data/badge.xml',
'data/gamification_karma_rank_data.xml',
'views/res_users_views.xml',
'views/gamification_karma_rank_views.xml',
'views/gamification_karma_tracking_views.xml',
'views/res_users_views.xml',
'views/gamification_badge_views.xml',
'views/gamification_badge_user_views.xml',
'views/gamification_goal_views.xml',
'views/gamification_goal_definition_views.xml',
'views/gamification_challenge_views.xml',
'views/gamification_challenge_line_views.xml',
'views/gamification_menus.xml',
'security/gamification_security.xml',
'security/ir.model.access.csv',
'data/ir_cron_data.xml',
'data/mail_template_data.xml', # keep before to populate challenge reports
'data/gamification_badge_data.xml',
'data/gamification_challenge_data.xml',
'data/gamification_karma_rank_data.xml',
],
'demo': [
'data/gamification_karma_rank_demo.xml',
@@ -38,16 +38,6 @@
<field name="res_id_field">user.company_id.id</field>
</record>
<record id="action_new_simplified_res_users" model="ir.actions.act_window">
<field name="name">Create User</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.users</field>
<field name="target">current</field>
<field name="view_id" ref="base.view_users_simple_form"/>
<field name="context">{}</field>
<field name="help">Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.</field>
</record>
<record model="gamification.goal.definition" id="definition_base_invite">
<field name="name">Invite new Users</field>
<field name="description">Create at least another user</field>
+6 -3
View File
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import goal
from . import challenge
from . import badge
from . import gamification_badge
from . import gamification_badge_user
from . import gamification_challenge
from . import gamification_challenge_line
from . import gamification_goal
from . import gamification_goal_definition
from . import gamification_karma_rank
from . import gamification_karma_tracking
from . import res_users
@@ -9,56 +9,6 @@ from odoo import api, fields, models, _, exceptions
_logger = logging.getLogger(__name__)
class BadgeUser(models.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
_description = 'Gamification User Badge'
_order = "create_date desc"
_rec_name = "badge_name"
user_id = fields.Many2one('res.users', string="User", required=True, ondelete="cascade", index=True)
sender_id = fields.Many2one('res.users', string="Sender", help="The user who has send the badge")
badge_id = fields.Many2one('gamification.badge', string='Badge', required=True, ondelete="cascade", index=True)
challenge_id = fields.Many2one('gamification.challenge', string='Challenge originating', help="If this badge was rewarded through a challenge")
comment = fields.Text('Comment')
badge_name = fields.Char(related='badge_id.name', string="Badge Name", readonly=False)
level = fields.Selection(
string='Badge Level', related="badge_id.level", store=True, readonly=True)
def _send_badge(self):
"""Send a notification to a user for receiving a badge
Does not verify constrains on badge granting.
The users are added to the owner_ids (create badge_user if needed)
The stats counters are incremented
:param ids: list(int) of badge users that will receive the badge
"""
template = self.env.ref('gamification.email_template_badge_received')
for badge_user in self:
self.env['mail.thread'].message_post_with_template(
template.id,
model=badge_user._name,
res_id=badge_user.id,
composition_mode='mass_mail',
# `website_forum` triggers `_cron_update` which triggers this method for template `Received Badge`
# for which `badge_user.user_id.partner_id.ids` equals `[8]`, which is then passed to `self.env['mail.compose.message'].create(...)`
# which expects a command list and not a list of ids. In master, this wasn't doing anything, at the end composer.partner_ids was [] and not [8]
# I believe this line is useless, it will take the partners to which the template must be send from the template itself (`partner_to`)
# The below line was therefore pointless.
# partner_ids=badge_user.user_id.partner_id.ids,
)
return True
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self.env['gamification.badge'].browse(vals['badge_id']).check_granting()
return super().create(vals_list)
class GamificationBadge(models.Model):
"""Badge object that users can send and receive"""
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class BadgeUser(models.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
_description = 'Gamification User Badge'
_order = "create_date desc"
_rec_name = "badge_name"
user_id = fields.Many2one('res.users', string="User", required=True, ondelete="cascade", index=True)
sender_id = fields.Many2one('res.users', string="Sender", help="The user who has send the badge")
badge_id = fields.Many2one('gamification.badge', string='Badge', required=True, ondelete="cascade", index=True)
challenge_id = fields.Many2one('gamification.challenge', string='Challenge originating', help="If this badge was rewarded through a challenge")
comment = fields.Text('Comment')
badge_name = fields.Char(related='badge_id.name', string="Badge Name", readonly=False)
level = fields.Selection(
string='Badge Level', related="badge_id.level", store=True, readonly=True)
def _send_badge(self):
"""Send a notification to a user for receiving a badge
Does not verify constrains on badge granting.
The users are added to the owner_ids (create badge_user if needed)
The stats counters are incremented
:param ids: list(int) of badge users that will receive the badge
"""
template = self.env.ref('gamification.email_template_badge_received')
for badge_user in self:
self.env['mail.thread'].message_post_with_template(
template.id,
model=badge_user._name,
res_id=badge_user.id,
composition_mode='mass_mail',
# `website_forum` triggers `_cron_update` which triggers this method for template `Received Badge`
# for which `badge_user.user_id.partner_id.ids` equals `[8]`, which is then passed to `self.env['mail.compose.message'].create(...)`
# which expects a command list and not a list of ids. In master, this wasn't doing anything, at the end composer.partner_ids was [] and not [8]
# I believe this line is useless, it will take the partners to which the template must be send from the template itself (`partner_to`)
# The below line was therefore pointless.
# partner_ids=badge_user.user_id.partner_id.ids,
)
return True
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self.env['gamification.badge'].browse(vals['badge_id']).check_granting()
return super().create(vals_list)
@@ -60,6 +60,7 @@ class Challenge(models.Model):
_inherit = 'mail.thread'
_order = 'end_date, start_date, name, id'
# description
name = fields.Char("Challenge Name", required=True, translate=True)
description = fields.Text("Description", translate=True)
state = fields.Selection([
@@ -71,10 +72,11 @@ class Challenge(models.Model):
manager_id = fields.Many2one(
'res.users', default=lambda self: self.env.uid,
string="Responsible", help="The user responsible for the challenge.",)
# members
user_ids = fields.Many2many('res.users', 'gamification_challenge_users_rel', string="Users", help="List of users participating to the challenge")
user_domain = fields.Char("User domain", help="Alternative to a list of users")
user_count = fields.Integer('# Users', compute='_compute_user_count')
# periodicity
period = fields.Selection([
('once', "Non recurring"),
('daily', "Daily"),
@@ -129,6 +131,26 @@ class Challenge(models.Model):
], string="Appears in", required=True, default='hr',
help="Define the visibility of the challenge through menus")
@api.depends('user_ids')
def _compute_user_count(self):
mapped_data = {}
if self.ids:
query = """
SELECT gamification_challenge_id, count(res_users_id)
FROM gamification_challenge_users_rel rel
LEFT JOIN res_users users
ON users.id=rel.res_users_id AND users.active = TRUE
WHERE gamification_challenge_id IN %s
GROUP BY gamification_challenge_id
"""
self.env.cr.execute(query, [tuple(self.ids)])
mapped_data = dict(
(challenge_id, user_count)
for challenge_id, user_count in self.env.cr.fetchall()
)
for challenge in self:
challenge.user_count = mapped_data.get(challenge.id, 0)
REPORT_OFFSETS = {
'daily': timedelta(days=1),
'weekly': timedelta(days=7),
@@ -315,6 +337,12 @@ class Challenge(models.Model):
challenge.report_progress()
return True
def action_view_users(self):
""" Redirect to the participants (users) list. """
action = self.env["ir.actions.actions"]._for_xml_id("base.action_res_users")
action['domain'] = [('id', 'in', self.user_ids.ids)]
return action
##### Automatic actions #####
def _generate_goals_from_challenge(self):
@@ -769,27 +797,3 @@ class Challenge(models.Model):
'badge_id': badge.id,
'challenge_id': self.id
})._send_badge()
class ChallengeLine(models.Model):
"""Gamification challenge line
Predefined goal for 'gamification_challenge'
These are generic list of goals with only the target goal defined
Should only be created for the gamification.challenge object
"""
_name = 'gamification.challenge.line'
_description = 'Gamification generic goal for challenge'
_order = "sequence, id"
challenge_id = fields.Many2one('gamification.challenge', string='Challenge', required=True, ondelete="cascade")
definition_id = fields.Many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade")
sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1)
target_goal = fields.Float('Target Value to Reach', required=True)
name = fields.Char("Name", related='definition_id.name', readonly=False)
condition = fields.Selection(string="Condition", related='definition_id.condition', readonly=True)
definition_suffix = fields.Char("Unit", related='definition_id.suffix', readonly=True)
definition_monetary = fields.Boolean("Monetary", related='definition_id.monetary', readonly=True)
definition_full_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ChallengeLine(models.Model):
"""Gamification challenge line
Predefined goal for 'gamification_challenge'
These are generic list of goals with only the target goal defined
Should only be created for the gamification.challenge object
"""
_name = 'gamification.challenge.line'
_description = 'Gamification generic goal for challenge'
_order = "sequence, id"
challenge_id = fields.Many2one('gamification.challenge', string='Challenge', required=True, ondelete="cascade")
definition_id = fields.Many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade")
sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1)
target_goal = fields.Float('Target Value to Reach', required=True)
name = fields.Char("Name", related='definition_id.name', readonly=False)
condition = fields.Selection(string="Condition", related='definition_id.condition', readonly=True)
definition_suffix = fields.Char("Unit", related='definition_id.suffix', readonly=True)
definition_monetary = fields.Boolean("Monetary", related='definition_id.monetary', readonly=True)
definition_full_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
@@ -6,141 +6,11 @@ import logging
from datetime import date, datetime, timedelta
from odoo import api, fields, models, _, exceptions
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval, time
_logger = logging.getLogger(__name__)
DOMAIN_TEMPLATE = "[('store', '=', True), '|', ('model_id', '=', model_id), ('model_id', 'in', model_inherited_ids)%s]"
class GoalDefinition(models.Model):
"""Goal definition
A goal definition contains the way to evaluate an objective
Each module wanting to be able to set goals to the users needs to create
a new gamification_goal_definition
"""
_name = 'gamification.goal.definition'
_description = 'Gamification Goal Definition'
name = fields.Char("Goal Definition", required=True, translate=True)
description = fields.Text("Goal Description")
monetary = fields.Boolean("Monetary Value", default=False, help="The target and current value are defined in the company currency.")
suffix = fields.Char("Suffix", help="The unit of the target and current values", translate=True)
full_suffix = fields.Char("Full Suffix", compute='_compute_full_suffix', help="The currency and suffix field")
computation_mode = fields.Selection([
('manually', "Recorded manually"),
('count', "Automatic: number of records"),
('sum', "Automatic: sum on a field"),
('python', "Automatic: execute a specific Python code"),
], default='manually', string="Computation Mode", required=True,
help="Define how the goals will be computed. The result of the operation will be stored in the field 'Current'.")
display_mode = fields.Selection([
('progress', "Progressive (using numerical values)"),
('boolean', "Exclusive (done or not-done)"),
], default='progress', string="Displayed as", required=True)
model_id = fields.Many2one('ir.model', string='Model', help='The model object for the field to evaluate')
model_inherited_ids = fields.Many2many('ir.model', related='model_id.inherited_model_ids')
field_id = fields.Many2one(
'ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate',
domain=DOMAIN_TEMPLATE % ''
)
field_date_id = fields.Many2one(
'ir.model.fields', string='Date Field', help='The date to use for the time period evaluated',
domain=DOMAIN_TEMPLATE % ", ('ttype', 'in', ('date', 'datetime'))"
)
domain = fields.Char(
"Filter Domain", required=True, default="[]",
help="Domain for filtering records. General rule, not user depending,"
" e.g. [('state', '=', 'done')]. The expression can contain"
" reference to 'user' which is a browse record of the current"
" user if not in batch mode.")
batch_mode = fields.Boolean("Batch Mode", help="Evaluate the expression in batch instead of once for each user")
batch_distinctive_field = fields.Many2one('ir.model.fields', string="Distinctive field for batch user", help="In batch mode, this indicates which field distinguishes one user from the other, e.g. user_id, partner_id...")
batch_user_expression = fields.Char("Evaluated expression for batch mode", help="The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id...")
compute_code = fields.Text("Python Code", help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id.")
condition = fields.Selection([
('higher', "The higher the better"),
('lower', "The lower the better")
], default='higher', required=True, string="Goal Performance",
help="A goal is considered as completed when the current value is compared to the value to reach")
action_id = fields.Many2one('ir.actions.act_window', string="Action", help="The action that will be called to update the goal value.")
res_id_field = fields.Char("ID Field of user", help="The field name on the user profile (res.users) containing the value for res_id for action.")
@api.depends('suffix', 'monetary') # also depends of user...
def _compute_full_suffix(self):
for goal in self:
items = []
if goal.monetary:
items.append(self.env.company.currency_id.symbol or u'¤')
if goal.suffix:
items.append(goal.suffix)
goal.full_suffix = u' '.join(items)
def _check_domain_validity(self):
# take admin as should always be present
for definition in self:
if definition.computation_mode not in ('count', 'sum'):
continue
Obj = self.env[definition.model_id.model]
try:
domain = safe_eval(definition.domain, {
'user': self.env.user.with_user(self.env.user)
})
# dummy search to make sure the domain is valid
Obj.search_count(domain)
except (ValueError, SyntaxError) as e:
msg = e
if isinstance(e, SyntaxError):
msg = (e.msg + '\n' + e.text)
raise exceptions.UserError(_("The domain for the definition %s seems incorrect, please check it.\n\n%s") % (definition.name, msg))
return True
def _check_model_validity(self):
""" make sure the selected field and model are usable"""
for definition in self:
try:
if not (definition.model_id and definition.field_id):
continue
Model = self.env[definition.model_id.model]
field = Model._fields.get(definition.field_id.name)
if not (field and field.store):
raise exceptions.UserError(_(
"The model configuration for the definition %(name)s seems incorrect, please check it.\n\n%(field_name)s not stored",
name=definition.name,
field_name=definition.field_id.name
))
except KeyError as e:
raise exceptions.UserError(_(
"The model configuration for the definition %(name)s seems incorrect, please check it.\n\n%(error)s not found",
name=definition.name,
error=e
))
@api.model_create_multi
def create(self, vals_list):
definitions = super(GoalDefinition, self).create(vals_list)
definitions.filtered_domain([
('computation_mode', 'in', ['count', 'sum']),
])._check_domain_validity()
definitions.filtered_domain([
('field_id', '=', 'True'),
])._check_model_validity()
return definitions
def write(self, vals):
res = super(GoalDefinition, self).write(vals)
if vals.get('computation_mode', 'count') in ('count', 'sum') and (vals.get('domain') or vals.get('model_id')):
self._check_domain_validity()
if vals.get('field_id') or vals.get('model_id') or vals.get('batch_mode'):
self._check_model_validity()
return res
class Goal(models.Model):
"""Goal instance for a user
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _, exceptions
from odoo.tools.safe_eval import safe_eval
DOMAIN_TEMPLATE = "[('store', '=', True), '|', ('model_id', '=', model_id), ('model_id', 'in', model_inherited_ids)%s]"
class GoalDefinition(models.Model):
"""Goal definition
A goal definition contains the way to evaluate an objective
Each module wanting to be able to set goals to the users needs to create
a new gamification_goal_definition
"""
_name = 'gamification.goal.definition'
_description = 'Gamification Goal Definition'
name = fields.Char("Goal Definition", required=True, translate=True)
description = fields.Text("Goal Description")
monetary = fields.Boolean("Monetary Value", default=False, help="The target and current value are defined in the company currency.")
suffix = fields.Char("Suffix", help="The unit of the target and current values", translate=True)
full_suffix = fields.Char("Full Suffix", compute='_compute_full_suffix', help="The currency and suffix field")
computation_mode = fields.Selection([
('manually', "Recorded manually"),
('count', "Automatic: number of records"),
('sum', "Automatic: sum on a field"),
('python', "Automatic: execute a specific Python code"),
], default='manually', string="Computation Mode", required=True,
help="Define how the goals will be computed. The result of the operation will be stored in the field 'Current'.")
display_mode = fields.Selection([
('progress', "Progressive (using numerical values)"),
('boolean', "Exclusive (done or not-done)"),
], default='progress', string="Displayed as", required=True)
model_id = fields.Many2one('ir.model', string='Model', help='The model object for the field to evaluate')
model_inherited_ids = fields.Many2many('ir.model', related='model_id.inherited_model_ids')
field_id = fields.Many2one(
'ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate',
domain=DOMAIN_TEMPLATE % ''
)
field_date_id = fields.Many2one(
'ir.model.fields', string='Date Field', help='The date to use for the time period evaluated',
domain=DOMAIN_TEMPLATE % ", ('ttype', 'in', ('date', 'datetime'))"
)
domain = fields.Char(
"Filter Domain", required=True, default="[]",
help="Domain for filtering records. General rule, not user depending,"
" e.g. [('state', '=', 'done')]. The expression can contain"
" reference to 'user' which is a browse record of the current"
" user if not in batch mode.")
batch_mode = fields.Boolean("Batch Mode", help="Evaluate the expression in batch instead of once for each user")
batch_distinctive_field = fields.Many2one('ir.model.fields', string="Distinctive field for batch user", help="In batch mode, this indicates which field distinguishes one user from the other, e.g. user_id, partner_id...")
batch_user_expression = fields.Char("Evaluated expression for batch mode", help="The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id...")
compute_code = fields.Text("Python Code", help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id.")
condition = fields.Selection([
('higher', "The higher the better"),
('lower', "The lower the better")
], default='higher', required=True, string="Goal Performance",
help="A goal is considered as completed when the current value is compared to the value to reach")
action_id = fields.Many2one('ir.actions.act_window', string="Action", help="The action that will be called to update the goal value.")
res_id_field = fields.Char("ID Field of user", help="The field name on the user profile (res.users) containing the value for res_id for action.")
@api.depends('suffix', 'monetary') # also depends of user...
def _compute_full_suffix(self):
for goal in self:
items = []
if goal.monetary:
items.append(self.env.company.currency_id.symbol or u'¤')
if goal.suffix:
items.append(goal.suffix)
goal.full_suffix = u' '.join(items)
def _check_domain_validity(self):
# take admin as should always be present
for definition in self:
if definition.computation_mode not in ('count', 'sum'):
continue
Obj = self.env[definition.model_id.model]
try:
domain = safe_eval(definition.domain, {
'user': self.env.user.with_user(self.env.user)
})
# dummy search to make sure the domain is valid
Obj.search_count(domain)
except (ValueError, SyntaxError) as e:
msg = e
if isinstance(e, SyntaxError):
msg = (e.msg + '\n' + e.text)
raise exceptions.UserError(_("The domain for the definition %s seems incorrect, please check it.\n\n%s") % (definition.name, msg))
return True
def _check_model_validity(self):
""" make sure the selected field and model are usable"""
for definition in self:
try:
if not (definition.model_id and definition.field_id):
continue
Model = self.env[definition.model_id.model]
field = Model._fields.get(definition.field_id.name)
if not (field and field.store):
raise exceptions.UserError(_(
"The model configuration for the definition %(name)s seems incorrect, please check it.\n\n%(field_name)s not stored",
name=definition.name,
field_name=definition.field_id.name
))
except KeyError as e:
raise exceptions.UserError(_(
"The model configuration for the definition %(name)s seems incorrect, please check it.\n\n%(error)s not found",
name=definition.name,
error=e
))
@api.model_create_multi
def create(self, vals_list):
definitions = super(GoalDefinition, self).create(vals_list)
definitions.filtered_domain([
('computation_mode', 'in', ['count', 'sum']),
])._check_domain_validity()
definitions.filtered_domain([
('field_id', '=', 'True'),
])._check_model_validity()
return definitions
def write(self, vals):
res = super(GoalDefinition, self).write(vals)
if vals.get('computation_mode', 'count') in ('count', 'sum') and (vals.get('domain') or vals.get('model_id')):
self._check_domain_validity()
if vals.get('field_id') or vals.get('model_id') or vals.get('batch_mode'):
self._check_model_validity()
return res
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="badge_user_kanban_view" model="ir.ui.view" >
<field name="name">Badge User Kanban View</field>
<field name="model">gamification.badge.user</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="badge_name"/>
<field name="badge_id"/>
<field name="user_id"/>
<field name="comment"/>
<field name="create_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white o_kanban_gamification">
<div class="o_kanban_content">
<div class="o_kanban_image">
<a type="open"><img t-att-src="kanban_image('gamification.badge', 'image_1024', record.badge_id.raw_value)" t-att-title="record.badge_name.value" t-att-alt="record.badge_name.value" /></a>
</div>
<div class="oe_kanban_details">
<h4 class="mt0 mb0">
<a class="o_kanban_record_title" type="open"><t t-esc="record.badge_name.raw_value" /></a>
</h4>
<t t-if="record.comment.raw_value">
<p class="o_kanban_record_subtitle"><em><field name="comment"/></em></p>
</t>
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="moment(record.create_date.raw_value).format('L')" /></p>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</data></odoo>
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Badge views -->
<record id="gamification_badge_view_search" model="ir.ui.view">
@@ -162,41 +163,4 @@
</kanban>
</field>
</record>
<!-- Badge user viewss -->
<record id="badge_user_kanban_view" model="ir.ui.view" >
<field name="name">Badge User Kanban View</field>
<field name="model">gamification.badge.user</field>
<field name="arch" type="xml">
<kanban class="oe_background_grey">
<field name="badge_name"/>
<field name="badge_id"/>
<field name="user_id"/>
<field name="comment"/>
<field name="create_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white o_kanban_gamification">
<div class="o_kanban_content">
<div class="o_kanban_image">
<a type="open"><img t-att-src="kanban_image('gamification.badge', 'image_1024', record.badge_id.raw_value)" t-att-title="record.badge_name.value" t-att-alt="record.badge_name.value" /></a>
</div>
<div class="oe_kanban_details">
<h4 class="mt0 mb0">
<a class="o_kanban_record_title" type="open"><t t-esc="record.badge_name.raw_value" /></a>
</h4>
<t t-if="record.comment.raw_value">
<p class="o_kanban_record_subtitle"><em><field name="comment"/></em></p>
</t>
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="moment(record.create_date.raw_value).format('L')" /></p>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="challenge_line_list_view" model="ir.ui.view">
<field name="name">Challenge line list</field>
<field name="model">gamification.challenge.line</field>
<field name="arch" type="xml">
<tree string="Challenge Lines" >
<field name="definition_id"/>
<field name="target_goal" string="Target"/>
</tree>
</field>
</record>
</data></odoo>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="challenge_list_view" model="ir.ui.view">
<field name="name">Challenges List</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<tree string="Goal definitions" decoration-info="state == 'draft'" decoration-muted="state == 'done'">
<tree string="Challenges" decoration-info="state == 'draft'" decoration-muted="state == 'done'">
<field name="name"/>
<field name="period"/>
<field name="manager_id"/>
@@ -12,26 +13,11 @@
</field>
</record>
<record id="goals_from_challenge_act" model="ir.actions.act_window">
<field name="res_model">gamification.goal</field>
<field name="name">Related Goals</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No goal found
</p><p>
There is no goal associated to this challenge matching your search.
Make sure that your challenge is active and assigned to at least one user.
</p>
</field>
</record>
<record id="challenge_form_view" model="ir.ui.view">
<field name="name">Challenge Form</field>
<field name="model">gamification.challenge</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<form string="Challenge">
<header>
<button string="Start Challenge" type="object" name="action_start" states="draft" class="oe_highlight"/>
<button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/>
@@ -41,11 +27,17 @@
<sheet>
<!-- action buttons -->
<div class="oe_button_box" name="button_box">
<button type="object"
name="action_view_users"
class="oe_stat_button"
icon="fa-users">
<field name="user_count" string="Participants" widget="statinfo"/>
</button>
<button type="action"
name="%(goals_from_challenge_act)d"
class="oe_stat_button"
icon="fa-gift"
attrs="{'invisible': [('state','=','draft')]}">
name="%(goals_from_challenge_act)d"
class="oe_stat_button"
icon="fa-gift"
attrs="{'invisible': [('state','=','draft')]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Related</span>
<span class="o_stat_text">Goals</span>
@@ -60,7 +52,6 @@
<label for="user_domain" string="Assign Challenge to"/>
<div>
<field name="user_domain" widget="domain" options="{'model': 'res.users'}" />
<field name="user_ids" widget="many2many_tags" />
</div>
</div>
<group>
@@ -141,7 +132,6 @@
<field name="arch" type="xml">
<kanban string="Challenges" class="oe_background_grey">
<field name="line_ids"/>
<field name="user_ids"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card o_kanban_gamification oe_kanban_global_click">
@@ -149,15 +139,16 @@
<strong>
<h4 class="o_kanban_record_title"><field name="name"/></h4>
<div class="o_kanban_record_subtitle">
<a type="action" name="%(goals_from_challenge_act)d" style="margin-right: 10px" tabindex="-1">
<span t-if="record.line_ids.raw_value.length gt 1"><t t-esc="record.line_ids.raw_value.length"/> Goals</span>
<span t-if="record.line_ids.raw_value.length lt 2"><t t-esc="record.line_ids.raw_value.length"/> Goal</span>
</a>
<a type="action" name="%(goals_from_challenge_act)d"
style="margin-right: 10px" tabindex="-1">
<span><t t-esc="record.line_ids.raw_value.length"/> Goals</span>
</a><br />
<a type="object" name="action_view_users"
style="margin-right: 10px" tabindex="-1">
<span><field name="user_count"/> Participants</span>
</a>
</div>
</strong>
<t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
<img class="o_kanban_badge_avatars" t-att-src="kanban_image('res.users', 'avatar_128', member)" t-att-data-member_id="member" alt="Member"/>
</t>
</div>
</div>
</t>
@@ -195,19 +186,6 @@
<field name="view_id" ref="challenge_form_view"/>
</record>
<!-- Line -->
<record id="challenge_line_list_view" model="ir.ui.view">
<field name="name">Challenge line list</field>
<field name="model">gamification.challenge.line</field>
<field name="arch" type="xml">
<tree string="Challenge Lines" >
<field name="definition_id"/>
<field name="target_goal" string="Target"/>
</tree>
</field>
</record>
<record id="challenge_search_view" model="ir.ui.view">
<field name="name">Challenge Search</field>
<field name="model">gamification.challenge</field>
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="goal_definition_list_action" model="ir.actions.act_window">
<field name="name">Goal Definitions</field>
<field name="res_model">gamification.goal.definition</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new goal definition
</p><p>
A goal definition is a technical specification of a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_definition_list_view" model="ir.ui.view">
<field name="name">Goal Definitions List</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<tree string="Goal Definitions">
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_definition_form_view" model="ir.ui.view">
<field name="name">Goal Definitions Form</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<sheet>
<label for="name"/>
<h1>
<field name="name" placeholder="e.g. Get started"/>
</h1>
<label for="description"/>
<div>
<field name="description" placeholder="e.g. Register to the platform" class="oe_inline"/>
</div>
<group string="How is the goal computed?" name="compute_details">
<field widget="radio" name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}"/>
<field name="model_inherited_ids" invisible="1"/>
<field name="field_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}"/>
<field name="field_date_id" class="oe_inline" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}"/>
<field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
<field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}"/>
<field name="condition" widget="radio"/>
</group>
<group string="Optimisation" name="optimisation" attrs="{'invisible': [('computation_mode', 'not in', ('sum', 'count'))]}">
<field name="batch_mode" />
<div colspan="2">In batch mode, the domain is evaluated globally. If enabled, do not use keyword 'user' in above filter domain.</div>
<field name="batch_distinctive_field" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}"
domain="[('model_id', '=', model_id)]" class="oe_inline" />
<field name="batch_user_expression" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}" class="oe_inline"
placeholder="e.g. user.partner_id.id"/>
</group>
<group string="Formatting Options" name="format_options">
<field name="display_mode" widget="radio" />
<field name="suffix" placeholder="e.g. days" class="oe_inline"/>
<field name="monetary"/>
</group>
<group string="Clickable Goals" name="clickable_goals" attrs="{'invisible': [('computation_mode', '=', 'manually')]}">
<field name="action_id" class="oe_inline"/>
<field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_definition_search_view" model="ir.ui.view">
<field name="name">Goal Definition Search</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<search string="Search Goal Definitions">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By">
<filter string="Model" name="model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" name="computationmode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</search>
</field>
</record>
</odoo>
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Goal views -->
<record id="goal_list_action" model="ir.actions.act_window">
<field name="name">Goals</field>
@@ -15,6 +17,21 @@
</field>
</record>
<record id="goals_from_challenge_act" model="ir.actions.act_window">
<field name="res_model">gamification.goal</field>
<field name="name">Related Goals</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No goal found
</p><p>
There is no goal associated to this challenge matching your search.
Make sure that your challenge is active and assigned to at least one user.
</p>
</field>
</record>
<record id="goal_list_view" model="ir.ui.view">
<field name="name">Goal List</field>
<field name="model">gamification.goal</field>
@@ -192,109 +209,4 @@
</kanban>
</field>
</record>
<!-- Goal definitions view -->
<record id="goal_definition_list_action" model="ir.actions.act_window">
<field name="name">Goal Definitions</field>
<field name="res_model">gamification.goal.definition</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new goal definition
</p><p>
A goal definition is a technical specification of a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_definition_list_view" model="ir.ui.view">
<field name="name">Goal Definitions List</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<tree string="Goal Definitions">
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_definition_form_view" model="ir.ui.view">
<field name="name">Goal Definitions Form</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<form string="Goal definitions">
<sheet>
<label for="name"/>
<h1>
<field name="name" placeholder="e.g. Get started"/>
</h1>
<label for="description"/>
<div>
<field name="description" placeholder="e.g. Register to the platform" class="oe_inline"/>
</div>
<group string="How is the goal computed?" name="compute_details">
<field widget="radio" name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}"/>
<field name="model_inherited_ids" invisible="1"/>
<field name="field_id" class="oe_inline"
attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}"/>
<field name="field_date_id" class="oe_inline" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}"/>
<field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
<field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}"/>
<field name="condition" widget="radio"/>
</group>
<group string="Optimisation" name="optimisation" attrs="{'invisible': [('computation_mode', 'not in', ('sum', 'count'))]}">
<field name="batch_mode" />
<div colspan="2">In batch mode, the domain is evaluated globally. If enabled, do not use keyword 'user' in above filter domain.</div>
<field name="batch_distinctive_field" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}"
domain="[('model_id', '=', model_id)]" class="oe_inline" />
<field name="batch_user_expression" attrs="{'invisible': [('batch_mode', '=', False)], 'required': [('batch_mode', '=', True)]}" class="oe_inline"
placeholder="e.g. user.partner_id.id"/>
</group>
<group string="Formatting Options" name="format_options">
<field name="display_mode" widget="radio" />
<field name="suffix" placeholder="e.g. days" class="oe_inline"/>
<field name="monetary"/>
</group>
<group string="Clickable Goals" name="clickable_goals" attrs="{'invisible': [('computation_mode', '=', 'manually')]}">
<field name="action_id" class="oe_inline"/>
<field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_definition_search_view" model="ir.ui.view">
<field name="name">Goal Definition Search</field>
<field name="model">gamification.goal.definition</field>
<field name="arch" type="xml">
<search string="Search Goal Definitions">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By">
<filter string="Model" name="model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" name="computationmode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</search>
</field>
</record>
<!-- menus in settings - technical feature required -->
<menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
<menuitem id="gamification_challenge_menu" parent="gamification_menu" action="challenge_list_action" sequence="0"/>
<menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="10"/>
<menuitem id="gamification_definition_menu" parent="gamification_menu" action="goal_definition_list_action" sequence="20"/>
<menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
</odoo>
@@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- ACTIONS -->
<record id="action_current_rank_users" model="ir.actions.act_window">
<field name="name">Users</field>
<field name="res_model">res.users</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('rank_id', '=', active_id)]</field>
</record>
<!-- Ranks views -->
<record id="gamification_karma_ranks_action" model="ir.actions.act_window">
@@ -88,7 +81,5 @@
</form>
</field>
</record>
<menuitem id="gamification_karma_ranks_menu" parent="gamification_menu" action="gamification_karma_ranks_action" sequence="40"/>
</data>
</odoo>
@@ -54,10 +54,5 @@
<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>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- menus in settings - technical feature required -->
<menuitem id="gamification_menu"
name="Gamification Tools"
parent="base.menu_administration"
groups="base.group_no_one" />
<menuitem id="gamification_challenge_menu"
parent="gamification_menu"
action="challenge_list_action"
sequence="0"/>
<menuitem id="gamification_goal_menu"
parent="gamification_menu"
action="goal_list_action"
sequence="10"/>
<menuitem id="gamification_definition_menu"
parent="gamification_menu"
action="goal_definition_list_action"
sequence="20"/>
<menuitem id="gamification_badge_menu"
parent="gamification_menu"
action="badge_list_action"
sequence="30"/>
<menuitem id="gamification_karma_ranks_menu"
parent="gamification_menu"
action="gamification_karma_ranks_action"
sequence="40"/>
<menuitem id="gamification_karma_tracking_menu"
parent="gamification_menu"
action="gamification_karma_tracking_action"
sequence="50"/>
</odoo>
@@ -13,5 +13,23 @@
</field>
</record>
<!-- ACTIONS -->
<record id="action_current_rank_users" model="ir.actions.act_window">
<field name="name">Users</field>
<field name="res_model">res.users</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('rank_id', '=', active_id)]</field>
</record>
<record id="action_new_simplified_res_users" model="ir.actions.act_window">
<field name="name">Create User</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.users</field>
<field name="target">current</field>
<field name="view_id" ref="base.view_users_simple_form"/>
<field name="context">{}</field>
<field name="help">Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.</field>
</record>
</data>
</odoo>