diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
index 66dd7be7431..428cc357047 100644
--- a/addons/gamification/data/goal_base.xml
+++ b/addons/gamification/data/goal_base.xml
@@ -13,8 +13,6 @@
You have not updated your progress for the goal ${object.definition_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.
-
- If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.
]]>
diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py
index 1d879393ed2..eb97b55ab07 100644
--- a/addons/gamification/models/badge.py
+++ b/addons/gamification/models/badge.py
@@ -39,7 +39,8 @@ class gamification_badge_user(osv.Model):
_columns = {
'user_id': fields.many2one('res.users', string="User", required=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),
+ 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True, ondelete="cascade"),
+ '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),
@@ -263,7 +264,7 @@ class gamification_badge(osv.Model):
def check_progress(self, cr, uid, context=None):
try:
- model, res_id = template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden')
+ 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')
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index 06d0c44baac..b5f9369ef1b 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -163,6 +163,8 @@ class gamification_challenge(osv.Model):
'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."),
'visibility_mode': fields.selection([
('personal', 'Individual Goals'),
@@ -257,7 +259,7 @@ class gamification_challenge(osv.Model):
elif vals.get('state') == 'draft':
# resetting progress
- if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context):
+ if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context):
raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
@@ -280,13 +282,13 @@ class gamification_challenge(osv.Model):
- Create the missing goals (eg: modified the challenge to add lines)
- Update every running challenge
"""
- # start planned challenges
+ # start scheduled challenges
planned_challenge_ids = self.search(cr, uid, [
('state', '=', 'draft'),
('start_date', '<=', fields.date.today())])
self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
- # close planned challenges
+ # close scheduled challenges
planned_challenge_ids = self.search(cr, uid, [
('state', '=', 'inprogress'),
('end_date', '>=', fields.date.today())])
@@ -312,7 +314,7 @@ class gamification_challenge(osv.Model):
goal_ids = goal_obj.search(cr, uid, [
('challenge_id', 'in', ids),
'|',
- ('state', 'in', ('inprogress', 'inprogress_update')),
+ ('state', '=', 'inprogress'),
'&',
('state', 'in', ('reached', 'failed')),
'|',
@@ -328,6 +330,8 @@ class gamification_challenge(osv.Model):
self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
+ # goal_group = goal_obj.read_group(cr, uid, [('challenge_id', '=', challenge.id), ('closed', '=', False)], fields=['id', 'line_id', 'target_goal'], groupby=['line_id'], context=context)
+
# goals closed but still opened at the last report date
closed_goals_to_report = goal_obj.search(cr, uid, [
('challenge_id', '=', challenge.id),
@@ -349,6 +353,7 @@ class gamification_challenge(osv.Model):
"""Update all the goals of a 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(cr, uid, goal_ids, context=context)
+ print self.pool.get('gamification.goal').read_group(cr, uid, [('challenge_id', '=', challenge_id), ('closed', '=', False)], fields=['id', 'line_id', 'target_goal'], groupby=['line_id'], context=context)
return True
@@ -377,6 +382,7 @@ class gamification_challenge(osv.Model):
can be called after each change in the list of users or lines.
:param list(int) ids: the list of challenge concerned"""
+ to_update = []
for challenge in self.browse(cr, uid, ids, context):
(start_date, end_date) = start_end_date_for_period(challenge.period)
@@ -403,7 +409,7 @@ class gamification_challenge(osv.Model):
canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
if canceled_goal_ids:
goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
- goal_obj.update(cr, uid, canceled_goal_ids, context=context)
+ to_update.extend(canceled_goal_ids)
# skip to next user
continue
@@ -425,8 +431,9 @@ class gamification_challenge(osv.Model):
values['remind_update_delay'] = challenge.remind_update_delay
new_goal_id = goal_obj.create(cr, uid, values, context)
+ to_update.append(new_goal_id)
- goal_obj.update(cr, uid, [new_goal_id], context=context)
+ goal_obj.update(cr, uid, to_update, context=context)
return True
@@ -460,7 +467,7 @@ class gamification_challenge(osv.Model):
'rank': ,
'user_id': ,
'name': ,
- 'state': ,
+ 'state': ,
'completeness': ,
'current': ,
}
@@ -478,7 +485,7 @@ class gamification_challenge(osv.Model):
'action': <{True,False}>,
'display_mode': <{progress,boolean}>,
'target': ,
- 'state': ,
+ 'state': ,
'completeness': ,
'current': ,
}
@@ -545,7 +552,7 @@ class gamification_challenge(osv.Model):
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, over top, skipping
+ # not own goal and too low to be in top
continue
line_data['goals'].append({
diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py
index 925b05de5ab..80b2226e329 100644
--- a/addons/gamification/models/goal.py
+++ b/addons/gamification/models/goal.py
@@ -88,8 +88,16 @@ class gamification_goal_definition(osv.Model):
string='Date Field',
help='The date to use for the time period evaluated'),
'domain': fields.char("Filter Domain",
- help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].",
+ 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("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([
@@ -102,7 +110,7 @@ class gamification_goal_definition(osv.Model):
'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.")
+ help="The field name on the user profile (res.users) containing the value for res_id for action."),
}
_defaults = {
@@ -158,7 +166,7 @@ class gamification_goal(osv.Model):
_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),
- 'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', 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',
@@ -175,7 +183,6 @@ class gamification_goal(osv.Model):
'state': fields.selection([
('draft', 'Draft'),
('inprogress', 'In progress'),
- ('inprogress_update', 'In progress (to update)'),
('reached', 'Reached'),
('failed', 'Failed'),
('canceled', 'Canceled'),
@@ -183,6 +190,8 @@ class gamification_goal(osv.Model):
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',
@@ -212,14 +221,14 @@ class gamification_goal(osv.Model):
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 and goal.state == 'inprogress':
+ if date.today() - last_update > delta_max:
# generate a remind report
temp_obj = self.pool.get('email.template')
template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
- return {'state': 'inprogress_update'}
+ return {'to_update': True}
return {}
def update(self, cr, uid, ids, context=None):
@@ -233,71 +242,123 @@ class gamification_goal(osv.Model):
if context is None:
context = {}
+ goals_by_definition = {}
+ goals_to_write = {}
+ all_goals = {}
for goal in self.browse(cr, uid, ids, context=context):
- towrite = {}
if goal.state in ('draft', 'canceled'):
- # skip if goal draft or canceled
+ # draft or canceled goals should not be recomputed
continue
- if goal.definition_id.computation_mode == 'manually':
- towrite.update(self._check_remind_delay(cr, uid, goal, context))
+ goals_by_definition.setdefault(goal.definition_id, []).append(goal)
+ goals_to_write[goal.id] = {}
+ all_goals[goal.id] = goal
- elif goal.definition_id.computation_mode == 'python':
- # execute the chosen method
- cxt = {
- 'self': self.pool.get('gamification.goal'),
- 'object': goal,
- 'pool': self.pool,
- 'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
- 'uid': uid,
- 'result': False,
- 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time
- }
- code = goal.definition_id.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', False)
- if result and type(result) in (float, int, long):
- if result != goal.current:
- towrite['current'] = result
- else:
- _logger.exception(_('Invalid return content from the evaluation of %s' % code))
+ for definition, goals in goals_by_definition.items():
+ if definition.computation_mode == 'manually':
+ for goal in goals:
+ goals_to_write[goal.id].update(self._check_remind_delay(cr, uid, goal, context))
+ elif definition.computation_mode == 'python':
+ # TODO batch execution
+ for goal in goals:
+ # execute the chosen method
+ cxt = {
+ 'self': self.pool.get('gamification.goal'),
+ 'object': goal,
+ 'pool': self.pool,
+ 'cr': cr,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'uid': uid,
+ 'result': False,
+ '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', False)
+ if result and type(result) in (float, int, long):
+ if result != goal.current:
+ goals_to_write[goal.id]['current'] = result
+ else:
+ _logger.exception(_('Invalid return content from the evaluation of code for definition %s' % definition.name))
else: # count or sum
- obj = self.pool.get(goal.definition_id.model_id.model)
- field_date_name = goal.definition_id.field_date_id.name
- # eval the domain with user replaced by goal user object
- domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id})
+ obj = self.pool.get(definition.model_id.model)
+ field_date_name = definition.field_date_id and definition.field_date_id.name or False
- # add temporal clause(s) to the domain if fields are filled on the goal
- if goal.start_date and field_date_name:
- domain.append((field_date_name, '>=', goal.start_date))
- if goal.end_date and field_date_name:
- domain.append((field_date_name, '<=', goal.end_date))
+ if definition.computation_mode == 'count' and definition.batch_mode:
- if goal.definition_id.computation_mode == 'sum':
- field_name = goal.definition_id.field_id.name
- res = obj.read_group(cr, uid, domain, [field_name], [field_name], context=context)
- new_value = res and res[0][field_name] or 0.0
+ general_domain = safe_eval(definition.domain)
+ # goal_distinct_values = {goal.id: safe_eval(definition.batch_user_expression, {'user': goal.user_id}) for goal in goals}
+ field_name = definition.batch_distinctive_field.name
+ # general_domain.append((field_name, 'in', list(set(goal_distinct_values.keys()))))
+ subqueries = {}
+ for goal in goals:
+ start_date = field_date_name and goal.start_date or False
+ end_date = field_date_name and goal.end_date or False
+ subqueries.setdefault((start_date, end_date), {}).update({goal.id:safe_eval(definition.batch_user_expression, {'user': goal.user_id})})
- else: # computation mode = count
- new_value = obj.search(cr, uid, domain, context=context, count=True)
+ for (start_date, end_date), query_goals in subqueries.items():
+ subquery_domain = list(general_domain)
+ subquery_domain.append((field_name, 'in', list(set(query_goals.values()))))
+ if start_date:
+ subquery_domain.append((field_date_name, '>=', start_date))
+ if end_date:
+ subquery_domain.append((field_date_name, '>=', end_date))
- # avoid useless write if the new value is the same as the old one
- if new_value != goal.current:
- towrite['current'] = new_value
+ user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context)
+
+ for goal in [g for g in goals if g.id in query_goals.keys()]:
+ for user_value in user_values:
+ # return format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...]
+ 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)
+ if new_value != goal.current:
+ goals_to_write[goal.id]['current'] = new_value
+
+ else:
+ for goal in goals:
+ # eval the domain with user replaced by goal user object
+ domain = safe_eval(definition.domain, {'user': goal.user_id})
+
+ # add temporal clause(s) to the domain if fields are filled on the goal
+ if goal.start_date and field_date_name:
+ domain.append((field_date_name, '>=', goal.start_date))
+ if goal.end_date and field_date_name:
+ domain.append((field_date_name, '<=', goal.end_date))
+
+ if definition.computation_mode == 'sum':
+ field_name = definition.field_id.name
+ res = obj.read_group(cr, uid, domain, [field_name], [field_name], context=context)
+ 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)
+
+ # avoid useless write if the new value is the same as the old one
+ if new_value != goal.current:
+ goals_to_write[goal.id]['current'] = new_value
+
+ for goal_id, value in goals_to_write.items():
+ if not value:
+ continue
+ goal = all_goals[goal_id]
# check goal target reached
- if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
- towrite['state'] = 'reached'
+ if (goal.definition_condition == 'higher' and value.get('current', goal.current) >= goal.target_goal) \
+ or (goal.definition_condition == 'lower' and value.get('current', goal.current) <= goal.target_goal):
+ value['state'] = 'reached'
# check goal failure
elif goal.end_date and fields.date.today() > goal.end_date:
- towrite['state'] = 'failed'
- if towrite:
- self.write(cr, uid, [goal.id], towrite, context=context)
+ value['state'] = 'failed'
+ value['closed'] = True
+ if value:
+ self.write(cr, uid, [goal.id], value, context=context)
return True
def action_start(self, cr, uid, ids, context=None):
diff --git a/addons/gamification/static/img/badge_hidden-image.png b/addons/gamification/static/img/badge_hidden-image.png
index 2b9040d5764..1c5cf7b6f5d 100644
Binary files a/addons/gamification/static/img/badge_hidden-image.png and b/addons/gamification/static/img/badge_hidden-image.png differ
diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js
index 2a8db5f369d..242f67d8759 100644
--- a/addons/gamification/static/src/js/gamification.js
+++ b/addons/gamification/static/src/js/gamification.js
@@ -49,21 +49,6 @@ openerp.gamification = function(instance) {
self.get_goal_todo_info();
});
});
- },
- 'click .oe_goal h4': function(event) {
- var self = this;
- this.kkeys = [];
- $(document).on('keydown.klistener', function(event) {
- if ("37,38,39,40,65,66".indexOf(event.keyCode) < 0) {
- $(document).off('keydown.klistener');
- } else {
- self.kkeys.push(event.keyCode);
- if (self.kkeys.toString().indexOf("38,38,40,40,37,39,37,39,66,65") >= 0) {
- new instance.web.Model('gamification.badge').call('check_progress', []);
- $(document).off('keydown.klistener');
- }
- }
- });
}
},
start: function() {
@@ -126,6 +111,13 @@ openerp.gamification = function(instance) {
}
});
+ instance.web.WebClient.include({
+ to_kitten: function() {
+ this._super();
+ new instance.web.Model('gamification.badge').call('check_progress', []);
+ }
+ });
+
instance.mail.Widget.include({
start: function() {
this._super();
diff --git a/addons/gamification/views/goal.xml b/addons/gamification/views/goal.xml
index 1c408e3ab11..528d5db0511 100644
--- a/addons/gamification/views/goal.xml
+++ b/addons/gamification/views/goal.xml
@@ -45,8 +45,8 @@
@@ -105,7 +105,7 @@
@@ -157,7 +157,7 @@
W
- N
+ N
X
@@ -241,15 +241,23 @@
-
-
+
+
+
+
+ In batch mode, the domain is evaluated globally. If enabled, do not use keyword 'user' in above filter domain.
+
+
+
-
+
diff --git a/addons/gamification/wizard/update_goal.py b/addons/gamification/wizard/update_goal.py
index cbda3fefbce..95ba76299ec 100644
--- a/addons/gamification/wizard/update_goal.py
+++ b/addons/gamification/wizard/update_goal.py
@@ -38,6 +38,7 @@ class goal_manual_wizard(osv.TransientModel):
towrite = {
'current': wiz.current,
'goal_id': wiz.goal_id.id,
+ 'to_update': False,
}
goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
goal_obj.update(cr, uid, [wiz.goal_id.id], context=context)