Merge lp:~openerp-dev/openobject-addons/trunk-gamification-mat into lp:openobject-addons
- trunk-gamification-mat
- Merge into trunk
Proposed by
Martin Trigaux (OpenERP)
Status: | Needs review |
---|---|
Proposed branch: | lp:~openerp-dev/openobject-addons/trunk-gamification-mat |
Merge into: | lp:openobject-addons |
Diff against target: |
6638 lines (+6351/-2) 44 files modified
gamification/__init__.py (+5/-0) gamification/__openerp__.py (+62/-0) gamification/badge.py (+299/-0) gamification/badge_data.xml (+906/-0) gamification/badge_view.xml (+219/-0) gamification/cron.xml (+16/-0) gamification/doc/gamification_plan_howto.rst (+107/-0) gamification/doc/goal.rst (+46/-0) gamification/doc/index.rst (+17/-0) gamification/goal.py (+404/-0) gamification/goal_base_data.xml (+232/-0) gamification/goal_type_data.py (+41/-0) gamification/goal_view.xml (+310/-0) gamification/html/index.html (+86/-0) gamification/plan.py (+804/-0) gamification/plan_view.xml (+291/-0) gamification/res_users.py (+177/-0) gamification/security/gamification_security.xml (+31/-0) gamification/security/ir.model.access.csv (+20/-0) gamification/static/lib/justgage/justgage.js (+946/-0) gamification/static/src/css/gamification.css (+216/-0) gamification/static/src/js/gamification.js (+174/-0) gamification/static/src/xml/gamification.xml (+112/-0) gamification/templates.py (+42/-0) gamification/templates/badge_received.mako (+17/-0) gamification/templates/base.mako (+13/-0) gamification/templates/group_progress.mako (+38/-0) gamification/templates/personal_progress.mako (+31/-0) gamification/templates/reminder.mako (+17/-0) gamification/test/goal_demo.yml (+53/-0) gamification_sale_crm/__openerp__.py (+32/-0) gamification_sale_crm/sale_crm_goals.xml (+174/-0) gamification_sale_crm/sale_crm_goals_demo.xml (+23/-0) hr/res_config.py (+2/-0) hr/res_config_view.xml (+4/-0) hr/static/src/css/hr.css (+1/-1) hr_evaluation/hr_evaluation.py (+4/-0) hr_evaluation/security/ir.model.access.csv (+1/-1) hr_gamification/__init__.py (+1/-0) hr_gamification/__openerp__.py (+38/-0) hr_gamification/gamification.py (+169/-0) hr_gamification/gamification_view.xml (+146/-0) hr_gamification/security/ir.model.access.csv (+5/-0) hr_gamification/static/src/js/gamification.js (+19/-0) |
To merge this branch: | bzr merge lp:~openerp-dev/openobject-addons/trunk-gamification-mat |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenERP Core Team | Pending | ||
Review via email: mp+170759@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
- 8781. By Martin Trigaux (OpenERP)
-
[FIX] gamification: send badge as superuser
- 8782. By Josse Colpaert (OpenERP)
-
[MERGE] Merge from trunk
Unmerged revisions
- 8782. By Josse Colpaert (OpenERP)
-
[MERGE] Merge from trunk
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'gamification' |
2 | === added file 'gamification/__init__.py' |
3 | --- gamification/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ gamification/__init__.py 2013-06-28 14:51:48 +0000 |
5 | @@ -0,0 +1,5 @@ |
6 | +import goal |
7 | +import goal_type_data |
8 | +import plan |
9 | +import res_users |
10 | +import badge |
11 | |
12 | === added file 'gamification/__openerp__.py' |
13 | --- gamification/__openerp__.py 1970-01-01 00:00:00 +0000 |
14 | +++ gamification/__openerp__.py 2013-06-28 14:51:48 +0000 |
15 | @@ -0,0 +1,62 @@ |
16 | +# -*- coding: utf-8 -*- |
17 | +############################################################################## |
18 | +# |
19 | +# OpenERP, Open Source Management Solution |
20 | +# Copyright (C) 2004-2013 Tiny SPRL (<http://openerp.com>). |
21 | +# |
22 | +# This program is free software: you can redistribute it and/or modify |
23 | +# it under the terms of the GNU Affero General Public License as |
24 | +# published by the Free Software Foundation, either version 3 of the |
25 | +# License, or (at your option) any later version. |
26 | +# |
27 | +# This program is distributed in the hope that it will be useful, |
28 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
29 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
30 | +# GNU Affero General Public License for more details. |
31 | +# |
32 | +# You should have received a copy of the GNU Affero General Public License |
33 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
34 | +# |
35 | +############################################################################## |
36 | +{ |
37 | + 'name': 'Gamification', |
38 | + 'version': '1.0', |
39 | + 'author': 'OpenERP SA', |
40 | + 'category': 'Human Ressources', |
41 | + 'depends': ['mail', 'email_template'], |
42 | + 'description': """ |
43 | +Gamification process |
44 | +==================== |
45 | +The Gamification module provides ways to evaluate and motivate the users of OpenERP. |
46 | + |
47 | +The users can be evaluated using goals and numerical objectives to reach. |
48 | +**Goals** are assigned through **plans** to evaluate and compare members of a team with each others and through time. |
49 | + |
50 | +For non-numerical achievements, **badges** can be granted to users. From a simple "thank you" to an exceptional achievement, a badge is an easy way to exprimate gratitude to a user for their good work. |
51 | + |
52 | +Both goals and badges are flexibles and can be adapted to a large range of modules and actions. When installed, this module creates easy goals to help new users to discover OpenERP and configure their user profile. |
53 | +""", |
54 | + |
55 | + 'data': [ |
56 | + 'plan_view.xml', |
57 | + 'badge_view.xml', |
58 | + 'goal_view.xml', |
59 | + 'cron.xml', |
60 | + 'security/gamification_security.xml', |
61 | + 'security/ir.model.access.csv', |
62 | + 'goal_base_data.xml', |
63 | + 'badge_data.xml', |
64 | + ], |
65 | + 'test': [ |
66 | + 'test/goal_demo.yml' |
67 | + ], |
68 | + 'installable': True, |
69 | + 'application': True, |
70 | + 'auto_install': True, |
71 | + 'css': ['static/src/css/gamification.css'], |
72 | + 'js': [ |
73 | + 'static/src/js/gamification.js', |
74 | + 'static/lib/justgage/justgage.js', |
75 | + ], |
76 | + 'qweb': ['static/src/xml/gamification.xml'], |
77 | +} |
78 | |
79 | === added file 'gamification/badge.py' |
80 | --- gamification/badge.py 1970-01-01 00:00:00 +0000 |
81 | +++ gamification/badge.py 2013-06-28 14:51:48 +0000 |
82 | @@ -0,0 +1,299 @@ |
83 | +# -*- coding: utf-8 -*- |
84 | +############################################################################## |
85 | +# |
86 | +# OpenERP, Open Source Management Solution |
87 | +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) |
88 | +# |
89 | +# This program is free software: you can redistribute it and/or modify |
90 | +# it under the terms of the GNU General Public License as published by |
91 | +# the Free Software Foundation, either version 3 of the License, or |
92 | +# (at your option) any later version. |
93 | +# |
94 | +# This program is distributed in the hope that it will be useful, |
95 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
96 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
97 | +# GNU General Public License for more details. |
98 | +# |
99 | +# You should have received a copy of the GNU General Public License |
100 | +# along with this program. If not, see <http://www.gnu.org/licenses/> |
101 | +# |
102 | +############################################################################## |
103 | + |
104 | +from openerp import SUPERUSER_ID |
105 | +from openerp.osv import fields, osv |
106 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF |
107 | +from openerp.tools.translate import _ |
108 | + |
109 | +# from templates import TemplateHelper |
110 | +from datetime import date |
111 | +import logging |
112 | + |
113 | +_logger = logging.getLogger(__name__) |
114 | + |
115 | + |
116 | +class gamification_badge_user(osv.Model): |
117 | + """User having received a badge""" |
118 | + |
119 | + _name = 'gamification.badge.user' |
120 | + _description = 'Gamification user badge' |
121 | + _order = "create_date desc" |
122 | + |
123 | + _columns = { |
124 | + 'user_id': fields.many2one('res.users', string="User", required=True), |
125 | + 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True), |
126 | + 'comment': fields.text('Comment'), |
127 | + 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"), |
128 | + 'create_date': fields.datetime('Created', readonly=True), |
129 | + 'create_uid': fields.many2one('res.users', 'Creator', readonly=True), |
130 | + } |
131 | + |
132 | + |
133 | +class gamification_badge(osv.Model): |
134 | + """Badge object that users can send and receive""" |
135 | + |
136 | + _name = 'gamification.badge' |
137 | + _description = 'Gamification badge' |
138 | + _inherit = ['mail.thread'] |
139 | + |
140 | + def _get_owners_info(self, cr, uid, ids, name, args, context=None): |
141 | + """Return: |
142 | + the list of unique res.users ids having received this badge |
143 | + the total number of time this badge was granted |
144 | + the total number of users this badge was granted to |
145 | + """ |
146 | + result = dict.fromkeys(ids, False) |
147 | + for obj in self.browse(cr, uid, ids, context=context): |
148 | + res = set() |
149 | + for owner in obj.owner_ids: |
150 | + res.add(owner.user_id.id) |
151 | + res = list(res) |
152 | + result[obj.id] = { |
153 | + 'unique_owner_ids': res, |
154 | + 'stat_count': len(obj.owner_ids), |
155 | + 'stat_count_distinct': len(res) |
156 | + } |
157 | + return result |
158 | + |
159 | + def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None): |
160 | + """Return stats related to badge users""" |
161 | + result = dict.fromkeys(ids, False) |
162 | + badge_user_obj = self.pool.get('gamification.badge.user') |
163 | + first_month_day = date.today().replace(day=1).strftime(DF) |
164 | + for obj in self.browse(cr, uid, ids, context=context): |
165 | + result[obj.id] = { |
166 | + 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid)], context=context, count=True), |
167 | + 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_date', '>=', first_month_day)], context=context, count=True), |
168 | + '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), |
169 | + '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) |
170 | + } |
171 | + return result |
172 | + |
173 | + def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None): |
174 | + """Computes the number of badges remaining the user can send |
175 | + |
176 | + 0 if not allowed or no remaining |
177 | + integer if limited sending |
178 | + -1 if infinite (should not be displayed) |
179 | + """ |
180 | + result = dict.fromkeys(ids, False) |
181 | + for badge in self.browse(cr, uid, ids, context=context): |
182 | + if self._can_grant_badge(cr, uid, uid, badge.id, context) != 1: |
183 | + #if the user cannot grant this badge at all, result is 0 |
184 | + result[badge.id] = 0 |
185 | + elif not badge.rule_max: |
186 | + #if there is no limitation, -1 is returned which mean 'infinite' |
187 | + result[badge.id] = -1 |
188 | + else: |
189 | + result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending |
190 | + return result |
191 | + |
192 | + _columns = { |
193 | + 'name': fields.char('Badge', required=True, translate=True), |
194 | + 'description': fields.text('Description'), |
195 | + 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"), |
196 | + # image_select: selection with a on_change to fill image with predefined picts |
197 | + 'rule_auth': fields.selection([ |
198 | + ('everyone', 'Everyone'), |
199 | + ('users', 'A selected list of users'), |
200 | + ('having', 'People having some badges'), |
201 | + ('nobody', 'No one, assigned through challenges'), |
202 | + ], |
203 | + string="Allowance to Grant", |
204 | + help="Who can grant this badge", |
205 | + required=True), |
206 | + 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users', |
207 | + string='Authorized Users', |
208 | + help="Only these people can give this badge"), |
209 | + 'rule_auth_badge_ids': fields.many2many('gamification.badge', |
210 | + 'rel_badge_badge', 'badge1_id', 'badge2_id', |
211 | + string='Required Badges', |
212 | + help="Only the people having these badges can give this badge"), |
213 | + |
214 | + 'rule_max': fields.boolean('Monthly Limited Sending', |
215 | + help="Check to set a monthly limit per person of sending this badge"), |
216 | + 'rule_max_number': fields.integer('Limitation Number', |
217 | + help="The maximum number of time this badge can be sent per month per person."), |
218 | + 'stat_my_monthly_sending': fields.function(_get_badge_user_stats, |
219 | + type="integer", |
220 | + string='My Monthly Sending Total', |
221 | + multi='badge_users', |
222 | + help="The number of time the current user has sent this badge this month."), |
223 | + 'remaining_sending': fields.function(_remaining_sending_calc, type='integer', |
224 | + string='Remaining Sending Allowed', help="If a maxium is set"), |
225 | + |
226 | + 'plan_ids': fields.one2many('gamification.goal.plan', 'reward_id', |
227 | + string="Reward of Challenges"), |
228 | + |
229 | + 'goal_type_ids': fields.many2many('gamification.goal.type', |
230 | + string='Goals Linked', |
231 | + help="The users that have succeeded theses goals will receive automatically the badge."), |
232 | + |
233 | + 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id', |
234 | + string='Owners', help='The list of instances of this badge granted to users'), |
235 | + 'unique_owner_ids': fields.function(_get_owners_info, |
236 | + string='Unique Owners', |
237 | + help="The list of unique users having received this badge.", |
238 | + multi='unique_users', |
239 | + type="many2many", relation="res.users"), |
240 | + |
241 | + 'stat_count': fields.function(_get_owners_info, string='Total', |
242 | + type="integer", |
243 | + multi='unique_users', |
244 | + help="The number of time this badge has been received."), |
245 | + 'stat_count_distinct': fields.function(_get_owners_info, |
246 | + type="integer", |
247 | + string='Number of users', |
248 | + multi='unique_users', |
249 | + help="The number of time this badge has been received by unique users."), |
250 | + 'stat_this_month': fields.function(_get_badge_user_stats, |
251 | + type="integer", |
252 | + string='Monthly total', |
253 | + multi='badge_users', |
254 | + help="The number of time this badge has been received this month."), |
255 | + 'stat_my': fields.function(_get_badge_user_stats, string='My Total', |
256 | + type="integer", |
257 | + multi='badge_users', |
258 | + help="The number of time the current user has received this badge."), |
259 | + 'stat_my_this_month': fields.function(_get_badge_user_stats, |
260 | + type="integer", |
261 | + string='My Monthly Total', |
262 | + multi='badge_users', |
263 | + help="The number of time the current user has received this badge this month."), |
264 | + } |
265 | + |
266 | + _defaults = { |
267 | + 'rule_auth': 'everyone', |
268 | + } |
269 | + |
270 | + def send_badge(self, cr, uid, badge_id, badge_user_ids, user_from=False, context=None): |
271 | + """Send a notification to a user for receiving a badge |
272 | + |
273 | + Does NOT verify constrains on badge granting. |
274 | + The users are added to the owner_ids (create badge_user if needed) |
275 | + The stats counters are incremented |
276 | + :param badge_id: id of the badge to deserve |
277 | + :param badge_user_ids: list(int) of badge users that will receive the badge |
278 | + :param user_from: optional id of the res.users object that has sent the badge |
279 | + """ |
280 | + badge = self.browse(cr, uid, badge_id, context=context) |
281 | + # template_env = TemplateHelper() |
282 | + |
283 | + res = None |
284 | + temp_obj = self.pool.get('email.template') |
285 | + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) |
286 | + ctx = context.copy() |
287 | + for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context=context): |
288 | + |
289 | + ctx.update({'user_from': self.pool.get('res.users').browse(cr, uid, user_from).name}) |
290 | + |
291 | + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx) |
292 | + |
293 | + # as SUPERUSER as normal user don't have write access on a badge |
294 | + 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) |
295 | + return res |
296 | + |
297 | + def check_granting(self, cr, uid, user_from_id, badge_id, context=None): |
298 | + """Check the user 'user_from_id' can grant the badge 'badge_id' and raise the appropriate exception |
299 | + if not""" |
300 | + status_code = self._can_grant_badge(cr, uid, user_from_id, badge_id, context=context) |
301 | + if status_code == 1: |
302 | + return True |
303 | + elif status_code == 2: |
304 | + raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.')) |
305 | + elif status_code == 3: |
306 | + raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.')) |
307 | + elif status_code == 4: |
308 | + raise osv.except_osv(_('Warning!'), _('You do not have the required badges.')) |
309 | + elif status_code == 5: |
310 | + raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.')) |
311 | + else: |
312 | + _logger.exception("Unknown badge status code: %d" % int(status_code)) |
313 | + return False |
314 | + |
315 | + def _can_grant_badge(self, cr, uid, user_from_id, badge_id, context=None): |
316 | + """Check if a user can grant a badge to another user |
317 | + |
318 | + :param user_from_id: the id of the res.users trying to send the badge |
319 | + :param badge_id: the granted badge id |
320 | + :return: integer representing the permission. |
321 | + 1: can grant |
322 | + 2: nobody can send |
323 | + 3: user not in the allowed list |
324 | + 4: don't have the required badges |
325 | + 5: user's monthly limit reached |
326 | + """ |
327 | + badge = self.browse(cr, uid, badge_id, context=context) |
328 | + |
329 | + if badge.rule_auth == 'nobody': |
330 | + return 2 |
331 | + |
332 | + elif badge.rule_auth == 'users' and user_from_id not in [user.id for user in badge.rule_auth_user_ids]: |
333 | + return 3 |
334 | + |
335 | + elif badge.rule_auth == 'having': |
336 | + all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', user_from_id)], context=context) |
337 | + for required_badge in badge.rule_auth_badge_ids: |
338 | + if required_badge.id not in all_user_badges: |
339 | + return 4 |
340 | + |
341 | + if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number: |
342 | + return 5 |
343 | + |
344 | + # badge.rule_auth == 'everyone' -> no check |
345 | + return 1 |
346 | + |
347 | + |
348 | +class grant_badge_wizard(osv.TransientModel): |
349 | + """ Wizard allowing to grant a badge to a user""" |
350 | + |
351 | + _name = 'gamification.badge.user.wizard' |
352 | + _columns = { |
353 | + 'user_id': fields.many2one("res.users", string='User', required=True), |
354 | + 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True), |
355 | + 'comment': fields.text('Comment'), |
356 | + } |
357 | + |
358 | + def action_grant_badge(self, cr, uid, ids, context=None): |
359 | + """Wizard action for sending a badge to a chosen user""" |
360 | + |
361 | + badge_obj = self.pool.get('gamification.badge') |
362 | + badge_user_obj = self.pool.get('gamification.badge.user') |
363 | + |
364 | + for wiz in self.browse(cr, uid, ids, context=context): |
365 | + if uid == wiz.user_id.id: |
366 | + raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself')) |
367 | + |
368 | + #check if the badge granting is legitimate |
369 | + if badge_obj.check_granting(cr, uid, user_from_id=uid, badge_id=wiz.badge_id.id, context=context): |
370 | + #create the badge |
371 | + values = { |
372 | + 'user_id': wiz.user_id.id, |
373 | + 'badge_id': wiz.badge_id.id, |
374 | + 'comment': wiz.comment, |
375 | + } |
376 | + badge_user = badge_user_obj.create(cr, uid, values, context=context) |
377 | + #notify the user |
378 | + result = badge_obj.send_badge(cr, uid, wiz.badge_id.id, [badge_user], user_from=uid, context=context) |
379 | + |
380 | + return result |
381 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
382 | |
383 | === added file 'gamification/badge_data.xml' |
384 | --- gamification/badge_data.xml 1970-01-01 00:00:00 +0000 |
385 | +++ gamification/badge_data.xml 2013-06-28 14:51:48 +0000 |
386 | @@ -0,0 +1,906 @@ |
387 | +<?xml version="1.0" encoding="utf-8"?> |
388 | +<openerp> |
389 | + <data noupdate="1"> |
390 | + <record id="badge_good_job" model="gamification.badge"> |
391 | + <field name="name">Good Job</field> |
392 | + <field name="description">You did great at your job.</field> |
393 | + <field name="rule_auth">everyone</field> |
394 | + <field name="image">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 |
604 | + RU5ErkJggg==</field> |
605 | + </record> |
606 | + |
607 | + <record id="badge_problem_solver" model="gamification.badge"> |
608 | + <field name="name">Problem Solver</field> |
609 | + <field name="description">No one can solve challenges like you do.</field> |
610 | + <field name="rule_auth">everyone</field> |
611 | + <field name="image">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</field> |
1003 | + </record> |
1004 | + |
1005 | + <record id="badge_idea" model="gamification.badge"> |
1006 | + <field name="name">Brilliant</field> |
1007 | + <field name="description">With your brilliant ideas, you are an inspiration to others.</field> |
1008 | + <field name="rule_auth">everyone</field> |
1009 | + <field name="rule_max">True</field> |
1010 | + <field name="rule_max_number">2</field> |
1011 | + <field name="image">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 |
1270 | + AABJRU5ErkJggg==</field> |
1271 | + </record> |
1272 | + </data> |
1273 | + |
1274 | + |
1275 | + <data noupdate="0"> |
1276 | + |
1277 | + <record id="email_template_badge_received" model="email.template"> |
1278 | + <field name="name">Received Badge</field> |
1279 | + <field name="body_html"><![CDATA[ |
1280 | + <p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> ! |
1281 | + % if ctx["user_from"] |
1282 | + This badge was granted by <strong>${ctx["user_from"]}</strong>. |
1283 | + % endif |
1284 | + </p> |
1285 | + |
1286 | + % if object.comment |
1287 | + <p><em>${object.comment}</em></p> |
1288 | + % endif |
1289 | + ]]></field> |
1290 | + </record> |
1291 | + </data> |
1292 | +</openerp> |
1293 | |
1294 | === added file 'gamification/badge_view.xml' |
1295 | --- gamification/badge_view.xml 1970-01-01 00:00:00 +0000 |
1296 | +++ gamification/badge_view.xml 2013-06-28 14:51:48 +0000 |
1297 | @@ -0,0 +1,219 @@ |
1298 | +<?xml version="1.0" encoding="UTF-8"?> |
1299 | +<openerp> |
1300 | + <data> |
1301 | + |
1302 | + <!-- Badge views --> |
1303 | + <record id="badge_list_action" model="ir.actions.act_window"> |
1304 | + <field name="name">Badges</field> |
1305 | + <field name="res_model">gamification.badge</field> |
1306 | + <field name="view_mode">kanban,tree,form</field> |
1307 | + <field name="help" type="html"> |
1308 | + <p class="oe_view_nocontent_create"> |
1309 | + Click to create a badge. |
1310 | + </p> |
1311 | + <p> |
1312 | + A badge is a symbolic token granted to a user as a sign of reward. |
1313 | + It can be deserved automatically when some conditions are met or manually by users. |
1314 | + Some badges are harder than others to get with specific conditions. |
1315 | + </p> |
1316 | + </field> |
1317 | + </record> |
1318 | + |
1319 | + |
1320 | + <record id="view_badge_wizard_grant" model="ir.ui.view"> |
1321 | + <field name="name">Grant Badge User Form</field> |
1322 | + <field name="model">gamification.badge.user.wizard</field> |
1323 | + <field name="arch" type="xml"> |
1324 | + <form string="Grant Badge To" version="7.0"> |
1325 | + Who would you like to reward? |
1326 | + <group> |
1327 | + <field name="user_id" nolabel="1" /> |
1328 | + <field name="badge_id" invisible="1"/> |
1329 | + <field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" class="oe_no_padding" /> |
1330 | + </group> |
1331 | + <footer> |
1332 | + <button string="Grant Badge" type="object" name="action_grant_badge" class="oe_highlight" /> or |
1333 | + <button string="Cancel" special="cancel" class="oe_link"/> |
1334 | + </footer> |
1335 | + </form> |
1336 | + </field> |
1337 | + </record> |
1338 | + |
1339 | + <act_window domain="[]" id="action_grant_wizard" |
1340 | + name="Grant Badge" |
1341 | + target="new" |
1342 | + res_model="gamification.badge.user.wizard" |
1343 | + context="{'default_badge_id': active_id, 'badge_id': active_id}" |
1344 | + view_type="form" view_mode="form" |
1345 | + view_id="gamification.view_badge_wizard_grant" /> |
1346 | + |
1347 | + <record id="badge_list_view" model="ir.ui.view"> |
1348 | + <field name="name">Badge List</field> |
1349 | + <field name="model">gamification.badge</field> |
1350 | + <field name="arch" type="xml"> |
1351 | + <tree string="Badge List"> |
1352 | + <field name="name"/> |
1353 | + <field name="stat_count"/> |
1354 | + <field name="stat_this_month"/> |
1355 | + <field name="stat_my"/> |
1356 | + <field name="rule_auth"/> |
1357 | + </tree> |
1358 | + </field> |
1359 | + </record> |
1360 | + |
1361 | + <record id="badge_form_view" model="ir.ui.view"> |
1362 | + <field name="name">Badge Form</field> |
1363 | + <field name="model">gamification.badge</field> |
1364 | + <field name="arch" type="xml"> |
1365 | + <form string="Badge" version="7.0"> |
1366 | + <header> |
1367 | + <button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" /> |
1368 | + <button string="Check Badge" type="object" name="check_automatic" groups="base.group_no_one" /> |
1369 | + </header> |
1370 | + <sheet> |
1371 | + <div class="oe_right oe_button_box"> |
1372 | + </div> |
1373 | + <field name="image" widget='image' class="oe_left oe_avatar"/> |
1374 | + <div class="oe_title"> |
1375 | + <label for="name" class="oe_edit_only"/> |
1376 | + <h1> |
1377 | + <field name="name"/> |
1378 | + </h1> |
1379 | + </div> |
1380 | + <group> |
1381 | + <field name="description" nolabel="1" placeholder="Badge Description" /> |
1382 | + </group> |
1383 | + <group string="Granting"> |
1384 | + <field name="rule_auth" widget="radio" /> |
1385 | + <field name="rule_auth_user_ids" attrs="{'invisible': [('rule_auth','!=','users')]}" widget="many2many_tags" /> |
1386 | + <field name="rule_auth_badge_ids" attrs="{'invisible': [('rule_auth','!=','having')]}" widget="many2many_tags" /> |
1387 | + <field name="rule_max" attrs="{'invisible': [('rule_auth','=','nobody')]}" /> |
1388 | + <field name="rule_max_number" attrs="{'invisible': ['|',('rule_max','=',False),('rule_auth','=','nobody')]}"/> |
1389 | + <label for="stat_my_monthly_sending"/> |
1390 | + <div> |
1391 | + <field name="stat_my_monthly_sending" attrs="{'invisible': [('rule_auth','=','nobody')]}" /> |
1392 | + <div attrs="{'invisible': [('remaining_sending','=',-1)]}" class="oe_grey"> |
1393 | + You can still grant <field name="remaining_sending" class="oe_inline"/> badges this month |
1394 | + </div> |
1395 | + <div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey"> |
1396 | + No monthly sending limit |
1397 | + </div> |
1398 | + </div> |
1399 | + </group> |
1400 | + <group string="Rewards for challenges"> |
1401 | + <field name="plan_ids" widget="many2many_kanban" nolabel="1" /> |
1402 | + </group> |
1403 | + <group string="Statistics"> |
1404 | + <group> |
1405 | + <field name="stat_count"/> |
1406 | + <field name="stat_this_month"/> |
1407 | + <field name="stat_count_distinct"/> |
1408 | + </group> |
1409 | + <group> |
1410 | + <field name="stat_my"/> |
1411 | + <field name="stat_my_this_month"/> |
1412 | + </group> |
1413 | + </group> |
1414 | + </sheet> |
1415 | + </form> |
1416 | + </field> |
1417 | + </record> |
1418 | + |
1419 | + |
1420 | + <record id="badge_kanban_view" model="ir.ui.view" > |
1421 | + <field name="name">Badge Kanban View</field> |
1422 | + <field name="model">gamification.badge</field> |
1423 | + <field name="arch" type="xml"> |
1424 | + <kanban version="7.0" class="oe_background_grey"> |
1425 | + <field name="name"/> |
1426 | + <field name="description"/> |
1427 | + <field name="image"/> |
1428 | + <field name="stat_my"/> |
1429 | + <field name="stat_count"/> |
1430 | + <field name="stat_this_month"/> |
1431 | + <field name="unique_owner_ids"/> |
1432 | + <field name="stat_my_monthly_sending"/> |
1433 | + <field name="remaining_sending" /> |
1434 | + <field name="rule_max_number" /> |
1435 | + <templates> |
1436 | + <t t-name="kanban-box"> |
1437 | + <div t-attf-class="#{record.stat_my.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'} oe_kanban_card oe_kanban_global_click oe_kanban_badge"> |
1438 | + <div class="oe_kanban_content"> |
1439 | + <div class="oe_kanban_left"> |
1440 | + <a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.image.raw_value)" t-att-title="record.name.value" width="90" height="90" /></a> |
1441 | + </div> |
1442 | + <div class="oe_no_overflow"> |
1443 | + <h4><field name="name"/></h4> |
1444 | + <t t-if="record.remaining_sending.value != 0"> |
1445 | + <button type="action" name="%(action_grant_wizard)d" class="oe_highlight">Grant</button> |
1446 | + <span class="oe_grey"> |
1447 | + <t t-if="record.remaining_sending.value != -1"> |
1448 | + <t t-esc="record.stat_my_monthly_sending.value"/>/<t t-esc="record.rule_max_number.value"/> |
1449 | + </t> |
1450 | + <t t-if="record.remaining_sending.value == -1"> |
1451 | + <t t-esc="record.stat_my_monthly_sending.value"/>/∞ |
1452 | + </t> |
1453 | + </span> |
1454 | + </t> |
1455 | + <t t-if="record.remaining_sending.value == 0"> |
1456 | + <div class="oe_grey">Can not grant</div> |
1457 | + </t> |
1458 | + <p> |
1459 | + <strong><t t-esc="record.stat_count.raw_value"/></strong> granted,<br/> |
1460 | + <strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month |
1461 | + </p> |
1462 | + </div> |
1463 | + <div class="oe_kanban_badge_avatars"> |
1464 | + <t t-if="record.description.value"> |
1465 | + <p><em><field name="description"/></em></p> |
1466 | + </t> |
1467 | + <a type="object" name="get_granted_employees"> |
1468 | + <t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner"> |
1469 | + <img t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/> |
1470 | + </t> |
1471 | + </a> |
1472 | + </div> |
1473 | + </div> |
1474 | + </div> |
1475 | + </t> |
1476 | + </templates> |
1477 | + </kanban> |
1478 | + </field> |
1479 | + </record> |
1480 | + |
1481 | + |
1482 | + <!-- Badge user viewss --> |
1483 | + |
1484 | + <record id="badge_user_kanban_view" model="ir.ui.view" > |
1485 | + <field name="name">Badge User Kanban View</field> |
1486 | + <field name="model">gamification.badge.user</field> |
1487 | + <field name="arch" type="xml"> |
1488 | + <kanban version="7.0" class="oe_background_grey"> |
1489 | + <field name="badge_name"/> |
1490 | + <field name="badge_id"/> |
1491 | + <field name="user_id"/> |
1492 | + <field name="comment"/> |
1493 | + <field name="create_date"/> |
1494 | + <templates> |
1495 | + <t t-name="kanban-box"> |
1496 | + <div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white"> |
1497 | + <div class="oe_kanban_content"> |
1498 | + <div class="oe_kanban_left"> |
1499 | + <a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.badge_id.raw_value)" t-att-title="record.badge_name.value" width="24" height="24" /></a> |
1500 | + </div> |
1501 | + <h4> |
1502 | + <a type="open"><t t-esc="record.badge_name.raw_value" /></a> |
1503 | + </h4> |
1504 | + <t t-if="record.comment.raw_value"> |
1505 | + <p><em><field name="comment"/></em></p> |
1506 | + </t> |
1507 | + <p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="record.create_date.raw_value.toString(Date.CultureInfo.formatPatterns.shortDate)" /></p> |
1508 | + </div> |
1509 | + </div> |
1510 | + </t> |
1511 | + </templates> |
1512 | + </kanban> |
1513 | + </field> |
1514 | + </record> |
1515 | + </data> |
1516 | +</openerp> |
1517 | |
1518 | === added file 'gamification/cron.xml' |
1519 | --- gamification/cron.xml 1970-01-01 00:00:00 +0000 |
1520 | +++ gamification/cron.xml 2013-06-28 14:51:48 +0000 |
1521 | @@ -0,0 +1,16 @@ |
1522 | +<?xml version="1.0" encoding="UTF-8"?> |
1523 | +<openerp> |
1524 | + <data> |
1525 | + <record forcecreate="True" id="ir_cron_check_plan" |
1526 | + model="ir.cron"> |
1527 | + <field name="name">Run Goal Plan Checker</field> |
1528 | + <field name="interval_number">1</field> |
1529 | + <field name="interval_type">days</field> |
1530 | + <field name="numbercall">-1</field> |
1531 | + <field eval="False" name="doall" /> |
1532 | + <field name="model">gamification.goal.plan</field> |
1533 | + <field name="function">_cron_update</field> |
1534 | + <field name="args">()</field> |
1535 | + </record> |
1536 | + </data> |
1537 | +</openerp> |
1538 | \ No newline at end of file |
1539 | |
1540 | === added directory 'gamification/doc' |
1541 | === added file 'gamification/doc/gamification_plan_howto.rst' |
1542 | --- gamification/doc/gamification_plan_howto.rst 1970-01-01 00:00:00 +0000 |
1543 | +++ gamification/doc/gamification_plan_howto.rst 2013-06-28 14:51:48 +0000 |
1544 | @@ -0,0 +1,107 @@ |
1545 | +How to create new challenge for my addon |
1546 | +======================================== |
1547 | + |
1548 | +Running example |
1549 | ++++++++++++++++ |
1550 | + |
1551 | +A module to create and manage groceries lists has been developped. To motivate users to use it, a challenge (gamification.goal.plan) is developped. This how to will explain the creation of a dedicated module and the XML file for the required data. |
1552 | + |
1553 | +Module |
1554 | +++++++ |
1555 | + |
1556 | +The challenge for my addon will consist of an auto-installed module containing only the definition of goals. Goal type are quite technical to create and should not be seen or modified through the web interface. |
1557 | + |
1558 | +If our groceries module is called ``groceries``, the structure will be consisted of three addons : |
1559 | + |
1560 | +:: |
1561 | + |
1562 | + addons/ |
1563 | + ... |
1564 | + gamification/ |
1565 | + groceries/ |
1566 | + groceries_gamification/ |
1567 | + __openerp__.py |
1568 | + groceries_goals.xml |
1569 | + |
1570 | +The ``__openerp__.py`` file containing the following information : |
1571 | + |
1572 | +:: |
1573 | + |
1574 | + { |
1575 | + ... |
1576 | + 'depends': ['gamification','groceries'], |
1577 | + 'data': ['groceries_goals.xml'], |
1578 | + 'auto_install': True, |
1579 | + } |
1580 | + |
1581 | + |
1582 | +Goal type definition |
1583 | ++++++++++++++++++++++ |
1584 | + |
1585 | +For our groceries module, we would like to evaluate the total number of items written on lists during a month. The evaluated value being a number of line in the database, the goal type computation mode will be ``count``. The XML data file will contain the following information : |
1586 | + |
1587 | +:: |
1588 | + |
1589 | + <record model="gamification.goal.type" id="type_groceries_nbr_items"> |
1590 | + <field name="name">Number of items</field> |
1591 | + <field name="computation_mode">count</field> |
1592 | + ... |
1593 | + </record> |
1594 | + |
1595 | +To be able to compute the number of lines, the model containing groceries is required. To be able to refine the computation on a time period, a reference date field should be mention. This field date must be a field of the selected model. In our example, we will use the ``gorceries_item`` model and the ``shopping_day`` field. |
1596 | + |
1597 | +:: |
1598 | + |
1599 | + <record model="gamification.goal.type" id="type_groceries_nbr_items"> |
1600 | + ... |
1601 | + <field name="model_id" eval="ref('groceries.model_groceries_item')" /> |
1602 | + <field name="field_date_id" eval="ref('groceries.field_groceries_item_shopping_day')" /> |
1603 | + </record> |
1604 | + |
1605 | +As we do not want to count every recorded item, we will use a domain to restrict the selection on the user (display the count only for the items the users has bought) and the state (only the items whose list is confirmed are included). The user restriction is made with the keyword ``user_id`` in the domain and should correspond to a many2one field with the relation ``res.users``. During the evaluation, it is replaced by the user ID of the linked goal. |
1606 | + |
1607 | +:: |
1608 | + |
1609 | + <record model="gamification.goal.type" id="type_groceries_nbr_items"> |
1610 | + ... |
1611 | + <field name="domain">[('shopper_id', '=', user_id), ('list_id.state', '=', 'confirmed')]</field> |
1612 | + </record> |
1613 | + |
1614 | +An action can also be defined to help users to quickly reach the screen where they will be able to modify their current value. This is done by adding the XML ID of the ir.action we want to call. In our example, we would like to open the grocery list form view owned by the user. |
1615 | + |
1616 | +If we do not specify a res_id to the action, only the list of records can be displayed. The restriction is done with the field ``res_id_field`` containing the field name of the user profile containing the required id. In our example, we assume the res.users model has been extended with a many2one field ``groceries_list`` to the model ``groceries.list``. |
1617 | + |
1618 | +:: |
1619 | + |
1620 | + <record model="gamification.goal.type" id="type_groceries_nbr_items"> |
1621 | + ... |
1622 | + <field name="action_id">groceries.action_groceries_list_form</field> |
1623 | + <field name="res_id_field">groceries_list.id</field> |
1624 | + </record> |
1625 | + |
1626 | + |
1627 | +Plan definition |
1628 | +++++++++++++++++ |
1629 | + |
1630 | +Once all the goal types are defined, a challenge (or goal plan) can be created. In our example, we would like to create a plan "Discover the Groceries Module" with simple tasks applied to every new user in the group ``groceries.shoppers_group``. This goal plan should only be applied once by user and with no ending period, no dates or peridocity is then selected. The goal will be started manually but specifying a value to ``start_date`` can make it start automatically. |
1631 | + |
1632 | +:: |
1633 | + |
1634 | + <record model="gamification.goal.plan" id="plan_groceries_discover"> |
1635 | + <field name="name">Discover the Groceries Module</field> |
1636 | + <field name="period">once</field> |
1637 | + <field name="visibility_mode">progressbar</field> |
1638 | + <field name="report_message_frequency">never</field> |
1639 | + <field name="planline_ids" eval="[(4, ref('planline_groceries_discover1'))]"/> |
1640 | + <field name="autojoin_group_id" eval="ref('groceries.shoppers_group')" /> |
1641 | + </record> |
1642 | + |
1643 | +To add goal types to a plan, planlines must be created. The value to reach is defined for a planline, this will be the ``target_goal`` for each goal generated. |
1644 | + |
1645 | +:: |
1646 | + |
1647 | + <record model="gamification.goal.planline" id="planline_groceries_discover1"> |
1648 | + <field name="type_id" eval="ref('type_groceries_nbr_items')" /> |
1649 | + <field name="target_goal">3</field> |
1650 | + <field name="plan_id" eval="ref('plan_groceries_discover')" /> |
1651 | + </record> |
1652 | \ No newline at end of file |
1653 | |
1654 | === added file 'gamification/doc/goal.rst' |
1655 | --- gamification/doc/goal.rst 1970-01-01 00:00:00 +0000 |
1656 | +++ gamification/doc/goal.rst 2013-06-28 14:51:48 +0000 |
1657 | @@ -0,0 +1,46 @@ |
1658 | +.. _gamification_goal: |
1659 | + |
1660 | +gamification.goal |
1661 | +================= |
1662 | + |
1663 | +Models |
1664 | +++++++ |
1665 | + |
1666 | +``gamification.goal`` for the generated goals from plans |
1667 | + |
1668 | +.. versionchanged:: 7.0 |
1669 | + |
1670 | +Fields |
1671 | +++++++ |
1672 | + |
1673 | + - ``type_id`` : The related gamification.goal.type object. |
1674 | + - ``user_id`` : The user responsible for the goal. Goal type domain filtering on the user id will use that value. |
1675 | + - ``planline_id`` : if the goal is generated from a plan, the planline used to generate this goal |
1676 | + - ``plan_id`` : if the goal is generated from a plan, related link from planline_id.plan_id |
1677 | + - ``start_date`` : the starting date for goal evaluation. If a goal is evaluated using a date field (eg: creation date of an invoice), this field will be used in the domain. Without starting date, every past recorded data will be considered in the goal type computation. |
1678 | + - ``end_date`` : the end date for goal evaluation, similar to start_date. When an end_date is passed, the goal update method will change to status to reached or failed depending of the current value. |
1679 | + - ``target_goal`` : the numerical value to reach (higher or lower depending of goal type) to reach a goal |
1680 | + - ``current`` : current computed value of the goal |
1681 | + - ``completeness`` : percentage of completion of a goal |
1682 | + - ``state`` : |
1683 | + - ``draft`` : goal not active and displayed in user's goal list. Only present for manual creation of goal as plans generate goals in progress. |
1684 | + - ``inprogress`` : a goal is started and is not closed yet |
1685 | + - ``inprogress_update`` : in case of manual goal, a number of day can be specified. If the last manual update from the user is older than this number of days, a reminder will be sent and the state changed to this. |
1686 | + - ``reached`` : the goal is succeeded |
1687 | + - ``failed`` : the goal is failed |
1688 | + - ``canceled`` : state if the goal plan is canceled |
1689 | + - ``remind_update_delay`` : the number of day before an inprogress goal is set to inprogress_update and a reminder is sent. |
1690 | + - ``last_update`` : the date of the last modification of this goal |
1691 | + - ``computation_mode`` : related field from the linked goal type |
1692 | + - ``type_description`` : related field from the linked goal type |
1693 | + - ``type_suffix`` : related field from the linked goal type |
1694 | + - ``type_condition`` : related field from the linked goal type |
1695 | + |
1696 | + |
1697 | +Methods |
1698 | ++++++++ |
1699 | + |
1700 | + - ``update`` : |
1701 | + Compute the current value of goal and change states accordingly and send reminder if needed. |
1702 | + - ``get_action`` : |
1703 | + Returns the action description specified for the goal modification. If an action XML ID is specified in the goal type definition it will be used. If a goal is manual, the action is a simple wizard to input a new value. |
1704 | |
1705 | === added file 'gamification/doc/index.rst' |
1706 | --- gamification/doc/index.rst 1970-01-01 00:00:00 +0000 |
1707 | +++ gamification/doc/index.rst 2013-06-28 14:51:48 +0000 |
1708 | @@ -0,0 +1,17 @@ |
1709 | +Gamification module documentation |
1710 | +================================= |
1711 | + |
1712 | +The Gamification module holds all the module and logic related to goals, plans and badges. The computation is mainly done in this module, other modules inherating from it should only add data and tests. |
1713 | + |
1714 | +Goals |
1715 | +----- |
1716 | +A **Goal** is an objective applied to an user with a numerical target to reach. It can have a starting and end date. Users usually do not create goals but relies on goal plans. |
1717 | + |
1718 | +A **Goal Type** is a generic objective that can be applied to any structure stored in the database and use numerical value. The creation of goal types is quite technical and should rarely be done. Once a generic goal is created, it can be associated to several goal plans with different numerical targets. |
1719 | + |
1720 | +A **Goal Plan** is a a set of goal types with a target value to reach applied to a group of users. It can be periodic to create and evaluate easily the performances of a team. |
1721 | + |
1722 | +Badges |
1723 | +------ |
1724 | +A **Badge** is a symbolic token granted to a user as a sign of reward. It can be offered by a user to another or automatically offered when some conditions are met. The conditions can either be a list of goal types succeeded or a user definied python code executed. |
1725 | + |
1726 | |
1727 | === added file 'gamification/goal.py' |
1728 | --- gamification/goal.py 1970-01-01 00:00:00 +0000 |
1729 | +++ gamification/goal.py 2013-06-28 14:51:48 +0000 |
1730 | @@ -0,0 +1,404 @@ |
1731 | +# -*- coding: utf-8 -*- |
1732 | +############################################################################## |
1733 | +# |
1734 | +# OpenERP, Open Source Management Solution |
1735 | +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) |
1736 | +# |
1737 | +# This program is free software: you can redistribute it and/or modify |
1738 | +# it under the terms of the GNU General Public License as published by |
1739 | +# the Free Software Foundation, either version 3 of the License, or |
1740 | +# (at your option) any later version. |
1741 | +# |
1742 | +# This program is distributed in the hope that it will be useful, |
1743 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1744 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1745 | +# GNU General Public License for more details. |
1746 | +# |
1747 | +# You should have received a copy of the GNU General Public License |
1748 | +# along with this program. If not, see <http://www.gnu.org/licenses/> |
1749 | +# |
1750 | +############################################################################## |
1751 | + |
1752 | +from openerp import SUPERUSER_ID |
1753 | +from openerp.osv import fields, osv |
1754 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF |
1755 | +from openerp.tools.safe_eval import safe_eval |
1756 | +from openerp.tools.translate import _ |
1757 | + |
1758 | +from datetime import date, datetime, timedelta |
1759 | + |
1760 | +import logging |
1761 | + |
1762 | +_logger = logging.getLogger(__name__) |
1763 | + |
1764 | + |
1765 | +class gamification_goal_type(osv.Model): |
1766 | + """Goal type definition |
1767 | + |
1768 | + A goal type defining a way to set an objective and evaluate it |
1769 | + Each module wanting to be able to set goals to the users needs to create |
1770 | + a new gamification_goal_type |
1771 | + """ |
1772 | + _name = 'gamification.goal.type' |
1773 | + _description = 'Gamification goal type' |
1774 | + _order = 'sequence' |
1775 | + |
1776 | + def _get_suffix(self, cr, uid, ids, field_name, arg, context=None): |
1777 | + res = dict.fromkeys(ids, '') |
1778 | + for goal in self.browse(cr, uid, ids, context=context): |
1779 | + if goal.suffix and not goal.monetary: |
1780 | + res[goal.id] = goal.suffix |
1781 | + elif goal.monetary: |
1782 | + # use the current user's company currency |
1783 | + user = self.pool.get('res.users').browse(cr, uid, uid, context) |
1784 | + if goal.suffix: |
1785 | + res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix) |
1786 | + else: |
1787 | + res[goal.id] = user.company_id.currency_id.symbol |
1788 | + else: |
1789 | + res[goal.id] = "" |
1790 | + return res |
1791 | + |
1792 | + _columns = { |
1793 | + 'name': fields.char('Goal Type', required=True, translate=True), |
1794 | + 'description': fields.text('Goal Description'), |
1795 | + 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."), |
1796 | + 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True), |
1797 | + 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"), |
1798 | + 'computation_mode': fields.selection([ |
1799 | + ('manually', 'Recorded manually'), |
1800 | + ('count', 'Automatic: number of records'), |
1801 | + ('sum', 'Automatic: sum on a field'), |
1802 | + ('python', 'Automatic: execute a specific Python code'), |
1803 | + ], |
1804 | + string="Computation Mode", |
1805 | + help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.", |
1806 | + required=True), |
1807 | + 'display_mode': fields.selection([ |
1808 | + ('progress', 'Progressive (using numerical values)'), |
1809 | + ('checkbox', 'Checkbox (done or not-done)'), |
1810 | + ], |
1811 | + string="Displayed as", required=True), |
1812 | + 'model_id': fields.many2one('ir.model', |
1813 | + string='Model', |
1814 | + help='The model object for the field to evaluate'), |
1815 | + 'field_id': fields.many2one('ir.model.fields', |
1816 | + string='Field to Sum', |
1817 | + help='The field containing the value to evaluate'), |
1818 | + 'field_date_id': fields.many2one('ir.model.fields', |
1819 | + string='Date Field', |
1820 | + help='The date to use for the time period evaluated'), |
1821 | + #TODO: actually it would be better to use 'user.id' in the domain definition, because it means user is a browse record and it's more flexible (i can do '[(country_id,=,user.partner_id.country_id.id)]) |
1822 | + |
1823 | + 'domain': fields.char("Filter Domain", |
1824 | + help="Technical filters rules to apply. Use 'user.id' (without marks) to limit the search to the evaluated user.", |
1825 | + required=True), |
1826 | + 'compute_code': fields.char('Compute Code', |
1827 | + help="The name of the python method that will be executed to compute the current value. See the file gamification/goal_type_data.py for examples."), |
1828 | + 'condition': fields.selection([ |
1829 | + ('higher', 'The higher the better'), |
1830 | + ('lower', 'The lower the better') |
1831 | + ], |
1832 | + string='Goal Performance', |
1833 | + help='A goal is considered as completed when the current value is compared to the value to reach', |
1834 | + required=True), |
1835 | + 'sequence': fields.integer('Sequence', help='Sequence number for ordering', required=True), |
1836 | + 'action_id': fields.many2one('ir.actions.act_window', string="Action", |
1837 | + help="The action that will be called to update the goal value."), |
1838 | + 'res_id_field': fields.char("ID Field of user", |
1839 | + help="The field name on the user profile (res.users) containing the value for res_id for action.") |
1840 | + } |
1841 | + |
1842 | + _defaults = { |
1843 | + 'sequence': 1, |
1844 | + 'condition': 'higher', |
1845 | + 'computation_mode': 'manually', |
1846 | + 'domain': "[]", |
1847 | + 'monetary': False, |
1848 | + 'display_mode': 'progress', |
1849 | + } |
1850 | + |
1851 | + |
1852 | +class gamification_goal(osv.Model): |
1853 | + """Goal instance for a user |
1854 | + |
1855 | + An individual goal for a user on a specified time period""" |
1856 | + |
1857 | + _name = 'gamification.goal' |
1858 | + _description = 'Gamification goal instance' |
1859 | + _inherit = 'mail.thread' |
1860 | + |
1861 | + def _get_completeness(self, cr, uid, ids, field_name, arg, context=None): |
1862 | + """Return the percentage of completeness of the goal, between 0 and 100""" |
1863 | + res = dict.fromkeys(ids, 0.0) |
1864 | + for goal in self.browse(cr, uid, ids, context=context): |
1865 | + if goal.type_condition == 'higher' and goal.current > 0: |
1866 | + res[goal.id] = min(100, round(100.0 * goal.current / goal.target_goal, 2)) |
1867 | + elif goal.current < goal.target_goal: |
1868 | + # a goal 'lower than' has only two values possible: 0 or 100% |
1869 | + res[goal.id] = 100.0 |
1870 | + return res |
1871 | + |
1872 | + def on_change_type_id(self, cr, uid, ids, type_id=False, context=None): |
1873 | + goal_type = self.pool.get('gamification.goal.type') |
1874 | + if not type_id: |
1875 | + return {'value': {'type_id': False}} |
1876 | + goal_type = goal_type.browse(cr, uid, type_id, context=context) |
1877 | + return {'value': {'computation_mode': goal_type.computation_mode, 'type_condition': goal_type.condition}} |
1878 | + |
1879 | + _columns = { |
1880 | + 'type_id': fields.many2one('gamification.goal.type', string='Goal Type', required=True, ondelete="cascade"), |
1881 | + 'user_id': fields.many2one('res.users', string='User', required=True), |
1882 | + 'planline_id': fields.many2one('gamification.goal.planline', string='Goal Planline', ondelete="cascade"), |
1883 | + 'plan_id': fields.related('planline_id', 'plan_id', |
1884 | + string="Plan", |
1885 | + type='many2one', |
1886 | + relation='gamification.goal.plan', |
1887 | + store=True), |
1888 | + 'start_date': fields.date('Start Date'), |
1889 | + 'end_date': fields.date('End Date'), # no start and end = always active |
1890 | + 'target_goal': fields.float('To Reach', |
1891 | + required=True, |
1892 | + track_visibility='always'), # no goal = global index |
1893 | + 'current': fields.float('Current Value', required=True, track_visibility='always'), |
1894 | + 'completeness': fields.function(_get_completeness, type='float', string='Completeness'), |
1895 | + 'state': fields.selection([ |
1896 | + ('draft', 'Draft'), |
1897 | + ('inprogress', 'In progress'), |
1898 | + ('inprogress_update', 'In progress (to update)'), |
1899 | + ('reached', 'Reached'), |
1900 | + ('failed', 'Failed'), |
1901 | + ('canceled', 'Canceled'), |
1902 | + ], |
1903 | + string='State', |
1904 | + required=True, |
1905 | + track_visibility='always'), |
1906 | + |
1907 | + 'computation_mode': fields.related('type_id', 'computation_mode', type='char', string="Type computation mode"), |
1908 | + 'remind_update_delay': fields.integer('Remind delay', |
1909 | + help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."), |
1910 | + 'last_update': fields.date('Last Update', |
1911 | + help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in goal plan). Ignored in case of non-manual goal or goal not linked to a plan."), |
1912 | + |
1913 | + 'type_description': fields.related('type_id', 'description', type='char', string='Type Description', readonly=True), |
1914 | + 'type_suffix': fields.related('type_id', 'suffix', type='char', string='Type Description', readonly=True), |
1915 | + 'type_condition': fields.related('type_id', 'condition', type='char', string='Type Condition', readonly=True), |
1916 | + 'type_suffix': fields.related('type_id', 'full_suffix', type="char", string="Suffix", readonly=True), |
1917 | + 'type_display': fields.related('type_id', 'display_mode', type="char", string="Display Mode", readonly=True), |
1918 | + } |
1919 | + |
1920 | + _defaults = { |
1921 | + 'current': 0, |
1922 | + 'state': 'draft', |
1923 | + 'start_date': fields.date.today, |
1924 | + } |
1925 | + _order = 'create_date desc, end_date desc, type_id, id' |
1926 | + |
1927 | + def _check_remind_delay(self, goal, context=None): |
1928 | + """Verify if a goal has not been updated for some time and send a |
1929 | + reminder message of needed. |
1930 | + |
1931 | + :return: data to write on the goal object |
1932 | + """ |
1933 | + if goal.remind_update_delay and goal.last_update: |
1934 | + delta_max = timedelta(days=goal.remind_update_delay) |
1935 | + last_update = datetime.strptime(goal.last_update, DF).date() |
1936 | + if date.today() - last_update > delta_max and goal.state == 'inprogress': |
1937 | + # generate a remind report |
1938 | + temp_obj = self.pool.get('email.template') |
1939 | + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context) |
1940 | + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context) |
1941 | + |
1942 | + self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment') |
1943 | + return {'state': 'inprogress_update'} |
1944 | + return {} |
1945 | + |
1946 | + def update(self, cr, uid, ids, context=None): |
1947 | + """Update the goals to recomputes values and change of states |
1948 | + |
1949 | + If a manual goal is not updated for enough time, the user will be |
1950 | + reminded to do so (done only once, in 'inprogress' state). |
1951 | + If a goal reaches the target value, the status is set to reached |
1952 | + If the end date is passed (at least +1 day, time not considered) without |
1953 | + the target value being reached, the goal is set as failed.""" |
1954 | + |
1955 | + for goal in self.browse(cr, uid, ids, context=context): |
1956 | + #TODO: towrite may be falsy, to avoid useless write on the object. Please check the whole thing is still working |
1957 | + towrite = {} |
1958 | + if goal.state in ('draft', 'canceled'): |
1959 | + # skip if goal draft or canceled |
1960 | + continue |
1961 | + |
1962 | + if goal.type_id.computation_mode == 'manually': |
1963 | + towrite.update(self._check_remind_delay(goal, context)) |
1964 | + |
1965 | + elif goal.type_id.computation_mode == 'python': |
1966 | + # execute the chosen method |
1967 | + values = {'cr': cr, 'uid': goal.user_id.id, 'context': context, 'self': self.pool.get('gamification.goal.type')} |
1968 | + result = safe_eval(goal.type_id.compute_code, values, {}) |
1969 | + |
1970 | + if type(result) in (float, int, long) and result != goal.current: |
1971 | + towrite['current'] = result |
1972 | + else: |
1973 | + _logger.exception(_('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code))) |
1974 | + # raise osv.except_osv(_('Error!'), _('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code))) |
1975 | + |
1976 | + else: # count or sum |
1977 | + obj = self.pool.get(goal.type_id.model_id.model) |
1978 | + field_date_name = goal.type_id.field_date_id.name |
1979 | + |
1980 | + # eval the domain with user_id replaced by goal user |
1981 | + domain = safe_eval(goal.type_id.domain, {'user': goal.user_id}) |
1982 | + |
1983 | + #add temporal clause(s) to the domain if fields are filled on the goal |
1984 | + if goal.start_date and field_date_name: |
1985 | + domain.append((field_date_name, '>=', goal.start_date)) |
1986 | + if goal.end_date and field_date_name: |
1987 | + domain.append((field_date_name, '<=', goal.end_date)) |
1988 | + |
1989 | + if goal.type_id.computation_mode == 'sum': |
1990 | + field_name = goal.type_id.field_id.name |
1991 | + res = obj.read_group(cr, uid, domain, [field_name], [''], context=context) |
1992 | + new_value = res and res[0][field_name] or 0.0 |
1993 | + |
1994 | + else: # computation mode = count |
1995 | + new_value = obj.search(cr, uid, domain, context=context, count=True) |
1996 | + |
1997 | + #avoid useless write if the new value is the same as the old one |
1998 | + if new_value != goal.current: |
1999 | + towrite['current'] = new_value |
2000 | + |
2001 | + # check goal target reached |
2002 | + #TODO: reached condition is wrong because it should check time constraints. |
2003 | + if (goal.type_id.condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.type_id.condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal): |
2004 | + towrite['state'] = 'reached' |
2005 | + |
2006 | + # check goal failure |
2007 | + elif goal.end_date and fields.date.today() > goal.end_date: |
2008 | + towrite['state'] = 'failed' |
2009 | + if towrite: |
2010 | + self.write(cr, uid, [goal.id], towrite, context=context) |
2011 | + return True |
2012 | + |
2013 | + def action_start(self, cr, uid, ids, context=None): |
2014 | + """Mark a goal as started. |
2015 | + |
2016 | + This should only be used when creating goals manually (in draft state)""" |
2017 | + self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) |
2018 | + return self.update(cr, uid, ids, context=context) |
2019 | + |
2020 | + def action_reach(self, cr, uid, ids, context=None): |
2021 | + """Mark a goal as reached. |
2022 | + |
2023 | + If the target goal condition is not met, the state will be reset to In |
2024 | + Progress at the next goal update until the end date.""" |
2025 | + return self.write(cr, uid, ids, {'state': 'reached'}, context=context) |
2026 | + |
2027 | + def action_fail(self, cr, uid, ids, context=None): |
2028 | + """Set the state of the goal to failed. |
2029 | + |
2030 | + A failed goal will be ignored in future checks.""" |
2031 | + return self.write(cr, uid, ids, {'state': 'failed'}, context=context) |
2032 | + |
2033 | + def action_cancel(self, cr, uid, ids, context=None): |
2034 | + """Reset the completion after setting a goal as reached or failed. |
2035 | + |
2036 | + This is only the current state, if the date and/or target criterias |
2037 | + match the conditions for a change of state, this will be applied at the |
2038 | + next goal update.""" |
2039 | + return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) |
2040 | + |
2041 | + def create(self, cr, uid, vals, context=None): |
2042 | + """Overwrite the create method to add a 'no_remind_goal' field to True""" |
2043 | + context = context or {} |
2044 | + context['no_remind_goal'] = True |
2045 | + return super(gamification_goal, self).create(cr, uid, vals, context=context) |
2046 | + |
2047 | + def write(self, cr, uid, ids, vals, context=None): |
2048 | + """Overwrite the write method to update the last_update field to today |
2049 | + |
2050 | + If the current value is changed and the report frequency is set to On |
2051 | + change, a report is generated |
2052 | + """ |
2053 | + vals['last_update'] = fields.date.today() |
2054 | + result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context) |
2055 | + for goal in self.browse(cr, uid, ids, context=context): |
2056 | + if goal.state != "draft" and ('type_id' in vals or 'user_id' in vals): |
2057 | + # avoid drag&drop in kanban view |
2058 | + raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal')) |
2059 | + |
2060 | + if vals.get('current'): |
2061 | + if 'no_remind_goal' in context: |
2062 | + # new goals should not be reported |
2063 | + continue |
2064 | + |
2065 | + if goal.plan_id and goal.plan_id.report_message_frequency == 'onchange': |
2066 | + self.pool.get('gamification.goal.plan').report_progress(cr, SUPERUSER_ID, goal.plan_id, users=[goal.user_id], context=context) |
2067 | + return result |
2068 | + |
2069 | + def get_action(self, cr, uid, goal_id, context=None): |
2070 | + """Get the ir.action related to update the goal |
2071 | + |
2072 | + In case of a manual goal, should return a wizard to update the value |
2073 | + :return: action description in a dictionnary |
2074 | + """ |
2075 | + goal = self.browse(cr, uid, goal_id, context=context) |
2076 | + if goal.type_id.action_id: |
2077 | + #open a the action linked on the goal |
2078 | + action = goal.type_id.action_id.read()[0] |
2079 | + |
2080 | + if goal.type_id.res_id_field: |
2081 | + current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context) |
2082 | + # this loop manages the cases where res_id_field is a browse record path (eg : company_id.currency_id.id) |
2083 | + field_names = goal.type_id.res_id_field.split('.') |
2084 | + res = current_user |
2085 | + for field_name in field_names[:]: |
2086 | + res = res.__getitem__(field_name) |
2087 | + action['res_id'] = res |
2088 | + |
2089 | + # if one element to display, should see it in form mode if possible |
2090 | + views = action['views'] |
2091 | + for (view_id, mode) in action['views']: |
2092 | + if mode == "form": |
2093 | + views = [(view_id, mode)] |
2094 | + break |
2095 | + action['views'] = views |
2096 | + return action |
2097 | + |
2098 | + if goal.computation_mode == 'manually': |
2099 | + #open a wizard window to update the value manually |
2100 | + action = { |
2101 | + 'name': _("Update %s") % goal.type_id.name, |
2102 | + 'id': goal_id, |
2103 | + 'type': 'ir.actions.act_window', |
2104 | + 'views': [[False, 'form']], |
2105 | + 'target': 'new', |
2106 | + } |
2107 | + action['context'] = {'default_goal_id': goal_id, 'default_current': goal.current} |
2108 | + action['res_model'] = 'gamification.goal.wizard' |
2109 | + return action |
2110 | + return False |
2111 | + |
2112 | + |
2113 | +class goal_manual_wizard(osv.TransientModel): |
2114 | + """Wizard type to update a manual goal""" |
2115 | + _name = 'gamification.goal.wizard' |
2116 | + _columns = { |
2117 | + 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True), |
2118 | + 'current': fields.float('Current'), |
2119 | + } |
2120 | + |
2121 | + def action_update_current(self, cr, uid, ids, context=None): |
2122 | + """Wizard action for updating the current value""" |
2123 | + |
2124 | + goal_obj = self.pool.get('gamification.goal') |
2125 | + |
2126 | + for wiz in self.browse(cr, uid, ids, context=context): |
2127 | + towrite = { |
2128 | + 'current': wiz.current, |
2129 | + 'goal_id': wiz.goal_id.id, |
2130 | + } |
2131 | + goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context) |
2132 | + goal_obj.update(cr, uid, [wiz.goal_id.id], context=context) |
2133 | + return {} |
2134 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
2135 | |
2136 | === added file 'gamification/goal_base_data.xml' |
2137 | --- gamification/goal_base_data.xml 1970-01-01 00:00:00 +0000 |
2138 | +++ gamification/goal_base_data.xml 2013-06-28 14:51:48 +0000 |
2139 | @@ -0,0 +1,232 @@ |
2140 | +<?xml version="1.0"?> |
2141 | +<openerp> |
2142 | + <data> |
2143 | + |
2144 | + <!-- goal types --> |
2145 | + <record model="gamification.goal.type" id="type_base_timezone"> |
2146 | + <field name="name">Set your Timezone</field> |
2147 | + <field name="description">Configure your profile and specify your timezone</field> |
2148 | + <field name="computation_mode">count</field> |
2149 | + <field name="display_mode">checkbox</field> |
2150 | + <field name="model_id" eval="ref('base.model_res_users')" /> |
2151 | + <field name="domain">[('id','=',user.id),('partner_id.tz', '!=', False)]</field> |
2152 | + <field name="action_id" eval="ref('base.action_res_users_my')" /> |
2153 | + <field name="res_id_field">id</field> |
2154 | + </record> |
2155 | + |
2156 | + <record model="gamification.goal.type" id="type_base_avatar"> |
2157 | + <field name="name">Set your Avatar</field> |
2158 | + <field name="description">In your user preference</field> |
2159 | + <field name="computation_mode">manually</field> |
2160 | + <field name="display_mode">checkbox</field> |
2161 | + <!-- problem : default avatar != False -> manually + check in write function --> |
2162 | + <field name="action_id" eval="ref('base.action_res_users_my')" /> |
2163 | + <field name="res_id_field">id</field> |
2164 | + </record> |
2165 | + |
2166 | + |
2167 | + <record model="gamification.goal.type" id="type_base_company_data"> |
2168 | + <field name="name">Set your Company Data</field> |
2169 | + <field name="description">Write some information about your company (specify at least a name)</field> |
2170 | + <field name="computation_mode">count</field> |
2171 | + <field name="display_mode">checkbox</field> |
2172 | + <field name="model_id" eval="ref('base.model_res_company')" /> |
2173 | + <field name="domain">[('user_ids', 'in', user.id), ('name', '!=', 'Your Company')]</field> |
2174 | + <field name="action_id" eval="ref('base.action_res_company_form')" /> |
2175 | + <field name="res_id_field">company_id.id</field> |
2176 | + </record> |
2177 | + |
2178 | + <record model="gamification.goal.type" id="type_base_company_logo"> |
2179 | + <field name="name">Set your Company Logo</field> |
2180 | + <field name="computation_mode">count</field> |
2181 | + <field name="display_mode">checkbox</field> |
2182 | + <field name="model_id" eval="ref('base.model_res_company')" /> |
2183 | + <field name="domain">[('user_ids', 'in', user.id),('logo', '!=', False)]</field> |
2184 | + <field name="action_id" eval="ref('base.action_res_company_form')" /> |
2185 | + <field name="res_id_field">company_id.id</field> |
2186 | + </record> |
2187 | + |
2188 | + <record id="action_new_simplified_res_users" model="ir.actions.act_window"> |
2189 | + <field name="name">Create User</field> |
2190 | + <field name="type">ir.actions.act_window</field> |
2191 | + <field name="res_model">res.users</field> |
2192 | + <field name="view_type">form</field> |
2193 | + <field name="target">current</field> |
2194 | + <field name="view_id" ref="base.view_users_simple_form"/> |
2195 | + <field name="context">{'default_groups_ref': ['base.group_user']}</field> |
2196 | + <field name="help">Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.</field> |
2197 | + </record> |
2198 | + |
2199 | + <record model="gamification.goal.type" id="type_base_invite"> |
2200 | + <field name="name">Invite new Users</field> |
2201 | + <field name="description">Create at least another user</field> |
2202 | + <field name="display_mode">checkbox</field> |
2203 | + <field name="computation_mode">count</field> |
2204 | + <field name="model_id" eval="ref('base.model_res_users')" /> |
2205 | + <field name="domain">[('id', '!=', user.id)]</field> |
2206 | + <field name="action_id" eval="ref('action_new_simplified_res_users')" /> |
2207 | + </record> |
2208 | + |
2209 | + <record model="gamification.goal.type" id="type_nbr_following"> |
2210 | + <field name="name">Mail Group Following</field> |
2211 | + <field name="description">Follow mail groups to receive news</field> |
2212 | + <field name="computation_mode">python</field> |
2213 | + <field name="compute_code">self.number_following(cr, uid, 'mail.group')</field> |
2214 | + <field name="action_id" eval="ref('mail.action_view_groups')" /> |
2215 | + </record> |
2216 | + |
2217 | + |
2218 | + <!-- plans --> |
2219 | + <record model="gamification.goal.plan" id="plan_base_discover"> |
2220 | + <field name="name">Complete your Profile</field> |
2221 | + <field name="period">once</field> |
2222 | + <field name="visibility_mode">progressbar</field> |
2223 | + <field name="report_message_frequency">never</field> |
2224 | + <field name="autojoin_group_id" eval="ref('base.group_user')" /> |
2225 | + <field name="state">inprogress</field> |
2226 | + <field name="category">other</field> |
2227 | + </record> |
2228 | + |
2229 | + <record model="gamification.goal.plan" id="plan_base_configure"> |
2230 | + <field name="name">Setup your Company</field> |
2231 | + <field name="period">once</field> |
2232 | + <field name="visibility_mode">progressbar</field> |
2233 | + <field name="report_message_frequency">never</field> |
2234 | + <field name="user_ids" eval="[(4, ref('base.user_root'))]" /> |
2235 | + <field name="state">inprogress</field> |
2236 | + <field name="category">other</field> |
2237 | + </record> |
2238 | + |
2239 | + <!-- planlines --> |
2240 | + <record model="gamification.goal.planline" id="planline_base_discover1"> |
2241 | + <field name="type_id" eval="ref('type_base_timezone')" /> |
2242 | + <field name="target_goal">1</field> |
2243 | + <field name="plan_id" eval="ref('plan_base_discover')" /> |
2244 | + </record> |
2245 | + <record model="gamification.goal.planline" id="planline_base_discover2"> |
2246 | + <field name="type_id" eval="ref('type_base_avatar')" /> |
2247 | + <field name="target_goal">1</field> |
2248 | + <field name="plan_id" eval="ref('plan_base_discover')" /> |
2249 | + </record> |
2250 | + |
2251 | + <record model="gamification.goal.planline" id="planline_base_admin2"> |
2252 | + <field name="type_id" eval="ref('type_base_company_logo')" /> |
2253 | + <field name="target_goal">1</field> |
2254 | + <field name="plan_id" eval="ref('plan_base_configure')" /> |
2255 | + </record> |
2256 | + <record model="gamification.goal.planline" id="planline_base_admin1"> |
2257 | + <field name="type_id" eval="ref('type_base_company_data')" /> |
2258 | + <field name="target_goal">1</field> |
2259 | + <field name="plan_id" eval="ref('plan_base_configure')" /> |
2260 | + </record> |
2261 | + <record model="gamification.goal.planline" id="planline_base_admin3"> |
2262 | + <field name="type_id" eval="ref('type_base_invite')" /> |
2263 | + <field name="target_goal">1</field> |
2264 | + <field name="plan_id" eval="ref('plan_base_configure')" /> |
2265 | + </record> |
2266 | + </data> |
2267 | + |
2268 | + <!-- Mail template is done in a NOUPDATE block |
2269 | + so users can freely customize/delete them --> |
2270 | + <data noupdate="0"> |
2271 | + <!--Email template --> |
2272 | + |
2273 | + <record id="email_template_goal_reminder" model="email.template"> |
2274 | + <field name="name">Reminder for Goal Update</field> |
2275 | + <field name="body_html"><![CDATA[ |
2276 | + <header> |
2277 | + <strong>Reminder ${object.name}</strong> |
2278 | + </header> |
2279 | + |
2280 | + <p class="oe_grey">${object.report_header or ''}</p> |
2281 | + |
2282 | + <p>You have not updated your progress for the goal ${object.type_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.</p> |
2283 | + |
2284 | + <p>If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.</p> |
2285 | + ]]></field> |
2286 | + </record> |
2287 | + |
2288 | + <record id="email_template_goal_progress_perso" model="email.template"> |
2289 | + <field name="name">Personal Goal Progress</field> |
2290 | + <field name="body_html"><![CDATA[ |
2291 | + <header> |
2292 | + <strong>${object.name}</strong> |
2293 | + </header> |
2294 | + <p class="oe_grey">${object.report_header or ''}</p> |
2295 | + |
2296 | + <table width="100%" border="1"> |
2297 | + <tr> |
2298 | + <th>Goal</th> |
2299 | + <th>Target</th> |
2300 | + <th>Current</th> |
2301 | + <th>Completeness</th> |
2302 | + </tr> |
2303 | + % for goal in ctx["goals"]: |
2304 | + <tr |
2305 | + % if goal.completeness >= 100: |
2306 | + style="font-weight:bold;" |
2307 | + % endif |
2308 | + > |
2309 | + <td>${goal.type_id.name}</td> |
2310 | + <td>${goal.target_goal} |
2311 | + % if goal.type_suffix: |
2312 | + ${goal.type_suffix} |
2313 | + % endif |
2314 | + </td> |
2315 | + <td>${goal.current} |
2316 | + % if goal.type_suffix: |
2317 | + ${goal.type_suffix} |
2318 | + % endif |
2319 | + </td> |
2320 | + <td>${goal.completeness} %</td> |
2321 | + </tr> |
2322 | + % endfor |
2323 | + </table>]]></field> |
2324 | + </record> |
2325 | + |
2326 | + <record id="email_template_goal_progress_group" model="email.template"> |
2327 | + <field name="name">Group Goal Progress</field> |
2328 | + <field name="body_html"><![CDATA[ |
2329 | + <header> |
2330 | + <strong>${object.name}</strong> |
2331 | + </header> |
2332 | + <p class="oe_grey">${object.report_header or ''}</p> |
2333 | + |
2334 | + % for planline in ctx['planlines_boards']: |
2335 | + <table width="100%" border="1"> |
2336 | + <tr> |
2337 | + <th colspan="4">${planline.goal_type.name}</th> |
2338 | + </tr> |
2339 | + <tr> |
2340 | + <th>#</th> |
2341 | + <th>Person</th> |
2342 | + <th>Completeness</th> |
2343 | + <th>Current</th> |
2344 | + </tr> |
2345 | + % for idx, goal in planline.board_goals: |
2346 | + % if idx < 3 or goal.user_id.id == user.id: |
2347 | + <tr |
2348 | + % if goal.completeness >= 100: |
2349 | + style="font-weight:bold;" |
2350 | + % endif |
2351 | + > |
2352 | + <td>${idx+1}</td> |
2353 | + <td>${goal.user_id.name}</td> |
2354 | + <td>${goal.completeness}%</td> |
2355 | + <td>${goal.current}/${goal.target_goal} |
2356 | + % if goal.type_suffix: |
2357 | + ${goal.type_suffix} |
2358 | + % endif |
2359 | + </td> |
2360 | + </tr> |
2361 | + % endif |
2362 | + % endfor |
2363 | + </table> |
2364 | + |
2365 | + <br/><br/> |
2366 | + |
2367 | + % endfor |
2368 | +]]></field> |
2369 | + </record> |
2370 | + </data> |
2371 | +</openerp> |
2372 | |
2373 | === added file 'gamification/goal_type_data.py' |
2374 | --- gamification/goal_type_data.py 1970-01-01 00:00:00 +0000 |
2375 | +++ gamification/goal_type_data.py 2013-06-28 14:51:48 +0000 |
2376 | @@ -0,0 +1,41 @@ |
2377 | +# -*- coding: utf-8 -*- |
2378 | +############################################################################## |
2379 | +# |
2380 | +# OpenERP, Open Source Management Solution |
2381 | +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) |
2382 | +# |
2383 | +# This program is free software: you can redistribute it and/or modify |
2384 | +# it under the terms of the GNU General Public License as published by |
2385 | +# the Free Software Foundation, either version 3 of the License, or |
2386 | +# (at your option) any later version. |
2387 | +# |
2388 | +# This program is distributed in the hope that it will be useful, |
2389 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2390 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2391 | +# GNU General Public License for more details. |
2392 | +# |
2393 | +# You should have received a copy of the GNU General Public License |
2394 | +# along with this program. If not, see <http://www.gnu.org/licenses/> |
2395 | +# |
2396 | +############################################################################## |
2397 | + |
2398 | +from openerp.osv import osv |
2399 | + |
2400 | +class gamification_goal_type_data(osv.Model): |
2401 | + """Goal type data |
2402 | + |
2403 | + Methods for more complex goals not possible with the 'sum' and 'count' mode. |
2404 | + Each method should return the value that will be set in the 'current' field |
2405 | + of a user's goal. The return type must be a float or integer. |
2406 | + """ |
2407 | + _inherit = 'gamification.goal.type' |
2408 | + |
2409 | +#TODO: is it usefull to have this method in a standalone file? why not directly in the computation field of the related goal type? |
2410 | + def number_following(self, cr, uid, xml_id="mail.thread", context=None): |
2411 | + """Return the number of 'xml_id' objects the user is following |
2412 | + |
2413 | + The model specified in 'xml_id' must inherit from mail.thread |
2414 | + """ |
2415 | + ref_obj = self.pool.get(xml_id) |
2416 | + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) |
2417 | + return ref_obj.search(cr, uid, [('message_follower_ids', '=', user.partner_id.id)], count=True, context=context) |
2418 | |
2419 | === added file 'gamification/goal_view.xml' |
2420 | --- gamification/goal_view.xml 1970-01-01 00:00:00 +0000 |
2421 | +++ gamification/goal_view.xml 2013-06-28 14:51:48 +0000 |
2422 | @@ -0,0 +1,310 @@ |
2423 | +<?xml version="1.0" encoding="UTF-8"?> |
2424 | +<openerp> |
2425 | + <data> |
2426 | + |
2427 | + <!-- Goal views --> |
2428 | + <record id="goal_list_action" model="ir.actions.act_window"> |
2429 | + <field name="name">Goals</field> |
2430 | + <field name="res_model">gamification.goal</field> |
2431 | + <field name="view_mode">tree,form,kanban</field> |
2432 | + <field name="context">{'search_default_group_by_user': True, 'search_default_group_by_type': True}</field> |
2433 | + <field name="help" type="html"> |
2434 | + <p class="oe_view_nocontent_create"> |
2435 | + Click to create a goal. |
2436 | + </p> |
2437 | + <p> |
2438 | + A goal is defined by a user and a goal type. |
2439 | + Goals can be created automatically by using goal plans. |
2440 | + </p> |
2441 | + </field> |
2442 | + </record> |
2443 | + |
2444 | + <record id="goal_list_view" model="ir.ui.view"> |
2445 | + <field name="name">Goal List</field> |
2446 | + <field name="model">gamification.goal</field> |
2447 | + <field name="arch" type="xml"> |
2448 | + <tree string="Goal List" colors="red:state == 'failed';green:state == 'reached';grey:state == 'canceled'"> |
2449 | + <field name="type_id" invisible="1" /> |
2450 | + <field name="user_id" invisible="1" /> |
2451 | + <field name="start_date"/> |
2452 | + <field name="end_date"/> |
2453 | + <field name="current"/> |
2454 | + <field name="target_goal"/> |
2455 | + <field name="completeness" widget="progressbar"/> |
2456 | + <field name="state" invisible="1"/> |
2457 | + <field name="planline_id" invisible="1"/> |
2458 | + </tree> |
2459 | + </field> |
2460 | + </record> |
2461 | + |
2462 | + <record id="goal_form_view" model="ir.ui.view"> |
2463 | + <field name="name">Goal Form</field> |
2464 | + <field name="model">gamification.goal</field> |
2465 | + <field name="arch" type="xml"> |
2466 | + <form string="Goal" version="7.0"> |
2467 | + <header> |
2468 | + <button string="Start goal" type="object" name="action_start" states="draft" class="oe_highlight"/> |
2469 | + |
2470 | + <button string="Goal Reached" type="object" name="action_reach" states="inprogress,inprogress_update" /> |
2471 | + <button string="Goal Failed" type="object" name="action_fail" states="inprogress,inprogress_update"/> |
2472 | + <button string="Reset Completion" type="object" name="action_cancel" states="failed,reached" groups="base.group_no_one" /> |
2473 | + <field name="state" widget="statusbar" statusbar_visible="draft,inprogress,reached" /> |
2474 | + </header> |
2475 | + <sheet> |
2476 | + <group> |
2477 | + <group string="Reference"> |
2478 | + <field name="type_id" on_change="on_change_type_id(type_id)" attrs="{'readonly':[('state','!=','draft')]}"/> |
2479 | + <field name="user_id" attrs="{'readonly':[('state','!=','draft')]}"/> |
2480 | + <field name="plan_id" attrs="{'readonly':[('state','!=','draft')]}"/> |
2481 | + </group> |
2482 | + <group string="Schedule"> |
2483 | + <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/> |
2484 | + <field name="end_date" /> |
2485 | + <field name="computation_mode" invisible="1"/> |
2486 | + |
2487 | + <label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/> |
2488 | + <div attrs="{'invisible':[('computation_mode','!=', 'manually')]}"> |
2489 | + <field name="remind_update_delay" class="oe_inline"/> |
2490 | + days |
2491 | + </div> |
2492 | + <field name="last_update" groups="base.group_no_one"/> |
2493 | + </group> |
2494 | + <group string="Data" colspan="4"> |
2495 | + <label for="target_goal" /> |
2496 | + <div> |
2497 | + <field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/> |
2498 | + <field name="type_suffix" class="oe_inline"/> |
2499 | + </div> |
2500 | + <label for="current" /> |
2501 | + <div> |
2502 | + <field name="current" class="oe_inline"/> |
2503 | + <button string="refresh" type="object" name="update" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" /> |
2504 | + <div class="oe_grey" attrs="{'invisible':[('type_id', '=', False)]}"> |
2505 | + Reached when current value is <strong><field name="type_condition" class="oe_inline"/></strong> than the target. |
2506 | + </div> |
2507 | + </div> |
2508 | + </group> |
2509 | + </group> |
2510 | + </sheet> |
2511 | + <div class="oe_chatter"> |
2512 | + <field name="message_follower_ids" widget="mail_followers"/> |
2513 | + <field name="message_ids" widget="mail_thread"/> |
2514 | + </div> |
2515 | + </form> |
2516 | + </field> |
2517 | + </record> |
2518 | + |
2519 | + <record id="goal_search_view" model="ir.ui.view"> |
2520 | + <field name="name">Goal Search</field> |
2521 | + <field name="model">gamification.goal</field> |
2522 | + <field name="arch" type="xml"> |
2523 | + <search string="Search Goals"> |
2524 | + <filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/> |
2525 | + <separator/> |
2526 | + <filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/> |
2527 | + <filter name="inprogress" string="Current" |
2528 | + domain="[ |
2529 | + '|', |
2530 | + ('state', 'in', ('inprogress', 'inprogress_update')), |
2531 | + ('end_date', '>=', context_today().strftime('%%Y-%%m-%%d')) |
2532 | + ]"/> |
2533 | + <filter name="closed" string="Passed" domain="[('state', 'in', ('reached', 'failed'))]"/> |
2534 | + <separator/> |
2535 | + |
2536 | + <field name="user_id"/> |
2537 | + <field name="type_id"/> |
2538 | + <field name="plan_id"/> |
2539 | + <group expand="0" string="Group By..."> |
2540 | + <filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/> |
2541 | + <filter name="group_by_type" string="Goal Type" domain="[]" context="{'group_by':'type_id'}"/> |
2542 | + <filter string="State" domain="[]" context="{'group_by':'state'}"/> |
2543 | + <filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/> |
2544 | + </group> |
2545 | + </search> |
2546 | + </field> |
2547 | + </record> |
2548 | + |
2549 | + <record id="goal_kanban_view" model="ir.ui.view" > |
2550 | + <field name="name">Goal Kanban View</field> |
2551 | + <field name="model">gamification.goal</field> |
2552 | + <field name="arch" type="xml"> |
2553 | + <kanban version="7.0" class="oe_background_grey"> |
2554 | + <field name="type_id"/> |
2555 | + <field name="user_id"/> |
2556 | + <field name="current"/> |
2557 | + <field name="completeness"/> |
2558 | + <field name="state"/> |
2559 | + <field name="target_goal"/> |
2560 | + <field name="type_condition"/> |
2561 | + <field name="type_suffix"/> |
2562 | + <field name="type_display"/> |
2563 | + <field name="start_date"/> |
2564 | + <field name="end_date"/> |
2565 | + <field name="last_update"/> |
2566 | + <templates> |
2567 | + <t t-name="kanban-tooltip"> |
2568 | + <field name="type_description"/> |
2569 | + </t> |
2570 | + <t t-name="kanban-box"> |
2571 | + <div t-attf-class="oe_kanban_card oe_gamification_goal oe_kanban_goal #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}"> |
2572 | + <div class="oe_kanban_content"> |
2573 | + <p><h4 class="oe_goal_name" tooltip="kanban-tooltip"><field name="type_id" /></h4></p> |
2574 | + <div class="oe_kanban_left"> |
2575 | + <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" /> |
2576 | + </div> |
2577 | + <field name="user_id" /> |
2578 | + <div class="oe_goal_state_block"> |
2579 | + <t t-if="record.type_display.raw_value == 'checkbox'"> |
2580 | + <div class="oe_goal_state oe_e"> |
2581 | + <t t-if="record.state.raw_value=='reached'"><span class="oe_green" title="Goal Reached">W</span></t> |
2582 | + <t t-if="record.state.raw_value=='inprogress' || record.state.raw_value=='inprogress_update'"><span title="Goal in Progress">N</span></t> |
2583 | + <t t-if="record.state.raw_value=='failed'"><span class="oe_red" title="Goal Failed">X</span></t> |
2584 | + </div> |
2585 | + </t> |
2586 | + <t t-if="record.type_display.raw_value == 'progress'"> |
2587 | + <t t-if="record.type_condition.raw_value =='higher'"> |
2588 | + <field name="current" widget="goal" options="{'max_field': 'target_goal', 'label_field': 'type_suffix'}"/> |
2589 | + </t> |
2590 | + <t t-if="record.type_condition.raw_value != 'higher'"> |
2591 | + <div t-attf-class="oe_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'oe_orange' : record.current.raw_value > record.target_goal.raw_value ? 'oe_red' : 'oe_green'}"> |
2592 | + <t t-esc="record.current.raw_value" /> |
2593 | + </div> |
2594 | + <em>Target: less than <t t-esc="record.target_goal.raw_value" /></em> |
2595 | + </t> |
2596 | + </t> |
2597 | + |
2598 | + </div> |
2599 | + <p> |
2600 | + <t t-if="record.start_date.value"> |
2601 | + From <t t-esc="record.start_date.value" /> |
2602 | + </t> |
2603 | + <t t-if="record.end_date.value"> |
2604 | + To <t t-esc="record.end_date.value" /> |
2605 | + </t> |
2606 | + </p> |
2607 | + </div> |
2608 | + </div> |
2609 | + </t> |
2610 | + </templates> |
2611 | + </kanban> |
2612 | + </field> |
2613 | + </record> |
2614 | + |
2615 | + |
2616 | + <!-- Goal types view --> |
2617 | + |
2618 | + <record id="goal_type_list_action" model="ir.actions.act_window"> |
2619 | + <field name="name">Goal Types</field> |
2620 | + <field name="res_model">gamification.goal.type</field> |
2621 | + <field name="view_mode">tree,form</field> |
2622 | + <field name="help" type="html"> |
2623 | + <p class="oe_view_nocontent_create"> |
2624 | + Click to create a goal type. |
2625 | + </p> |
2626 | + <p> |
2627 | + A goal type is a technical model of goal defining a condition to reach. |
2628 | + The dates, values to reach or users are defined in goal instance. |
2629 | + </p> |
2630 | + </field> |
2631 | + </record> |
2632 | + |
2633 | + <record id="goal_type_list_view" model="ir.ui.view"> |
2634 | + <field name="name">Goal Types List</field> |
2635 | + <field name="model">gamification.goal.type</field> |
2636 | + <field name="arch" type="xml"> |
2637 | + <tree string="Goal types"> |
2638 | + <field name="sequence" widget="handle"/> |
2639 | + <field name="name"/> |
2640 | + <field name="computation_mode"/> |
2641 | + </tree> |
2642 | + </field> |
2643 | + </record> |
2644 | + |
2645 | + |
2646 | + <record id="goal_type_form_view" model="ir.ui.view"> |
2647 | + <field name="name">Goal Types Form</field> |
2648 | + <field name="model">gamification.goal.type</field> |
2649 | + <field name="arch" type="xml"> |
2650 | + <form string="Goal types" version="7.0"> |
2651 | + <sheet> |
2652 | + <label for="name" class="oe_edit_only"/> |
2653 | + <h1> |
2654 | + <field name="name" class="oe_inline"/> |
2655 | + </h1> |
2656 | + <label for="description" class="oe_edit_only"/> |
2657 | + <div> |
2658 | + <field name="description" class="oe_inline"/> |
2659 | + </div> |
2660 | + |
2661 | + <group string="How to compute the goal?"> |
2662 | + |
2663 | + <field widget="radio" name="computation_mode"/> |
2664 | + |
2665 | + <!-- Hide the fields below if manually --> |
2666 | + <field name="model_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/> |
2667 | + <field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" class="oe_inline"/> |
2668 | + <field name="field_date_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]" class="oe_inline"/> |
2669 | + <field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/> |
2670 | + <field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. self.my_method(cr, uid)"/> |
2671 | + <field name="condition" widget="radio"/> |
2672 | + </group> |
2673 | + <group string="Formating Options"> |
2674 | + <field name="display_mode" widget="radio" /> |
2675 | + <field name="suffix" placeholder="e.g. days"/> |
2676 | + <field name="monetary"/> |
2677 | + </group> |
2678 | + <group string="Clickable Goals"> |
2679 | + <field name="action_id" class="oe_inline"/> |
2680 | + <field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/> |
2681 | + </group> |
2682 | + |
2683 | + </sheet> |
2684 | + </form> |
2685 | + </field> |
2686 | + </record> |
2687 | + |
2688 | + <record id="goal_type_search_view" model="ir.ui.view"> |
2689 | + <field name="name">Goal Type Search</field> |
2690 | + <field name="model">gamification.goal.type</field> |
2691 | + <field name="arch" type="xml"> |
2692 | + <search string="Search Goal Types"> |
2693 | + <field name="name"/> |
2694 | + <field name="model_id"/> |
2695 | + <field name="field_id"/> |
2696 | + <group expand="0" string="Group By..."> |
2697 | + <filter string="Model" domain="[]" context="{'group_by':'model_id'}"/> |
2698 | + <filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/> |
2699 | + </group> |
2700 | + </search> |
2701 | + </field> |
2702 | + </record> |
2703 | + |
2704 | + |
2705 | + <record id="view_goal_wizard_update_current" model="ir.ui.view"> |
2706 | + <field name="name">Update the current value of the Goal</field> |
2707 | + <field name="model">gamification.goal.wizard</field> |
2708 | + <field name="arch" type="xml"> |
2709 | + <form string="Grant Badge To" version="7.0"> |
2710 | + Set the current value you have reached for this goal |
2711 | + <group> |
2712 | + <field name="goal_id" invisible="1"/> |
2713 | + <field name="current" /> |
2714 | + </group> |
2715 | + <footer> |
2716 | + <button string="Update" type="object" name="action_update_current" class="oe_highlight" /> or |
2717 | + <button string="Cancel" special="cancel" class="oe_link"/> |
2718 | + </footer> |
2719 | + </form> |
2720 | + </field> |
2721 | + </record> |
2722 | + |
2723 | + |
2724 | + <!-- menus in settings - technical feature required --> |
2725 | + <menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" /> |
2726 | + <menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/> |
2727 | + <menuitem id="gamification_plan_menu" parent="gamification_menu" action="goal_plan_list_action" sequence="10"/> |
2728 | + <menuitem id="gamification_type_menu" parent="gamification_menu" action="goal_type_list_action" sequence="20"/> |
2729 | + <menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/> |
2730 | + |
2731 | + </data> |
2732 | +</openerp> |
2733 | |
2734 | === added directory 'gamification/html' |
2735 | === added file 'gamification/html/index.html' |
2736 | --- gamification/html/index.html 1970-01-01 00:00:00 +0000 |
2737 | +++ gamification/html/index.html 2013-06-28 14:51:48 +0000 |
2738 | @@ -0,0 +1,86 @@ |
2739 | +<section class="oe_container"> |
2740 | + <div class="oe_row oe_spaced"> |
2741 | + <div class="oe_span12"> |
2742 | + <h2 class="oe_slogan">Drive Engagement with Gamification</h2> |
2743 | + <h3 class="oe_slogan">Leverage natural desire for competition</h3> |
2744 | + <p class="oe_mt32"> |
2745 | + Reinforce good habits and improve win rates with real-time recognition and rewards inspired by <a href="http://en.wikipedia.org/wiki/Gamification">game mechanics</a>. Align teams around clear business objectives with challenges, personal objectives and team leader boards. |
2746 | + </p> |
2747 | + <div class="oe_span4 oe_centered"> |
2748 | + <h3>Leaderboards</h3> |
2749 | + <div class="oe_row_img oe_centered"> |
2750 | + <img class="oe_picture" src="crm_game_01.png"> |
2751 | + </div> |
2752 | + <p> |
2753 | + Promote leaders and competition amongst sales team with performance ratios. |
2754 | + </p> |
2755 | + </div> |
2756 | + <div class="oe_span4 oe_centered"> |
2757 | + <h3>Personnal Objectives</h3> |
2758 | + <div class="oe_row_img"> |
2759 | + <img class="oe_picture" src="crm_game_02.png"> |
2760 | + </div> |
2761 | + <p> |
2762 | + Assign clear goals to users to align them with the company objectives. |
2763 | + </p> |
2764 | + </div> |
2765 | + <div class="oe_span4 oe_centered"> |
2766 | + <h3>Visual Information</h3> |
2767 | + <div class="oe_row_img oe_centered"> |
2768 | + <img class="oe_picture" src="crm_game_03.png"> |
2769 | + </div> |
2770 | + <p> |
2771 | + See in an glance the progress of each user. |
2772 | + </p> |
2773 | + </div> |
2774 | + </div> |
2775 | +</section> |
2776 | + |
2777 | + |
2778 | +<section class="oe_container oe_dark"> |
2779 | + <div class="oe_row oe_spaced"> |
2780 | + <h2 class="oe_slogan">Create custom Challenges</h2> |
2781 | + <div class="oe_span6"> |
2782 | + <p class="oe_mt32"> |
2783 | +Use predefined goals to generate easily your own challenges. Assign it to a team or individual users. Receive feedback as often as needed: daily, weekly... Repeat it automatically to compare progresses through time. |
2784 | + </p> |
2785 | + </div> |
2786 | + <div class="oe_span6"> |
2787 | + <div class="oe_row_img oe_centered"> |
2788 | + <img class="oe_picture oe_screenshot" src="crm_sc_05.png"> |
2789 | + </div> |
2790 | + </div> |
2791 | + </div> |
2792 | +</section> |
2793 | + |
2794 | +<section class="oe_container"> |
2795 | + <div class="oe_row oe_spaced"> |
2796 | + <h2 class="oe_slogan">Motivate with Badges</h2> |
2797 | + <div class="oe_span6"> |
2798 | + <div class="oe_row_img oe_centered"> |
2799 | + <img class="oe_picture" src="crm_linkedin.png"> |
2800 | + </div> |
2801 | + </div> |
2802 | + <div class="oe_span6"> |
2803 | + <p class="oe_mt32"> |
2804 | +Inspire achievement with recognition of coworker's good work by rewarding badges. These can be deserved manually or upon completion of challenges. Add fun to the competition with rare badges. |
2805 | + </p> |
2806 | + </div> |
2807 | + </div> |
2808 | +</section> |
2809 | + |
2810 | +<section class="oe_container oe_dark"> |
2811 | + <div class="oe_row oe_spaced"> |
2812 | + <h2 class="oe_slogan">Adapt to any module</h2> |
2813 | + <div class="oe_span6"> |
2814 | + <p class="oe_mt32"> |
2815 | +Create goals linked to any module. The evaluation system is very flexible and can be used for many different tasks : sales evaluation, creation of events, project completion or even helping new users to complete their profile. |
2816 | + </p> |
2817 | + </div> |
2818 | + <div class="oe_span6"> |
2819 | + <div class="oe_row_img oe_centered"> |
2820 | + <img class="oe_picture oe_screenshot" src="crm_sc_02.png"> |
2821 | + </div> |
2822 | + </div> |
2823 | + </div> |
2824 | +</section> |
2825 | |
2826 | === added file 'gamification/plan.py' |
2827 | --- gamification/plan.py 1970-01-01 00:00:00 +0000 |
2828 | +++ gamification/plan.py 2013-06-28 14:51:48 +0000 |
2829 | @@ -0,0 +1,804 @@ |
2830 | +# -*- coding: utf-8 -*- |
2831 | +############################################################################## |
2832 | +# |
2833 | +# OpenERP, Open Source Management Solution |
2834 | +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) |
2835 | +# |
2836 | +# This program is free software: you can redistribute it and/or modify |
2837 | +# it under the terms of the GNU General Public License as published by |
2838 | +# the Free Software Foundation, either version 3 of the License, or |
2839 | +# (at your option) any later version. |
2840 | +# |
2841 | +# This program is distributed in the hope that it will be useful, |
2842 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2843 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2844 | +# GNU General Public License for more details. |
2845 | +# |
2846 | +# You should have received a copy of the GNU General Public License |
2847 | +# along with this program. If not, see <http://www.gnu.org/licenses/> |
2848 | +# |
2849 | +############################################################################## |
2850 | + |
2851 | +from openerp.osv import fields, osv |
2852 | +from openerp.tools.translate import _ |
2853 | + |
2854 | +# from templates import TemplateHelper |
2855 | + |
2856 | +from datetime import date, datetime, timedelta |
2857 | +import calendar |
2858 | +import logging |
2859 | +_logger = logging.getLogger(__name__) |
2860 | + |
2861 | + |
2862 | +def start_end_date_for_period(period, default_start_date=False, default_end_date=False): |
2863 | + """Return the start and end date for a goal period based on today |
2864 | + |
2865 | + :return: (start_date, end_date), datetime.date objects, False if the period is |
2866 | + not defined or unknown""" |
2867 | + today = date.today() |
2868 | + if period == 'daily': |
2869 | + start_date = today |
2870 | + end_date = start_date |
2871 | + elif period == 'weekly': |
2872 | + delta = timedelta(days=today.weekday()) |
2873 | + start_date = today - delta |
2874 | + end_date = start_date + timedelta(days=7) |
2875 | + elif period == 'monthly': |
2876 | + month_range = calendar.monthrange(today.year, today.month) |
2877 | + start_date = today.replace(day=1) |
2878 | + end_date = today.replace(day=month_range[1]) |
2879 | + elif period == 'yearly': |
2880 | + start_date = today.replace(month=1, day=1) |
2881 | + end_date = today.replace(month=12, day=31) |
2882 | + else: # period == 'once': |
2883 | + start_date = default_start_date # for manual goal, start each time |
2884 | + end_date = default_end_date |
2885 | + |
2886 | + if start_date and end_date: |
2887 | + return (start_date.isoformat(), end_date.isoformat()) |
2888 | + else: |
2889 | + return (start_date, end_date) |
2890 | + |
2891 | + |
2892 | +class gamification_goal_plan(osv.Model): |
2893 | + """Gamification goal plan |
2894 | + |
2895 | + Set of predifined goals to be able to automate goal settings or |
2896 | + quickly apply several goals manually to a group of users |
2897 | + |
2898 | + If 'user_ids' is defined and 'period' is different than 'one', the set will |
2899 | + be assigned to the users for each period (eg: every 1st of each month if |
2900 | + 'monthly' is selected) |
2901 | + """ |
2902 | + |
2903 | + _name = 'gamification.goal.plan' |
2904 | + _description = 'Gamification goal plan' |
2905 | + _inherit = 'mail.thread' |
2906 | + |
2907 | + def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None): |
2908 | + """Return the next report date based on the last report date and report |
2909 | + period. |
2910 | + |
2911 | + :return: a string in isoformat representing the date""" |
2912 | + res = {} |
2913 | + for plan in self.browse(cr, uid, ids, context): |
2914 | + last = datetime.strptime(plan.last_report_date, '%Y-%m-%d').date() |
2915 | + if plan.report_message_frequency == 'daily': |
2916 | + next = last + timedelta(days=1) |
2917 | + res[plan.id] = next.isoformat() |
2918 | + elif plan.report_message_frequency == 'weekly': |
2919 | + next = last + timedelta(days=7) |
2920 | + res[plan.id] = next.isoformat() |
2921 | + elif plan.report_message_frequency == 'monthly': |
2922 | + month_range = calendar.monthrange(last.year, last.month) |
2923 | + next = last.replace(day=month_range[1]) + timedelta(days=1) |
2924 | + res[plan.id] = next.isoformat() |
2925 | + elif plan.report_message_frequency == 'yearly': |
2926 | + res[plan.id] = last.replace(year=last.year + 1).isoformat() |
2927 | + # frequency == 'once', reported when closed only |
2928 | + else: |
2929 | + res[plan.id] = False |
2930 | + |
2931 | + return res |
2932 | + |
2933 | + def _planline_count(self, cr, uid, ids, field_name, arg, context=None): |
2934 | + res = dict.fromkeys(ids, 0) |
2935 | + for plan in self.browse(cr, uid, ids, context): |
2936 | + res[plan.id] = len(plan.planline_ids) |
2937 | + return res |
2938 | + |
2939 | + _columns = { |
2940 | + 'name': fields.char('Challenge Name', required=True, translate=True), |
2941 | + 'description': fields.text('Description', translate=True), |
2942 | + 'state': fields.selection([ |
2943 | + ('draft', 'Draft'), |
2944 | + ('inprogress', 'In Progress'), |
2945 | + ('done', 'Done'), |
2946 | + ], |
2947 | + string='State', |
2948 | + required=True), |
2949 | + 'manager_id': fields.many2one('res.users', |
2950 | + string='Responsible', help="The user responsible for the challenge."), |
2951 | + |
2952 | + 'user_ids': fields.many2many('res.users', 'user_ids', |
2953 | + string='Users', |
2954 | + help="List of users to which the goal will be set"), |
2955 | + 'autojoin_group_id': fields.many2one('res.groups', |
2956 | + string='Auto-subscription Group', |
2957 | + help='Group of users whose members will automatically be added to the users'), |
2958 | + |
2959 | + 'period': fields.selection([ |
2960 | + ('once', 'Non recurring'), |
2961 | + ('daily', 'Daily'), |
2962 | + ('weekly', 'Weekly'), |
2963 | + ('monthly', 'Monthly'), |
2964 | + ('yearly', 'Yearly') |
2965 | + ], |
2966 | + string='Periodicity', |
2967 | + help='Period of automatic goal assigment. If none is selected, should be launched manually.', |
2968 | + required=True), |
2969 | + 'start_date': fields.date('Start Date', |
2970 | + help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."), |
2971 | + 'end_date': fields.date('End Date', |
2972 | + help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."), |
2973 | + |
2974 | + 'proposed_user_ids': fields.many2many('res.users', 'proposed_user_ids', |
2975 | + string="Suggest to users"), |
2976 | + |
2977 | + 'planline_ids': fields.one2many('gamification.goal.planline', 'plan_id', |
2978 | + string='Planline', |
2979 | + help="List of goals that will be set", |
2980 | + required=True), |
2981 | + 'planline_count': fields.function(_planline_count, type='integer', string="Planlines"), |
2982 | + |
2983 | + 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"), |
2984 | + 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"), |
2985 | + 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"), |
2986 | + 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"), |
2987 | + 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'), |
2988 | + |
2989 | + 'visibility_mode': fields.selection([ |
2990 | + ('progressbar', 'Individual Goals'), |
2991 | + ('board', 'Leader Board (Group Ranking)'), |
2992 | + ], |
2993 | + string="Display Mode", required=True), |
2994 | + 'report_message_frequency': fields.selection([ |
2995 | + ('never', 'Never'), |
2996 | + ('onchange', 'On change'), |
2997 | + ('daily', 'Daily'), |
2998 | + ('weekly', 'Weekly'), |
2999 | + ('monthly', 'Monthly'), |
3000 | + ('yearly', 'Yearly') |
3001 | + ], |
3002 | + string="Report Frequency", required=True), |
3003 | + 'report_message_group_id': fields.many2one('mail.group', |
3004 | + string='Send a copy to', |
3005 | + help='Group that will receive a copy of the report in addition to the user'), |
3006 | + 'report_header': fields.text('Report Header'), |
3007 | + 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after', |
3008 | + help="Never reminded if no value or zero is specified."), |
3009 | + 'last_report_date': fields.date('Last Report Date'), |
3010 | + 'next_report_date': fields.function(_get_next_report_date, |
3011 | + type='date', |
3012 | + string='Next Report Date'), |
3013 | + |
3014 | + 'category': fields.selection([ |
3015 | + ('hr', 'Human Ressources / Engagement'), |
3016 | + ('other', 'Settings / Gamification Tools'), |
3017 | + ], |
3018 | + string="Appears in", help="Define the visibility of the challenge through menus", required=True), |
3019 | + } |
3020 | + |
3021 | + _defaults = { |
3022 | + 'period': 'once', |
3023 | + 'state': 'draft', |
3024 | + 'visibility_mode' : 'progressbar', |
3025 | + 'report_message_frequency' : 'onchange', |
3026 | + 'last_report_date': fields.date.today, |
3027 | + 'start_date': fields.date.today, |
3028 | + 'manager_id': lambda s, cr, uid, c: uid, |
3029 | + 'category': 'hr', |
3030 | + 'reward_failure': False, |
3031 | + } |
3032 | + |
3033 | + _sort = 'end_date, start_date, name' |
3034 | + |
3035 | + def write(self, cr, uid, ids, vals, context=None): |
3036 | + """Overwrite the write method to add the user of groups""" |
3037 | + context = context or {} |
3038 | + if not ids: |
3039 | + return True |
3040 | + |
3041 | + # unsubscribe removed users from the plan |
3042 | + # users are not able to manually unsubscribe to challenges so should |
3043 | + # do it for them when not concerned anymore |
3044 | + if vals.get('user_ids'): |
3045 | + for action_tuple in vals['user_ids']: |
3046 | + if action_tuple[0] == 3: |
3047 | + # form (3, ID), remove one |
3048 | + self.message_unsubscribe_users(cr, uid, ids, [action_tuple[1]], context=context) |
3049 | + if action_tuple[0] == 5: |
3050 | + # form (5,), remove all |
3051 | + for plan in self.browse(cr, uid, ids, context=context): |
3052 | + self.message_unsubscribe_users(cr, uid, [plan.id], [user.id for user in plan.user_ids], context=context) |
3053 | + if action_tuple[0] == 6: |
3054 | + # form (6, False, [IDS]), replace by IDS |
3055 | + for plan in self.browse(cr, uid, ids, context=context): |
3056 | + removed_users = set([user.id for user in plan.user_ids]) - set(action_tuple[2]) |
3057 | + self.message_unsubscribe_users(cr, uid, [plan.id], list(removed_users), context=context) |
3058 | + |
3059 | + write_res = super(gamification_goal_plan, self).write(cr, uid, ids, vals, context=context) |
3060 | + |
3061 | + # add users when change the group auto-subscription |
3062 | + if 'autojoin_group_id' in vals: |
3063 | + new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) |
3064 | + group_user_ids = [user.id for user in new_group.users] |
3065 | + for plan in self.browse(cr, uid, ids, context=context): |
3066 | + self.write(cr, uid, [plan.id], {'user_ids': [(4, user) for user in group_user_ids]}, context=context) |
3067 | + |
3068 | + # subscribe new users to the plan |
3069 | + if 'user_ids' in vals: |
3070 | + for plan in self.browse(cr, uid, ids, context=context): |
3071 | + self.message_subscribe_users(cr, uid, ids, [user.id for user in plan.user_ids], context=context) |
3072 | + return write_res |
3073 | + |
3074 | + ##### Update ##### |
3075 | + |
3076 | + def _cron_update(self, cr, uid, context=None, ids=False): |
3077 | + """Daily cron check. |
3078 | + |
3079 | + Start planned plans (in draft and with start_date = today) |
3080 | + Create the goals for planlines not linked to goals (eg: modified the |
3081 | + plan to add planlines) |
3082 | + Update every plan running |
3083 | + """ |
3084 | + if not context: context = {} |
3085 | + |
3086 | + # start planned plans |
3087 | + planned_plan_ids = self.search(cr, uid, [ |
3088 | + ('state', '=', 'draft'), |
3089 | + ('start_date', '<=', fields.date.today())]) |
3090 | + self.action_start(cr, uid, planned_plan_ids, context=context) |
3091 | + |
3092 | + # close planned plans |
3093 | + planned_plan_ids = self.search(cr, uid, [ |
3094 | + ('state', '=', 'inprogress'), |
3095 | + ('end_date', '>=', fields.date.today())]) |
3096 | + self.action_close(cr, uid, planned_plan_ids, context=context) |
3097 | + |
3098 | + if not ids: |
3099 | + ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context) |
3100 | + |
3101 | + return self._update_all(cr, uid, ids, context=context) |
3102 | + |
3103 | + def _update_all(self, cr, uid, ids, context=None): |
3104 | + """Update the plans and related goals |
3105 | + |
3106 | + :param list(int) ids: the ids of the plans to update, if False will |
3107 | + update only plans in progress.""" |
3108 | + if not context: context = {} |
3109 | + goal_obj = self.pool.get('gamification.goal') |
3110 | + |
3111 | + # we use yesterday to update the goals that just ended |
3112 | + yesterday = date.today() - timedelta(days=1) |
3113 | + goal_ids = goal_obj.search(cr, uid, [ |
3114 | + ('plan_id', 'in', ids), |
3115 | + '|', |
3116 | + ('state', 'in', ('inprogress', 'inprogress_update')), |
3117 | + '&', |
3118 | + ('state', 'in', ('reached', 'failed')), |
3119 | + '|', |
3120 | + ('end_date', '>=', yesterday.isoformat()), |
3121 | + ('end_date', '=', False) |
3122 | + ], context=context) |
3123 | + # update every running goal already generated linked to selected plans |
3124 | + goal_obj.update(cr, uid, goal_ids, context=context) |
3125 | + |
3126 | + for plan in self.browse(cr, uid, ids, context=context): |
3127 | + if plan.autojoin_group_id: |
3128 | + # check in case of new users in plan, this happens if manager removed users in plan manually |
3129 | + self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context) |
3130 | + self.generate_goals_from_plan(cr, uid, [plan.id], context=context) |
3131 | + |
3132 | + # goals closed but still opened at the last report date |
3133 | + closed_goals_to_report = goal_obj.search(cr, uid, [ |
3134 | + ('plan_id', '=', plan.id), |
3135 | + ('start_date', '>=', plan.last_report_date), |
3136 | + ('end_date', '<=', plan.last_report_date) |
3137 | + ]) |
3138 | + |
3139 | + if len(closed_goals_to_report) > 0: |
3140 | + # some goals need a final report |
3141 | + self.report_progress(cr, uid, plan, subset_goal_ids=closed_goals_to_report, context=context) |
3142 | + |
3143 | + if fields.date.today() == plan.next_report_date: |
3144 | + self.report_progress(cr, uid, plan, context=context) |
3145 | + |
3146 | + self.check_challenge_reward(cr, uid, ids, context=context) |
3147 | + return True |
3148 | + |
3149 | + def quick_update(self, cr, uid, plan_id, context=None): |
3150 | + """Update all the goals of a plan, no generation of new goals""" |
3151 | + if not context: context = {} |
3152 | + plan = self.browse(cr, uid, plan_id, context=context) |
3153 | + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', '=', plan_id)], context=context) |
3154 | + self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context) |
3155 | + return True |
3156 | + |
3157 | + ##### User actions ##### |
3158 | + |
3159 | + def action_start(self, cr, uid, ids, context=None): |
3160 | + """Start a draft goal plan |
3161 | + |
3162 | + Change the state of the plan to in progress and generate related goals |
3163 | + """ |
3164 | + # subscribe users if autojoin group |
3165 | + for plan in self.browse(cr, uid, ids, context=context): |
3166 | + if plan.autojoin_group_id: |
3167 | + self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context) |
3168 | + |
3169 | + self.write(cr, uid, plan.id, {'state': 'inprogress'}, context=context) |
3170 | + self.message_post(cr, uid, plan.id, body="New challenge started.", context=context) |
3171 | + return self.generate_goals_from_plan(cr, uid, ids, context=context) |
3172 | + |
3173 | + def action_check(self, cr, uid, ids, context=None): |
3174 | + """Check a goal plan |
3175 | + |
3176 | + Create goals that haven't been created yet (eg: if added users of planlines) |
3177 | + Recompute the current value for each goal related""" |
3178 | + return self._update_all(cr, uid, ids=ids, context=context) |
3179 | + |
3180 | + def action_close(self, cr, uid, ids, context=None): |
3181 | + """Close a plan in progress |
3182 | + |
3183 | + Change the state of the plan to in done |
3184 | + Does NOT close the related goals, this is handled by the goal itself""" |
3185 | + self.check_challenge_reward(cr, uid, ids, force=True, context=context) |
3186 | + return self.write(cr, uid, ids, {'state': 'done'}, context=context) |
3187 | + |
3188 | + def action_reset(self, cr, uid, ids, context=None): |
3189 | + """Reset a closed goal plan |
3190 | + |
3191 | + Change the state of the plan to in progress |
3192 | + Closing a plan does not affect the goals so neither does reset""" |
3193 | + return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context) |
3194 | + |
3195 | + def action_cancel(self, cr, uid, ids, context=None): |
3196 | + """Cancel a plan in progress |
3197 | + |
3198 | + Change the state of the plan to draft |
3199 | + Cancel the related goals""" |
3200 | + self.write(cr, uid, ids, {'state': 'draft'}, context=context) |
3201 | + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', 'in', ids)], context=context) |
3202 | + self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context) |
3203 | + |
3204 | + return True |
3205 | + |
3206 | + def action_report_progress(self, cr, uid, ids, context=None): |
3207 | + """Manual report of a goal, does not influence automatic report frequency""" |
3208 | + for plan in self.browse(cr, uid, ids, context): |
3209 | + self.report_progress(cr, uid, plan, context=context) |
3210 | + return True |
3211 | + |
3212 | + ##### Automatic actions ##### |
3213 | + |
3214 | + def generate_goals_from_plan(self, cr, uid, ids, context=None): |
3215 | + """Generate the list of goals linked to a plan. |
3216 | + |
3217 | + If goals already exist for this planline, the planline is skipped. This |
3218 | + can be called after each change in the user or planline list. |
3219 | + :param list(int) ids: the list of plan concerned""" |
3220 | + |
3221 | + for plan in self.browse(cr, uid, ids, context): |
3222 | + (start_date, end_date) = start_end_date_for_period(plan.period) |
3223 | + |
3224 | + # if no periodicity, use plan dates |
3225 | + if not start_date and plan.start_date: |
3226 | + start_date = plan.start_date |
3227 | + if not end_date and plan.end_date: |
3228 | + end_date = plan.end_date |
3229 | + |
3230 | + for planline in plan.planline_ids: |
3231 | + for user in plan.user_ids: |
3232 | + |
3233 | + goal_obj = self.pool.get('gamification.goal') |
3234 | + domain = [('planline_id', '=', planline.id), ('user_id', '=', user.id)] |
3235 | + if start_date: |
3236 | + domain.append(('start_date', '=', start_date)) |
3237 | + |
3238 | + # goal already existing for this planline ? |
3239 | + if len(goal_obj.search(cr, uid, domain, context=context)) > 0: |
3240 | + |
3241 | + # resume canceled goals |
3242 | + domain.append(('state', '=', 'canceled')) |
3243 | + canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context) |
3244 | + goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context) |
3245 | + goal_obj.update(cr, uid, canceled_goal_ids, context=context) |
3246 | + |
3247 | + # skip to next user |
3248 | + continue |
3249 | + |
3250 | + values = { |
3251 | + 'type_id': planline.type_id.id, |
3252 | + 'planline_id': planline.id, |
3253 | + 'user_id': user.id, |
3254 | + 'target_goal': planline.target_goal, |
3255 | + 'state': 'inprogress', |
3256 | + } |
3257 | + |
3258 | + if start_date: |
3259 | + values['start_date'] = start_date |
3260 | + if end_date: |
3261 | + values['end_date'] = end_date |
3262 | + |
3263 | + if planline.plan_id.remind_update_delay: |
3264 | + values['remind_update_delay'] = planline.plan_id.remind_update_delay |
3265 | + |
3266 | + new_goal_id = goal_obj.create(cr, uid, values, context) |
3267 | + |
3268 | + goal_obj.update(cr, uid, [new_goal_id], context=context) |
3269 | + |
3270 | + return True |
3271 | + |
3272 | + ##### JS utilities ##### |
3273 | + |
3274 | + def get_board_goal_info(self, cr, uid, plan, subset_goal_ids=False, context=None): |
3275 | + """Get the list of latest goals for a plan, sorted by user ranking for each planline""" |
3276 | + |
3277 | + goal_obj = self.pool.get('gamification.goal') |
3278 | + planlines_boards = [] |
3279 | + (start_date, end_date) = start_end_date_for_period(plan.period) |
3280 | + |
3281 | + for planline in plan.planline_ids: |
3282 | + |
3283 | + domain = [ |
3284 | + ('planline_id', '=', planline.id), |
3285 | + ('state', 'in', ('inprogress', 'inprogress_update', |
3286 | + 'reached', 'failed')), |
3287 | + ] |
3288 | + |
3289 | + if subset_goal_ids: |
3290 | + goal_ids = goal_obj.search(cr, uid, domain, context=context) |
3291 | + common_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids] |
3292 | + else: |
3293 | + # if no subset goals, use the dates for restriction |
3294 | + if start_date: |
3295 | + domain.append(('start_date', '=', start_date)) |
3296 | + if end_date: |
3297 | + domain.append(('end_date', '=', end_date)) |
3298 | + common_goal_ids = goal_obj.search(cr, uid, domain, context=context) |
3299 | + |
3300 | + board_goals = [goal for goal in goal_obj.browse(cr, uid, common_goal_ids, context=context)] |
3301 | + |
3302 | + if len(board_goals) == 0: |
3303 | + # planline has no generated goals |
3304 | + continue |
3305 | + |
3306 | + # most complete first, current if same percentage (eg: if several 100%) |
3307 | + sorted_board = enumerate(sorted(board_goals, key=lambda k: (k.completeness, k.current), reverse=True)) |
3308 | + planlines_boards.append({'goal_type': planline.type_id, 'board_goals': sorted_board, 'target_goal': planline.target_goal}) |
3309 | + return planlines_boards |
3310 | + |
3311 | + def get_indivual_goal_info(self, cr, uid, user_id, plan, subset_goal_ids=False, context=None): |
3312 | + """Get the list of latest goals of a user for a plan""" |
3313 | + domain = [ |
3314 | + ('plan_id', '=', plan.id), |
3315 | + ('user_id', '=', user_id), |
3316 | + ('state', 'in', ('inprogress', 'inprogress_update', |
3317 | + 'reached', 'failed')), |
3318 | + ] |
3319 | + goal_obj = self.pool.get('gamification.goal') |
3320 | + (start_date, end_date) = start_end_date_for_period(plan.period) |
3321 | + |
3322 | + if subset_goal_ids: |
3323 | + # use the domain for safety, don't want irrelevant report if wrong argument |
3324 | + goal_ids = goal_obj.search(cr, uid, domain, context=context) |
3325 | + related_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids] |
3326 | + else: |
3327 | + # if no subset goals, use the dates for restriction |
3328 | + if start_date: |
3329 | + domain.append(('start_date', '=', start_date)) |
3330 | + if end_date: |
3331 | + domain.append(('end_date', '=', end_date)) |
3332 | + related_goal_ids = goal_obj.search(cr, uid, domain, context=context) |
3333 | + |
3334 | + if len(related_goal_ids) == 0: |
3335 | + return False |
3336 | + |
3337 | + goals = [] |
3338 | + all_done = True |
3339 | + for goal in goal_obj.browse(cr, uid, related_goal_ids, context=context): |
3340 | + if goal.end_date: |
3341 | + if goal.end_date < fields.date.today(): |
3342 | + # do not include goals of previous plan run |
3343 | + continue |
3344 | + else: |
3345 | + all_done = False |
3346 | + else: |
3347 | + if goal.state == 'inprogress' or goal.state == 'inprogress_update': |
3348 | + all_done = False |
3349 | + |
3350 | + goals.append(goal) |
3351 | + |
3352 | + if all_done: |
3353 | + # skip plans where all goal are done or failed |
3354 | + return False |
3355 | + else: |
3356 | + return goals |
3357 | + |
3358 | + ##### Reporting ##### |
3359 | + |
3360 | + def report_progress(self, cr, uid, plan, context=None, users=False, subset_goal_ids=False): |
3361 | + """Post report about the progress of the goals |
3362 | + |
3363 | + :param plan: the plan object that need to be reported |
3364 | + :param users: the list(res.users) of users that are concerned by |
3365 | + the report. If False, will send the report to every user concerned |
3366 | + (goal users and group that receive a copy). Only used for plan with |
3367 | + a visibility mode set to 'personal'. |
3368 | + :param goal_ids: the list(int) of goal ids linked to the plan for |
3369 | + the report. If not specified, use the goals for the current plan |
3370 | + period. This parameter can be used to produce report for previous plan |
3371 | + periods. |
3372 | + :param subset_goal_ids: a list(int) of goal ids to restrict the report |
3373 | + """ |
3374 | + |
3375 | + context = context or {} |
3376 | + goal_obj = self.pool.get('gamification.goal') |
3377 | + # template_env = TemplateHelper() |
3378 | + temp_obj = self.pool.get('email.template') |
3379 | + ctx = context.copy() |
3380 | + if plan.visibility_mode == 'board': |
3381 | + planlines_boards = self.get_board_goal_info(cr, uid, plan, subset_goal_ids, context) |
3382 | + |
3383 | + ctx.update({'planlines_boards': planlines_boards}) |
3384 | + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context) |
3385 | + body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal.plan', plan.id, context=context) |
3386 | + |
3387 | + # body_html = template_env.get_template('group_progress.mako').render({'object': plan, 'planlines_boards': planlines_boards, 'uid': uid}) |
3388 | + |
3389 | + # send to every follower of the plan |
3390 | + self.message_post(cr, uid, plan.id, |
3391 | + body=body_html, |
3392 | + context=context, |
3393 | + subtype='mail.mt_comment') |
3394 | + if plan.report_message_group_id: |
3395 | + self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id, |
3396 | + body=body_html, |
3397 | + context=context, |
3398 | + subtype='mail.mt_comment') |
3399 | + |
3400 | + else: |
3401 | + # generate individual reports |
3402 | + for user in users or plan.user_ids: |
3403 | + goals = self.get_indivual_goal_info(cr, uid, user.id, plan, subset_goal_ids, context=context) |
3404 | + if not goals: |
3405 | + continue |
3406 | + |
3407 | + ctx.update({'goals': goals}) |
3408 | + template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context) |
3409 | + body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.goal.plan', plan.id, context=context) |
3410 | + # send message only to users |
3411 | + self.message_post(cr, uid, 0, |
3412 | + body=body_html, |
3413 | + partner_ids=[(4, user.partner_id.id)], |
3414 | + context=context, |
3415 | + subtype='mail.mt_comment') |
3416 | + if plan.report_message_group_id: |
3417 | + self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id, |
3418 | + body=body_html, |
3419 | + context=context, |
3420 | + subtype='mail.mt_comment') |
3421 | + return self.write(cr, uid, plan.id, {'last_report_date': fields.date.today()}, context=context) |
3422 | + |
3423 | + ##### Challenges ##### |
3424 | + |
3425 | + def accept_challenge(self, cr, uid, plan_ids, context=None, user_id=None): |
3426 | + """The user accept the suggested challenge""" |
3427 | + context = context or {} |
3428 | + user_id = user_id or uid |
3429 | + user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) |
3430 | + message = "%s has joined the challenge" % user.name |
3431 | + self.message_post(cr, uid, plan_ids, body=message, context=context) |
3432 | + self.write(cr, uid, plan_ids, {'proposed_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context) |
3433 | + return self.generate_goals_from_plan(cr, uid, plan_ids, context=context) |
3434 | + |
3435 | + def discard_challenge(self, cr, uid, plan_ids, context=None, user_id=None): |
3436 | + """The user discard the suggested challenge""" |
3437 | + context = context or {} |
3438 | + user_id = user_id or uid |
3439 | + user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) |
3440 | + message = "%s has refused the challenge" % user.name |
3441 | + self.message_post(cr, uid, plan_ids, body=message, context=context) |
3442 | + return self.write(cr, uid, plan_ids, {'proposed_user_ids': (3, user_id)}, context=context) |
3443 | + |
3444 | + def reply_challenge_wizard(self, cr, uid, plan_id, context=None): |
3445 | + context = context or {} |
3446 | + mod_obj = self.pool.get('ir.model.data') |
3447 | + act_obj = self.pool.get('ir.actions.act_window') |
3448 | + result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard') |
3449 | + id = result and result[1] or False |
3450 | + result = act_obj.read(cr, uid, [id], context=context)[0] |
3451 | + result['res_id'] = plan_id |
3452 | + return result |
3453 | + |
3454 | + def check_challenge_reward(self, cr, uid, plan_ids, force=False, context=None): |
3455 | + """Actions for the end of a challenge |
3456 | + |
3457 | + If a reward was selected, grant it to the correct users. |
3458 | + Rewards granted at: |
3459 | + - the end date for a challenge with no periodicity |
3460 | + - the end of a period for challenge with periodicity |
3461 | + - when a challenge is manually closed |
3462 | + (if no end date, a running challenge is never rewarded) |
3463 | + """ |
3464 | + context = context or {} |
3465 | + for plan in self.browse(cr, uid, plan_ids, context=context): |
3466 | + (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date) |
3467 | + yesterday = date.today() - timedelta(days=1) |
3468 | + if end_date == yesterday.isoformat() or force: |
3469 | + # open chatter message |
3470 | + message_body = _("The challenge %s is finished." % plan.name) |
3471 | + |
3472 | + # reward for everybody succeeding |
3473 | + rewarded_users = [] |
3474 | + if plan.reward_id: |
3475 | + for user in plan.user_ids: |
3476 | + reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [ |
3477 | + ('plan_id', '=', plan.id), |
3478 | + ('user_id', '=', user.id), |
3479 | + ('start_date', '=', start_date), |
3480 | + ('end_date', '=', end_date), |
3481 | + ('state', '=', 'reached') |
3482 | + ], context=context) |
3483 | + if len(reached_goal_ids) == len(plan.planline_ids): |
3484 | + self.reward_user(cr, uid, user.id, plan.reward_id.id, context) |
3485 | + rewarded_users.append(user) |
3486 | + |
3487 | + if rewarded_users: |
3488 | + message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (plan.reward_id.name, ", ".join([user.name for user in rewarded_users]))) |
3489 | + else: |
3490 | + message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.") |
3491 | + |
3492 | + # reward bests |
3493 | + if plan.reward_first_id: |
3494 | + (first_user, second_user, third_user) = self.get_top3_users(cr, uid, plan, context) |
3495 | + if first_user: |
3496 | + self.reward_user(cr, uid, first_user.id, plan.reward_first_id.id, context) |
3497 | + message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :") |
3498 | + message_body += "<br/> 1. %s - %s" % (first_user.name, plan.reward_first_id.name) |
3499 | + else: |
3500 | + message_body += _("Nobody reached the required conditions to receive special badges.") |
3501 | + |
3502 | + if second_user and plan.reward_second_id: |
3503 | + self.reward_user(cr, uid, second_user.id, plan.reward_second_id.id, context) |
3504 | + message_body += "<br/> 2. %s - %s" % (second_user.name, plan.reward_second_id.name) |
3505 | + if third_user and plan.reward_third_id: |
3506 | + self.reward_user(cr, uid, third_user.id, plan.reward_second_id.id, context) |
3507 | + message_body += "<br/> 3. %s - %s" % (third_user.name, plan.reward_third_id.name) |
3508 | + |
3509 | + self.message_post(cr, uid, plan.id, body=message_body, context=context) |
3510 | + return True |
3511 | + |
3512 | + def get_top3_users(self, cr, uid, plan, context=None): |
3513 | + """Get the top 3 users for a defined plan |
3514 | + |
3515 | + Ranking criterias: |
3516 | + 1. succeed every goal of the challenge |
3517 | + 2. total completeness of each goal (can be over 100) |
3518 | + Top 3 is computed only for users succeeding every goal of the challenge, |
3519 | + except if reward_failure is True, in which case every user is |
3520 | + considered. |
3521 | + :return: ('first', 'second', 'third'), tuple containing the res.users |
3522 | + objects of the top 3 users. If no user meets the criterias for a rank, |
3523 | + it is set to False. Nobody can receive a rank is noone receives the |
3524 | + higher one (eg: if 'second' == False, 'third' will be False) |
3525 | + """ |
3526 | + goal_obj = self.pool.get('gamification.goal') |
3527 | + (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date) |
3528 | + challengers = [] |
3529 | + for user in plan.user_ids: |
3530 | + all_reached = True |
3531 | + total_completness = 0 |
3532 | + # every goal of the user for the running period |
3533 | + goal_ids = goal_obj.search(cr, uid, [ |
3534 | + ('plan_id', '=', plan.id), |
3535 | + ('user_id', '=', user.id), |
3536 | + ('start_date', '=', start_date), |
3537 | + ('end_date', '=', end_date) |
3538 | + ], context=context) |
3539 | + for goal in goal_obj.browse(cr, uid, goal_ids, context=context): |
3540 | + if goal.state != 'reached': |
3541 | + all_reached = False |
3542 | + if goal.type_condition == 'higher': |
3543 | + # can be over 100 |
3544 | + total_completness += 100.0 * goal.current / goal.target_goal |
3545 | + elif goal.state == 'reached': |
3546 | + # for lower goals, can not get percentage so 0 or 100 |
3547 | + total_completness += 100 |
3548 | + |
3549 | + challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness}) |
3550 | + sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True) |
3551 | + |
3552 | + if len(sorted_challengers) == 0 or (not plan.reward_failure and not sorted_challengers[0]['all_reached']): |
3553 | + # nobody succeeded |
3554 | + return (False, False, False) |
3555 | + if len(sorted_challengers) == 1 or (not plan.reward_failure and not sorted_challengers[1]['all_reached']): |
3556 | + # only one user succeeded |
3557 | + return (sorted_challengers[0]['user'], False, False) |
3558 | + if len(sorted_challengers) == 2 or (not plan.reward_failure and not sorted_challengers[2]['all_reached']): |
3559 | + # only one user succeeded |
3560 | + return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False) |
3561 | + return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user']) |
3562 | + |
3563 | + def reward_user(self, cr, uid, user_id, badge_id, context=None): |
3564 | + """Create a badge user and send the badge to him""" |
3565 | + user_badge_id = self.pool.get('gamification.badge.user').create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context) |
3566 | + return self.pool.get('gamification.badge').send_badge(cr, uid, badge_id, [user_badge_id], user_from=None, context=context) |
3567 | + |
3568 | + |
3569 | +class gamification_goal_planline(osv.Model): |
3570 | + """Gamification goal planline |
3571 | + |
3572 | + Predifined goal for 'gamification_goal_plan' |
3573 | + These are generic list of goals with only the target goal defined |
3574 | + Should only be created for the gamification_goal_plan object |
3575 | + """ |
3576 | + |
3577 | + _name = 'gamification.goal.planline' |
3578 | + _description = 'Gamification generic goal for plan' |
3579 | + _order = "sequence, sequence_type, id" |
3580 | + |
3581 | + def _get_planline_types(self, cr, uid, ids, context=None): |
3582 | + """Return the ids of planline items related to the gamification.goal.type |
3583 | + objects in 'ids (used to update the value of 'sequence_type')'""" |
3584 | + |
3585 | + result = {} |
3586 | + for goal_type in self.pool.get('gamification.goal.type').browse(cr, uid, ids, context=context): |
3587 | + domain = [('type_id', '=', goal_type.id)] |
3588 | + planline_ids = self.pool.get('gamification.goal.planline').search(cr, uid, domain, context=context) |
3589 | + for p_id in planline_ids: |
3590 | + result[p_id] = True |
3591 | + return result.keys() |
3592 | + |
3593 | + def on_change_type_id(self, cr, uid, ids, type_id=False, context=None): |
3594 | + goal_type = self.pool.get('gamification.goal.type') |
3595 | + if not type_id: |
3596 | + return {'value': {'type_id': False}} |
3597 | + goal_type = goal_type.browse(cr, uid, type_id, context=context) |
3598 | + ret = {'value': { |
3599 | + 'type_condition': goal_type.condition, |
3600 | + 'type_full_suffix': goal_type.full_suffix}} |
3601 | + return ret |
3602 | + |
3603 | + _columns = { |
3604 | + 'name': fields.related('type_id', 'name', string="Name"), |
3605 | + 'plan_id': fields.many2one('gamification.goal.plan', |
3606 | + string='Plan', |
3607 | + required=True, |
3608 | + ondelete="cascade"), |
3609 | + 'type_id': fields.many2one('gamification.goal.type', |
3610 | + string='Goal Type', |
3611 | + required=True, |
3612 | + ondelete="cascade"), |
3613 | + 'target_goal': fields.float('Target Value to Reach', |
3614 | + required=True), |
3615 | + 'sequence': fields.integer('Sequence', |
3616 | + help='Sequence number for ordering'), |
3617 | + 'sequence_type': fields.related('type_id', 'sequence', |
3618 | + type='integer', |
3619 | + string='Sequence', |
3620 | + readonly=True, |
3621 | + store={ |
3622 | + 'gamification.goal.type': (_get_planline_types, ['sequence'], 10), |
3623 | + }), |
3624 | + 'type_condition': fields.related('type_id', 'condition', type="selection", |
3625 | + readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]), |
3626 | + 'type_suffix': fields.related('type_id', 'suffix', type="char", readonly=True, string="Unit"), |
3627 | + 'type_monetary': fields.related('type_id', 'monetary', type="boolean", readonly=True, string="Monetary"), |
3628 | + 'type_full_suffix': fields.related('type_id', 'full_suffix', type="char", readonly=True, string="Suffix"), |
3629 | + } |
3630 | + |
3631 | + _default = { |
3632 | + 'sequence': 1, |
3633 | + } |
3634 | |
3635 | === added file 'gamification/plan_view.xml' |
3636 | --- gamification/plan_view.xml 1970-01-01 00:00:00 +0000 |
3637 | +++ gamification/plan_view.xml 2013-06-28 14:51:48 +0000 |
3638 | @@ -0,0 +1,291 @@ |
3639 | +<?xml version="1.0" encoding="UTF-8"?> |
3640 | +<openerp> |
3641 | + <data> |
3642 | + |
3643 | + <record id="goal_plan_list_view" model="ir.ui.view"> |
3644 | + <field name="name">Challenges List</field> |
3645 | + <field name="model">gamification.goal.plan</field> |
3646 | + <field name="arch" type="xml"> |
3647 | + <tree string="Goal types" colors="blue:state == 'draft';grey:state == 'done'"> |
3648 | + <field name="name"/> |
3649 | + <field name="period"/> |
3650 | + <field name="manager_id"/> |
3651 | + <field name="state"/> |
3652 | + </tree> |
3653 | + </field> |
3654 | + </record> |
3655 | + |
3656 | + <record id="goals_from_plan_act" model="ir.actions.act_window"> |
3657 | + <field name="res_model">gamification.goal</field> |
3658 | + <field name="name">Related Goals</field> |
3659 | + <field name="view_mode">kanban,tree</field> |
3660 | + <field name="context">{'search_default_group_by_type': True, 'search_default_inprogress': True, 'search_default_plan_id': active_id, 'default_plan_id': active_id}</field> |
3661 | + <field name="help" type="html"> |
3662 | + <p> |
3663 | + There is no goals associated to this challenge matching your search. |
3664 | + Make sure that your challenge is active and assigned to at least one user. |
3665 | + </p> |
3666 | + </field> |
3667 | + </record> |
3668 | + |
3669 | + <record id="goal_plan_form_view" model="ir.ui.view"> |
3670 | + <field name="name">Challenge Form</field> |
3671 | + <field name="model">gamification.goal.plan</field> |
3672 | + <field name="arch" type="xml"> |
3673 | + <form string="Goal types" version="7.0"> |
3674 | + <header> |
3675 | + <button string="Start Now" type="object" name="action_start" states="draft" class="oe_highlight"/> |
3676 | + <button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/> |
3677 | + <button string="Close Challenge" type="object" name="action_close" states="inprogress" class="oe_highlight"/> |
3678 | + <button string="Reset to Draft" type="object" name="action_cancel" states="inprogress"/> |
3679 | + <button string="Reset Completion" type="object" name="action_reset" states="done"/> |
3680 | + <button string="Report Progress" type="object" name="action_report_progress" states="inprogress,done" groups="base.group_no_one"/> |
3681 | + <field name="state" widget="statusbar"/> |
3682 | + </header> |
3683 | + <sheet> |
3684 | + |
3685 | + <div class="oe_title"> |
3686 | + <label for="name" class="oe_edit_only"/> |
3687 | + <h1> |
3688 | + <field name="name" placeholder="e.g. Monthly Sales Objectives"/> |
3689 | + </h1> |
3690 | + <label for="user_ids" class="oe_edit_only" string="Assign Challenge To"/> |
3691 | + <div> |
3692 | + <field name="user_ids" widget="many2many_tags" /> |
3693 | + </div> |
3694 | + </div> |
3695 | + |
3696 | + <!-- action buttons --> |
3697 | + <div class="oe_right oe_button_box"> |
3698 | + <button type="action" name="%(goals_from_plan_act)d" string="Related Goals" attrs="{'invisible': [('state','=','draft')]}" /> |
3699 | + </div> |
3700 | + <group> |
3701 | + <group> |
3702 | + <field name="period" attrs="{'readonly':[('state','!=','draft')]}"/> |
3703 | + <field name="visibility_mode" widget="radio" colspan="1" /> |
3704 | + </group> |
3705 | + <group> |
3706 | + <field name="manager_id"/> |
3707 | + <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/> |
3708 | + <field name="end_date" attrs="{'readonly':[('state','!=','draft')]}"/> |
3709 | + </group> |
3710 | + </group> |
3711 | + <notebook> |
3712 | + <page string="Goals"> |
3713 | + <field name="planline_ids" nolabel="1" colspan="4"> |
3714 | + <tree string="Planline List" version="7.0" editable="bottom" > |
3715 | + <field name="sequence" widget="handle"/> |
3716 | + <field name="type_id" on_change="on_change_type_id(type_id)" /> |
3717 | + <field name="type_condition"/> |
3718 | + <field name="target_goal"/> |
3719 | + <field name="type_full_suffix"/> |
3720 | + </tree> |
3721 | + </field> |
3722 | + <field name="description" placeholder="Describe the challenge: what is does, who it targets, why it matters..."/> |
3723 | + </page> |
3724 | + <page string="Reward"> |
3725 | + <group> |
3726 | + <field name="reward_id"/> |
3727 | + <field name="reward_first_id" /> |
3728 | + <field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" /> |
3729 | + <field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" /> |
3730 | + <field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" /> |
3731 | + </group> |
3732 | + <div class="oe_grey"> |
3733 | + <p>Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.</p> |
3734 | + </div> |
3735 | + </page> |
3736 | + <page string="Advanced Options"> |
3737 | + <group string="Subscriptions"> |
3738 | + <field name="autojoin_group_id" /> |
3739 | + <field name="proposed_user_ids" widget="many2many_tags" /> |
3740 | + </group> |
3741 | + <group string="Notification Messages"> |
3742 | + <field name="report_message_frequency" /> |
3743 | + <field name="report_header" placeholder="e.g. The following message contains the current progress of the sale team..." attrs="{'invisible': [('report_message_frequency','=','never')]}" /> |
3744 | + <field name="report_message_group_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" /> |
3745 | + </group> |
3746 | + <group string="Reminders for Manual Goals"> |
3747 | + <label for="remind_update_delay" /> |
3748 | + <div> |
3749 | + <field name="remind_update_delay" class="oe_inline"/> days |
3750 | + </div> |
3751 | + </group> |
3752 | + <group string="Category" groups="base.group_no_one"> |
3753 | + <field name="category" widget="radio" /> |
3754 | + </group> |
3755 | + </page> |
3756 | + </notebook> |
3757 | + |
3758 | + </sheet> |
3759 | + <div class="oe_chatter"> |
3760 | + <field name="message_follower_ids" widget="mail_followers"/> |
3761 | + <field name="message_ids" widget="mail_thread"/> |
3762 | + </div> |
3763 | + </form> |
3764 | + </field> |
3765 | + </record> |
3766 | + |
3767 | + <record model="ir.ui.view" id="view_goal_plan_kanban"> |
3768 | + <field name="name">Challenge Kanban</field> |
3769 | + <field name="model">gamification.goal.plan</field> |
3770 | + <field name="arch" type="xml"> |
3771 | + <kanban version="7.0" class="oe_background_grey"> |
3772 | + <field name="planline_ids"/> |
3773 | + <field name="planline_count"/> |
3774 | + <field name="user_ids"/> |
3775 | + <templates> |
3776 | + <t t-name="kanban-box"> |
3777 | + <div t-attf-class="oe_kanban_card oe_kanban_goal oe_kanban_global_click"> |
3778 | + <div class="oe_dropdown_toggle oe_dropdown_kanban"> |
3779 | + <span class="oe_e">í</span> |
3780 | + <ul class="oe_dropdown_menu"> |
3781 | + <li><a type="edit">Configure Challenge</a></li> |
3782 | + </ul> |
3783 | + </div> |
3784 | + <div class="oe_kanban_content"> |
3785 | + |
3786 | + <h4><field name="name"/></h4> |
3787 | + <div class="oe_kanban_project_list"> |
3788 | + <a type="action" name="%(goals_from_plan_act)d" style="margin-right: 10px"> |
3789 | + <span t-if="record.planline_count.raw_value gt 1"><field name="planline_count"/> Goals</span> |
3790 | + <span t-if="record.planline_count.raw_value lt 2"><field name="planline_count"/> Goal</span> |
3791 | + </a> |
3792 | + </div> |
3793 | + <div class="oe_kanban_badge_avatars"> |
3794 | + <t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member"> |
3795 | + <img t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/> |
3796 | + </t> |
3797 | + </div> |
3798 | + </div> |
3799 | + </div> |
3800 | + </t> |
3801 | + </templates> |
3802 | + </kanban> |
3803 | + </field> |
3804 | + </record> |
3805 | + |
3806 | + <record id="goal_plan_list_action" model="ir.actions.act_window"> |
3807 | + <field name="name">Challenges</field> |
3808 | + <field name="res_model">gamification.goal.plan</field> |
3809 | + <field name="view_mode">kanban,tree,form</field> |
3810 | + <field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field> |
3811 | + <field name="help" type="html"> |
3812 | + <p class="oe_view_nocontent_create"> |
3813 | + Click to create a challenge. |
3814 | + </p> |
3815 | + <p> |
3816 | + Assign a list of goals to chosen users to evaluate them. |
3817 | + The challenge can use a period (weekly, monthly...) for automatic creation of goals. |
3818 | + The goals are created for the specified users or member of the group. |
3819 | + </p> |
3820 | + </field> |
3821 | + </record> |
3822 | + <!-- Specify form view ID to avoid selecting view_challenge_wizard --> |
3823 | + <record id="goal_plan_list_action_view1" model="ir.actions.act_window.view"> |
3824 | + <field eval="1" name="sequence"/> |
3825 | + <field name="view_mode">kanban</field> |
3826 | + <field name="act_window_id" ref="goal_plan_list_action"/> |
3827 | + <field name="view_id" ref="view_goal_plan_kanban"/> |
3828 | + </record> |
3829 | + <record id="goal_plan_list_action_view2" model="ir.actions.act_window.view"> |
3830 | + <field eval="10" name="sequence"/> |
3831 | + <field name="view_mode">form</field> |
3832 | + <field name="act_window_id" ref="goal_plan_list_action"/> |
3833 | + <field name="view_id" ref="goal_plan_form_view"/> |
3834 | + </record> |
3835 | + |
3836 | + <!-- Planline --> |
3837 | + <record id="goal_planline_list_view" model="ir.ui.view"> |
3838 | + <field name="name">Goal planline list</field> |
3839 | + <field name="model">gamification.goal.planline</field> |
3840 | + <field name="arch" type="xml"> |
3841 | + <tree string="planline list" > |
3842 | + <field name="type_id"/> |
3843 | + <field name="target_goal"/> |
3844 | + </tree> |
3845 | + </field> |
3846 | + </record> |
3847 | + |
3848 | + |
3849 | + <record id="goal_plan_search_view" model="ir.ui.view"> |
3850 | + <field name="name">Challenge Search</field> |
3851 | + <field name="model">gamification.goal.plan</field> |
3852 | + <field name="arch" type="xml"> |
3853 | + <search string="Search Challenges"> |
3854 | + <filter name="inprogress" string="Running Challenges" |
3855 | + domain="[('state', '=', 'inprogress')]"/> |
3856 | + <filter name="hr_plans" string="HR Challenges" |
3857 | + domain="[('category', '=', 'hr')]"/> |
3858 | + <field name="name"/> |
3859 | + <group expand="0" string="Group By..."> |
3860 | + <filter string="State" domain="[]" context="{'group_by':'state'}"/> |
3861 | + <filter string="Period" domain="[]" context="{'group_by':'period'}"/> |
3862 | + </group> |
3863 | + </search> |
3864 | + </field> |
3865 | + </record> |
3866 | + |
3867 | + |
3868 | + <record id="view_challenge_wizard" model="ir.ui.view"> |
3869 | + <field name="name">Challenge Wizard</field> |
3870 | + <field name="model">gamification.goal.plan</field> |
3871 | + <field name="arch" type="xml"> |
3872 | + <form string="Challenge" version="7.0"> |
3873 | + <field name="reward_failure" invisible="1"/> |
3874 | + <div class="oe_title"> |
3875 | + <h1><field name="name" nolabel="1" readonly="1"/></h1> |
3876 | + </div> |
3877 | + <field name="description" nolabel="1" readonly="1" /> |
3878 | + <group> |
3879 | + <field name="start_date" readonly="1" /> |
3880 | + <field name="end_date" readonly="1" /> |
3881 | + <field name="user_ids" string="Participating" readonly="1" widget="many2many_tags" /> |
3882 | + <field name="proposed_user_ids" string="Invited" readonly="1" widget="many2many_tags" /> |
3883 | + </group> |
3884 | + <group string="Goals"> |
3885 | + <field name="planline_ids" nolabel="1" readonly="1" colspan="4"> |
3886 | + <tree string="Planline List" version="7.0" editable="bottom" > |
3887 | + <field name="sequence" widget="handle"/> |
3888 | + <field name="type_id"/> |
3889 | + <field name="type_condition"/> |
3890 | + <field name="target_goal"/> |
3891 | + <field name="type_full_suffix"/> |
3892 | + </tree> |
3893 | + </field> |
3894 | + </group> |
3895 | + <group string="Reward"> |
3896 | + <div class="oe_grey" attrs="{'invisible': ['|',('reward_id','!=',False),('reward_first_id','!=',False)]}"> |
3897 | + There is no reward upon completion of this challenge. |
3898 | + </div> |
3899 | + <group attrs="{'invisible': [('reward_id','=',False),('reward_first_id','=',False)]}"> |
3900 | + <field name="reward_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" /> |
3901 | + <field name="reward_first_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" /> |
3902 | + <field name="reward_second_id" readonly="1" attrs="{'invisible': [('reward_second_id','=', False)]}" /> |
3903 | + <field name="reward_third_id" readonly="1" attrs="{'invisible': [('reward_third_id','=', False)]}" /> |
3904 | + </group> |
3905 | + <div class="oe_grey" attrs="{'invisible': [('reward_failure','=',False)]}"> |
3906 | + Even if the challenge is failed, best challengers will be rewarded |
3907 | + </div> |
3908 | + </group> |
3909 | + <footer> |
3910 | + <center> |
3911 | + <button string="Accept" type="object" name="accept_challenge" class="oe_highlight" /> |
3912 | + <button string="Reject" type="object" name="discard_challenge"/> or |
3913 | + <button string="reply later" special="cancel" class="oe_link"/> |
3914 | + </center> |
3915 | + </footer> |
3916 | + </form> |
3917 | + </field> |
3918 | + </record> |
3919 | + |
3920 | + <record id="challenge_wizard" model="ir.actions.act_window"> |
3921 | + <field name="name">Challenge Description</field> |
3922 | + <field name="res_model">gamification.goal.plan</field> |
3923 | + <field name="view_type">form</field> |
3924 | + <field name="view_id" ref="view_challenge_wizard"/> |
3925 | + <field name="target">new</field> |
3926 | + </record> |
3927 | + |
3928 | + </data> |
3929 | +</openerp> |
3930 | \ No newline at end of file |
3931 | |
3932 | === added file 'gamification/res_users.py' |
3933 | --- gamification/res_users.py 1970-01-01 00:00:00 +0000 |
3934 | +++ gamification/res_users.py 2013-06-28 14:51:48 +0000 |
3935 | @@ -0,0 +1,177 @@ |
3936 | +# -*- coding: utf-8 -*- |
3937 | +############################################################################## |
3938 | +# |
3939 | +# OpenERP, Open Source Management Solution |
3940 | +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) |
3941 | +# |
3942 | +# This program is free software: you can redistribute it and/or modify |
3943 | +# it under the terms of the GNU General Public License as published by |
3944 | +# the Free Software Foundation, either version 3 of the License, or |
3945 | +# (at your option) any later version. |
3946 | +# |
3947 | +# This program is distributed in the hope that it will be useful, |
3948 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3949 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3950 | +# GNU General Public License for more details. |
3951 | +# |
3952 | +# You should have received a copy of the GNU General Public License |
3953 | +# along with this program. If not, see <http://www.gnu.org/licenses/> |
3954 | +# |
3955 | +############################################################################## |
3956 | + |
3957 | +from openerp.osv import osv |
3958 | + |
3959 | + |
3960 | +class res_users_gamification_group(osv.Model): |
3961 | + """ Update of res.users class |
3962 | + - if adding groups to an user, check gamification.goal.plan linked to |
3963 | + this group, and the user. This is done by overriding the write method. |
3964 | + """ |
3965 | + _name = 'res.users' |
3966 | + _inherit = ['res.users'] |
3967 | + |
3968 | + def write(self, cr, uid, ids, vals, context=None): |
3969 | + write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context) |
3970 | + if vals.get('groups_id'): |
3971 | + # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} |
3972 | + user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4] |
3973 | + user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] |
3974 | + |
3975 | + goal_plan_obj = self.pool.get('gamification.goal.plan') |
3976 | + plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) |
3977 | + if plan_ids: |
3978 | + goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) |
3979 | + |
3980 | + if vals.get('image'): |
3981 | + goal_type_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'type_base_avatar', context) |
3982 | + goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('type_id', '=', goal_type_id.id), ('user_id', 'in', ids)], context=context) |
3983 | + values = {'state': 'reached', 'current': 1} |
3984 | + self.pool.get('gamification.goal').write(cr, uid, goal_ids, values, context=context) |
3985 | + return write_res |
3986 | + |
3987 | + def get_goals_todo_info(self, cr, uid, context=None): |
3988 | + """Return the list of goals assigned to the user, grouped by plan |
3989 | + |
3990 | + This method intends to return processable data in javascript in the |
3991 | + goal_list_to_do template. The output format is not constant as the |
3992 | + required information is different between individual and board goal |
3993 | + types |
3994 | + :return: list of dictionnaries for each goal to display |
3995 | + """ |
3996 | + all_goals_info = [] |
3997 | + plan_obj = self.pool.get('gamification.goal.plan') |
3998 | + |
3999 | + plan_ids = plan_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) |
4000 | + for plan in plan_obj.browse(cr, uid, plan_ids, context=context): |
4001 | + # serialize goals info to be able to use it in javascript |
4002 | + serialized_goals_info = { |
4003 | + 'id': plan.id, |
4004 | + 'name': plan.name, |
4005 | + 'visibility_mode': plan.visibility_mode, |
4006 | + } |
4007 | + user = self.browse(cr, uid, uid, context=context) |
4008 | + serialized_goals_info['currency'] = user.company_id.currency_id.id |
4009 | + |
4010 | + if plan.visibility_mode == 'board': |
4011 | + # board report should be grouped by planline for all users |
4012 | + goals_info = plan_obj.get_board_goal_info(cr, uid, plan, subset_goal_ids=False, context=context) |
4013 | + |
4014 | + if len(goals_info) == 0: |
4015 | + # plan with no valid planlines |
4016 | + continue |
4017 | + |
4018 | + serialized_goals_info['planlines'] = [] |
4019 | + for planline_board in goals_info: |
4020 | + vals = {'type_name': planline_board['goal_type'].name, |
4021 | + 'type_description': planline_board['goal_type'].description, |
4022 | + 'type_condition': planline_board['goal_type'].condition, |
4023 | + 'type_computation_mode': planline_board['goal_type'].computation_mode, |
4024 | + 'type_monetary': planline_board['goal_type'].monetary, |
4025 | + 'type_suffix': planline_board['goal_type'].suffix, |
4026 | + 'type_action': True if planline_board['goal_type'].action_id else False, |
4027 | + 'type_display': planline_board['goal_type'].display_mode, |
4028 | + 'target_goal': planline_board['target_goal'], |
4029 | + 'goals': []} |
4030 | + for goal in planline_board['board_goals']: |
4031 | + # Keep only the Top 3 and the current user |
4032 | + if goal[0] > 2 and goal[1].user_id.id != uid: |
4033 | + continue |
4034 | + |
4035 | + vals['goals'].append({ |
4036 | + 'rank': goal[0] + 1, |
4037 | + 'id': goal[1].id, |
4038 | + 'user_id': goal[1].user_id.id, |
4039 | + 'user_name': goal[1].user_id.name, |
4040 | + 'state': goal[1].state, |
4041 | + 'completeness': goal[1].completeness, |
4042 | + 'current': goal[1].current, |
4043 | + 'target_goal': goal[1].target_goal, |
4044 | + }) |
4045 | + if uid == goal[1].user_id.id: |
4046 | + vals['own_goal_id'] = goal[1].id |
4047 | + serialized_goals_info['planlines'].append(vals) |
4048 | + |
4049 | + else: |
4050 | + # individual report are simply a list of goal |
4051 | + goals_info = plan_obj.get_indivual_goal_info(cr, uid, uid, plan, subset_goal_ids=False, context=context) |
4052 | + |
4053 | + if not goals_info: |
4054 | + continue |
4055 | + |
4056 | + serialized_goals_info['goals'] = [] |
4057 | + for goal in goals_info: |
4058 | + serialized_goals_info['goals'].append({ |
4059 | + 'id': goal.id, |
4060 | + 'type_name': goal.type_id.name, |
4061 | + 'type_description': goal.type_description, |
4062 | + 'type_condition': goal.type_id.condition, |
4063 | + 'type_monetary': goal.type_id.monetary, |
4064 | + 'type_suffix': goal.type_id.suffix, |
4065 | + 'type_action': True if goal.type_id.action_id else False, |
4066 | + 'type_display': goal.type_id.display_mode, |
4067 | + 'state': goal.state, |
4068 | + 'completeness': goal.completeness, |
4069 | + 'computation_mode': goal.computation_mode, |
4070 | + 'current': goal.current, |
4071 | + 'target_goal': goal.target_goal, |
4072 | + }) |
4073 | + |
4074 | + all_goals_info.append(serialized_goals_info) |
4075 | + return all_goals_info |
4076 | + |
4077 | + def get_challenge_suggestions(self, cr, uid, context=None): |
4078 | + """Return the list of goal plans suggested to the user""" |
4079 | + plan_info = [] |
4080 | + goal_plan_obj = self.pool.get('gamification.goal.plan') |
4081 | + plan_ids = goal_plan_obj.search(cr, uid, [('proposed_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) |
4082 | + for plan in goal_plan_obj.browse(cr, uid, plan_ids, context=context): |
4083 | + values = { |
4084 | + 'id': plan.id, |
4085 | + 'name': plan.name, |
4086 | + 'description': plan.description, |
4087 | + } |
4088 | + plan_info.append(values) |
4089 | + return plan_info |
4090 | + |
4091 | + |
4092 | +class res_groups_gamification_group(osv.Model): |
4093 | + """ Update of res.groups class |
4094 | + - if adding users from a group, check gamification.goal.plan linked to |
4095 | + this group, and the user. This is done by overriding the write method. |
4096 | + """ |
4097 | + _name = 'res.groups' |
4098 | + _inherit = 'res.groups' |
4099 | + |
4100 | + def write(self, cr, uid, ids, vals, context=None): |
4101 | + write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context) |
4102 | + if vals.get('users'): |
4103 | + # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} |
4104 | + user_ids = [command[1] for command in vals['users'] if command[0] == 4] |
4105 | + user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]] |
4106 | + |
4107 | + goal_plan_obj = self.pool.get('gamification.goal.plan') |
4108 | + plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context) |
4109 | + if plan_ids: |
4110 | + goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) |
4111 | + return write_res |
4112 | +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
4113 | |
4114 | === added directory 'gamification/security' |
4115 | === added file 'gamification/security/gamification_security.xml' |
4116 | --- gamification/security/gamification_security.xml 1970-01-01 00:00:00 +0000 |
4117 | +++ gamification/security/gamification_security.xml 2013-06-28 14:51:48 +0000 |
4118 | @@ -0,0 +1,31 @@ |
4119 | +<?xml version="1.0" ?> |
4120 | +<openerp> |
4121 | + <data noupdate="1"> |
4122 | + <record model="ir.module.category" id="module_goal_category"> |
4123 | + <field name="name">Gamification</field> |
4124 | + <field name="description"></field> |
4125 | + <field name="sequence">17</field> |
4126 | + </record> |
4127 | + <record id="group_goal_manager" model="res.groups"> |
4128 | + <field name="name">Manager</field> |
4129 | + <field name="category_id" ref="module_goal_category"/> |
4130 | + <field name="users" eval="[(4, ref('base.user_root'))]"/> |
4131 | + </record> |
4132 | + |
4133 | + <record id="goal_user_visibility" model="ir.rule"> |
4134 | + <field name="name">User can only see his/her goals or goal from the same plan in board visibility</field> |
4135 | + <field name="model_id" ref="model_gamification_goal"/> |
4136 | + <field name="groups" eval="[(4, ref('base.group_user'))]"/> |
4137 | + <field name="perm_read" eval="True"/> |
4138 | + <field name="perm_write" eval="True"/> |
4139 | + <field name="perm_create" eval="False"/> |
4140 | + <field name="perm_unlink" eval="False"/> |
4141 | + <field name="domain_force">[ |
4142 | + '|', |
4143 | + ('user_id','=',user.id), |
4144 | + '&', |
4145 | + ('plan_id.user_ids','in',user.id), |
4146 | + ('plan_id.visibility_mode','=','board')]</field> |
4147 | + </record> |
4148 | + </data> |
4149 | +</openerp> |
4150 | |
4151 | === added file 'gamification/security/ir.model.access.csv' |
4152 | --- gamification/security/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
4153 | +++ gamification/security/ir.model.access.csv 2013-06-28 14:51:48 +0000 |
4154 | @@ -0,0 +1,20 @@ |
4155 | +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink |
4156 | + |
4157 | +goal_anybody,"Goal Anybody",model_gamification_goal,,1,1,0,0 |
4158 | +goal_manager,"Goal Manager",model_gamification_goal,group_goal_manager,1,1,1,1 |
4159 | + |
4160 | +goal_type_anybody,"Goal Type Anybody",model_gamification_goal_type,,1,0,0,0 |
4161 | +goal_type_manager,"Goal Type Manager",model_gamification_goal_type,group_goal_manager,1,1,1,1 |
4162 | + |
4163 | +plan_anybody,"Goal Plan Anybody",model_gamification_goal_plan,,1,0,0,0 |
4164 | +plan_manager,"Goal Plan Manager",model_gamification_goal_plan,group_goal_manager,1,1,1,1 |
4165 | + |
4166 | +planline_anybody,"Goal Planline Anybody",model_gamification_goal_planline,,1,0,0,0 |
4167 | +planline_manager,"Goal Planline Manager",model_gamification_goal_planline,group_goal_manager,1,1,1,1 |
4168 | + |
4169 | +badge_anybody,"Badge Anybody",model_gamification_badge,,1,0,0,0 |
4170 | +badge_manager,"Badge Manager",model_gamification_badge,group_goal_manager,1,1,1,1 |
4171 | + |
4172 | +badge_user_anybody,"Badge-user Anybody",model_gamification_badge_user,,1,0,0,0 |
4173 | +badge_user_user,"Badge-user User",model_gamification_badge_user,base.group_user,1,1,1,0 |
4174 | +badge_user_manager,"Badge-user Manager",model_gamification_badge_user,group_goal_manager,1,1,1,1 |
4175 | |
4176 | === added directory 'gamification/static' |
4177 | === added directory 'gamification/static/lib' |
4178 | === added directory 'gamification/static/lib/justgage' |
4179 | === added file 'gamification/static/lib/justgage/justgage.js' |
4180 | --- gamification/static/lib/justgage/justgage.js 1970-01-01 00:00:00 +0000 |
4181 | +++ gamification/static/lib/justgage/justgage.js 2013-06-28 14:51:48 +0000 |
4182 | @@ -0,0 +1,946 @@ |
4183 | +/** |
4184 | + * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :) |
4185 | + * Check http://www.justgage.com for official releases |
4186 | + * Licensed under MIT. |
4187 | + * @author Bojan Djuricic (@Toorshia) |
4188 | + * |
4189 | + * LATEST UPDATES |
4190 | + |
4191 | + * ----------------------------- |
4192 | + * April 18, 2013. |
4193 | + * ----------------------------- |
4194 | + * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48 |
4195 | + * width - force gauge width |
4196 | + * height - force gauge height |
4197 | + |
4198 | + * ----------------------------- |
4199 | + * April 17, 2013. |
4200 | + * ----------------------------- |
4201 | + * fix - https://github.com/toorshia/justgage/issues/49 |
4202 | + |
4203 | + * ----------------------------- |
4204 | + * April 01, 2013. |
4205 | + * ----------------------------- |
4206 | + * fix - https://github.com/toorshia/justgage/issues/46 |
4207 | + |
4208 | + * ----------------------------- |
4209 | + * March 26, 2013. |
4210 | + * ----------------------------- |
4211 | + * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.) |
4212 | + |
4213 | + * ----------------------------- |
4214 | + * March 23, 2013. |
4215 | + * ----------------------------- |
4216 | + * counter - option to animate value in counting fashion |
4217 | + * fix - https://github.com/toorshia/justgage/issues/45 |
4218 | + |
4219 | + * ----------------------------- |
4220 | + * March 13, 2013. |
4221 | + * ----------------------------- |
4222 | + * refresh method - added optional 'max' parameter to use when you need to update max value |
4223 | + |
4224 | + * ----------------------------- |
4225 | + * February 26, 2013. |
4226 | + * ----------------------------- |
4227 | + * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value |
4228 | + * fixed a missing parameters bug when calling generateShadow() for IE < 9 |
4229 | + |
4230 | + * ----------------------------- |
4231 | + * December 31, 2012. |
4232 | + * ----------------------------- |
4233 | + * fixed text y-position for hidden divs - workaround for Raphael <tspan> 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491 |
4234 | + * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups |
4235 | + * Min and Max labels are now auto-off when in donut mode |
4236 | + * Start angle in donut mode is now 90 |
4237 | + * donutStartAngle - option to define start angle for donut |
4238 | + |
4239 | + * ----------------------------- |
4240 | + * November 25, 2012. |
4241 | + * ----------------------------- |
4242 | + * Option to define custom rendering function for displayed value |
4243 | + |
4244 | + * ----------------------------- |
4245 | + * November 19, 2012. |
4246 | + * ----------------------------- |
4247 | + * Config.value is now updated after gauge refresh |
4248 | + |
4249 | + * ----------------------------- |
4250 | + * November 13, 2012. |
4251 | + * ----------------------------- |
4252 | + * Donut display mode added |
4253 | + * Option to hide value label |
4254 | + * Option to enable responsive gauge size |
4255 | + * Removed default title attribute |
4256 | + * Option to accept min and max defined as string values |
4257 | + * Option to configure value symbol |
4258 | + * Fixed bad aspect ratio calculations |
4259 | + * Option to configure minimum font size for all texts |
4260 | + * Option to show shorthand big numbers (human friendly) |
4261 | + */ |
4262 | + |
4263 | + JustGage = function(config) { |
4264 | + |
4265 | + // if (!config.id) {alert("Missing id parameter for gauge!"); return false;} |
4266 | + // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;} |
4267 | + |
4268 | + var obj = this; |
4269 | + |
4270 | + // configurable parameters |
4271 | + obj.config = |
4272 | + { |
4273 | + // id : string |
4274 | + // this is container element id |
4275 | + id : config.id, |
4276 | + |
4277 | + // parentNode : node object |
4278 | + // this is container element |
4279 | + parentNode : (config.parentNode) ? config.parentNode : null, |
4280 | + |
4281 | + // width : int |
4282 | + // gauge width |
4283 | + width : (config.width) ? config.width : null, |
4284 | + |
4285 | + // height : int |
4286 | + // gauge height |
4287 | + height : (config.height) ? config.height : null, |
4288 | + |
4289 | + // title : string |
4290 | + // gauge title |
4291 | + title : (config.title) ? config.title : "", |
4292 | + |
4293 | + // titleFontColor : string |
4294 | + // color of gauge title |
4295 | + titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999", |
4296 | + |
4297 | + // value : int |
4298 | + // value gauge is showing |
4299 | + value : (config.value) ? config.value : 0, |
4300 | + |
4301 | + // valueFontColor : string |
4302 | + // color of label showing current value |
4303 | + valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101", |
4304 | + |
4305 | + // symbol : string |
4306 | + // special symbol to show next to value |
4307 | + symbol : (config.symbol) ? config.symbol : "", |
4308 | + |
4309 | + // min : int |
4310 | + // min value |
4311 | + min : (config.min) ? parseFloat(config.min) : 0, |
4312 | + |
4313 | + // max : int |
4314 | + // max value |
4315 | + max : (config.max) ? parseFloat(config.max) : 100, |
4316 | + |
4317 | + // humanFriendlyDecimal : int |
4318 | + // number of decimal places for our human friendly number to contain |
4319 | + humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0, |
4320 | + |
4321 | + // textRenderer: func |
4322 | + // function applied before rendering text |
4323 | + textRenderer : (config.textRenderer) ? config.textRenderer : null, |
4324 | + |
4325 | + // gaugeWidthScale : float |
4326 | + // width of the gauge element |
4327 | + gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0, |
4328 | + |
4329 | + // gaugeColor : string |
4330 | + // background color of gauge element |
4331 | + gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb", |
4332 | + |
4333 | + // label : string |
4334 | + // text to show below value |
4335 | + label : (config.label) ? config.label : "", |
4336 | + |
4337 | + // labelFontColor : string |
4338 | + // color of label showing label under value |
4339 | + labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3", |
4340 | + |
4341 | + // shadowOpacity : int |
4342 | + // 0 ~ 1 |
4343 | + shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2, |
4344 | + |
4345 | + // shadowSize: int |
4346 | + // inner shadow size |
4347 | + shadowSize : (config.shadowSize) ? config.shadowSize : 5, |
4348 | + |
4349 | + // shadowVerticalOffset : int |
4350 | + // how much shadow is offset from top |
4351 | + shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3, |
4352 | + |
4353 | + // levelColors : string[] |
4354 | + // colors of indicator, from lower to upper, in RGB format |
4355 | + levelColors : (config.levelColors) ? config.levelColors : [ |
4356 | + "#a9d70b", |
4357 | + "#f9c802", |
4358 | + "#ff0000" |
4359 | + ], |
4360 | + |
4361 | + // startAnimationTime : int |
4362 | + // length of initial animation |
4363 | + startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700, |
4364 | + |
4365 | + // startAnimationType : string |
4366 | + // type of initial animation (linear, >, <, <>, bounce) |
4367 | + startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">", |
4368 | + |
4369 | + // refreshAnimationTime : int |
4370 | + // length of refresh animation |
4371 | + refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700, |
4372 | + |
4373 | + // refreshAnimationType : string |
4374 | + // type of refresh animation (linear, >, <, <>, bounce) |
4375 | + refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">", |
4376 | + |
4377 | + // donutStartAngle : int |
4378 | + // angle to start from when in donut mode |
4379 | + donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90, |
4380 | + |
4381 | + // valueMinFontSize : int |
4382 | + // absolute minimum font size for the value |
4383 | + valueMinFontSize : config.valueMinFontSize || 16, |
4384 | + |
4385 | + // titleMinFontSize |
4386 | + // absolute minimum font size for the title |
4387 | + titleMinFontSize : config.titleMinFontSize || 10, |
4388 | + |
4389 | + // labelMinFontSize |
4390 | + // absolute minimum font size for the label |
4391 | + labelMinFontSize : config.labelMinFontSize || 10, |
4392 | + |
4393 | + // minLabelMinFontSize |
4394 | + // absolute minimum font size for the minimum label |
4395 | + minLabelMinFontSize : config.minLabelMinFontSize || 10, |
4396 | + |
4397 | + // maxLabelMinFontSize |
4398 | + // absolute minimum font size for the maximum label |
4399 | + maxLabelMinFontSize : config.maxLabelMinFontSize || 10, |
4400 | + |
4401 | + // hideValue : bool |
4402 | + // hide value text |
4403 | + hideValue : (config.hideValue) ? config.hideValue : false, |
4404 | + |
4405 | + // hideMinMax : bool |
4406 | + // hide min and max values |
4407 | + hideMinMax : (config.hideMinMax) ? config.hideMinMax : false, |
4408 | + |
4409 | + // hideInnerShadow : bool |
4410 | + // hide inner shadow |
4411 | + hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false, |
4412 | + |
4413 | + // humanFriendly : bool |
4414 | + // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M) |
4415 | + humanFriendly : (config.humanFriendly) ? config.humanFriendly : false, |
4416 | + |
4417 | + // noGradient : bool |
4418 | + // whether to use gradual color change for value, or sector-based |
4419 | + noGradient : (config.noGradient) ? config.noGradient : false, |
4420 | + |
4421 | + // donut : bool |
4422 | + // show full donut gauge |
4423 | + donut : (config.donut) ? config.donut : false, |
4424 | + |
4425 | + // relativeGaugeSize : bool |
4426 | + // whether gauge size should follow changes in container element size |
4427 | + relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false, |
4428 | + |
4429 | + // counter : bool |
4430 | + // animate level number change |
4431 | + counter : (config.counter) ? config.counter : false, |
4432 | + |
4433 | + // decimals : int |
4434 | + // number of digits after floating point |
4435 | + decimals : (config.decimals) ? config.decimals : 0, |
4436 | + |
4437 | + // customSectors : [] of objects |
4438 | + // number of digits after floating point |
4439 | + customSectors : (config.customSectors) ? config.customSectors : [] |
4440 | + }; |
4441 | + |
4442 | + // variables |
4443 | + var |
4444 | + canvasW, |
4445 | + canvasH, |
4446 | + widgetW, |
4447 | + widgetH, |
4448 | + aspect, |
4449 | + dx, |
4450 | + dy, |
4451 | + titleFontSize, |
4452 | + titleX, |
4453 | + titleY, |
4454 | + valueFontSize, |
4455 | + valueX, |
4456 | + valueY, |
4457 | + labelFontSize, |
4458 | + labelX, |
4459 | + labelY, |
4460 | + minFontSize, |
4461 | + minX, |
4462 | + minY, |
4463 | + maxFontSize, |
4464 | + maxX, |
4465 | + maxY; |
4466 | + |
4467 | + // overflow values |
4468 | + if (obj.config.value > obj.config.max) obj.config.value = obj.config.max; |
4469 | + if (obj.config.value < obj.config.min) obj.config.value = obj.config.min; |
4470 | + obj.originalValue = config.value; |
4471 | + |
4472 | + // create canvas |
4473 | + if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) { |
4474 | + obj.canvas = Raphael(obj.config.id, "100%", "100%"); |
4475 | + } else if (obj.config.parentNode !== null) { |
4476 | + obj.canvas = Raphael(obj.config.parentNode, "100%", "100%"); |
4477 | + } |
4478 | + |
4479 | + if (obj.config.relativeGaugeSize === true) { |
4480 | + obj.canvas.setViewBox(0, 0, 200, 150, true); |
4481 | + } |
4482 | + |
4483 | + // canvas dimensions |
4484 | + if (obj.config.relativeGaugeSize === true) { |
4485 | + canvasW = 200; |
4486 | + canvasH = 150; |
4487 | + } else if (obj.config.width !== null && obj.config.height !== null) { |
4488 | + canvasW = obj.config.width; |
4489 | + canvasH = obj.config.height; |
4490 | + } else if (obj.config.parentNode !== null) { |
4491 | + obj.canvas.setViewBox(0, 0, 200, 150, true); |
4492 | + canvasW = 200; |
4493 | + canvasH = 150; |
4494 | + } else { |
4495 | + canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1; |
4496 | + canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1; |
4497 | + } |
4498 | + |
4499 | + // widget dimensions |
4500 | + if (obj.config.donut === true) { |
4501 | + |
4502 | + // DONUT ******************************* |
4503 | + |
4504 | + // width more than height |
4505 | + if(canvasW > canvasH) { |
4506 | + widgetH = canvasH; |
4507 | + widgetW = widgetH; |
4508 | + // width less than height |
4509 | + } else if (canvasW < canvasH) { |
4510 | + widgetW = canvasW; |
4511 | + widgetH = widgetW; |
4512 | + // if height don't fit, rescale both |
4513 | + if(widgetH > canvasH) { |
4514 | + aspect = widgetH / canvasH; |
4515 | + widgetH = widgetH / aspect; |
4516 | + widgetW = widgetH / aspect; |
4517 | + } |
4518 | + // equal |
4519 | + } else { |
4520 | + widgetW = canvasW; |
4521 | + widgetH = widgetW; |
4522 | + } |
4523 | + |
4524 | + // delta |
4525 | + dx = (canvasW - widgetW)/2; |
4526 | + dy = (canvasH - widgetH)/2; |
4527 | + |
4528 | + // title |
4529 | + titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10; |
4530 | + titleX = dx + widgetW / 2; |
4531 | + titleY = dy + widgetH / 11; |
4532 | + |
4533 | + // value |
4534 | + valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18; |
4535 | + valueX = dx + widgetW / 2; |
4536 | + if(obj.config.label !== '') { |
4537 | + valueY = dy + widgetH / 1.85; |
4538 | + } else { |
4539 | + valueY = dy + widgetH / 1.7; |
4540 | + } |
4541 | + |
4542 | + // label |
4543 | + labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; |
4544 | + labelX = dx + widgetW / 2; |
4545 | + labelY = valueY + labelFontSize; |
4546 | + |
4547 | + // min |
4548 | + minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; |
4549 | + minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ; |
4550 | + minY = labelY; |
4551 | + |
4552 | + // max |
4553 | + maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10; |
4554 | + maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ; |
4555 | + maxY = labelY; |
4556 | + |
4557 | + } else { |
4558 | + // HALF ******************************* |
4559 | + |
4560 | + // width more than height |
4561 | + if(canvasW > canvasH) { |
4562 | + widgetH = canvasH; |
4563 | + widgetW = widgetH * 1.25; |
4564 | + //if width doesn't fit, rescale both |
4565 | + if(widgetW > canvasW) { |
4566 | + aspect = widgetW / canvasW; |
4567 | + widgetW = widgetW / aspect; |
4568 | + widgetH = widgetH / aspect; |
4569 | + } |
4570 | + // width less than height |
4571 | + } else if (canvasW < canvasH) { |
4572 | + widgetW = canvasW; |
4573 | + widgetH = widgetW / 1.25; |
4574 | + // if height don't fit, rescale both |
4575 | + if(widgetH > canvasH) { |
4576 | + aspect = widgetH / canvasH; |
4577 | + widgetH = widgetH / aspect; |
4578 | + widgetW = widgetH / aspect; |
4579 | + } |
4580 | + // equal |
4581 | + } else { |
4582 | + widgetW = canvasW; |
4583 | + widgetH = widgetW * 0.75; |
4584 | + } |
4585 | + |
4586 | + // delta |
4587 | + dx = (canvasW - widgetW)/2; |
4588 | + dy = (canvasH - widgetH)/2; |
4589 | + |
4590 | + // title |
4591 | + titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize; |
4592 | + titleX = dx + widgetW / 2; |
4593 | + titleY = dy + widgetH / 6.4; |
4594 | + |
4595 | + // value |
4596 | + valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize; |
4597 | + valueX = dx + widgetW / 2; |
4598 | + valueY = dy + widgetH / 1.275; |
4599 | + |
4600 | + // label |
4601 | + labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize; |
4602 | + labelX = dx + widgetW / 2; |
4603 | + labelY = valueY + valueFontSize / 2 + 5; |
4604 | + |
4605 | + // min |
4606 | + minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize; |
4607 | + minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ; |
4608 | + minY = labelY; |
4609 | + |
4610 | + // max |
4611 | + maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize; |
4612 | + maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ; |
4613 | + maxY = labelY; |
4614 | + } |
4615 | + |
4616 | + // parameters |
4617 | + obj.params = { |
4618 | + canvasW : canvasW, |
4619 | + canvasH : canvasH, |
4620 | + widgetW : widgetW, |
4621 | + widgetH : widgetH, |
4622 | + dx : dx, |
4623 | + dy : dy, |
4624 | + titleFontSize : titleFontSize, |
4625 | + titleX : titleX, |
4626 | + titleY : titleY, |
4627 | + valueFontSize : valueFontSize, |
4628 | + valueX : valueX, |
4629 | + valueY : valueY, |
4630 | + labelFontSize : labelFontSize, |
4631 | + labelX : labelX, |
4632 | + labelY : labelY, |
4633 | + minFontSize : minFontSize, |
4634 | + minX : minX, |
4635 | + minY : minY, |
4636 | + maxFontSize : maxFontSize, |
4637 | + maxX : maxX, |
4638 | + maxY : maxY |
4639 | + }; |
4640 | + |
4641 | + // var clear |
4642 | + canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null |
4643 | + |
4644 | + // pki - custom attribute for generating gauge paths |
4645 | + obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) { |
4646 | + |
4647 | + var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path; |
4648 | + |
4649 | + if (donut) { |
4650 | + alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI; |
4651 | + Ro = w / 2 - w / 7; |
4652 | + Ri = Ro - w / 6.666666666666667 * gws; |
4653 | + |
4654 | + Cx = w / 2 + dx; |
4655 | + Cy = h / 1.95 + dy; |
4656 | + |
4657 | + Xo = w / 2 + dx + Ro * Math.cos(alpha); |
4658 | + Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha); |
4659 | + Xi = w / 2 + dx + Ri * Math.cos(alpha); |
4660 | + Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha); |
4661 | + |
4662 | + path += "M" + (Cx - Ri) + "," + Cy + " "; |
4663 | + path += "L" + (Cx - Ro) + "," + Cy + " "; |
4664 | + if (value > ((max - min) / 2)) { |
4665 | + path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " "; |
4666 | + } |
4667 | + path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " "; |
4668 | + path += "L" + Xi + "," + Yi + " "; |
4669 | + if (value > ((max - min) / 2)) { |
4670 | + path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " "; |
4671 | + } |
4672 | + path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " "; |
4673 | + path += "Z "; |
4674 | + |
4675 | + return { path: path }; |
4676 | + |
4677 | + } else { |
4678 | + alpha = (1 - (value - min) / (max - min)) * Math.PI; |
4679 | + Ro = w / 2 - w / 10; |
4680 | + Ri = Ro - w / 6.666666666666667 * gws; |
4681 | + |
4682 | + Cx = w / 2 + dx; |
4683 | + Cy = h / 1.25 + dy; |
4684 | + |
4685 | + Xo = w / 2 + dx + Ro * Math.cos(alpha); |
4686 | + Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha); |
4687 | + Xi = w / 2 + dx + Ri * Math.cos(alpha); |
4688 | + Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha); |
4689 | + |
4690 | + path += "M" + (Cx - Ri) + "," + Cy + " "; |
4691 | + path += "L" + (Cx - Ro) + "," + Cy + " "; |
4692 | + path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " "; |
4693 | + path += "L" + Xi + "," + Yi + " "; |
4694 | + path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " "; |
4695 | + path += "Z "; |
4696 | + |
4697 | + return { path: path }; |
4698 | + } |
4699 | + |
4700 | + // var clear |
4701 | + alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null; |
4702 | + }; |
4703 | + |
4704 | + // gauge |
4705 | + obj.gauge = obj.canvas.path().attr({ |
4706 | + "stroke": "none", |
4707 | + "fill": obj.config.gaugeColor, |
4708 | + pki: [ |
4709 | + obj.config.max, |
4710 | + obj.config.min, |
4711 | + obj.config.max, |
4712 | + obj.params.widgetW, |
4713 | + obj.params.widgetH, |
4714 | + obj.params.dx, |
4715 | + obj.params.dy, |
4716 | + obj.config.gaugeWidthScale, |
4717 | + obj.config.donut |
4718 | + ] |
4719 | + }); |
4720 | + |
4721 | + // level |
4722 | + obj.level = obj.canvas.path().attr({ |
4723 | + "stroke": "none", |
4724 | + "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors), |
4725 | + pki: [ |
4726 | + obj.config.min, |
4727 | + obj.config.min, |
4728 | + obj.config.max, |
4729 | + obj.params.widgetW, |
4730 | + obj.params.widgetH, |
4731 | + obj.params.dx, |
4732 | + obj.params.dy, |
4733 | + obj.config.gaugeWidthScale, |
4734 | + obj.config.donut |
4735 | + ] |
4736 | + }); |
4737 | + if(obj.config.donut) { |
4738 | + obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy)); |
4739 | + } |
4740 | + |
4741 | + // title |
4742 | + obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title); |
4743 | + obj.txtTitle.attr({ |
4744 | + "font-size":obj.params.titleFontSize, |
4745 | + "font-weight":"bold", |
4746 | + "font-family":"Arial", |
4747 | + "fill":obj.config.titleFontColor, |
4748 | + "fill-opacity":"1" |
4749 | + }); |
4750 | + setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY); |
4751 | + |
4752 | + // value |
4753 | + obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0); |
4754 | + obj.txtValue.attr({ |
4755 | + "font-size":obj.params.valueFontSize, |
4756 | + "font-weight":"bold", |
4757 | + "font-family":"Arial", |
4758 | + "fill":obj.config.valueFontColor, |
4759 | + "fill-opacity":"0" |
4760 | + }); |
4761 | + setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); |
4762 | + |
4763 | + // label |
4764 | + obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label); |
4765 | + obj.txtLabel.attr({ |
4766 | + "font-size":obj.params.labelFontSize, |
4767 | + "font-weight":"normal", |
4768 | + "font-family":"Arial", |
4769 | + "fill":obj.config.labelFontColor, |
4770 | + "fill-opacity":"0" |
4771 | + }); |
4772 | + setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY); |
4773 | + |
4774 | + // min |
4775 | + obj.txtMinimum = obj.config.min; |
4776 | + if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal ); |
4777 | + obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum); |
4778 | + obj.txtMin.attr({ |
4779 | + "font-size":obj.params.minFontSize, |
4780 | + "font-weight":"normal", |
4781 | + "font-family":"Arial", |
4782 | + "fill":obj.config.labelFontColor, |
4783 | + "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1" |
4784 | + }); |
4785 | + setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY); |
4786 | + |
4787 | + // max |
4788 | + obj.txtMaximum = obj.config.max; |
4789 | + if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal ); |
4790 | + obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum); |
4791 | + obj.txtMax.attr({ |
4792 | + "font-size":obj.params.maxFontSize, |
4793 | + "font-weight":"normal", |
4794 | + "font-family":"Arial", |
4795 | + "fill":obj.config.labelFontColor, |
4796 | + "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1" |
4797 | + }); |
4798 | + setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY); |
4799 | + |
4800 | + var defs = obj.canvas.canvas.childNodes[1]; |
4801 | + var svg = "http://www.w3.org/2000/svg"; |
4802 | + |
4803 | + if (ie < 9) { |
4804 | + onCreateElementNsReady(function() { |
4805 | + obj.generateShadow(svg, defs); |
4806 | + }); |
4807 | + } else { |
4808 | + obj.generateShadow(svg, defs); |
4809 | + } |
4810 | + |
4811 | + // var clear |
4812 | + defs, svg = null; |
4813 | + |
4814 | + // set value to display |
4815 | + if(obj.config.textRenderer) { |
4816 | + obj.originalValue = obj.config.textRenderer(obj.originalValue); |
4817 | + } else if(obj.config.humanFriendly) { |
4818 | + obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol; |
4819 | + } else { |
4820 | + obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol; |
4821 | + } |
4822 | + |
4823 | + if(obj.config.counter === true) { |
4824 | + //on each animation frame |
4825 | + eve.on("raphael.anim.frame." + (obj.level.id), function() { |
4826 | + var currentValue = obj.level.attr("pki"); |
4827 | + if(obj.config.textRenderer) { |
4828 | + obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0]))); |
4829 | + } else if(obj.config.humanFriendly) { |
4830 | + obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol); |
4831 | + } else { |
4832 | + obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol); |
4833 | + } |
4834 | + setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); |
4835 | + currentValue = null; |
4836 | + }); |
4837 | + //on animation end |
4838 | + eve.on("raphael.anim.finish." + (obj.level.id), function() { |
4839 | + obj.txtValue.attr({"text" : obj.originalValue}); |
4840 | + setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); |
4841 | + }); |
4842 | + } else { |
4843 | + //on animation start |
4844 | + eve.on("raphael.anim.start." + (obj.level.id), function() { |
4845 | + obj.txtValue.attr({"text" : obj.originalValue}); |
4846 | + setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); |
4847 | + }); |
4848 | + } |
4849 | + |
4850 | + // animate gauge level, value & label |
4851 | + obj.level.animate({ |
4852 | + pki: [ |
4853 | + obj.config.value, |
4854 | + obj.config.min, |
4855 | + obj.config.max, |
4856 | + obj.params.widgetW, |
4857 | + obj.params.widgetH, |
4858 | + obj.params.dx, |
4859 | + obj.params.dy, |
4860 | + obj.config.gaugeWidthScale, |
4861 | + obj.config.donut |
4862 | + ] |
4863 | + }, obj.config.startAnimationTime, obj.config.startAnimationType); |
4864 | + obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType); |
4865 | + obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType); |
4866 | +}; |
4867 | + |
4868 | +/** Refresh gauge level */ |
4869 | +JustGage.prototype.refresh = function(val, max) { |
4870 | + |
4871 | + var obj = this; |
4872 | + var displayVal, color, max = max || null; |
4873 | + |
4874 | + // set new max |
4875 | + if(max !== null) { |
4876 | + obj.config.max = max; |
4877 | + |
4878 | + obj.txtMaximum = obj.config.max; |
4879 | + if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal ); |
4880 | + obj.txtMax.attr({"text" : obj.txtMaximum}); |
4881 | + setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY); |
4882 | + } |
4883 | + |
4884 | + // overflow values |
4885 | + displayVal = val; |
4886 | + if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);} |
4887 | + if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);} |
4888 | + |
4889 | + color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors); |
4890 | + |
4891 | + if(obj.config.textRenderer) { |
4892 | + displayVal = obj.config.textRenderer(displayVal); |
4893 | + } else if( obj.config.humanFriendly ) { |
4894 | + displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol; |
4895 | + } else { |
4896 | + displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol; |
4897 | + } |
4898 | + obj.originalValue = displayVal; |
4899 | + obj.config.value = val * 1; |
4900 | + |
4901 | + if(!obj.config.counter) { |
4902 | + obj.txtValue.attr({"text":displayVal}); |
4903 | + setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY); |
4904 | + } |
4905 | + |
4906 | + obj.level.animate({ |
4907 | + pki: [ |
4908 | + obj.config.value, |
4909 | + obj.config.min, |
4910 | + obj.config.max, |
4911 | + obj.params.widgetW, |
4912 | + obj.params.widgetH, |
4913 | + obj.params.dx, |
4914 | + obj.params.dy, |
4915 | + obj.config.gaugeWidthScale, |
4916 | + obj.config.donut |
4917 | + ], |
4918 | + "fill":color |
4919 | + }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType); |
4920 | + |
4921 | + // var clear |
4922 | + obj, displayVal, color, max = null; |
4923 | +}; |
4924 | + |
4925 | +/** Generate shadow */ |
4926 | +JustGage.prototype.generateShadow = function(svg, defs) { |
4927 | + |
4928 | + var obj = this; |
4929 | + var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3; |
4930 | + |
4931 | + // FILTER |
4932 | + gaussFilter = document.createElementNS(svg,"filter"); |
4933 | + gaussFilter.setAttribute("id","inner-shadow"); |
4934 | + defs.appendChild(gaussFilter); |
4935 | + |
4936 | + // offset |
4937 | + feOffset = document.createElementNS(svg,"feOffset"); |
4938 | + feOffset.setAttribute("dx", 0); |
4939 | + feOffset.setAttribute("dy", obj.config.shadowVerticalOffset); |
4940 | + gaussFilter.appendChild(feOffset); |
4941 | + |
4942 | + // blur |
4943 | + feGaussianBlur = document.createElementNS(svg,"feGaussianBlur"); |
4944 | + feGaussianBlur.setAttribute("result","offset-blur"); |
4945 | + feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize); |
4946 | + gaussFilter.appendChild(feGaussianBlur); |
4947 | + |
4948 | + // composite 1 |
4949 | + feComposite1 = document.createElementNS(svg,"feComposite"); |
4950 | + feComposite1.setAttribute("operator","out"); |
4951 | + feComposite1.setAttribute("in", "SourceGraphic"); |
4952 | + feComposite1.setAttribute("in2","offset-blur"); |
4953 | + feComposite1.setAttribute("result","inverse"); |
4954 | + gaussFilter.appendChild(feComposite1); |
4955 | + |
4956 | + // flood |
4957 | + feFlood = document.createElementNS(svg,"feFlood"); |
4958 | + feFlood.setAttribute("flood-color","black"); |
4959 | + feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity); |
4960 | + feFlood.setAttribute("result","color"); |
4961 | + gaussFilter.appendChild(feFlood); |
4962 | + |
4963 | + // composite 2 |
4964 | + feComposite2 = document.createElementNS(svg,"feComposite"); |
4965 | + feComposite2.setAttribute("operator","in"); |
4966 | + feComposite2.setAttribute("in", "color"); |
4967 | + feComposite2.setAttribute("in2","inverse"); |
4968 | + feComposite2.setAttribute("result","shadow"); |
4969 | + gaussFilter.appendChild(feComposite2); |
4970 | + |
4971 | + // composite 3 |
4972 | + feComposite3 = document.createElementNS(svg,"feComposite"); |
4973 | + feComposite3.setAttribute("operator","over"); |
4974 | + feComposite3.setAttribute("in", "shadow"); |
4975 | + feComposite3.setAttribute("in2","SourceGraphic"); |
4976 | + gaussFilter.appendChild(feComposite3); |
4977 | + |
4978 | + // set shadow |
4979 | + if (!obj.config.hideInnerShadow) { |
4980 | + obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)"); |
4981 | + obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)"); |
4982 | + } |
4983 | + |
4984 | + // var clear |
4985 | + gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null; |
4986 | + |
4987 | +}; |
4988 | + |
4989 | +/** Get color for value */ |
4990 | +function getColor(val, pct, col, noGradient, custSec) { |
4991 | + |
4992 | + var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color; |
4993 | + var noGradient = noGradient || custSec.length > 0; |
4994 | + |
4995 | + if(custSec.length > 0) { |
4996 | + for(var i = 0; i < custSec.length; i++) { |
4997 | + if(val > custSec[i].lo && val <= custSec[i].hi) { |
4998 | + return custSec[i].color; |
4999 | + } |
5000 | + } |
The diff has been truncated for viewing.