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)