diff --git a/addons/gamification/__openerp__.py b/addons/gamification/__openerp__.py
index 789c89affe9..bc136a8fa67 100644
--- a/addons/gamification/__openerp__.py
+++ b/addons/gamification/__openerp__.py
@@ -33,6 +33,4 @@ Both goals and badges are flexibles and can be adapted to a large range of modul
'data/badge.xml',
'views/gamification.xml',
],
- 'application': False,
- 'auto_install': False,
}
diff --git a/addons/gamification/data/badge.xml b/addons/gamification/data/badge.xml
index ed0c03e13ee..a928a342122 100644
--- a/addons/gamification/data/badge.xml
+++ b/addons/gamification/data/badge.xml
@@ -1,5 +1,4 @@
-
-
+Good Job
@@ -58,4 +57,4 @@
]]>
-
+
diff --git a/addons/gamification/data/cron.xml b/addons/gamification/data/cron.xml
index 105f7d25681..6e273ef4405 100644
--- a/addons/gamification/data/cron.xml
+++ b/addons/gamification/data/cron.xml
@@ -1,5 +1,4 @@
-
-
+
@@ -13,4 +12,4 @@
()
-
\ No newline at end of file
+
diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
index cac0068b7da..59e2bc2b523 100644
--- a/addons/gamification/data/goal_base.xml
+++ b/addons/gamification/data/goal_base.xml
@@ -1,5 +1,4 @@
-
-
+
@@ -163,7 +162,7 @@
oncepersonalnever
-
+ inprogressother
@@ -173,7 +172,7 @@
oncepersonalnever
-
+ inprogressother
@@ -202,4 +201,4 @@
-
+
diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py
index 59ccdfdd1b8..098cfde66d8 100644
--- a/addons/gamification/models/badge.py
+++ b/addons/gamification/models/badge.py
@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import SUPERUSER_ID
-from openerp.osv import fields, osv
-from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
-from openerp.tools.translate import _
-
-from datetime import date
import logging
-from openerp.exceptions import UserError
+from datetime import date
+
+from odoo import api, fields, models, _, exceptions
_logger = logging.getLogger(__name__)
-class gamification_badge_user(osv.Model):
+class BadgeUser(models.Model):
"""User having received a badge"""
_name = 'gamification.badge.user'
@@ -20,19 +16,16 @@ class gamification_badge_user(osv.Model):
_order = "create_date desc"
_rec_name = "badge_name"
- _columns = {
- 'user_id': fields.many2one('res.users', string="User", required=True, ondelete="cascade", select=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", select=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.related('badge_id', 'name', type="char", string="Badge Name"),
- 'create_date': fields.datetime('Created', readonly=True),
- 'create_uid': fields.many2one('res.users', string='Creator', readonly=True),
- }
+ user_id = fields.Many2one('res.users', string="User", required=True, ondelete="cascade", select=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", select=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")
+ create_date = fields.Datetime('Created', readonly=True)
+ create_uid = fields.Many2one('res.users', string='Creator', readonly=True)
-
- def _send_badge(self, cr, uid, ids, context=None):
+ def _send_badge(self):
"""Send a notification to a user for receiving a badge
Does not verify constrains on badge granting.
@@ -40,27 +33,29 @@ class gamification_badge_user(osv.Model):
The stats counters are incremented
:param ids: list(int) of badge users that will receive the badge
"""
- res = True
- temp_obj = self.pool.get('mail.template')
- user_obj = self.pool.get('res.users')
- template_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'gamification', 'email_template_badge_received')[1]
- for badge_user in self.browse(cr, uid, ids, context=context):
- template = temp_obj.get_email_template(cr, uid, template_id, badge_user.id, context=context)
- body_html = temp_obj.render_template(cr, uid, template.body_html, 'gamification.badge.user', badge_user.id, context=template._context)
- res = user_obj.message_post(
- cr, uid, badge_user.user_id.id,
+ Template = self.env['mail.template']
+ template_id = self.env.ref('gamification.email_template_badge_received')
+
+ for badge_user in self:
+ # .ids would trigger the "multi mode" which returns a mapping of
+ # res_id to templates
+ template = template_id.get_email_template(badge_user.id)
+ body_html = Template.with_context(template._context).render_template(template.body_html, 'gamification.badge.user', badge_user.ids)
+ badge_user.user_id.message_post(
body=body_html,
subtype='gamification.mt_badge_granted',
- partner_ids=[badge_user.user_id.partner_id.id],
- context=context)
- return res
+ partner_ids=badge_user.user_id.partner_id.ids
+ )
- def create(self, cr, uid, vals, context=None):
- self.pool.get('gamification.badge').check_granting(cr, uid, badge_id=vals.get('badge_id'), context=context)
- return super(gamification_badge_user, self).create(cr, uid, vals, context=context)
+ return True
+
+ @api.model
+ def create(self, vals):
+ self.env['gamification.badge'].browse(vals['badge_id']).check_granting()
+ return super(BadgeUser, self).create(vals)
-class gamification_badge(osv.Model):
+class GamificationBadge(models.Model):
"""Badge object that users can send and receive"""
CAN_GRANT = 1
@@ -73,204 +68,180 @@ class gamification_badge(osv.Model):
_description = 'Gamification badge'
_inherit = ['mail.thread']
- def _get_owners_info(self, cr, uid, ids, name, args, context=None):
+ name = fields.Char('Badge', required=True, translate=True)
+ active = fields.Boolean('Active', default=True)
+ description = fields.Text('Description', translate=True)
+ image = fields.Binary("Image", attachment=True, help="This field holds the image used for the badge, limited to 256x256")
+
+ rule_auth = fields.Selection([
+ ('everyone', 'Everyone'),
+ ('users', 'A selected list of users'),
+ ('having', 'People having some badges'),
+ ('nobody', 'No one, assigned through challenges'),
+ ], default='everyone',
+ string="Allowance to Grant", help="Who can grant this badge", required=True)
+ rule_auth_user_ids = fields.Many2many(
+ 'res.users', 'rel_badge_auth_users',
+ string='Authorized Users',
+ help="Only these people can give this badge")
+ rule_auth_badge_ids = fields.Many2many(
+ 'gamification.badge', 'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id',
+ string='Required Badges',
+ help="Only the people having these badges can give this badge")
+
+ rule_max = fields.Boolean('Monthly Limited Sending', help="Check to set a monthly limit per person of sending this badge")
+ rule_max_number = fields.Integer('Limitation Number', help="The maximum number of time this badge can be sent per month per person.")
+ challenge_ids = fields.One2many('gamification.challenge', 'reward_id', string="Reward of Challenges")
+
+ goal_definition_ids = fields.Many2many(
+ 'gamification.goal.definition', 'badge_unlocked_definition_rel',
+ string='Rewarded by', help="The users that have succeeded theses goals will receive automatically the badge.")
+
+ owner_ids = fields.One2many(
+ 'gamification.badge.user', 'badge_id',
+ string='Owners', help='The list of instances of this badge granted to users')
+
+ stat_count = fields.Integer("Total", compute='_get_owners_info', help="The number of time this badge has been received.")
+ stat_count_distinct = fields.Integer("Number of users", compute='_get_owners_info', help="The number of time this badge has been received by unique users.")
+ unique_owner_ids = fields.Many2many(
+ 'res.users', string="Unique Owners", compute='_get_owners_info',
+ help="The list of unique users having received this badge.")
+
+ stat_this_month = fields.Integer(
+ "Monthly total", compute='_get_badge_user_stats',
+ help="The number of time this badge has been received this month.")
+ stat_my = fields.Integer(
+ "My Total", compute='_get_badge_user_stats',
+ help="The number of time the current user has received this badge.")
+ stat_my_this_month = fields.Integer(
+ "My Monthly Total", compute='_get_badge_user_stats',
+ help="The number of time the current user has received this badge this month.")
+ stat_my_monthly_sending = fields.Integer(
+ 'My Monthly Sending Total',
+ compute='_get_badge_user_stats',
+ help="The number of time the current user has sent this badge this month.")
+
+ remaining_sending = fields.Integer(
+ "Remaining Sending Allowed", compute='_remaining_sending_calc',
+ help="If a maximum is set")
+
+ @api.depends('owner_ids')
+ def _get_owners_info(self):
"""Return:
the list of unique res.users ids having received this badge
the total number of time this badge was granted
the total number of users this badge was granted to
"""
- result = dict((res_id, {'stat_count': 0, 'stat_count_distinct': 0, 'unique_owner_ids': []}) for res_id in ids)
-
- cr.execute("""
+ self.env.cr.execute("""
SELECT badge_id, count(user_id) as stat_count,
count(distinct(user_id)) as stat_count_distinct,
array_agg(distinct(user_id)) as unique_owner_ids
FROM gamification_badge_user
WHERE badge_id in %s
GROUP BY badge_id
- """, (tuple(ids),))
- for (badge_id, stat_count, stat_count_distinct, unique_owner_ids) in cr.fetchall():
- result[badge_id] = {
- 'stat_count': stat_count,
- 'stat_count_distinct': stat_count_distinct,
- 'unique_owner_ids': unique_owner_ids,
- }
- return result
+ """, [tuple(self.ids)])
- def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
+ defaults = {
+ 'stat_count': 0,
+ 'stat_count_distinct': 0,
+ 'unique_owner_ids': [],
+ }
+ mapping = {
+ badge_id: {
+ 'stat_count': count,
+ 'stat_count_distinct': distinct_count,
+ 'unique_owner_ids': owner_ids,
+ }
+ for (badge_id, count, distinct_count, owner_ids) in self.env.cr._obj
+ }
+ for badge in self:
+ badge.update(mapping.get(badge.id, defaults))
+
+ @api.depends('owner_ids.badge_id', 'owner_ids.create_date', 'owner_ids.user_id')
+ def _get_badge_user_stats(self):
"""Return stats related to badge users"""
- result = dict.fromkeys(ids, False)
- badge_user_obj = self.pool.get('gamification.badge.user')
- first_month_day = date.today().replace(day=1).strftime(DF)
- for bid in ids:
- result[bid] = {
- 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True),
- 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True),
- 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True),
- 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True)
- }
- return result
+ first_month_day = fields.Date.to_string(date.today().replace(day=1))
- def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
+ for badge in self:
+ owners = badge.owner_ids
+ badge.stats_my = sum(o.user_id == self.env.user for o in owners)
+ badge.stats_this_month = sum(o.create_date >= first_month_day for o in owners)
+ badge.stats_my_this_month = sum(
+ o.user_id == self.env.user and o.create_date >= first_month_day
+ for o in owners
+ )
+ badge.stats_my_monthly_sending = sum(
+ o.create_uid == self.env.user and o.create_date >= first_month_day
+ for o in owners
+ )
+
+ @api.depends(
+ 'rule_auth',
+ 'rule_auth_user_ids',
+ 'rule_auth_badge_ids',
+ 'rule_max',
+ 'rule_max_number',
+ 'stat_my_monthly_sending',
+ )
+ def _remaining_sending_calc(self):
"""Computes the number of badges remaining the user can send
0 if not allowed or no remaining
integer if limited sending
-1 if infinite (should not be displayed)
"""
- result = dict.fromkeys(ids, False)
- for badge in self.browse(cr, uid, ids, context=context):
- if self._can_grant_badge(cr, uid, badge.id, context) != 1:
+ for badge in self:
+ if badge._can_grant_badge() != self.CAN_GRANT:
# if the user cannot grant this badge at all, result is 0
- result[badge.id] = 0
+ badge.remaining_sending = 0
elif not badge.rule_max:
# if there is no limitation, -1 is returned which means 'infinite'
- result[badge.id] = -1
+ badge.remaining_sending = -1
else:
- result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
- return result
+ badge.remaining_sending = badge.rule_max_number - badge.stat_my_monthly_sending
- _columns = {
- 'name': fields.char('Badge', required=True, translate=True),
- 'description': fields.text('Description', translate=True),
- 'image': fields.binary("Image", attachment=True,
- help="This field holds the image used for the badge, limited to 256x256"),
- 'rule_auth': fields.selection([
- ('everyone', 'Everyone'),
- ('users', 'A selected list of users'),
- ('having', 'People having some badges'),
- ('nobody', 'No one, assigned through challenges'),
- ],
- string="Allowance to Grant",
- help="Who can grant this badge",
- required=True),
- 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
- string='Authorized Users',
- help="Only these people can give this badge"),
- 'rule_auth_badge_ids': fields.many2many('gamification.badge',
- 'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id',
- string='Required Badges',
- help="Only the people having these badges can give this badge"),
-
- 'rule_max': fields.boolean('Monthly Limited Sending',
- help="Check to set a monthly limit per person of sending this badge"),
- 'rule_max_number': fields.integer('Limitation Number',
- help="The maximum number of time this badge can be sent per month per person."),
- 'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
- type="integer",
- string='My Monthly Sending Total',
- multi='badge_users',
- help="The number of time the current user has sent this badge this month."),
- 'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
- string='Remaining Sending Allowed', help="If a maxium is set"),
-
- 'challenge_ids': fields.one2many('gamification.challenge', 'reward_id',
- string="Reward of Challenges"),
-
- 'goal_definition_ids': fields.many2many('gamification.goal.definition', 'badge_unlocked_definition_rel',
- string='Rewarded by',
- help="The users that have succeeded theses goals will receive automatically the badge."),
-
- 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
- string='Owners', help='The list of instances of this badge granted to users'),
- 'active': fields.boolean('Active'),
- 'unique_owner_ids': fields.function(_get_owners_info,
- string='Unique Owners',
- help="The list of unique users having received this badge.",
- multi='unique_users',
- type="many2many", relation="res.users"),
-
- 'stat_count': fields.function(_get_owners_info, string='Total',
- type="integer",
- multi='stat_users',
- help="The number of time this badge has been received."),
- 'stat_count_distinct': fields.function(_get_owners_info,
- type="integer",
- string='Number of users',
- multi='stat_users',
- help="The number of time this badge has been received by unique users."),
- 'stat_this_month': fields.function(_get_badge_user_stats,
- type="integer",
- string='Monthly total',
- multi='badge_users',
- help="The number of time this badge has been received this month."),
- 'stat_my': fields.function(_get_badge_user_stats, string='My Total',
- type="integer",
- multi='badge_users',
- help="The number of time the current user has received this badge."),
- 'stat_my_this_month': fields.function(_get_badge_user_stats,
- type="integer",
- string='My Monthly Total',
- multi='badge_users',
- help="The number of time the current user has received this badge this month."),
- }
-
- _defaults = {
- 'rule_auth': 'everyone',
- 'active': True,
- }
-
- def check_granting(self, cr, uid, badge_id, context=None):
+ def check_granting(self):
"""Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception
if not
Do not check for SUPERUSER_ID
"""
- status_code = self._can_grant_badge(cr, uid, badge_id, context=context)
+ status_code = self._can_grant_badge()
if status_code == self.CAN_GRANT:
return True
elif status_code == self.NOBODY_CAN_GRANT:
- raise UserError(_('This badge can not be sent by users.'))
+ raise exceptions.UserError(_('This badge can not be sent by users.'))
elif status_code == self.USER_NOT_VIP:
- raise UserError(_('You are not in the user allowed list.'))
+ raise exceptions.UserError(_('You are not in the user allowed list.'))
elif status_code == self.BADGE_REQUIRED:
- raise UserError(_('You do not have the required badges.'))
+ raise exceptions.UserError(_('You do not have the required badges.'))
elif status_code == self.TOO_MANY:
- raise UserError(_('You have already sent this badge too many time this month.'))
+ raise exceptions.UserError(_('You have already sent this badge too many time this month.'))
else:
- _logger.exception("Unknown badge status code: %d" % int(status_code))
+ _logger.error("Unknown badge status code: %s" % status_code)
return False
- def _can_grant_badge(self, cr, uid, badge_id, context=None):
+ def _can_grant_badge(self):
"""Check if a user can grant a badge to another user
:param uid: the id of the res.users trying to send the badge
:param badge_id: the granted badge id
:return: integer representing the permission.
"""
- if uid == SUPERUSER_ID:
+ if self.env.user._is_admin():
return self.CAN_GRANT
- badge = self.browse(cr, uid, badge_id, context=context)
-
- if badge.rule_auth == 'nobody':
+ if self.rule_auth == 'nobody':
return self.NOBODY_CAN_GRANT
-
- elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]:
+ elif self.rule_auth == 'users' and self.env.user not in self.rule_auth_user_ids:
return self.USER_NOT_VIP
+ elif self.rule_auth == 'having':
+ all_user_badges = self.env['gamification.badge.user'].search([('user_id', '=', self.env.uid)])
+ if self.rule_auth_badge_ids - all_user_badges:
+ return self.BADGE_REQUIRED
- elif badge.rule_auth == 'having':
- all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context)
- for required_badge in badge.rule_auth_badge_ids:
- if required_badge.id not in all_user_badges:
- return self.BADGE_REQUIRED
-
- if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
+ if self.rule_max and self.stat_my_monthly_sending >= self.rule_max_number:
return self.TOO_MANY
# badge.rule_auth == 'everyone' -> no check
return self.CAN_GRANT
-
- def check_progress(self, cr, uid, context=None):
- try:
- model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden')
- except ValueError:
- return True
- badge_user_obj = self.pool.get('gamification.badge.user')
- if not badge_user_obj.search(cr, uid, [('user_id', '=', uid), ('badge_id', '=', res_id)], context=context):
- values = {
- 'user_id': uid,
- 'badge_id': res_id,
- }
- badge_user_obj.create(cr, SUPERUSER_ID, values, context=context)
- return True
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index 1a5446247f9..028e58e2173 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -1,16 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from openerp import SUPERUSER_ID
-from openerp.osv import fields, osv
-from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT as DF
-from openerp.tools.safe_eval import safe_eval as eval
-from openerp.tools.translate import _
-
-from datetime import date, datetime, timedelta
-import calendar
+import itertools
import logging
-from openerp.exceptions import UserError
+from datetime import date, timedelta
+
+from dateutil.relativedelta import relativedelta, MO
+
+from odoo import api, models, fields, _, exceptions
+from odoo.tools.safe_eval import safe_eval
+
_logger = logging.getLogger(__name__)
# display top 3 in ranking, could be db variable
@@ -29,13 +27,11 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date
start_date = today
end_date = start_date
elif period == 'weekly':
- delta = timedelta(days=today.weekday())
- start_date = today - delta
+ start_date = today + relativedelta(weekday=MO(-1))
end_date = start_date + timedelta(days=7)
elif period == 'monthly':
- month_range = calendar.monthrange(today.year, today.month)
start_date = today.replace(day=1)
- end_date = today.replace(day=month_range[1])
+ end_date = today + relativedelta(months=1, day=1, days=-1)
elif period == 'yearly':
start_date = today.replace(month=1, day=1)
end_date = today.replace(month=12, day=31)
@@ -45,9 +41,9 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date
return (start_date, end_date)
- return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF))
+ return fields.Datetime.to_string(start_date), fields.Datetime.to_string(end_date)
-class gamification_challenge(osv.Model):
+class Challenge(models.Model):
"""Gamification challenge
Set of predifined objectives assigned to people with rules for recurrence and
@@ -61,233 +57,192 @@ class gamification_challenge(osv.Model):
_name = 'gamification.challenge'
_description = 'Gamification challenge'
_inherit = 'mail.thread'
-
- def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
- """Return the next report date based on the last report date and report
- period.
-
- :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
- res = {}
- for challenge in self.browse(cr, uid, ids, context=context):
- last = datetime.strptime(challenge.last_report_date, DF).date()
- if challenge.report_message_frequency == 'daily':
- next = last + timedelta(days=1)
- res[challenge.id] = next.strftime(DF)
- elif challenge.report_message_frequency == 'weekly':
- next = last + timedelta(days=7)
- res[challenge.id] = next.strftime(DF)
- elif challenge.report_message_frequency == 'monthly':
- month_range = calendar.monthrange(last.year, last.month)
- next = last.replace(day=month_range[1]) + timedelta(days=1)
- res[challenge.id] = next.strftime(DF)
- elif challenge.report_message_frequency == 'yearly':
- res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
- # frequency == 'once', reported when closed only
- else:
- res[challenge.id] = False
-
- return res
-
- def _get_categories(self, cr, uid, context=None):
- return [
- ('hr', 'Human Resources / Engagement'),
- ('other', 'Settings / Gamification Tools'),
- ]
-
- def _get_report_template(self, cr, uid, context=None):
- try:
- return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
- except ValueError:
- return False
-
_order = 'end_date, start_date, name, id'
- _columns = {
- 'name': fields.char('Challenge Name', required=True, translate=True),
- 'description': fields.text('Description', translate=True),
- 'state': fields.selection([
- ('draft', 'Draft'),
- ('inprogress', 'In Progress'),
- ('done', 'Done'),
- ], copy=False,
- string='State', required=True, track_visibility='onchange'),
- 'manager_id': fields.many2one('res.users',
- string='Responsible', help="The user responsible for the challenge."),
- '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"),
+ name = fields.Char("Challenge Name", required=True, translate=True)
+ description = fields.Text("Description", translate=True)
+ state = fields.Selection([
+ ('draft', "Draft"),
+ ('inprogress', "In Progress"),
+ ('done', "Done"),
+ ], default='draft', copy=False,
+ string="State", required=True, track_visibility='onchange')
+ manager_id = fields.Many2one(
+ 'res.users', default=lambda self: self.env.uid,
+ string="Responsible", help="The user responsible for the challenge.",)
- 'period': fields.selection([
- ('once', 'Non recurring'),
- ('daily', 'Daily'),
- ('weekly', 'Weekly'),
- ('monthly', 'Monthly'),
- ('yearly', 'Yearly')
- ],
- string='Periodicity',
- help='Period of automatic goal assigment. If none is selected, should be launched manually.',
- required=True),
- 'start_date': fields.date('Start Date',
- help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
- 'end_date': fields.date('End Date',
- help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
+ 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")
- 'invited_user_ids': fields.many2many('res.users', 'gamification_invited_user_ids_rel',
- string="Suggest to users"),
+ period = fields.Selection([
+ ('once', "Non recurring"),
+ ('daily', "Daily"),
+ ('weekly', "Weekly"),
+ ('monthly', "Monthly"),
+ ('yearly', "Yearly")
+ ], default='once',
+ string="Periodicity",
+ help="Period of automatic goal assigment. If none is selected, should be launched manually.",
+ required=True)
+ start_date = fields.Date("Start Date", help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date.")
+ end_date = fields.Date("End Date", help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date.")
- 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
- string='Lines',
- help="List of goals that will be set",
- required=True, copy=True),
+ invited_user_ids = fields.Many2many('res.users', 'gamification_invited_user_ids_rel', string="Suggest to users")
- 'reward_id': fields.many2one('gamification.badge', string="For Every Succeeding User"),
- 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
- 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
- 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
- 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
- 'reward_realtime': fields.boolean('Reward as soon as every goal is reached',
- help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge."),
+ line_ids = fields.One2many('gamification.challenge.line', 'challenge_id',
+ string="Lines",
+ help="List of goals that will be set",
+ required=True, copy=True)
- 'visibility_mode': fields.selection([
- ('personal', 'Individual Goals'),
- ('ranking', 'Leader Board (Group Ranking)'),
- ],
- string="Display Mode", required=True),
+ reward_id = fields.Many2one('gamification.badge', string="For Every Succeeding User")
+ reward_first_id = fields.Many2one('gamification.badge', string="For 1st user")
+ reward_second_id = fields.Many2one('gamification.badge', string="For 2nd user")
+ reward_third_id = fields.Many2one('gamification.badge', string="For 3rd user")
+ reward_failure = fields.Boolean("Reward Bests if not Succeeded?")
+ reward_realtime = fields.Boolean("Reward as soon as every goal is reached", default=True, help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge.")
- 'report_message_frequency': fields.selection([
- ('never', 'Never'),
- ('onchange', 'On change'),
- ('daily', 'Daily'),
- ('weekly', 'Weekly'),
- ('monthly', 'Monthly'),
- ('yearly', 'Yearly')
- ],
- string="Report Frequency", required=True),
- 'report_message_group_id': fields.many2one('mail.channel',
- string='Send a copy to',
- help='Group that will receive a copy of the report in addition to the user'),
- 'report_template_id': fields.many2one('mail.template', string="Report Template", required=True),
- 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
- help="Never reminded if no value or zero is specified."),
- 'last_report_date': fields.date('Last Report Date'),
- 'next_report_date': fields.function(_get_next_report_date,
- type='date', string='Next Report Date', store=True),
+ visibility_mode = fields.Selection([
+ ('personal', "Individual Goals"),
+ ('ranking', "Leader Board (Group Ranking)"),
+ ], default='personal',
+ string="Display Mode", required=True)
- 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
- string="Appears in", help="Define the visibility of the challenge through menus", required=True),
- }
+ report_message_frequency = fields.Selection([
+ ('never', "Never"),
+ ('onchange', "On change"),
+ ('daily', "Daily"),
+ ('weekly', "Weekly"),
+ ('monthly', "Monthly"),
+ ('yearly', "Yearly")
+ ], default='never',
+ string="Report Frequency", required=True)
+ report_message_group_id = fields.Many2one('mail.channel', string="Send a copy to", help="Group that will receive a copy of the report in addition to the user")
+ report_template_id = fields.Many2one('mail.template', default=lambda self: self._get_report_template(), string="Report Template", required=True)
+ remind_update_delay = fields.Integer("Non-updated manual goals will be reminded after", help="Never reminded if no value or zero is specified.")
+ last_report_date = fields.Date("Last Report Date", default=fields.Date.today)
+ next_report_date = fields.Date("Next Report Date", compute='_get_next_report_date', store=True)
- _defaults = {
- 'period': 'once',
- 'state': 'draft',
- 'visibility_mode': 'personal',
- 'report_message_frequency': 'never',
- 'last_report_date': fields.date.today,
- 'manager_id': lambda s, cr, uid, c: uid,
- 'category': 'hr',
- 'reward_failure': False,
- 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
- 'reward_realtime': True,
+ category = fields.Selection([
+ ('hr', 'Human Resources / Engagement'),
+ ('other', 'Settings / Gamification Tools'),
+ ], string="Appears in", required=True, default='hr',
+ help="Define the visibility of the challenge through menus")
+
+ REPORT_OFFSETS = {
+ 'daily': timedelta(days=1),
+ 'weekly': timedelta(days=7),
+ 'monthly': relativedelta(months=1),
+ 'yearly': relativedelta(years=1),
}
+ @api.depends('last_report_date', 'report_message_frequency')
+ def _get_next_report_date(self):
+ """ Return the next report date based on the last report date and
+ report period.
+ """
+ for challenge in self:
+ last = fields.Datetime.from_string(challenge.last_report_date).date()
+ offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency)
+ if offset:
+ challenge.next_report_date = fields.Date.to_string(last + offset)
+ else:
+ challenge.next_report_date = False
- def create(self, cr, uid, vals, context=None):
+ def _get_report_template(self):
+ template = self.env.ref('gamification.simple_report_template', raise_if_not_found=False)
+
+ return template.id if template else False
+
+ @api.model
+ def create(self, vals):
"""Overwrite the create method to add the user of groups"""
if vals.get('user_domain'):
- user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
+ users = self._get_challenger_users(vals.get('user_domain'))
if not vals.get('user_ids'):
vals['user_ids'] = []
- vals['user_ids'] += [(4, user_id) for user_id in user_ids]
+ vals['user_ids'].extend((4, user.id) for user in users)
- return super(gamification_challenge, self).create(cr, uid, vals, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- if isinstance(ids, (int,long)):
- ids = [ids]
+ return super(Challenge, self).create(vals)
+ @api.multi
+ def write(self, vals):
if vals.get('user_domain'):
- user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
+ users = self._get_challenger_users(vals.get('user_domain'))
if not vals.get('user_ids'):
vals['user_ids'] = []
- vals['user_ids'] += [(4, user_id) for user_id in user_ids]
+ vals['user_ids'].extend((4, user.id) for user in users)
- write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
+ write_res = super(Challenge, self).write(vals)
if vals.get('report_message_frequency', 'never') != 'never':
# _recompute_challenge_users do not set users for challenges with no reports, subscribing them now
- for challenge in self.browse(cr, uid, ids, context=context):
- self.message_subscribe(cr, uid, [challenge.id], [user.partner_id.id for user in challenge.user_ids], context=context)
+ for challenge in self:
+ challenge.message_subscribe([user.partner_id.id for user in challenge.user_ids])
if vals.get('state') == 'inprogress':
- self._recompute_challenge_users(cr, uid, ids, context=context)
- self._generate_goals_from_challenge(cr, uid, ids, context=context)
+ self._recompute_challenge_users()
+ self._generate_goals_from_challenge()
elif vals.get('state') == 'done':
- self.check_challenge_reward(cr, uid, ids, force=True, context=context)
+ self._check_challenge_reward(force=True)
elif vals.get('state') == 'draft':
# resetting progress
- if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context):
- raise UserError(_("You can not reset a challenge with unfinished goals."))
+ if self.env['gamification.goal'].search([('challenge_id', 'in', self.ids), ('state', '=', 'inprogress')], limit=1):
+ raise exceptions.UserError(_("You can not reset a challenge with unfinished goals."))
return write_res
##### Update #####
- def _cron_update(self, cr, uid, context=None, ids=False):
+ @api.model # FIXME: check how cron functions are called to see if decorator necessary
+ def _cron_update(self, ids=False):
"""Daily cron check.
- Start planned challenges (in draft and with start_date = today)
- Create the missing goals (eg: modified the challenge to add lines)
- Update every running challenge
"""
- if context is None:
- context = {}
-
# start scheduled challenges
- planned_challenge_ids = self.search(cr, uid, [
+ planned_challenges = self.search([
('state', '=', 'draft'),
- ('start_date', '<=', fields.date.today())])
- if planned_challenge_ids:
- self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
+ ('start_date', '<=', fields.Date.today())
+ ])
+ if planned_challenges:
+ planned_challenges.write({'state': 'inprogress'})
# close scheduled challenges
- planned_challenge_ids = self.search(cr, uid, [
+ scheduled_challenges = self.search([
('state', '=', 'inprogress'),
- ('end_date', '<', fields.date.today())])
- if planned_challenge_ids:
- self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
+ ('end_date', '<', fields.Date.today())
+ ])
+ if scheduled_challenges:
+ scheduled_challenges.write({'state': 'done'})
- if not ids:
- ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
+ records = self.browse(ids) if ids else self.search([('state', '=', 'inprogress')])
# in cron mode, will do intermediate commits
- # TODO in trunk: replace by parameter
- context = dict(context, commit_gamification=True)
- return self._update_all(cr, uid, ids, context=context)
+ # FIXME: replace by parameter
+ return records.with_context(commit_gamification=True)._update_all()
- def _update_all(self, cr, uid, ids, context=None):
+ def _update_all(self):
"""Update the challenges and related goals
:param list(int) ids: the ids of the challenges to update, if False will
update only challenges in progress."""
- if not ids:
+ if not self:
return True
- if isinstance(ids, (int,long)):
- ids = [ids]
-
- goal_obj = self.pool.get('gamification.goal')
+ Goals = self.env['gamification.goal']
# include yesterday goals to update the goals that just ended
# exclude goals for users that did not connect since the last update
- yesterday = date.today() - timedelta(days=1)
- cr.execute("""SELECT gg.id
+ yesterday = fields.Date.to_string(date.today() - timedelta(days=1))
+ self.env.cr.execute("""SELECT gg.id
FROM gamification_goal as gg,
gamification_challenge as gc,
res_users as ru,
@@ -302,106 +257,87 @@ class gamification_challenge(osv.Model):
OR (gg.state = 'reached'
AND (gg.end_date >= %s OR gg.end_date IS NULL)))
GROUP BY gg.id
- """, (tuple(ids), yesterday.strftime(DF)))
- goal_ids = [res[0] for res in cr.fetchall()]
- # update every running goal already generated linked to selected challenges
- goal_obj.update_goal(cr, uid, goal_ids, context=context)
+ """, [tuple(self.ids), yesterday])
- self._recompute_challenge_users(cr, uid, ids, context=context)
- self._generate_goals_from_challenge(cr, uid, ids, context=context)
+ Goals.browse(goal_id for [goal_id] in self.env.cr.fetchall()).update_goal()
- for challenge in self.browse(cr, uid, ids, context=context):
+ self._recompute_challenge_users()
+ self._generate_goals_from_challenge()
- if challenge.last_report_date != fields.date.today():
+ for challenge in self:
+ if challenge.last_report_date != fields.Date.today():
# goals closed but still opened at the last report date
- closed_goals_to_report = goal_obj.search(cr, uid, [
+ closed_goals_to_report = Goals.search([
('challenge_id', '=', challenge.id),
('start_date', '>=', challenge.last_report_date),
('end_date', '<=', challenge.last_report_date)
])
- if challenge.next_report_date and fields.date.today() >= challenge.next_report_date:
- self.report_progress(cr, uid, challenge, context=context)
-
- elif len(closed_goals_to_report) > 0:
+ if challenge.next_report_date and fields.Date.today() >= challenge.next_report_date:
+ challenge.report_progress()
+ elif closed_goals_to_report:
# some goals need a final report
- self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
+ challenge.report_progress(subset_goals=closed_goals_to_report)
- self.check_challenge_reward(cr, uid, ids, context=context)
+ self._check_challenge_reward()
return True
- def quick_update(self, cr, uid, challenge_id, context=None):
- """Update all the goals of a specific challenge, no generation of new goals"""
- goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
- self.pool.get('gamification.goal').update_goal(cr, uid, goal_ids, context=context)
- return True
+ def _get_challenger_users(self, domain):
+ # FIXME: literal_eval?
+ user_domain = safe_eval(domain)
+ return self.env['res.users'].search(user_domain)
- def _get_challenger_users(self, cr, uid, domain, context=None):
- user_domain = eval(ustr(domain))
- return self.pool['res.users'].search(cr, uid, user_domain, context=context)
-
- def _recompute_challenge_users(self, cr, uid, challenge_ids, context=None):
+ def _recompute_challenge_users(self):
"""Recompute the domain to add new users and remove the one no longer matching the domain"""
- for challenge in self.browse(cr, uid, challenge_ids, context=context):
- if challenge.user_domain:
+ for challenge in self.filtered(lambda c: c.user_domain):
+ current_users = challenge.user_ids
+ new_users = self._get_challenger_users(challenge.user_domain)
- old_user_ids = [user.id for user in challenge.user_ids]
- new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
- to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
- to_add_ids = list(set(new_user_ids) - set(old_user_ids))
-
- write_op = [(3, user_id) for user_id in to_remove_ids]
- write_op += [(4, user_id) for user_id in to_add_ids]
- if write_op:
- self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
+ if current_users != new_users:
+ challenge.user_ids = new_users
return True
- def action_start(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_start(self):
"""Start a challenge"""
- return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+ return self.write({'state': 'inprogress'})
- def action_check(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_check(self):
"""Check a challenge
Create goals that haven't been created yet (eg: if added users)
Recompute the current value for each goal related"""
- goal_obj = self.pool['gamification.goal']
- goal_ids = goal_obj.search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context)
- goal_obj.unlink(cr, uid, goal_ids, context=context)
- return self._update_all(cr, uid, ids=ids, context=context)
+ self.env['gamification.goal'].search([
+ ('challenge_id', 'in', self.ids),
+ ('state', '=', 'inprogress')
+ ]).unlink()
- def action_report_progress(self, cr, uid, ids, context=None):
+ return self._update_all()
+
+ @api.multi
+ def action_report_progress(self):
"""Manual report of a goal, does not influence automatic report frequency"""
- if isinstance(ids, (int,long)):
- ids = [ids]
- for challenge in self.browse(cr, uid, ids, context=context):
- self.report_progress(cr, uid, challenge, context=context)
+ for challenge in self:
+ challenge.report_progress()
return True
-
##### Automatic actions #####
- def _generate_goals_from_challenge(self, cr, uid, ids, context=None):
+ def _generate_goals_from_challenge(self):
"""Generate the goals for each line and user.
If goals already exist for this line and user, the line is skipped. This
can be called after each change in the list of users or lines.
:param list(int) ids: the list of challenge concerned"""
- goal_obj = self.pool.get('gamification.goal')
- for challenge in self.browse(cr, uid, ids, context=context):
- (start_date, end_date) = start_end_date_for_period(challenge.period)
- to_update = []
-
- # if no periodicity, use challenge dates
- if not start_date and challenge.start_date:
- start_date = challenge.start_date
- if not end_date and challenge.end_date:
- end_date = challenge.end_date
+ Goals = self.env['gamification.goal']
+ for challenge in self:
+ (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ to_update = Goals.browse(())
for line in challenge.line_ids:
-
# there is potentially a lot of users
# detect the ones with no goal linked to this line
date_clause = ""
@@ -420,18 +356,17 @@ class gamification_challenge(osv.Model):
WHERE line_id = %s
{date_clause}
""".format(date_clause=date_clause)
+ self.env.cr.execute(query, query_params)
+ user_with_goal_ids = {it for [it] in self.env.cr._obj}
- cr.execute(query, query_params)
- user_with_goal_ids = cr.dictfetchall()
-
- participant_user_ids = [user.id for user in challenge.user_ids]
- user_without_goal_ids = list(set(participant_user_ids) - set([user['user_id'] for user in user_with_goal_ids]))
- user_squating_challenge_ids = list(set([user['user_id'] for user in user_with_goal_ids]) - set(participant_user_ids))
+ participant_user_ids = set(challenge.user_ids.ids)
+ user_squating_challenge_ids = user_with_goal_ids - participant_user_ids
if user_squating_challenge_ids:
# users that used to match the challenge
- goal_to_remove_ids = goal_obj.search(cr, uid, [('challenge_id', '=', challenge.id), ('user_id', 'in', user_squating_challenge_ids)], context=context)
- goal_obj.unlink(cr, uid, goal_to_remove_ids, context=context)
-
+ Goals.search([
+ ('challenge_id', '=', challenge.id),
+ ('user_id', 'in', list(user_squating_challenge_ids))
+ ]).unlink()
values = {
'definition_id': line.definition_id.id,
@@ -454,25 +389,29 @@ class gamification_challenge(osv.Model):
if challenge.remind_update_delay:
values['remind_update_delay'] = challenge.remind_update_delay
- for user_id in user_without_goal_ids:
- values.update({'user_id': user_id})
- goal_id = goal_obj.create(cr, uid, values, context=context)
- to_update.append(goal_id)
+ for user_id in (participant_user_ids - user_with_goal_ids):
+ values['user_id'] = user_id
+ to_update |= Goals.create(values)
- goal_obj.update_goal(cr, uid, to_update, context=context)
+ to_update.update_goal()
return True
##### JS utilities #####
- def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
+ def _get_serialized_challenge_lines(self, user=(), restrict_goals=(), restrict_top=0):
"""Return a serialised version of the goals information if the user has not completed every goal
- :challenge: browse record of challenge to compute
- :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
- :restrict_goal_ids: compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge
- :restrict_top: for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
- restrict_goal_ids has priority over restrict_top
+ :param user: user retrieving progress (False if no distinction,
+ only for ranking challenges)
+ :param restrict_goals: compute only the results for this subset of
+ gamification.goal ids, if False retrieve every
+ goal of current running challenge
+ :param int restrict_top: for challenge lines where visibility_mode is
+ ``ranking``, retrieve only the best
+ ``restrict_top`` results and itself, if 0
+ retrieve all restrict_goal_ids has priority
+ over restrict_top
format list
# if visibility_mode == 'ranking'
@@ -516,12 +455,11 @@ class gamification_challenge(osv.Model):
'current': ,
}
"""
- goal_obj = self.pool.get('gamification.goal')
- (start_date, end_date) = start_end_date_for_period(challenge.period)
+ Goals = self.env['gamification.goal']
+ (start_date, end_date) = start_end_date_for_period(self.period)
res_lines = []
- all_reached = True
- for line in challenge.line_ids:
+ for line in self.line_ids:
line_data = {
'name': line.definition_id.name,
'description': line.definition_id.description,
@@ -537,8 +475,8 @@ class gamification_challenge(osv.Model):
('line_id', '=', line.id),
('state', '!=', 'draft'),
]
- if restrict_goal_ids:
- domain.append(('ids', 'in', restrict_goal_ids))
+ if restrict_goals:
+ domain.append(('ids', 'in', restrict_goals.ids))
else:
# if no subset goals, use the dates for restriction
if start_date:
@@ -546,152 +484,123 @@ class gamification_challenge(osv.Model):
if end_date:
domain.append(('end_date', '=', end_date))
- if challenge.visibility_mode == 'personal':
- if not user_id:
- raise UserError(_("Retrieving progress for personal challenge without user information"))
- domain.append(('user_id', '=', user_id))
- sorting = goal_obj._order
- limit = 1
- else:
- line_data.update({
- 'own_goal_id': False,
- 'goals': [],
- })
- sorting = "completeness desc, current desc"
- limit = False
+ if self.visibility_mode == 'personal':
+ if not user:
+ raise exceptions.UserError(_("Retrieving progress for personal challenge without user information"))
- goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
- ranking = 0
- for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
- if challenge.visibility_mode == 'personal':
- # limit=1 so only one result
- line_data.update({
- 'id': goal.id,
- 'current': goal.current,
- 'completeness': goal.completeness,
- 'state': goal.state,
- })
- if goal.state != 'reached':
- all_reached = False
- else:
- ranking += 1
- if user_id and goal.user_id.id == user_id:
- line_data['own_goal_id'] = goal.id
- elif restrict_top and ranking > restrict_top:
- # not own goal and too low to be in top
- continue
+ domain.append(('user_id', '=', user.id))
- line_data['goals'].append({
- 'id': goal.id,
- 'user_id': goal.user_id.id,
- 'name': goal.user_id.name,
- 'rank': ranking,
- 'current': goal.current,
- 'completeness': goal.completeness,
- 'state': goal.state,
- })
- if goal.state != 'reached':
- all_reached = False
- if goal_ids:
+ goal = Goals.search(domain, limit=1)
+ if not goal:
+ continue
+
+ if goal.state != 'reached':
+ return []
+ line_data.update(goal.read(['id', 'current', 'completeness', 'state'])[0])
+ res_lines.append(line_data)
+ continue
+
+ line_data['own_goal_id'] = False,
+ line_data['goals'] = []
+
+ goals = Goals.search(domain, order="completeness desc, current desc")
+ for ranking, goal in enumerate(goals):
+ # bail immediately if any goal of the challenge hasn't been
+ # reached
+ if goal.state != 'reached':
+ return []
+
+ if user and goal.user_id == user:
+ line_data['own_goal_id'] = goal.id
+ elif restrict_top and ranking > restrict_top:
+ # not own goal and too low to be in top
+ continue
+
+ line_data['goals'].append({
+ 'id': goal.id,
+ 'user_id': goal.user_id.id,
+ 'name': goal.user_id.name,
+ 'rank': ranking,
+ 'current': goal.current,
+ 'completeness': goal.completeness,
+ 'state': goal.state,
+ })
+ if goals:
res_lines.append(line_data)
- if all_reached:
- return []
return res_lines
##### Reporting #####
- def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
+ def report_progress(self, users=(), subset_goals=False):
"""Post report about the progress of the goals
- :param challenge: the challenge object that need to be reported
- :param users: the list(res.users) of users that are concerned by
- the report. If False, will send the report to every user concerned
- (goal users and group that receive a copy). Only used for challenge with
- a visibility mode set to 'personal'.
- :param goal_ids: the list(int) of goal ids linked to the challenge for
- the report. If not specified, use the goals for the current challenge
- period. This parameter can be used to produce report for previous challenge
- periods.
- :param subset_goal_ids: a list(int) of goal ids to restrict the report
+ :param users: users that are concerned by the report. If False, will
+ send the report to every user concerned (goal users and
+ group that receive a copy). Only used for challenge with
+ a visibility mode set to 'personal'.
+ :param subset_goals: goals to restrict the report
"""
- if context is None:
- context = {}
- temp_obj = self.pool.get('mail.template')
- ctx = context.copy()
+ challenge = self
+
+ MailTemplates = self.env['mail.template']
if challenge.visibility_mode == 'ranking':
- lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
+ lines_boards = challenge._get_serialized_challenge_lines(restrict_goals=subset_goals)
- ctx.update({'challenge_lines': lines_boards})
- body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+ body_html = MailTemplates.with_context(challenge_lines=lines_boards).render_template(challenge.report_template_id.body_html, 'gamification.challenge', challenge.id)
# send to every follower and participant of the challenge
- self.message_post(cr, uid, challenge.id,
+ challenge.message_post(
body=body_html,
- partner_ids=[user.partner_id.id for user in challenge.user_ids],
- context=context,
+ partner_ids=challenge.mapped('user_ids.partner_id.id'),
subtype='mail.mt_comment')
if challenge.report_message_group_id:
- self.pool.get('mail.channel').message_post(cr, uid, challenge.report_message_group_id.id,
+ challenge.report_message_group_id.message_post(
body=body_html,
- context=context,
subtype='mail.mt_comment')
else:
# generate individual reports
- for user in users or challenge.user_ids:
- goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
- if not goals:
+ for user in (users or challenge.user_ids):
+ lines = challenge._get_serialized_challenge_lines(user, restrict_goals=subset_goals)
+ if not lines:
continue
- ctx.update({'challenge_lines': goals})
- body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
+ body_html = MailTemplates.sudo(user).with_context(challenge_lines=lines).render_template(
+ challenge.report_template_id.body_html,
+ 'gamification.challenge',
+ challenge.id)
# send message only to users, not on the challenge
- self.message_post(cr, uid, 0,
- body=body_html,
- partner_ids=[(4, user.partner_id.id)],
- context=context,
- subtype='mail.mt_comment')
+ self.env['gamification.challenge'].message_post(
+ body=body_html,
+ partner_ids=[(4, user.partner_id.id)],
+ subtype='mail.mt_comment'
+ )
if challenge.report_message_group_id:
- self.pool.get('mail.channel').message_post(cr, uid, challenge.report_message_group_id.id,
- body=body_html,
- context=context,
- subtype='mail.mt_comment')
- return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
+ challenge.report_message_group_id.message_post(
+ body=body_html,
+ subtype='mail.mt_comment')
+ return challenge.write({'last_report_date': fields.Date.today()})
##### Challenges #####
- # TODO in trunk, remove unused parameter user_id
- def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
- """The user accept the suggested challenge"""
- return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
+ @api.multi
+ def accept_challenge(self):
+ user = self.env.user
+ sudoed = self.sudo()
+ sudoed.message_post(body=_("%s has joined the challenge") % user.name)
+ sudoed.write({'invited_user_ids': [(3, user.id)], 'user_ids': [(4, user.id)]})
+ return sudoed._generate_goals_from_challenge()
- def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
- user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
- message = "%s has joined the challenge" % user.name
- self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
- self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
- return self._generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
-
- # TODO in trunk, remove unused parameter user_id
- def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
+ @api.multi
+ def discard_challenge(self):
"""The user discard the suggested challenge"""
- return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
+ user = self.env.user
+ sudoed = self.sudo()
+ sudoed.message_post(body=_("%s has refused the challenge") % user.name)
+ return sudoed.write({'invited_user_ids': (3, user.id)})
- def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
- user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
- message = "%s has refused the challenge" % user.name
- self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
- return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
-
- def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
- result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
- id = result and result[1] or False
- result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
- result['res_id'] = challenge_id
- return result
-
- def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
+ def _check_challenge_reward(self, force=False):
"""Actions for the end of a challenge
If a reward was selected, grant it to the correct users.
@@ -701,184 +610,176 @@ class gamification_challenge(osv.Model):
- when a challenge is manually closed
(if no end date, a running challenge is never rewarded)
"""
- if isinstance(ids, (int,long)):
- ids = [ids]
- commit = context.get('commit_gamification', False)
- for challenge in self.browse(cr, uid, ids, context=context):
+ commit = self.env.context.get('commit_gamification') and self.env.cr.commit
+
+ for challenge in self:
(start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
yesterday = date.today() - timedelta(days=1)
- rewarded_users = []
- challenge_ended = end_date == yesterday.strftime(DF) or force
+ rewarded_users = self.env['res.users']
+ challenge_ended = force or end_date == fields.Date.to_string(yesterday)
if challenge.reward_id and (challenge_ended or challenge.reward_realtime):
# not using start_date as intemportal goals have a start date but no end_date
- reached_goals = self.pool.get('gamification.goal').read_group(cr, uid, [
+ reached_goals = self.env['gamification.goal'].read_group([
('challenge_id', '=', challenge.id),
('end_date', '=', end_date),
('state', '=', 'reached')
- ], fields=['user_id'], groupby=['user_id'], context=context)
+ ], fields=['user_id'], groupby=['user_id'])
for reach_goals_user in reached_goals:
if reach_goals_user['user_id_count'] == len(challenge.line_ids):
# the user has succeeded every assigned goal
- user_id = reach_goals_user['user_id'][0]
+ user = self.env['res.users'].browse(reach_goals_user['user_id'][0])
if challenge.reward_realtime:
- badges = self.pool['gamification.badge.user'].search(cr, uid, [
+ badges = self.env['gamification.badge.user'].search_count([
('challenge_id', '=', challenge.id),
('badge_id', '=', challenge.reward_id.id),
- ('user_id', '=', user_id),
- ], count=True, context=context)
+ ('user_id', '=', user.id),
+ ])
if badges > 0:
# has already recieved the badge for this challenge
continue
- self.reward_user(cr, uid, user_id, challenge.reward_id.id, challenge.id, context=context)
- rewarded_users.append(user_id)
+ challenge._reward_user(user, challenge.reward_id)
+ rewarded_users |= user
if commit:
- cr.commit()
+ commit()
if challenge_ended:
# open chatter message
message_body = _("The challenge %s is finished.") % challenge.name
if rewarded_users:
- user_names = self.pool['res.users'].name_get(cr, uid, rewarded_users, context=context)
- message_body += _(" Reward (badge %s) for every succeeding user was sent to %s.") % (challenge.reward_id.name, ", ".join([name for (user_id, name) in user_names]))
+ user_names = rewarded_users.name_get()
+ message_body += _(" Reward (badge %s) for every succeeding user was sent to %s.") % (challenge.reward_id.name, ", ".join(name for (user_id, name) in user_names))
else:
message_body += _(" Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.")
# reward bests
+ reward_message = _(" %(rank)d. %(user_name)s - %(reward_name)s")
if challenge.reward_first_id:
- (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context=context)
+ (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING)
if first_user:
- self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, challenge.id, context=context)
+ challenge._reward_user(first_user, challenge.reward_first_id)
message_body += _(" Special rewards were sent to the top competing users. The ranking for this challenge is :")
- message_body += " 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
+ message_body += reward_message % {
+ 'rank': 1,
+ 'user_name': first_user.name,
+ 'reward_name': challenge.reward_first_id.name,
+ }
else:
message_body += _("Nobody reached the required conditions to receive special badges.")
if second_user and challenge.reward_second_id:
- self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, challenge.id, context=context)
- message_body += " 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
+ challenge._reward_user(second_user, challenge.reward_second_id)
+ message_body += reward_message % {
+ 'rank': 2,
+ 'user_name': second_user.name,
+ 'reward_name': challenge.reward_second_id.name,
+ }
if third_user and challenge.reward_third_id:
- self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, challenge.id, context=context)
- message_body += " 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
+ challenge._reward_user(third_user, challenge.reward_third_id)
+ message_body += reward_message % {
+ 'rank': 3,
+ 'user_name': third_user.name,
+ 'reward_name': challenge.reward_third_id.name,
+ }
- self.message_post(cr, uid, challenge.id,
+ challenge.message_post(
partner_ids=[user.partner_id.id for user in challenge.user_ids],
- body=message_body,
- context=context)
+ body=message_body)
if commit:
- cr.commit()
+ commit()
return True
- def get_top3_users(self, cr, uid, challenge, context=None):
- """Get the top 3 users for a defined challenge
+ def _get_topN_users(self, n):
+ """Get the top N users for a defined challenge
Ranking criterias:
1. succeed every goal of the challenge
2. total completeness of each goal (can be over 100)
- Top 3 is computed only for users succeeding every goal of the challenge,
- except if reward_failure is True, in which case every user is
- considered.
- :return: ('first', 'second', 'third'), tuple containing the res.users
- objects of the top 3 users. If no user meets the criterias for a rank,
- it is set to False. Nobody can receive a rank is noone receives the
- higher one (eg: if 'second' == False, 'third' will be False)
+
+ Only users having reached every goal of the challenge will be returned
+ unless the challenge ``reward_failure`` is set, in which case any user
+ may be considered.
+
+ :returns: an iterable of exactly N records, either User objects or
+ False if there was no user for the rank. There can be no
+ False between two users (if users[k] = False then
+ users[k+1] = False
"""
- goal_obj = self.pool.get('gamification.goal')
- (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
+ Goals = self.env['gamification.goal']
+ (start_date, end_date) = start_end_date_for_period(self.period, self.start_date, self.end_date)
challengers = []
- for user in challenge.user_ids:
+ for user in self.user_ids:
all_reached = True
- total_completness = 0
+ total_completeness = 0
# every goal of the user for the running period
- goal_ids = goal_obj.search(cr, uid, [
- ('challenge_id', '=', challenge.id),
+ goal_ids = Goals.search([
+ ('challenge_id', '=', self.id),
('user_id', '=', user.id),
('start_date', '=', start_date),
('end_date', '=', end_date)
- ], context=context)
- for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
+ ])
+ for goal in goal_ids:
if goal.state != 'reached':
all_reached = False
if goal.definition_condition == 'higher':
# can be over 100
- total_completness += 100.0 * goal.current / goal.target_goal
+ total_completeness += 100.0 * goal.current / goal.target_goal
elif goal.state == 'reached':
# for lower goals, can not get percentage so 0 or 100
- total_completness += 100
+ total_completeness += 100
- challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
- sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
+ challengers.append({'user': user, 'all_reached': all_reached, 'total_completeness': total_completeness})
- if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
- # nobody succeeded
- return (False, False, False)
- if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
- # only one user succeeded
- return (sorted_challengers[0]['user'], False, False)
- if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
- # only one user succeeded
- return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
- return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
+ challengers.sort(key=lambda k: (k['all_reached'], k['total_completeness']), reverse=True)
+ if not self.reward_failure:
+ # only keep the fully successful challengers at the front, could
+ # probably use filter since the successful ones are at the front
+ challengers = itertools.takewhile(lambda c: c['all_reached'], challengers)
- def reward_user(self, cr, uid, user_id, badge_id, challenge_id=False, context=None):
+ # append a tail of False, then keep the first N
+ challengers = itertools.islice(
+ itertools.chain(
+ (c['user'] for c in challengers),
+ itertools.repeat(False),
+ ), 0, n
+ )
+
+ return tuple(challengers)
+
+ def _reward_user(self, user, badge):
"""Create a badge user and send the badge to him
- :param user_id: the user to reward
- :param badge_id: the concerned badge
+ :param user: the user to reward
+ :param badge: the concerned badge
"""
- badge_user_obj = self.pool.get('gamification.badge.user')
- user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id, 'challenge_id':challenge_id}, context=context)
- return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
+ return self.env['gamification.badge.user'].create({
+ 'user_id': user.id,
+ 'badge_id': badge.id,
+ 'challenge_id': self.id
+ })._send_badge()
-class gamification_challenge_line(osv.Model):
+class ChallengeLine(models.Model):
"""Gamification challenge line
- Predifined goal for 'gamification_challenge'
+ 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
+ Should only be created for the gamification.challenge object
"""
-
_name = 'gamification.challenge.line'
_description = 'Gamification generic goal for challenge'
_order = "sequence, id"
- def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
- goal_definition = self.pool.get('gamification.goal.definition')
- if not definition_id:
- return {'value': {'definition_id': False}}
- goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
- ret = {
- 'value': {
- 'condition': goal_definition.condition,
- 'definition_full_suffix': goal_definition.full_suffix
- }
- }
- return ret
+ 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")
- _columns = {
- 'name': fields.related('definition_id', 'name', string="Name", type="char"),
- '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"),
- 'target_goal': fields.float('Target Value to Reach',
- required=True),
- 'sequence': fields.integer('Sequence',
- help='Sequence number for ordering'),
- 'condition': fields.related('definition_id', 'condition', type="selection",
- readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
- 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
- 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
- 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
- }
+ sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1)
+ target_goal = fields.Float('Target Value to Reach', required=True)
- _default = {
- 'sequence': 1,
- }
+ name = fields.Char("Name", related='definition_id.name')
+ condition = fields.Selection("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)
diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py
index a219d7ddc01..19d81c4cbde 100644
--- a/addons/gamification/models/goal.py
+++ b/addons/gamification/models/goal.py
@@ -1,21 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import SUPERUSER_ID
-from openerp.osv import fields, osv, expression
-from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
-from openerp.tools.safe_eval import safe_eval
-from openerp.tools.translate import _
-from openerp.exceptions import UserError
-
import logging
import time
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
+
_logger = logging.getLogger(__name__)
-class gamification_goal_definition(osv.Model):
+class GoalDefinition(models.Model):
"""Goal definition
A goal definition contains the way to evaluate an objective
@@ -25,273 +22,233 @@ class gamification_goal_definition(osv.Model):
_name = 'gamification.goal.definition'
_description = 'Gamification goal definition'
- def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
- res = dict.fromkeys(ids, '')
- for goal in self.browse(cr, uid, ids, context=context):
- if goal.suffix and not goal.monetary:
- res[goal.id] = goal.suffix
- elif goal.monetary:
- # use the current user's company currency
- user = self.pool.get('res.users').browse(cr, uid, uid, context)
- if goal.suffix:
- res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
- else:
- res[goal.id] = user.company_id.currency_id.symbol
- else:
- res[goal.id] = ""
- return res
+ 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="Defined how will be computed the goals. 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')
+ field_id = fields.Many2one('ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate')
+ field_date_id = fields.Many2one('ir.model.fields', string='Date Field', help='The date to use for the time period evaluated')
+ 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.")
- _columns = {
- 'name': fields.char('Goal Definition', required=True, translate=True),
- 'description': fields.text('Goal Description'),
- 'monetary': fields.boolean('Monetary Value', 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.function(_get_suffix, type="char", string="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'),
- ],
- string="Computation Mode",
- help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
- required=True),
- 'display_mode': fields.selection([
- ('progress', 'Progressive (using numerical values)'),
- ('boolean', 'Exclusive (done or not-done)'),
- ],
- string="Displayed as", required=True),
- 'model_id': fields.many2one('ir.model',
- string='Model',
- help='The model object for the field to evaluate'),
- 'field_id': fields.many2one('ir.model.fields',
- string='Field to Sum',
- help='The field containing the value to evaluate'),
- 'field_date_id': fields.many2one('ir.model.fields',
- string='Date Field',
- help='The date to use for the time period evaluated'),
- 'domain': fields.char("Filter Domain",
- 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.",
- required=True),
+ 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 distinct one user form 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.")
- '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 distinct one user form the other, e.g. user_id, partner_id..."),
- 'batch_user_expression': fields.char("Evaluted 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')
- ],
- string='Goal Performance',
- help='A goal is considered as completed when the current value is compared to the value to reach',
- required=True),
- '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 = []
- _defaults = {
- 'condition': 'higher',
- 'computation_mode': 'manually',
- 'domain': "[]",
- 'monetary': False,
- 'display_mode': 'progress',
- }
+ if goal.monetary:
+ items.append(self.env.user.company_id.currency_id.symbol or u'¤')
+ if goal.suffix:
+ items.append(goal.suffix)
- def number_following(self, cr, uid, model_name="mail.thread", context=None):
- """Return the number of 'model_name' objects the user is following
+ goal.full_suffix = u' '.join(items)
- The model specified in 'model_name' must inherit from mail.thread
- """
- user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context)
-
- def _check_domain_validity(self, cr, uid, ids, context=None):
+ def _check_domain_validity(self):
# take admin as should always be present
- superuser = self.pool['res.users'].browse(cr, uid, SUPERUSER_ID, context=context)
- for definition in self.browse(cr, uid, ids, context=context):
+ for definition in self:
if definition.computation_mode not in ('count', 'sum'):
continue
- obj = self.pool[definition.model_id.model]
+ Obj = self.env[definition.model_id.model]
try:
- domain = safe_eval(definition.domain, {'user': superuser})
- # demmy search to make sure the domain is valid
- obj.search(cr, uid, domain, context=context, count=True)
+ domain = safe_eval(definition.domain, {
+ 'user': self.env.user.sudo(self.env.user)
+ })
+ # dummy search to make sure the domain is valid
+ Obj.search_count(domain)
except (ValueError, SyntaxError), e:
msg = e.message or (e.msg + '\n' + e.text)
- raise UserError(_("The domain for the definition %s seems incorrect, please check it.\n\n%s") % (definition.name, msg))
+ 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, cr, uid, ids, context=None):
+ def _check_model_validity(self):
""" make sure the selected field and model are usable"""
- for definition in self.browse(cr, uid, ids, context=context):
+ for definition in self:
try:
- if not definition.model_id or not definition.field_id:
+ if not (definition.model_id and definition.field_id):
continue
- model = self.pool[definition.model_id.model]
- field = model._fields[definition.field_id.name]
- if not field.store:
- raise UserError(
+ 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 %s seems incorrect, please check it.\n\n%s not stored") % (definition.name, definition.field_id.name))
except KeyError, e:
- raise UserError(
+ raise exceptions.UserError(
_("The model configuration for the definition %s seems incorrect, please check it.\n\n%s not found") % (definition.name, e.message))
- def create(self, cr, uid, vals, context=None):
- res_id = super(gamification_goal_definition, self).create(cr, uid, vals, context=context)
- if vals.get('computation_mode') in ('count', 'sum'):
- self._check_domain_validity(cr, uid, [res_id], context=context)
+ @api.model
+ def create(self, vals):
+ definition = super(GoalDefinition, self).create(vals)
+ if definition.computation_mode in ('count', 'sum'):
+ definition._check_domain_validity()
if vals.get('field_id'):
- self._check_model_validity(cr, uid, [res_id], context=context)
- return res_id
+ definition._check_model_validity()
+ return definition
- def write(self, cr, uid, ids, vals, context=None):
- res = super(gamification_goal_definition, self).write(cr, uid, ids, vals, context=context)
+ @api.multi
+ 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(cr, uid, ids, context=context)
+ self._check_domain_validity()
if vals.get('field_id') or vals.get('model_id') or vals.get('batch_mode'):
- self._check_model_validity(cr, uid, ids, context=context)
+ self._check_model_validity()
return res
- def on_change_model_id(self, cr, uid, ids, model_id, context=None):
+ @api.onchange('model_id')
+ def _change_model_id(self):
"""Force domain for the `field_id` and `field_date_id` fields"""
- if not model_id:
+ if not self.model_id:
return {'domain': {'field_id': expression.FALSE_DOMAIN, 'field_date_id': expression.FALSE_DOMAIN}}
- model = self.pool['ir.model'].browse(cr, uid, model_id, context=context)
- model_fields_domain = [('store', '=', True),
- '|', ('model_id', '=', model_id), ('model_id', 'in', model.inherited_model_ids.ids)]
+ model = self.env['ir.model'].browse(self.model_id)
+ model_fields_domain = [
+ ('store', '=', True),
+ '|', ('model_id', '=', self.model_id),
+ ('model_id', 'in', model.inherited_model_ids.ids)]
model_date_fields_domain = expression.AND([[('ttype', 'in', ('date', 'datetime'))], model_fields_domain])
return {'domain': {'field_id': model_fields_domain, 'field_date_id': model_date_fields_domain}}
-class gamification_goal(osv.Model):
+class Goal(models.Model):
"""Goal instance for a user
An individual goal for a user on a specified time period"""
_name = 'gamification.goal'
_description = 'Gamification goal instance'
-
- def _get_completion(self, cr, uid, ids, field_name, arg, context=None):
- """Return the percentage of completeness of the goal, between 0 and 100"""
- res = dict.fromkeys(ids, 0.0)
- for goal in self.browse(cr, uid, ids, context=context):
- if goal.definition_condition == 'higher':
- if goal.current >= goal.target_goal:
- res[goal.id] = 100.0
- else:
- res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2)
- elif goal.current < goal.target_goal:
- # a goal 'lower than' has only two values possible: 0 or 100%
- res[goal.id] = 100.0
- else:
- res[goal.id] = 0.0
- return res
-
- def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
- goal_definition = self.pool.get('gamification.goal.definition')
- if not definition_id:
- return {'value': {'definition_id': False}}
- goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
- return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}}
-
- _columns = {
- 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"),
- 'user_id': fields.many2one('res.users', string='User', required=True, auto_join=True, ondelete="cascade"),
- 'line_id': fields.many2one('gamification.challenge.line', string='Challenge Line', ondelete="cascade"),
- 'challenge_id': fields.related('line_id', 'challenge_id',
- string="Challenge",
- type='many2one',
- relation='gamification.challenge',
- store=True, readonly=True,
- help="Challenge that generated the goal, assign challenge to users to generate goals with a value in this field."),
- 'start_date': fields.date('Start Date'),
- 'end_date': fields.date('End Date'), # no start and end = always active
- 'target_goal': fields.float('To Reach',
- required=True,
- track_visibility='always'), # no goal = global index
- 'current': fields.float('Current Value', required=True, track_visibility='always'),
- 'completeness': fields.function(_get_completion, type='float', string='Completeness'),
- 'state': fields.selection([
- ('draft', 'Draft'),
- ('inprogress', 'In progress'),
- ('reached', 'Reached'),
- ('failed', 'Failed'),
- ('canceled', 'Canceled'),
- ],
- string='State',
- required=True,
- track_visibility='always'),
- 'to_update': fields.boolean('To update'),
- 'closed': fields.boolean('Closed goal', help="These goals will not be recomputed."),
-
- 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"),
- 'remind_update_delay': fields.integer('Remind delay',
- help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
- 'last_update': fields.date('Last Update',
- help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge."),
-
- 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True),
- 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True),
- 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True),
- 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True),
- }
-
- _defaults = {
- 'current': 0,
- 'state': 'draft',
- 'start_date': fields.date.today,
- }
_order = 'start_date desc, end_date desc, definition_id, id'
- def _check_remind_delay(self, cr, uid, goal, context=None):
+ definition_id = fields.Many2one('gamification.goal.definition', string="Goal Definition", required=True, ondelete="cascade")
+ user_id = fields.Many2one('res.users', string="User", required=True, auto_join=True, ondelete="cascade")
+ line_id = fields.Many2one('gamification.challenge.line', string="Challenge Line", ondelete="cascade")
+ challenge_id = fields.Many2one(
+ related='line_id.challenge_id', store=True, readonly=True,
+ help="Challenge that generated the goal, assign challenge to users "
+ "to generate goals with a value in this field.")
+ start_date = fields.Date("Start Date", default=fields.Date.today)
+ end_date = fields.Date("End Date") # no start and end = always active
+ target_goal = fields.Float('To Reach', required=True, track_visibility='always')
+# no goal = global index
+ current = fields.Float("Current Value", required=True, default=0, track_visibility='always')
+ completeness = fields.Float("Completeness", compute='_get_completion')
+ state = fields.Selection([
+ ('draft', "Draft"),
+ ('inprogress', "In progress"),
+ ('reached', "Reached"),
+ ('failed', "Failed"),
+ ('canceled', "Canceled"),
+ ], default='draft', string='State', required=True, track_visibility='always')
+ to_update = fields.Boolean('To update')
+ closed = fields.Boolean('Closed goal', help="These goals will not be recomputed.")
+
+ computation_mode = fields.Selection(related='definition_id.computation_mode')
+ remind_update_delay = fields.Integer(
+ "Remind delay", help="The number of days after which the user "
+ "assigned to a manual goal will be reminded. "
+ "Never reminded if no value is specified.")
+ last_update = fields.Date(
+ "Last Update",
+ help="In case of manual goal, reminders are sent if the goal as not "
+ "been updated for a while (defined in challenge). Ignored in "
+ "case of non-manual goal or goal not linked to a challenge.")
+
+ definition_description = fields.Text("Definition Description", related='definition_id.description', readonly=True)
+ definition_condition = fields.Selection("Definition Condition", related='definition_id.condition', readonly=True)
+ definition_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True)
+ definition_display = fields.Selection("Display Mode", related='definition_id.display_mode', readonly=True)
+
+ @api.depends('current', 'target_goal', 'definition_id.condition')
+ def _get_completion(self):
+ """Return the percentage of completeness of the goal, between 0 and 100"""
+ for goal in self:
+ if goal.definition_condition == 'higher':
+ if goal.current >= goal.target_goal:
+ goal.completeness = 100.0
+ else:
+ goal.completeness = round(100.0 * goal.current / goal.target_goal, 2)
+ elif goal.current < goal.target_goal:
+ # a goal 'lower than' has only two values possible: 0 or 100%
+ goal.completeness = 100.0
+ else:
+ goal.completeness = 0.0
+
+ def _check_remind_delay(self):
"""Verify if a goal has not been updated for some time and send a
reminder message of needed.
:return: data to write on the goal object
"""
- temp_obj = self.pool['mail.template']
- if goal.remind_update_delay and goal.last_update:
- delta_max = timedelta(days=goal.remind_update_delay)
- last_update = datetime.strptime(goal.last_update, DF).date()
- if date.today() - last_update > delta_max:
- # generate a remind report
- temp_obj = self.pool.get('mail.template')
- template_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'gamification', 'email_template_goal_reminder')[0]
- template = temp_obj.get_email_template(cr, uid, template_id, goal.id, context=context)
- body_html = temp_obj.render_template(cr, uid, template.body_html, 'gamification.goal', goal.id, context=template._context)
- self.pool['mail.thread'].message_post(cr, uid, 0, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
- return {'to_update': True}
- return {}
+ if not (self.remind_update_delay and self.last_update):
+ return {}
- def _get_write_values(self, cr, uid, goal, new_value, context=None):
+ delta_max = timedelta(days=self.remind_update_delay)
+ last_update = fields.Date.from_string(self.last_update)
+ if date.today() - last_update < delta_max:
+ return {}
+
+ # generate a reminder report
+ template = self.env.ref('gamification.email_template_goal_reminder')\
+ .get_email_template(self.id)
+ body_html = self.env['mail.template'].with_context(template._context)\
+ .render_template(template.body_html, 'gamification.goal', self.id)
+ self.env['mail.thread'].message_post(
+ body=body_html,
+ partner_ids=[ self.user_id.partner_id.id],
+ subtype='mail.mt_comment'
+ )
+
+ return {'to_update': True}
+
+ def _get_write_values(self, new_value):
"""Generate values to write after recomputation of a goal score"""
- if new_value == goal.current:
+ if new_value == self.current:
# avoid useless write if the new value is the same as the old one
return {}
- result = {goal.id: {'current': new_value}}
- if (goal.definition_id.condition == 'higher' and new_value >= goal.target_goal) \
- or (goal.definition_id.condition == 'lower' and new_value <= goal.target_goal):
+ result = {'current': new_value}
+ if (self.definition_id.condition == 'higher' and new_value >= self.target_goal) \
+ or (self.definition_id.condition == 'lower' and new_value <= self.target_goal):
# success, do no set closed as can still change
- result[goal.id]['state'] = 'reached'
+ result['state'] = 'reached'
- elif goal.end_date and fields.date.today() > goal.end_date:
+ elif self.end_date and fields.Date.today() > self.end_date:
# check goal failure
- result[goal.id]['state'] = 'failed'
- result[goal.id]['closed'] = True
+ result['state'] = 'failed'
+ result['closed'] = True
- return result
+ return {self: result}
- def update_goal(self, cr, uid, ids, context=None):
+ @api.multi
+ def update_goal(self):
"""Update the goals to recomputes values and change of states
If a manual goal is not updated for enough time, the user will be
@@ -299,49 +256,47 @@ class gamification_goal(osv.Model):
If a goal reaches the target value, the status is set to reached
If the end date is passed (at least +1 day, time not considered) without
the target value being reached, the goal is set as failed."""
- if context is None:
- context = {}
- commit = context.get('commit_gamification', False)
-
goals_by_definition = {}
- for goal in self.browse(cr, uid, ids, context=context):
+ for goal in self:
goals_by_definition.setdefault(goal.definition_id, []).append(goal)
for definition, goals in goals_by_definition.items():
- goals_to_write = dict((goal.id, {}) for goal in goals)
+ goals_to_write = {}
if definition.computation_mode == 'manually':
for goal in goals:
- goals_to_write[goal.id].update(self._check_remind_delay(cr, uid, goal, context))
+ goals_to_write[goal] = goal._check_remind_delay()
elif definition.computation_mode == 'python':
# TODO batch execution
for goal in goals:
# execute the chosen method
cxt = {
- 'self': self.pool.get('gamification.goal'),
+ 'self': self.pool['gamification.goal'],
'object': goal,
'pool': self.pool,
- 'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
- 'uid': uid,
- 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
+ 'cr': self.env.cr,
+ 'context': dict(self.env.context), # copy context to prevent side-effects of eval
+ 'uid': self.env.uid,
+ 'date': date,
+ 'datetime': datetime,
+ 'timedelta': timedelta,
+ 'time': time,
}
code = definition.compute_code.strip()
safe_eval(code, cxt, mode="exec", nocopy=True)
# the result of the evaluated codeis put in the 'result' local variable, propagated to the context
result = cxt.get('result')
- if result is not None and type(result) in (float, int, long):
- goals_to_write.update(
- self._get_write_values(cr, uid, goal, result, context=context)
- )
-
+ if result is not None and isinstance(result, (float, int, long)):
+ goals_to_write.update(goal._get_write_values(result))
else:
- _logger.exception(_('Invalid return content from the evaluation of code for definition %s') % definition.name)
+ _logger.error(
+ "Invalid return content '%r' from the evaluation "
+ "of code for definition %s, expected a number",
+ result, definition.name)
else: # count or sum
+ Obj = self.env[definition.model_id.model]
- obj = self.pool.get(definition.model_id.model)
- field_date_name = definition.field_date_id and definition.field_date_id.name or False
-
+ field_date_name = definition.field_date_id.name
if definition.computation_mode == 'count' and definition.batch_mode:
# batch mode, trying to do as much as possible in one request
general_domain = safe_eval(definition.domain)
@@ -363,21 +318,19 @@ class gamification_goal(osv.Model):
if field_name == 'id':
# grouping on id does not work and is similar to search anyway
- user_ids = obj.search(cr, uid, subquery_domain, context=context)
- user_values = [{'id': user_id, 'id_count': 1} for user_id in user_ids]
+ users = Obj.search(subquery_domain)
+ user_values = [{'id': user.id, 'id_count': 1} for user in users]
else:
- user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context)
+ user_values = Obj.read_group(subquery_domain, fields=[field_name], groupby=[field_name])
# user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...]
- for goal in [g for g in goals if g.id in query_goals.keys()]:
+ for goal in [g for g in goals if g.id in query_goals]:
for user_value in user_values:
queried_value = field_name in user_value and user_value[field_name] or False
if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], (int, long)):
queried_value = queried_value[0]
if queried_value == query_goals[goal.id]:
new_value = user_value.get(field_name+'_count', goal.current)
- goals_to_write.update(
- self._get_write_values(cr, uid, goal, new_value, context=context)
- )
+ goals_to_write.update(goal._get_write_values(new_value))
else:
for goal in goals:
@@ -393,111 +346,111 @@ class gamification_goal(osv.Model):
if definition.computation_mode == 'sum':
field_name = definition.field_id.name
# TODO for master: group on user field in batch mode
- res = obj.read_group(cr, uid, domain, [field_name], [], context=context)
+ res = Obj.read_group(domain, [field_name], [])
new_value = res and res[0][field_name] or 0.0
else: # computation mode = count
- new_value = obj.search(cr, uid, domain, context=context, count=True)
+ new_value = Obj.search_count(domain)
- goals_to_write.update(
- self._get_write_values(cr, uid, goal, new_value, context=context)
- )
+ goals_to_write.update(goal._get_write_values(new_value))
- for goal_id, value in goals_to_write.items():
- if not value:
+ for goal, values in goals_to_write.iteritems():
+ if not values:
continue
- self.write(cr, uid, [goal_id], value, context=context)
- if commit:
- cr.commit()
+ goal.write(values)
+ if self.env.context.get('commit_gamification'):
+ self.env.cr.commit()
return True
- def action_start(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_start(self):
"""Mark a goal as started.
This should only be used when creating goals manually (in draft state)"""
- self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
- return self.update_goal(cr, uid, ids, context=context)
+ self.write({'state': 'inprogress'})
+ return self.update_goal()
- def action_reach(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_reach(self):
"""Mark a goal as reached.
If the target goal condition is not met, the state will be reset to In
Progress at the next goal update until the end date."""
- return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
+ return self.write({'state': 'reached'})
- def action_fail(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_fail(self):
"""Set the state of the goal to failed.
A failed goal will be ignored in future checks."""
- return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
+ return self.write({'state': 'failed'})
- def action_cancel(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_cancel(self):
"""Reset the completion after setting a goal as reached or failed.
- This is only the current state, if the date and/or target criterias
+ This is only the current state, if the date and/or target criteria
match the conditions for a change of state, this will be applied at the
next goal update."""
- return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
+ return self.write({'state': 'inprogress'})
- def create(self, cr, uid, vals, context=None):
- """Overwrite the create method to add a 'no_remind_goal' field to True"""
- context = dict(context or {})
- context['no_remind_goal'] = True
- return super(gamification_goal, self).create(cr, uid, vals, context=context)
+ @api.model
+ def create(self, vals):
+ return super(Goal, self.with_context(no_remind_goal=True)).create(vals)
- def write(self, cr, uid, ids, vals, context=None):
+ @api.multi
+ def write(self, vals):
"""Overwrite the write method to update the last_update field to today
If the current value is changed and the report frequency is set to On
change, a report is generated
"""
- if context is None:
- context = {}
- vals['last_update'] = fields.date.today()
- result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
- for goal in self.browse(cr, uid, ids, context=context):
+ vals['last_update'] = fields.Date.today()
+ result = super(Goal, self).write(vals)
+ for goal in self:
if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals):
# avoid drag&drop in kanban view
- raise UserError(_('Can not modify the configuration of a started goal'))
+ raise exceptions.UserError(_('Can not modify the configuration of a started goal'))
- if vals.get('current'):
- if 'no_remind_goal' in context:
- # new goals should not be reported
- continue
-
- if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange':
- self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context)
+ if vals.get('current') and 'no_remind_goal' not in self.env.context:
+ if goal.challenge_id.report_message_frequency == 'onchange':
+ goal.challenge_id.sudo().report_progress(users=goal.user_id)
return result
- def get_action(self, cr, uid, goal_id, context=None):
+ @api.multi
+ def get_action(self):
"""Get the ir.action related to update the goal
In case of a manual goal, should return a wizard to update the value
- :return: action description in a dictionnary
+ :return: action description in a dictionary
"""
- goal = self.browse(cr, uid, goal_id, context=context)
-
- if goal.definition_id.action_id:
+ if self.definition_id.action_id:
# open a the action linked to the goal
- action = goal.definition_id.action_id.read()[0]
+ action = self.definition_id.action_id.read()[0]
- if goal.definition_id.res_id_field:
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user})
+ if self.definition_id.res_id_field:
+ current_user = self.env.user.sudo(self.env.user)
+ action['res_id'] = safe_eval(self.definition_id.res_id_field, {
+ 'user': current_user
+ })
# if one element to display, should see it in form mode if possible
- action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views']
+ action['views'] = [
+ (view_id, mode)
+ for (view_id, mode) in action['views']
+ if mode == 'form'
+ ] or action['views']
return action
- if goal.computation_mode == 'manually':
+ if self.computation_mode == 'manually':
# open a wizard window to update the value manually
action = {
- 'name': _("Update %s") % goal.definition_id.name,
- 'id': goal_id,
+ 'name': _("Update %s") % self.definition_id.name,
+ 'id': self.id,
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'target': 'new',
- 'context': {'default_goal_id': goal_id, 'default_current': goal.current},
+ 'context': {'default_goal_id': self.id, 'default_current': self.current},
'res_model': 'gamification.goal.wizard'
}
return action
diff --git a/addons/gamification/security/gamification_security.xml b/addons/gamification/security/gamification_security.xml
index 2609590a67e..a9e71a57c20 100644
--- a/addons/gamification/security/gamification_security.xml
+++ b/addons/gamification/security/gamification_security.xml
@@ -1,5 +1,4 @@
-
-
+Manager
@@ -43,4 +42,4 @@
-
+
diff --git a/addons/gamification/tests/test_challenge.py b/addons/gamification/tests/test_challenge.py
index 2b1060b0031..5b4e4e9dd80 100644
--- a/addons/gamification/tests/test_challenge.py
+++ b/addons/gamification/tests/test_challenge.py
@@ -1,72 +1,52 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.tests import common
+from odoo.tests import common
class test_challenge(common.TransactionCase):
- def setUp(self):
- super(test_challenge, self).setUp()
- cr, uid = self.cr, self.uid
- self.data_obj = self.registry('ir.model.data')
- self.user_obj = self.registry('res.users')
-
- self.challenge_obj = self.registry('gamification.challenge')
- self.line_obj = self.registry('gamification.challenge.line')
- self.goal_obj = self.registry('gamification.goal')
- self.badge_obj = self.registry('gamification.badge')
- self.badge_user_obj = self.registry('gamification.badge.user')
-
- self.demo_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'user_demo')[1]
- self.group_user_id = self.data_obj.get_object_reference(cr, uid, 'base', 'group_user')[1]
- self.challenge_base_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'challenge_base_discover')[1]
- self.definition_timezone_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'definition_base_timezone')[1]
- self.badge_id = self.data_obj.get_object_reference(cr, uid, 'gamification', 'badge_good_job')[1]
-
def test_00_join_challenge(self):
- cr, uid, context = self.cr, self.uid, {}
-
- user_ids = self.user_obj.search(cr, uid, [('groups_id', '=', self.group_user_id)])
- challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
+ employees_group = self.env.ref('base.group_user')
+ user_ids = employees_group.users
+ challenge = self.env.ref('gamification.challenge_base_discover')
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids), "Not enough users in base challenge")
- self.user_obj.create(cr, uid, {
+ self.env['res.users'].with_context(no_reset_password=True).create({
'name': 'R2D2',
'login': 'r2d2@openerp.com',
'email': 'r2d2@openerp.com',
- 'groups_id': [(6, 0, [self.group_user_id])]
- }, {'no_reset_password': True})
+ 'groups_id': [(6, 0, [employees_group.id])]
+ })
- self.challenge_obj._update_all(cr, uid, [self.challenge_base_id], context=context)
- challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
+ challenge._update_all()
self.assertGreaterEqual(len(challenge.user_ids), len(user_ids)+1, "These are not droids you are looking for")
def test_10_reach_challenge(self):
- cr, uid, context = self.cr, self.uid, {}
-
- self.challenge_obj.write(cr, uid, [self.challenge_base_id], {'state': 'inprogress'}, context=context)
- challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context)
- challenge_user_ids = [user.id for user in challenge.user_ids]
+ Goals = self.env['gamification.goal']
+ challenge = self.env.ref('gamification.challenge_base_discover')
+ challenge.state = 'inprogress'
self.assertEqual(challenge.state, 'inprogress', "Challenge failed the change of state")
- line_ids = self.line_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id)], context=context)
- goal_ids = self.goal_obj.search(cr, uid, [('challenge_id', '=', self.challenge_base_id), ('state', '!=', 'draft')], context=context)
- self.assertEqual(len(goal_ids), len(line_ids)*len(challenge_user_ids), "Incorrect number of goals generated, should be 1 goal per user, per challenge line")
+ goal_ids = Goals.search([('challenge_id', '=', challenge.id), ('state', '!=', 'draft')])
+ self.assertEqual(len(goal_ids), len(challenge.line_ids) * len(challenge.user_ids.ids), "Incorrect number of goals generated, should be 1 goal per user, per challenge line")
+ demo = self.env.ref('base.user_demo')
# demo user will set a timezone
- self.user_obj.write(cr, uid, self.demo_user_id, {'tz': "Europe/Brussels"}, context=context)
- goal_ids = self.goal_obj.search(cr, uid, [('user_id', '=', self.demo_user_id), ('definition_id', '=', self.definition_timezone_id)], context=context)
+ demo.tz = "Europe/Brussels"
+ goal_ids = Goals.search([('user_id', '=', demo.id), ('definition_id', '=', self.env.ref('gamification.definition_base_timezone').id)])
- self.goal_obj.update_goal(cr, uid, goal_ids, context=context)
- reached_goal_ids = self.goal_obj.search(cr, uid, [('id', 'in', goal_ids), ('state', '=', 'reached')], context=context)
- self.assertEqual(set(goal_ids), set(reached_goal_ids), "Not every goal was reached after changing timezone")
+ goal_ids.update_goal()
+
+ missed = goal_ids.filtered(lambda g: g.state != 'reached')
+ self.assertFalse(missed, "Not every goal was reached after changing timezone")
# reward for two firsts as admin may have timezone
- self.challenge_obj.write(cr, uid, self.challenge_base_id, {'reward_first_id': self.badge_id, 'reward_second_id': self.badge_id}, context=context)
- self.challenge_obj.write(cr, uid, self.challenge_base_id, {'state': 'done'}, context=context)
+ badge_id = self.env.ref('gamification.badge_good_job').id
+ challenge.write({'reward_first_id': badge_id, 'reward_second_id': badge_id})
+ challenge.state = 'done'
- badge_ids = self.badge_user_obj.search(cr, uid, [('badge_id', '=', self.badge_id), ('user_id', '=', self.demo_user_id)])
- self.assertGreater(len(badge_ids), 0, "Demo user has not received the badge")
\ No newline at end of file
+ badge_ids = self.env['gamification.badge.user'].search([('badge_id', '=', badge_id), ('user_id', '=', demo.id)])
+ self.assertEqual(len(badge_ids), 1, "Demo user has not received the badge")
diff --git a/addons/gamification/views/badge.xml b/addons/gamification/views/badge.xml
index a2618ecfa29..64fb68732ed 100644
--- a/addons/gamification/views/badge.xml
+++ b/addons/gamification/views/badge.xml
@@ -1,188 +1,184 @@
-
-
-
+
+
+
+ Badges
+ gamification.badge
+ kanban,tree,form
+
+
+ Click to create a badge.
+
+
+ A badge is a symbolic token granted to a user as a sign of reward.
+ It can be deserved automatically when some conditions are met or manually by users.
+ Some badges are harder than others to get with specific conditions.
+
- A badge is a symbolic token granted to a user as a sign of reward.
- It can be deserved automatically when some conditions are met or manually by users.
- Some badges are harder than others to get with specific conditions.
-