diff options
| author | Thibault Delavallée <tde@openerp.com> | 2014-01-06 14:57:00 +0100 |
|---|---|---|
| committer | Thibault Delavallée <tde@openerp.com> | 2014-01-06 14:57:00 +0100 |
| commit | 49c0b7a82003a889edc19cb189d0d6d0ebda2c5f (patch) | |
| tree | d24c9d3fe3baad3ba06f11d4bf334e2504fb6092 /addons/gamification/models/challenge.py | |
| parent | c4c268c902a134efdf0f097053c9b28d6b14417c (diff) | |
| parent | ced63415b7319e12fb9d31a3e370850278f697c1 (diff) | |
[MERGE] Sync with trunk
bzr revid: tde@openerp.com-20140106135700-rt0g44w5xmpbtxiu
Diffstat (limited to 'addons/gamification/models/challenge.py')
| -rw-r--r-- | addons/gamification/models/challenge.py | 821 |
1 files changed, 821 insertions, 0 deletions
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py new file mode 100644 index 00000000000..ae27aa96719 --- /dev/null +++ b/addons/gamification/models/challenge.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA (<http://www.openerp.com>) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/> +# +############################################################################## + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from openerp.tools.translate import _ + +from datetime import date, datetime, timedelta +import calendar +import logging +_logger = logging.getLogger(__name__) + +# display top 3 in ranking, could be db variable +MAX_VISIBILITY_RANKING = 3 + +def start_end_date_for_period(period, default_start_date=False, default_end_date=False): + """Return the start and end date for a goal period based on today + + :return: (start_date, end_date), datetime.date objects, False if the period is + not defined or unknown""" + today = date.today() + if period == 'daily': + start_date = today + end_date = start_date + elif period == 'weekly': + delta = timedelta(days=today.weekday()) + start_date = today - delta + end_date = start_date + timedelta(days=7) + elif period == 'monthly': + month_range = calendar.monthrange(today.year, today.month) + start_date = today.replace(day=1) + end_date = today.replace(day=month_range[1]) + elif period == 'yearly': + start_date = today.replace(month=1, day=1) + end_date = today.replace(month=12, day=31) + else: # period == 'once': + start_date = default_start_date # for manual goal, start each time + end_date = default_end_date + + if start_date and end_date: + return (start_date.strftime(DF), end_date.strftime(DF)) + else: + return (start_date, end_date) + + +class gamification_challenge(osv.Model): + """Gamification challenge + + Set of predifined objectives assigned to people with rules for recurrence and + rewards + + If 'user_ids' is defined and 'period' is different than 'one', the set will + be assigned to the users for each period (eg: every 1st of each month if + 'monthly' is selected) + """ + + _name = 'gamification.challenge' + _description = 'Gamification challenge' + _inherit = 'mail.thread' + + def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None): + """Return the next report date based on the last report date and report + period. + + :return: a string in DEFAULT_SERVER_DATE_FORMAT representing the date""" + res = {} + for challenge in self.browse(cr, uid, ids, context): + last = datetime.strptime(challenge.last_report_date, DF).date() + if challenge.report_message_frequency == 'daily': + next = last + timedelta(days=1) + res[challenge.id] = next.strftime(DF) + elif challenge.report_message_frequency == 'weekly': + next = last + timedelta(days=7) + res[challenge.id] = next.strftime(DF) + elif challenge.report_message_frequency == 'monthly': + month_range = calendar.monthrange(last.year, last.month) + next = last.replace(day=month_range[1]) + timedelta(days=1) + res[challenge.id] = next.strftime(DF) + elif challenge.report_message_frequency == 'yearly': + res[challenge.id] = last.replace(year=last.year + 1).strftime(DF) + # frequency == 'once', reported when closed only + else: + res[challenge.id] = False + + return res + + def _get_categories(self, cr, uid, context=None): + return [ + ('hr', 'Human Ressources / Engagement'), + ('other', 'Settings / Gamification Tools'), + ] + + def _get_report_template(self, cr, uid, context=None): + try: + return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'simple_report_template')[1] + except ValueError: + return False + + _order = 'end_date, start_date, name, id' + _columns = { + 'name': fields.char('Challenge Name', required=True, translate=True), + 'description': fields.text('Description', translate=True), + 'state': fields.selection([ + ('draft', 'Draft'), + ('inprogress', 'In Progress'), + ('done', 'Done'), + ], + string='State', required=True, track_visibility='onchange'), + 'manager_id': fields.many2one('res.users', + string='Responsible', help="The user responsible for the challenge."), + + 'user_ids': fields.many2many('res.users', 'user_ids', + string='Users', + help="List of users participating to the challenge"), + 'autojoin_group_id': fields.many2one('res.groups', + string='Auto-subscription Group', + help='Group of users whose members will be automatically added to user_ids once the challenge is started'), + + 'period': fields.selection([ + ('once', 'Non recurring'), + ('daily', 'Daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('yearly', 'Yearly') + ], + string='Periodicity', + help='Period of automatic goal assigment. If none is selected, should be launched manually.', + required=True), + 'start_date': fields.date('Start Date', + help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."), + 'end_date': fields.date('End Date', + help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."), + + 'invited_user_ids': fields.many2many('res.users', 'invited_user_ids', + string="Suggest to users"), + + 'line_ids': fields.one2many('gamification.challenge.line', 'challenge_id', + string='Lines', + help="List of goals that will be set", + required=True), + + 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"), + 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"), + 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"), + 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"), + 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'), + + 'visibility_mode': fields.selection([ + ('personal', 'Individual Goals'), + ('ranking', 'Leader Board (Group Ranking)'), + ], + string="Display Mode", required=True), + + 'report_message_frequency': fields.selection([ + ('never', 'Never'), + ('onchange', 'On change'), + ('daily', 'Daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('yearly', 'Yearly') + ], + string="Report Frequency", required=True), + 'report_message_group_id': fields.many2one('mail.group', + string='Send a copy to', + help='Group that will receive a copy of the report in addition to the user'), + 'report_template_id': fields.many2one('email.template', string="Report Template", required=True), + 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after', + help="Never reminded if no value or zero is specified."), + 'last_report_date': fields.date('Last Report Date'), + 'next_report_date': fields.function(_get_next_report_date, + type='date', string='Next Report Date', store=True), + + 'category': fields.selection(lambda s, *a, **k: s._get_categories(*a, **k), + string="Appears in", help="Define the visibility of the challenge through menus", required=True), + } + + _defaults = { + 'period': 'once', + 'state': 'draft', + 'visibility_mode': 'personal', + 'report_message_frequency': 'never', + 'last_report_date': fields.date.today, + 'start_date': fields.date.today, + 'manager_id': lambda s, cr, uid, c: uid, + 'category': 'hr', + 'reward_failure': False, + 'report_template_id': lambda s, *a, **k: s._get_report_template(*a, **k), + } + + + def create(self, cr, uid, vals, context=None): + """Overwrite the create method to add the user of groups""" + + # add users when change the group auto-subscription + if vals.get('autojoin_group_id'): + new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) + + if not vals.get('user_ids'): + vals['user_ids'] = [] + vals['user_ids'] += [(4, user.id) for user in new_group.users] + + create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context) + + # subscribe new users to the challenge + if vals.get('user_ids'): + # done with browse after super to be sure catch all after orm process + challenge = self.browse(cr, uid, create_res, context=context) + self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context) + + return create_res + + def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, (int,long)): + ids = [ids] + + # add users when change the group auto-subscription + if vals.get('autojoin_group_id'): + new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) + + if not vals.get('user_ids'): + vals['user_ids'] = [] + vals['user_ids'] += [(4, user.id) for user in new_group.users] + + if vals.get('state') == 'inprogress': + # starting a challenge + if not vals.get('autojoin_group_id'): + # starting challenge, add users in autojoin group + if not vals.get('user_ids'): + vals['user_ids'] = [] + for challenge in self.browse(cr, uid, ids, context=context): + if challenge.autojoin_group_id: + vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users] + + self.generate_goals_from_challenge(cr, uid, ids, context=context) + + elif vals.get('state') == 'done': + self.check_challenge_reward(cr, uid, ids, force=True, context=context) + + elif vals.get('state') == 'draft': + # resetting progress + if self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids), ('state', 'in', ['inprogress', 'inprogress_update'])], context=context): + raise osv.except_osv("Error", "You can not reset a challenge with unfinished goals.") + + write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context) + + # subscribe new users to the challenge + if vals.get('user_ids'): + # done with browse after super if changes in groups + for challenge in self.browse(cr, uid, ids, context=context): + self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context) + + return write_res + + + ##### Update ##### + + def _cron_update(self, cr, uid, context=None, ids=False): + """Daily cron check. + + - Start planned challenges (in draft and with start_date = today) + - Create the missing goals (eg: modified the challenge to add lines) + - Update every running challenge + """ + # start planned challenges + planned_challenge_ids = self.search(cr, uid, [ + ('state', '=', 'draft'), + ('start_date', '<=', fields.date.today())]) + self.write(cr, uid, planned_challenge_ids, {'state': 'inprogress'}, context=context) + + # close planned challenges + planned_challenge_ids = self.search(cr, uid, [ + ('state', '=', 'inprogress'), + ('end_date', '>=', fields.date.today())]) + self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context) + + if not ids: + ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context) + + return self._update_all(cr, uid, ids, context=context) + + def _update_all(self, cr, uid, ids, context=None): + """Update the challenges and related goals + + :param list(int) ids: the ids of the challenges to update, if False will + update only challenges in progress.""" + if isinstance(ids, (int,long)): + ids = [ids] + + goal_obj = self.pool.get('gamification.goal') + + # we use yesterday to update the goals that just ended + yesterday = date.today() - timedelta(days=1) + goal_ids = goal_obj.search(cr, uid, [ + ('challenge_id', 'in', ids), + '|', + ('state', 'in', ('inprogress', 'inprogress_update')), + '&', + ('state', 'in', ('reached', 'failed')), + '|', + ('end_date', '>=', yesterday.strftime(DF)), + ('end_date', '=', False) + ], context=context) + # update every running goal already generated linked to selected challenges + goal_obj.update(cr, uid, goal_ids, context=context) + + for challenge in self.browse(cr, uid, ids, context=context): + if challenge.autojoin_group_id: + # check in case of new users in challenge, this happens if manager removed users in challenge manually + self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context) + self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context) + + # goals closed but still opened at the last report date + closed_goals_to_report = goal_obj.search(cr, uid, [ + ('challenge_id', '=', challenge.id), + ('start_date', '>=', challenge.last_report_date), + ('end_date', '<=', challenge.last_report_date) + ]) + + if len(closed_goals_to_report) > 0: + # some goals need a final report + self.report_progress(cr, uid, challenge, subset_goal_ids=closed_goals_to_report, context=context) + + if fields.date.today() == challenge.next_report_date: + self.report_progress(cr, uid, challenge, context=context) + + self.check_challenge_reward(cr, uid, ids, context=context) + return True + + def quick_update(self, cr, uid, challenge_id, context=None): + """Update all the goals of a challenge, no generation of new goals""" + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', '=', challenge_id)], context=context) + self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context) + return True + + + def action_check(self, cr, uid, ids, context=None): + """Check a challenge + + Create goals that haven't been created yet (eg: if added users) + Recompute the current value for each goal related""" + return self._update_all(cr, uid, ids=ids, context=context) + + def action_report_progress(self, cr, uid, ids, context=None): + """Manual report of a goal, does not influence automatic report frequency""" + if isinstance(ids, (int,long)): + ids = [ids] + for challenge in self.browse(cr, uid, ids, context): + self.report_progress(cr, uid, challenge, context=context) + return True + + + ##### Automatic actions ##### + + def generate_goals_from_challenge(self, cr, uid, ids, context=None): + """Generate the goals for each line and user. + + If goals already exist for this line and user, the line is skipped. This + can be called after each change in the list of users or lines. + :param list(int) ids: the list of challenge concerned""" + + for challenge in self.browse(cr, uid, ids, context): + (start_date, end_date) = start_end_date_for_period(challenge.period) + + # if no periodicity, use challenge dates + if not start_date and challenge.start_date: + start_date = challenge.start_date + if not end_date and challenge.end_date: + end_date = challenge.end_date + + for line in challenge.line_ids: + for user in challenge.user_ids: + + goal_obj = self.pool.get('gamification.goal') + domain = [('line_id', '=', line.id), ('user_id', '=', user.id)] + if start_date: + domain.append(('start_date', '=', start_date)) + + # goal already existing for this line ? + if len(goal_obj.search(cr, uid, domain, context=context)) > 0: + + # resume canceled goals + domain.append(('state', '=', 'canceled')) + canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context) + goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context) + goal_obj.update(cr, uid, canceled_goal_ids, context=context) + + # skip to next user + continue + + values = { + 'definition_id': line.definition_id.id, + 'line_id': line.id, + 'user_id': user.id, + 'target_goal': line.target_goal, + 'state': 'inprogress', + } + + if start_date: + values['start_date'] = start_date + if end_date: + values['end_date'] = end_date + + if challenge.remind_update_delay: + values['remind_update_delay'] = challenge.remind_update_delay + + new_goal_id = goal_obj.create(cr, uid, values, context) + + goal_obj.update(cr, uid, [new_goal_id], context=context) + + return True + + ##### JS utilities ##### + + def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None): + """Return a serialised version of the goals information + + :challenge: browse record of challenge to compute + :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges) + :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 + :restrict_top: <int> for challenge lines where visibility_mode == 'ranking', retrieve only these bests results and itself, if False retrieve all + restrict_goal_ids has priority over restrict_top + + format list + # if visibility_mode == 'ranking' + { + 'name': <gamification.goal.description name>, + 'description': <gamification.goal.description description>, + 'condition': <reach condition {lower,higher}>, + 'computation_mode': <target computation {manually,count,sum,python}>, + 'monetary': <{True,False}>, + 'suffix': <value suffix>, + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': <challenge line target>, + 'own_goal_id': <gamification.goal id where user_id == uid>, + 'goals': [ + { + 'id': <gamification.goal id>, + 'rank': <user ranking>, + 'user_id': <res.users id>, + 'name': <res.users name>, + 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>, + 'completeness': <percentage>, + 'current': <current value>, + } + ] + }, + # if visibility_mode == 'personal' + { + 'id': <gamification.goal id>, + 'name': <gamification.goal.description name>, + 'description': <gamification.goal.description description>, + 'condition': <reach condition {lower,higher}>, + 'computation_mode': <target computation {manually,count,sum,python}>, + 'monetary': <{True,False}>, + 'suffix': <value suffix>, + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': <challenge line target>, + 'state': <gamification.goal state {draft,inprogress,inprogress_update,reached,failed,canceled}>, + 'completeness': <percentage>, + 'current': <current value>, + } + """ + goal_obj = self.pool.get('gamification.goal') + (start_date, end_date) = start_end_date_for_period(challenge.period) + + res_lines = [] + for line in challenge.line_ids: + line_data = { + 'name': line.definition_id.name, + 'description': line.definition_id.description, + 'condition': line.definition_id.condition, + 'computation_mode': line.definition_id.computation_mode, + 'monetary': line.definition_id.monetary, + 'suffix': line.definition_id.suffix, + 'action': True if line.definition_id.action_id else False, + 'display_mode': line.definition_id.display_mode, + 'target': line.target_goal, + } + domain = [ + ('line_id', '=', line.id), + ('state', '!=', 'draft'), + ] + if restrict_goal_ids: + domain.append(('ids', 'in', restrict_goal_ids)) + else: + # if no subset goals, use the dates for restriction + if start_date: + domain.append(('start_date', '=', start_date)) + if end_date: + domain.append(('end_date', '=', end_date)) + + if challenge.visibility_mode == 'personal': + if not user_id: + raise osv.except_osv(_('Error!'),_("Retrieving progress for personal challenge without user information")) + domain.append(('user_id', '=', user_id)) + sorting = goal_obj._order + limit = 1 + # initialise in case search returns no results + line_data.update({ + 'id': 0, + 'current': 0, + 'completeness': 0, + 'state': 'draft', + }) + else: + line_data.update({ + 'own_goal_id': False, + 'goals': [], + }) + sorting = "completeness desc, current desc" + limit = False + + goal_ids = goal_obj.search(cr, uid, domain, order=sorting, limit=limit, context=context) + ranking = 0 + for goal in goal_obj.browse(cr, uid, goal_ids, context=context): + if challenge.visibility_mode == 'personal': + # limit=1 so only one result + line_data.update({ + 'id': goal.id, + 'current': goal.current, + 'completeness': goal.completeness, + 'state': goal.state, + }) + else: + ranking += 1 + if user_id and goal.user_id.id == user_id: + line_data['own_goal_id'] = goal.id + elif restrict_top and ranking > restrict_top: + # not own goal, over top, skipping + continue + + line_data['goals'].append({ + 'id': goal.id, + 'user_id': goal.user_id.id, + 'name': goal.user_id.name, + 'rank': ranking, + 'current': goal.current, + 'completeness': goal.completeness, + 'state': goal.state, + }) + res_lines.append(line_data) + return res_lines + + ##### Reporting ##### + + def report_progress(self, cr, uid, challenge, context=None, users=False, subset_goal_ids=False): + """Post report about the progress of the goals + + :param challenge: the challenge object that need to be reported + :param users: the list(res.users) of users that are concerned by + the report. If False, will send the report to every user concerned + (goal users and group that receive a copy). Only used for challenge with + a visibility mode set to 'personal'. + :param goal_ids: the list(int) of goal ids linked to the challenge for + the report. If not specified, use the goals for the current challenge + period. This parameter can be used to produce report for previous challenge + periods. + :param subset_goal_ids: a list(int) of goal ids to restrict the report + """ + if context is None: + context = {} + + temp_obj = self.pool.get('email.template') + ctx = context.copy() + if challenge.visibility_mode == 'ranking': + lines_boards = self._get_serialized_challenge_lines(cr, uid, challenge, user_id=False, restrict_goal_ids=subset_goal_ids, restrict_top=False, context=context) + + ctx.update({'challenge_lines': lines_boards}) + body_html = temp_obj.render_template(cr, uid, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) + + # send to every follower of the challenge + self.message_post(cr, uid, challenge.id, + body=body_html, + context=context, + subtype='mail.mt_comment') + if challenge.report_message_group_id: + self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id, + body=body_html, + context=context, + subtype='mail.mt_comment') + + else: + # generate individual reports + for user in users or challenge.user_ids: + goals = self._get_serialized_challenge_lines(cr, uid, challenge, user.id, restrict_goal_ids=subset_goal_ids, context=context) + if not goals: + continue + + ctx.update({'challenge_lines': goals}) + body_html = temp_obj.render_template(cr, user.id, challenge.report_template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) + + # send message only to users, not on the challenge + self.message_post(cr, uid, 0, + body=body_html, + partner_ids=[(4, user.partner_id.id)], + context=context, + subtype='mail.mt_comment') + if challenge.report_message_group_id: + self.pool.get('mail.group').message_post(cr, uid, challenge.report_message_group_id.id, + body=body_html, + context=context, + subtype='mail.mt_comment') + return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context) + + ##### Challenges ##### + def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): + """The user accept the suggested challenge""" + user_id = user_id or uid + user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) + message = "%s has joined the challenge" % user.name + self.message_post(cr, uid, challenge_ids, body=message, context=context) + self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context) + return self.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + + def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): + """The user discard the suggested challenge""" + user_id = user_id or uid + user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) + message = "%s has refused the challenge" % user.name + self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context) + return self.write(cr, uid, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context) + + def reply_challenge_wizard(self, cr, uid, challenge_id, context=None): + result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard') + id = result and result[1] or False + result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0] + result['res_id'] = challenge_id + return result + + def check_challenge_reward(self, cr, uid, ids, force=False, context=None): + """Actions for the end of a challenge + + If a reward was selected, grant it to the correct users. + Rewards granted at: + - the end date for a challenge with no periodicity + - the end of a period for challenge with periodicity + - when a challenge is manually closed + (if no end date, a running challenge is never rewarded) + """ + if isinstance(ids, (int,long)): + ids = [ids] + context = context or {} + for challenge in self.browse(cr, uid, ids, context=context): + (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) + yesterday = date.today() - timedelta(days=1) + if end_date == yesterday.strftime(DF) or force: + # open chatter message + message_body = _("The challenge %s is finished." % challenge.name) + + # reward for everybody succeeding + rewarded_users = [] + if challenge.reward_id: + for user in challenge.user_ids: + reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [ + ('challenge_id', '=', challenge.id), + ('user_id', '=', user.id), + ('start_date', '=', start_date), + ('end_date', '=', end_date), + ('state', '=', 'reached') + ], context=context) + if len(reached_goal_ids) == len(challenge.line_ids): + self.reward_user(cr, uid, user.id, challenge.reward_id.id, context) + rewarded_users.append(user) + + if rewarded_users: + 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]))) + else: + message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.") + + # reward bests + if challenge.reward_first_id: + (first_user, second_user, third_user) = self.get_top3_users(cr, uid, challenge, context) + if first_user: + self.reward_user(cr, uid, first_user.id, challenge.reward_first_id.id, context) + message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :") + message_body += "<br/> 1. %s - %s" % (first_user.name, challenge.reward_first_id.name) + else: + message_body += _("Nobody reached the required conditions to receive special badges.") + + if second_user and challenge.reward_second_id: + self.reward_user(cr, uid, second_user.id, challenge.reward_second_id.id, context) + message_body += "<br/> 2. %s - %s" % (second_user.name, challenge.reward_second_id.name) + if third_user and challenge.reward_third_id: + self.reward_user(cr, uid, third_user.id, challenge.reward_second_id.id, context) + message_body += "<br/> 3. %s - %s" % (third_user.name, challenge.reward_third_id.name) + + self.message_post(cr, uid, challenge.id, body=message_body, context=context) + return True + + def get_top3_users(self, cr, uid, challenge, context=None): + """Get the top 3 users for a defined challenge + + Ranking criterias: + 1. succeed every goal of the challenge + 2. total completeness of each goal (can be over 100) + Top 3 is computed only for users succeeding every goal of the challenge, + except if reward_failure is True, in which case every user is + considered. + :return: ('first', 'second', 'third'), tuple containing the res.users + objects of the top 3 users. If no user meets the criterias for a rank, + it is set to False. Nobody can receive a rank is noone receives the + higher one (eg: if 'second' == False, 'third' will be False) + """ + goal_obj = self.pool.get('gamification.goal') + (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) + challengers = [] + for user in challenge.user_ids: + all_reached = True + total_completness = 0 + # every goal of the user for the running period + goal_ids = goal_obj.search(cr, uid, [ + ('challenge_id', '=', challenge.id), + ('user_id', '=', user.id), + ('start_date', '=', start_date), + ('end_date', '=', end_date) + ], context=context) + for goal in goal_obj.browse(cr, uid, goal_ids, context=context): + if goal.state != 'reached': + all_reached = False + if goal.definition_condition == 'higher': + # can be over 100 + total_completness += 100.0 * goal.current / goal.target_goal + elif goal.state == 'reached': + # for lower goals, can not get percentage so 0 or 100 + total_completness += 100 + + challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness}) + sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True) + + if len(sorted_challengers) == 0 or (not challenge.reward_failure and not sorted_challengers[0]['all_reached']): + # nobody succeeded + return (False, False, False) + if len(sorted_challengers) == 1 or (not challenge.reward_failure and not sorted_challengers[1]['all_reached']): + # only one user succeeded + return (sorted_challengers[0]['user'], False, False) + if len(sorted_challengers) == 2 or (not challenge.reward_failure and not sorted_challengers[2]['all_reached']): + # only one user succeeded + return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False) + return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user']) + + def reward_user(self, cr, uid, user_id, badge_id, context=None): + """Create a badge user and send the badge to him + + :param user_id: the user to reward + :param badge_id: the concerned badge + """ + badge_user_obj = self.pool.get('gamification.badge.user') + user_badge_id = badge_user_obj.create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context) + return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context) + + +class gamification_challenge_line(osv.Model): + """Gamification challenge line + + Predifined goal for 'gamification_challenge' + These are generic list of goals with only the target goal defined + Should only be created for the gamification_challenge object + """ + + _name = 'gamification.challenge.line' + _description = 'Gamification generic goal for challenge' + _order = "sequence, id" + + def on_change_definition_id(self, cr, uid, ids, definition_id=False, context=None): + goal_definition = self.pool.get('gamification.goal.definition') + if not definition_id: + return {'value': {'definition_id': False}} + goal_definition = goal_definition.browse(cr, uid, definition_id, context=context) + ret = { + 'value': { + 'condition': goal_definition.condition, + 'definition_full_suffix': goal_definition.full_suffix + } + } + return ret + + _columns = { + 'name': fields.related('definition_id', 'name', string="Name"), + 'challenge_id': fields.many2one('gamification.challenge', + string='Challenge', + required=True, + ondelete="cascade"), + 'definition_id': fields.many2one('gamification.goal.definition', + string='Goal Definition', + required=True, + ondelete="cascade"), + 'target_goal': fields.float('Target Value to Reach', + required=True), + 'sequence': fields.integer('Sequence', + help='Sequence number for ordering'), + 'condition': fields.related('definition_id', 'condition', type="selection", + readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]), + 'definition_suffix': fields.related('definition_id', 'suffix', type="char", readonly=True, string="Unit"), + 'definition_monetary': fields.related('definition_id', 'monetary', type="boolean", readonly=True, string="Monetary"), + 'definition_full_suffix': fields.related('definition_id', 'full_suffix', type="char", readonly=True, string="Suffix"), + } + + _default = { + 'sequence': 1, + } |
