1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
22 from openerp import SUPERUSER_ID
23 from openerp.osv import fields, osv
24 from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT as DF
25 from openerp.tools.safe_eval import safe_eval as eval
26 from openerp.tools.translate import _
28 from datetime import date, datetime, timedelta
31 _logger = logging.getLogger(__name__)
33 # display top 3 in ranking, could be db variable
34 MAX_VISIBILITY_RANKING = 3
36 def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
37 """Return the start and end date for a goal period based on today
39 :return: (start_date, end_date), datetime.date objects, False if the period is
40 not defined or unknown"""
45 elif period == 'weekly':
46 delta = timedelta(days=today.weekday())
47 start_date = today - delta
48 end_date = start_date + timedelta(days=7)
49 elif period == 'monthly':
50 month_range = calendar.monthrange(today.year, today.month)
51 start_date = today.replace(day=1)
52 end_date = today.replace(day=month_range[1])
53 elif period == 'yearly':
54 start_date = today.replace(month=1, day=1)
55 end_date = today.replace(month=12, day=31)
56 else: # period == 'once':
57 start_date = default_start_date # for manual goal, start each time
58 end_date = default_end_date
60 if start_date and end_date:
61 return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF))
63 return (start_date, end_date)
66 class gamification_challenge(osv.Model):
67 """Gamification challenge
69 Set of predifined objectives assigned to people with rules for recurrence and
72 If 'user_ids' is defined and 'period' is different than 'one', the set will
73 be assigned to the users for each period (eg: every 1st of each month if
74 'monthly' is selected)
77 _name = 'gamification.challenge'
78 _description = 'Gamification challenge'
79 _inherit = 'mail.thread'
81 def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
82 """Return the next report date based on the last report date and report
85 :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
87 for challenge in self.browse(cr, uid, ids, context=context):
88 last = datetime.strptime(challenge.last_report_date, DF).date()
89 if challenge.report_message_frequency == 'daily':
90 next = last + timedelta(days=1)
91 res[challenge.id] = next.strftime(DF)
92 elif challenge.report_message_frequency == 'weekly':
93 next = last + timedelta(days=7)
94 res[challenge.id] = next.strftime(DF)
95 elif challenge.report_message_frequency == 'monthly':
96 month_range = calendar.monthrange(last.year, last.month)
97 next = last.replace(day=month_range[1]) + timedelta(days=1)
98 res[challenge.id] = next.strftime(DF)
99 elif challenge.report_message_frequency == 'yearly':
100 res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
101 # frequency == 'once', reported when closed only
103 res[challenge.id] = False
107 def _get_categories(self, cr, uid, context=None):
109 ('hr', 'Human Ressources / Engagement'),
110 ('other', 'Settings / Gamification Tools'),
113 def _get_report_template(self, cr, uid, context=None):
115 return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
119 _order = 'end_date, start_date, name, id'
121 'name': fields.char('Challenge Name', required=True, translate=True),
122 'description': fields.text('Description', translate=True),
123 'state': fields.selection([
125 ('inprogress', 'In Progress'),
128 string='State', required=True, track_visibility='onchange'),
129 'manager_id': fields.many2one('res.users',
130 string='Responsible', help="The user responsible for the challenge."),
132 'user_ids': fields.many2many('res.users', 'gamification_challenge_users_rel',
134 help="List of users participating to the challenge"),
135 'user_domain': fields.char('User domain', help="Alternative to a list of users"),
137 'period': fields.selection([
138 ('once', 'Non recurring'),
140 ('weekly', 'Weekly'),
141 ('monthly', 'Monthly'),
144 string='Periodicity',
145 help='Period of automatic goal assigment. If none is selected, should be launched manually.',
147 'start_date': fields.date('Start Date',
148 help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
149 'end_date': fields.date('End Date',
150 help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
152 'invited_user_ids': fields.many2many('res.users', 'gamification_invited_user_ids_rel',
153 string="Suggest to users"),
155 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
157 help="List of goals that will be set",
158 required=True, copy=True),
160 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
161 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
162 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
163 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
164 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
165 'reward_realtime': fields.boolean('Reward as soon as every goal is reached',
166 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."),
168 'visibility_mode': fields.selection([
169 ('personal', 'Individual Goals'),
170 ('ranking', 'Leader Board (Group Ranking)'),
172 string="Display Mode", required=True),
174 'report_message_frequency': fields.selection([
176 ('onchange', 'On change'),
178 ('weekly', 'Weekly'),
179 ('monthly', 'Monthly'),
182 string="Report Frequency", required=True),
183 'report_message_group_id': fields.many2one('mail.group',
184 string='Send a copy to',
185 help='Group that will receive a copy of the report in addition to the user'),
186 'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
187 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
188 help="Never reminded if no value or zero is specified."),
189 'last_report_date': fields.date('Last Report Date'),
190 'next_report_date': fields.function(_get_next_report_date,
191 type='date', string='Next Report Date', store=True),
193 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
194 string="Appears in", help="Define the visibility of the challenge through menus", required=True),
200 'visibility_mode': 'personal',
201 'report_message_frequency': 'never',
202 'last_report_date': fields.date.today,
203 'manager_id': lambda s, cr, uid, c: uid,
205 'reward_failure': False,
206 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
207 'reward_realtime': True,
211 def create(self, cr, uid, vals, context=None):
212 """Overwrite the create method to add the user of groups"""
214 if vals.get('user_domain'):
215 user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
217 if not vals.get('user_ids'):
218 vals['user_ids'] = []
219 vals['user_ids'] += [(4, user_id) for user_id in user_ids]
221 return super(gamification_challenge, self).create(cr, uid, vals, context=context)
223 def write(self, cr, uid, ids, vals, context=None):
224 if isinstance(ids, (int,long)):
227 if vals.get('user_domain'):
228 user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
230 if not vals.get('user_ids'):
231 vals['user_ids'] = []
232 vals['user_ids'] += [(4, user_id) for user_id in user_ids]
234 write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
236 if vals.get('report_message_frequency', 'never') != 'never':
237 # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now
238 for challenge in self.browse(cr, uid, ids, context=context):
239 self.message_subscribe(cr, uid, [challenge.id], [user.partner_id.id for user in challenge.user_ids], context=context)
241 if vals.get('state') == 'inprogress':
242 self._recompute_challenge_users(cr, uid, ids, context=context)
243 self._generate_goals_from_challenge(cr, uid, ids, context=context)
245 elif vals.get('state') == 'done':
246 self.check_challenge_reward(cr, uid, ids, force=True, context=context)
248 elif vals.get('state') == 'draft':
250 if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context):
251 raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
258 def _cron_update(self, cr, uid, context=None, ids=False):
261 - Start planned challenges (in draft and with start_date = today)
262 - Create the missing goals (eg: modified the challenge to add lines)
263 - Update every running challenge
268 # start scheduled challenges
269 planned_challenge_ids = self.search(cr, uid, [
270 ('state', '=', 'draft'),
271 ('start_date', '<=', fields.date.today())])
272 if planned_challenge_ids:
273 self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
275 # close scheduled challenges
276 planned_challenge_ids = self.search(cr, uid, [
277 ('state', '=', 'inprogress'),
278 ('end_date', '>=', fields.date.today())])
279 if planned_challenge_ids:
280 self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
283 ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
285 # in cron mode, will do intermediate commits
286 # TODO in trunk: replace by parameter
287 context = dict(context, commit_gamification=True)
288 return self._update_all(cr, uid, ids, context=context)
290 def _update_all(self, cr, uid, ids, context=None):
291 """Update the challenges and related goals
293 :param list(int) ids: the ids of the challenges to update, if False will
294 update only challenges in progress."""
298 if isinstance(ids, (int,long)):
301 goal_obj = self.pool.get('gamification.goal')
303 # we use yesterday to update the goals that just ended
304 yesterday = date.today() - timedelta(days=1)
305 goal_ids = goal_obj.search(cr, uid, [
306 ('challenge_id', 'in', ids),
308 ('state', '=', 'inprogress'),
310 ('state', 'in', ('reached', 'failed')),
312 ('end_date', '>=', yesterday.strftime(DF)),
313 ('end_date', '=', False)
315 # update every running goal already generated linked to selected challenges
316 goal_obj.update(cr, uid, goal_ids, context=context)
318 self._recompute_challenge_users(cr, uid, ids, context=context)
319 self._generate_goals_from_challenge(cr, uid, ids, context=context)
321 for challenge in self.browse(cr, uid, ids, context=context):
323 if challenge.last_report_date != fields.date.today():
324 # goals closed but still opened at the last report date
325 closed_goals_to_report = goal_obj.search(cr, uid, [
326 ('challenge_id', '=', challenge.id),
327 ('start_date', '>=', challenge.last_report_date),
328 ('end_date', '<=', challenge.last_report_date)
331 if challenge.next_report_date and fields.date.today() >= challenge.next_report_date:
332 self.report_progress(cr, uid, challenge, context=context)
334 elif len(closed_goals_to_report) > 0:
335 # some goals need a final report
336 self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
338 self.check_challenge_reward(cr, uid, ids, context=context)
341 def quick_update(self, cr, uid, challenge_id, context=None):
342 """Update all the goals of a specific challenge, no generation of new goals"""
343 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
344 self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
347 def _get_challenger_users(self, cr, uid, domain, context=None):
348 user_domain = eval(ustr(domain))
349 return self.pool['res.users'].search(cr, uid, user_domain, context=context)
351 def _recompute_challenge_users(self, cr, uid, challenge_ids, context=None):
352 """Recompute the domain to add new users and remove the one no longer matching the domain"""
353 for challenge in self.browse(cr, uid, challenge_ids, context=context):
354 if challenge.user_domain:
356 old_user_ids = [user.id for user in challenge.user_ids]
357 new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
358 to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
359 to_add_ids = list(set(new_user_ids) - set(old_user_ids))
361 write_op = [(3, user_id) for user_id in to_remove_ids]
362 write_op += [(4, user_id) for user_id in to_add_ids]
364 self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
368 def action_start(self, cr, uid, ids, context=None):
369 """Start a challenge"""
370 return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
372 def action_check(self, cr, uid, ids, context=None):
375 Create goals that haven't been created yet (eg: if added users)
376 Recompute the current value for each goal related"""
377 return self._update_all(cr, uid, ids=ids, context=context)
379 def action_report_progress(self, cr, uid, ids, context=None):
380 """Manual report of a goal, does not influence automatic report frequency"""
381 if isinstance(ids, (int,long)):
383 for challenge in self.browse(cr, uid, ids, context=context):
384 self.report_progress(cr, uid, challenge, context=context)
388 ##### Automatic actions #####
390 def _generate_goals_from_challenge(self, cr, uid, ids, context=None):
391 """Generate the goals for each line and user.
393 If goals already exist for this line and user, the line is skipped. This
394 can be called after each change in the list of users or lines.
395 :param list(int) ids: the list of challenge concerned"""
397 goal_obj = self.pool.get('gamification.goal')
398 for challenge in self.browse(cr, uid, ids, context=context):
399 (start_date, end_date) = start_end_date_for_period(challenge.period)
402 # if no periodicity, use challenge dates
403 if not start_date and challenge.start_date:
404 start_date = challenge.start_date
405 if not end_date and challenge.end_date:
406 end_date = challenge.end_date
408 for line in challenge.line_ids:
410 # there is potentially a lot of users
411 # detect the ones with no goal linked to this line
413 query_params = [line.id]
415 date_clause += "AND g.start_date = %s"
416 query_params.append(start_date)
418 date_clause += "AND g.end_date = %s"
419 query_params.append(end_date)
421 query = """SELECT u.id AS user_id
423 LEFT JOIN gamification_goal g
424 ON (u.id = g.user_id)
427 """.format(date_clause=date_clause)
429 cr.execute(query, query_params)
430 user_with_goal_ids = cr.dictfetchall()
432 participant_user_ids = [user.id for user in challenge.user_ids]
433 user_without_goal_ids = list(set(participant_user_ids) - set([user['user_id'] for user in user_with_goal_ids]))
434 user_squating_challenge_ids = list(set([user['user_id'] for user in user_with_goal_ids]) - set(participant_user_ids))
435 if user_squating_challenge_ids:
436 # users that used to match the challenge
437 goal_to_remove_ids = goal_obj.search(cr, uid, [('challenge_id', '=', challenge.id), ('user_id', 'in', user_squating_challenge_ids)], context=context)
438 goal_obj.unlink(cr, uid, goal_to_remove_ids, context=context)
442 'definition_id': line.definition_id.id,
444 'target_goal': line.target_goal,
445 'state': 'inprogress',
449 values['start_date'] = start_date
451 values['end_date'] = end_date
453 # the goal is initialised over the limit to make sure we will compute it at least once
454 if line.condition == 'higher':
455 values['current'] = line.target_goal - 1
457 values['current'] = line.target_goal + 1
459 if challenge.remind_update_delay:
460 values['remind_update_delay'] = challenge.remind_update_delay
462 for user_id in user_without_goal_ids:
463 values.update({'user_id': user_id})
464 goal_id = goal_obj.create(cr, uid, values, context=context)
465 to_update.append(goal_id)
467 goal_obj.update(cr, uid, to_update, context=context)
471 ##### JS utilities #####
473 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
474 """Return a serialised version of the goals information if the user has not completed every goal
476 :challenge: browse record of challenge to compute
477 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
478 :restrict_goal_ids: <list(int)> compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge
479 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
480 restrict_goal_ids has priority over restrict_top
483 # if visibility_mode == 'ranking'
485 'name': <gamification.goal.description name>,
486 'description': <gamification.goal.description description>,
487 'condition': <reach condition {lower,higher}>,
488 'computation_mode': <target computation {manually,count,sum,python}>,
489 'monetary': <{True,False}>,
490 'suffix': <value suffix>,
491 'action': <{True,False}>,
492 'display_mode': <{progress,boolean}>,
493 'target': <challenge line target>,
494 'own_goal_id': <gamification.goal id where user_id == uid>,
497 'id': <gamification.goal id>,
498 'rank': <user ranking>,
499 'user_id': <res.users id>,
500 'name': <res.users name>,
501 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
502 'completeness': <percentage>,
503 'current': <current value>,
507 # if visibility_mode == 'personal'
509 'id': <gamification.goal id>,
510 'name': <gamification.goal.description name>,
511 'description': <gamification.goal.description description>,
512 'condition': <reach condition {lower,higher}>,
513 'computation_mode': <target computation {manually,count,sum,python}>,
514 'monetary': <{True,False}>,
515 'suffix': <value suffix>,
516 'action': <{True,False}>,
517 'display_mode': <{progress,boolean}>,
518 'target': <challenge line target>,
519 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
520 'completeness': <percentage>,
521 'current': <current value>,
524 goal_obj = self.pool.get('gamification.goal')
525 (start_date, end_date) = start_end_date_for_period(challenge.period)
529 for line in challenge.line_ids:
531 'name': line.definition_id.name,
532 'description': line.definition_id.description,
533 'condition': line.definition_id.condition,
534 'computation_mode': line.definition_id.computation_mode,
535 'monetary': line.definition_id.monetary,
536 'suffix': line.definition_id.suffix,
537 'action': True if line.definition_id.action_id else False,
538 'display_mode': line.definition_id.display_mode,
539 'target': line.target_goal,
542 ('line_id', '=', line.id),
543 ('state', '!=', 'draft'),
545 if restrict_goal_ids:
546 domain.append(('ids', 'in', restrict_goal_ids))
548 # if no subset goals, use the dates for restriction
550 domain.append(('start_date', '=', start_date))
552 domain.append(('end_date', '=', end_date))
554 if challenge.visibility_mode == 'personal':
556 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
557 domain.append(('user_id', '=', user_id))
558 sorting = goal_obj._order
562 'own_goal_id': False,
565 sorting = "completeness desc, current desc"
568 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
570 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
571 if challenge.visibility_mode == 'personal':
572 # limit=1 so only one result
575 'current': goal.current,
576 'completeness': goal.completeness,
579 if goal.state != 'reached':
583 if user_id and goal.user_id.id == user_id:
584 line_data['own_goal_id'] = goal.id
585 elif restrict_top and ranking > restrict_top:
586 # not own goal and too low to be in top
589 line_data['goals'].append({
591 'user_id': goal.user_id.id,
592 'name': goal.user_id.name,
594 'current': goal.current,
595 'completeness': goal.completeness,
598 if goal.state != 'reached':
601 res_lines.append(line_data)
606 ##### Reporting #####
608 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
609 """Post report about the progress of the goals
611 :param challenge: the challenge object that need to be reported
612 :param users: the list(res.users) of users that are concerned by
613 the report. If False, will send the report to every user concerned
614 (goal users and group that receive a copy). Only used for challenge with
615 a visibility mode set to 'personal'.
616 :param goal_ids: the list(int) of goal ids linked to the challenge for
617 the report. If not specified, use the goals for the current challenge
618 period. This parameter can be used to produce report for previous challenge
620 :param subset_goal_ids: a list(int) of goal ids to restrict the report
625 temp_obj = self.pool.get('email.template')
627 if challenge.visibility_mode == 'ranking':
628 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
630 ctx.update({'challenge_lines': lines_boards})
631 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
633 # send to every follower and participant of the challenge
634 self.message_post(cr, uid, challenge.id,
636 partner_ids=[user.partner_id.id for user in challenge.user_ids],
638 subtype='mail.mt_comment')
639 if challenge.report_message_group_id:
640 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
643 subtype='mail.mt_comment')
646 # generate individual reports
647 for user in users or challenge.user_ids:
648 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
652 ctx.update({'challenge_lines': goals})
653 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
655 # send message only to users, not on the challenge
656 self.message_post(cr, uid, 0,
658 partner_ids=[(4, user.partner_id.id)],
660 subtype='mail.mt_comment')
661 if challenge.report_message_group_id:
662 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
665 subtype='mail.mt_comment')
666 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
668 ##### Challenges #####
669 # TODO in trunk, remove unused parameter user_id
670 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
671 """The user accept the suggested challenge"""
672 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
674 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
675 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
676 message = "%s has joined the challenge" % user.name
677 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
678 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
679 return self._generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
681 # TODO in trunk, remove unused parameter user_id
682 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
683 """The user discard the suggested challenge"""
684 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
686 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
687 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
688 message = "%s has refused the challenge" % user.name
689 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
690 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
692 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
693 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
694 id = result and result[1] or False
695 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
696 result['res_id'] = challenge_id
699 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
700 """Actions for the end of a challenge
702 If a reward was selected, grant it to the correct users.
704 - the end date for a challenge with no periodicity
705 - the end of a period for challenge with periodicity
706 - when a challenge is manually closed
707 (if no end date, a running challenge is never rewarded)
709 if isinstance(ids, (int,long)):
711 commit = context.get('commit_gamification', False)
712 for challenge in self.browse(cr, uid, ids, context=context):
713 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
714 yesterday = date.today() - timedelta(days=1)
717 challenge_ended = end_date == yesterday.strftime(DF) or force
718 if challenge.reward_id and (challenge_ended or challenge.reward_realtime):
719 # not using start_date as intemportal goals have a start date but no end_date
720 reached_goals = self.pool.get('gamification.goal').read_group(cr, uid, [
721 ('challenge_id', '=', challenge.id),
722 ('end_date', '=', end_date),
723 ('state', '=', 'reached')
724 ], fields=['user_id'], groupby=['user_id'], context=context)
725 for reach_goals_user in reached_goals:
726 if reach_goals_user['user_id_count'] == len(challenge.line_ids):
727 # the user has succeeded every assigned goal
728 user_id = reach_goals_user['user_id'][0]
729 if challenge.reward_realtime:
730 badges = self.pool['gamification.badge.user'].search(cr, uid, [
731 ('challenge_id', '=', challenge.id),
732 ('badge_id', '=', challenge.reward_id.id),
733 ('user_id', '=', user_id),
734 ], count=True, context=context)
736 # has already recieved the badge for this challenge
738 self.reward_user(cr, uid, user_id, challenge.reward_id.id, challenge.id, context=context)
739 rewarded_users.append(user_id)
744 # open chatter message
745 message_body = _("The challenge %s is finished." % challenge.name)
748 user_names = self.pool['res.users'].name_get(cr, uid, rewarded_users, context=context)
749 message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([name for (user_id, name) in user_names])))
751 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
754 if challenge.reward_first_id:
755 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context=context)
757 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, challenge.id, context=context)
758 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
759 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
761 message_body += _("Nobody reached the required conditions to receive special badges.")
763 if second_user and challenge.reward_second_id:
764 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, challenge.id, context=context)
765 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
766 if third_user and challenge.reward_third_id:
767 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, challenge.id, context=context)
768 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
770 self.message_post(cr, uid, challenge.id,
771 partner_ids=[user.partner_id.id for user in challenge.user_ids],
779 def get_top3_users(self, cr, uid, challenge, context=None):
780 """Get the top 3 users for a defined challenge
783 1. succeed every goal of the challenge
784 2. total completeness of each goal (can be over 100)
785 Top 3 is computed only for users succeeding every goal of the challenge,
786 except if reward_failure is True, in which case every user is
788 :return: ('first', 'second', 'third'), tuple containing the res.users
789 objects of the top 3 users. If no user meets the criterias for a rank,
790 it is set to False. Nobody can receive a rank is noone receives the
791 higher one (eg: if 'second' == False, 'third' will be False)
793 goal_obj = self.pool.get('gamification.goal')
794 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
796 for user in challenge.user_ids:
798 total_completness = 0
799 # every goal of the user for the running period
800 goal_ids = goal_obj.search(cr, uid, [
801 ('challenge_id', '=', challenge.id),
802 ('user_id', '=', user.id),
803 ('start_date', '=', start_date),
804 ('end_date', '=', end_date)
806 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
807 if goal.state != 'reached':
809 if goal.definition_condition == 'higher':
811 total_completness += 100.0 * goal.current / goal.target_goal
812 elif goal.state == 'reached':
813 # for lower goals, can not get percentage so 0 or 100
814 total_completness += 100
816 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
817 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
819 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
821 return (False, False, False)
822 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
823 # only one user succeeded
824 return (sorted_challengers[0]['user'], False, False)
825 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
826 # only one user succeeded
827 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
828 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
830 def reward_user(self, cr, uid, user_id, badge_id, challenge_id=False, context=None):
831 """Create a badge user and send the badge to him
833 :param user_id: the user to reward
834 :param badge_id: the concerned badge
836 badge_user_obj = self.pool.get('gamification.badge.user')
837 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id, 'challenge_id':challenge_id}, context=context)
838 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
841 class gamification_challenge_line(osv.Model):
842 """Gamification challenge line
844 Predifined goal for 'gamification_challenge'
845 These are generic list of goals with only the target goal defined
846 Should only be created for the gamification_challenge object
849 _name = 'gamification.challenge.line'
850 _description = 'Gamification generic goal for challenge'
851 _order = "sequence, id"
853 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
854 goal_definition = self.pool.get('gamification.goal.definition')
855 if not definition_id:
856 return {'value': {'definition_id': False}}
857 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
860 'condition': goal_definition.condition,
861 'definition_full_suffix': goal_definition.full_suffix
867 'name': fields.related('definition_id', 'name', string="Name", type="char"),
868 'challenge_id': fields.many2one('gamification.challenge',
872 'definition_id': fields.many2one('gamification.goal.definition',
873 string='Goal Definition',
876 'target_goal': fields.float('Target Value to Reach',
878 'sequence': fields.integer('Sequence',
879 help='Sequence number for ordering'),
880 'condition': fields.related('definition_id', 'condition', type="selection",
881 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
882 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
883 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
884 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),