1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
22 from openerp import SUPERUSER_ID
23 from openerp.osv import fields, osv
24 from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT as DF
25 from openerp.tools.safe_eval import safe_eval as eval
26 from openerp.tools.translate import _
28 from datetime import date, datetime, timedelta
31 _logger = logging.getLogger(__name__)
33 # display top 3 in ranking, could be db variable
34 MAX_VISIBILITY_RANKING = 3
36 def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
37 """Return the start and end date for a goal period based on today
39 :return: (start_date, end_date), datetime.date objects, False if the period is
40 not defined or unknown"""
45 elif period == 'weekly':
46 delta = timedelta(days=today.weekday())
47 start_date = today - delta
48 end_date = start_date + timedelta(days=7)
49 elif period == 'monthly':
50 month_range = calendar.monthrange(today.year, today.month)
51 start_date = today.replace(day=1)
52 end_date = today.replace(day=month_range[1])
53 elif period == 'yearly':
54 start_date = today.replace(month=1, day=1)
55 end_date = today.replace(month=12, day=31)
56 else: # period == 'once':
57 start_date = default_start_date # for manual goal, start each time
58 end_date = default_end_date
60 if start_date and end_date:
61 return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF))
63 return (start_date, end_date)
66 class gamification_challenge(osv.Model):
67 """Gamification challenge
69 Set of predifined objectives assigned to people with rules for recurrence and
72 If 'user_ids' is defined and 'period' is different than 'one', the set will
73 be assigned to the users for each period (eg: every 1st of each month if
74 'monthly' is selected)
77 _name = 'gamification.challenge'
78 _description = 'Gamification challenge'
79 _inherit = 'mail.thread'
81 def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
82 """Return the next report date based on the last report date and report
85 :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date"""
87 for challenge in self.browse(cr, uid, ids, context=context):
88 last = datetime.strptime(challenge.last_report_date, DF).date()
89 if challenge.report_message_frequency == 'daily':
90 next = last + timedelta(days=1)
91 res[challenge.id] = next.strftime(DF)
92 elif challenge.report_message_frequency == 'weekly':
93 next = last + timedelta(days=7)
94 res[challenge.id] = next.strftime(DF)
95 elif challenge.report_message_frequency == 'monthly':
96 month_range = calendar.monthrange(last.year, last.month)
97 next = last.replace(day=month_range[1]) + timedelta(days=1)
98 res[challenge.id] = next.strftime(DF)
99 elif challenge.report_message_frequency == 'yearly':
100 res[challenge.id] = last.replace(year=last.year + 1).strftime(DF)
101 # frequency == 'once', reported when closed only
103 res[challenge.id] = False
107 def _get_categories(self, cr, uid, context=None):
109 ('hr', 'Human Ressources / Engagement'),
110 ('other', 'Settings / Gamification Tools'),
113 def _get_report_template(self, cr, uid, context=None):
115 return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1]
119 _order = 'end_date, start_date, name, id'
121 'name': fields.char('Challenge Name', required=True, translate=True),
122 'description': fields.text('Description', translate=True),
123 'state': fields.selection([
125 ('inprogress', 'In Progress'),
128 string='State', required=True, track_visibility='onchange'),
129 'manager_id': fields.many2one('res.users',
130 string='Responsible', help="The user responsible for the challenge."),
132 'user_ids': fields.many2many('res.users', 'gamification_challenge_users_rel',
134 help="List of users participating to the challenge"),
135 'user_domain': fields.char('User domain', help="Alternative to a list of users"),
137 'period': fields.selection([
138 ('once', 'Non recurring'),
140 ('weekly', 'Weekly'),
141 ('monthly', 'Monthly'),
144 string='Periodicity',
145 help='Period of automatic goal assigment. If none is selected, should be launched manually.',
147 'start_date': fields.date('Start Date',
148 help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
149 'end_date': fields.date('End Date',
150 help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
152 'invited_user_ids': fields.many2many('res.users', 'gamification_invited_user_ids_rel',
153 string="Suggest to users"),
155 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id',
157 help="List of goals that will be set",
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 """Generate the goals for each line and user.
386 If goals already exist for this line and user, the line is skipped. This
387 can be called after each change in the list of users or lines.
388 :param list(int) ids: the list of challenge concerned"""
390 goal_obj = self.pool.get('gamification.goal')
391 for challenge in self.browse(cr, uid, ids, context=context):
392 (start_date, end_date) = start_end_date_for_period(challenge.period)
395 # if no periodicity, use challenge dates
396 if not start_date and challenge.start_date:
397 start_date = challenge.start_date
398 if not end_date and challenge.end_date:
399 end_date = challenge.end_date
401 for line in challenge.line_ids:
403 # there is potentially a lot of users
404 # detect the ones with no goal linked to this line
406 query_params = [line.id]
408 date_clause += "AND g.start_date = %s"
409 query_params.append(start_date)
411 date_clause += "AND g.end_date = %s"
412 query_params.append(end_date)
414 query = """SELECT u.id AS user_id
416 LEFT JOIN gamification_goal g
417 ON (u.id = g.user_id)
420 """.format(date_clause=date_clause)
422 cr.execute(query, query_params)
423 user_with_goal_ids = cr.dictfetchall()
425 participant_user_ids = [user.id for user in challenge.user_ids]
426 user_without_goal_ids = list(set(participant_user_ids) - set([user['user_id'] for user in user_with_goal_ids]))
427 user_squating_challenge_ids = list(set([user['user_id'] for user in user_with_goal_ids]) - set(participant_user_ids))
428 if user_squating_challenge_ids:
429 # users that used to match the challenge
430 goal_to_remove_ids = goal_obj.search(cr, uid, [('challenge_id', '=', challenge.id), ('user_id', 'in', user_squating_challenge_ids)], context=context)
431 goal_obj.unlink(cr, uid, goal_to_remove_ids, context=context)
435 'definition_id': line.definition_id.id,
437 'target_goal': line.target_goal,
438 'state': 'inprogress',
442 values['start_date'] = start_date
444 values['end_date'] = end_date
446 # the goal is initialised over the limit to make sure we will compute it at least once
447 if line.condition == 'higher':
448 values['current'] = line.target_goal - 1
450 values['current'] = line.target_goal + 1
452 if challenge.remind_update_delay:
453 values['remind_update_delay'] = challenge.remind_update_delay
455 for user_id in user_without_goal_ids:
456 values.update({'user_id': user_id})
457 goal_id = goal_obj.create(cr, uid, values, context=context)
458 to_update.append(goal_id)
460 goal_obj.update(cr, uid, to_update, context=context)
464 ##### JS utilities #####
466 def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None):
467 """Return a serialised version of the goals information if the user has not completed every goal
469 :challenge: browse record of challenge to compute
470 :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges)
471 :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
472 :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all
473 restrict_goal_ids has priority over restrict_top
476 # if visibility_mode == 'ranking'
478 'name': <gamification.goal.description name>,
479 'description': <gamification.goal.description description>,
480 'condition': <reach condition {lower,higher}>,
481 'computation_mode': <target computation {manually,count,sum,python}>,
482 'monetary': <{True,False}>,
483 'suffix': <value suffix>,
484 'action': <{True,False}>,
485 'display_mode': <{progress,boolean}>,
486 'target': <challenge line target>,
487 'own_goal_id': <gamification.goal id where user_id == uid>,
490 'id': <gamification.goal id>,
491 'rank': <user ranking>,
492 'user_id': <res.users id>,
493 'name': <res.users name>,
494 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
495 'completeness': <percentage>,
496 'current': <current value>,
500 # if visibility_mode == 'personal'
502 'id': <gamification.goal id>,
503 'name': <gamification.goal.description name>,
504 'description': <gamification.goal.description description>,
505 'condition': <reach condition {lower,higher}>,
506 'computation_mode': <target computation {manually,count,sum,python}>,
507 'monetary': <{True,False}>,
508 'suffix': <value suffix>,
509 'action': <{True,False}>,
510 'display_mode': <{progress,boolean}>,
511 'target': <challenge line target>,
512 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
513 'completeness': <percentage>,
514 'current': <current value>,
517 goal_obj = self.pool.get('gamification.goal')
518 (start_date, end_date) = start_end_date_for_period(challenge.period)
522 for line in challenge.line_ids:
524 'name': line.definition_id.name,
525 'description': line.definition_id.description,
526 'condition': line.definition_id.condition,
527 'computation_mode': line.definition_id.computation_mode,
528 'monetary': line.definition_id.monetary,
529 'suffix': line.definition_id.suffix,
530 'action': True if line.definition_id.action_id else False,
531 'display_mode': line.definition_id.display_mode,
532 'target': line.target_goal,
535 ('line_id', '=', line.id),
536 ('state', '!=', 'draft'),
538 if restrict_goal_ids:
539 domain.append(('ids', 'in', restrict_goal_ids))
541 # if no subset goals, use the dates for restriction
543 domain.append(('start_date', '=', start_date))
545 domain.append(('end_date', '=', end_date))
547 if challenge.visibility_mode == 'personal':
549 raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information"))
550 domain.append(('user_id', '=', user_id))
551 sorting = goal_obj._order
555 'own_goal_id': False,
558 sorting = "completeness desc, current desc"
561 goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context)
563 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
564 if challenge.visibility_mode == 'personal':
565 # limit=1 so only one result
568 'current': goal.current,
569 'completeness': goal.completeness,
572 if goal.state != 'reached':
576 if user_id and goal.user_id.id == user_id:
577 line_data['own_goal_id'] = goal.id
578 elif restrict_top and ranking > restrict_top:
579 # not own goal and too low to be in top
582 line_data['goals'].append({
584 'user_id': goal.user_id.id,
585 'name': goal.user_id.name,
587 'current': goal.current,
588 'completeness': goal.completeness,
591 if goal.state != 'reached':
594 res_lines.append(line_data)
599 ##### Reporting #####
601 def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False):
602 """Post report about the progress of the goals
604 :param challenge: the challenge object that need to be reported
605 :param users: the list(res.users) of users that are concerned by
606 the report. If False, will send the report to every user concerned
607 (goal users and group that receive a copy). Only used for challenge with
608 a visibility mode set to 'personal'.
609 :param goal_ids: the list(int) of goal ids linked to the challenge for
610 the report. If not specified, use the goals for the current challenge
611 period. This parameter can be used to produce report for previous challenge
613 :param subset_goal_ids: a list(int) of goal ids to restrict the report
618 temp_obj = self.pool.get('email.template')
620 if challenge.visibility_mode == 'ranking':
621 lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context)
623 ctx.update({'challenge_lines': lines_boards})
624 body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
626 # send to every follower and participant of the challenge
627 self.message_post(cr, uid, challenge.id,
629 partner_ids=[user.partner_id.id for user in challenge.user_ids],
631 subtype='mail.mt_comment')
632 if challenge.report_message_group_id:
633 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
636 subtype='mail.mt_comment')
639 # generate individual reports
640 for user in users or challenge.user_ids:
641 goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context)
645 ctx.update({'challenge_lines': goals})
646 body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx)
648 # send message only to users, not on the challenge
649 self.message_post(cr, uid, 0,
651 partner_ids=[(4, user.partner_id.id)],
653 subtype='mail.mt_comment')
654 if challenge.report_message_group_id:
655 self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id,
658 subtype='mail.mt_comment')
659 return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context)
661 ##### Challenges #####
662 # TODO in trunk, remove unused parameter user_id
663 def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
664 """The user accept the suggested challenge"""
665 return self._accept_challenge(cr, uid, uid, challenge_ids, context=context)
667 def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None):
668 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
669 message = "%s has joined the challenge" % user.name
670 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
671 self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
672 return self._generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context)
674 # TODO in trunk, remove unused parameter user_id
675 def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None):
676 """The user discard the suggested challenge"""
677 return self._discard_challenge(cr, uid, uid, challenge_ids, context=context)
679 def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None):
680 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
681 message = "%s has refused the challenge" % user.name
682 self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context)
683 return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context)
685 def reply_challenge_wizard(self, cr, uid, challenge_id, context=None):
686 result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
687 id = result and result[1] or False
688 result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0]
689 result['res_id'] = challenge_id
692 def check_challenge_reward(self, cr, uid, ids, force=False, context=None):
693 """Actions for the end of a challenge
695 If a reward was selected, grant it to the correct users.
697 - the end date for a challenge with no periodicity
698 - the end of a period for challenge with periodicity
699 - when a challenge is manually closed
700 (if no end date, a running challenge is never rewarded)
702 if isinstance(ids, (int,long)):
704 for challenge in self.browse(cr, uid, ids, context=context):
705 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
706 yesterday = date.today() - timedelta(days=1)
709 challenge_ended = end_date == yesterday.strftime(DF) or force
710 if challenge.reward_id and challenge_ended or challenge.reward_realtime:
711 # not using start_date as intemportal goals have a start date but no end_date
712 reached_goals = self.pool.get('gamification.goal').read_group(cr, uid, [
713 ('challenge_id', '=', challenge.id),
714 ('end_date', '=', end_date),
715 ('state', '=', 'reached')
716 ], fields=['user_id'], groupby=['user_id'], context=context)
717 for reach_goals_user in reached_goals:
718 if reach_goals_user['user_id_count'] == len(challenge.line_ids):
719 # the user has succeeded every assigned goal
720 user_id = reach_goals_user['user_id'][0]
721 if challenge.reward_realtime:
722 badges = self.pool['gamification.badge.user'].search(cr, uid, [
723 ('challenge_id', '=', challenge.id),
724 ('badge_id', '=', challenge.reward_id.id),
725 ('user_id', '=', user_id),
726 ], count=True, context=context)
728 # has already recieved the badge for this challenge
730 self.reward_user(cr, uid, user_id, challenge.reward_id.id, challenge.id, context=context)
731 rewarded_users.append(user_id)
734 # open chatter message
735 message_body = _("The challenge %s is finished." % challenge.name)
738 user_names = self.pool['res.users'].name_get(cr, uid, rewarded_users, context=context)
739 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])))
741 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
744 if challenge.reward_first_id:
745 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context=context)
747 self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, challenge.id, context=context)
748 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
749 message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name)
751 message_body += _("Nobody reached the required conditions to receive special badges.")
753 if second_user and challenge.reward_second_id:
754 self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, challenge.id, context=context)
755 message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name)
756 if third_user and challenge.reward_third_id:
757 self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, challenge.id, context=context)
758 message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name)
760 self.message_post(cr, uid, challenge.id,
761 partner_ids=[user.partner_id.id for user in challenge.user_ids],
767 def get_top3_users(self, cr, uid, challenge, context=None):
768 """Get the top 3 users for a defined challenge
771 1. succeed every goal of the challenge
772 2. total completeness of each goal (can be over 100)
773 Top 3 is computed only for users succeeding every goal of the challenge,
774 except if reward_failure is True, in which case every user is
776 :return: ('first', 'second', 'third'), tuple containing the res.users
777 objects of the top 3 users. If no user meets the criterias for a rank,
778 it is set to False. Nobody can receive a rank is noone receives the
779 higher one (eg: if 'second' == False, 'third' will be False)
781 goal_obj = self.pool.get('gamification.goal')
782 (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
784 for user in challenge.user_ids:
786 total_completness = 0
787 # every goal of the user for the running period
788 goal_ids = goal_obj.search(cr, uid, [
789 ('challenge_id', '=', challenge.id),
790 ('user_id', '=', user.id),
791 ('start_date', '=', start_date),
792 ('end_date', '=', end_date)
794 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
795 if goal.state != 'reached':
797 if goal.definition_condition == 'higher':
799 total_completness += 100.0 * goal.current / goal.target_goal
800 elif goal.state == 'reached':
801 # for lower goals, can not get percentage so 0 or 100
802 total_completness += 100
804 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
805 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
807 if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']):
809 return (False, False, False)
810 if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']):
811 # only one user succeeded
812 return (sorted_challengers[0]['user'], False, False)
813 if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']):
814 # only one user succeeded
815 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
816 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
818 def reward_user(self, cr, uid, user_id, badge_id, challenge_id=False, context=None):
819 """Create a badge user and send the badge to him
821 :param user_id: the user to reward
822 :param badge_id: the concerned badge
824 badge_user_obj = self.pool.get('gamification.badge.user')
825 user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id, 'challenge_id':challenge_id}, context=context)
826 return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context)
829 class gamification_challenge_line(osv.Model):
830 """Gamification challenge line
832 Predifined goal for 'gamification_challenge'
833 These are generic list of goals with only the target goal defined
834 Should only be created for the gamification_challenge object
837 _name = 'gamification.challenge.line'
838 _description = 'Gamification generic goal for challenge'
839 _order = "sequence, id"
841 def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None):
842 goal_definition = self.pool.get('gamification.goal.definition')
843 if not definition_id:
844 return {'value': {'definition_id': False}}
845 goal_definition = goal_definition.browse(cr, uid, definition_id, context=context)
848 'condition': goal_definition.condition,
849 'definition_full_suffix': goal_definition.full_suffix
855 'name': fields.related('definition_id', 'name', string="Name"),
856 'challenge_id': fields.many2one('gamification.challenge',
860 'definition_id': fields.many2one('gamification.goal.definition',
861 string='Goal Definition',
864 'target_goal': fields.float('Target Value to Reach',
866 'sequence': fields.integer('Sequence',
867 help='Sequence number for ordering'),
868 'condition': fields.related('definition_id', 'condition', type="selection",
869 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
870 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"),
871 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
872 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"),