diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/gamification/models/challenge.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/gamification/models/challenge.py')
| -rw-r--r-- | addons/gamification/models/challenge.py | 795 |
1 files changed, 795 insertions, 0 deletions
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py new file mode 100644 index 00000000..3e216165 --- /dev/null +++ b/addons/gamification/models/challenge.py @@ -0,0 +1,795 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import ast +import itertools +import logging +from datetime import date, timedelta + +from dateutil.relativedelta import relativedelta, MO + +from odoo import api, models, fields, _, exceptions +from odoo.tools import ustr + +_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 + + :param str default_start_date: string date in DEFAULT_SERVER_DATE_FORMAT format + :param str default_end_date: string date in DEFAULT_SERVER_DATE_FORMAT format + + :return: (start_date, end_date), dates in string format, 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': + start_date = today + relativedelta(weekday=MO(-1)) + end_date = start_date + timedelta(days=7) + elif period == 'monthly': + start_date = today.replace(day=1) + end_date = today + relativedelta(months=1, day=1, days=-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 + + return (start_date, end_date) + + return fields.Datetime.to_string(start_date), fields.Datetime.to_string(end_date) + +class Challenge(models.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' + _order = 'end_date, start_date, name, id' + + 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"), + ], default='draft', copy=False, + string="State", required=True, tracking=True) + manager_id = fields.Many2one( + 'res.users', default=lambda self: self.env.uid, + string="Responsible", help="The user responsible for the challenge.",) + + user_ids = fields.Many2many('res.users', 'gamification_challenge_users_rel', string="Users", help="List of users participating to the challenge") + user_domain = fields.Char("User domain", help="Alternative to a list of users") + + period = fields.Selection([ + ('once', "Non recurring"), + ('daily', "Daily"), + ('weekly', "Weekly"), + ('monthly', "Monthly"), + ('yearly', "Yearly") + ], default='once', + 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', 'gamification_invited_user_ids_rel', 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, copy=True) + + reward_id = fields.Many2one('gamification.badge', string="For Every Succeeding 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?") + reward_realtime = fields.Boolean("Reward as soon as every goal is reached", default=True, 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.") + + visibility_mode = fields.Selection([ + ('personal', "Individual Goals"), + ('ranking', "Leader Board (Group Ranking)"), + ], default='personal', + string="Display Mode", required=True) + + report_message_frequency = fields.Selection([ + ('never', "Never"), + ('onchange', "On change"), + ('daily', "Daily"), + ('weekly', "Weekly"), + ('monthly', "Monthly"), + ('yearly', "Yearly") + ], default='never', + string="Report Frequency", required=True) + report_message_group_id = fields.Many2one('mail.channel', 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('mail.template', default=lambda self: self._get_report_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", default=fields.Date.today) + next_report_date = fields.Date("Next Report Date", compute='_get_next_report_date', store=True) + + challenge_category = fields.Selection([ + ('hr', 'Human Resources / Engagement'), + ('other', 'Settings / Gamification Tools'), + ], string="Appears in", required=True, default='hr', + help="Define the visibility of the challenge through menus") + + REPORT_OFFSETS = { + 'daily': timedelta(days=1), + 'weekly': timedelta(days=7), + 'monthly': relativedelta(months=1), + 'yearly': relativedelta(years=1), + } + @api.depends('last_report_date', 'report_message_frequency') + def _get_next_report_date(self): + """ Return the next report date based on the last report date and + report period. + """ + for challenge in self: + last = challenge.last_report_date + offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency) + + if offset: + challenge.next_report_date = last + offset + else: + challenge.next_report_date = False + + def _get_report_template(self): + template = self.env.ref('gamification.simple_report_template', raise_if_not_found=False) + + return template.id if template else False + + @api.model + def create(self, vals): + """Overwrite the create method to add the user of groups""" + + if vals.get('user_domain'): + users = self._get_challenger_users(ustr(vals.get('user_domain'))) + + if not vals.get('user_ids'): + vals['user_ids'] = [] + vals['user_ids'].extend((4, user.id) for user in users) + + return super(Challenge, self).create(vals) + + def write(self, vals): + if vals.get('user_domain'): + users = self._get_challenger_users(ustr(vals.get('user_domain'))) + + if not vals.get('user_ids'): + vals['user_ids'] = [] + vals['user_ids'].extend((4, user.id) for user in users) + + write_res = super(Challenge, self).write(vals) + + if vals.get('report_message_frequency', 'never') != 'never': + # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now + for challenge in self: + challenge.message_subscribe([user.partner_id.id for user in challenge.user_ids]) + + if vals.get('state') == 'inprogress': + self._recompute_challenge_users() + self._generate_goals_from_challenge() + + elif vals.get('state') == 'done': + self._check_challenge_reward(force=True) + + elif vals.get('state') == 'draft': + # resetting progress + if self.env['gamification.goal'].search([('challenge_id', 'in', self.ids), ('state', '=', 'inprogress')], limit=1): + raise exceptions.UserError(_("You can not reset a challenge with unfinished goals.")) + + return write_res + + + ##### Update ##### + + @api.model # FIXME: check how cron functions are called to see if decorator necessary + def _cron_update(self, ids=False, commit=True): + """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 + """ + # in cron mode, will do intermediate commits + # cannot be replaced by a parameter because it is intended to impact side-effects of + # write operations + self = self.with_context(commit_gamification=commit) + # start scheduled challenges + planned_challenges = self.search([ + ('state', '=', 'draft'), + ('start_date', '<=', fields.Date.today()) + ]) + if planned_challenges: + planned_challenges.write({'state': 'inprogress'}) + + # close scheduled challenges + scheduled_challenges = self.search([ + ('state', '=', 'inprogress'), + ('end_date', '<', fields.Date.today()) + ]) + if scheduled_challenges: + scheduled_challenges.write({'state': 'done'}) + + records = self.browse(ids) if ids else self.search([('state', '=', 'inprogress')]) + + return records._update_all() + + def _update_all(self): + """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 not self: + return True + + Goals = self.env['gamification.goal'] + + # include yesterday goals to update the goals that just ended + # exclude goals for users that did not connect since the last update + yesterday = fields.Date.to_string(date.today() - timedelta(days=1)) + self.env.cr.execute("""SELECT gg.id + FROM gamification_goal as gg + JOIN res_users_log as log ON gg.user_id = log.create_uid + WHERE gg.write_date < log.create_date + AND gg.closed IS NOT TRUE + AND gg.challenge_id IN %s + AND (gg.state = 'inprogress' + OR (gg.state = 'reached' AND gg.end_date >= %s)) + GROUP BY gg.id + """, [tuple(self.ids), yesterday]) + + Goals.browse(goal_id for [goal_id] in self.env.cr.fetchall()).update_goal() + + self._recompute_challenge_users() + self._generate_goals_from_challenge() + + for challenge in self: + if challenge.last_report_date != fields.Date.today(): + # goals closed but still opened at the last report date + closed_goals_to_report = Goals.search([ + ('challenge_id', '=', challenge.id), + ('start_date', '>=', challenge.last_report_date), + ('end_date', '<=', challenge.last_report_date) + ]) + + if challenge.next_report_date and fields.Date.today() >= challenge.next_report_date: + challenge.report_progress() + elif closed_goals_to_report: + # some goals need a final report + challenge.report_progress(subset_goals=closed_goals_to_report) + + self._check_challenge_reward() + return True + + def _get_challenger_users(self, domain): + user_domain = ast.literal_eval(domain) + return self.env['res.users'].search(user_domain) + + def _recompute_challenge_users(self): + """Recompute the domain to add new users and remove the one no longer matching the domain""" + for challenge in self.filtered(lambda c: c.user_domain): + current_users = challenge.user_ids + new_users = self._get_challenger_users(challenge.user_domain) + + if current_users != new_users: + challenge.user_ids = new_users + + return True + + def action_start(self): + """Start a challenge""" + return self.write({'state': 'inprogress'}) + + def action_check(self): + """Check a challenge + + Create goals that haven't been created yet (eg: if added users) + Recompute the current value for each goal related""" + self.env['gamification.goal'].search([ + ('challenge_id', 'in', self.ids), + ('state', '=', 'inprogress') + ]).unlink() + + return self._update_all() + + def action_report_progress(self): + """Manual report of a goal, does not influence automatic report frequency""" + for challenge in self: + challenge.report_progress() + return True + + ##### Automatic actions ##### + + def _generate_goals_from_challenge(self): + """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""" + + Goals = self.env['gamification.goal'] + for challenge in self: + (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) + to_update = Goals.browse(()) + + for line in challenge.line_ids: + # there is potentially a lot of users + # detect the ones with no goal linked to this line + date_clause = "" + query_params = [line.id] + if start_date: + date_clause += " AND g.start_date = %s" + query_params.append(start_date) + if end_date: + date_clause += " AND g.end_date = %s" + query_params.append(end_date) + + query = """SELECT u.id AS user_id + FROM res_users u + LEFT JOIN gamification_goal g + ON (u.id = g.user_id) + WHERE line_id = %s + {date_clause} + """.format(date_clause=date_clause) + self.env.cr.execute(query, query_params) + user_with_goal_ids = {it for [it] in self.env.cr._obj} + + participant_user_ids = set(challenge.user_ids.ids) + user_squating_challenge_ids = user_with_goal_ids - participant_user_ids + if user_squating_challenge_ids: + # users that used to match the challenge + Goals.search([ + ('challenge_id', '=', challenge.id), + ('user_id', 'in', list(user_squating_challenge_ids)) + ]).unlink() + + values = { + 'definition_id': line.definition_id.id, + 'line_id': line.id, + 'target_goal': line.target_goal, + 'state': 'inprogress', + } + + if start_date: + values['start_date'] = start_date + if end_date: + values['end_date'] = end_date + + # the goal is initialised over the limit to make sure we will compute it at least once + if line.condition == 'higher': + values['current'] = min(line.target_goal - 1, 0) + else: + values['current'] = max(line.target_goal + 1, 0) + + if challenge.remind_update_delay: + values['remind_update_delay'] = challenge.remind_update_delay + + for user_id in (participant_user_ids - user_with_goal_ids): + values['user_id'] = user_id + to_update |= Goals.create(values) + + to_update.update_goal() + + if self.env.context.get('commit_gamification'): + self.env.cr.commit() + + return True + + ##### JS utilities ##### + + def _get_serialized_challenge_lines(self, user=(), restrict_goals=(), restrict_top=0): + """Return a serialised version of the goals information if the user has not completed every goal + + :param user: user retrieving progress (False if no distinction, + only for ranking challenges) + :param restrict_goals: compute only the results for this subset of + gamification.goal ids, if False retrieve every + goal of current running challenge + :param int restrict_top: for challenge lines where visibility_mode is + ``ranking``, retrieve only the best + ``restrict_top`` results and itself, if 0 + 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,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,reached,failed,canceled}>, + 'completeness': <percentage>, + 'current': <current value>, + } + """ + Goals = self.env['gamification.goal'] + (start_date, end_date) = start_end_date_for_period(self.period) + + res_lines = [] + for line in self.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_goals: + domain.append(('id', 'in', restrict_goals.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 self.visibility_mode == 'personal': + if not user: + raise exceptions.UserError(_("Retrieving progress for personal challenge without user information")) + + domain.append(('user_id', '=', user.id)) + + goal = Goals.search(domain, limit=1) + if not goal: + continue + + if goal.state != 'reached': + return [] + line_data.update(goal.read(['id', 'current', 'completeness', 'state'])[0]) + res_lines.append(line_data) + continue + + line_data['own_goal_id'] = False, + line_data['goals'] = [] + if line.condition=='higher': + goals = Goals.search(domain, order="completeness desc, current desc") + else: + goals = Goals.search(domain, order="completeness desc, current asc") + if not goals: + continue + + for ranking, goal in enumerate(goals): + if user and goal.user_id == user: + line_data['own_goal_id'] = goal.id + elif restrict_top and ranking > restrict_top: + # not own goal and too low to be in top + 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, + }) + if len(goals) < 3: + # display at least the top 3 in the results + missing = 3 - len(goals) + for ranking, mock_goal in enumerate([{'id': False, + 'user_id': False, + 'name': '', + 'current': 0, + 'completeness': 0, + 'state': False}] * missing, + start=len(goals)): + mock_goal['rank'] = ranking + line_data['goals'].append(mock_goal) + + res_lines.append(line_data) + return res_lines + + ##### Reporting ##### + + def report_progress(self, users=(), subset_goals=False): + """Post report about the progress of the goals + + :param users: 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 subset_goals: goals to restrict the report + """ + + challenge = self + + if challenge.visibility_mode == 'ranking': + lines_boards = challenge._get_serialized_challenge_lines(restrict_goals=subset_goals) + + body_html = challenge.report_template_id.with_context(challenge_lines=lines_boards)._render_field('body_html', challenge.ids)[challenge.id] + + # send to every follower and participant of the challenge + challenge.message_post( + body=body_html, + partner_ids=challenge.mapped('user_ids.partner_id.id'), + subtype_xmlid='mail.mt_comment', + email_layout_xmlid='mail.mail_notification_light', + ) + if challenge.report_message_group_id: + challenge.report_message_group_id.message_post( + body=body_html, + subtype_xmlid='mail.mt_comment') + + else: + # generate individual reports + for user in (users or challenge.user_ids): + lines = challenge._get_serialized_challenge_lines(user, restrict_goals=subset_goals) + if not lines: + continue + + body_html = challenge.report_template_id.with_user(user).with_context(challenge_lines=lines)._render_field('body_html', challenge.ids)[challenge.id] + + # notify message only to users, do not post on the challenge + challenge.message_notify( + body=body_html, + partner_ids=[user.partner_id.id], + subtype_xmlid='mail.mt_comment', + email_layout_xmlid='mail.mail_notification_light', + ) + if challenge.report_message_group_id: + challenge.report_message_group_id.message_post( + body=body_html, + subtype_xmlid='mail.mt_comment', + email_layout_xmlid='mail.mail_notification_light', + ) + return challenge.write({'last_report_date': fields.Date.today()}) + + ##### Challenges ##### + def accept_challenge(self): + user = self.env.user + sudoed = self.sudo() + sudoed.message_post(body=_("%s has joined the challenge", user.name)) + sudoed.write({'invited_user_ids': [(3, user.id)], 'user_ids': [(4, user.id)]}) + return sudoed._generate_goals_from_challenge() + + def discard_challenge(self): + """The user discard the suggested challenge""" + user = self.env.user + sudoed = self.sudo() + sudoed.message_post(body=_("%s has refused the challenge", user.name)) + return sudoed.write({'invited_user_ids': (3, user.id)}) + + def _check_challenge_reward(self, force=False): + """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) + """ + commit = self.env.context.get('commit_gamification') and self.env.cr.commit + + for challenge in self: + (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) + yesterday = date.today() - timedelta(days=1) + + rewarded_users = self.env['res.users'] + challenge_ended = force or end_date == fields.Date.to_string(yesterday) + if challenge.reward_id and (challenge_ended or challenge.reward_realtime): + # not using start_date as intemportal goals have a start date but no end_date + reached_goals = self.env['gamification.goal'].read_group([ + ('challenge_id', '=', challenge.id), + ('end_date', '=', end_date), + ('state', '=', 'reached') + ], fields=['user_id'], groupby=['user_id']) + for reach_goals_user in reached_goals: + if reach_goals_user['user_id_count'] == len(challenge.line_ids): + # the user has succeeded every assigned goal + user = self.env['res.users'].browse(reach_goals_user['user_id'][0]) + if challenge.reward_realtime: + badges = self.env['gamification.badge.user'].search_count([ + ('challenge_id', '=', challenge.id), + ('badge_id', '=', challenge.reward_id.id), + ('user_id', '=', user.id), + ]) + if badges > 0: + # has already recieved the badge for this challenge + continue + challenge._reward_user(user, challenge.reward_id) + rewarded_users |= user + if commit: + commit() + + if challenge_ended: + # open chatter message + message_body = _("The challenge %s is finished.", challenge.name) + + if rewarded_users: + user_names = rewarded_users.name_get() + message_body += _( + "<br/>Reward (badge %(badge_name)s) for every succeeding user was sent to %(users)s.", + badge_name=challenge.reward_id.name, + users=", ".join(name for (user_id, name) in user_names) + ) + else: + message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.") + + # reward bests + reward_message = _("<br/> %(rank)d. %(user_name)s - %(reward_name)s") + if challenge.reward_first_id: + (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING) + if first_user: + challenge._reward_user(first_user, challenge.reward_first_id) + message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :") + message_body += reward_message % { + 'rank': 1, + 'user_name': first_user.name, + 'reward_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: + challenge._reward_user(second_user, challenge.reward_second_id) + message_body += reward_message % { + 'rank': 2, + 'user_name': second_user.name, + 'reward_name': challenge.reward_second_id.name, + } + if third_user and challenge.reward_third_id: + challenge._reward_user(third_user, challenge.reward_third_id) + message_body += reward_message % { + 'rank': 3, + 'user_name': third_user.name, + 'reward_name': challenge.reward_third_id.name, + } + + challenge.message_post( + partner_ids=[user.partner_id.id for user in challenge.user_ids], + body=message_body) + if commit: + commit() + + return True + + def _get_topN_users(self, n): + """Get the top N users for a defined challenge + + Ranking criterias: + 1. succeed every goal of the challenge + 2. total completeness of each goal (can be over 100) + + Only users having reached every goal of the challenge will be returned + unless the challenge ``reward_failure`` is set, in which case any user + may be considered. + + :returns: an iterable of exactly N records, either User objects or + False if there was no user for the rank. There can be no + False between two users (if users[k] = False then + users[k+1] = False + """ + Goals = self.env['gamification.goal'] + (start_date, end_date) = start_end_date_for_period(self.period, self.start_date, self.end_date) + challengers = [] + for user in self.user_ids: + all_reached = True + total_completeness = 0 + # every goal of the user for the running period + goal_ids = Goals.search([ + ('challenge_id', '=', self.id), + ('user_id', '=', user.id), + ('start_date', '=', start_date), + ('end_date', '=', end_date) + ]) + for goal in goal_ids: + if goal.state != 'reached': + all_reached = False + if goal.definition_condition == 'higher': + # can be over 100 + total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0 + elif goal.state == 'reached': + # for lower goals, can not get percentage so 0 or 100 + total_completeness += 100 + + challengers.append({'user': user, 'all_reached': all_reached, 'total_completeness': total_completeness}) + + challengers.sort(key=lambda k: (k['all_reached'], k['total_completeness']), reverse=True) + if not self.reward_failure: + # only keep the fully successful challengers at the front, could + # probably use filter since the successful ones are at the front + challengers = itertools.takewhile(lambda c: c['all_reached'], challengers) + + # append a tail of False, then keep the first N + challengers = itertools.islice( + itertools.chain( + (c['user'] for c in challengers), + itertools.repeat(False), + ), 0, n + ) + + return tuple(challengers) + + def _reward_user(self, user, badge): + """Create a badge user and send the badge to him + + :param user: the user to reward + :param badge: the concerned badge + """ + return self.env['gamification.badge.user'].create({ + 'user_id': user.id, + 'badge_id': badge.id, + 'challenge_id': self.id + })._send_badge() + + +class ChallengeLine(models.Model): + """Gamification challenge line + + Predefined 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" + + 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") + + sequence = fields.Integer('Sequence', help='Sequence number for ordering', default=1) + target_goal = fields.Float('Target Value to Reach', required=True) + + name = fields.Char("Name", related='definition_id.name', readonly=False) + condition = fields.Selection(string="Condition", related='definition_id.condition', readonly=True) + definition_suffix = fields.Char("Unit", related='definition_id.suffix', readonly=True) + definition_monetary = fields.Boolean("Monetary", related='definition_id.monetary', readonly=True) + definition_full_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True) |
