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 # the goal is initialised over the limit to make sure we will compute it at least once
426 if line.condition == 'higher':
427 values['current'] = line.target_goal - 1
429 values['current'] = line.target_goal + 1
431 if challenge.remind_update_delay:
432 values['remind_update_delay'] = challenge.remind_update_delay
434 new_goal_id = goal_obj.create(cr, uid, values, context)
436 goal_obj.update(cr, uid, [new_goal_id], context=context)
440 ##### JS utilities #####
442 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
443 """Return a serialised version of the goals information if the user has not completed every goal
445 :challenge: browse record of challenge to compute
446 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
447 :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
448 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
449 restrict_goal_ids has priority over restrict_top
452 # if visibility_mode == 'ranking'
454 'name': <gamification.goal.description name>,
455 'description': <gamification.goal.description description>,
456 'condition': <reach condition {lower,higher}>,
457 'computation_mode': <target computation {manually,count,sum,python}>,
458 'monetary': <{True,False}>,
459 'suffix': <value suffix>,
460 'action': <{True,False}>,
461 'display_mode': <{progress,boolean}>,
462 'target': <challenge line target>,
463 'own_goal_id': <gamification.goal id where user_id == uid>,
466 'id': <gamification.goal id>,
467 'rank': <user ranking>,
468 'user_id': <res.users id>,
469 'name': <res.users name>,
470 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
471 'completeness': <percentage>,
472 'current': <current value>,
476 # if visibility_mode == 'personal'
478 'id': <gamification.goal id>,
479 'name': <gamification.goal.description name>,
480 'description': <gamification.goal.description description>,
481 'condition': <reach condition {lower,higher}>,
482 'computation_mode': <target computation {manually,count,sum,python}>,
483 'monetary': <{True,False}>,
484 'suffix': <value suffix>,
485 'action': <{True,False}>,
486 'display_mode': <{progress,boolean}>,
487 'target': <challenge line target>,
488 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>,
489 'completeness': <percentage>,
490 'current': <current value>,
493 goal_obj = self.pool.get('gamification.goal')
494 (start_date, end_date) = start_end_date_for_period(challenge.period)
498 for line in challenge.line_ids:
500 'name': line.definition_id.name,
501 'description': line.definition_id.description,
502 'condition': line.definition_id.condition,
503 'computation_mode': line.definition_id.computation_mode,
504 'monetary': line.definition_id.monetary,
505 'suffix': line.definition_id.suffix,
506 'action': True if line.definition_id.action_id else False,
507 'display_mode': line.definition_id.display_mode,
508 'target': line.target_goal,
511 ('line_id', '=', line.id),
512 ('state', '!=', 'draft'),
514 if restrict_goal_ids:
515 domain.append(('ids', 'in', restrict_goal_ids))
517 # if no subset goals, use the dates for restriction
519 domain.append(('start_date', '=', start_date))
521 domain.append(('end_date', '=', end_date))
523 if challenge.visibility_mode == 'personal':
525 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
526 domain.append(('user_id', '=', user_id))
527 sorting = goal_obj._order
531 'own_goal_id': False,
534 sorting = "completeness desc, current desc"
537 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
539 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
540 if challenge.visibility_mode == 'personal':
541 # limit=1 so only one result
544 'current': goal.current,
545 'completeness': goal.completeness,
548 if goal.state != 'reached':
552 if user_id and goal.user_id.id == user_id:
553 line_data['own_goal_id'] = goal.id
554 elif restrict_top and ranking > restrict_top:
555 # not own goal, over top, skipping
558 line_data['goals'].append({
560 'user_id': goal.user_id.id,
561 'name': goal.user_id.name,
563 'current': goal.current,
564 'completeness': goal.completeness,
567 if goal.state != 'reached':
570 res_lines.append(line_data)
575 ##### Reporting #####
577 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
578 """Post report about the progress of the goals
580 :param challenge: the challenge object that need to be reported
581 :param users: the list(res.users) of users that are concerned by
582 the report. If False, will send the report to every user concerned
583 (goal users and group that receive a copy). Only used for challenge with
584 a visibility mode set to 'personal'.
585 :param goal_ids: the list(int) of goal ids linked to the challenge for
586 the report. If not specified, use the goals for the current challenge
587 period. This parameter can be used to produce report for previous challenge
589 :param subset_goal_ids: a list(int) of goal ids to restrict the report
594 temp_obj = self.pool.get('email.template')
596 if challenge.visibility_mode == 'ranking':
597 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
599 ctx.update({'challenge_lines': lines_boards})
600 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
602 # send to every follower of the challenge
603 self.message_post(cr, uid, challenge.id,
606 subtype='mail.mt_comment')
607 if challenge.report_message_group_id:
608 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
611 subtype='mail.mt_comment')
614 # generate individual reports
615 for user in users or challenge.user_ids:
616 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
620 ctx.update({'challenge_lines': goals})
621 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
623 # send message only to users, not on the challenge
624 self.message_post(cr, uid, 0,
626 partner_ids=[(4, user.partner_id.id)],
628 subtype='mail.mt_comment')
629 if challenge.report_message_group_id:
630 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
633 subtype='mail.mt_comment')
634 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
636 ##### Challenges #####
637 # TODO in trunk, remove unused parameter user_id
638 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
639 """The user accept the suggested challenge"""
640 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
642 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
643 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
644 message = "%s has joined the challenge" % user.name
645 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
646 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
647 return self.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
649 # TODO in trunk, remove unused parameter user_id
650 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
651 """The user discard the suggested challenge"""
652 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
654 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
655 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
656 message = "%s has refused the challenge" % user.name
657 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
658 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
660 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
661 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
662 id = result and result[1] or False
663 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
664 result['res_id'] = challenge_id
667 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
668 """Actions for the end of a challenge
670 If a reward was selected, grant it to the correct users.
672 - the end date for a challenge with no periodicity
673 - the end of a period for challenge with periodicity
674 - when a challenge is manually closed
675 (if no end date, a running challenge is never rewarded)
677 if isinstance(ids, (int,long)):
679 context = context or {}
680 for challenge in self.browse(cr, uid, ids, context=context):
681 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
682 yesterday = date.today() - timedelta(days=1)
683 if end_date == yesterday.strftime(DF) or force:
684 # open chatter message
685 message_body = _("The challenge %s is finished." % challenge.name)
687 # reward for everybody succeeding
689 if challenge.reward_id:
690 for user in challenge.user_ids:
691 reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
692 ('challenge_id', '=', challenge.id),
693 ('user_id', '=', user.id),
694 ('start_date', '=', start_date),
695 ('end_date', '=', end_date),
696 ('state', '=', 'reached')
698 if len(reached_goal_ids) == len(challenge.line_ids):
699 self.reward_user(cr, uid, user.id, challenge.reward_id.id, context)
700 rewarded_users.append(user)
703 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])))
705 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
708 if challenge.reward_first_id:
709 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context)
711 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context)
712 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
713 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
715 message_body += _("Nobody reached the required conditions to receive special badges.")
717 if second_user and challenge.reward_second_id:
718 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context)
719 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
720 if third_user and challenge.reward_third_id:
721 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context)
722 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
724 self.message_post(cr, uid, challenge.id, body=message_body, context=context)
727 def get_top3_users(self, cr, uid, challenge, context=None):
728 """Get the top 3 users for a defined challenge
731 1. succeed every goal of the challenge
732 2. total completeness of each goal (can be over 100)
733 Top 3 is computed only for users succeeding every goal of the challenge,
734 except if reward_failure is True, in which case every user is
736 :return: ('first', 'second', 'third'), tuple containing the res.users
737 objects of the top 3 users. If no user meets the criterias for a rank,
738 it is set to False. Nobody can receive a rank is noone receives the
739 higher one (eg: if 'second' == False, 'third' will be False)
741 goal_obj = self.pool.get('gamification.goal')
742 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
744 for user in challenge.user_ids:
746 total_completness = 0
747 # every goal of the user for the running period
748 goal_ids = goal_obj.search(cr, uid, [
749 ('challenge_id', '=', challenge.id),
750 ('user_id', '=', user.id),
751 ('start_date', '=', start_date),
752 ('end_date', '=', end_date)
754 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
755 if goal.state != 'reached':
757 if goal.definition_condition == 'higher':
759 total_completness += 100.0 * goal.current / goal.target_goal
760 elif goal.state == 'reached':
761 # for lower goals, can not get percentage so 0 or 100
762 total_completness += 100
764 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
765 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
767 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
769 return (False, False, False)
770 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
771 # only one user succeeded
772 return (sorted_challengers[0]['user'], False, False)
773 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
774 # only one user succeeded
775 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
776 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
778 def reward_user(self, cr, uid, user_id, badge_id, context=None):
779 """Create a badge user and send the badge to him
781 :param user_id: the user to reward
782 :param badge_id: the concerned badge
784 badge_user_obj = self.pool.get('gamification.badge.user')
785 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
786 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
789 class gamification_challenge_line(osv.Model):
790 """Gamification challenge line
792 Predifined goal for 'gamification_challenge'
793 These are generic list of goals with only the target goal defined
794 Should only be created for the gamification_challenge object
797 _name = 'gamification.challenge.line'
798 _description = 'Gamification generic goal for challenge'
799 _order = "sequence, id"
801 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
802 goal_definition = self.pool.get('gamification.goal.definition')
803 if not definition_id:
804 return {'value': {'definition_id': False}}
805 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
808 'condition': goal_definition.condition,
809 'definition_full_suffix': goal_definition.full_suffix
815 'name': fields.related('definition_id', 'name', string="Name"),
816 'challenge_id': fields.many2one('gamification.challenge',
820 'definition_id': fields.many2one('gamification.goal.definition',
821 string='Goal Definition',
824 'target_goal': fields.float('Target Value to Reach',
826 'sequence': fields.integer('Sequence',
827 help='Sequence number for ordering'),
828 'condition': fields.related('definition_id', 'condition', type="selection",
829 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
830 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
831 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
832 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),