From 122c15c48d430c502adf2df45bad4405a864b5ce Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 17 Dec 2013 17:15:41 +0100 Subject: [REF] gamification: pretty much changing half of the code to make tde happy... bzr revid: mat@openerp.com-20131217161541-oxsgy7gmko2x6qui --- addons/gamification/models/__init__.py | 25 ++ addons/gamification/models/badge.py | 299 +++++++++++++ addons/gamification/models/challenge.py | 766 ++++++++++++++++++++++++++++++++ addons/gamification/models/goal.py | 398 +++++++++++++++++ addons/gamification/models/res_users.py | 177 ++++++++ 5 files changed, 1665 insertions(+) create mode 100644 addons/gamification/models/__init__.py create mode 100644 addons/gamification/models/badge.py create mode 100644 addons/gamification/models/challenge.py create mode 100644 addons/gamification/models/goal.py create mode 100644 addons/gamification/models/res_users.py (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/__init__.py b/addons/gamification/models/__init__.py new file mode 100644 index 00000000000..812a08b1e19 --- /dev/null +++ b/addons/gamification/models/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import goal +import challenge +import res_users +import badge diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py new file mode 100644 index 00000000000..6aa2f89e74e --- /dev/null +++ b/addons/gamification/models/badge.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA () +# +# 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 +# +############################################################################## + +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 templates import TemplateHelper +from datetime import date +import logging + +_logger = logging.getLogger(__name__) + + +class gamification_badge_user(osv.Model): + """User having received a badge""" + + _name = 'gamification.badge.user' + _description = 'Gamification user badge' + _order = "create_date desc" + + _columns = { + 'user_id': fields.many2one('res.users', string="User", required=True), + 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True), + 'comment': fields.text('Comment'), + 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"), + 'create_date': fields.datetime('Created', readonly=True), + 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), + } + + +class gamification_badge(osv.Model): + """Badge object that users can send and receive""" + + _name = 'gamification.badge' + _description = 'Gamification badge' + _inherit = ['mail.thread'] + + def _get_owners_info(self, cr, uid, ids, name, args, context=None): + """Return: + the list of unique res.users ids having received this badge + the total number of time this badge was granted + the total number of users this badge was granted to + """ + result = dict.fromkeys(ids, False) + for obj in self.browse(cr, uid, ids, context=context): + res = set() + for owner in obj.owner_ids: + res.add(owner.user_id.id) + res = list(res) + result[obj.id] = { + 'unique_owner_ids': res, + 'stat_count': len(obj.owner_ids), + 'stat_count_distinct': len(res) + } + return result + + def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None): + """Return stats related to badge users""" + result = dict.fromkeys(ids, False) + badge_user_obj = self.pool.get('gamification.badge.user') + first_month_day = date.today().replace(day=1).strftime(DF) + for obj in self.browse(cr, uid, ids, context=context): + result[obj.id] = { + 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid)], context=context, count=True), + 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_date', '>=', first_month_day)], context=context, count=True), + 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True), + 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True) + } + return result + + def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None): + """Computes the number of badges remaining the user can send + + 0 if not allowed or no remaining + integer if limited sending + -1 if infinite (should not be displayed) + """ + result = dict.fromkeys(ids, False) + for badge in self.browse(cr, uid, ids, context=context): + if self._can_grant_badge(cr, uid, uid, badge.id, context) != 1: + # if the user cannot grant this badge at all, result is 0 + result[badge.id] = 0 + elif not badge.rule_max: + # if there is no limitation, -1 is returned which means 'infinite' + result[badge.id] = -1 + else: + result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending + return result + + _columns = { + 'name': fields.char('Badge', required=True, translate=True), + 'description': fields.text('Description'), + 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"), + # image_select: selection with a on_change to fill image with predefined picts + 'rule_auth': fields.selection([ + ('everyone', 'Everyone'), + ('users', 'A selected list of users'), + ('having', 'People having some badges'), + ('nobody', 'No one, assigned through challenges'), + ], + string="Allowance to Grant", + help="Who can grant this badge", + required=True), + 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users', + string='Authorized Users', + help="Only these people can give this badge"), + 'rule_auth_badge_ids': fields.many2many('gamification.badge', + 'rel_badge_badge', 'badge1_id', 'badge2_id', + string='Required Badges', + help="Only the people having these badges can give this badge"), + + 'rule_max': fields.boolean('Monthly Limited Sending', + help="Check to set a monthly limit per person of sending this badge"), + 'rule_max_number': fields.integer('Limitation Number', + help="The maximum number of time this badge can be sent per month per person."), + 'stat_my_monthly_sending': fields.function(_get_badge_user_stats, + type="integer", + string='My Monthly Sending Total', + multi='badge_users', + help="The number of time the current user has sent this badge this month."), + 'remaining_sending': fields.function(_remaining_sending_calc, type='integer', + string='Remaining Sending Allowed', help="If a maxium is set"), + + 'challenge_ids': fields.one2many('gamification.challenge', 'reward_id', + string="Reward of Challenges"), + + 'goal_definition_ids': fields.many2many('gamification.goal.definition', 'badge_unlocked_definition_rel', + string='Rewarded by', + help="The users that have succeeded theses goals will receive automatically the badge."), + + 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id', + string='Owners', help='The list of instances of this badge granted to users'), + 'unique_owner_ids': fields.function(_get_owners_info, + string='Unique Owners', + help="The list of unique users having received this badge.", + multi='unique_users', + type="many2many", relation="res.users"), + + 'stat_count': fields.function(_get_owners_info, string='Total', + type="integer", + multi='unique_users', + help="The number of time this badge has been received."), + 'stat_count_distinct': fields.function(_get_owners_info, + type="integer", + string='Number of users', + multi='unique_users', + help="The number of time this badge has been received by unique users."), + 'stat_this_month': fields.function(_get_badge_user_stats, + type="integer", + string='Monthly total', + multi='badge_users', + help="The number of time this badge has been received this month."), + 'stat_my': fields.function(_get_badge_user_stats, string='My Total', + type="integer", + multi='badge_users', + help="The number of time the current user has received this badge."), + 'stat_my_this_month': fields.function(_get_badge_user_stats, + type="integer", + string='My Monthly Total', + multi='badge_users', + help="The number of time the current user has received this badge this month."), + } + + _defaults = { + 'rule_auth': 'everyone', + } + + def send_badge(self, cr, uid, badge_id, badge_user_ids, user_from=False, context=None): + """Send a notification to a user for receiving a badge + + Does NOT verify constrains on badge granting. + The users are added to the owner_ids (create badge_user if needed) + The stats counters are incremented + :param badge_id: id of the badge to deserve + :param badge_user_ids: list(int) of badge users that will receive the badge + :param user_from: optional id of the res.users object that has sent the badge + """ + badge = self.browse(cr, uid, badge_id, context=context) + # template_env = TemplateHelper() + + res = None + temp_obj = self.pool.get('email.template') + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) + ctx = context.copy() + for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context=context): + + ctx.update({'user_from': self.pool.get('res.users').browse(cr, uid, user_from).name}) + + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) + + # as SUPERUSER as normal user don't have write access on a badge + res = self.message_post(cr, SUPERUSER_ID, badge.id, partner_ids=[badge_user.user_id.partner_id.id], body=body_html, type='comment', subtype='mt_comment', context=context) + return res + + def check_granting(self, cr, uid, user_from_id, badge_id, context=None): + """Check the user 'user_from_id' can grant the badge 'badge_id' and raise the appropriate exception + if not""" + status_code = self._can_grant_badge(cr, uid, user_from_id, badge_id, context=context) + if status_code == 1: + return True + elif status_code == 2: + raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.')) + elif status_code == 3: + raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.')) + elif status_code == 4: + raise osv.except_osv(_('Warning!'), _('You do not have the required badges.')) + elif status_code == 5: + raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.')) + else: + _logger.exception("Unknown badge status code: %d" % int(status_code)) + return False + + def _can_grant_badge(self, cr, uid, user_from_id, badge_id, context=None): + """Check if a user can grant a badge to another user + + :param user_from_id: the id of the res.users trying to send the badge + :param badge_id: the granted badge id + :return: integer representing the permission. + 1: can grant + 2: nobody can send + 3: user not in the allowed list + 4: don't have the required badges + 5: user's monthly limit reached + """ + badge = self.browse(cr, uid, badge_id, context=context) + + if badge.rule_auth == 'nobody': + return 2 + + elif badge.rule_auth == 'users' and user_from_id not in [user.id for user in badge.rule_auth_user_ids]: + return 3 + + elif badge.rule_auth == 'having': + all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', user_from_id)], context=context) + for required_badge in badge.rule_auth_badge_ids: + if required_badge.id not in all_user_badges: + return 4 + + if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number: + return 5 + + # badge.rule_auth == 'everyone' -> no check + return 1 + + +class grant_badge_wizard(osv.TransientModel): + """ Wizard allowing to grant a badge to a user""" + + _name = 'gamification.badge.user.wizard' + _columns = { + 'user_id': fields.many2one("res.users", string='User', required=True), + 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True), + 'comment': fields.text('Comment'), + } + + def action_grant_badge(self, cr, uid, ids, context=None): + """Wizard action for sending a badge to a chosen user""" + + badge_obj = self.pool.get('gamification.badge') + badge_user_obj = self.pool.get('gamification.badge.user') + + for wiz in self.browse(cr, uid, ids, context=context): + if uid == wiz.user_id.id: + raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself')) + + #check if the badge granting is legitimate + if badge_obj.check_granting(cr, uid, user_from_id=uid, badge_id=wiz.badge_id.id, context=context): + #create the badge + values = { + 'user_id': wiz.user_id.id, + 'badge_id': wiz.badge_id.id, + 'comment': wiz.comment, + } + badge_user = badge_user_obj.create(cr, uid, values, context=context) + #notify the user + result = badge_obj.send_badge(cr, uid, wiz.badge_id.id, [badge_user], user_from=uid, context=context) + + return result +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py new file mode 100644 index 00000000000..f0873a368d7 --- /dev/null +++ b/addons/gamification/models/challenge.py @@ -0,0 +1,766 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA () +# +# 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 +# +############################################################################## + +from openerp.osv import fields, osv +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from openerp.tools.translate import _ + +# from templates import TemplateHelper + +from datetime import date, datetime, timedelta +import calendar +import logging +_logger = logging.getLogger(__name__) + + +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 + + _sort = '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), + '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([ + ('progressbar', 'Individual Goals'), + ('board', '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_header': fields.text('Report Header'), + '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'), + + 'category': fields.selection([ + ('hr', 'Human Ressources / Engagement'), + ('other', 'Settings / Gamification Tools'), + ], + string="Appears in", help="Define the visibility of the challenge through menus", required=True), + } + + _defaults = { + 'period': 'once', + 'state': 'draft', + 'visibility_mode': 'progressbar', + '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, + } + + def write(self, cr, uid, ids, vals, context=None): + """Overwrite the write method to add the user of groups""" + context = context or {} + if not ids: + return True + + # 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 'user_ids' not in vals: + vals['user_ids'] = [] + vals['user_ids'] += [(4, user.id) for user in new_group.users] + + 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 + """ + if not context: context = {} + + # start planned challenges + planned_challenge_ids = self.search(cr, uid, [ + ('state', '=', 'draft'), + ('start_date', '<=', fields.date.today())]) + self.action_start(cr, uid, planned_challenge_ids, context=context) + + # close planned challenges + planned_challenge_ids = self.search(cr, uid, [ + ('state', '=', 'inprogress'), + ('end_date', '>=', fields.date.today())]) + self.action_close(cr, uid, planned_challenge_ids, 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 not context: context = {} + 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""" + if not context: context = {} + 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 + + + ##### User actions ##### + + def action_start(self, cr, uid, ids, context=None): + """Start a draft challenge + + Change the state of the challenge to in progress and generate related goals + """ + # subscribe users if autojoin group + for challenge in self.browse(cr, uid, ids, context=context): + if challenge.autojoin_group_id: + self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context) + + self.write(cr, uid, challenge.id, {'state': 'inprogress'}, context=context) + self.message_post(cr, uid, challenge.id, body="New challenge started.", context=context) + return self.generate_goals_from_challenge(cr, uid, ids, context=context) + + 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_close(self, cr, uid, ids, context=None): + """Close a challenge in progress + + Change the state of the challenge to in done + Does NOT close the related goals, this is handled by the goal itself""" + self.check_challenge_reward(cr, uid, ids, force=True, context=context) + return self.write(cr, uid, ids, {'state': 'done'}, context=context) + + def action_reset(self, cr, uid, ids, context=None): + """Reset a closed challenge + + Change the state of the challenge to in progress + Closing a challenge does not affect the goals so neither does reset""" + return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) + + def action_cancel(self, cr, uid, ids, context=None): + """Cancel a challenge in progress + + Change the state of the challenge to draft + Cancel the related goals""" + self.write(cr, uid, ids, {'state': 'draft'}, context=context) + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids)], context=context) + self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context) + + return True + + def action_report_progress(self, cr, uid, ids, context=None): + """Manual report of a goal, does not influence automatic report frequency""" + 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_board_goal_info(self, cr, uid, challenge, subset_goal_ids=False, context=None): + """Get the list of latest goals for a challenge, sorted by user ranking for each line""" + + goal_obj = self.pool.get('gamification.goal') + lines_boards = [] + (start_date, end_date) = start_end_date_for_period(challenge.period) + + for line in challenge.line_ids: + + domain = [ + ('line_id', '=', line.id), + ('state', 'in', ('inprogress', 'inprogress_update', + 'reached', 'failed')), + ] + + if subset_goal_ids: + goal_ids = goal_obj.search(cr, uid, domain, context=context) + common_goal_ids = [goal for goal in goal_ids if goal in subset_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)) + common_goal_ids = goal_obj.search(cr, uid, domain, context=context) + + board_goals = [goal for goal in goal_obj.browse(cr, uid, common_goal_ids, context=context)] + + if len(board_goals) == 0: + # line has no generated goals + continue + + # most complete first, current if same percentage (eg: if several 100%) + sorted_board = enumerate(sorted(board_goals, key=lambda k: (k.completeness, k.current), reverse=True)) + lines_boards.append({'goal_definition': line.definition_id, 'board_goals': sorted_board, 'target_goal': line.target_goal}) + return lines_boards + + def get_indivual_goal_info(self, cr, uid, user_id, challenge, subset_goal_ids=False, context=None): + """Get the list of latest goals of a user for a challenge""" + domain = [ + ('challenge_id', '=', challenge.id), + ('user_id', '=', user_id), + ('state', 'in', ('inprogress', 'inprogress_update', + 'reached', 'failed')), + ] + goal_obj = self.pool.get('gamification.goal') + (start_date, end_date) = start_end_date_for_period(challenge.period) + + if subset_goal_ids: + # use the domain for safety, don't want irrelevant report if wrong argument + goal_ids = goal_obj.search(cr, uid, domain, context=context) + related_goal_ids = [goal for goal in goal_ids if goal in subset_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)) + related_goal_ids = goal_obj.search(cr, uid, domain, context=context) + + if len(related_goal_ids) == 0: + return False + + goals = [] + all_done = True + for goal in goal_obj.browse(cr, uid, related_goal_ids, context=context): + if goal.end_date: + if goal.end_date < fields.date.today(): + # do not include goals of previous challenge run + continue + else: + all_done = False + else: + if goal.state == 'inprogress' or goal.state == 'inprogress_update': + all_done = False + + goals.append(goal) + + if all_done: + # skip challenges where all goal are done or failed + return False + else: + return goals + + ##### 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 + """ + + context = context or {} + # template_env = TemplateHelper() + temp_obj = self.pool.get('email.template') + ctx = context.copy() + if challenge.visibility_mode == 'board': + lines_boards = self.get_board_goal_info(cr, uid, challenge, subset_goal_ids, context) + + ctx.update({'lines_boards': lines_boards}) + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context) + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.challenge', challenge.id, context=context) + + # body_html = template_env.get_template('group_progress.mako').render({'object': challenge, 'lines_boards': lines_boards, 'uid': uid}) + + # 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_indivual_goal_info(cr, uid, user.id, challenge, subset_goal_ids, context=context) + if not goals: + continue + + ctx.update({'goals': goals}) + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) + body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.challenge', challenge.id, context=context) + # send message only to users + 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""" + context = context or {} + 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, uid, 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""" + context = context or {} + 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, uid, 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): + context = context or {} + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard') + id = result and result[1] or False + result = act_obj.read(cr, uid, [id], context=context)[0] + result['res_id'] = challenge_id + return result + + def check_challenge_reward(self, cr, uid, challenge_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) + """ + context = context or {} + for challenge in self.browse(cr, uid, challenge_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 += _("
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 += _("
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 += _("
Special rewards were sent to the top competing users. The ranking for this challenge is :") + message_body += "
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 += "
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 += "
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""" + user_badge_id = self.pool.get('gamification.badge.user').create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context) + return self.pool.get('gamification.badge').send_badge(cr, uid, badge_id, [user_badge_id], user_from=None, 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': { + 'definition_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'), + 'definition_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, + } diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py new file mode 100644 index 00000000000..5a170a904c6 --- /dev/null +++ b/addons/gamification/models/goal.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA () +# +# 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 +# +############################################################################## + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from openerp.tools.safe_eval import safe_eval +from openerp.tools.translate import _ + +from datetime import date, datetime, timedelta + +import logging + +_logger = logging.getLogger(__name__) + + +class gamification_goal_definition(osv.Model): + """Goal definition + + A goal definition contains the way to evaluate an objective + Each module wanting to be able to set goals to the users needs to create + a new gamification_goal_definition + """ + _name = 'gamification.goal.definition' + _description = 'Gamification goal definition' + + def _get_suffix(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, '') + for goal in self.browse(cr, uid, ids, context=context): + if goal.suffix and not goal.monetary: + res[goal.id] = goal.suffix + elif goal.monetary: + # use the current user's company currency + user = self.pool.get('res.users').browse(cr, uid, uid, context) + if goal.suffix: + res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix) + else: + res[goal.id] = user.company_id.currency_id.symbol + else: + res[goal.id] = "" + return res + + _columns = { + 'name': fields.char('Goal Definition', required=True, translate=True), + 'description': fields.text('Goal Description'), + 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."), + 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True), + 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"), + 'computation_mode': fields.selection([ + ('manually', 'Recorded manually'), + ('count', 'Automatic: number of records'), + ('sum', 'Automatic: sum on a field'), + ('python', 'Automatic: execute a specific Python code'), + ], + string="Computation Mode", + help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.", + required=True), + 'display_mode': fields.selection([ + ('progress', 'Progressive (using numerical values)'), + ('checkbox', 'Checkbox (done or not-done)'), + ], + string="Displayed as", required=True), + 'model_id': fields.many2one('ir.model', + string='Model', + help='The model object for the field to evaluate'), + 'field_id': fields.many2one('ir.model.fields', + string='Field to Sum', + help='The field containing the value to evaluate'), + 'field_date_id': fields.many2one('ir.model.fields', + string='Date Field', + help='The date to use for the time period evaluated'), + 'domain': fields.char("Filter Domain", + help="Technical filters rules to apply. Use 'user.id' (without marks) to limit the search to the evaluated user.", + required=True), + 'compute_code': fields.char('Compute Code', + help="The name of the python method that will be executed to compute the current value. See the file gamification/goal_definition_data.py for examples."), + 'condition': fields.selection([ + ('higher', 'The higher the better'), + ('lower', 'The lower the better') + ], + string='Goal Performance', + help='A goal is considered as completed when the current value is compared to the value to reach', + required=True), + 'action_id': fields.many2one('ir.actions.act_window', string="Action", + help="The action that will be called to update the goal value."), + 'res_id_field': fields.char("ID Field of user", + help="The field name on the user profile (res.users) containing the value for res_id for action.") + } + + _defaults = { + 'condition': 'higher', + 'computation_mode': 'manually', + 'domain': "[]", + 'monetary': False, + 'display_mode': 'progress', + } + + +class gamification_goal(osv.Model): + """Goal instance for a user + + An individual goal for a user on a specified time period""" + + _name = 'gamification.goal' + _description = 'Gamification goal instance' + _inherit = 'mail.thread' + + def _get_completeness(self, cr, uid, ids, field_name, arg, context=None): + """Return the percentage of completeness of the goal, between 0 and 100""" + res = dict.fromkeys(ids, 0.0) + for goal in self.browse(cr, uid, ids, context=context): + if goal.definition_condition == 'higher': + if goal.current >= goal.target_goal: + res[goal.id] = 100.0 + else: + res[goal.id] = round(100.0 * goal.current / goal.target_goal, 2) + elif goal.current < goal.target_goal: + # a goal 'lower than' has only two values possible: 0 or 100% + res[goal.id] = 100.0 + else: + res[goal.id] = 0.0 + return res + + 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) + return {'value': {'computation_mode': goal_definition.computation_mode, 'definition_condition': goal_definition.condition}} + + _columns = { + 'definition_id': fields.many2one('gamification.goal.definition', string='Goal Definition', required=True, ondelete="cascade"), + 'user_id': fields.many2one('res.users', string='User', required=True), + 'line_id': fields.many2one('gamification.challenge.line', string='Goal Line', ondelete="cascade"), + 'challenge_id': fields.related('line_id', 'challenge_id', + string="Challenge", + type='many2one', + relation='gamification.challenge', + store=True), + 'start_date': fields.date('Start Date'), + 'end_date': fields.date('End Date'), # no start and end = always active + 'target_goal': fields.float('To Reach', + required=True, + track_visibility='always'), # no goal = global index + 'current': fields.float('Current Value', required=True, track_visibility='always'), + 'completeness': fields.function(_get_completeness, type='float', string='Completeness'), + 'state': fields.selection([ + ('draft', 'Draft'), + ('inprogress', 'In progress'), + ('inprogress_update', 'In progress (to update)'), + ('reached', 'Reached'), + ('failed', 'Failed'), + ('canceled', 'Canceled'), + ], + string='State', + required=True, + track_visibility='always'), + + 'computation_mode': fields.related('definition_id', 'computation_mode', type='char', string="Computation mode"), + 'remind_update_delay': fields.integer('Remind delay', + help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."), + 'last_update': fields.date('Last Update', + help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge."), + + 'definition_description': fields.related('definition_id', 'description', type='char', string='Definition Description', readonly=True), + 'definition_condition': fields.related('definition_id', 'condition', type='char', string='Definition Condition', readonly=True), + 'definition_suffix': fields.related('definition_id', 'full_suffix', type="char", string="Suffix", readonly=True), + 'definition_display': fields.related('definition_id', 'display_mode', type="char", string="Display Mode", readonly=True), + } + + _defaults = { + 'current': 0, + 'state': 'draft', + 'start_date': fields.date.today, + } + _order = 'create_date desc, end_date desc, definition_id, id' + + def _check_remind_delay(self, cr, uid, goal, context=None): + """Verify if a goal has not been updated for some time and send a + reminder message of needed. + + :return: data to write on the goal object + """ + if goal.remind_update_delay and goal.last_update: + delta_max = timedelta(days=goal.remind_update_delay) + last_update = datetime.strptime(goal.last_update, DF).date() + if date.today() - last_update > delta_max and goal.state == 'inprogress': + # generate a remind report + temp_obj = self.pool.get('email.template') + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context) + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context) + + self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment') + return {'state': 'inprogress_update'} + return {} + + def update(self, cr, uid, ids, context=None): + """Update the goals to recomputes values and change of states + + If a manual goal is not updated for enough time, the user will be + reminded to do so (done only once, in 'inprogress' state). + If a goal reaches the target value, the status is set to reached + If the end date is passed (at least +1 day, time not considered) without + the target value being reached, the goal is set as failed.""" + + for goal in self.browse(cr, uid, ids, context=context): + towrite = {} + if goal.state in ('draft', 'canceled'): + # skip if goal draft or canceled + continue + + if goal.definition_id.computation_mode == 'manually': + towrite.update(self._check_remind_delay(cr, uid, goal, context)) + + elif goal.definition_id.computation_mode == 'python': + # execute the chosen method + values = {'cr': cr, 'uid': goal.user_id.id, 'context': context, 'self': self.pool.get('gamification.goal.definition')} + result = safe_eval(goal.definition_id.compute_code, values, {}) + + if type(result) in (float, int, long) and result != goal.current: + towrite['current'] = result + else: + _logger.exception(_('Unvalid return content from the evaluation of %s' % str(goal.definition_id.compute_code))) + # raise osv.except_osv(_('Error!'), _('Unvalid return content from the evaluation of %s' % str(goal.definition_id.compute_code))) + + else: # count or sum + obj = self.pool.get(goal.definition_id.model_id.model) + field_date_name = goal.definition_id.field_date_id.name + + # eval the domain with user replaced by goal user object + domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id}) + + #add temporal clause(s) to the domain if fields are filled on the goal + if goal.start_date and field_date_name: + domain.append((field_date_name, '>=', goal.start_date)) + if goal.end_date and field_date_name: + domain.append((field_date_name, '<=', goal.end_date)) + + if goal.definition_id.computation_mode == 'sum': + field_name = goal.definition_id.field_id.name + res = obj.read_group(cr, uid, domain, [field_name], [''], context=context) + new_value = res and res[0][field_name] or 0.0 + + else: # computation mode = count + new_value = obj.search(cr, uid, domain, context=context, count=True) + + #avoid useless write if the new value is the same as the old one + if new_value != goal.current: + towrite['current'] = new_value + + # check goal target reached + if (goal.definition_id.condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_id.condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal): + towrite['state'] = 'reached' + + # check goal failure + elif goal.end_date and fields.date.today() > goal.end_date: + towrite['state'] = 'failed' + if towrite: + self.write(cr, uid, [goal.id], towrite, context=context) + return True + + def action_start(self, cr, uid, ids, context=None): + """Mark a goal as started. + + This should only be used when creating goals manually (in draft state)""" + self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) + return self.update(cr, uid, ids, context=context) + + def action_reach(self, cr, uid, ids, context=None): + """Mark a goal as reached. + + If the target goal condition is not met, the state will be reset to In + Progress at the next goal update until the end date.""" + return self.write(cr, uid, ids, {'state': 'reached'}, context=context) + + def action_fail(self, cr, uid, ids, context=None): + """Set the state of the goal to failed. + + A failed goal will be ignored in future checks.""" + return self.write(cr, uid, ids, {'state': 'failed'}, context=context) + + def action_cancel(self, cr, uid, ids, context=None): + """Reset the completion after setting a goal as reached or failed. + + This is only the current state, if the date and/or target criterias + match the conditions for a change of state, this will be applied at the + next goal update.""" + return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) + + def create(self, cr, uid, vals, context=None): + """Overwrite the create method to add a 'no_remind_goal' field to True""" + context = context or {} + context['no_remind_goal'] = True + return super(gamification_goal, self).create(cr, uid, vals, context=context) + + def write(self, cr, uid, ids, vals, context=None): + """Overwrite the write method to update the last_update field to today + + If the current value is changed and the report frequency is set to On + change, a report is generated + """ + vals['last_update'] = fields.date.today() + result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context) + for goal in self.browse(cr, uid, ids, context=context): + if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals): + # avoid drag&drop in kanban view + raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal')) + + if vals.get('current'): + if 'no_remind_goal' in context: + # new goals should not be reported + continue + + if goal.challenge_id and goal.challenge_id.report_message_frequency == 'onchange': + self.pool.get('gamification.challenge').report_progress(cr, SUPERUSER_ID, goal.challenge_id, users=[goal.user_id], context=context) + return result + + def get_action(self, cr, uid, goal_id, context=None): + """Get the ir.action related to update the goal + + In case of a manual goal, should return a wizard to update the value + :return: action description in a dictionnary + """ + goal = self.browse(cr, uid, goal_id, context=context) + + if goal.definition_id.action_id: + #open a the action linked on the goal + action = goal.definition_id.action_id.read()[0] + + if goal.definition_id.res_id_field: + current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user}) + + # if one element to display, should see it in form mode if possible + views = action['views'] + for (view_id, mode) in action['views']: + if mode == "form": + views = [(view_id, mode)] + break + action['views'] = views + return action + + if goal.computation_mode == 'manually': + #open a wizard window to update the value manually + action = { + 'name': _("Update %s") % goal.definition_id.name, + 'id': goal_id, + 'type': 'ir.actions.act_window', + 'views': [[False, 'form']], + 'target': 'new', + } + action['context'] = {'default_goal_id': goal_id, 'default_current': goal.current} + action['res_model'] = 'gamification.goal.wizard' + return action + + return False + + +class goal_manual_wizard(osv.TransientModel): + """Wizard to update a manual goal""" + _name = 'gamification.goal.wizard' + _columns = { + 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True), + 'current': fields.float('Current'), + } + + def action_update_current(self, cr, uid, ids, context=None): + """Wizard action for updating the current value""" + + goal_obj = self.pool.get('gamification.goal') + + for wiz in self.browse(cr, uid, ids, context=context): + towrite = { + 'current': wiz.current, + 'goal_id': wiz.goal_id.id, + } + goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context) + goal_obj.update(cr, uid, [wiz.goal_id.id], context=context) + return {} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py new file mode 100644 index 00000000000..38e784247fd --- /dev/null +++ b/addons/gamification/models/res_users.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013 OpenERP SA () +# +# 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 +# +############################################################################## + +from openerp.osv import osv + + +class res_users_gamification_group(osv.Model): + """ Update of res.users class + - if adding groups to an user, check gamification.challenge linked to + this group, and the user. This is done by overriding the write method. + """ + _name = 'res.users' + _inherit = ['res.users'] + + def write(self, cr, uid, ids, vals, context=None): + write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context) + if vals.get('groups_id'): + # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} + user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4] + user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] + + challenge_obj = self.pool.get('gamification.challenge') + challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) + if challenge_ids: + challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) + + if vals.get('image'): + goal_definition_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'definition_base_avatar', context) + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('definition_id', '=', goal_definition_id.id), ('user_id', 'in', ids)], context=context) + values = {'state': 'reached', 'current': 1} + self.pool.get('gamification.goal').write(cr, uid, goal_ids, values, context=context) + return write_res + + def get_goals_todo_info(self, cr, uid, context=None): + """Return the list of goals assigned to the user, grouped by challenge + + This method intends to return processable data in javascript in the + goal_list_to_do template. The output format is not constant as the + required information is different between individual and board goal + definitions + :return: list of dictionnaries for each goal to display + """ + all_goals_info = [] + challenge_obj = self.pool.get('gamification.challenge') + + challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) + for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context): + # serialize goals info to be able to use it in javascript + serialized_goals_info = { + 'id': challenge.id, + 'name': challenge.name, + 'visibility_mode': challenge.visibility_mode, + } + user = self.browse(cr, uid, uid, context=context) + serialized_goals_info['currency'] = user.company_id.currency_id.id + + if challenge.visibility_mode == 'board': + # board report should be grouped by line for all users + goals_info = challenge_obj.get_board_goal_info(cr, uid, challenge, subset_goal_ids=False, context=context) + + if len(goals_info) == 0: + # challenge with no valid lines + continue + + serialized_goals_info['lines'] = [] + for line_board in goals_info: + vals = {'definition_name': line_board['goal_definition'].name, + 'definition_description': line_board['goal_definition'].description, + 'definition_condition': line_board['goal_definition'].condition, + 'computation_mode': line_board['goal_definition'].computation_mode, + 'definition_monetary': line_board['goal_definition'].monetary, + 'definition_suffix': line_board['goal_definition'].suffix, + 'definition_action': True if line_board['goal_definition'].action_id else False, + 'definition_display': line_board['goal_definition'].display_mode, + 'target_goal': line_board['target_goal'], + 'goals': []} + for goal in line_board['board_goals']: + # Keep only the Top 3 and the current user + if goal[0] > 2 and goal[1].user_id.id != uid: + continue + + vals['goals'].append({ + 'rank': goal[0] + 1, + 'id': goal[1].id, + 'user_id': goal[1].user_id.id, + 'user_name': goal[1].user_id.name, + 'state': goal[1].state, + 'completeness': goal[1].completeness, + 'current': goal[1].current, + 'target_goal': goal[1].target_goal, + }) + if uid == goal[1].user_id.id: + vals['own_goal_id'] = goal[1].id + serialized_goals_info['lines'].append(vals) + + else: + # individual report are simply a list of goal + goals_info = challenge_obj.get_indivual_goal_info(cr, uid, uid, challenge, subset_goal_ids=False, context=context) + + if not goals_info: + continue + + serialized_goals_info['goals'] = [] + for goal in goals_info: + serialized_goals_info['goals'].append({ + 'id': goal.id, + 'definition_name': goal.definition_id.name, + 'definition_description': goal.definition_description, + 'definition_condition': goal.definition_id.condition, + 'definition_monetary': goal.definition_id.monetary, + 'definition_suffix': goal.definition_id.suffix, + 'definition_action': True if goal.definition_id.action_id else False, + 'definition_display': goal.definition_id.display_mode, + 'state': goal.state, + 'completeness': goal.completeness, + 'computation_mode': goal.computation_mode, + 'current': goal.current, + 'target_goal': goal.target_goal, + }) + + all_goals_info.append(serialized_goals_info) + return all_goals_info + + def get_challenge_suggestions(self, cr, uid, context=None): + """Return the list of challenges suggested to the user""" + challenge_info = [] + challenge_obj = self.pool.get('gamification.challenge') + challenge_ids = challenge_obj.search(cr, uid, [('invited_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) + for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context): + values = { + 'id': challenge.id, + 'name': challenge.name, + 'description': challenge.description, + } + challenge_info.append(values) + return challenge_info + + +class res_groups_gamification_group(osv.Model): + """ Update of res.groups class + - if adding users from a group, check gamification.challenge linked to + this group, and the user. This is done by overriding the write method. + """ + _name = 'res.groups' + _inherit = 'res.groups' + + def write(self, cr, uid, ids, vals, context=None): + write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context) + if vals.get('users'): + # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} + user_ids = [command[1] for command in vals['users'] if command[0] == 4] + user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]] + + challenge_obj = self.pool.get('gamification.challenge') + challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context) + if challenge_ids: + challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) + return write_res +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: -- cgit v1.2.3 From c184adab447610a8191fde58c522e881e9d9bbf3 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 17 Dec 2013 17:46:19 +0100 Subject: [REF] gamification bzr revid: mat@openerp.com-20131217164619-ewgj8cclza3u44pp --- addons/gamification/models/challenge.py | 12 ++---------- addons/gamification/models/res_users.py | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index f0873a368d7..73ad5f4d982 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -198,7 +198,6 @@ class gamification_challenge(osv.Model): def write(self, cr, uid, ids, vals, context=None): """Overwrite the write method to add the user of groups""" - context = context or {} if not ids: return True @@ -230,8 +229,6 @@ class gamification_challenge(osv.Model): - Create the missing goals (eg: modified the challenge to add lines) - Update every running challenge """ - if not context: context = {} - # start planned challenges planned_challenge_ids = self.search(cr, uid, [ ('state', '=', 'draft'), @@ -254,7 +251,6 @@ class gamification_challenge(osv.Model): :param list(int) ids: the ids of the challenges to update, if False will update only challenges in progress.""" - if not context: context = {} goal_obj = self.pool.get('gamification.goal') # we use yesterday to update the goals that just ended @@ -297,7 +293,6 @@ class gamification_challenge(osv.Model): def quick_update(self, cr, uid, challenge_id, context=None): """Update all the goals of a challenge, no generation of new goals""" - if not context: context = {} 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 @@ -521,8 +516,8 @@ class gamification_challenge(osv.Model): periods. :param subset_goal_ids: a list(int) of goal ids to restrict the report """ - - context = context or {} + if context is None: + context = {} # template_env = TemplateHelper() temp_obj = self.pool.get('email.template') ctx = context.copy() @@ -573,7 +568,6 @@ class gamification_challenge(osv.Model): def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): """The user accept the suggested challenge""" - context = context or {} 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 @@ -583,7 +577,6 @@ class gamification_challenge(osv.Model): def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): """The user discard the suggested challenge""" - context = context or {} 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 @@ -591,7 +584,6 @@ class gamification_challenge(osv.Model): 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): - context = context or {} mod_obj = self.pool.get('ir.model.data') act_obj = self.pool.get('ir.actions.act_window') result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard') diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 38e784247fd..0f0144e0710 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -76,7 +76,7 @@ class res_users_gamification_group(osv.Model): # board report should be grouped by line for all users goals_info = challenge_obj.get_board_goal_info(cr, uid, challenge, subset_goal_ids=False, context=context) - if len(goals_info) == 0: + if not goals_info: # challenge with no valid lines continue -- cgit v1.2.3 From 4bc29b9ffae6177dfcc42c69216f29e482c0716e Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 17 Dec 2013 18:02:32 +0100 Subject: [REF] gamification bzr revid: mat@openerp.com-20131217170232-e0zruxzl1wlycaji --- addons/gamification/models/challenge.py | 21 +++++++++++---------- addons/gamification/models/res_users.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 73ad5f4d982..31a22584787 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -101,6 +101,11 @@ class gamification_challenge(osv.Model): res[challenge.id] = False return res + def _get_categories(self, cr, uid, context=None): + return [ + ('hr', 'Human Ressources / Engagement'), + ('other', 'Settings / Gamification Tools'), + ] _sort = 'end_date, start_date, name, id' _columns = { @@ -153,8 +158,8 @@ class gamification_challenge(osv.Model): 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'), 'visibility_mode': fields.selection([ - ('progressbar', 'Individual Goals'), - ('board', 'Leader Board (Group Ranking)'), + ('personal', 'Individual Goals'), + ('ranking', 'Leader Board (Group Ranking)'), ], string="Display Mode", required=True), 'report_message_frequency': fields.selection([ @@ -174,20 +179,16 @@ class gamification_challenge(osv.Model): 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'), + type='date', string='Next Report Date', store=True), - 'category': fields.selection([ - ('hr', 'Human Ressources / Engagement'), - ('other', 'Settings / Gamification Tools'), - ], + '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': 'progressbar', + 'visibility_mode': 'personal', 'report_message_frequency': 'never', 'last_report_date': fields.date.today, 'start_date': fields.date.today, @@ -521,7 +522,7 @@ class gamification_challenge(osv.Model): # template_env = TemplateHelper() temp_obj = self.pool.get('email.template') ctx = context.copy() - if challenge.visibility_mode == 'board': + if challenge.visibility_mode == 'ranking': lines_boards = self.get_board_goal_info(cr, uid, challenge, subset_goal_ids, context) ctx.update({'lines_boards': lines_boards}) diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 0f0144e0710..ef544c95692 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -72,7 +72,7 @@ class res_users_gamification_group(osv.Model): user = self.browse(cr, uid, uid, context=context) serialized_goals_info['currency'] = user.company_id.currency_id.id - if challenge.visibility_mode == 'board': + if challenge.visibility_mode == 'ranking': # board report should be grouped by line for all users goals_info = challenge_obj.get_board_goal_info(cr, uid, challenge, subset_goal_ids=False, context=context) -- cgit v1.2.3 From b63950e0dc49220bed8e9e2c1a7f922f5300b490 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 18 Dec 2013 09:59:07 +0100 Subject: [REF] gamification: mode to adequate folders bzr revid: mat@openerp.com-20131218085907-6501sfdif2rk4a0b --- addons/gamification/models/badge.py | 38 --------------------------------- addons/gamification/models/goal.py | 24 --------------------- addons/gamification/models/res_users.py | 4 ++-- 3 files changed, 2 insertions(+), 64 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 6aa2f89e74e..ad3767cbbc4 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -24,7 +24,6 @@ from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF from openerp.tools.translate import _ -# from templates import TemplateHelper from datetime import date import logging @@ -111,7 +110,6 @@ class gamification_badge(osv.Model): 'name': fields.char('Badge', required=True, translate=True), 'description': fields.text('Description'), 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"), - # image_select: selection with a on_change to fill image with predefined picts 'rule_auth': fields.selection([ ('everyone', 'Everyone'), ('users', 'A selected list of users'), @@ -261,39 +259,3 @@ class gamification_badge(osv.Model): # badge.rule_auth == 'everyone' -> no check return 1 - - -class grant_badge_wizard(osv.TransientModel): - """ Wizard allowing to grant a badge to a user""" - - _name = 'gamification.badge.user.wizard' - _columns = { - 'user_id': fields.many2one("res.users", string='User', required=True), - 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True), - 'comment': fields.text('Comment'), - } - - def action_grant_badge(self, cr, uid, ids, context=None): - """Wizard action for sending a badge to a chosen user""" - - badge_obj = self.pool.get('gamification.badge') - badge_user_obj = self.pool.get('gamification.badge.user') - - for wiz in self.browse(cr, uid, ids, context=context): - if uid == wiz.user_id.id: - raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself')) - - #check if the badge granting is legitimate - if badge_obj.check_granting(cr, uid, user_from_id=uid, badge_id=wiz.badge_id.id, context=context): - #create the badge - values = { - 'user_id': wiz.user_id.id, - 'badge_id': wiz.badge_id.id, - 'comment': wiz.comment, - } - badge_user = badge_user_obj.create(cr, uid, values, context=context) - #notify the user - result = badge_obj.send_badge(cr, uid, wiz.badge_id.id, [badge_user], user_from=uid, context=context) - - return result -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 5a170a904c6..529f706fecd 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -372,27 +372,3 @@ class gamification_goal(osv.Model): return action return False - - -class goal_manual_wizard(osv.TransientModel): - """Wizard to update a manual goal""" - _name = 'gamification.goal.wizard' - _columns = { - 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True), - 'current': fields.float('Current'), - } - - def action_update_current(self, cr, uid, ids, context=None): - """Wizard action for updating the current value""" - - goal_obj = self.pool.get('gamification.goal') - - for wiz in self.browse(cr, uid, ids, context=context): - towrite = { - 'current': wiz.current, - 'goal_id': wiz.goal_id.id, - } - goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context) - goal_obj.update(cr, uid, [wiz.goal_id.id], context=context) - return {} -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index ef544c95692..6c8ca7646b3 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -61,6 +61,7 @@ class res_users_gamification_group(osv.Model): all_goals_info = [] challenge_obj = self.pool.get('gamification.challenge') + user = self.browse(cr, uid, uid, context=context) challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context): # serialize goals info to be able to use it in javascript @@ -68,9 +69,8 @@ class res_users_gamification_group(osv.Model): 'id': challenge.id, 'name': challenge.name, 'visibility_mode': challenge.visibility_mode, + 'currency': user.company_id.currency_id.id } - user = self.browse(cr, uid, uid, context=context) - serialized_goals_info['currency'] = user.company_id.currency_id.id if challenge.visibility_mode == 'ranking': # board report should be grouped by line for all users -- cgit v1.2.3 From e7887b8b8f892832c5b65b82358d022496a2729d Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 18 Dec 2013 12:12:43 +0100 Subject: [REF] gamification: python tests and other improvements bzr revid: mat@openerp.com-20131218111243-npnauv6mxhb4nttg --- addons/gamification/models/challenge.py | 39 ++++++++++++++++++++++++++++----- addons/gamification/models/goal.py | 5 ++++- addons/gamification/models/res_users.py | 20 ++++++++++++----- 3 files changed, 52 insertions(+), 12 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 31a22584787..4d34fe7b640 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -197,11 +197,30 @@ class gamification_challenge(osv.Model): 'reward_failure': False, } + + 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 'user_ids' not in vals: + 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): """Overwrite the write method to add the user of groups""" - if not ids: - return True - # 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) @@ -306,6 +325,8 @@ class gamification_challenge(osv.Model): Change the state of the challenge to in progress and generate related goals """ + if isinstance(ids, (int,long)): + ids = [ids] # subscribe users if autojoin group for challenge in self.browse(cr, uid, ids, context=context): if challenge.autojoin_group_id: @@ -320,6 +341,8 @@ class gamification_challenge(osv.Model): Create goals that haven't been created yet (eg: if added users) Recompute the current value for each goal related""" + if isinstance(ids, (int,long)): + ids = [ids] return self._update_all(cr, uid, ids=ids, context=context) def action_close(self, cr, uid, ids, context=None): @@ -342,6 +365,8 @@ class gamification_challenge(osv.Model): Change the state of the challenge to draft Cancel the related goals""" + if isinstance(ids, (int,long)): + ids = [ids] self.write(cr, uid, ids, {'state': 'draft'}, context=context) goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids)], context=context) self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context) @@ -350,6 +375,8 @@ class gamification_challenge(osv.Model): 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 @@ -593,7 +620,7 @@ class gamification_challenge(osv.Model): result['res_id'] = challenge_id return result - def check_challenge_reward(self, cr, uid, challenge_ids, force=False, context=None): + 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. @@ -603,8 +630,10 @@ class gamification_challenge(osv.Model): - 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, challenge_ids, context=context): + 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: diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 529f706fecd..5464b51a4ea 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -307,7 +307,8 @@ class gamification_goal(osv.Model): def create(self, cr, uid, vals, context=None): """Overwrite the create method to add a 'no_remind_goal' field to True""" - context = context or {} + if context is None: + context = {} context['no_remind_goal'] = True return super(gamification_goal, self).create(cr, uid, vals, context=context) @@ -317,6 +318,8 @@ class gamification_goal(osv.Model): If the current value is changed and the report frequency is set to On change, a report is generated """ + if context is None: + context = {} vals['last_update'] = fields.date.today() result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context) for goal in self.browse(cr, uid, ids, context=context): diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 6c8ca7646b3..d831ab2ea25 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -31,6 +31,7 @@ class res_users_gamification_group(osv.Model): _inherit = ['res.users'] def write(self, cr, uid, ids, vals, context=None): + """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge""" write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context) if vals.get('groups_id'): # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} @@ -41,12 +42,20 @@ class res_users_gamification_group(osv.Model): challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) if challenge_ids: challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) + return write_res - if vals.get('image'): - goal_definition_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'definition_base_avatar', context) - goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('definition_id', '=', goal_definition_id.id), ('user_id', 'in', ids)], context=context) - values = {'state': 'reached', 'current': 1} - self.pool.get('gamification.goal').write(cr, uid, goal_ids, values, context=context) + def create(self, cr, uid, vals, context=None): + """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge""" + write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context) + if vals.get('groups_id'): + # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} + user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4] + user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] + + challenge_obj = self.pool.get('gamification.challenge') + challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) + if challenge_ids: + challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context) return write_res def get_goals_todo_info(self, cr, uid, context=None): @@ -174,4 +183,3 @@ class res_groups_gamification_group(osv.Model): if challenge_ids: challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) return write_res -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: -- cgit v1.2.3 From 553e85f369a1383a91e9e10f4237434d1d523a27 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 18 Dec 2013 13:14:59 +0100 Subject: [FIX] gamification: move manifest order and remove useless browse bzr revid: mat@openerp.com-20131218121459-zlfumwysdx9qm7d9 --- addons/gamification/models/badge.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index ad3767cbbc4..099176dc88f 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -78,12 +78,12 @@ class gamification_badge(osv.Model): result = dict.fromkeys(ids, False) badge_user_obj = self.pool.get('gamification.badge.user') first_month_day = date.today().replace(day=1).strftime(DF) - for obj in self.browse(cr, uid, ids, context=context): - result[obj.id] = { - 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid)], context=context, count=True), - 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_date', '>=', first_month_day)], context=context, count=True), - 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True), - 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True) + for bid in ids: + result[id] = { + 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True), + 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True), + 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True), + 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True) } return result -- cgit v1.2.3 From 938a07a3610337e4aafebdc334aef33d9bcf152f Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 18 Dec 2013 14:00:39 +0100 Subject: [FIX] gamification: check grant badges are now sistematic except for superuser (always possible) bzr revid: mat@openerp.com-20131218130039-y5vp0wt48ejd3jrh --- addons/gamification/models/badge.py | 76 ++++++++++++++++++--------------- addons/gamification/models/challenge.py | 11 +++-- 2 files changed, 49 insertions(+), 38 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 099176dc88f..4f11c4192f0 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -43,10 +43,37 @@ class gamification_badge_user(osv.Model): 'comment': fields.text('Comment'), 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"), 'create_date': fields.datetime('Created', readonly=True), - 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), + 'create_uid': fields.many2one('res.users', string='Creator', readonly=True), } + def _send_badge(self, cr, uid, ids, user_from=False, context=None): + """Send a notification to a user for receiving a badge + + Does not verify constrains on badge granting. + The users are added to the owner_ids (create badge_user if needed) + The stats counters are incremented + :param ids: list(int) of badge users that will receive the badge + :param user_from: optional id of the res.users object that has sent the badge + """ + res = True + temp_obj = self.pool.get('email.template') + user_obj = self.pool.get('res.users') + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) + ctx = context.copy() + for badge_user in self.browse(cr, uid, ids, context=context): + + ctx.update({'user_from': user_obj.browse(cr, uid, user_from).name}) + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) + + res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context) + return res + + def create(self, cr, uid, vals, context=None): + self.pool.get('gamification.badge').check_granting(cr, uid, badge_id=vals.get('badge_id'), context=context) + return super(gamification_badge_user, self).create(cr, uid, vals, context=context) + + class gamification_badge(osv.Model): """Badge object that users can send and receive""" @@ -96,7 +123,7 @@ class gamification_badge(osv.Model): """ result = dict.fromkeys(ids, False) for badge in self.browse(cr, uid, ids, context=context): - if self._can_grant_badge(cr, uid, uid, badge.id, context) != 1: + if self._can_grant_badge(cr, uid, badge.id, context) != 1: # if the user cannot grant this badge at all, result is 0 result[badge.id] = 0 elif not badge.rule_max: @@ -183,37 +210,13 @@ class gamification_badge(osv.Model): 'rule_auth': 'everyone', } - def send_badge(self, cr, uid, badge_id, badge_user_ids, user_from=False, context=None): - """Send a notification to a user for receiving a badge + def check_granting(self, cr, uid, badge_id, context=None): + """Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception + if not - Does NOT verify constrains on badge granting. - The users are added to the owner_ids (create badge_user if needed) - The stats counters are incremented - :param badge_id: id of the badge to deserve - :param badge_user_ids: list(int) of badge users that will receive the badge - :param user_from: optional id of the res.users object that has sent the badge + Do not check for SUPERUSER_ID """ - badge = self.browse(cr, uid, badge_id, context=context) - # template_env = TemplateHelper() - - res = None - temp_obj = self.pool.get('email.template') - template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) - ctx = context.copy() - for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context=context): - - ctx.update({'user_from': self.pool.get('res.users').browse(cr, uid, user_from).name}) - - body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) - - # as SUPERUSER as normal user don't have write access on a badge - res = self.message_post(cr, SUPERUSER_ID, badge.id, partner_ids=[badge_user.user_id.partner_id.id], body=body_html, type='comment', subtype='mt_comment', context=context) - return res - - def check_granting(self, cr, uid, user_from_id, badge_id, context=None): - """Check the user 'user_from_id' can grant the badge 'badge_id' and raise the appropriate exception - if not""" - status_code = self._can_grant_badge(cr, uid, user_from_id, badge_id, context=context) + status_code = self._can_grant_badge(cr, uid, badge_id, context=context) if status_code == 1: return True elif status_code == 2: @@ -228,10 +231,10 @@ class gamification_badge(osv.Model): _logger.exception("Unknown badge status code: %d" % int(status_code)) return False - def _can_grant_badge(self, cr, uid, user_from_id, badge_id, context=None): + def _can_grant_badge(self, cr, uid, badge_id, context=None): """Check if a user can grant a badge to another user - :param user_from_id: the id of the res.users trying to send the badge + :param uid: the id of the res.users trying to send the badge :param badge_id: the granted badge id :return: integer representing the permission. 1: can grant @@ -240,16 +243,19 @@ class gamification_badge(osv.Model): 4: don't have the required badges 5: user's monthly limit reached """ + if uid == SUPERUSER_ID: + return 1 + badge = self.browse(cr, uid, badge_id, context=context) if badge.rule_auth == 'nobody': return 2 - elif badge.rule_auth == 'users' and user_from_id not in [user.id for user in badge.rule_auth_user_ids]: + elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]: return 3 elif badge.rule_auth == 'having': - all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', user_from_id)], context=context) + all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context) for required_badge in badge.rule_auth_badge_ids: if required_badge.id not in all_user_badges: return 4 diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 4d34fe7b640..0f3e64626cb 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -732,9 +732,14 @@ class gamification_challenge(osv.Model): 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""" - user_badge_id = self.pool.get('gamification.badge.user').create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context) - return self.pool.get('gamification.badge').send_badge(cr, uid, badge_id, [user_badge_id], user_from=None, context=context) + """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], user_from=None, context=context) class gamification_challenge_line(osv.Model): -- cgit v1.2.3 From 90dfa5895d61fae037046aea2b64b25d9903df15 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 18 Dec 2013 15:03:33 +0100 Subject: [FIX] gamification: send badge on user, fix function field bzr revid: mat@openerp.com-20131218140333-nipcc21z5snhac1c --- addons/gamification/models/badge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 4f11c4192f0..99ad7f93edd 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -106,7 +106,7 @@ class gamification_badge(osv.Model): badge_user_obj = self.pool.get('gamification.badge.user') first_month_day = date.today().replace(day=1).strftime(DF) for bid in ids: - result[id] = { + result[bid] = { 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True), 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True), 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True), -- cgit v1.2.3 From f77aefc07ace66587bc13f7d2fa55755968437c5 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 20 Dec 2013 15:45:21 +0100 Subject: [REF] gamification: cleaning methods and reports bzr revid: mat@openerp.com-20131220144521-wyu1gx8bcitu6nk4 --- addons/gamification/models/challenge.py | 215 ++++++++++++++++++++------------ addons/gamification/models/goal.py | 1 + addons/gamification/models/res_users.py | 99 ++++----------- 3 files changed, 156 insertions(+), 159 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 0f3e64626cb..7cbd784b9bf 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -19,17 +19,18 @@ # ############################################################################## +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 templates import TemplateHelper - 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 @@ -107,7 +108,7 @@ class gamification_challenge(osv.Model): ('other', 'Settings / Gamification Tools'), ] - _sort = 'end_date, start_date, name, id' + _order = 'end_date, start_date, name, id' _columns = { 'name': fields.char('Challenge Name', required=True, translate=True), 'description': fields.text('Description', translate=True), @@ -444,89 +445,137 @@ class gamification_challenge(osv.Model): ##### JS utilities ##### - def get_board_goal_info(self, cr, uid, challenge, subset_goal_ids=False, context=None): - """Get the list of latest goals for a challenge, sorted by user ranking for each line""" - + 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: compute only the results for this subset if gamification.goal ids, if False retrieve every goal of current running challenge + :restrict_top: 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': , + 'description': , + 'condition': , + 'computation_mode': , + 'monetary': <{True,False}>, + 'suffix': , + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': , + 'own_goal_id': , + 'goals': [ + { + 'id': , + 'rank': , + 'user_id': , + 'name': , + 'state': , + 'completeness': , + 'current': , + } + ] + }, + # if visibility_mode == 'personal' + { + 'id': , + 'name': , + 'description': , + 'condition': , + 'computation_mode': , + 'monetary': <{True,False}>, + 'suffix': , + 'action': <{True,False}>, + 'display_mode': <{progress,boolean}>, + 'target': , + 'state': , + 'completeness': , + 'current': , + } + """ goal_obj = self.pool.get('gamification.goal') - lines_boards = [] (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', 'in', ('inprogress', 'inprogress_update', - 'reached', 'failed')), + ('state', '!=', 'draft'), ] - - if subset_goal_ids: - goal_ids = goal_obj.search(cr, uid, domain, context=context) - common_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids] + 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)) - common_goal_ids = goal_obj.search(cr, uid, domain, context=context) - - board_goals = [goal for goal in goal_obj.browse(cr, uid, common_goal_ids, context=context)] - - if len(board_goals) == 0: - # line has no generated goals - continue - - # most complete first, current if same percentage (eg: if several 100%) - sorted_board = enumerate(sorted(board_goals, key=lambda k: (k.completeness, k.current), reverse=True)) - lines_boards.append({'goal_definition': line.definition_id, 'board_goals': sorted_board, 'target_goal': line.target_goal}) - return lines_boards - - def get_indivual_goal_info(self, cr, uid, user_id, challenge, subset_goal_ids=False, context=None): - """Get the list of latest goals of a user for a challenge""" - domain = [ - ('challenge_id', '=', challenge.id), - ('user_id', '=', user_id), - ('state', 'in', ('inprogress', 'inprogress_update', - 'reached', 'failed')), - ] - goal_obj = self.pool.get('gamification.goal') - (start_date, end_date) = start_end_date_for_period(challenge.period) - if subset_goal_ids: - # use the domain for safety, don't want irrelevant report if wrong argument - goal_ids = goal_obj.search(cr, uid, domain, context=context) - related_goal_ids = [goal for goal in goal_ids if goal in subset_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)) - related_goal_ids = goal_obj.search(cr, uid, domain, context=context) - - if len(related_goal_ids) == 0: - return False - - goals = [] - all_done = True - for goal in goal_obj.browse(cr, uid, related_goal_ids, context=context): - if goal.end_date: - if goal.end_date < fields.date.today(): - # do not include goals of previous challenge run - continue - else: - all_done = False + 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: - if goal.state == 'inprogress' or goal.state == 'inprogress_update': - all_done = False - - goals.append(goal) + 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 - if all_done: - # skip challenges where all goal are done or failed - return False - else: - return goals + 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 ##### @@ -550,11 +599,11 @@ class gamification_challenge(osv.Model): temp_obj = self.pool.get('email.template') ctx = context.copy() if challenge.visibility_mode == 'ranking': - lines_boards = self.get_board_goal_info(cr, uid, challenge, subset_goal_ids, context) + 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({'lines_boards': lines_boards}) - template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context) - body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.challenge', challenge.id, context=context) + ctx.update({'challenge_lines': lines_boards}) + template_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context) + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) # body_html = template_env.get_template('group_progress.mako').render({'object': challenge, 'lines_boards': lines_boards, 'uid': uid}) @@ -572,14 +621,14 @@ class gamification_challenge(osv.Model): else: # generate individual reports for user in users or challenge.user_ids: - goals = self.get_indivual_goal_info(cr, uid, user.id, challenge, subset_goal_ids, context=context) + 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({'goals': goals}) + ctx.update({'challenge_lines': goals}) template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) - body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.challenge', challenge.id, context=context) - # send message only to users + body_html = temp_obj.render_template(cr, user.id, 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)], @@ -600,7 +649,7 @@ class gamification_challenge(osv.Model): 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, uid, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, 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): @@ -608,7 +657,7 @@ class gamification_challenge(osv.Model): 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, uid, challenge_ids, body=message, context=context) + 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): @@ -710,7 +759,7 @@ class gamification_challenge(osv.Model): for goal in goal_obj.browse(cr, uid, goal_ids, context=context): if goal.state != 'reached': all_reached = False - if goal.definition_condition == 'higher': + if goal.condition == 'higher': # can be over 100 total_completness += 100.0 * goal.current / goal.target_goal elif goal.state == 'reached': @@ -761,7 +810,7 @@ class gamification_challenge_line(osv.Model): goal_definition = goal_definition.browse(cr, uid, definition_id, context=context) ret = { 'value': { - 'definition_condition': goal_definition.condition, + 'condition': goal_definition.condition, 'definition_full_suffix': goal_definition.full_suffix } } @@ -781,7 +830,7 @@ class gamification_challenge_line(osv.Model): required=True), 'sequence': fields.integer('Sequence', help='Sequence number for ordering'), - 'definition_condition': fields.related('definition_id', 'condition', type="selection", + '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"), diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 5464b51a4ea..52eebb63857 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -76,6 +76,7 @@ class gamification_goal_definition(osv.Model): 'display_mode': fields.selection([ ('progress', 'Progressive (using numerical values)'), ('checkbox', 'Checkbox (done or not-done)'), + ('boolean', 'Exclusive (done or not-done)'), ], string="Displayed as", required=True), 'model_id': fields.many2one('ir.model', diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index d831ab2ea25..408ddddf082 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -20,7 +20,7 @@ ############################################################################## from openerp.osv import osv - +from challenge import MAX_VISIBILITY_RANKING class res_users_gamification_group(osv.Model): """ Update of res.users class @@ -58,14 +58,23 @@ class res_users_gamification_group(osv.Model): challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context) return write_res - def get_goals_todo_info(self, cr, uid, context=None): - """Return the list of goals assigned to the user, grouped by challenge + # def get_goals_todo_info(self, cr, uid, context=None): + + def get_serialised_gamification_summary(self, cr, uid, context=None): + return self._serialised_goals_summary(cr, uid, user_id=uid, context=context) - This method intends to return processable data in javascript in the - goal_list_to_do template. The output format is not constant as the - required information is different between individual and board goal - definitions - :return: list of dictionnaries for each goal to display + def _serialised_goals_summary(self, cr, uid, user_id, context=None): + """Return a serialised list of goals assigned to the user, grouped by challenge + + [ + { + 'id': , + 'name': , + 'visibility_mode': , + 'currency': , + 'lines': [(see gamification_challenge._get_serialized_challenge_lines() format)] + }, + ] """ all_goals_info = [] challenge_obj = self.pool.get('gamification.challenge') @@ -74,78 +83,14 @@ class res_users_gamification_group(osv.Model): challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context): # serialize goals info to be able to use it in javascript - serialized_goals_info = { + all_goals_info.append({ 'id': challenge.id, 'name': challenge.name, 'visibility_mode': challenge.visibility_mode, - 'currency': user.company_id.currency_id.id - } + 'currency': user.company_id.currency_id.id, + 'lines': challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context), + }) - if challenge.visibility_mode == 'ranking': - # board report should be grouped by line for all users - goals_info = challenge_obj.get_board_goal_info(cr, uid, challenge, subset_goal_ids=False, context=context) - - if not goals_info: - # challenge with no valid lines - continue - - serialized_goals_info['lines'] = [] - for line_board in goals_info: - vals = {'definition_name': line_board['goal_definition'].name, - 'definition_description': line_board['goal_definition'].description, - 'definition_condition': line_board['goal_definition'].condition, - 'computation_mode': line_board['goal_definition'].computation_mode, - 'definition_monetary': line_board['goal_definition'].monetary, - 'definition_suffix': line_board['goal_definition'].suffix, - 'definition_action': True if line_board['goal_definition'].action_id else False, - 'definition_display': line_board['goal_definition'].display_mode, - 'target_goal': line_board['target_goal'], - 'goals': []} - for goal in line_board['board_goals']: - # Keep only the Top 3 and the current user - if goal[0] > 2 and goal[1].user_id.id != uid: - continue - - vals['goals'].append({ - 'rank': goal[0] + 1, - 'id': goal[1].id, - 'user_id': goal[1].user_id.id, - 'user_name': goal[1].user_id.name, - 'state': goal[1].state, - 'completeness': goal[1].completeness, - 'current': goal[1].current, - 'target_goal': goal[1].target_goal, - }) - if uid == goal[1].user_id.id: - vals['own_goal_id'] = goal[1].id - serialized_goals_info['lines'].append(vals) - - else: - # individual report are simply a list of goal - goals_info = challenge_obj.get_indivual_goal_info(cr, uid, uid, challenge, subset_goal_ids=False, context=context) - - if not goals_info: - continue - - serialized_goals_info['goals'] = [] - for goal in goals_info: - serialized_goals_info['goals'].append({ - 'id': goal.id, - 'definition_name': goal.definition_id.name, - 'definition_description': goal.definition_description, - 'definition_condition': goal.definition_id.condition, - 'definition_monetary': goal.definition_id.monetary, - 'definition_suffix': goal.definition_id.suffix, - 'definition_action': True if goal.definition_id.action_id else False, - 'definition_display': goal.definition_id.display_mode, - 'state': goal.state, - 'completeness': goal.completeness, - 'computation_mode': goal.computation_mode, - 'current': goal.current, - 'target_goal': goal.target_goal, - }) - - all_goals_info.append(serialized_goals_info) return all_goals_info def get_challenge_suggestions(self, cr, uid, context=None): @@ -171,7 +116,9 @@ class res_groups_gamification_group(osv.Model): _name = 'res.groups' _inherit = 'res.groups' + # No need to overwrite create as very unlikely to be the value in the autojoin_group_id field def write(self, cr, uid, ids, vals, context=None): + """Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge""" write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context) if vals.get('users'): # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} -- cgit v1.2.3 From aec8f50b20bf330fbe3c997924e20f0dbff43e55 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 20 Dec 2013 16:05:57 +0100 Subject: [IMP] gamification: use constances bzr revid: mat@openerp.com-20131220150557-xaomxnrdb4bxcex3 --- addons/gamification/models/badge.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 99ad7f93edd..b1380627e39 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -29,6 +29,11 @@ import logging _logger = logging.getLogger(__name__) +CAN_GRANT = 1 +NOBODY_CAN_GRANT = 2 +USER_NOT_VIP = 3 +BADGE_REQUIRED = 4 +TOO_MANY = 5 class gamification_badge_user(osv.Model): """User having received a badge""" @@ -217,15 +222,15 @@ class gamification_badge(osv.Model): Do not check for SUPERUSER_ID """ status_code = self._can_grant_badge(cr, uid, badge_id, context=context) - if status_code == 1: + if status_code == CAN_GRANT: return True - elif status_code == 2: + elif status_code == NOBODY_CAN_GRANT: raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.')) - elif status_code == 3: + elif status_code == USER_NOT_VIP: raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.')) - elif status_code == 4: + elif status_code == BADGE_REQUIRED: raise osv.except_osv(_('Warning!'), _('You do not have the required badges.')) - elif status_code == 5: + elif status_code == TOO_MANY: raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.')) else: _logger.exception("Unknown badge status code: %d" % int(status_code)) @@ -237,31 +242,26 @@ class gamification_badge(osv.Model): :param uid: the id of the res.users trying to send the badge :param badge_id: the granted badge id :return: integer representing the permission. - 1: can grant - 2: nobody can send - 3: user not in the allowed list - 4: don't have the required badges - 5: user's monthly limit reached """ if uid == SUPERUSER_ID: - return 1 + return CAN_GRANT badge = self.browse(cr, uid, badge_id, context=context) if badge.rule_auth == 'nobody': - return 2 + return NOBODY_CAN_GRANT elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]: - return 3 + return USER_NOT_VIP elif badge.rule_auth == 'having': all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context) for required_badge in badge.rule_auth_badge_ids: if required_badge.id not in all_user_badges: - return 4 + return BADGE_REQUIRED if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number: - return 5 + return TOO_MANY # badge.rule_auth == 'everyone' -> no check - return 1 + return CAN_GRANT -- cgit v1.2.3 From c0eef311602c986e5ab54ebba016eb12d49e1467 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 23 Dec 2013 16:54:04 +0100 Subject: [REF] gamification: add user_from_id on badge, remove outdated-doc, remove useless message_post call bzr revid: mat@openerp.com-20131223155404-h4in4ocr8jakq3ff --- addons/gamification/models/badge.py | 21 ++++++++++++++++++--- addons/gamification/models/challenge.py | 5 ++--- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index b1380627e39..2b997384467 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -44,6 +44,7 @@ class gamification_badge_user(osv.Model): _columns = { 'user_id': fields.many2one('res.users', string="User", required=True), + 'user_from_id': fields.many2one('res.users', string="User From"), 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True), 'comment': fields.text('Comment'), 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"), @@ -52,14 +53,13 @@ class gamification_badge_user(osv.Model): } - def _send_badge(self, cr, uid, ids, user_from=False, context=None): + def _send_badge(self, cr, uid, ids, context=None): """Send a notification to a user for receiving a badge Does not verify constrains on badge granting. The users are added to the owner_ids (create badge_user if needed) The stats counters are incremented :param ids: list(int) of badge users that will receive the badge - :param user_from: optional id of the res.users object that has sent the badge """ res = True temp_obj = self.pool.get('email.template') @@ -68,7 +68,6 @@ class gamification_badge_user(osv.Model): ctx = context.copy() for badge_user in self.browse(cr, uid, ids, context=context): - ctx.update({'user_from': user_obj.browse(cr, uid, user_from).name}) body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context) @@ -180,6 +179,7 @@ class gamification_badge(osv.Model): 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id', string='Owners', help='The list of instances of this badge granted to users'), + 'active': fields.boolean('Active'), 'unique_owner_ids': fields.function(_get_owners_info, string='Unique Owners', help="The list of unique users having received this badge.", @@ -213,6 +213,7 @@ class gamification_badge(osv.Model): _defaults = { 'rule_auth': 'everyone', + 'active': True, } def check_granting(self, cr, uid, badge_id, context=None): @@ -265,3 +266,17 @@ class gamification_badge(osv.Model): # badge.rule_auth == 'everyone' -> no check return CAN_GRANT + + def check_progress(self, cr, uid, context=None): + try: + model, res_id = template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden') + except ValueError: + return True + badge_user_obj = self.pool.get('gamification.badge.user') + if not badge_user_obj.search(cr, uid, [('user_id', '=', uid), ('badge_id', '=', res_id)], context=context): + values = { + 'user_id': uid, + 'badge_id': res_id, + } + badge_user_obj.create(cr, SUPERUSER_ID, values, context=context) + return True \ No newline at end of file diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 7cbd784b9bf..83ec0fd5beb 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -626,7 +626,7 @@ class gamification_challenge(osv.Model): continue ctx.update({'challenge_lines': goals}) - template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) + template_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) body_html = temp_obj.render_template(cr, user.id, 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, @@ -642,7 +642,6 @@ class gamification_challenge(osv.Model): 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 @@ -788,7 +787,7 @@ class gamification_challenge(osv.Model): """ 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], user_from=None, context=context) + return badge_user_obj._send_badge(cr, uid, [user_badge_id], context=context) class gamification_challenge_line(osv.Model): -- cgit v1.2.3 From f4e42879d3430f36033d416c205a63e547aa4068 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 23 Dec 2013 17:01:43 +0100 Subject: [REF] gamification: rename user_from_id -> sender_id, move constants bzr revid: mat@openerp.com-20131223160143-8mgmwrgf6i3wo8ku --- addons/gamification/models/badge.py | 41 +++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 22 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 2b997384467..943e236382a 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -29,12 +29,6 @@ import logging _logger = logging.getLogger(__name__) -CAN_GRANT = 1 -NOBODY_CAN_GRANT = 2 -USER_NOT_VIP = 3 -BADGE_REQUIRED = 4 -TOO_MANY = 5 - class gamification_badge_user(osv.Model): """User having received a badge""" @@ -44,7 +38,7 @@ class gamification_badge_user(osv.Model): _columns = { 'user_id': fields.many2one('res.users', string="User", required=True), - 'user_from_id': fields.many2one('res.users', string="User From"), + 'sender_id': fields.many2one('res.users', string="Sender", help="The user who has send the badge"), 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True), 'comment': fields.text('Comment'), 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"), @@ -65,11 +59,8 @@ class gamification_badge_user(osv.Model): temp_obj = self.pool.get('email.template') user_obj = self.pool.get('res.users') template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) - ctx = context.copy() for badge_user in self.browse(cr, uid, ids, context=context): - - body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) - + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=context) res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context) return res @@ -81,6 +72,12 @@ class gamification_badge_user(osv.Model): class gamification_badge(osv.Model): """Badge object that users can send and receive""" + CAN_GRANT = 1 + NOBODY_CAN_GRANT = 2 + USER_NOT_VIP = 3 + BADGE_REQUIRED = 4 + TOO_MANY = 5 + _name = 'gamification.badge' _description = 'Gamification badge' _inherit = ['mail.thread'] @@ -223,15 +220,15 @@ class gamification_badge(osv.Model): Do not check for SUPERUSER_ID """ status_code = self._can_grant_badge(cr, uid, badge_id, context=context) - if status_code == CAN_GRANT: + if status_code == self.CAN_GRANT: return True - elif status_code == NOBODY_CAN_GRANT: + elif status_code == self.NOBODY_CAN_GRANT: raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.')) - elif status_code == USER_NOT_VIP: + elif status_code == self.USER_NOT_VIP: raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.')) - elif status_code == BADGE_REQUIRED: + elif status_code == self.BADGE_REQUIRED: raise osv.except_osv(_('Warning!'), _('You do not have the required badges.')) - elif status_code == TOO_MANY: + elif status_code == self.TOO_MANY: raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.')) else: _logger.exception("Unknown badge status code: %d" % int(status_code)) @@ -245,27 +242,27 @@ class gamification_badge(osv.Model): :return: integer representing the permission. """ if uid == SUPERUSER_ID: - return CAN_GRANT + return self.CAN_GRANT badge = self.browse(cr, uid, badge_id, context=context) if badge.rule_auth == 'nobody': - return NOBODY_CAN_GRANT + return self.NOBODY_CAN_GRANT elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]: - return USER_NOT_VIP + return self.USER_NOT_VIP elif badge.rule_auth == 'having': all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context) for required_badge in badge.rule_auth_badge_ids: if required_badge.id not in all_user_badges: - return BADGE_REQUIRED + return self.BADGE_REQUIRED if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number: - return TOO_MANY + return self.TOO_MANY # badge.rule_auth == 'everyone' -> no check - return CAN_GRANT + return self.CAN_GRANT def check_progress(self, cr, uid, context=None): try: -- cgit v1.2.3 From 7c6a6b26f181328b8d9a50dc09313a6e862252d0 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 23 Dec 2013 17:12:29 +0100 Subject: [FIX] gamification: goal.condition -> goal.definition_condition, user_ids not in vals -> not vals.get(user_ids) bzr revid: mat@openerp.com-20131223161229-lpoevttkt6ezt2ia --- addons/gamification/models/badge.py | 5 +---- addons/gamification/models/challenge.py | 6 +++--- addons/gamification/models/goal.py | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 943e236382a..1d879393ed2 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -90,10 +90,7 @@ class gamification_badge(osv.Model): """ result = dict.fromkeys(ids, False) for obj in self.browse(cr, uid, ids, context=context): - res = set() - for owner in obj.owner_ids: - res.add(owner.user_id.id) - res = list(res) + res = list(set(owner.user_id.id for owner in obj.owner_ids)) result[obj.id] = { 'unique_owner_ids': res, 'stat_count': len(obj.owner_ids), diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 83ec0fd5beb..feeb0b3642e 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -206,7 +206,7 @@ class gamification_challenge(osv.Model): if vals.get('autojoin_group_id'): new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) - if 'user_ids' not in vals: + if not vals.get('user_ids'): vals['user_ids'] = [] vals['user_ids'] += [(4, user.id) for user in new_group.users] @@ -226,7 +226,7 @@ class gamification_challenge(osv.Model): if vals.get('autojoin_group_id'): new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) - if 'user_ids' not in vals: + if not vals.get('user_ids'): vals['user_ids'] = [] vals['user_ids'] += [(4, user.id) for user in new_group.users] @@ -758,7 +758,7 @@ class gamification_challenge(osv.Model): for goal in goal_obj.browse(cr, uid, goal_ids, context=context): if goal.state != 'reached': all_reached = False - if goal.condition == 'higher': + if goal.definition_condition == 'higher': # can be over 100 total_completness += 100.0 * goal.current / goal.target_goal elif goal.state == 'reached': diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 52eebb63857..7c665f41e68 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -268,7 +268,7 @@ class gamification_goal(osv.Model): towrite['current'] = new_value # check goal target reached - if (goal.definition_id.condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_id.condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal): + if (goal.definition_condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.definition_condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal): towrite['state'] = 'reached' # check goal failure -- cgit v1.2.3 From 6f5815f44de8afa6fd357dd79c7ec8cca121c357 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 23 Dec 2013 17:32:14 +0100 Subject: [REF] gamification: cahnge some tde remarks, Long live Cthulhu bzr revid: mat@openerp.com-20131223163214-f6yp2yxf5ja1wq22 --- addons/gamification/models/goal.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 7c665f41e68..698d20d0159 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -89,7 +89,7 @@ class gamification_goal_definition(osv.Model): string='Date Field', help='The date to use for the time period evaluated'), 'domain': fields.char("Filter Domain", - help="Technical filters rules to apply. Use 'user.id' (without marks) to limit the search to the evaluated user.", + help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].", required=True), 'compute_code': fields.char('Compute Code', help="The name of the python method that will be executed to compute the current value. See the file gamification/goal_definition_data.py for examples."), @@ -124,7 +124,7 @@ class gamification_goal(osv.Model): _description = 'Gamification goal instance' _inherit = 'mail.thread' - def _get_completeness(self, cr, uid, ids, field_name, arg, context=None): + def _get_completion(self, cr, uid, ids, field_name, arg, context=None): """Return the percentage of completeness of the goal, between 0 and 100""" res = dict.fromkeys(ids, 0.0) for goal in self.browse(cr, uid, ids, context=context): @@ -162,7 +162,7 @@ class gamification_goal(osv.Model): required=True, track_visibility='always'), # no goal = global index 'current': fields.float('Current Value', required=True, track_visibility='always'), - 'completeness': fields.function(_get_completeness, type='float', string='Completeness'), + 'completeness': fields.function(_get_completion, type='float', string='Completeness'), 'state': fields.selection([ ('draft', 'Draft'), ('inprogress', 'In progress'), @@ -346,7 +346,7 @@ class gamification_goal(osv.Model): goal = self.browse(cr, uid, goal_id, context=context) if goal.definition_id.action_id: - #open a the action linked on the goal + # open a the action linked to the goal action = goal.definition_id.action_id.read()[0] if goal.definition_id.res_id_field: @@ -354,25 +354,20 @@ class gamification_goal(osv.Model): action['res_id'] = safe_eval(goal.definition_id.res_id_field, {'user': current_user}) # if one element to display, should see it in form mode if possible - views = action['views'] - for (view_id, mode) in action['views']: - if mode == "form": - views = [(view_id, mode)] - break - action['views'] = views + action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views'] return action if goal.computation_mode == 'manually': - #open a wizard window to update the value manually + # open a wizard window to update the value manually action = { 'name': _("Update %s") % goal.definition_id.name, 'id': goal_id, 'type': 'ir.actions.act_window', 'views': [[False, 'form']], 'target': 'new', + 'context': {'default_goal_id': goal_id, 'default_current': goal.current}, + 'res_model': 'gamification.goal.wizard' } - action['context'] = {'default_goal_id': goal_id, 'default_current': goal.current} - action['res_model'] = 'gamification.goal.wizard' return action return False -- cgit v1.2.3 From aefd64e8cfe41db54588dac2c50cf2a50316cc7a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 24 Dec 2013 14:00:23 +0100 Subject: [REF] gamification: compute_code is now similar to ir.actions, cleanup code bzr revid: mat@openerp.com-20131224130023-e4gc91ell38zdtfj --- addons/gamification/models/goal.py | 49 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 698d20d0159..8792e219d71 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -25,9 +25,9 @@ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF from openerp.tools.safe_eval import safe_eval from openerp.tools.translate import _ -from datetime import date, datetime, timedelta - import logging +import time +from datetime import date, datetime, timedelta _logger = logging.getLogger(__name__) @@ -75,7 +75,6 @@ class gamification_goal_definition(osv.Model): required=True), 'display_mode': fields.selection([ ('progress', 'Progressive (using numerical values)'), - ('checkbox', 'Checkbox (done or not-done)'), ('boolean', 'Exclusive (done or not-done)'), ], string="Displayed as", required=True), @@ -91,8 +90,8 @@ class gamification_goal_definition(osv.Model): 'domain': fields.char("Filter Domain", help="Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)].", required=True), - 'compute_code': fields.char('Compute Code', - help="The name of the python method that will be executed to compute the current value. See the file gamification/goal_definition_data.py for examples."), + 'compute_code': fields.text('Python Code', + help="Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id."), 'condition': fields.selection([ ('higher', 'The higher the better'), ('lower', 'The lower the better') @@ -114,6 +113,15 @@ class gamification_goal_definition(osv.Model): 'display_mode': 'progress', } + def number_following(self, cr, uid, model_name="mail.thread", context=None): + """Return the number of 'model_name' objects the user is following + + The model specified in 'model_name' must inherit from mail.thread + """ + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + return self.pool.get('mail.followers').search(cr, uid, [('res_model', '=', model_name), ('partner_id', '=', user.partner_id.id)], count=True, context=context) + + class gamification_goal(osv.Model): """Goal instance for a user @@ -221,6 +229,8 @@ class gamification_goal(osv.Model): If a goal reaches the target value, the status is set to reached If the end date is passed (at least +1 day, time not considered) without the target value being reached, the goal is set as failed.""" + if context is None: + context = {} for goal in self.browse(cr, uid, ids, context=context): towrite = {} @@ -233,14 +243,25 @@ class gamification_goal(osv.Model): elif goal.definition_id.computation_mode == 'python': # execute the chosen method - values = {'cr': cr, 'uid': goal.user_id.id, 'context': context, 'self': self.pool.get('gamification.goal.definition')} - result = safe_eval(goal.definition_id.compute_code, values, {}) - - if type(result) in (float, int, long) and result != goal.current: - towrite['current'] = result + cxt = { + 'self': self.pool.get('gamification.goal'), + 'object': goal, + 'pool': self.pool, + 'cr': cr, + 'context': dict(context), # copy context to prevent side-effects of eval + 'uid': uid, + 'result': False, + 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time + } + code = goal.definition_id.compute_code.strip() + safe_eval(code, cxt, mode="exec", nocopy=True) + # the result of the evaluated codeis put in the 'result' local variable, propagated to the context + result = cxt.get('result', False) + if result and type(result) in (float, int, long): + if result != goal.current: + towrite['current'] = result else: - _logger.exception(_('Unvalid return content from the evaluation of %s' % str(goal.definition_id.compute_code))) - # raise osv.except_osv(_('Error!'), _('Unvalid return content from the evaluation of %s' % str(goal.definition_id.compute_code))) + _logger.exception(_('Invalid return content from the evaluation of %s' % code)) else: # count or sum obj = self.pool.get(goal.definition_id.model_id.model) @@ -249,7 +270,7 @@ class gamification_goal(osv.Model): # eval the domain with user replaced by goal user object domain = safe_eval(goal.definition_id.domain, {'user': goal.user_id}) - #add temporal clause(s) to the domain if fields are filled on the goal + # add temporal clause(s) to the domain if fields are filled on the goal if goal.start_date and field_date_name: domain.append((field_date_name, '>=', goal.start_date)) if goal.end_date and field_date_name: @@ -263,7 +284,7 @@ class gamification_goal(osv.Model): else: # computation mode = count new_value = obj.search(cr, uid, domain, context=context, count=True) - #avoid useless write if the new value is the same as the old one + # avoid useless write if the new value is the same as the old one if new_value != goal.current: towrite['current'] = new_value -- cgit v1.2.3 From 25117ca97f0bc61b27a407f48b97136151fe8303 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 24 Dec 2013 15:11:40 +0100 Subject: [REF] gamification: use statusbar instead of action buttons bzr revid: mat@openerp.com-20131224141140-vc0v5lpm0ivfl6i4 --- addons/gamification/models/challenge.py | 78 ++++++++++++--------------------- 1 file changed, 29 insertions(+), 49 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index feeb0b3642e..50b949ff03b 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -221,7 +221,9 @@ class gamification_challenge(osv.Model): return create_res def write(self, cr, uid, ids, vals, context=None): - """Overwrite the write method to add the user of groups""" + 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) @@ -230,6 +232,29 @@ class gamification_challenge(osv.Model): vals['user_ids'] = [] vals['user_ids'] += [(4, user.id) for user in new_group.users] + print vals.get('state') + if vals.get('state') == 'inprogress': + + 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] + if challenge.state == 'draft': + # going from draft to inprogress + self.message_post(cr, uid, challenge.id, body="New challenge started.", context=context) + 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 @@ -272,6 +297,9 @@ class gamification_challenge(osv.Model): :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 @@ -319,61 +347,13 @@ class gamification_challenge(osv.Model): return True - ##### User actions ##### - - def action_start(self, cr, uid, ids, context=None): - """Start a draft challenge - - Change the state of the challenge to in progress and generate related goals - """ - if isinstance(ids, (int,long)): - ids = [ids] - # subscribe users if autojoin group - for challenge in self.browse(cr, uid, ids, context=context): - if challenge.autojoin_group_id: - self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context) - - self.write(cr, uid, challenge.id, {'state': 'inprogress'}, context=context) - self.message_post(cr, uid, challenge.id, body="New challenge started.", context=context) - return self.generate_goals_from_challenge(cr, uid, ids, context=context) - 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""" - if isinstance(ids, (int,long)): - ids = [ids] return self._update_all(cr, uid, ids=ids, context=context) - def action_close(self, cr, uid, ids, context=None): - """Close a challenge in progress - - Change the state of the challenge to in done - Does NOT close the related goals, this is handled by the goal itself""" - self.check_challenge_reward(cr, uid, ids, force=True, context=context) - return self.write(cr, uid, ids, {'state': 'done'}, context=context) - - def action_reset(self, cr, uid, ids, context=None): - """Reset a closed challenge - - Change the state of the challenge to in progress - Closing a challenge does not affect the goals so neither does reset""" - return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) - - def action_cancel(self, cr, uid, ids, context=None): - """Cancel a challenge in progress - - Change the state of the challenge to draft - Cancel the related goals""" - if isinstance(ids, (int,long)): - ids = [ids] - self.write(cr, uid, ids, {'state': 'draft'}, context=context) - goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('challenge_id', 'in', ids)], context=context) - self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context) - - return True - 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)): -- cgit v1.2.3 From be7bc54edc2fcc0abb94450d22f37ae4d4b11dce Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 24 Dec 2013 15:45:14 +0100 Subject: [REF] gamification: use m2o to email.template for report progress, create new field report_template_id, cleaning code bzr revid: mat@openerp.com-20131224144514-lmbhw88nj7aa3r8i --- addons/gamification/models/challenge.py | 37 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 50b949ff03b..12f1b62c6fb 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -102,11 +102,18 @@ class gamification_challenge(osv.Model): res[challenge.id] = False return res + def _get_categories(self, cr, uid, context=None): - return [ - ('hr', 'Human Ressources / Engagement'), - ('other', 'Settings / Gamification Tools'), - ] + 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 = { @@ -163,6 +170,7 @@ class gamification_challenge(osv.Model): ('ranking', 'Leader Board (Group Ranking)'), ], string="Display Mode", required=True), + 'report_message_frequency': fields.selection([ ('never', 'Never'), ('onchange', 'On change'), @@ -175,7 +183,7 @@ class gamification_challenge(osv.Model): '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_header': fields.text('Report Header'), + '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'), @@ -196,6 +204,7 @@ class gamification_challenge(osv.Model): '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), } @@ -232,7 +241,6 @@ class gamification_challenge(osv.Model): vals['user_ids'] = [] vals['user_ids'] += [(4, user.id) for user in new_group.users] - print vals.get('state') if vals.get('state') == 'inprogress': if not vals.get('autojoin_group_id'): @@ -575,17 +583,14 @@ class gamification_challenge(osv.Model): """ if context is None: context = {} - # template_env = TemplateHelper() + 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}) - template_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context) - body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) - - # body_html = template_env.get_template('group_progress.mako').render({'object': challenge, 'lines_boards': lines_boards, 'uid': uid}) + 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, @@ -606,8 +611,8 @@ class gamification_challenge(osv.Model): continue ctx.update({'challenge_lines': goals}) - template_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) - body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.challenge', challenge.id, context=ctx) + 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, @@ -640,11 +645,9 @@ class gamification_challenge(osv.Model): 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): - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard') + result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard') id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] + result = self.pool.get('ir.actions.act_window').read(cr, uid, [id], context=context)[0] result['res_id'] = challenge_id return result -- cgit v1.2.3 From ff83d9eb4df60ff0fce9ae540bb88ef29a9f9676 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 24 Dec 2013 15:54:49 +0100 Subject: [FIX] gamification: remove import data, add trackvisibility on challenge instead of sending message bzr revid: mat@openerp.com-20131224145449-93b3z97lagu12yri --- addons/gamification/models/challenge.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 12f1b62c6fb..5bbee5bbede 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -124,8 +124,7 @@ class gamification_challenge(osv.Model): ('inprogress', 'In Progress'), ('done', 'Done'), ], - string='State', - required=True), + string='State', required=True, track_visibility='onchange'), 'manager_id': fields.many2one('res.users', string='Responsible', help="The user responsible for the challenge."), @@ -250,9 +249,7 @@ class gamification_challenge(osv.Model): 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] - if challenge.state == 'draft': - # going from draft to inprogress - self.message_post(cr, uid, challenge.id, body="New challenge started.", context=context) + self.generate_goals_from_challenge(cr, uid, ids, context=context) elif vals.get('state') == 'done': -- cgit v1.2.3 From 13bfaef3a3625deb4ba1d05c50647b6126175c6f Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 24 Dec 2013 16:15:43 +0100 Subject: [FIX] gamification: remove old calls to action_start bzr revid: mat@openerp.com-20131224151543-oofn9wc2w3933gjy --- addons/gamification/models/challenge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'addons/gamification/models') diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 5bbee5bbede..ae27aa96719 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -241,7 +241,7 @@ class gamification_challenge(osv.Model): 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'): @@ -284,13 +284,13 @@ class gamification_challenge(osv.Model): planned_challenge_ids = self.search(cr, uid, [ ('state', '=', 'draft'), ('start_date', '<=', fields.date.today())]) - self.action_start(cr, uid, planned_challenge_ids, context=context) + 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.action_close(cr, uid, planned_challenge_ids, context=context) + self.write(cr, uid, planned_challenge_ids, {'state': 'done'}, context=context) if not ids: ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context) -- cgit v1.2.3