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 DEFAULT_SERVER_DATE_FORMAT as DF
25 from openerp.tools.translate import _
27 from datetime import date, datetime, timedelta
30 _logger = logging.getLogger(__name__)
32 # display top 3 in ranking, could be db variable
33 MAX_VISIBILITY_RANKING = 3
35 def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
36 """Return the start and end date for a goal period based on today
38 :return: (start_date, end_date), datetime.date objects, False if the period is
39 not defined or unknown"""
44 elif period == 'weekly':
45 delta = timedelta(days=today.weekday())
46 start_date = today - delta
47 end_date = start_date + timedelta(days=7)
48 elif period == 'monthly':
49 month_range = calendar.monthrange(today.year, today.month)
50 start_date = today.replace(day=1)
51 end_date = today.replace(day=month_range[1])
52 elif period == 'yearly':
53 start_date = today.replace(month=1, day=1)
54 end_date = today.replace(month=12, day=31)
55 else: # period == 'once':
56 start_date = default_start_date # for manual goal, start each time
57 end_date = default_end_date
59 if start_date and end_date:
60 return (start_date.strftime(DF), end_date.strftime(DF))
62 return (start_date, end_date)
65 class gamification_challenge(osv.Model):
66 """Gamification challenge
68 Set of predifined objectives assigned to people with rules for recurrence and
71 If 'user_ids' is defined and 'period' is different than 'one', the set will
72 be assigned to the users for each period (eg: every 1st of each month if
73 'monthly' is selected)
76 _name = 'gamification.challenge'
77 _description = 'Gamification challenge'
78 _inherit = 'mail.thread'
80 def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
81 """Return the next report date based on the last report date and report
84 :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
86 for challenge in self.browse(cr, uid, ids, context):
87 last = datetime.strptime(challenge.last_report_date, DF).date()
88 if challenge.report_message_frequency == 'daily':
89 next = last + timedelta(days=1)
90 res[challenge.id] = next.strftime(DF)
91 elif challenge.report_message_frequency == 'weekly':
92 next = last + timedelta(days=7)
93 res[challenge.id] = next.strftime(DF)
94 elif challenge.report_message_frequency == 'monthly':
95 month_range = calendar.monthrange(last.year, last.month)
96 next = last.replace(day=month_range[1]) + timedelta(days=1)
97 res[challenge.id] = next.strftime(DF)
98 elif challenge.report_message_frequency == 'yearly':
99 res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
100 # frequency == 'once', reported when closed only
102 res[challenge.id] = False
106 def _get_categories(self, cr, uid, context=None):
108 ('hr', 'Human Ressources / Engagement'),
109 ('other', 'Settings / Gamification Tools'),
112 def _get_report_template(self, cr, uid, context=None):
114 return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
118 _order = 'end_date, start_date, name, id'
120 'name': fields.char('Challenge Name', required=True, translate=True),
121 'description': fields.text('Description', translate=True),
122 'state': fields.selection([
124 ('inprogress', 'In Progress'),
127 string='State', required=True, track_visibility='onchange'),
128 'manager_id': fields.many2one('res.users',
129 string='Responsible', help="The user responsible for the challenge."),
131 'user_ids': fields.many2many('res.users', 'user_ids',
133 help="List of users participating to the challenge"),
134 'autojoin_group_id': fields.many2one('res.groups',
135 string='Auto-subscription Group',
136 help='Group of users whose members will be automatically added to user_ids once the challenge is started'),
138 'period': fields.selection([
139 ('once', 'Non recurring'),
141 ('weekly', 'Weekly'),
142 ('monthly', 'Monthly'),
145 string='Periodicity',
146 help='Period of automatic goal assigment. If none is selected, should be launched manually.',
148 'start_date': fields.date('Start Date',
149 help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
150 'end_date': fields.date('End Date',
151 help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
153 'invited_user_ids': fields.many2many('res.users', 'invited_user_ids',
154 string="Suggest to users"),
156 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
158 help="List of goals that will be set",
161 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
162 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
163 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
164 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
165 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
167 'visibility_mode': fields.selection([
168 ('personal', 'Individual Goals'),
169 ('ranking', 'Leader Board (Group Ranking)'),
171 string="Display Mode", required=True),
173 'report_message_frequency': fields.selection([
175 ('onchange', 'On change'),
177 ('weekly', 'Weekly'),
178 ('monthly', 'Monthly'),
181 string="Report Frequency", required=True),
182 'report_message_group_id': fields.many2one('mail.group',
183 string='Send a copy to',
184 help='Group that will receive a copy of the report in addition to the user'),
185 'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
186 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
187 help="Never reminded if no value or zero is specified."),
188 'last_report_date': fields.date('Last Report Date'),
189 'next_report_date': fields.function(_get_next_report_date,
190 type='date', string='Next Report Date', store=True),
192 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
193 string="Appears in", help="Define the visibility of the challenge through menus", required=True),
199 'visibility_mode': 'personal',
200 'report_message_frequency': 'never',
201 'last_report_date': fields.date.today,
202 'manager_id': lambda s, cr, uid, c: uid,
204 'reward_failure': False,
205 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
209 def create(self, cr, uid, vals, context=None):
210 """Overwrite the create method to add the user of groups"""
212 # add users when change the group auto-subscription
213 if vals.get('autojoin_group_id'):
214 new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
216 if not vals.get('user_ids'):
217 vals['user_ids'] = []
218 vals['user_ids'] += [(4, user.id) for user in new_group.users]
220 create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
222 # subscribe new users to the challenge
223 if vals.get('user_ids'):
224 # done with browse after super to be sure catch all after orm process
225 challenge = self.browse(cr, uid, create_res, context=context)
226 self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
230 def write(self, cr, uid, ids, vals, context=None):
231 if isinstance(ids, (int,long)):
234 # add users when change the group auto-subscription
235 if vals.get('autojoin_group_id'):
236 new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
238 if not vals.get('user_ids'):
239 vals['user_ids'] = []
240 vals['user_ids'] += [(4, user.id) for user in new_group.users]
242 if vals.get('state') == 'inprogress':
243 # starting a challenge
244 if not vals.get('autojoin_group_id'):
245 # starting challenge, add users in autojoin group
246 if not vals.get('user_ids'):
247 vals['user_ids'] = []
248 for challenge in self.browse(cr, uid, ids, context=context):
249 if challenge.autojoin_group_id:
250 vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
252 self.generate_goals_from_challenge(cr, uid, ids, context=context)
254 elif vals.get('state') == 'done':
255 self.check_challenge_reward(cr, uid, ids, force=True, context=context)
257 elif vals.get('state') == 'draft':
259 if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context):
260 raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
262 write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
264 # subscribe new users to the challenge
265 if vals.get('user_ids'):
266 # done with browse after super if changes in groups
267 for challenge in self.browse(cr, uid, ids, context=context):
268 self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
275 def _cron_update(self, cr, uid, context=None, ids=False):
278 - Start planned challenges (in draft and with start_date = today)
279 - Create the missing goals (eg: modified the challenge to add lines)
280 - Update every running challenge
282 # start planned challenges
283 planned_challenge_ids = self.search(cr, uid, [
284 ('state', '=', 'draft'),
285 ('start_date', '<=', fields.date.today())])
286 if planned_challenge_ids:
287 self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
289 # close planned challenges
290 planned_challenge_ids = self.search(cr, uid, [
291 ('state', '=', 'inprogress'),
292 ('end_date', '>=', fields.date.today())])
293 if planned_challenge_ids:
294 self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
297 ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
299 return self._update_all(cr, uid, ids, context=context)
301 def _update_all(self, cr, uid, ids, context=None):
302 """Update the challenges and related goals
304 :param list(int) ids: the ids of the challenges to update, if False will
305 update only challenges in progress."""
306 if isinstance(ids, (int,long)):
309 goal_obj = self.pool.get('gamification.goal')
311 # we use yesterday to update the goals that just ended
312 yesterday = date.today() - timedelta(days=1)
313 goal_ids = goal_obj.search(cr, uid, [
314 ('challenge_id', 'in', ids),
316 ('state', 'in', ('inprogress', 'inprogress_update')),
318 ('state', 'in', ('reached', 'failed')),
320 ('end_date', '>=', yesterday.strftime(DF)),
321 ('end_date', '=', False)
323 # update every running goal already generated linked to selected challenges
324 goal_obj.update(cr, uid, goal_ids, context=context)
326 for challenge in self.browse(cr, uid, ids, context=context):
327 if challenge.autojoin_group_id:
328 # check in case of new users in challenge, this happens if manager removed users in challenge manually
329 self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
330 self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
332 # goals closed but still opened at the last report date
333 closed_goals_to_report = goal_obj.search(cr, uid, [
334 ('challenge_id', '=', challenge.id),
335 ('start_date', '>=', challenge.last_report_date),
336 ('end_date', '<=', challenge.last_report_date)
339 if len(closed_goals_to_report) > 0:
340 # some goals need a final report
341 self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
343 if fields.date.today() == challenge.next_report_date:
344 self.report_progress(cr, uid, challenge, context=context)
346 self.check_challenge_reward(cr, uid, ids, context=context)
349 def quick_update(self, cr, uid, challenge_id, context=None):
350 """Update all the goals of a challenge, no generation of new goals"""
351 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
352 self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
356 def action_check(self, cr, uid, ids, context=None):
359 Create goals that haven't been created yet (eg: if added users)
360 Recompute the current value for each goal related"""
361 return self._update_all(cr, uid, ids=ids, context=context)
363 def action_report_progress(self, cr, uid, ids, context=None):
364 """Manual report of a goal, does not influence automatic report frequency"""
365 if isinstance(ids, (int,long)):
367 for challenge in self.browse(cr, uid, ids, context):
368 self.report_progress(cr, uid, challenge, context=context)
372 ##### Automatic actions #####
374 def generate_goals_from_challenge(self, cr, uid, ids, context=None):
375 """Generate the goals for each line and user.
377 If goals already exist for this line and user, the line is skipped. This
378 can be called after each change in the list of users or lines.
379 :param list(int) ids: the list of challenge concerned"""
381 goal_obj = self.pool.get('gamification.goal')
382 for challenge in self.browse(cr, uid, ids, context):
383 (start_date, end_date) = start_end_date_for_period(challenge.period)
385 # if no periodicity, use challenge dates
386 if not start_date and challenge.start_date:
387 start_date = challenge.start_date
388 if not end_date and challenge.end_date:
389 end_date = challenge.end_date
391 for line in challenge.line_ids:
392 # FIXME: allow to restrict to a subset of users
393 for user in challenge.user_ids:
395 domain = [('line_id', '=', line.id), ('user_id', '=', user.id)]
397 domain.append(('start_date', '=', start_date))
399 # goal already existing for this line ?
400 if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
402 # resume canceled goals
403 domain.append(('state', '=', 'canceled'))
404 canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
405 if canceled_goal_ids:
406 goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
407 goal_obj.update(cr, uid, canceled_goal_ids, context=context)
413 'definition_id': line.definition_id.id,
416 'target_goal': line.target_goal,
417 'state': 'inprogress',
421 values['start_date'] = start_date
423 values['end_date'] = end_date
425 if challenge.remind_update_delay:
426 values['remind_update_delay'] = challenge.remind_update_delay
428 new_goal_id = goal_obj.create(cr, uid, values, context)
430 goal_obj.update(cr, uid, [new_goal_id], context=context)
434 ##### JS utilities #####
436 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
437 """Return a serialised version of the goals information if the user has not completed every goal
439 :challenge: browse record of challenge to compute
440 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
441 :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
442 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
443 restrict_goal_ids has priority over restrict_top
446 # if visibility_mode == 'ranking'
448 'name': <gamification.goal.description name>,
449 'description': <gamification.goal.description description>,
450 'condition': <reach condition {lower,higher}>,
451 'computation_mode': <target computation {manually,count,sum,python}>,
452 'monetary': <{True,False}>,
453 'suffix': <value suffix>,
454 'action': <{True,False}>,
455 'display_mode': <{progress,boolean}>,
456 'target': <challenge line target>,
457 'own_goal_id': <gamification.goal id where user_id == uid>,
460 'id': <gamification.goal id>,
461 'rank': <user ranking>,
462 'user_id': <res.users id>,
463 'name': <res.users name>,
464 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
465 'completeness': <percentage>,
466 'current': <current value>,
470 # if visibility_mode == 'personal'
472 'id': <gamification.goal id>,
473 'name': <gamification.goal.description name>,
474 'description': <gamification.goal.description description>,
475 'condition': <reach condition {lower,higher}>,
476 'computation_mode': <target computation {manually,count,sum,python}>,
477 'monetary': <{True,False}>,
478 'suffix': <value suffix>,
479 'action': <{True,False}>,
480 'display_mode': <{progress,boolean}>,
481 'target': <challenge line target>,
482 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
483 'completeness': <percentage>,
484 'current': <current value>,
487 goal_obj = self.pool.get('gamification.goal')
488 (start_date, end_date) = start_end_date_for_period(challenge.period)
492 for line in challenge.line_ids:
494 'name': line.definition_id.name,
495 'description': line.definition_id.description,
496 'condition': line.definition_id.condition,
497 'computation_mode': line.definition_id.computation_mode,
498 'monetary': line.definition_id.monetary,
499 'suffix': line.definition_id.suffix,
500 'action': True if line.definition_id.action_id else False,
501 'display_mode': line.definition_id.display_mode,
502 'target': line.target_goal,
505 ('line_id', '=', line.id),
506 ('state', '!=', 'draft'),
508 if restrict_goal_ids:
509 domain.append(('ids', 'in', restrict_goal_ids))
511 # if no subset goals, use the dates for restriction
513 domain.append(('start_date', '=', start_date))
515 domain.append(('end_date', '=', end_date))
517 if challenge.visibility_mode == 'personal':
519 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
520 domain.append(('user_id', '=', user_id))
521 sorting = goal_obj._order
525 'own_goal_id': False,
528 sorting = "completeness desc, current desc"
531 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
533 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
534 if challenge.visibility_mode == 'personal':
535 # limit=1 so only one result
538 'current': goal.current,
539 'completeness': goal.completeness,
542 if goal.state != 'reached':
546 if user_id and goal.user_id.id == user_id:
547 line_data['own_goal_id'] = goal.id
548 elif restrict_top and ranking > restrict_top:
549 # not own goal, over top, skipping
552 line_data['goals'].append({
554 'user_id': goal.user_id.id,
555 'name': goal.user_id.name,
557 'current': goal.current,
558 'completeness': goal.completeness,
561 if goal.state != 'reached':
564 res_lines.append(line_data)
569 ##### Reporting #####
571 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
572 """Post report about the progress of the goals
574 :param challenge: the challenge object that need to be reported
575 :param users: the list(res.users) of users that are concerned by
576 the report. If False, will send the report to every user concerned
577 (goal users and group that receive a copy). Only used for challenge with
578 a visibility mode set to 'personal'.
579 :param goal_ids: the list(int) of goal ids linked to the challenge for
580 the report. If not specified, use the goals for the current challenge
581 period. This parameter can be used to produce report for previous challenge
583 :param subset_goal_ids: a list(int) of goal ids to restrict the report
588 temp_obj = self.pool.get('email.template')
590 if challenge.visibility_mode == 'ranking':
591 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
593 ctx.update({'challenge_lines': lines_boards})
594 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
596 # send to every follower of the challenge
597 self.message_post(cr, uid, challenge.id,
600 subtype='mail.mt_comment')
601 if challenge.report_message_group_id:
602 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
605 subtype='mail.mt_comment')
608 # generate individual reports
609 for user in users or challenge.user_ids:
610 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
614 ctx.update({'challenge_lines': goals})
615 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
617 # send message only to users, not on the challenge
618 self.message_post(cr, uid, 0,
620 partner_ids=[(4, user.partner_id.id)],
622 subtype='mail.mt_comment')
623 if challenge.report_message_group_id:
624 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
627 subtype='mail.mt_comment')
628 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
630 ##### Challenges #####
631 # TODO in trunk, remove unused parameter user_id
632 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
633 """The user accept the suggested challenge"""
634 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
636 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
637 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
638 message = "%s has joined the challenge" % user.name
639 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
640 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
641 return self.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
643 # TODO in trunk, remove unused parameter user_id
644 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
645 """The user discard the suggested challenge"""
646 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
648 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
649 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
650 message = "%s has refused the challenge" % user.name
651 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
652 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
654 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
655 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
656 id = result and result[1] or False
657 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
658 result['res_id'] = challenge_id
661 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
662 """Actions for the end of a challenge
664 If a reward was selected, grant it to the correct users.
666 - the end date for a challenge with no periodicity
667 - the end of a period for challenge with periodicity
668 - when a challenge is manually closed
669 (if no end date, a running challenge is never rewarded)
671 if isinstance(ids, (int,long)):
673 context = context or {}
674 for challenge in self.browse(cr, uid, ids, context=context):
675 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
676 yesterday = date.today() - timedelta(days=1)
677 if end_date == yesterday.strftime(DF) or force:
678 # open chatter message
679 message_body = _("The challenge %s is finished." % challenge.name)
681 # reward for everybody succeeding
683 if challenge.reward_id:
684 for user in challenge.user_ids:
685 reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
686 ('challenge_id', '=', challenge.id),
687 ('user_id', '=', user.id),
688 ('start_date', '=', start_date),
689 ('end_date', '=', end_date),
690 ('state', '=', 'reached')
692 if len(reached_goal_ids) == len(challenge.line_ids):
693 self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
694 rewarded_users.append(user)
697 message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([user.name for user in rewarded_users])))
699 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
702 if challenge.reward_first_id:
703 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
705 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
706 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
707 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
709 message_body += _("Nobody reached the required conditions to receive special badges.")
711 if second_user and challenge.reward_second_id:
712 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
713 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
714 if third_user and challenge.reward_third_id:
715 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
716 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
718 self.message_post(cr, uid, challenge.id, body=message_body, context=context)
721 def get_top3_users(self, cr, uid, challenge, context=None):
722 """Get the top 3 users for a defined challenge
725 1. succeed every goal of the challenge
726 2. total completeness of each goal (can be over 100)
727 Top 3 is computed only for users succeeding every goal of the challenge,
728 except if reward_failure is True, in which case every user is
730 :return: ('first', 'second', 'third'), tuple containing the res.users
731 objects of the top 3 users. If no user meets the criterias for a rank,
732 it is set to False. Nobody can receive a rank is noone receives the
733 higher one (eg: if 'second' == False, 'third' will be False)
735 goal_obj = self.pool.get('gamification.goal')
736 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
738 for user in challenge.user_ids:
740 total_completness = 0
741 # every goal of the user for the running period
742 goal_ids = goal_obj.search(cr, uid, [
743 ('challenge_id', '=', challenge.id),
744 ('user_id', '=', user.id),
745 ('start_date', '=', start_date),
746 ('end_date', '=', end_date)
748 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
749 if goal.state != 'reached':
751 if goal.definition_condition == 'higher':
753 total_completness += 100.0 * goal.current / goal.target_goal
754 elif goal.state == 'reached':
755 # for lower goals, can not get percentage so 0 or 100
756 total_completness += 100
758 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
759 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
761 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
763 return (False, False, False)
764 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
765 # only one user succeeded
766 return (sorted_challengers[0]['user'], False, False)
767 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
768 # only one user succeeded
769 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
770 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
772 def reward_user(self, cr, uid, user_id, badge_id, context=None):
773 """Create a badge user and send the badge to him
775 :param user_id: the user to reward
776 :param badge_id: the concerned badge
778 badge_user_obj = self.pool.get('gamification.badge.user')
779 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
780 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
783 class gamification_challenge_line(osv.Model):
784 """Gamification challenge line
786 Predifined goal for 'gamification_challenge'
787 These are generic list of goals with only the target goal defined
788 Should only be created for the gamification_challenge object
791 _name = 'gamification.challenge.line'
792 _description = 'Gamification generic goal for challenge'
793 _order = "sequence, id"
795 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
796 goal_definition = self.pool.get('gamification.goal.definition')
797 if not definition_id:
798 return {'value': {'definition_id': False}}
799 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
802 'condition': goal_definition.condition,
803 'definition_full_suffix': goal_definition.full_suffix
809 'name': fields.related('definition_id', 'name', string="Name"),
810 'challenge_id': fields.many2one('gamification.challenge',
814 'definition_id': fields.many2one('gamification.goal.definition',
815 string='Goal Definition',
818 'target_goal': fields.float('Target Value to Reach',
820 'sequence': fields.integer('Sequence',
821 help='Sequence number for ordering'),
822 'condition': fields.related('definition_id', 'condition', type="selection",
823 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
824 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
825 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
826 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),