1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
22 from openerp import SUPERUSER_ID
23 from openerp.osv import fields, osv
24 from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT as DF
25 from openerp.tools.safe_eval import safe_eval as eval
26 from openerp.tools.translate import _
28 from datetime import date, datetime, timedelta
31 _logger = logging.getLogger(__name__)
33 # display top 3 in ranking, could be db variable
34 MAX_VISIBILITY_RANKING = 3
36 def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
37 """Return the start and end date for a goal period based on today
39 :return: (start_date, end_date), datetime.date objects, False if the period is
40 not defined or unknown"""
45 elif period == 'weekly':
46 delta = timedelta(days=today.weekday())
47 start_date = today - delta
48 end_date = start_date + timedelta(days=7)
49 elif period == 'monthly':
50 month_range = calendar.monthrange(today.year, today.month)
51 start_date = today.replace(day=1)
52 end_date = today.replace(day=month_range[1])
53 elif period == 'yearly':
54 start_date = today.replace(month=1, day=1)
55 end_date = today.replace(month=12, day=31)
56 else: # period == 'once':
57 start_date = default_start_date # for manual goal, start each time
58 end_date = default_end_date
60 if start_date and end_date:
61 return (start_date.strftime(DF), end_date.strftime(DF))
63 return (start_date, end_date)
66 class gamification_challenge(osv.Model):
67 """Gamification challenge
69 Set of predifined objectives assigned to people with rules for recurrence and
72 If 'user_ids' is defined and 'period' is different than 'one', the set will
73 be assigned to the users for each period (eg: every 1st of each month if
74 'monthly' is selected)
77 _name = 'gamification.challenge'
78 _description = 'Gamification challenge'
79 _inherit = 'mail.thread'
81 def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
82 """Return the next report date based on the last report date and report
85 :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
87 for challenge in self.browse(cr, uid, ids, context=context):
88 last = datetime.strptime(challenge.last_report_date, DF).date()
89 if challenge.report_message_frequency == 'daily':
90 next = last + timedelta(days=1)
91 res[challenge.id] = next.strftime(DF)
92 elif challenge.report_message_frequency == 'weekly':
93 next = last + timedelta(days=7)
94 res[challenge.id] = next.strftime(DF)
95 elif challenge.report_message_frequency == 'monthly':
96 month_range = calendar.monthrange(last.year, last.month)
97 next = last.replace(day=month_range[1]) + timedelta(days=1)
98 res[challenge.id] = next.strftime(DF)
99 elif challenge.report_message_frequency == 'yearly':
100 res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
101 # frequency == 'once', reported when closed only
103 res[challenge.id] = False
107 def _get_categories(self, cr, uid, context=None):
109 ('hr', 'Human Ressources / Engagement'),
110 ('other', 'Settings / Gamification Tools'),
113 def _get_report_template(self, cr, uid, context=None):
115 return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
119 _order = 'end_date, start_date, name, id'
121 'name': fields.char('Challenge Name', required=True, translate=True),
122 'description': fields.text('Description', translate=True),
123 'state': fields.selection([
125 ('inprogress', 'In Progress'),
128 string='State', required=True, track_visibility='onchange'),
129 'manager_id': fields.many2one('res.users',
130 string='Responsible', help="The user responsible for the challenge."),
132 'user_ids': fields.many2many('res.users', 'gamification_challenge_users_rel',
134 help="List of users participating to the challenge"),
135 'user_domain': fields.char('User domain', help="Alternative to a list of users"),
137 'period': fields.selection([
138 ('once', 'Non recurring'),
140 ('weekly', 'Weekly'),
141 ('monthly', 'Monthly'),
144 string='Periodicity',
145 help='Period of automatic goal assigment. If none is selected, should be launched manually.',
147 'start_date': fields.date('Start Date',
148 help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
149 'end_date': fields.date('End Date',
150 help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
152 'invited_user_ids': fields.many2many('res.users', 'gamification_invited_user_ids_rel',
153 string="Suggest to users"),
155 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
157 help="List of goals that will be set",
160 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
161 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
162 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
163 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
164 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
165 'reward_realtime': fields.boolean('Reward as soon as every goal is reached',
166 help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge."),
168 'visibility_mode': fields.selection([
169 ('personal', 'Individual Goals'),
170 ('ranking', 'Leader Board (Group Ranking)'),
172 string="Display Mode", required=True),
174 'report_message_frequency': fields.selection([
176 ('onchange', 'On change'),
178 ('weekly', 'Weekly'),
179 ('monthly', 'Monthly'),
182 string="Report Frequency", required=True),
183 'report_message_group_id': fields.many2one('mail.group',
184 string='Send a copy to',
185 help='Group that will receive a copy of the report in addition to the user'),
186 'report_template_id': fields.many2one('email.template', string="Report Template", required=True),
187 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
188 help="Never reminded if no value or zero is specified."),
189 'last_report_date': fields.date('Last Report Date'),
190 'next_report_date': fields.function(_get_next_report_date,
191 type='date', string='Next Report Date', store=True),
193 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k),
194 string="Appears in", help="Define the visibility of the challenge through menus", required=True),
200 'visibility_mode': 'personal',
201 'report_message_frequency': 'never',
202 'last_report_date': fields.date.today,
203 'manager_id': lambda s, cr, uid, c: uid,
205 'reward_failure': False,
206 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k),
210 def create(self, cr, uid, vals, context=None):
211 """Overwrite the create method to add the user of groups"""
213 if vals.get('user_domain'):
214 user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
216 if not vals.get('user_ids'):
217 vals['user_ids'] = []
218 vals['user_ids'] += [(4, user_id) for user_id in user_ids]
220 return super(gamification_challenge, self).create(cr, uid, vals, context=context)
222 def write(self, cr, uid, ids, vals, context=None):
223 if isinstance(ids, (int,long)):
226 if vals.get('user_domain'):
227 user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context)
229 if not vals.get('user_ids'):
230 vals['user_ids'] = []
231 vals['user_ids'] += [(4, user_id) for user_id in user_ids]
233 write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context)
235 if vals.get('report_message_frequency', 'never') != 'never':
236 # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now
237 for challenge in self.browse(cr, uid, ids, context=context):
238 self.message_subscribe(cr, uid, [challenge.id], [user.partner_id.id for user in challenge.user_ids], context=context)
240 if vals.get('state') == 'inprogress':
241 self._recompute_challenge_users(cr, uid, ids, context=context)
242 self._generate_goals_from_challenge(cr, uid, ids, context=context)
244 elif vals.get('state') == 'done':
245 self.check_challenge_reward(cr, uid, ids, force=True, context=context)
247 elif vals.get('state') == 'draft':
249 if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context):
250 raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.")
257 def _cron_update(self, cr, uid, context=None, ids=False):
260 - Start planned challenges (in draft and with start_date = today)
261 - Create the missing goals (eg: modified the challenge to add lines)
262 - Update every running challenge
267 # start scheduled challenges
268 planned_challenge_ids = self.search(cr, uid, [
269 ('state', '=', 'draft'),
270 ('start_date', '<=', fields.date.today())])
271 if planned_challenge_ids:
272 self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context)
274 # close scheduled challenges
275 planned_challenge_ids = self.search(cr, uid, [
276 ('state', '=', 'inprogress'),
277 ('end_date', '>=', fields.date.today())])
278 if planned_challenge_ids:
279 self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context)
282 ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
284 # in cron mode, will do intermediate commits
285 # TODO in trunk: replace by parameter
286 context.update({'commit_gamification': True})
287 return self._update_all(cr, uid, ids, context=context)
289 def _update_all(self, cr, uid, ids, context=None):
290 """Update the challenges and related goals
292 :param list(int) ids: the ids of the challenges to update, if False will
293 update only challenges in progress."""
294 if isinstance(ids, (int,long)):
297 goal_obj = self.pool.get('gamification.goal')
299 # we use yesterday to update the goals that just ended
300 yesterday = date.today() - timedelta(days=1)
301 goal_ids = goal_obj.search(cr, uid, [
302 ('challenge_id', 'in', ids),
304 ('state', '=', 'inprogress'),
306 ('state', 'in', ('reached', 'failed')),
308 ('end_date', '>=', yesterday.strftime(DF)),
309 ('end_date', '=', False)
311 # update every running goal already generated linked to selected challenges
312 goal_obj.update(cr, uid, goal_ids, context=context)
314 self._recompute_challenge_users(cr, uid, ids, context=context)
315 self._generate_goals_from_challenge(cr, uid, ids, context=context)
317 for challenge in self.browse(cr, uid, ids, context=context):
319 if challenge.last_report_date != fields.date.today():
320 # goals closed but still opened at the last report date
321 closed_goals_to_report = goal_obj.search(cr, uid, [
322 ('challenge_id', '=', challenge.id),
323 ('start_date', '>=', challenge.last_report_date),
324 ('end_date', '<=', challenge.last_report_date)
327 if challenge.next_report_date and fields.date.today() >= challenge.next_report_date:
328 self.report_progress(cr, uid, challenge, context=context)
330 elif len(closed_goals_to_report) > 0:
331 # some goals need a final report
332 self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context)
334 self.check_challenge_reward(cr, uid, ids, context=context)
337 def quick_update(self, cr, uid, challenge_id, context=None):
338 """Update all the goals of a specific challenge, no generation of new goals"""
339 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context)
340 self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
343 def _get_challenger_users(self, cr, uid, domain, context=None):
344 user_domain = eval(ustr(domain))
345 return self.pool['res.users'].search(cr, uid, user_domain, context=context)
347 def _recompute_challenge_users(self, cr, uid, challenge_ids, context=None):
348 """Recompute the domain to add new users and remove the one no longer matching the domain"""
349 for challenge in self.browse(cr, uid, challenge_ids, context=context):
350 if challenge.user_domain:
352 old_user_ids = [user.id for user in challenge.user_ids]
353 new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context)
354 to_remove_ids = list(set(old_user_ids) - set(new_user_ids))
355 to_add_ids = list(set(new_user_ids) - set(old_user_ids))
357 write_op = [(3, user_id) for user_id in to_remove_ids]
358 write_op += [(4, user_id) for user_id in to_add_ids]
360 self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context)
365 def action_check(self, cr, uid, ids, context=None):
368 Create goals that haven't been created yet (eg: if added users)
369 Recompute the current value for each goal related"""
370 return self._update_all(cr, uid, ids=ids, context=context)
372 def action_report_progress(self, cr, uid, ids, context=None):
373 """Manual report of a goal, does not influence automatic report frequency"""
374 if isinstance(ids, (int,long)):
376 for challenge in self.browse(cr, uid, ids, context=context):
377 self.report_progress(cr, uid, challenge, context=context)
381 ##### Automatic actions #####
383 def generate_goals_from_challenge(self, cr, uid, ids, context=None):
384 _logger.warning("Deprecated, use private method _generate_goals_from_challenge(...) instead.")
385 return self._generate_goals_from_challenge(cr, uid, ids, context=context)
387 def _generate_goals_from_challenge(self, cr, uid, ids, context=None):
388 """Generate the goals for each line and user.
390 If goals already exist for this line and user, the line is skipped. This
391 can be called after each change in the list of users or lines.
392 :param list(int) ids: the list of challenge concerned"""
394 goal_obj = self.pool.get('gamification.goal')
395 for challenge in self.browse(cr, uid, ids, context=context):
396 (start_date, end_date) = start_end_date_for_period(challenge.period)
399 # if no periodicity, use challenge dates
400 if not start_date and challenge.start_date:
401 start_date = challenge.start_date
402 if not end_date and challenge.end_date:
403 end_date = challenge.end_date
405 for line in challenge.line_ids:
407 # there is potentially a lot of users
408 # detect the ones with no goal linked to this line
410 query_params = [line.id]
412 date_clause += "AND g.start_date = %s"
413 query_params.append(start_date)
415 date_clause += "AND g.end_date = %s"
416 query_params.append(end_date)
418 query = """SELECT u.id AS user_id
420 LEFT JOIN gamification_goal g
421 ON (u.id = g.user_id)
424 """.format(date_clause=date_clause)
426 cr.execute(query, query_params)
427 user_with_goal_ids = cr.dictfetchall()
429 participant_user_ids = [user.id for user in challenge.user_ids]
430 user_without_goal_ids = list(set(participant_user_ids) - set([user['user_id'] for user in user_with_goal_ids]))
431 user_squating_challenge_ids = list(set([user['user_id'] for user in user_with_goal_ids]) - set(participant_user_ids))
432 if user_squating_challenge_ids:
433 # users that used to match the challenge
434 goal_to_remove_ids = goal_obj.search(cr, uid, [('challenge_id', '=', challenge.id), ('user_id', 'in', user_squating_challenge_ids)], context=context)
435 goal_obj.unlink(cr, uid, goal_to_remove_ids, context=context)
439 'definition_id': line.definition_id.id,
441 'target_goal': line.target_goal,
442 'state': 'inprogress',
446 values['start_date'] = start_date
448 values['end_date'] = end_date
450 # the goal is initialised over the limit to make sure we will compute it at least once
451 if line.condition == 'higher':
452 values['current'] = line.target_goal - 1
454 values['current'] = line.target_goal + 1
456 if challenge.remind_update_delay:
457 values['remind_update_delay'] = challenge.remind_update_delay
459 for user_id in user_without_goal_ids:
460 values.update({'user_id': user_id})
461 goal_id = goal_obj.create(cr, uid, values, context=context)
462 to_update.append(goal_id)
464 goal_obj.update(cr, uid, to_update, context=context)
468 ##### JS utilities #####
470 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
471 """Return a serialised version of the goals information if the user has not completed every goal
473 :challenge: browse record of challenge to compute
474 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
475 :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
476 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
477 restrict_goal_ids has priority over restrict_top
480 # if visibility_mode == 'ranking'
482 'name': <gamification.goal.description name>,
483 'description': <gamification.goal.description description>,
484 'condition': <reach condition {lower,higher}>,
485 'computation_mode': <target computation {manually,count,sum,python}>,
486 'monetary': <{True,False}>,
487 'suffix': <value suffix>,
488 'action': <{True,False}>,
489 'display_mode': <{progress,boolean}>,
490 'target': <challenge line target>,
491 'own_goal_id': <gamification.goal id where user_id == uid>,
494 'id': <gamification.goal id>,
495 'rank': <user ranking>,
496 'user_id': <res.users id>,
497 'name': <res.users name>,
498 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
499 'completeness': <percentage>,
500 'current': <current value>,
504 # if visibility_mode == 'personal'
506 'id': <gamification.goal id>,
507 'name': <gamification.goal.description name>,
508 'description': <gamification.goal.description description>,
509 'condition': <reach condition {lower,higher}>,
510 'computation_mode': <target computation {manually,count,sum,python}>,
511 'monetary': <{True,False}>,
512 'suffix': <value suffix>,
513 'action': <{True,False}>,
514 'display_mode': <{progress,boolean}>,
515 'target': <challenge line target>,
516 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
517 'completeness': <percentage>,
518 'current': <current value>,
521 goal_obj = self.pool.get('gamification.goal')
522 (start_date, end_date) = start_end_date_for_period(challenge.period)
526 for line in challenge.line_ids:
528 'name': line.definition_id.name,
529 'description': line.definition_id.description,
530 'condition': line.definition_id.condition,
531 'computation_mode': line.definition_id.computation_mode,
532 'monetary': line.definition_id.monetary,
533 'suffix': line.definition_id.suffix,
534 'action': True if line.definition_id.action_id else False,
535 'display_mode': line.definition_id.display_mode,
536 'target': line.target_goal,
539 ('line_id', '=', line.id),
540 ('state', '!=', 'draft'),
542 if restrict_goal_ids:
543 domain.append(('ids', 'in', restrict_goal_ids))
545 # if no subset goals, use the dates for restriction
547 domain.append(('start_date', '=', start_date))
549 domain.append(('end_date', '=', end_date))
551 if challenge.visibility_mode == 'personal':
553 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
554 domain.append(('user_id', '=', user_id))
555 sorting = goal_obj._order
559 'own_goal_id': False,
562 sorting = "completeness desc, current desc"
565 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
567 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
568 if challenge.visibility_mode == 'personal':
569 # limit=1 so only one result
572 'current': goal.current,
573 'completeness': goal.completeness,
576 if goal.state != 'reached':
580 if user_id and goal.user_id.id == user_id:
581 line_data['own_goal_id'] = goal.id
582 elif restrict_top and ranking > restrict_top:
583 # not own goal and too low to be in top
586 line_data['goals'].append({
588 'user_id': goal.user_id.id,
589 'name': goal.user_id.name,
591 'current': goal.current,
592 'completeness': goal.completeness,
595 if goal.state != 'reached':
598 res_lines.append(line_data)
603 ##### Reporting #####
605 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
606 """Post report about the progress of the goals
608 :param challenge: the challenge object that need to be reported
609 :param users: the list(res.users) of users that are concerned by
610 the report. If False, will send the report to every user concerned
611 (goal users and group that receive a copy). Only used for challenge with
612 a visibility mode set to 'personal'.
613 :param goal_ids: the list(int) of goal ids linked to the challenge for
614 the report. If not specified, use the goals for the current challenge
615 period. This parameter can be used to produce report for previous challenge
617 :param subset_goal_ids: a list(int) of goal ids to restrict the report
622 temp_obj = self.pool.get('email.template')
624 if challenge.visibility_mode == 'ranking':
625 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
627 ctx.update({'challenge_lines': lines_boards})
628 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
630 # send to every follower and participant of the challenge
631 self.message_post(cr, uid, challenge.id,
633 partner_ids=[user.partner_id.id for user in challenge.user_ids],
635 subtype='mail.mt_comment')
636 if challenge.report_message_group_id:
637 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
640 subtype='mail.mt_comment')
643 # generate individual reports
644 for user in users or challenge.user_ids:
645 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
649 ctx.update({'challenge_lines': goals})
650 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
652 # send message only to users, not on the challenge
653 self.message_post(cr, uid, 0,
655 partner_ids=[(4, user.partner_id.id)],
657 subtype='mail.mt_comment')
658 if challenge.report_message_group_id:
659 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
662 subtype='mail.mt_comment')
663 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
665 ##### Challenges #####
666 # TODO in trunk, remove unused parameter user_id
667 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
668 """The user accept the suggested challenge"""
669 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
671 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
672 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
673 message = "%s has joined the challenge" % user.name
674 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
675 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
676 return self._generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
678 # TODO in trunk, remove unused parameter user_id
679 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
680 """The user discard the suggested challenge"""
681 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
683 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
684 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
685 message = "%s has refused the challenge" % user.name
686 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
687 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
689 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
690 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
691 id = result and result[1] or False
692 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
693 result['res_id'] = challenge_id
696 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
697 """Actions for the end of a challenge
699 If a reward was selected, grant it to the correct users.
701 - the end date for a challenge with no periodicity
702 - the end of a period for challenge with periodicity
703 - when a challenge is manually closed
704 (if no end date, a running challenge is never rewarded)
706 if isinstance(ids, (int,long)):
708 for challenge in self.browse(cr, uid, ids, context=context):
709 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
710 yesterday = date.today() - timedelta(days=1)
713 challenge_ended = end_date == yesterday.strftime(DF) or force
714 if challenge.reward_id and challenge_ended or challenge.reward_realtime:
715 # not using start_date as intemportal goals have a start date but no end_date
716 reached_goals = self.pool.get('gamification.goal').read_group(cr, uid, [
717 ('challenge_id', '=', challenge.id),
718 ('end_date', '=', end_date),
719 ('state', '=', 'reached')
720 ], fields=['user_id'], groupby=['user_id'], context=context)
721 for reach_goals_user in reached_goals:
722 if reach_goals_user['user_id_count'] == len(challenge.line_ids):
723 # the user has succeeded every assigned goal
724 user_id = reach_goals_user['user_id'][0]
725 if challenge.reward_realtime:
726 badges = self.pool['gamification.badge.user'].search(cr, uid, [
727 ('challenge_id', '=', challenge.id),
728 ('badge_id', '=', challenge.reward_id.id),
729 ('user_id', '=', user_id),
730 ], count=True, context=context)
732 # has already recieved the badge for this challenge
734 self.reward_user(cr, uid, user_id, challenge.reward_id.id, challenge.id, context=context)
735 rewarded_users.append(user_id)
738 # open chatter message
739 message_body = _("The challenge %s is finished." % challenge.name)
742 user_names = self.pool['res.users'].name_get(cr, uid, rewarded_users, context=context)
743 message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (challenge.reward_id.name, ", ".join([name for (user_id, name) in user_names])))
745 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
748 if challenge.reward_first_id:
749 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context=context)
751 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, challenge.id, context=context)
752 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
753 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
755 message_body += _("Nobody reached the required conditions to receive special badges.")
757 if second_user and challenge.reward_second_id:
758 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, challenge.id, context=context)
759 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
760 if third_user and challenge.reward_third_id:
761 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, challenge.id, context=context)
762 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
764 self.message_post(cr, uid, challenge.id,
765 partner_ids=[user.partner_id.id for user in challenge.user_ids],
771 def get_top3_users(self, cr, uid, challenge, context=None):
772 """Get the top 3 users for a defined challenge
775 1. succeed every goal of the challenge
776 2. total completeness of each goal (can be over 100)
777 Top 3 is computed only for users succeeding every goal of the challenge,
778 except if reward_failure is True, in which case every user is
780 :return: ('first', 'second', 'third'), tuple containing the res.users
781 objects of the top 3 users. If no user meets the criterias for a rank,
782 it is set to False. Nobody can receive a rank is noone receives the
783 higher one (eg: if 'second' == False, 'third' will be False)
785 goal_obj = self.pool.get('gamification.goal')
786 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
788 for user in challenge.user_ids:
790 total_completness = 0
791 # every goal of the user for the running period
792 goal_ids = goal_obj.search(cr, uid, [
793 ('challenge_id', '=', challenge.id),
794 ('user_id', '=', user.id),
795 ('start_date', '=', start_date),
796 ('end_date', '=', end_date)
798 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
799 if goal.state != 'reached':
801 if goal.definition_condition == 'higher':
803 total_completness += 100.0 * goal.current / goal.target_goal
804 elif goal.state == 'reached':
805 # for lower goals, can not get percentage so 0 or 100
806 total_completness += 100
808 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
809 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
811 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
813 return (False, False, False)
814 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
815 # only one user succeeded
816 return (sorted_challengers[0]['user'], False, False)
817 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
818 # only one user succeeded
819 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
820 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
822 def reward_user(self, cr, uid, user_id, badge_id, challenge_id=False, context=None):
823 """Create a badge user and send the badge to him
825 :param user_id: the user to reward
826 :param badge_id: the concerned badge
828 badge_user_obj = self.pool.get('gamification.badge.user')
829 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id, 'challenge_id':challenge_id}, context=context)
830 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
833 class gamification_challenge_line(osv.Model):
834 """Gamification challenge line
836 Predifined goal for 'gamification_challenge'
837 These are generic list of goals with only the target goal defined
838 Should only be created for the gamification_challenge object
841 _name = 'gamification.challenge.line'
842 _description = 'Gamification generic goal for challenge'
843 _order = "sequence, id"
845 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
846 goal_definition = self.pool.get('gamification.goal.definition')
847 if not definition_id:
848 return {'value': {'definition_id': False}}
849 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
852 'condition': goal_definition.condition,
853 'definition_full_suffix': goal_definition.full_suffix
859 'name': fields.related('definition_id', 'name', string="Name"),
860 'challenge_id': fields.many2one('gamification.challenge',
864 'definition_id': fields.many2one('gamification.goal.definition',
865 string='Goal Definition',
868 'target_goal': fields.float('Target Value to Reach',
870 'sequence': fields.integer('Sequence',
871 help='Sequence number for ordering'),
872 'condition': fields.related('definition_id', 'condition', type="selection",
873 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
874 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
875 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
876 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),