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 'start_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),
210 def create(self, cr, uid, vals, context=None):
211 """Overwrite the create method to add the user of groups"""
213 # add users when change the group auto-subscription
214 if vals.get('autojoin_group_id'):
215 new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
217 if not vals.get('user_ids'):
218 vals['user_ids'] = []
219 vals['user_ids'] += [(4, user.id) for user in new_group.users]
221 create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context)
223 # subscribe new users to the challenge
224 if vals.get('user_ids'):
225 # done with browse after super to be sure catch all after orm process
226 challenge = self.browse(cr, uid, create_res, context=context)
227 self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
231 def write(self, cr, uid, ids, vals, context=None):
232 if isinstance(ids, (int,long)):
235 # add users when change the group auto-subscription
236 if vals.get('autojoin_group_id'):
237 new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
239 if not vals.get('user_ids'):
240 vals['user_ids'] = []
241 vals['user_ids'] += [(4, user.id) for user in new_group.users]
243 if vals.get('state') == 'inprogress':
244 # starting a challenge
245 if not vals.get('autojoin_group_id'):
246 # starting challenge, add users in autojoin group
247 if not vals.get('user_ids'):
248 vals['user_ids'] = []
249 for challenge in self.browse(cr, uid, ids, context=context):
250 if challenge.autojoin_group_id:
251 vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users]
253 self.generate_goals_from_challenge(cr, uid, ids, context=context)
255 elif vals.get('state') == 'done':
256 self.check_challenge_reward(cr, uid, ids, force=True, context=context)
258 elif vals.get('state') == 'draft':
260 if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context):
261 raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
263 write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
265 # subscribe new users to the challenge
266 if vals.get('user_ids'):
267 # done with browse after super if changes in groups
268 for challenge in self.browse(cr, uid, ids, context=context):
269 self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context)
276 def _cron_update(self, cr, uid, context=None, ids=False):
279 - Start planned challenges (in draft and with start_date = today)
280 - Create the missing goals (eg: modified the challenge to add lines)
281 - Update every running challenge
283 # start planned challenges
284 planned_challenge_ids = self.search(cr, uid, [
285 ('state', '=', 'draft'),
286 ('start_date', '<=', fields.date.today())])
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 self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
296 ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
298 return self._update_all(cr, uid, ids, context=context)
300 def _update_all(self, cr, uid, ids, context=None):
301 """Update the challenges and related goals
303 :param list(int) ids: the ids of the challenges to update, if False will
304 update only challenges in progress."""
305 if isinstance(ids, (int,long)):
308 goal_obj = self.pool.get('gamification.goal')
310 # we use yesterday to update the goals that just ended
311 yesterday = date.today() - timedelta(days=1)
312 goal_ids = goal_obj.search(cr, uid, [
313 ('challenge_id', 'in', ids),
315 ('state', 'in', ('inprogress', 'inprogress_update')),
317 ('state', 'in', ('reached', 'failed')),
319 ('end_date', '>=', yesterday.strftime(DF)),
320 ('end_date', '=', False)
322 # update every running goal already generated linked to selected challenges
323 goal_obj.update(cr, uid, goal_ids, context=context)
325 for challenge in self.browse(cr, uid, ids, context=context):
326 if challenge.autojoin_group_id:
327 # check in case of new users in challenge, this happens if manager removed users in challenge manually
328 self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context)
329 self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context)
331 # goals closed but still opened at the last report date
332 closed_goals_to_report = goal_obj.search(cr, uid, [
333 ('challenge_id', '=', challenge.id),
334 ('start_date', '>=', challenge.last_report_date),
335 ('end_date', '<=', challenge.last_report_date)
338 if len(closed_goals_to_report) > 0:
339 # some goals need a final report
340 self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
342 if fields.date.today() == challenge.next_report_date:
343 self.report_progress(cr, uid, challenge, context=context)
345 self.check_challenge_reward(cr, uid, ids, context=context)
348 def quick_update(self, cr, uid, challenge_id, context=None):
349 """Update all the goals of a challenge, no generation of new goals"""
350 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
351 self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
355 def action_check(self, cr, uid, ids, context=None):
358 Create goals that haven't been created yet (eg: if added users)
359 Recompute the current value for each goal related"""
360 return self._update_all(cr, uid, ids=ids, context=context)
362 def action_report_progress(self, cr, uid, ids, context=None):
363 """Manual report of a goal, does not influence automatic report frequency"""
364 if isinstance(ids, (int,long)):
366 for challenge in self.browse(cr, uid, ids, context):
367 self.report_progress(cr, uid, challenge, context=context)
371 ##### Automatic actions #####
373 def generate_goals_from_challenge(self, cr, uid, ids, context=None):
374 """Generate the goals for each line and user.
376 If goals already exist for this line and user, the line is skipped. This
377 can be called after each change in the list of users or lines.
378 :param list(int) ids: the list of challenge concerned"""
380 goal_obj = self.pool.get('gamification.goal')
381 for challenge in self.browse(cr, uid, ids, context):
382 (start_date, end_date) = start_end_date_for_period(challenge.period)
384 # if no periodicity, use challenge dates
385 if not start_date and challenge.start_date:
386 start_date = challenge.start_date
387 if not end_date and challenge.end_date:
388 end_date = challenge.end_date
390 for line in challenge.line_ids:
391 # FIXME: allow to restrict to a subset of users
392 for user in challenge.user_ids:
394 domain = [('line_id', '=', line.id), ('user_id', '=', user.id)]
396 domain.append(('start_date', '=', start_date))
398 # goal already existing for this line ?
399 if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
401 # resume canceled goals
402 domain.append(('state', '=', 'canceled'))
403 canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
404 if canceled_goal_ids:
405 goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
406 goal_obj.update(cr, uid, canceled_goal_ids, context=context)
412 'definition_id': line.definition_id.id,
415 'target_goal': line.target_goal,
416 'state': 'inprogress',
420 values['start_date'] = start_date
422 values['end_date'] = end_date
424 if challenge.remind_update_delay:
425 values['remind_update_delay'] = challenge.remind_update_delay
427 new_goal_id = goal_obj.create(cr, uid, values, context)
429 goal_obj.update(cr, uid, [new_goal_id], context=context)
433 ##### JS utilities #####
435 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
436 """Return a serialised version of the goals information if the user has not completed every goal
438 :challenge: browse record of challenge to compute
439 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
440 :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
441 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
442 restrict_goal_ids has priority over restrict_top
445 # if visibility_mode == 'ranking'
447 'name': <gamification.goal.description name>,
448 'description': <gamification.goal.description description>,
449 'condition': <reach condition {lower,higher}>,
450 'computation_mode': <target computation {manually,count,sum,python}>,
451 'monetary': <{True,False}>,
452 'suffix': <value suffix>,
453 'action': <{True,False}>,
454 'display_mode': <{progress,boolean}>,
455 'target': <challenge line target>,
456 'own_goal_id': <gamification.goal id where user_id == uid>,
459 'id': <gamification.goal id>,
460 'rank': <user ranking>,
461 'user_id': <res.users id>,
462 'name': <res.users name>,
463 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
464 'completeness': <percentage>,
465 'current': <current value>,
469 # if visibility_mode == 'personal'
471 'id': <gamification.goal id>,
472 'name': <gamification.goal.description name>,
473 'description': <gamification.goal.description description>,
474 'condition': <reach condition {lower,higher}>,
475 'computation_mode': <target computation {manually,count,sum,python}>,
476 'monetary': <{True,False}>,
477 'suffix': <value suffix>,
478 'action': <{True,False}>,
479 'display_mode': <{progress,boolean}>,
480 'target': <challenge line target>,
481 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
482 'completeness': <percentage>,
483 'current': <current value>,
486 goal_obj = self.pool.get('gamification.goal')
487 (start_date, end_date) = start_end_date_for_period(challenge.period)
491 for line in challenge.line_ids:
493 'name': line.definition_id.name,
494 'description': line.definition_id.description,
495 'condition': line.definition_id.condition,
496 'computation_mode': line.definition_id.computation_mode,
497 'monetary': line.definition_id.monetary,
498 'suffix': line.definition_id.suffix,
499 'action': True if line.definition_id.action_id else False,
500 'display_mode': line.definition_id.display_mode,
501 'target': line.target_goal,
504 ('line_id', '=', line.id),
505 ('state', '!=', 'draft'),
507 if restrict_goal_ids:
508 domain.append(('ids', 'in', restrict_goal_ids))
510 # if no subset goals, use the dates for restriction
512 domain.append(('start_date', '=', start_date))
514 domain.append(('end_date', '=', end_date))
516 if challenge.visibility_mode == 'personal':
518 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
519 domain.append(('user_id', '=', user_id))
520 sorting = goal_obj._order
524 'own_goal_id': False,
527 sorting = "completeness desc, current desc"
530 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
532 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
533 if challenge.visibility_mode == 'personal':
534 # limit=1 so only one result
537 'current': goal.current,
538 'completeness': goal.completeness,
541 if goal.state != 'reached':
545 if user_id and goal.user_id.id == user_id:
546 line_data['own_goal_id'] = goal.id
547 elif restrict_top and ranking > restrict_top:
548 # not own goal, over top, skipping
551 line_data['goals'].append({
553 'user_id': goal.user_id.id,
554 'name': goal.user_id.name,
556 'current': goal.current,
557 'completeness': goal.completeness,
560 if goal.state != 'reached':
563 res_lines.append(line_data)
568 ##### Reporting #####
570 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
571 """Post report about the progress of the goals
573 :param challenge: the challenge object that need to be reported
574 :param users: the list(res.users) of users that are concerned by
575 the report. If False, will send the report to every user concerned
576 (goal users and group that receive a copy). Only used for challenge with
577 a visibility mode set to 'personal'.
578 :param goal_ids: the list(int) of goal ids linked to the challenge for
579 the report. If not specified, use the goals for the current challenge
580 period. This parameter can be used to produce report for previous challenge
582 :param subset_goal_ids: a list(int) of goal ids to restrict the report
587 temp_obj = self.pool.get('email.template')
589 if challenge.visibility_mode == 'ranking':
590 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
592 ctx.update({'challenge_lines': lines_boards})
593 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
595 # send to every follower of the challenge
596 self.message_post(cr, uid, challenge.id,
599 subtype='mail.mt_comment')
600 if challenge.report_message_group_id:
601 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
604 subtype='mail.mt_comment')
607 # generate individual reports
608 for user in users or challenge.user_ids:
609 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
613 ctx.update({'challenge_lines': goals})
614 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
616 # send message only to users, not on the challenge
617 self.message_post(cr, uid, 0,
619 partner_ids=[(4, user.partner_id.id)],
621 subtype='mail.mt_comment')
622 if challenge.report_message_group_id:
623 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
626 subtype='mail.mt_comment')
627 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
629 ##### Challenges #####
630 # TODO in trunk, remove unused parameter user_id
631 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
632 """The user accept the suggested challenge"""
633 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
635 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
636 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
637 message = "%s has joined the challenge" % user.name
638 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
639 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
640 return self.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
642 # TODO in trunk, remove unused parameter user_id
643 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
644 """The user discard the suggested challenge"""
645 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
647 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
648 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
649 message = "%s has refused the challenge" % user.name
650 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
651 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
653 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
654 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
655 id = result and result[1] or False
656 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
657 result['res_id'] = challenge_id
660 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
661 """Actions for the end of a challenge
663 If a reward was selected, grant it to the correct users.
665 - the end date for a challenge with no periodicity
666 - the end of a period for challenge with periodicity
667 - when a challenge is manually closed
668 (if no end date, a running challenge is never rewarded)
670 if isinstance(ids, (int,long)):
672 context = context or {}
673 for challenge in self.browse(cr, uid, ids, context=context):
674 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
675 yesterday = date.today() - timedelta(days=1)
676 if end_date == yesterday.strftime(DF) or force:
677 # open chatter message
678 message_body = _("The challenge %s is finished." % challenge.name)
680 # reward for everybody succeeding
682 if challenge.reward_id:
683 for user in challenge.user_ids:
684 reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
685 ('challenge_id', '=', challenge.id),
686 ('user_id', '=', user.id),
687 ('start_date', '=', start_date),
688 ('end_date', '=', end_date),
689 ('state', '=', 'reached')
691 if len(reached_goal_ids) == len(challenge.line_ids):
692 self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
693 rewarded_users.append(user)
696 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])))
698 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
701 if challenge.reward_first_id:
702 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
704 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
705 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
706 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
708 message_body += _("Nobody reached the required conditions to receive special badges.")
710 if second_user and challenge.reward_second_id:
711 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
712 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
713 if third_user and challenge.reward_third_id:
714 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
715 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
717 self.message_post(cr, uid, challenge.id, body=message_body, context=context)
720 def get_top3_users(self, cr, uid, challenge, context=None):
721 """Get the top 3 users for a defined challenge
724 1. succeed every goal of the challenge
725 2. total completeness of each goal (can be over 100)
726 Top 3 is computed only for users succeeding every goal of the challenge,
727 except if reward_failure is True, in which case every user is
729 :return: ('first', 'second', 'third'), tuple containing the res.users
730 objects of the top 3 users. If no user meets the criterias for a rank,
731 it is set to False. Nobody can receive a rank is noone receives the
732 higher one (eg: if 'second' == False, 'third' will be False)
734 goal_obj = self.pool.get('gamification.goal')
735 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
737 for user in challenge.user_ids:
739 total_completness = 0
740 # every goal of the user for the running period
741 goal_ids = goal_obj.search(cr, uid, [
742 ('challenge_id', '=', challenge.id),
743 ('user_id', '=', user.id),
744 ('start_date', '=', start_date),
745 ('end_date', '=', end_date)
747 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
748 if goal.state != 'reached':
750 if goal.definition_condition == 'higher':
752 total_completness += 100.0 * goal.current / goal.target_goal
753 elif goal.state == 'reached':
754 # for lower goals, can not get percentage so 0 or 100
755 total_completness += 100
757 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
758 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
760 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
762 return (False, False, False)
763 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
764 # only one user succeeded
765 return (sorted_challengers[0]['user'], False, False)
766 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
767 # only one user succeeded
768 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
769 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
771 def reward_user(self, cr, uid, user_id, badge_id, context=None):
772 """Create a badge user and send the badge to him
774 :param user_id: the user to reward
775 :param badge_id: the concerned badge
777 badge_user_obj = self.pool.get('gamification.badge.user')
778 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
779 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
782 class gamification_challenge_line(osv.Model):
783 """Gamification challenge line
785 Predifined goal for 'gamification_challenge'
786 These are generic list of goals with only the target goal defined
787 Should only be created for the gamification_challenge object
790 _name = 'gamification.challenge.line'
791 _description = 'Gamification generic goal for challenge'
792 _order = "sequence, id"
794 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
795 goal_definition = self.pool.get('gamification.goal.definition')
796 if not definition_id:
797 return {'value': {'definition_id': False}}
798 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
801 'condition': goal_definition.condition,
802 'definition_full_suffix': goal_definition.full_suffix
808 'name': fields.related('definition_id', 'name', string="Name"),
809 'challenge_id': fields.many2one('gamification.challenge',
813 'definition_id': fields.many2one('gamification.goal.definition',
814 string='Goal Definition',
817 'target_goal': fields.float('Target Value to Reach',
819 'sequence': fields.integer('Sequence',
820 help='Sequence number for ordering'),
821 'condition': fields.related('definition_id', 'condition', type="selection",
822 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
823 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
824 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
825 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),