Merge lp:~openerp-dev/openobject-addons/trunk-gamification-mat into lp:openobject-addons

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
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+170759@code.launchpad.net
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
=== added directory 'gamification'
=== added file 'gamification/__init__.py'
--- gamification/__init__.py 1970-01-01 00:00:00 +0000
+++ gamification/__init__.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,5 @@
1import goal
2import goal_type_data
3import plan
4import res_users
5import badge
06
=== added file 'gamification/__openerp__.py'
--- gamification/__openerp__.py 1970-01-01 00:00:00 +0000
+++ gamification/__openerp__.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,62 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2004-2013 Tiny SPRL (<http://openerp.com>).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21{
22 'name': 'Gamification',
23 'version': '1.0',
24 'author': 'OpenERP SA',
25 'category': 'Human Ressources',
26 'depends': ['mail', 'email_template'],
27 'description': """
28Gamification process
29====================
30The Gamification module provides ways to evaluate and motivate the users of OpenERP.
31
32The users can be evaluated using goals and numerical objectives to reach.
33**Goals** are assigned through **plans** to evaluate and compare members of a team with each others and through time.
34
35For 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.
36
37Both 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.
38""",
39
40 'data': [
41 'plan_view.xml',
42 'badge_view.xml',
43 'goal_view.xml',
44 'cron.xml',
45 'security/gamification_security.xml',
46 'security/ir.model.access.csv',
47 'goal_base_data.xml',
48 'badge_data.xml',
49 ],
50 'test': [
51 'test/goal_demo.yml'
52 ],
53 'installable': True,
54 'application': True,
55 'auto_install': True,
56 'css': ['static/src/css/gamification.css'],
57 'js': [
58 'static/src/js/gamification.js',
59 'static/lib/justgage/justgage.js',
60 ],
61 'qweb': ['static/src/xml/gamification.xml'],
62}
063
=== added file 'gamification/badge.py'
--- gamification/badge.py 1970-01-01 00:00:00 +0000
+++ gamification/badge.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,299 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>
19#
20##############################################################################
21
22from openerp import SUPERUSER_ID
23from openerp.osv import fields, osv
24from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
25from openerp.tools.translate import _
26
27# from templates import TemplateHelper
28from datetime import date
29import logging
30
31_logger = logging.getLogger(__name__)
32
33
34class gamification_badge_user(osv.Model):
35 """User having received a badge"""
36
37 _name = 'gamification.badge.user'
38 _description = 'Gamification user badge'
39 _order = "create_date desc"
40
41 _columns = {
42 'user_id': fields.many2one('res.users', string="User", required=True),
43 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True),
44 'comment': fields.text('Comment'),
45 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"),
46 'create_date': fields.datetime('Created', readonly=True),
47 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
48 }
49
50
51class gamification_badge(osv.Model):
52 """Badge object that users can send and receive"""
53
54 _name = 'gamification.badge'
55 _description = 'Gamification badge'
56 _inherit = ['mail.thread']
57
58 def _get_owners_info(self, cr, uid, ids, name, args, context=None):
59 """Return:
60 the list of unique res.users ids having received this badge
61 the total number of time this badge was granted
62 the total number of users this badge was granted to
63 """
64 result = dict.fromkeys(ids, False)
65 for obj in self.browse(cr, uid, ids, context=context):
66 res = set()
67 for owner in obj.owner_ids:
68 res.add(owner.user_id.id)
69 res = list(res)
70 result[obj.id] = {
71 'unique_owner_ids': res,
72 'stat_count': len(obj.owner_ids),
73 'stat_count_distinct': len(res)
74 }
75 return result
76
77 def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
78 """Return stats related to badge users"""
79 result = dict.fromkeys(ids, False)
80 badge_user_obj = self.pool.get('gamification.badge.user')
81 first_month_day = date.today().replace(day=1).strftime(DF)
82 for obj in self.browse(cr, uid, ids, context=context):
83 result[obj.id] = {
84 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid)], context=context, count=True),
85 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_date', '>=', first_month_day)], context=context, count=True),
86 '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),
87 '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)
88 }
89 return result
90
91 def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
92 """Computes the number of badges remaining the user can send
93
94 0 if not allowed or no remaining
95 integer if limited sending
96 -1 if infinite (should not be displayed)
97 """
98 result = dict.fromkeys(ids, False)
99 for badge in self.browse(cr, uid, ids, context=context):
100 if self._can_grant_badge(cr, uid, uid, badge.id, context) != 1:
101 #if the user cannot grant this badge at all, result is 0
102 result[badge.id] = 0
103 elif not badge.rule_max:
104 #if there is no limitation, -1 is returned which mean 'infinite'
105 result[badge.id] = -1
106 else:
107 result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
108 return result
109
110 _columns = {
111 'name': fields.char('Badge', required=True, translate=True),
112 'description': fields.text('Description'),
113 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"),
114 # image_select: selection with a on_change to fill image with predefined picts
115 'rule_auth': fields.selection([
116 ('everyone', 'Everyone'),
117 ('users', 'A selected list of users'),
118 ('having', 'People having some badges'),
119 ('nobody', 'No one, assigned through challenges'),
120 ],
121 string="Allowance to Grant",
122 help="Who can grant this badge",
123 required=True),
124 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
125 string='Authorized Users',
126 help="Only these people can give this badge"),
127 'rule_auth_badge_ids': fields.many2many('gamification.badge',
128 'rel_badge_badge', 'badge1_id', 'badge2_id',
129 string='Required Badges',
130 help="Only the people having these badges can give this badge"),
131
132 'rule_max': fields.boolean('Monthly Limited Sending',
133 help="Check to set a monthly limit per person of sending this badge"),
134 'rule_max_number': fields.integer('Limitation Number',
135 help="The maximum number of time this badge can be sent per month per person."),
136 'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
137 type="integer",
138 string='My Monthly Sending Total',
139 multi='badge_users',
140 help="The number of time the current user has sent this badge this month."),
141 'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
142 string='Remaining Sending Allowed', help="If a maxium is set"),
143
144 'plan_ids': fields.one2many('gamification.goal.plan', 'reward_id',
145 string="Reward of Challenges"),
146
147 'goal_type_ids': fields.many2many('gamification.goal.type',
148 string='Goals Linked',
149 help="The users that have succeeded theses goals will receive automatically the badge."),
150
151 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
152 string='Owners', help='The list of instances of this badge granted to users'),
153 'unique_owner_ids': fields.function(_get_owners_info,
154 string='Unique Owners',
155 help="The list of unique users having received this badge.",
156 multi='unique_users',
157 type="many2many", relation="res.users"),
158
159 'stat_count': fields.function(_get_owners_info, string='Total',
160 type="integer",
161 multi='unique_users',
162 help="The number of time this badge has been received."),
163 'stat_count_distinct': fields.function(_get_owners_info,
164 type="integer",
165 string='Number of users',
166 multi='unique_users',
167 help="The number of time this badge has been received by unique users."),
168 'stat_this_month': fields.function(_get_badge_user_stats,
169 type="integer",
170 string='Monthly total',
171 multi='badge_users',
172 help="The number of time this badge has been received this month."),
173 'stat_my': fields.function(_get_badge_user_stats, string='My Total',
174 type="integer",
175 multi='badge_users',
176 help="The number of time the current user has received this badge."),
177 'stat_my_this_month': fields.function(_get_badge_user_stats,
178 type="integer",
179 string='My Monthly Total',
180 multi='badge_users',
181 help="The number of time the current user has received this badge this month."),
182 }
183
184 _defaults = {
185 'rule_auth': 'everyone',
186 }
187
188 def send_badge(self, cr, uid, badge_id, badge_user_ids, user_from=False, context=None):
189 """Send a notification to a user for receiving a badge
190
191 Does NOT verify constrains on badge granting.
192 The users are added to the owner_ids (create badge_user if needed)
193 The stats counters are incremented
194 :param badge_id: id of the badge to deserve
195 :param badge_user_ids: list(int) of badge users that will receive the badge
196 :param user_from: optional id of the res.users object that has sent the badge
197 """
198 badge = self.browse(cr, uid, badge_id, context=context)
199 # template_env = TemplateHelper()
200
201 res = None
202 temp_obj = self.pool.get('email.template')
203 template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context)
204 ctx = context.copy()
205 for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context=context):
206
207 ctx.update({'user_from': self.pool.get('res.users').browse(cr, uid, user_from).name})
208
209 body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx)
210
211 # as SUPERUSER as normal user don't have write access on a badge
212 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)
213 return res
214
215 def check_granting(self, cr, uid, user_from_id, badge_id, context=None):
216 """Check the user 'user_from_id' can grant the badge 'badge_id' and raise the appropriate exception
217 if not"""
218 status_code = self._can_grant_badge(cr, uid, user_from_id, badge_id, context=context)
219 if status_code == 1:
220 return True
221 elif status_code == 2:
222 raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.'))
223 elif status_code == 3:
224 raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.'))
225 elif status_code == 4:
226 raise osv.except_osv(_('Warning!'), _('You do not have the required badges.'))
227 elif status_code == 5:
228 raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.'))
229 else:
230 _logger.exception("Unknown badge status code: %d" % int(status_code))
231 return False
232
233 def _can_grant_badge(self, cr, uid, user_from_id, badge_id, context=None):
234 """Check if a user can grant a badge to another user
235
236 :param user_from_id: the id of the res.users trying to send the badge
237 :param badge_id: the granted badge id
238 :return: integer representing the permission.
239 1: can grant
240 2: nobody can send
241 3: user not in the allowed list
242 4: don't have the required badges
243 5: user's monthly limit reached
244 """
245 badge = self.browse(cr, uid, badge_id, context=context)
246
247 if badge.rule_auth == 'nobody':
248 return 2
249
250 elif badge.rule_auth == 'users' and user_from_id not in [user.id for user in badge.rule_auth_user_ids]:
251 return 3
252
253 elif badge.rule_auth == 'having':
254 all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', user_from_id)], context=context)
255 for required_badge in badge.rule_auth_badge_ids:
256 if required_badge.id not in all_user_badges:
257 return 4
258
259 if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
260 return 5
261
262 # badge.rule_auth == 'everyone' -> no check
263 return 1
264
265
266class grant_badge_wizard(osv.TransientModel):
267 """ Wizard allowing to grant a badge to a user"""
268
269 _name = 'gamification.badge.user.wizard'
270 _columns = {
271 'user_id': fields.many2one("res.users", string='User', required=True),
272 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True),
273 'comment': fields.text('Comment'),
274 }
275
276 def action_grant_badge(self, cr, uid, ids, context=None):
277 """Wizard action for sending a badge to a chosen user"""
278
279 badge_obj = self.pool.get('gamification.badge')
280 badge_user_obj = self.pool.get('gamification.badge.user')
281
282 for wiz in self.browse(cr, uid, ids, context=context):
283 if uid == wiz.user_id.id:
284 raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself'))
285
286 #check if the badge granting is legitimate
287 if badge_obj.check_granting(cr, uid, user_from_id=uid, badge_id=wiz.badge_id.id, context=context):
288 #create the badge
289 values = {
290 'user_id': wiz.user_id.id,
291 'badge_id': wiz.badge_id.id,
292 'comment': wiz.comment,
293 }
294 badge_user = badge_user_obj.create(cr, uid, values, context=context)
295 #notify the user
296 result = badge_obj.send_badge(cr, uid, wiz.badge_id.id, [badge_user], user_from=uid, context=context)
297
298 return result
299# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
0300
=== added file 'gamification/badge_data.xml'
--- gamification/badge_data.xml 1970-01-01 00:00:00 +0000
+++ gamification/badge_data.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,906 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="1">
4 <record id="badge_good_job" model="gamification.badge">
5 <field name="name">Good Job</field>
6 <field name="description">You did great at your job.</field>
7 <field name="rule_auth">everyone</field>
8 <field name="image">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
218 RU5ErkJggg==</field>
219 </record>
220
221 <record id="badge_problem_solver" model="gamification.badge">
222 <field name="name">Problem Solver</field>
223 <field name="description">No one can solve challenges like you do.</field>
224 <field name="rule_auth">everyone</field>
225 <field name="image">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</field>
617 </record>
618
619 <record id="badge_idea" model="gamification.badge">
620 <field name="name">Brilliant</field>
621 <field name="description">With your brilliant ideas, you are an inspiration to others.</field>
622 <field name="rule_auth">everyone</field>
623 <field name="rule_max">True</field>
624 <field name="rule_max_number">2</field>
625 <field name="image">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
884 AABJRU5ErkJggg==</field>
885 </record>
886 </data>
887
888
889 <data noupdate="0">
890
891 <record id="email_template_badge_received" model="email.template">
892 <field name="name">Received Badge</field>
893 <field name="body_html"><![CDATA[
894 <p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> !
895 % if ctx["user_from"]
896 This badge was granted by <strong>${ctx["user_from"]}</strong>.
897 % endif
898 </p>
899
900 % if object.comment
901 <p><em>${object.comment}</em></p>
902 % endif
903 ]]></field>
904 </record>
905 </data>
906</openerp>
0907
=== added file 'gamification/badge_view.xml'
--- gamification/badge_view.xml 1970-01-01 00:00:00 +0000
+++ gamification/badge_view.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,219 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4
5 <!-- Badge views -->
6 <record id="badge_list_action" model="ir.actions.act_window">
7 <field name="name">Badges</field>
8 <field name="res_model">gamification.badge</field>
9 <field name="view_mode">kanban,tree,form</field>
10 <field name="help" type="html">
11 <p class="oe_view_nocontent_create">
12 Click to create a badge.
13 </p>
14 <p>
15 A badge is a symbolic token granted to a user as a sign of reward.
16 It can be deserved automatically when some conditions are met or manually by users.
17 Some badges are harder than others to get with specific conditions.
18 </p>
19 </field>
20 </record>
21
22
23 <record id="view_badge_wizard_grant" model="ir.ui.view">
24 <field name="name">Grant Badge User Form</field>
25 <field name="model">gamification.badge.user.wizard</field>
26 <field name="arch" type="xml">
27 <form string="Grant Badge To" version="7.0">
28 Who would you like to reward?
29 <group>
30 <field name="user_id" nolabel="1" />
31 <field name="badge_id" invisible="1"/>
32 <field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" class="oe_no_padding" />
33 </group>
34 <footer>
35 <button string="Grant Badge" type="object" name="action_grant_badge" class="oe_highlight" /> or
36 <button string="Cancel" special="cancel" class="oe_link"/>
37 </footer>
38 </form>
39 </field>
40 </record>
41
42 <act_window domain="[]" id="action_grant_wizard"
43 name="Grant Badge"
44 target="new"
45 res_model="gamification.badge.user.wizard"
46 context="{'default_badge_id': active_id, 'badge_id': active_id}"
47 view_type="form" view_mode="form"
48 view_id="gamification.view_badge_wizard_grant" />
49
50 <record id="badge_list_view" model="ir.ui.view">
51 <field name="name">Badge List</field>
52 <field name="model">gamification.badge</field>
53 <field name="arch" type="xml">
54 <tree string="Badge List">
55 <field name="name"/>
56 <field name="stat_count"/>
57 <field name="stat_this_month"/>
58 <field name="stat_my"/>
59 <field name="rule_auth"/>
60 </tree>
61 </field>
62 </record>
63
64 <record id="badge_form_view" model="ir.ui.view">
65 <field name="name">Badge Form</field>
66 <field name="model">gamification.badge</field>
67 <field name="arch" type="xml">
68 <form string="Badge" version="7.0">
69 <header>
70 <button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" />
71 <button string="Check Badge" type="object" name="check_automatic" groups="base.group_no_one" />
72 </header>
73 <sheet>
74 <div class="oe_right oe_button_box">
75 </div>
76 <field name="image" widget='image' class="oe_left oe_avatar"/>
77 <div class="oe_title">
78 <label for="name" class="oe_edit_only"/>
79 <h1>
80 <field name="name"/>
81 </h1>
82 </div>
83 <group>
84 <field name="description" nolabel="1" placeholder="Badge Description" />
85 </group>
86 <group string="Granting">
87 <field name="rule_auth" widget="radio" />
88 <field name="rule_auth_user_ids" attrs="{'invisible': [('rule_auth','!=','users')]}" widget="many2many_tags" />
89 <field name="rule_auth_badge_ids" attrs="{'invisible': [('rule_auth','!=','having')]}" widget="many2many_tags" />
90 <field name="rule_max" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
91 <field name="rule_max_number" attrs="{'invisible': ['|',('rule_max','=',False),('rule_auth','=','nobody')]}"/>
92 <label for="stat_my_monthly_sending"/>
93 <div>
94 <field name="stat_my_monthly_sending" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
95 <div attrs="{'invisible': [('remaining_sending','=',-1)]}" class="oe_grey">
96 You can still grant <field name="remaining_sending" class="oe_inline"/> badges this month
97 </div>
98 <div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey">
99 No monthly sending limit
100 </div>
101 </div>
102 </group>
103 <group string="Rewards for challenges">
104 <field name="plan_ids" widget="many2many_kanban" nolabel="1" />
105 </group>
106 <group string="Statistics">
107 <group>
108 <field name="stat_count"/>
109 <field name="stat_this_month"/>
110 <field name="stat_count_distinct"/>
111 </group>
112 <group>
113 <field name="stat_my"/>
114 <field name="stat_my_this_month"/>
115 </group>
116 </group>
117 </sheet>
118 </form>
119 </field>
120 </record>
121
122
123 <record id="badge_kanban_view" model="ir.ui.view" >
124 <field name="name">Badge Kanban View</field>
125 <field name="model">gamification.badge</field>
126 <field name="arch" type="xml">
127 <kanban version="7.0" class="oe_background_grey">
128 <field name="name"/>
129 <field name="description"/>
130 <field name="image"/>
131 <field name="stat_my"/>
132 <field name="stat_count"/>
133 <field name="stat_this_month"/>
134 <field name="unique_owner_ids"/>
135 <field name="stat_my_monthly_sending"/>
136 <field name="remaining_sending" />
137 <field name="rule_max_number" />
138 <templates>
139 <t t-name="kanban-box">
140 <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">
141 <div class="oe_kanban_content">
142 <div class="oe_kanban_left">
143 <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>
144 </div>
145 <div class="oe_no_overflow">
146 <h4><field name="name"/></h4>
147 <t t-if="record.remaining_sending.value != 0">
148 <button type="action" name="%(action_grant_wizard)d" class="oe_highlight">Grant</button>
149 <span class="oe_grey">
150 <t t-if="record.remaining_sending.value != -1">
151 <t t-esc="record.stat_my_monthly_sending.value"/>/<t t-esc="record.rule_max_number.value"/>
152 </t>
153 <t t-if="record.remaining_sending.value == -1">
154 <t t-esc="record.stat_my_monthly_sending.value"/>/∞
155 </t>
156 </span>
157 </t>
158 <t t-if="record.remaining_sending.value == 0">
159 <div class="oe_grey">Can not grant</div>
160 </t>
161 <p>
162 <strong><t t-esc="record.stat_count.raw_value"/></strong> granted,<br/>
163 <strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
164 </p>
165 </div>
166 <div class="oe_kanban_badge_avatars">
167 <t t-if="record.description.value">
168 <p><em><field name="description"/></em></p>
169 </t>
170 <a type="object" name="get_granted_employees">
171 <t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
172 <img t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/>
173 </t>
174 </a>
175 </div>
176 </div>
177 </div>
178 </t>
179 </templates>
180 </kanban>
181 </field>
182 </record>
183
184
185 <!-- Badge user viewss -->
186
187 <record id="badge_user_kanban_view" model="ir.ui.view" >
188 <field name="name">Badge User Kanban View</field>
189 <field name="model">gamification.badge.user</field>
190 <field name="arch" type="xml">
191 <kanban version="7.0" class="oe_background_grey">
192 <field name="badge_name"/>
193 <field name="badge_id"/>
194 <field name="user_id"/>
195 <field name="comment"/>
196 <field name="create_date"/>
197 <templates>
198 <t t-name="kanban-box">
199 <div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white">
200 <div class="oe_kanban_content">
201 <div class="oe_kanban_left">
202 <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>
203 </div>
204 <h4>
205 <a type="open"><t t-esc="record.badge_name.raw_value" /></a>
206 </h4>
207 <t t-if="record.comment.raw_value">
208 <p><em><field name="comment"/></em></p>
209 </t>
210 <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>
211 </div>
212 </div>
213 </t>
214 </templates>
215 </kanban>
216 </field>
217 </record>
218 </data>
219</openerp>
0220
=== added file 'gamification/cron.xml'
--- gamification/cron.xml 1970-01-01 00:00:00 +0000
+++ gamification/cron.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,16 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4 <record forcecreate="True" id="ir_cron_check_plan"
5 model="ir.cron">
6 <field name="name">Run Goal Plan Checker</field>
7 <field name="interval_number">1</field>
8 <field name="interval_type">days</field>
9 <field name="numbercall">-1</field>
10 <field eval="False" name="doall" />
11 <field name="model">gamification.goal.plan</field>
12 <field name="function">_cron_update</field>
13 <field name="args">()</field>
14 </record>
15 </data>
16</openerp>
0\ No newline at end of file17\ No newline at end of file
118
=== added directory 'gamification/doc'
=== added file 'gamification/doc/gamification_plan_howto.rst'
--- gamification/doc/gamification_plan_howto.rst 1970-01-01 00:00:00 +0000
+++ gamification/doc/gamification_plan_howto.rst 2013-06-28 14:51:48 +0000
@@ -0,0 +1,107 @@
1How to create new challenge for my addon
2========================================
3
4Running example
5+++++++++++++++
6
7A 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.
8
9Module
10++++++
11
12The 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.
13
14If our groceries module is called ``groceries``, the structure will be consisted of three addons :
15
16::
17
18 addons/
19 ...
20 gamification/
21 groceries/
22 groceries_gamification/
23 __openerp__.py
24 groceries_goals.xml
25
26The ``__openerp__.py`` file containing the following information :
27
28::
29
30 {
31 ...
32 'depends': ['gamification','groceries'],
33 'data': ['groceries_goals.xml'],
34 'auto_install': True,
35 }
36
37
38Goal type definition
39+++++++++++++++++++++
40
41For 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 :
42
43::
44
45 <record model="gamification.goal.type" id="type_groceries_nbr_items">
46 <field name="name">Number of items</field>
47 <field name="computation_mode">count</field>
48 ...
49 </record>
50
51To 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.
52
53::
54
55 <record model="gamification.goal.type" id="type_groceries_nbr_items">
56 ...
57 <field name="model_id" eval="ref('groceries.model_groceries_item')" />
58 <field name="field_date_id" eval="ref('groceries.field_groceries_item_shopping_day')" />
59 </record>
60
61As 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.
62
63::
64
65 <record model="gamification.goal.type" id="type_groceries_nbr_items">
66 ...
67 <field name="domain">[('shopper_id', '=', user_id), ('list_id.state', '=', 'confirmed')]</field>
68 </record>
69
70An 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.
71
72If 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``.
73
74::
75
76 <record model="gamification.goal.type" id="type_groceries_nbr_items">
77 ...
78 <field name="action_id">groceries.action_groceries_list_form</field>
79 <field name="res_id_field">groceries_list.id</field>
80 </record>
81
82
83Plan definition
84++++++++++++++++
85
86Once 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.
87
88::
89
90 <record model="gamification.goal.plan" id="plan_groceries_discover">
91 <field name="name">Discover the Groceries Module</field>
92 <field name="period">once</field>
93 <field name="visibility_mode">progressbar</field>
94 <field name="report_message_frequency">never</field>
95 <field name="planline_ids" eval="[(4, ref('planline_groceries_discover1'))]"/>
96 <field name="autojoin_group_id" eval="ref('groceries.shoppers_group')" />
97 </record>
98
99To 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.
100
101::
102
103 <record model="gamification.goal.planline" id="planline_groceries_discover1">
104 <field name="type_id" eval="ref('type_groceries_nbr_items')" />
105 <field name="target_goal">3</field>
106 <field name="plan_id" eval="ref('plan_groceries_discover')" />
107 </record>
0\ No newline at end of file108\ No newline at end of file
1109
=== added file 'gamification/doc/goal.rst'
--- gamification/doc/goal.rst 1970-01-01 00:00:00 +0000
+++ gamification/doc/goal.rst 2013-06-28 14:51:48 +0000
@@ -0,0 +1,46 @@
1.. _gamification_goal:
2
3gamification.goal
4=================
5
6Models
7++++++
8
9``gamification.goal`` for the generated goals from plans
10
11.. versionchanged:: 7.0
12
13Fields
14++++++
15
16 - ``type_id`` : The related gamification.goal.type object.
17 - ``user_id`` : The user responsible for the goal. Goal type domain filtering on the user id will use that value.
18 - ``planline_id`` : if the goal is generated from a plan, the planline used to generate this goal
19 - ``plan_id`` : if the goal is generated from a plan, related link from planline_id.plan_id
20 - ``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.
21 - ``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.
22 - ``target_goal`` : the numerical value to reach (higher or lower depending of goal type) to reach a goal
23 - ``current`` : current computed value of the goal
24 - ``completeness`` : percentage of completion of a goal
25 - ``state`` :
26 - ``draft`` : goal not active and displayed in user's goal list. Only present for manual creation of goal as plans generate goals in progress.
27 - ``inprogress`` : a goal is started and is not closed yet
28 - ``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.
29 - ``reached`` : the goal is succeeded
30 - ``failed`` : the goal is failed
31 - ``canceled`` : state if the goal plan is canceled
32 - ``remind_update_delay`` : the number of day before an inprogress goal is set to inprogress_update and a reminder is sent.
33 - ``last_update`` : the date of the last modification of this goal
34 - ``computation_mode`` : related field from the linked goal type
35 - ``type_description`` : related field from the linked goal type
36 - ``type_suffix`` : related field from the linked goal type
37 - ``type_condition`` : related field from the linked goal type
38
39
40Methods
41+++++++
42
43 - ``update`` :
44 Compute the current value of goal and change states accordingly and send reminder if needed.
45 - ``get_action`` :
46 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.
047
=== added file 'gamification/doc/index.rst'
--- gamification/doc/index.rst 1970-01-01 00:00:00 +0000
+++ gamification/doc/index.rst 2013-06-28 14:51:48 +0000
@@ -0,0 +1,17 @@
1Gamification module documentation
2=================================
3
4The 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.
5
6Goals
7-----
8A **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.
9
10A **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.
11
12A **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.
13
14Badges
15------
16A **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.
17
018
=== added file 'gamification/goal.py'
--- gamification/goal.py 1970-01-01 00:00:00 +0000
+++ gamification/goal.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,404 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>
19#
20##############################################################################
21
22from openerp import SUPERUSER_ID
23from openerp.osv import fields, osv
24from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
25from openerp.tools.safe_eval import safe_eval
26from openerp.tools.translate import _
27
28from datetime import date, datetime, timedelta
29
30import logging
31
32_logger = logging.getLogger(__name__)
33
34
35class gamification_goal_type(osv.Model):
36 """Goal type definition
37
38 A goal type defining a way to set an objective and evaluate it
39 Each module wanting to be able to set goals to the users needs to create
40 a new gamification_goal_type
41 """
42 _name = 'gamification.goal.type'
43 _description = 'Gamification goal type'
44 _order = 'sequence'
45
46 def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
47 res = dict.fromkeys(ids, '')
48 for goal in self.browse(cr, uid, ids, context=context):
49 if goal.suffix and not goal.monetary:
50 res[goal.id] = goal.suffix
51 elif goal.monetary:
52 # use the current user's company currency
53 user = self.pool.get('res.users').browse(cr, uid, uid, context)
54 if goal.suffix:
55 res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
56 else:
57 res[goal.id] = user.company_id.currency_id.symbol
58 else:
59 res[goal.id] = ""
60 return res
61
62 _columns = {
63 'name': fields.char('Goal Type', required=True, translate=True),
64 'description': fields.text('Goal Description'),
65 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
66 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
67 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
68 'computation_mode': fields.selection([
69 ('manually', 'Recorded manually'),
70 ('count', 'Automatic: number of records'),
71 ('sum', 'Automatic: sum on a field'),
72 ('python', 'Automatic: execute a specific Python code'),
73 ],
74 string="Computation Mode",
75 help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
76 required=True),
77 'display_mode': fields.selection([
78 ('progress', 'Progressive (using numerical values)'),
79 ('checkbox', 'Checkbox (done or not-done)'),
80 ],
81 string="Displayed as", required=True),
82 'model_id': fields.many2one('ir.model',
83 string='Model',
84 help='The model object for the field to evaluate'),
85 'field_id': fields.many2one('ir.model.fields',
86 string='Field to Sum',
87 help='The field containing the value to evaluate'),
88 'field_date_id': fields.many2one('ir.model.fields',
89 string='Date Field',
90 help='The date to use for the time period evaluated'),
91 #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)])
92
93 'domain': fields.char("Filter Domain",
94 help="Technical filters rules to apply. Use 'user.id' (without marks) to limit the search to the evaluated user.",
95 required=True),
96 'compute_code': fields.char('Compute Code',
97 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."),
98 'condition': fields.selection([
99 ('higher', 'The higher the better'),
100 ('lower', 'The lower the better')
101 ],
102 string='Goal Performance',
103 help='A goal is considered as completed when the current value is compared to the value to reach',
104 required=True),
105 'sequence': fields.integer('Sequence', help='Sequence number for ordering', required=True),
106 'action_id': fields.many2one('ir.actions.act_window', string="Action",
107 help="The action that will be called to update the goal value."),
108 'res_id_field': fields.char("ID Field of user",
109 help="The field name on the user profile (res.users) containing the value for res_id for action.")
110 }
111
112 _defaults = {
113 'sequence': 1,
114 'condition': 'higher',
115 'computation_mode': 'manually',
116 'domain': "[]",
117 'monetary': False,
118 'display_mode': 'progress',
119 }
120
121
122class gamification_goal(osv.Model):
123 """Goal instance for a user
124
125 An individual goal for a user on a specified time period"""
126
127 _name = 'gamification.goal'
128 _description = 'Gamification goal instance'
129 _inherit = 'mail.thread'
130
131 def _get_completeness(self, cr, uid, ids, field_name, arg, context=None):
132 """Return the percentage of completeness of the goal, between 0 and 100"""
133 res = dict.fromkeys(ids, 0.0)
134 for goal in self.browse(cr, uid, ids, context=context):
135 if goal.type_condition == 'higher' and goal.current > 0:
136 res[goal.id] = min(100, round(100.0 * goal.current / goal.target_goal, 2))
137 elif goal.current < goal.target_goal:
138 # a goal 'lower than' has only two values possible: 0 or 100%
139 res[goal.id] = 100.0
140 return res
141
142 def on_change_type_id(self, cr, uid, ids, type_id=False, context=None):
143 goal_type = self.pool.get('gamification.goal.type')
144 if not type_id:
145 return {'value': {'type_id': False}}
146 goal_type = goal_type.browse(cr, uid, type_id, context=context)
147 return {'value': {'computation_mode': goal_type.computation_mode, 'type_condition': goal_type.condition}}
148
149 _columns = {
150 'type_id': fields.many2one('gamification.goal.type', string='Goal Type', required=True, ondelete="cascade"),
151 'user_id': fields.many2one('res.users', string='User', required=True),
152 'planline_id': fields.many2one('gamification.goal.planline', string='Goal Planline', ondelete="cascade"),
153 'plan_id': fields.related('planline_id', 'plan_id',
154 string="Plan",
155 type='many2one',
156 relation='gamification.goal.plan',
157 store=True),
158 'start_date': fields.date('Start Date'),
159 'end_date': fields.date('End Date'), # no start and end = always active
160 'target_goal': fields.float('To Reach',
161 required=True,
162 track_visibility='always'), # no goal = global index
163 'current': fields.float('Current Value', required=True, track_visibility='always'),
164 'completeness': fields.function(_get_completeness, type='float', string='Completeness'),
165 'state': fields.selection([
166 ('draft', 'Draft'),
167 ('inprogress', 'In progress'),
168 ('inprogress_update', 'In progress (to update)'),
169 ('reached', 'Reached'),
170 ('failed', 'Failed'),
171 ('canceled', 'Canceled'),
172 ],
173 string='State',
174 required=True,
175 track_visibility='always'),
176
177 'computation_mode': fields.related('type_id', 'computation_mode', type='char', string="Type computation mode"),
178 'remind_update_delay': fields.integer('Remind delay',
179 help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
180 'last_update': fields.date('Last Update',
181 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."),
182
183 'type_description': fields.related('type_id', 'description', type='char', string='Type Description', readonly=True),
184 'type_suffix': fields.related('type_id', 'suffix', type='char', string='Type Description', readonly=True),
185 'type_condition': fields.related('type_id', 'condition', type='char', string='Type Condition', readonly=True),
186 'type_suffix': fields.related('type_id', 'full_suffix', type="char", string="Suffix", readonly=True),
187 'type_display': fields.related('type_id', 'display_mode', type="char", string="Display Mode", readonly=True),
188 }
189
190 _defaults = {
191 'current': 0,
192 'state': 'draft',
193 'start_date': fields.date.today,
194 }
195 _order = 'create_date desc, end_date desc, type_id, id'
196
197 def _check_remind_delay(self, goal, context=None):
198 """Verify if a goal has not been updated for some time and send a
199 reminder message of needed.
200
201 :return: data to write on the goal object
202 """
203 if goal.remind_update_delay and goal.last_update:
204 delta_max = timedelta(days=goal.remind_update_delay)
205 last_update = datetime.strptime(goal.last_update, DF).date()
206 if date.today() - last_update > delta_max and goal.state == 'inprogress':
207 # generate a remind report
208 temp_obj = self.pool.get('email.template')
209 template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
210 body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
211
212 self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
213 return {'state': 'inprogress_update'}
214 return {}
215
216 def update(self, cr, uid, ids, context=None):
217 """Update the goals to recomputes values and change of states
218
219 If a manual goal is not updated for enough time, the user will be
220 reminded to do so (done only once, in 'inprogress' state).
221 If a goal reaches the target value, the status is set to reached
222 If the end date is passed (at least +1 day, time not considered) without
223 the target value being reached, the goal is set as failed."""
224
225 for goal in self.browse(cr, uid, ids, context=context):
226 #TODO: towrite may be falsy, to avoid useless write on the object. Please check the whole thing is still working
227 towrite = {}
228 if goal.state in ('draft', 'canceled'):
229 # skip if goal draft or canceled
230 continue
231
232 if goal.type_id.computation_mode == 'manually':
233 towrite.update(self._check_remind_delay(goal, context))
234
235 elif goal.type_id.computation_mode == 'python':
236 # execute the chosen method
237 values = {'cr': cr, 'uid': goal.user_id.id, 'context': context, 'self': self.pool.get('gamification.goal.type')}
238 result = safe_eval(goal.type_id.compute_code, values, {})
239
240 if type(result) in (float, int, long) and result != goal.current:
241 towrite['current'] = result
242 else:
243 _logger.exception(_('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code)))
244 # raise osv.except_osv(_('Error!'), _('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code)))
245
246 else: # count or sum
247 obj = self.pool.get(goal.type_id.model_id.model)
248 field_date_name = goal.type_id.field_date_id.name
249
250 # eval the domain with user_id replaced by goal user
251 domain = safe_eval(goal.type_id.domain, {'user': goal.user_id})
252
253 #add temporal clause(s) to the domain if fields are filled on the goal
254 if goal.start_date and field_date_name:
255 domain.append((field_date_name, '>=', goal.start_date))
256 if goal.end_date and field_date_name:
257 domain.append((field_date_name, '<=', goal.end_date))
258
259 if goal.type_id.computation_mode == 'sum':
260 field_name = goal.type_id.field_id.name
261 res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
262 new_value = res and res[0][field_name] or 0.0
263
264 else: # computation mode = count
265 new_value = obj.search(cr, uid, domain, context=context, count=True)
266
267 #avoid useless write if the new value is the same as the old one
268 if new_value != goal.current:
269 towrite['current'] = new_value
270
271 # check goal target reached
272 #TODO: reached condition is wrong because it should check time constraints.
273 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):
274 towrite['state'] = 'reached'
275
276 # check goal failure
277 elif goal.end_date and fields.date.today() > goal.end_date:
278 towrite['state'] = 'failed'
279 if towrite:
280 self.write(cr, uid, [goal.id], towrite, context=context)
281 return True
282
283 def action_start(self, cr, uid, ids, context=None):
284 """Mark a goal as started.
285
286 This should only be used when creating goals manually (in draft state)"""
287 self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
288 return self.update(cr, uid, ids, context=context)
289
290 def action_reach(self, cr, uid, ids, context=None):
291 """Mark a goal as reached.
292
293 If the target goal condition is not met, the state will be reset to In
294 Progress at the next goal update until the end date."""
295 return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
296
297 def action_fail(self, cr, uid, ids, context=None):
298 """Set the state of the goal to failed.
299
300 A failed goal will be ignored in future checks."""
301 return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
302
303 def action_cancel(self, cr, uid, ids, context=None):
304 """Reset the completion after setting a goal as reached or failed.
305
306 This is only the current state, if the date and/or target criterias
307 match the conditions for a change of state, this will be applied at the
308 next goal update."""
309 return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
310
311 def create(self, cr, uid, vals, context=None):
312 """Overwrite the create method to add a 'no_remind_goal' field to True"""
313 context = context or {}
314 context['no_remind_goal'] = True
315 return super(gamification_goal, self).create(cr, uid, vals, context=context)
316
317 def write(self, cr, uid, ids, vals, context=None):
318 """Overwrite the write method to update the last_update field to today
319
320 If the current value is changed and the report frequency is set to On
321 change, a report is generated
322 """
323 vals['last_update'] = fields.date.today()
324 result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
325 for goal in self.browse(cr, uid, ids, context=context):
326 if goal.state != "draft" and ('type_id' in vals or 'user_id' in vals):
327 # avoid drag&drop in kanban view
328 raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal'))
329
330 if vals.get('current'):
331 if 'no_remind_goal' in context:
332 # new goals should not be reported
333 continue
334
335 if goal.plan_id and goal.plan_id.report_message_frequency == 'onchange':
336 self.pool.get('gamification.goal.plan').report_progress(cr, SUPERUSER_ID, goal.plan_id, users=[goal.user_id], context=context)
337 return result
338
339 def get_action(self, cr, uid, goal_id, context=None):
340 """Get the ir.action related to update the goal
341
342 In case of a manual goal, should return a wizard to update the value
343 :return: action description in a dictionnary
344 """
345 goal = self.browse(cr, uid, goal_id, context=context)
346 if goal.type_id.action_id:
347 #open a the action linked on the goal
348 action = goal.type_id.action_id.read()[0]
349
350 if goal.type_id.res_id_field:
351 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
352 # this loop manages the cases where res_id_field is a browse record path (eg : company_id.currency_id.id)
353 field_names = goal.type_id.res_id_field.split('.')
354 res = current_user
355 for field_name in field_names[:]:
356 res = res.__getitem__(field_name)
357 action['res_id'] = res
358
359 # if one element to display, should see it in form mode if possible
360 views = action['views']
361 for (view_id, mode) in action['views']:
362 if mode == "form":
363 views = [(view_id, mode)]
364 break
365 action['views'] = views
366 return action
367
368 if goal.computation_mode == 'manually':
369 #open a wizard window to update the value manually
370 action = {
371 'name': _("Update %s") % goal.type_id.name,
372 'id': goal_id,
373 'type': 'ir.actions.act_window',
374 'views': [[False, 'form']],
375 'target': 'new',
376 }
377 action['context'] = {'default_goal_id': goal_id, 'default_current': goal.current}
378 action['res_model'] = 'gamification.goal.wizard'
379 return action
380 return False
381
382
383class goal_manual_wizard(osv.TransientModel):
384 """Wizard type to update a manual goal"""
385 _name = 'gamification.goal.wizard'
386 _columns = {
387 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True),
388 'current': fields.float('Current'),
389 }
390
391 def action_update_current(self, cr, uid, ids, context=None):
392 """Wizard action for updating the current value"""
393
394 goal_obj = self.pool.get('gamification.goal')
395
396 for wiz in self.browse(cr, uid, ids, context=context):
397 towrite = {
398 'current': wiz.current,
399 'goal_id': wiz.goal_id.id,
400 }
401 goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
402 goal_obj.update(cr, uid, [wiz.goal_id.id], context=context)
403 return {}
404# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
0405
=== added file 'gamification/goal_base_data.xml'
--- gamification/goal_base_data.xml 1970-01-01 00:00:00 +0000
+++ gamification/goal_base_data.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,232 @@
1<?xml version="1.0"?>
2<openerp>
3 <data>
4
5 <!-- goal types -->
6 <record model="gamification.goal.type" id="type_base_timezone">
7 <field name="name">Set your Timezone</field>
8 <field name="description">Configure your profile and specify your timezone</field>
9 <field name="computation_mode">count</field>
10 <field name="display_mode">checkbox</field>
11 <field name="model_id" eval="ref('base.model_res_users')" />
12 <field name="domain">[('id','=',user.id),('partner_id.tz', '!=', False)]</field>
13 <field name="action_id" eval="ref('base.action_res_users_my')" />
14 <field name="res_id_field">id</field>
15 </record>
16
17 <record model="gamification.goal.type" id="type_base_avatar">
18 <field name="name">Set your Avatar</field>
19 <field name="description">In your user preference</field>
20 <field name="computation_mode">manually</field>
21 <field name="display_mode">checkbox</field>
22 <!-- problem : default avatar != False -> manually + check in write function -->
23 <field name="action_id" eval="ref('base.action_res_users_my')" />
24 <field name="res_id_field">id</field>
25 </record>
26
27
28 <record model="gamification.goal.type" id="type_base_company_data">
29 <field name="name">Set your Company Data</field>
30 <field name="description">Write some information about your company (specify at least a name)</field>
31 <field name="computation_mode">count</field>
32 <field name="display_mode">checkbox</field>
33 <field name="model_id" eval="ref('base.model_res_company')" />
34 <field name="domain">[('user_ids', 'in', user.id), ('name', '!=', 'Your Company')]</field>
35 <field name="action_id" eval="ref('base.action_res_company_form')" />
36 <field name="res_id_field">company_id.id</field>
37 </record>
38
39 <record model="gamification.goal.type" id="type_base_company_logo">
40 <field name="name">Set your Company Logo</field>
41 <field name="computation_mode">count</field>
42 <field name="display_mode">checkbox</field>
43 <field name="model_id" eval="ref('base.model_res_company')" />
44 <field name="domain">[('user_ids', 'in', user.id),('logo', '!=', False)]</field>
45 <field name="action_id" eval="ref('base.action_res_company_form')" />
46 <field name="res_id_field">company_id.id</field>
47 </record>
48
49 <record id="action_new_simplified_res_users" model="ir.actions.act_window">
50 <field name="name">Create User</field>
51 <field name="type">ir.actions.act_window</field>
52 <field name="res_model">res.users</field>
53 <field name="view_type">form</field>
54 <field name="target">current</field>
55 <field name="view_id" ref="base.view_users_simple_form"/>
56 <field name="context">{'default_groups_ref': ['base.group_user']}</field>
57 <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>
58 </record>
59
60 <record model="gamification.goal.type" id="type_base_invite">
61 <field name="name">Invite new Users</field>
62 <field name="description">Create at least another user</field>
63 <field name="display_mode">checkbox</field>
64 <field name="computation_mode">count</field>
65 <field name="model_id" eval="ref('base.model_res_users')" />
66 <field name="domain">[('id', '!=', user.id)]</field>
67 <field name="action_id" eval="ref('action_new_simplified_res_users')" />
68 </record>
69
70 <record model="gamification.goal.type" id="type_nbr_following">
71 <field name="name">Mail Group Following</field>
72 <field name="description">Follow mail groups to receive news</field>
73 <field name="computation_mode">python</field>
74 <field name="compute_code">self.number_following(cr, uid, 'mail.group')</field>
75 <field name="action_id" eval="ref('mail.action_view_groups')" />
76 </record>
77
78
79 <!-- plans -->
80 <record model="gamification.goal.plan" id="plan_base_discover">
81 <field name="name">Complete your Profile</field>
82 <field name="period">once</field>
83 <field name="visibility_mode">progressbar</field>
84 <field name="report_message_frequency">never</field>
85 <field name="autojoin_group_id" eval="ref('base.group_user')" />
86 <field name="state">inprogress</field>
87 <field name="category">other</field>
88 </record>
89
90 <record model="gamification.goal.plan" id="plan_base_configure">
91 <field name="name">Setup your Company</field>
92 <field name="period">once</field>
93 <field name="visibility_mode">progressbar</field>
94 <field name="report_message_frequency">never</field>
95 <field name="user_ids" eval="[(4, ref('base.user_root'))]" />
96 <field name="state">inprogress</field>
97 <field name="category">other</field>
98 </record>
99
100 <!-- planlines -->
101 <record model="gamification.goal.planline" id="planline_base_discover1">
102 <field name="type_id" eval="ref('type_base_timezone')" />
103 <field name="target_goal">1</field>
104 <field name="plan_id" eval="ref('plan_base_discover')" />
105 </record>
106 <record model="gamification.goal.planline" id="planline_base_discover2">
107 <field name="type_id" eval="ref('type_base_avatar')" />
108 <field name="target_goal">1</field>
109 <field name="plan_id" eval="ref('plan_base_discover')" />
110 </record>
111
112 <record model="gamification.goal.planline" id="planline_base_admin2">
113 <field name="type_id" eval="ref('type_base_company_logo')" />
114 <field name="target_goal">1</field>
115 <field name="plan_id" eval="ref('plan_base_configure')" />
116 </record>
117 <record model="gamification.goal.planline" id="planline_base_admin1">
118 <field name="type_id" eval="ref('type_base_company_data')" />
119 <field name="target_goal">1</field>
120 <field name="plan_id" eval="ref('plan_base_configure')" />
121 </record>
122 <record model="gamification.goal.planline" id="planline_base_admin3">
123 <field name="type_id" eval="ref('type_base_invite')" />
124 <field name="target_goal">1</field>
125 <field name="plan_id" eval="ref('plan_base_configure')" />
126 </record>
127 </data>
128
129 <!-- Mail template is done in a NOUPDATE block
130 so users can freely customize/delete them -->
131 <data noupdate="0">
132 <!--Email template -->
133
134 <record id="email_template_goal_reminder" model="email.template">
135 <field name="name">Reminder for Goal Update</field>
136 <field name="body_html"><![CDATA[
137 <header>
138 <strong>Reminder ${object.name}</strong>
139 </header>
140
141 <p class="oe_grey">${object.report_header or ''}</p>
142
143 <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>
144
145 <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>
146 ]]></field>
147 </record>
148
149 <record id="email_template_goal_progress_perso" model="email.template">
150 <field name="name">Personal Goal Progress</field>
151 <field name="body_html"><![CDATA[
152 <header>
153 <strong>${object.name}</strong>
154 </header>
155 <p class="oe_grey">${object.report_header or ''}</p>
156
157 <table width="100%" border="1">
158 <tr>
159 <th>Goal</th>
160 <th>Target</th>
161 <th>Current</th>
162 <th>Completeness</th>
163 </tr>
164 % for goal in ctx["goals"]:
165 <tr
166 % if goal.completeness >= 100:
167 style="font-weight:bold;"
168 % endif
169 >
170 <td>${goal.type_id.name}</td>
171 <td>${goal.target_goal}
172 % if goal.type_suffix:
173 ${goal.type_suffix}
174 % endif
175 </td>
176 <td>${goal.current}
177 % if goal.type_suffix:
178 ${goal.type_suffix}
179 % endif
180 </td>
181 <td>${goal.completeness} %</td>
182 </tr>
183 % endfor
184 </table>]]></field>
185 </record>
186
187 <record id="email_template_goal_progress_group" model="email.template">
188 <field name="name">Group Goal Progress</field>
189 <field name="body_html"><![CDATA[
190 <header>
191 <strong>${object.name}</strong>
192 </header>
193 <p class="oe_grey">${object.report_header or ''}</p>
194
195 % for planline in ctx['planlines_boards']:
196 <table width="100%" border="1">
197 <tr>
198 <th colspan="4">${planline.goal_type.name}</th>
199 </tr>
200 <tr>
201 <th>#</th>
202 <th>Person</th>
203 <th>Completeness</th>
204 <th>Current</th>
205 </tr>
206 % for idx, goal in planline.board_goals:
207 % if idx < 3 or goal.user_id.id == user.id:
208 <tr
209 % if goal.completeness >= 100:
210 style="font-weight:bold;"
211 % endif
212 >
213 <td>${idx+1}</td>
214 <td>${goal.user_id.name}</td>
215 <td>${goal.completeness}%</td>
216 <td>${goal.current}/${goal.target_goal}
217 % if goal.type_suffix:
218 ${goal.type_suffix}
219 % endif
220 </td>
221 </tr>
222 % endif
223 % endfor
224 </table>
225
226 <br/><br/>
227
228 % endfor
229]]></field>
230 </record>
231 </data>
232</openerp>
0233
=== added file 'gamification/goal_type_data.py'
--- gamification/goal_type_data.py 1970-01-01 00:00:00 +0000
+++ gamification/goal_type_data.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,41 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>
19#
20##############################################################################
21
22from openerp.osv import osv
23
24class gamification_goal_type_data(osv.Model):
25 """Goal type data
26
27 Methods for more complex goals not possible with the 'sum' and 'count' mode.
28 Each method should return the value that will be set in the 'current' field
29 of a user's goal. The return type must be a float or integer.
30 """
31 _inherit = 'gamification.goal.type'
32
33#TODO: is it usefull to have this method in a standalone file? why not directly in the computation field of the related goal type?
34 def number_following(self, cr, uid, xml_id="mail.thread", context=None):
35 """Return the number of 'xml_id' objects the user is following
36
37 The model specified in 'xml_id' must inherit from mail.thread
38 """
39 ref_obj = self.pool.get(xml_id)
40 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
41 return ref_obj.search(cr, uid, [('message_follower_ids', '=', user.partner_id.id)], count=True, context=context)
042
=== added file 'gamification/goal_view.xml'
--- gamification/goal_view.xml 1970-01-01 00:00:00 +0000
+++ gamification/goal_view.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,310 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4
5 <!-- Goal views -->
6 <record id="goal_list_action" model="ir.actions.act_window">
7 <field name="name">Goals</field>
8 <field name="res_model">gamification.goal</field>
9 <field name="view_mode">tree,form,kanban</field>
10 <field name="context">{'search_default_group_by_user': True, 'search_default_group_by_type': True}</field>
11 <field name="help" type="html">
12 <p class="oe_view_nocontent_create">
13 Click to create a goal.
14 </p>
15 <p>
16 A goal is defined by a user and a goal type.
17 Goals can be created automatically by using goal plans.
18 </p>
19 </field>
20 </record>
21
22 <record id="goal_list_view" model="ir.ui.view">
23 <field name="name">Goal List</field>
24 <field name="model">gamification.goal</field>
25 <field name="arch" type="xml">
26 <tree string="Goal List" colors="red:state == 'failed';green:state == 'reached';grey:state == 'canceled'">
27 <field name="type_id" invisible="1" />
28 <field name="user_id" invisible="1" />
29 <field name="start_date"/>
30 <field name="end_date"/>
31 <field name="current"/>
32 <field name="target_goal"/>
33 <field name="completeness" widget="progressbar"/>
34 <field name="state" invisible="1"/>
35 <field name="planline_id" invisible="1"/>
36 </tree>
37 </field>
38 </record>
39
40 <record id="goal_form_view" model="ir.ui.view">
41 <field name="name">Goal Form</field>
42 <field name="model">gamification.goal</field>
43 <field name="arch" type="xml">
44 <form string="Goal" version="7.0">
45 <header>
46 <button string="Start goal" type="object" name="action_start" states="draft" class="oe_highlight"/>
47
48 <button string="Goal Reached" type="object" name="action_reach" states="inprogress,inprogress_update" />
49 <button string="Goal Failed" type="object" name="action_fail" states="inprogress,inprogress_update"/>
50 <button string="Reset Completion" type="object" name="action_cancel" states="failed,reached" groups="base.group_no_one" />
51 <field name="state" widget="statusbar" statusbar_visible="draft,inprogress,reached" />
52 </header>
53 <sheet>
54 <group>
55 <group string="Reference">
56 <field name="type_id" on_change="on_change_type_id(type_id)" attrs="{'readonly':[('state','!=','draft')]}"/>
57 <field name="user_id" attrs="{'readonly':[('state','!=','draft')]}"/>
58 <field name="plan_id" attrs="{'readonly':[('state','!=','draft')]}"/>
59 </group>
60 <group string="Schedule">
61 <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
62 <field name="end_date" />
63 <field name="computation_mode" invisible="1"/>
64
65 <label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/>
66 <div attrs="{'invisible':[('computation_mode','!=', 'manually')]}">
67 <field name="remind_update_delay" class="oe_inline"/>
68 days
69 </div>
70 <field name="last_update" groups="base.group_no_one"/>
71 </group>
72 <group string="Data" colspan="4">
73 <label for="target_goal" />
74 <div>
75 <field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/>
76 <field name="type_suffix" class="oe_inline"/>
77 </div>
78 <label for="current" />
79 <div>
80 <field name="current" class="oe_inline"/>
81 <button string="refresh" type="object" name="update" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" />
82 <div class="oe_grey" attrs="{'invisible':[('type_id', '=', False)]}">
83 Reached when current value is <strong><field name="type_condition" class="oe_inline"/></strong> than the target.
84 </div>
85 </div>
86 </group>
87 </group>
88 </sheet>
89 <div class="oe_chatter">
90 <field name="message_follower_ids" widget="mail_followers"/>
91 <field name="message_ids" widget="mail_thread"/>
92 </div>
93 </form>
94 </field>
95 </record>
96
97 <record id="goal_search_view" model="ir.ui.view">
98 <field name="name">Goal Search</field>
99 <field name="model">gamification.goal</field>
100 <field name="arch" type="xml">
101 <search string="Search Goals">
102 <filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/>
103 <separator/>
104 <filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
105 <filter name="inprogress" string="Current"
106 domain="[
107 '|',
108 ('state', 'in', ('inprogress', 'inprogress_update')),
109 ('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
110 ]"/>
111 <filter name="closed" string="Passed" domain="[('state', 'in', ('reached', 'failed'))]"/>
112 <separator/>
113
114 <field name="user_id"/>
115 <field name="type_id"/>
116 <field name="plan_id"/>
117 <group expand="0" string="Group By...">
118 <filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/>
119 <filter name="group_by_type" string="Goal Type" domain="[]" context="{'group_by':'type_id'}"/>
120 <filter string="State" domain="[]" context="{'group_by':'state'}"/>
121 <filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/>
122 </group>
123 </search>
124 </field>
125 </record>
126
127 <record id="goal_kanban_view" model="ir.ui.view" >
128 <field name="name">Goal Kanban View</field>
129 <field name="model">gamification.goal</field>
130 <field name="arch" type="xml">
131 <kanban version="7.0" class="oe_background_grey">
132 <field name="type_id"/>
133 <field name="user_id"/>
134 <field name="current"/>
135 <field name="completeness"/>
136 <field name="state"/>
137 <field name="target_goal"/>
138 <field name="type_condition"/>
139 <field name="type_suffix"/>
140 <field name="type_display"/>
141 <field name="start_date"/>
142 <field name="end_date"/>
143 <field name="last_update"/>
144 <templates>
145 <t t-name="kanban-tooltip">
146 <field name="type_description"/>
147 </t>
148 <t t-name="kanban-box">
149 <div t-attf-class="oe_kanban_card oe_gamification_goal oe_kanban_goal #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
150 <div class="oe_kanban_content">
151 <p><h4 class="oe_goal_name" tooltip="kanban-tooltip"><field name="type_id" /></h4></p>
152 <div class="oe_kanban_left">
153 <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" />
154 </div>
155 <field name="user_id" />
156 <div class="oe_goal_state_block">
157 <t t-if="record.type_display.raw_value == 'checkbox'">
158 <div class="oe_goal_state oe_e">
159 <t t-if="record.state.raw_value=='reached'"><span class="oe_green" title="Goal Reached">W</span></t>
160 <t t-if="record.state.raw_value=='inprogress' || record.state.raw_value=='inprogress_update'"><span title="Goal in Progress">N</span></t>
161 <t t-if="record.state.raw_value=='failed'"><span class="oe_red" title="Goal Failed">X</span></t>
162 </div>
163 </t>
164 <t t-if="record.type_display.raw_value == 'progress'">
165 <t t-if="record.type_condition.raw_value =='higher'">
166 <field name="current" widget="goal" options="{'max_field': 'target_goal', 'label_field': 'type_suffix'}"/>
167 </t>
168 <t t-if="record.type_condition.raw_value != 'higher'">
169 <div t-attf-class="oe_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'oe_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'oe_red' : 'oe_green'}">
170 <t t-esc="record.current.raw_value" />
171 </div>
172 <em>Target: less than <t t-esc="record.target_goal.raw_value" /></em>
173 </t>
174 </t>
175
176 </div>
177 <p>
178 <t t-if="record.start_date.value">
179 From <t t-esc="record.start_date.value" />
180 </t>
181 <t t-if="record.end_date.value">
182 To <t t-esc="record.end_date.value" />
183 </t>
184 </p>
185 </div>
186 </div>
187 </t>
188 </templates>
189 </kanban>
190 </field>
191 </record>
192
193
194 <!-- Goal types view -->
195
196 <record id="goal_type_list_action" model="ir.actions.act_window">
197 <field name="name">Goal Types</field>
198 <field name="res_model">gamification.goal.type</field>
199 <field name="view_mode">tree,form</field>
200 <field name="help" type="html">
201 <p class="oe_view_nocontent_create">
202 Click to create a goal type.
203 </p>
204 <p>
205 A goal type is a technical model of goal defining a condition to reach.
206 The dates, values to reach or users are defined in goal instance.
207 </p>
208 </field>
209 </record>
210
211 <record id="goal_type_list_view" model="ir.ui.view">
212 <field name="name">Goal Types List</field>
213 <field name="model">gamification.goal.type</field>
214 <field name="arch" type="xml">
215 <tree string="Goal types">
216 <field name="sequence" widget="handle"/>
217 <field name="name"/>
218 <field name="computation_mode"/>
219 </tree>
220 </field>
221 </record>
222
223
224 <record id="goal_type_form_view" model="ir.ui.view">
225 <field name="name">Goal Types Form</field>
226 <field name="model">gamification.goal.type</field>
227 <field name="arch" type="xml">
228 <form string="Goal types" version="7.0">
229 <sheet>
230 <label for="name" class="oe_edit_only"/>
231 <h1>
232 <field name="name" class="oe_inline"/>
233 </h1>
234 <label for="description" class="oe_edit_only"/>
235 <div>
236 <field name="description" class="oe_inline"/>
237 </div>
238
239 <group string="How to compute the goal?">
240
241 <field widget="radio" name="computation_mode"/>
242
243 <!-- Hide the fields below if manually -->
244 <field name="model_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
245 <field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" class="oe_inline"/>
246 <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"/>
247 <field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
248 <field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. self.my_method(cr, uid)"/>
249 <field name="condition" widget="radio"/>
250 </group>
251 <group string="Formating Options">
252 <field name="display_mode" widget="radio" />
253 <field name="suffix" placeholder="e.g. days"/>
254 <field name="monetary"/>
255 </group>
256 <group string="Clickable Goals">
257 <field name="action_id" class="oe_inline"/>
258 <field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
259 </group>
260
261 </sheet>
262 </form>
263 </field>
264 </record>
265
266 <record id="goal_type_search_view" model="ir.ui.view">
267 <field name="name">Goal Type Search</field>
268 <field name="model">gamification.goal.type</field>
269 <field name="arch" type="xml">
270 <search string="Search Goal Types">
271 <field name="name"/>
272 <field name="model_id"/>
273 <field name="field_id"/>
274 <group expand="0" string="Group By...">
275 <filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
276 <filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
277 </group>
278 </search>
279 </field>
280 </record>
281
282
283 <record id="view_goal_wizard_update_current" model="ir.ui.view">
284 <field name="name">Update the current value of the Goal</field>
285 <field name="model">gamification.goal.wizard</field>
286 <field name="arch" type="xml">
287 <form string="Grant Badge To" version="7.0">
288 Set the current value you have reached for this goal
289 <group>
290 <field name="goal_id" invisible="1"/>
291 <field name="current" />
292 </group>
293 <footer>
294 <button string="Update" type="object" name="action_update_current" class="oe_highlight" /> or
295 <button string="Cancel" special="cancel" class="oe_link"/>
296 </footer>
297 </form>
298 </field>
299 </record>
300
301
302 <!-- menus in settings - technical feature required -->
303 <menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
304 <menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
305 <menuitem id="gamification_plan_menu" parent="gamification_menu" action="goal_plan_list_action" sequence="10"/>
306 <menuitem id="gamification_type_menu" parent="gamification_menu" action="goal_type_list_action" sequence="20"/>
307 <menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
308
309 </data>
310</openerp>
0311
=== added directory 'gamification/html'
=== added file 'gamification/html/index.html'
--- gamification/html/index.html 1970-01-01 00:00:00 +0000
+++ gamification/html/index.html 2013-06-28 14:51:48 +0000
@@ -0,0 +1,86 @@
1<section class="oe_container">
2 <div class="oe_row oe_spaced">
3 <div class="oe_span12">
4 <h2 class="oe_slogan">Drive Engagement with Gamification</h2>
5 <h3 class="oe_slogan">Leverage natural desire for competition</h3>
6 <p class="oe_mt32">
7 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.
8 </p>
9 <div class="oe_span4 oe_centered">
10 <h3>Leaderboards</h3>
11 <div class="oe_row_img oe_centered">
12 <img class="oe_picture" src="crm_game_01.png">
13 </div>
14 <p>
15 Promote leaders and competition amongst sales team with performance ratios.
16 </p>
17 </div>
18 <div class="oe_span4 oe_centered">
19 <h3>Personnal Objectives</h3>
20 <div class="oe_row_img">
21 <img class="oe_picture" src="crm_game_02.png">
22 </div>
23 <p>
24 Assign clear goals to users to align them with the company objectives.
25 </p>
26 </div>
27 <div class="oe_span4 oe_centered">
28 <h3>Visual Information</h3>
29 <div class="oe_row_img oe_centered">
30 <img class="oe_picture" src="crm_game_03.png">
31 </div>
32 <p>
33 See in an glance the progress of each user.
34 </p>
35 </div>
36 </div>
37</section>
38
39
40<section class="oe_container oe_dark">
41 <div class="oe_row oe_spaced">
42 <h2 class="oe_slogan">Create custom Challenges</h2>
43 <div class="oe_span6">
44 <p class="oe_mt32">
45Use 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.
46 </p>
47 </div>
48 <div class="oe_span6">
49 <div class="oe_row_img oe_centered">
50 <img class="oe_picture oe_screenshot" src="crm_sc_05.png">
51 </div>
52 </div>
53 </div>
54</section>
55
56<section class="oe_container">
57 <div class="oe_row oe_spaced">
58 <h2 class="oe_slogan">Motivate with Badges</h2>
59 <div class="oe_span6">
60 <div class="oe_row_img oe_centered">
61 <img class="oe_picture" src="crm_linkedin.png">
62 </div>
63 </div>
64 <div class="oe_span6">
65 <p class="oe_mt32">
66Inspire 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.
67 </p>
68 </div>
69 </div>
70</section>
71
72<section class="oe_container oe_dark">
73 <div class="oe_row oe_spaced">
74 <h2 class="oe_slogan">Adapt to any module</h2>
75 <div class="oe_span6">
76 <p class="oe_mt32">
77Create 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.
78 </p>
79 </div>
80 <div class="oe_span6">
81 <div class="oe_row_img oe_centered">
82 <img class="oe_picture oe_screenshot" src="crm_sc_02.png">
83 </div>
84 </div>
85 </div>
86</section>
087
=== added file 'gamification/plan.py'
--- gamification/plan.py 1970-01-01 00:00:00 +0000
+++ gamification/plan.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,804 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>
19#
20##############################################################################
21
22from openerp.osv import fields, osv
23from openerp.tools.translate import _
24
25# from templates import TemplateHelper
26
27from datetime import date, datetime, timedelta
28import calendar
29import logging
30_logger = logging.getLogger(__name__)
31
32
33def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
34 """Return the start and end date for a goal period based on today
35
36 :return: (start_date, end_date), datetime.date objects, False if the period is
37 not defined or unknown"""
38 today = date.today()
39 if period == 'daily':
40 start_date = today
41 end_date = start_date
42 elif period == 'weekly':
43 delta = timedelta(days=today.weekday())
44 start_date = today - delta
45 end_date = start_date + timedelta(days=7)
46 elif period == 'monthly':
47 month_range = calendar.monthrange(today.year, today.month)
48 start_date = today.replace(day=1)
49 end_date = today.replace(day=month_range[1])
50 elif period == 'yearly':
51 start_date = today.replace(month=1, day=1)
52 end_date = today.replace(month=12, day=31)
53 else: # period == 'once':
54 start_date = default_start_date # for manual goal, start each time
55 end_date = default_end_date
56
57 if start_date and end_date:
58 return (start_date.isoformat(), end_date.isoformat())
59 else:
60 return (start_date, end_date)
61
62
63class gamification_goal_plan(osv.Model):
64 """Gamification goal plan
65
66 Set of predifined goals to be able to automate goal settings or
67 quickly apply several goals manually to a group of users
68
69 If 'user_ids' is defined and 'period' is different than 'one', the set will
70 be assigned to the users for each period (eg: every 1st of each month if
71 'monthly' is selected)
72 """
73
74 _name = 'gamification.goal.plan'
75 _description = 'Gamification goal plan'
76 _inherit = 'mail.thread'
77
78 def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
79 """Return the next report date based on the last report date and report
80 period.
81
82 :return: a string in isoformat representing the date"""
83 res = {}
84 for plan in self.browse(cr, uid, ids, context):
85 last = datetime.strptime(plan.last_report_date, '%Y-%m-%d').date()
86 if plan.report_message_frequency == 'daily':
87 next = last + timedelta(days=1)
88 res[plan.id] = next.isoformat()
89 elif plan.report_message_frequency == 'weekly':
90 next = last + timedelta(days=7)
91 res[plan.id] = next.isoformat()
92 elif plan.report_message_frequency == 'monthly':
93 month_range = calendar.monthrange(last.year, last.month)
94 next = last.replace(day=month_range[1]) + timedelta(days=1)
95 res[plan.id] = next.isoformat()
96 elif plan.report_message_frequency == 'yearly':
97 res[plan.id] = last.replace(year=last.year + 1).isoformat()
98 # frequency == 'once', reported when closed only
99 else:
100 res[plan.id] = False
101
102 return res
103
104 def _planline_count(self, cr, uid, ids, field_name, arg, context=None):
105 res = dict.fromkeys(ids, 0)
106 for plan in self.browse(cr, uid, ids, context):
107 res[plan.id] = len(plan.planline_ids)
108 return res
109
110 _columns = {
111 'name': fields.char('Challenge Name', required=True, translate=True),
112 'description': fields.text('Description', translate=True),
113 'state': fields.selection([
114 ('draft', 'Draft'),
115 ('inprogress', 'In Progress'),
116 ('done', 'Done'),
117 ],
118 string='State',
119 required=True),
120 'manager_id': fields.many2one('res.users',
121 string='Responsible', help="The user responsible for the challenge."),
122
123 'user_ids': fields.many2many('res.users', 'user_ids',
124 string='Users',
125 help="List of users to which the goal will be set"),
126 'autojoin_group_id': fields.many2one('res.groups',
127 string='Auto-subscription Group',
128 help='Group of users whose members will automatically be added to the users'),
129
130 'period': fields.selection([
131 ('once', 'Non recurring'),
132 ('daily', 'Daily'),
133 ('weekly', 'Weekly'),
134 ('monthly', 'Monthly'),
135 ('yearly', 'Yearly')
136 ],
137 string='Periodicity',
138 help='Period of automatic goal assigment. If none is selected, should be launched manually.',
139 required=True),
140 'start_date': fields.date('Start Date',
141 help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
142 'end_date': fields.date('End Date',
143 help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
144
145 'proposed_user_ids': fields.many2many('res.users', 'proposed_user_ids',
146 string="Suggest to users"),
147
148 'planline_ids': fields.one2many('gamification.goal.planline', 'plan_id',
149 string='Planline',
150 help="List of goals that will be set",
151 required=True),
152 'planline_count': fields.function(_planline_count, type='integer', string="Planlines"),
153
154 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
155 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
156 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
157 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
158 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
159
160 'visibility_mode': fields.selection([
161 ('progressbar', 'Individual Goals'),
162 ('board', 'Leader Board (Group Ranking)'),
163 ],
164 string="Display Mode", required=True),
165 'report_message_frequency': fields.selection([
166 ('never', 'Never'),
167 ('onchange', 'On change'),
168 ('daily', 'Daily'),
169 ('weekly', 'Weekly'),
170 ('monthly', 'Monthly'),
171 ('yearly', 'Yearly')
172 ],
173 string="Report Frequency", required=True),
174 'report_message_group_id': fields.many2one('mail.group',
175 string='Send a copy to',
176 help='Group that will receive a copy of the report in addition to the user'),
177 'report_header': fields.text('Report Header'),
178 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
179 help="Never reminded if no value or zero is specified."),
180 'last_report_date': fields.date('Last Report Date'),
181 'next_report_date': fields.function(_get_next_report_date,
182 type='date',
183 string='Next Report Date'),
184
185 'category': fields.selection([
186 ('hr', 'Human Ressources / Engagement'),
187 ('other', 'Settings / Gamification Tools'),
188 ],
189 string="Appears in", help="Define the visibility of the challenge through menus", required=True),
190 }
191
192 _defaults = {
193 'period': 'once',
194 'state': 'draft',
195 'visibility_mode' : 'progressbar',
196 'report_message_frequency' : 'onchange',
197 'last_report_date': fields.date.today,
198 'start_date': fields.date.today,
199 'manager_id': lambda s, cr, uid, c: uid,
200 'category': 'hr',
201 'reward_failure': False,
202 }
203
204 _sort = 'end_date, start_date, name'
205
206 def write(self, cr, uid, ids, vals, context=None):
207 """Overwrite the write method to add the user of groups"""
208 context = context or {}
209 if not ids:
210 return True
211
212 # unsubscribe removed users from the plan
213 # users are not able to manually unsubscribe to challenges so should
214 # do it for them when not concerned anymore
215 if vals.get('user_ids'):
216 for action_tuple in vals['user_ids']:
217 if action_tuple[0] == 3:
218 # form (3, ID), remove one
219 self.message_unsubscribe_users(cr, uid, ids, [action_tuple[1]], context=context)
220 if action_tuple[0] == 5:
221 # form (5,), remove all
222 for plan in self.browse(cr, uid, ids, context=context):
223 self.message_unsubscribe_users(cr, uid, [plan.id], [user.id for user in plan.user_ids], context=context)
224 if action_tuple[0] == 6:
225 # form (6, False, [IDS]), replace by IDS
226 for plan in self.browse(cr, uid, ids, context=context):
227 removed_users = set([user.id for user in plan.user_ids]) - set(action_tuple[2])
228 self.message_unsubscribe_users(cr, uid, [plan.id], list(removed_users), context=context)
229
230 write_res = super(gamification_goal_plan, self).write(cr, uid, ids, vals, context=context)
231
232 # add users when change the group auto-subscription
233 if 'autojoin_group_id' in vals:
234 new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
235 group_user_ids = [user.id for user in new_group.users]
236 for plan in self.browse(cr, uid, ids, context=context):
237 self.write(cr, uid, [plan.id], {'user_ids': [(4, user) for user in group_user_ids]}, context=context)
238
239 # subscribe new users to the plan
240 if 'user_ids' in vals:
241 for plan in self.browse(cr, uid, ids, context=context):
242 self.message_subscribe_users(cr, uid, ids, [user.id for user in plan.user_ids], context=context)
243 return write_res
244
245 ##### Update #####
246
247 def _cron_update(self, cr, uid, context=None, ids=False):
248 """Daily cron check.
249
250 Start planned plans (in draft and with start_date = today)
251 Create the goals for planlines not linked to goals (eg: modified the
252 plan to add planlines)
253 Update every plan running
254 """
255 if not context: context = {}
256
257 # start planned plans
258 planned_plan_ids = self.search(cr, uid, [
259 ('state', '=', 'draft'),
260 ('start_date', '<=', fields.date.today())])
261 self.action_start(cr, uid, planned_plan_ids, context=context)
262
263 # close planned plans
264 planned_plan_ids = self.search(cr, uid, [
265 ('state', '=', 'inprogress'),
266 ('end_date', '>=', fields.date.today())])
267 self.action_close(cr, uid, planned_plan_ids, context=context)
268
269 if not ids:
270 ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
271
272 return self._update_all(cr, uid, ids, context=context)
273
274 def _update_all(self, cr, uid, ids, context=None):
275 """Update the plans and related goals
276
277 :param list(int) ids: the ids of the plans to update, if False will
278 update only plans in progress."""
279 if not context: context = {}
280 goal_obj = self.pool.get('gamification.goal')
281
282 # we use yesterday to update the goals that just ended
283 yesterday = date.today() - timedelta(days=1)
284 goal_ids = goal_obj.search(cr, uid, [
285 ('plan_id', 'in', ids),
286 '|',
287 ('state', 'in', ('inprogress', 'inprogress_update')),
288 '&',
289 ('state', 'in', ('reached', 'failed')),
290 '|',
291 ('end_date', '>=', yesterday.isoformat()),
292 ('end_date', '=', False)
293 ], context=context)
294 # update every running goal already generated linked to selected plans
295 goal_obj.update(cr, uid, goal_ids, context=context)
296
297 for plan in self.browse(cr, uid, ids, context=context):
298 if plan.autojoin_group_id:
299 # check in case of new users in plan, this happens if manager removed users in plan manually
300 self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context)
301 self.generate_goals_from_plan(cr, uid, [plan.id], context=context)
302
303 # goals closed but still opened at the last report date
304 closed_goals_to_report = goal_obj.search(cr, uid, [
305 ('plan_id', '=', plan.id),
306 ('start_date', '>=', plan.last_report_date),
307 ('end_date', '<=', plan.last_report_date)
308 ])
309
310 if len(closed_goals_to_report) > 0:
311 # some goals need a final report
312 self.report_progress(cr, uid, plan, subset_goal_ids=closed_goals_to_report, context=context)
313
314 if fields.date.today() == plan.next_report_date:
315 self.report_progress(cr, uid, plan, context=context)
316
317 self.check_challenge_reward(cr, uid, ids, context=context)
318 return True
319
320 def quick_update(self, cr, uid, plan_id, context=None):
321 """Update all the goals of a plan, no generation of new goals"""
322 if not context: context = {}
323 plan = self.browse(cr, uid, plan_id, context=context)
324 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', '=', plan_id)], context=context)
325 self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
326 return True
327
328 ##### User actions #####
329
330 def action_start(self, cr, uid, ids, context=None):
331 """Start a draft goal plan
332
333 Change the state of the plan to in progress and generate related goals
334 """
335 # subscribe users if autojoin group
336 for plan in self.browse(cr, uid, ids, context=context):
337 if plan.autojoin_group_id:
338 self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context)
339
340 self.write(cr, uid, plan.id, {'state': 'inprogress'}, context=context)
341 self.message_post(cr, uid, plan.id, body="New challenge started.", context=context)
342 return self.generate_goals_from_plan(cr, uid, ids, context=context)
343
344 def action_check(self, cr, uid, ids, context=None):
345 """Check a goal plan
346
347 Create goals that haven't been created yet (eg: if added users of planlines)
348 Recompute the current value for each goal related"""
349 return self._update_all(cr, uid, ids=ids, context=context)
350
351 def action_close(self, cr, uid, ids, context=None):
352 """Close a plan in progress
353
354 Change the state of the plan to in done
355 Does NOT close the related goals, this is handled by the goal itself"""
356 self.check_challenge_reward(cr, uid, ids, force=True, context=context)
357 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
358
359 def action_reset(self, cr, uid, ids, context=None):
360 """Reset a closed goal plan
361
362 Change the state of the plan to in progress
363 Closing a plan does not affect the goals so neither does reset"""
364 return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
365
366 def action_cancel(self, cr, uid, ids, context=None):
367 """Cancel a plan in progress
368
369 Change the state of the plan to draft
370 Cancel the related goals"""
371 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
372 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', 'in', ids)], context=context)
373 self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context)
374
375 return True
376
377 def action_report_progress(self, cr, uid, ids, context=None):
378 """Manual report of a goal, does not influence automatic report frequency"""
379 for plan in self.browse(cr, uid, ids, context):
380 self.report_progress(cr, uid, plan, context=context)
381 return True
382
383 ##### Automatic actions #####
384
385 def generate_goals_from_plan(self, cr, uid, ids, context=None):
386 """Generate the list of goals linked to a plan.
387
388 If goals already exist for this planline, the planline is skipped. This
389 can be called after each change in the user or planline list.
390 :param list(int) ids: the list of plan concerned"""
391
392 for plan in self.browse(cr, uid, ids, context):
393 (start_date, end_date) = start_end_date_for_period(plan.period)
394
395 # if no periodicity, use plan dates
396 if not start_date and plan.start_date:
397 start_date = plan.start_date
398 if not end_date and plan.end_date:
399 end_date = plan.end_date
400
401 for planline in plan.planline_ids:
402 for user in plan.user_ids:
403
404 goal_obj = self.pool.get('gamification.goal')
405 domain = [('planline_id', '=', planline.id), ('user_id', '=', user.id)]
406 if start_date:
407 domain.append(('start_date', '=', start_date))
408
409 # goal already existing for this planline ?
410 if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
411
412 # resume canceled goals
413 domain.append(('state', '=', 'canceled'))
414 canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
415 goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
416 goal_obj.update(cr, uid, canceled_goal_ids, context=context)
417
418 # skip to next user
419 continue
420
421 values = {
422 'type_id': planline.type_id.id,
423 'planline_id': planline.id,
424 'user_id': user.id,
425 'target_goal': planline.target_goal,
426 'state': 'inprogress',
427 }
428
429 if start_date:
430 values['start_date'] = start_date
431 if end_date:
432 values['end_date'] = end_date
433
434 if planline.plan_id.remind_update_delay:
435 values['remind_update_delay'] = planline.plan_id.remind_update_delay
436
437 new_goal_id = goal_obj.create(cr, uid, values, context)
438
439 goal_obj.update(cr, uid, [new_goal_id], context=context)
440
441 return True
442
443 ##### JS utilities #####
444
445 def get_board_goal_info(self, cr, uid, plan, subset_goal_ids=False, context=None):
446 """Get the list of latest goals for a plan, sorted by user ranking for each planline"""
447
448 goal_obj = self.pool.get('gamification.goal')
449 planlines_boards = []
450 (start_date, end_date) = start_end_date_for_period(plan.period)
451
452 for planline in plan.planline_ids:
453
454 domain = [
455 ('planline_id', '=', planline.id),
456 ('state', 'in', ('inprogress', 'inprogress_update',
457 'reached', 'failed')),
458 ]
459
460 if subset_goal_ids:
461 goal_ids = goal_obj.search(cr, uid, domain, context=context)
462 common_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids]
463 else:
464 # if no subset goals, use the dates for restriction
465 if start_date:
466 domain.append(('start_date', '=', start_date))
467 if end_date:
468 domain.append(('end_date', '=', end_date))
469 common_goal_ids = goal_obj.search(cr, uid, domain, context=context)
470
471 board_goals = [goal for goal in goal_obj.browse(cr, uid, common_goal_ids, context=context)]
472
473 if len(board_goals) == 0:
474 # planline has no generated goals
475 continue
476
477 # most complete first, current if same percentage (eg: if several 100%)
478 sorted_board = enumerate(sorted(board_goals, key=lambda k: (k.completeness, k.current), reverse=True))
479 planlines_boards.append({'goal_type': planline.type_id, 'board_goals': sorted_board, 'target_goal': planline.target_goal})
480 return planlines_boards
481
482 def get_indivual_goal_info(self, cr, uid, user_id, plan, subset_goal_ids=False, context=None):
483 """Get the list of latest goals of a user for a plan"""
484 domain = [
485 ('plan_id', '=', plan.id),
486 ('user_id', '=', user_id),
487 ('state', 'in', ('inprogress', 'inprogress_update',
488 'reached', 'failed')),
489 ]
490 goal_obj = self.pool.get('gamification.goal')
491 (start_date, end_date) = start_end_date_for_period(plan.period)
492
493 if subset_goal_ids:
494 # use the domain for safety, don't want irrelevant report if wrong argument
495 goal_ids = goal_obj.search(cr, uid, domain, context=context)
496 related_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids]
497 else:
498 # if no subset goals, use the dates for restriction
499 if start_date:
500 domain.append(('start_date', '=', start_date))
501 if end_date:
502 domain.append(('end_date', '=', end_date))
503 related_goal_ids = goal_obj.search(cr, uid, domain, context=context)
504
505 if len(related_goal_ids) == 0:
506 return False
507
508 goals = []
509 all_done = True
510 for goal in goal_obj.browse(cr, uid, related_goal_ids, context=context):
511 if goal.end_date:
512 if goal.end_date < fields.date.today():
513 # do not include goals of previous plan run
514 continue
515 else:
516 all_done = False
517 else:
518 if goal.state == 'inprogress' or goal.state == 'inprogress_update':
519 all_done = False
520
521 goals.append(goal)
522
523 if all_done:
524 # skip plans where all goal are done or failed
525 return False
526 else:
527 return goals
528
529 ##### Reporting #####
530
531 def report_progress(self, cr, uid, plan, context=None, users=False, subset_goal_ids=False):
532 """Post report about the progress of the goals
533
534 :param plan: the plan object that need to be reported
535 :param users: the list(res.users) of users that are concerned by
536 the report. If False, will send the report to every user concerned
537 (goal users and group that receive a copy). Only used for plan with
538 a visibility mode set to 'personal'.
539 :param goal_ids: the list(int) of goal ids linked to the plan for
540 the report. If not specified, use the goals for the current plan
541 period. This parameter can be used to produce report for previous plan
542 periods.
543 :param subset_goal_ids: a list(int) of goal ids to restrict the report
544 """
545
546 context = context or {}
547 goal_obj = self.pool.get('gamification.goal')
548 # template_env = TemplateHelper()
549 temp_obj = self.pool.get('email.template')
550 ctx = context.copy()
551 if plan.visibility_mode == 'board':
552 planlines_boards = self.get_board_goal_info(cr, uid, plan, subset_goal_ids, context)
553
554 ctx.update({'planlines_boards': planlines_boards})
555 template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context)
556 body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal.plan', plan.id, context=context)
557
558 # body_html = template_env.get_template('group_progress.mako').render({'object': plan, 'planlines_boards': planlines_boards, 'uid': uid})
559
560 # send to every follower of the plan
561 self.message_post(cr, uid, plan.id,
562 body=body_html,
563 context=context,
564 subtype='mail.mt_comment')
565 if plan.report_message_group_id:
566 self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
567 body=body_html,
568 context=context,
569 subtype='mail.mt_comment')
570
571 else:
572 # generate individual reports
573 for user in users or plan.user_ids:
574 goals = self.get_indivual_goal_info(cr, uid, user.id, plan, subset_goal_ids, context=context)
575 if not goals:
576 continue
577
578 ctx.update({'goals': goals})
579 template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context)
580 body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.goal.plan', plan.id, context=context)
581 # send message only to users
582 self.message_post(cr, uid, 0,
583 body=body_html,
584 partner_ids=[(4, user.partner_id.id)],
585 context=context,
586 subtype='mail.mt_comment')
587 if plan.report_message_group_id:
588 self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
589 body=body_html,
590 context=context,
591 subtype='mail.mt_comment')
592 return self.write(cr, uid, plan.id, {'last_report_date': fields.date.today()}, context=context)
593
594 ##### Challenges #####
595
596 def accept_challenge(self, cr, uid, plan_ids, context=None, user_id=None):
597 """The user accept the suggested challenge"""
598 context = context or {}
599 user_id = user_id or uid
600 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
601 message = "%s has joined the challenge" % user.name
602 self.message_post(cr, uid, plan_ids, body=message, context=context)
603 self.write(cr, uid, plan_ids, {'proposed_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
604 return self.generate_goals_from_plan(cr, uid, plan_ids, context=context)
605
606 def discard_challenge(self, cr, uid, plan_ids, context=None, user_id=None):
607 """The user discard the suggested challenge"""
608 context = context or {}
609 user_id = user_id or uid
610 user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
611 message = "%s has refused the challenge" % user.name
612 self.message_post(cr, uid, plan_ids, body=message, context=context)
613 return self.write(cr, uid, plan_ids, {'proposed_user_ids': (3, user_id)}, context=context)
614
615 def reply_challenge_wizard(self, cr, uid, plan_id, context=None):
616 context = context or {}
617 mod_obj = self.pool.get('ir.model.data')
618 act_obj = self.pool.get('ir.actions.act_window')
619 result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
620 id = result and result[1] or False
621 result = act_obj.read(cr, uid, [id], context=context)[0]
622 result['res_id'] = plan_id
623 return result
624
625 def check_challenge_reward(self, cr, uid, plan_ids, force=False, context=None):
626 """Actions for the end of a challenge
627
628 If a reward was selected, grant it to the correct users.
629 Rewards granted at:
630 - the end date for a challenge with no periodicity
631 - the end of a period for challenge with periodicity
632 - when a challenge is manually closed
633 (if no end date, a running challenge is never rewarded)
634 """
635 context = context or {}
636 for plan in self.browse(cr, uid, plan_ids, context=context):
637 (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date)
638 yesterday = date.today() - timedelta(days=1)
639 if end_date == yesterday.isoformat() or force:
640 # open chatter message
641 message_body = _("The challenge %s is finished." % plan.name)
642
643 # reward for everybody succeeding
644 rewarded_users = []
645 if plan.reward_id:
646 for user in plan.user_ids:
647 reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
648 ('plan_id', '=', plan.id),
649 ('user_id', '=', user.id),
650 ('start_date', '=', start_date),
651 ('end_date', '=', end_date),
652 ('state', '=', 'reached')
653 ], context=context)
654 if len(reached_goal_ids) == len(plan.planline_ids):
655 self.reward_user(cr, uid, user.id, plan.reward_id.id, context)
656 rewarded_users.append(user)
657
658 if rewarded_users:
659 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])))
660 else:
661 message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
662
663 # reward bests
664 if plan.reward_first_id:
665 (first_user, second_user, third_user) = self.get_top3_users(cr, uid, plan, context)
666 if first_user:
667 self.reward_user(cr, uid, first_user.id, plan.reward_first_id.id, context)
668 message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
669 message_body += "<br/> 1. %s - %s" % (first_user.name, plan.reward_first_id.name)
670 else:
671 message_body += _("Nobody reached the required conditions to receive special badges.")
672
673 if second_user and plan.reward_second_id:
674 self.reward_user(cr, uid, second_user.id, plan.reward_second_id.id, context)
675 message_body += "<br/> 2. %s - %s" % (second_user.name, plan.reward_second_id.name)
676 if third_user and plan.reward_third_id:
677 self.reward_user(cr, uid, third_user.id, plan.reward_second_id.id, context)
678 message_body += "<br/> 3. %s - %s" % (third_user.name, plan.reward_third_id.name)
679
680 self.message_post(cr, uid, plan.id, body=message_body, context=context)
681 return True
682
683 def get_top3_users(self, cr, uid, plan, context=None):
684 """Get the top 3 users for a defined plan
685
686 Ranking criterias:
687 1. succeed every goal of the challenge
688 2. total completeness of each goal (can be over 100)
689 Top 3 is computed only for users succeeding every goal of the challenge,
690 except if reward_failure is True, in which case every user is
691 considered.
692 :return: ('first', 'second', 'third'), tuple containing the res.users
693 objects of the top 3 users. If no user meets the criterias for a rank,
694 it is set to False. Nobody can receive a rank is noone receives the
695 higher one (eg: if 'second' == False, 'third' will be False)
696 """
697 goal_obj = self.pool.get('gamification.goal')
698 (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date)
699 challengers = []
700 for user in plan.user_ids:
701 all_reached = True
702 total_completness = 0
703 # every goal of the user for the running period
704 goal_ids = goal_obj.search(cr, uid, [
705 ('plan_id', '=', plan.id),
706 ('user_id', '=', user.id),
707 ('start_date', '=', start_date),
708 ('end_date', '=', end_date)
709 ], context=context)
710 for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
711 if goal.state != 'reached':
712 all_reached = False
713 if goal.type_condition == 'higher':
714 # can be over 100
715 total_completness += 100.0 * goal.current / goal.target_goal
716 elif goal.state == 'reached':
717 # for lower goals, can not get percentage so 0 or 100
718 total_completness += 100
719
720 challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
721 sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
722
723 if len(sorted_challengers) == 0 or (not plan.reward_failure and not sorted_challengers[0]['all_reached']):
724 # nobody succeeded
725 return (False, False, False)
726 if len(sorted_challengers) == 1 or (not plan.reward_failure and not sorted_challengers[1]['all_reached']):
727 # only one user succeeded
728 return (sorted_challengers[0]['user'], False, False)
729 if len(sorted_challengers) == 2 or (not plan.reward_failure and not sorted_challengers[2]['all_reached']):
730 # only one user succeeded
731 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
732 return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
733
734 def reward_user(self, cr, uid, user_id, badge_id, context=None):
735 """Create a badge user and send the badge to him"""
736 user_badge_id = self.pool.get('gamification.badge.user').create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
737 return self.pool.get('gamification.badge').send_badge(cr, uid, badge_id, [user_badge_id], user_from=None, context=context)
738
739
740class gamification_goal_planline(osv.Model):
741 """Gamification goal planline
742
743 Predifined goal for 'gamification_goal_plan'
744 These are generic list of goals with only the target goal defined
745 Should only be created for the gamification_goal_plan object
746 """
747
748 _name = 'gamification.goal.planline'
749 _description = 'Gamification generic goal for plan'
750 _order = "sequence, sequence_type, id"
751
752 def _get_planline_types(self, cr, uid, ids, context=None):
753 """Return the ids of planline items related to the gamification.goal.type
754 objects in 'ids (used to update the value of 'sequence_type')'"""
755
756 result = {}
757 for goal_type in self.pool.get('gamification.goal.type').browse(cr, uid, ids, context=context):
758 domain = [('type_id', '=', goal_type.id)]
759 planline_ids = self.pool.get('gamification.goal.planline').search(cr, uid, domain, context=context)
760 for p_id in planline_ids:
761 result[p_id] = True
762 return result.keys()
763
764 def on_change_type_id(self, cr, uid, ids, type_id=False, context=None):
765 goal_type = self.pool.get('gamification.goal.type')
766 if not type_id:
767 return {'value': {'type_id': False}}
768 goal_type = goal_type.browse(cr, uid, type_id, context=context)
769 ret = {'value': {
770 'type_condition': goal_type.condition,
771 'type_full_suffix': goal_type.full_suffix}}
772 return ret
773
774 _columns = {
775 'name': fields.related('type_id', 'name', string="Name"),
776 'plan_id': fields.many2one('gamification.goal.plan',
777 string='Plan',
778 required=True,
779 ondelete="cascade"),
780 'type_id': fields.many2one('gamification.goal.type',
781 string='Goal Type',
782 required=True,
783 ondelete="cascade"),
784 'target_goal': fields.float('Target Value to Reach',
785 required=True),
786 'sequence': fields.integer('Sequence',
787 help='Sequence number for ordering'),
788 'sequence_type': fields.related('type_id', 'sequence',
789 type='integer',
790 string='Sequence',
791 readonly=True,
792 store={
793 'gamification.goal.type': (_get_planline_types, ['sequence'], 10),
794 }),
795 'type_condition': fields.related('type_id', 'condition', type="selection",
796 readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
797 'type_suffix': fields.related('type_id', 'suffix', type="char", readonly=True, string="Unit"),
798 'type_monetary': fields.related('type_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
799 'type_full_suffix': fields.related('type_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
800 }
801
802 _default = {
803 'sequence': 1,
804 }
0805
=== added file 'gamification/plan_view.xml'
--- gamification/plan_view.xml 1970-01-01 00:00:00 +0000
+++ gamification/plan_view.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,291 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4
5 <record id="goal_plan_list_view" model="ir.ui.view">
6 <field name="name">Challenges List</field>
7 <field name="model">gamification.goal.plan</field>
8 <field name="arch" type="xml">
9 <tree string="Goal types" colors="blue:state == 'draft';grey:state == 'done'">
10 <field name="name"/>
11 <field name="period"/>
12 <field name="manager_id"/>
13 <field name="state"/>
14 </tree>
15 </field>
16 </record>
17
18 <record id="goals_from_plan_act" model="ir.actions.act_window">
19 <field name="res_model">gamification.goal</field>
20 <field name="name">Related Goals</field>
21 <field name="view_mode">kanban,tree</field>
22 <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>
23 <field name="help" type="html">
24 <p>
25 There is no goals associated to this challenge matching your search.
26 Make sure that your challenge is active and assigned to at least one user.
27 </p>
28 </field>
29 </record>
30
31 <record id="goal_plan_form_view" model="ir.ui.view">
32 <field name="name">Challenge Form</field>
33 <field name="model">gamification.goal.plan</field>
34 <field name="arch" type="xml">
35 <form string="Goal types" version="7.0">
36 <header>
37 <button string="Start Now" type="object" name="action_start" states="draft" class="oe_highlight"/>
38 <button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/>
39 <button string="Close Challenge" type="object" name="action_close" states="inprogress" class="oe_highlight"/>
40 <button string="Reset to Draft" type="object" name="action_cancel" states="inprogress"/>
41 <button string="Reset Completion" type="object" name="action_reset" states="done"/>
42 <button string="Report Progress" type="object" name="action_report_progress" states="inprogress,done" groups="base.group_no_one"/>
43 <field name="state" widget="statusbar"/>
44 </header>
45 <sheet>
46
47 <div class="oe_title">
48 <label for="name" class="oe_edit_only"/>
49 <h1>
50 <field name="name" placeholder="e.g. Monthly Sales Objectives"/>
51 </h1>
52 <label for="user_ids" class="oe_edit_only" string="Assign Challenge To"/>
53 <div>
54 <field name="user_ids" widget="many2many_tags" />
55 </div>
56 </div>
57
58 <!-- action buttons -->
59 <div class="oe_right oe_button_box">
60 <button type="action" name="%(goals_from_plan_act)d" string="Related Goals" attrs="{'invisible': [('state','=','draft')]}" />
61 </div>
62 <group>
63 <group>
64 <field name="period" attrs="{'readonly':[('state','!=','draft')]}"/>
65 <field name="visibility_mode" widget="radio" colspan="1" />
66 </group>
67 <group>
68 <field name="manager_id"/>
69 <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
70 <field name="end_date" attrs="{'readonly':[('state','!=','draft')]}"/>
71 </group>
72 </group>
73 <notebook>
74 <page string="Goals">
75 <field name="planline_ids" nolabel="1" colspan="4">
76 <tree string="Planline List" version="7.0" editable="bottom" >
77 <field name="sequence" widget="handle"/>
78 <field name="type_id" on_change="on_change_type_id(type_id)" />
79 <field name="type_condition"/>
80 <field name="target_goal"/>
81 <field name="type_full_suffix"/>
82 </tree>
83 </field>
84 <field name="description" placeholder="Describe the challenge: what is does, who it targets, why it matters..."/>
85 </page>
86 <page string="Reward">
87 <group>
88 <field name="reward_id"/>
89 <field name="reward_first_id" />
90 <field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" />
91 <field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" />
92 <field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" />
93 </group>
94 <div class="oe_grey">
95 <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>
96 </div>
97 </page>
98 <page string="Advanced Options">
99 <group string="Subscriptions">
100 <field name="autojoin_group_id" />
101 <field name="proposed_user_ids" widget="many2many_tags" />
102 </group>
103 <group string="Notification Messages">
104 <field name="report_message_frequency" />
105 <field name="report_header" placeholder="e.g. The following message contains the current progress of the sale team..." attrs="{'invisible': [('report_message_frequency','=','never')]}" />
106 <field name="report_message_group_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
107 </group>
108 <group string="Reminders for Manual Goals">
109 <label for="remind_update_delay" />
110 <div>
111 <field name="remind_update_delay" class="oe_inline"/> days
112 </div>
113 </group>
114 <group string="Category" groups="base.group_no_one">
115 <field name="category" widget="radio" />
116 </group>
117 </page>
118 </notebook>
119
120 </sheet>
121 <div class="oe_chatter">
122 <field name="message_follower_ids" widget="mail_followers"/>
123 <field name="message_ids" widget="mail_thread"/>
124 </div>
125 </form>
126 </field>
127 </record>
128
129 <record model="ir.ui.view" id="view_goal_plan_kanban">
130 <field name="name">Challenge Kanban</field>
131 <field name="model">gamification.goal.plan</field>
132 <field name="arch" type="xml">
133 <kanban version="7.0" class="oe_background_grey">
134 <field name="planline_ids"/>
135 <field name="planline_count"/>
136 <field name="user_ids"/>
137 <templates>
138 <t t-name="kanban-box">
139 <div t-attf-class="oe_kanban_card oe_kanban_goal oe_kanban_global_click">
140 <div class="oe_dropdown_toggle oe_dropdown_kanban">
141 <span class="oe_e">í</span>
142 <ul class="oe_dropdown_menu">
143 <li><a type="edit">Configure Challenge</a></li>
144 </ul>
145 </div>
146 <div class="oe_kanban_content">
147
148 <h4><field name="name"/></h4>
149 <div class="oe_kanban_project_list">
150 <a type="action" name="%(goals_from_plan_act)d" style="margin-right: 10px">
151 <span t-if="record.planline_count.raw_value gt 1"><field name="planline_count"/> Goals</span>
152 <span t-if="record.planline_count.raw_value lt 2"><field name="planline_count"/> Goal</span>
153 </a>
154 </div>
155 <div class="oe_kanban_badge_avatars">
156 <t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
157 <img t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/>
158 </t>
159 </div>
160 </div>
161 </div>
162 </t>
163 </templates>
164 </kanban>
165 </field>
166 </record>
167
168 <record id="goal_plan_list_action" model="ir.actions.act_window">
169 <field name="name">Challenges</field>
170 <field name="res_model">gamification.goal.plan</field>
171 <field name="view_mode">kanban,tree,form</field>
172 <field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
173 <field name="help" type="html">
174 <p class="oe_view_nocontent_create">
175 Click to create a challenge.
176 </p>
177 <p>
178 Assign a list of goals to chosen users to evaluate them.
179 The challenge can use a period (weekly, monthly...) for automatic creation of goals.
180 The goals are created for the specified users or member of the group.
181 </p>
182 </field>
183 </record>
184 <!-- Specify form view ID to avoid selecting view_challenge_wizard -->
185 <record id="goal_plan_list_action_view1" model="ir.actions.act_window.view">
186 <field eval="1" name="sequence"/>
187 <field name="view_mode">kanban</field>
188 <field name="act_window_id" ref="goal_plan_list_action"/>
189 <field name="view_id" ref="view_goal_plan_kanban"/>
190 </record>
191 <record id="goal_plan_list_action_view2" model="ir.actions.act_window.view">
192 <field eval="10" name="sequence"/>
193 <field name="view_mode">form</field>
194 <field name="act_window_id" ref="goal_plan_list_action"/>
195 <field name="view_id" ref="goal_plan_form_view"/>
196 </record>
197
198 <!-- Planline -->
199 <record id="goal_planline_list_view" model="ir.ui.view">
200 <field name="name">Goal planline list</field>
201 <field name="model">gamification.goal.planline</field>
202 <field name="arch" type="xml">
203 <tree string="planline list" >
204 <field name="type_id"/>
205 <field name="target_goal"/>
206 </tree>
207 </field>
208 </record>
209
210
211 <record id="goal_plan_search_view" model="ir.ui.view">
212 <field name="name">Challenge Search</field>
213 <field name="model">gamification.goal.plan</field>
214 <field name="arch" type="xml">
215 <search string="Search Challenges">
216 <filter name="inprogress" string="Running Challenges"
217 domain="[('state', '=', 'inprogress')]"/>
218 <filter name="hr_plans" string="HR Challenges"
219 domain="[('category', '=', 'hr')]"/>
220 <field name="name"/>
221 <group expand="0" string="Group By...">
222 <filter string="State" domain="[]" context="{'group_by':'state'}"/>
223 <filter string="Period" domain="[]" context="{'group_by':'period'}"/>
224 </group>
225 </search>
226 </field>
227 </record>
228
229
230 <record id="view_challenge_wizard" model="ir.ui.view">
231 <field name="name">Challenge Wizard</field>
232 <field name="model">gamification.goal.plan</field>
233 <field name="arch" type="xml">
234 <form string="Challenge" version="7.0">
235 <field name="reward_failure" invisible="1"/>
236 <div class="oe_title">
237 <h1><field name="name" nolabel="1" readonly="1"/></h1>
238 </div>
239 <field name="description" nolabel="1" readonly="1" />
240 <group>
241 <field name="start_date" readonly="1" />
242 <field name="end_date" readonly="1" />
243 <field name="user_ids" string="Participating" readonly="1" widget="many2many_tags" />
244 <field name="proposed_user_ids" string="Invited" readonly="1" widget="many2many_tags" />
245 </group>
246 <group string="Goals">
247 <field name="planline_ids" nolabel="1" readonly="1" colspan="4">
248 <tree string="Planline List" version="7.0" editable="bottom" >
249 <field name="sequence" widget="handle"/>
250 <field name="type_id"/>
251 <field name="type_condition"/>
252 <field name="target_goal"/>
253 <field name="type_full_suffix"/>
254 </tree>
255 </field>
256 </group>
257 <group string="Reward">
258 <div class="oe_grey" attrs="{'invisible': ['|',('reward_id','!=',False),('reward_first_id','!=',False)]}">
259 There is no reward upon completion of this challenge.
260 </div>
261 <group attrs="{'invisible': [('reward_id','=',False),('reward_first_id','=',False)]}">
262 <field name="reward_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
263 <field name="reward_first_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
264 <field name="reward_second_id" readonly="1" attrs="{'invisible': [('reward_second_id','=', False)]}" />
265 <field name="reward_third_id" readonly="1" attrs="{'invisible': [('reward_third_id','=', False)]}" />
266 </group>
267 <div class="oe_grey" attrs="{'invisible': [('reward_failure','=',False)]}">
268 Even if the challenge is failed, best challengers will be rewarded
269 </div>
270 </group>
271 <footer>
272 <center>
273 <button string="Accept" type="object" name="accept_challenge" class="oe_highlight" />
274 <button string="Reject" type="object" name="discard_challenge"/> or
275 <button string="reply later" special="cancel" class="oe_link"/>
276 </center>
277 </footer>
278 </form>
279 </field>
280 </record>
281
282 <record id="challenge_wizard" model="ir.actions.act_window">
283 <field name="name">Challenge Description</field>
284 <field name="res_model">gamification.goal.plan</field>
285 <field name="view_type">form</field>
286 <field name="view_id" ref="view_challenge_wizard"/>
287 <field name="target">new</field>
288 </record>
289
290 </data>
291</openerp>
0\ No newline at end of file292\ No newline at end of file
1293
=== added file 'gamification/res_users.py'
--- gamification/res_users.py 1970-01-01 00:00:00 +0000
+++ gamification/res_users.py 2013-06-28 14:51:48 +0000
@@ -0,0 +1,177 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# OpenERP, Open Source Management Solution
5# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>
19#
20##############################################################################
21
22from openerp.osv import osv
23
24
25class res_users_gamification_group(osv.Model):
26 """ Update of res.users class
27 - if adding groups to an user, check gamification.goal.plan linked to
28 this group, and the user. This is done by overriding the write method.
29 """
30 _name = 'res.users'
31 _inherit = ['res.users']
32
33 def write(self, cr, uid, ids, vals, context=None):
34 write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
35 if vals.get('groups_id'):
36 # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
37 user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
38 user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
39
40 goal_plan_obj = self.pool.get('gamification.goal.plan')
41 plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
42 if plan_ids:
43 goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
44
45 if vals.get('image'):
46 goal_type_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'type_base_avatar', context)
47 goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('type_id', '=', goal_type_id.id), ('user_id', 'in', ids)], context=context)
48 values = {'state': 'reached', 'current': 1}
49 self.pool.get('gamification.goal').write(cr, uid, goal_ids, values, context=context)
50 return write_res
51
52 def get_goals_todo_info(self, cr, uid, context=None):
53 """Return the list of goals assigned to the user, grouped by plan
54
55 This method intends to return processable data in javascript in the
56 goal_list_to_do template. The output format is not constant as the
57 required information is different between individual and board goal
58 types
59 :return: list of dictionnaries for each goal to display
60 """
61 all_goals_info = []
62 plan_obj = self.pool.get('gamification.goal.plan')
63
64 plan_ids = plan_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
65 for plan in plan_obj.browse(cr, uid, plan_ids, context=context):
66 # serialize goals info to be able to use it in javascript
67 serialized_goals_info = {
68 'id': plan.id,
69 'name': plan.name,
70 'visibility_mode': plan.visibility_mode,
71 }
72 user = self.browse(cr, uid, uid, context=context)
73 serialized_goals_info['currency'] = user.company_id.currency_id.id
74
75 if plan.visibility_mode == 'board':
76 # board report should be grouped by planline for all users
77 goals_info = plan_obj.get_board_goal_info(cr, uid, plan, subset_goal_ids=False, context=context)
78
79 if len(goals_info) == 0:
80 # plan with no valid planlines
81 continue
82
83 serialized_goals_info['planlines'] = []
84 for planline_board in goals_info:
85 vals = {'type_name': planline_board['goal_type'].name,
86 'type_description': planline_board['goal_type'].description,
87 'type_condition': planline_board['goal_type'].condition,
88 'type_computation_mode': planline_board['goal_type'].computation_mode,
89 'type_monetary': planline_board['goal_type'].monetary,
90 'type_suffix': planline_board['goal_type'].suffix,
91 'type_action': True if planline_board['goal_type'].action_id else False,
92 'type_display': planline_board['goal_type'].display_mode,
93 'target_goal': planline_board['target_goal'],
94 'goals': []}
95 for goal in planline_board['board_goals']:
96 # Keep only the Top 3 and the current user
97 if goal[0] > 2 and goal[1].user_id.id != uid:
98 continue
99
100 vals['goals'].append({
101 'rank': goal[0] + 1,
102 'id': goal[1].id,
103 'user_id': goal[1].user_id.id,
104 'user_name': goal[1].user_id.name,
105 'state': goal[1].state,
106 'completeness': goal[1].completeness,
107 'current': goal[1].current,
108 'target_goal': goal[1].target_goal,
109 })
110 if uid == goal[1].user_id.id:
111 vals['own_goal_id'] = goal[1].id
112 serialized_goals_info['planlines'].append(vals)
113
114 else:
115 # individual report are simply a list of goal
116 goals_info = plan_obj.get_indivual_goal_info(cr, uid, uid, plan, subset_goal_ids=False, context=context)
117
118 if not goals_info:
119 continue
120
121 serialized_goals_info['goals'] = []
122 for goal in goals_info:
123 serialized_goals_info['goals'].append({
124 'id': goal.id,
125 'type_name': goal.type_id.name,
126 'type_description': goal.type_description,
127 'type_condition': goal.type_id.condition,
128 'type_monetary': goal.type_id.monetary,
129 'type_suffix': goal.type_id.suffix,
130 'type_action': True if goal.type_id.action_id else False,
131 'type_display': goal.type_id.display_mode,
132 'state': goal.state,
133 'completeness': goal.completeness,
134 'computation_mode': goal.computation_mode,
135 'current': goal.current,
136 'target_goal': goal.target_goal,
137 })
138
139 all_goals_info.append(serialized_goals_info)
140 return all_goals_info
141
142 def get_challenge_suggestions(self, cr, uid, context=None):
143 """Return the list of goal plans suggested to the user"""
144 plan_info = []
145 goal_plan_obj = self.pool.get('gamification.goal.plan')
146 plan_ids = goal_plan_obj.search(cr, uid, [('proposed_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
147 for plan in goal_plan_obj.browse(cr, uid, plan_ids, context=context):
148 values = {
149 'id': plan.id,
150 'name': plan.name,
151 'description': plan.description,
152 }
153 plan_info.append(values)
154 return plan_info
155
156
157class res_groups_gamification_group(osv.Model):
158 """ Update of res.groups class
159 - if adding users from a group, check gamification.goal.plan linked to
160 this group, and the user. This is done by overriding the write method.
161 """
162 _name = 'res.groups'
163 _inherit = 'res.groups'
164
165 def write(self, cr, uid, ids, vals, context=None):
166 write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
167 if vals.get('users'):
168 # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
169 user_ids = [command[1] for command in vals['users'] if command[0] == 4]
170 user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
171
172 goal_plan_obj = self.pool.get('gamification.goal.plan')
173 plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context)
174 if plan_ids:
175 goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
176 return write_res
177# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
0178
=== added directory 'gamification/security'
=== added file 'gamification/security/gamification_security.xml'
--- gamification/security/gamification_security.xml 1970-01-01 00:00:00 +0000
+++ gamification/security/gamification_security.xml 2013-06-28 14:51:48 +0000
@@ -0,0 +1,31 @@
1<?xml version="1.0" ?>
2<openerp>
3 <data noupdate="1">
4 <record model="ir.module.category" id="module_goal_category">
5 <field name="name">Gamification</field>
6 <field name="description"></field>
7 <field name="sequence">17</field>
8 </record>
9 <record id="group_goal_manager" model="res.groups">
10 <field name="name">Manager</field>
11 <field name="category_id" ref="module_goal_category"/>
12 <field name="users" eval="[(4, ref('base.user_root'))]"/>
13 </record>
14
15 <record id="goal_user_visibility" model="ir.rule">
16 <field name="name">User can only see his/her goals or goal from the same plan in board visibility</field>
17 <field name="model_id" ref="model_gamification_goal"/>
18 <field name="groups" eval="[(4, ref('base.group_user'))]"/>
19 <field name="perm_read" eval="True"/>
20 <field name="perm_write" eval="True"/>
21 <field name="perm_create" eval="False"/>
22 <field name="perm_unlink" eval="False"/>
23 <field name="domain_force">[
24 '|',
25 ('user_id','=',user.id),
26 '&amp;',
27 ('plan_id.user_ids','in',user.id),
28 ('plan_id.visibility_mode','=','board')]</field>
29 </record>
30 </data>
31</openerp>
032
=== added file 'gamification/security/ir.model.access.csv'
--- gamification/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ gamification/security/ir.model.access.csv 2013-06-28 14:51:48 +0000
@@ -0,0 +1,20 @@
1id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
2
3goal_anybody,"Goal Anybody",model_gamification_goal,,1,1,0,0
4goal_manager,"Goal Manager",model_gamification_goal,group_goal_manager,1,1,1,1
5
6goal_type_anybody,"Goal Type Anybody",model_gamification_goal_type,,1,0,0,0
7goal_type_manager,"Goal Type Manager",model_gamification_goal_type,group_goal_manager,1,1,1,1
8
9plan_anybody,"Goal Plan Anybody",model_gamification_goal_plan,,1,0,0,0
10plan_manager,"Goal Plan Manager",model_gamification_goal_plan,group_goal_manager,1,1,1,1
11
12planline_anybody,"Goal Planline Anybody",model_gamification_goal_planline,,1,0,0,0
13planline_manager,"Goal Planline Manager",model_gamification_goal_planline,group_goal_manager,1,1,1,1
14
15badge_anybody,"Badge Anybody",model_gamification_badge,,1,0,0,0
16badge_manager,"Badge Manager",model_gamification_badge,group_goal_manager,1,1,1,1
17
18badge_user_anybody,"Badge-user Anybody",model_gamification_badge_user,,1,0,0,0
19badge_user_user,"Badge-user User",model_gamification_badge_user,base.group_user,1,1,1,0
20badge_user_manager,"Badge-user Manager",model_gamification_badge_user,group_goal_manager,1,1,1,1
021
=== added directory 'gamification/static'
=== added directory 'gamification/static/lib'
=== added directory 'gamification/static/lib/justgage'
=== added file 'gamification/static/lib/justgage/justgage.js'
--- gamification/static/lib/justgage/justgage.js 1970-01-01 00:00:00 +0000
+++ gamification/static/lib/justgage/justgage.js 2013-06-28 14:51:48 +0000
@@ -0,0 +1,946 @@
1/**
2 * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
3 * Check http://www.justgage.com for official releases
4 * Licensed under MIT.
5 * @author Bojan Djuricic (@Toorshia)
6 *
7 * LATEST UPDATES
8
9 * -----------------------------
10 * April 18, 2013.
11 * -----------------------------
12 * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
13 * width - force gauge width
14 * height - force gauge height
15
16 * -----------------------------
17 * April 17, 2013.
18 * -----------------------------
19 * fix - https://github.com/toorshia/justgage/issues/49
20
21 * -----------------------------
22 * April 01, 2013.
23 * -----------------------------
24 * fix - https://github.com/toorshia/justgage/issues/46
25
26 * -----------------------------
27 * March 26, 2013.
28 * -----------------------------
29 * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
30
31 * -----------------------------
32 * March 23, 2013.
33 * -----------------------------
34 * counter - option to animate value in counting fashion
35 * fix - https://github.com/toorshia/justgage/issues/45
36
37 * -----------------------------
38 * March 13, 2013.
39 * -----------------------------
40 * refresh method - added optional 'max' parameter to use when you need to update max value
41
42 * -----------------------------
43 * February 26, 2013.
44 * -----------------------------
45 * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
46 * fixed a missing parameters bug when calling generateShadow() for IE < 9
47
48 * -----------------------------
49 * December 31, 2012.
50 * -----------------------------
51 * fixed text y-position for hidden divs - workaround for Raphael <tspan> 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
52 * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
53 * Min and Max labels are now auto-off when in donut mode
54 * Start angle in donut mode is now 90
55 * donutStartAngle - option to define start angle for donut
56
57 * -----------------------------
58 * November 25, 2012.
59 * -----------------------------
60 * Option to define custom rendering function for displayed value
61
62 * -----------------------------
63 * November 19, 2012.
64 * -----------------------------
65 * Config.value is now updated after gauge refresh
66
67 * -----------------------------
68 * November 13, 2012.
69 * -----------------------------
70 * Donut display mode added
71 * Option to hide value label
72 * Option to enable responsive gauge size
73 * Removed default title attribute
74 * Option to accept min and max defined as string values
75 * Option to configure value symbol
76 * Fixed bad aspect ratio calculations
77 * Option to configure minimum font size for all texts
78 * Option to show shorthand big numbers (human friendly)
79 */
80
81 JustGage = function(config) {
82
83 // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
84 // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
85
86 var obj = this;
87
88 // configurable parameters
89 obj.config =
90 {
91 // id : string
92 // this is container element id
93 id : config.id,
94
95 // parentNode : node object
96 // this is container element
97 parentNode : (config.parentNode) ? config.parentNode : null,
98
99 // width : int
100 // gauge width
101 width : (config.width) ? config.width : null,
102
103 // height : int
104 // gauge height
105 height : (config.height) ? config.height : null,
106
107 // title : string
108 // gauge title
109 title : (config.title) ? config.title : "",
110
111 // titleFontColor : string
112 // color of gauge title
113 titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
114
115 // value : int
116 // value gauge is showing
117 value : (config.value) ? config.value : 0,
118
119 // valueFontColor : string
120 // color of label showing current value
121 valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
122
123 // symbol : string
124 // special symbol to show next to value
125 symbol : (config.symbol) ? config.symbol : "",
126
127 // min : int
128 // min value
129 min : (config.min) ? parseFloat(config.min) : 0,
130
131 // max : int
132 // max value
133 max : (config.max) ? parseFloat(config.max) : 100,
134
135 // humanFriendlyDecimal : int
136 // number of decimal places for our human friendly number to contain
137 humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
138
139 // textRenderer: func
140 // function applied before rendering text
141 textRenderer : (config.textRenderer) ? config.textRenderer : null,
142
143 // gaugeWidthScale : float
144 // width of the gauge element
145 gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
146
147 // gaugeColor : string
148 // background color of gauge element
149 gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
150
151 // label : string
152 // text to show below value
153 label : (config.label) ? config.label : "",
154
155 // labelFontColor : string
156 // color of label showing label under value
157 labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
158
159 // shadowOpacity : int
160 // 0 ~ 1
161 shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
162
163 // shadowSize: int
164 // inner shadow size
165 shadowSize : (config.shadowSize) ? config.shadowSize : 5,
166
167 // shadowVerticalOffset : int
168 // how much shadow is offset from top
169 shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
170
171 // levelColors : string[]
172 // colors of indicator, from lower to upper, in RGB format
173 levelColors : (config.levelColors) ? config.levelColors : [
174 "#a9d70b",
175 "#f9c802",
176 "#ff0000"
177 ],
178
179 // startAnimationTime : int
180 // length of initial animation
181 startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
182
183 // startAnimationType : string
184 // type of initial animation (linear, >, <, <>, bounce)
185 startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
186
187 // refreshAnimationTime : int
188 // length of refresh animation
189 refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
190
191 // refreshAnimationType : string
192 // type of refresh animation (linear, >, <, <>, bounce)
193 refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
194
195 // donutStartAngle : int
196 // angle to start from when in donut mode
197 donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
198
199 // valueMinFontSize : int
200 // absolute minimum font size for the value
201 valueMinFontSize : config.valueMinFontSize || 16,
202
203 // titleMinFontSize
204 // absolute minimum font size for the title
205 titleMinFontSize : config.titleMinFontSize || 10,
206
207 // labelMinFontSize
208 // absolute minimum font size for the label
209 labelMinFontSize : config.labelMinFontSize || 10,
210
211 // minLabelMinFontSize
212 // absolute minimum font size for the minimum label
213 minLabelMinFontSize : config.minLabelMinFontSize || 10,
214
215 // maxLabelMinFontSize
216 // absolute minimum font size for the maximum label
217 maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
218
219 // hideValue : bool
220 // hide value text
221 hideValue : (config.hideValue) ? config.hideValue : false,
222
223 // hideMinMax : bool
224 // hide min and max values
225 hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
226
227 // hideInnerShadow : bool
228 // hide inner shadow
229 hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
230
231 // humanFriendly : bool
232 // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
233 humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
234
235 // noGradient : bool
236 // whether to use gradual color change for value, or sector-based
237 noGradient : (config.noGradient) ? config.noGradient : false,
238
239 // donut : bool
240 // show full donut gauge
241 donut : (config.donut) ? config.donut : false,
242
243 // relativeGaugeSize : bool
244 // whether gauge size should follow changes in container element size
245 relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
246
247 // counter : bool
248 // animate level number change
249 counter : (config.counter) ? config.counter : false,
250
251 // decimals : int
252 // number of digits after floating point
253 decimals : (config.decimals) ? config.decimals : 0,
254
255 // customSectors : [] of objects
256 // number of digits after floating point
257 customSectors : (config.customSectors) ? config.customSectors : []
258 };
259
260 // variables
261 var
262 canvasW,
263 canvasH,
264 widgetW,
265 widgetH,
266 aspect,
267 dx,
268 dy,
269 titleFontSize,
270 titleX,
271 titleY,
272 valueFontSize,
273 valueX,
274 valueY,
275 labelFontSize,
276 labelX,
277 labelY,
278 minFontSize,
279 minX,
280 minY,
281 maxFontSize,
282 maxX,
283 maxY;
284
285 // overflow values
286 if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
287 if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
288 obj.originalValue = config.value;
289
290 // create canvas
291 if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
292 obj.canvas = Raphael(obj.config.id, "100%", "100%");
293 } else if (obj.config.parentNode !== null) {
294 obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
295 }
296
297 if (obj.config.relativeGaugeSize === true) {
298 obj.canvas.setViewBox(0, 0, 200, 150, true);
299 }
300
301 // canvas dimensions
302 if (obj.config.relativeGaugeSize === true) {
303 canvasW = 200;
304 canvasH = 150;
305 } else if (obj.config.width !== null && obj.config.height !== null) {
306 canvasW = obj.config.width;
307 canvasH = obj.config.height;
308 } else if (obj.config.parentNode !== null) {
309 obj.canvas.setViewBox(0, 0, 200, 150, true);
310 canvasW = 200;
311 canvasH = 150;
312 } else {
313 canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
314 canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
315 }
316
317 // widget dimensions
318 if (obj.config.donut === true) {
319
320 // DONUT *******************************
321
322 // width more than height
323 if(canvasW > canvasH) {
324 widgetH = canvasH;
325 widgetW = widgetH;
326 // width less than height
327 } else if (canvasW < canvasH) {
328 widgetW = canvasW;
329 widgetH = widgetW;
330 // if height don't fit, rescale both
331 if(widgetH > canvasH) {
332 aspect = widgetH / canvasH;
333 widgetH = widgetH / aspect;
334 widgetW = widgetH / aspect;
335 }
336 // equal
337 } else {
338 widgetW = canvasW;
339 widgetH = widgetW;
340 }
341
342 // delta
343 dx = (canvasW - widgetW)/2;
344 dy = (canvasH - widgetH)/2;
345
346 // title
347 titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
348 titleX = dx + widgetW / 2;
349 titleY = dy + widgetH / 11;
350
351 // value
352 valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
353 valueX = dx + widgetW / 2;
354 if(obj.config.label !== '') {
355 valueY = dy + widgetH / 1.85;
356 } else {
357 valueY = dy + widgetH / 1.7;
358 }
359
360 // label
361 labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
362 labelX = dx + widgetW / 2;
363 labelY = valueY + labelFontSize;
364
365 // min
366 minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
367 minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
368 minY = labelY;
369
370 // max
371 maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
372 maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
373 maxY = labelY;
374
375 } else {
376 // HALF *******************************
377
378 // width more than height
379 if(canvasW > canvasH) {
380 widgetH = canvasH;
381 widgetW = widgetH * 1.25;
382 //if width doesn't fit, rescale both
383 if(widgetW > canvasW) {
384 aspect = widgetW / canvasW;
385 widgetW = widgetW / aspect;
386 widgetH = widgetH / aspect;
387 }
388 // width less than height
389 } else if (canvasW < canvasH) {
390 widgetW = canvasW;
391 widgetH = widgetW / 1.25;
392 // if height don't fit, rescale both
393 if(widgetH > canvasH) {
394 aspect = widgetH / canvasH;
395 widgetH = widgetH / aspect;
396 widgetW = widgetH / aspect;
397 }
398 // equal
399 } else {
400 widgetW = canvasW;
401 widgetH = widgetW * 0.75;
402 }
403
404 // delta
405 dx = (canvasW - widgetW)/2;
406 dy = (canvasH - widgetH)/2;
407
408 // title
409 titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
410 titleX = dx + widgetW / 2;
411 titleY = dy + widgetH / 6.4;
412
413 // value
414 valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
415 valueX = dx + widgetW / 2;
416 valueY = dy + widgetH / 1.275;
417
418 // label
419 labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
420 labelX = dx + widgetW / 2;
421 labelY = valueY + valueFontSize / 2 + 5;
422
423 // min
424 minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
425 minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
426 minY = labelY;
427
428 // max
429 maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
430 maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
431 maxY = labelY;
432 }
433
434 // parameters
435 obj.params = {
436 canvasW : canvasW,
437 canvasH : canvasH,
438 widgetW : widgetW,
439 widgetH : widgetH,
440 dx : dx,
441 dy : dy,
442 titleFontSize : titleFontSize,
443 titleX : titleX,
444 titleY : titleY,
445 valueFontSize : valueFontSize,
446 valueX : valueX,
447 valueY : valueY,
448 labelFontSize : labelFontSize,
449 labelX : labelX,
450 labelY : labelY,
451 minFontSize : minFontSize,
452 minX : minX,
453 minY : minY,
454 maxFontSize : maxFontSize,
455 maxX : maxX,
456 maxY : maxY
457 };
458
459 // var clear
460 canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
461
462 // pki - custom attribute for generating gauge paths
463 obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
464
465 var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
466
467 if (donut) {
468 alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
469 Ro = w / 2 - w / 7;
470 Ri = Ro - w / 6.666666666666667 * gws;
471
472 Cx = w / 2 + dx;
473 Cy = h / 1.95 + dy;
474
475 Xo = w / 2 + dx + Ro * Math.cos(alpha);
476 Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
477 Xi = w / 2 + dx + Ri * Math.cos(alpha);
478 Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
479
480 path += "M" + (Cx - Ri) + "," + Cy + " ";
481 path += "L" + (Cx - Ro) + "," + Cy + " ";
482 if (value > ((max - min) / 2)) {
483 path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
484 }
485 path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
486 path += "L" + Xi + "," + Yi + " ";
487 if (value > ((max - min) / 2)) {
488 path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
489 }
490 path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
491 path += "Z ";
492
493 return { path: path };
494
495 } else {
496 alpha = (1 - (value - min) / (max - min)) * Math.PI;
497 Ro = w / 2 - w / 10;
498 Ri = Ro - w / 6.666666666666667 * gws;
499
500 Cx = w / 2 + dx;
501 Cy = h / 1.25 + dy;
502
503 Xo = w / 2 + dx + Ro * Math.cos(alpha);
504 Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
505 Xi = w / 2 + dx + Ri * Math.cos(alpha);
506 Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
507
508 path += "M" + (Cx - Ri) + "," + Cy + " ";
509 path += "L" + (Cx - Ro) + "," + Cy + " ";
510 path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
511 path += "L" + Xi + "," + Yi + " ";
512 path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
513 path += "Z ";
514
515 return { path: path };
516 }
517
518 // var clear
519 alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
520 };
521
522 // gauge
523 obj.gauge = obj.canvas.path().attr({
524 "stroke": "none",
525 "fill": obj.config.gaugeColor,
526 pki: [
527 obj.config.max,
528 obj.config.min,
529 obj.config.max,
530 obj.params.widgetW,
531 obj.params.widgetH,
532 obj.params.dx,
533 obj.params.dy,
534 obj.config.gaugeWidthScale,
535 obj.config.donut
536 ]
537 });
538
539 // level
540 obj.level = obj.canvas.path().attr({
541 "stroke": "none",
542 "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),
543 pki: [
544 obj.config.min,
545 obj.config.min,
546 obj.config.max,
547 obj.params.widgetW,
548 obj.params.widgetH,
549 obj.params.dx,
550 obj.params.dy,
551 obj.config.gaugeWidthScale,
552 obj.config.donut
553 ]
554 });
555 if(obj.config.donut) {
556 obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
557 }
558
559 // title
560 obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
561 obj.txtTitle.attr({
562 "font-size":obj.params.titleFontSize,
563 "font-weight":"bold",
564 "font-family":"Arial",
565 "fill":obj.config.titleFontColor,
566 "fill-opacity":"1"
567 });
568 setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
569
570 // value
571 obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
572 obj.txtValue.attr({
573 "font-size":obj.params.valueFontSize,
574 "font-weight":"bold",
575 "font-family":"Arial",
576 "fill":obj.config.valueFontColor,
577 "fill-opacity":"0"
578 });
579 setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
580
581 // label
582 obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
583 obj.txtLabel.attr({
584 "font-size":obj.params.labelFontSize,
585 "font-weight":"normal",
586 "font-family":"Arial",
587 "fill":obj.config.labelFontColor,
588 "fill-opacity":"0"
589 });
590 setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
591
592 // min
593 obj.txtMinimum = obj.config.min;
594 if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
595 obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
596 obj.txtMin.attr({
597 "font-size":obj.params.minFontSize,
598 "font-weight":"normal",
599 "font-family":"Arial",
600 "fill":obj.config.labelFontColor,
601 "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
602 });
603 setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
604
605 // max
606 obj.txtMaximum = obj.config.max;
607 if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
608 obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
609 obj.txtMax.attr({
610 "font-size":obj.params.maxFontSize,
611 "font-weight":"normal",
612 "font-family":"Arial",
613 "fill":obj.config.labelFontColor,
614 "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
615 });
616 setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
617
618 var defs = obj.canvas.canvas.childNodes[1];
619 var svg = "http://www.w3.org/2000/svg";
620
621 if (ie < 9) {
622 onCreateElementNsReady(function() {
623 obj.generateShadow(svg, defs);
624 });
625 } else {
626 obj.generateShadow(svg, defs);
627 }
628
629 // var clear
630 defs, svg = null;
631
632 // set value to display
633 if(obj.config.textRenderer) {
634 obj.originalValue = obj.config.textRenderer(obj.originalValue);
635 } else if(obj.config.humanFriendly) {
636 obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
637 } else {
638 obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
639 }
640
641 if(obj.config.counter === true) {
642 //on each animation frame
643 eve.on("raphael.anim.frame." + (obj.level.id), function() {
644 var currentValue = obj.level.attr("pki");
645 if(obj.config.textRenderer) {
646 obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
647 } else if(obj.config.humanFriendly) {
648 obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
649 } else {
650 obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
651 }
652 setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
653 currentValue = null;
654 });
655 //on animation end
656 eve.on("raphael.anim.finish." + (obj.level.id), function() {
657 obj.txtValue.attr({"text" : obj.originalValue});
658 setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
659 });
660 } else {
661 //on animation start
662 eve.on("raphael.anim.start." + (obj.level.id), function() {
663 obj.txtValue.attr({"text" : obj.originalValue});
664 setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
665 });
666 }
667
668 // animate gauge level, value & label
669 obj.level.animate({
670 pki: [
671 obj.config.value,
672 obj.config.min,
673 obj.config.max,
674 obj.params.widgetW,
675 obj.params.widgetH,
676 obj.params.dx,
677 obj.params.dy,
678 obj.config.gaugeWidthScale,
679 obj.config.donut
680 ]
681 }, obj.config.startAnimationTime, obj.config.startAnimationType);
682 obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
683 obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
684};
685
686/** Refresh gauge level */
687JustGage.prototype.refresh = function(val, max) {
688
689 var obj = this;
690 var displayVal, color, max = max || null;
691
692 // set new max
693 if(max !== null) {
694 obj.config.max = max;
695
696 obj.txtMaximum = obj.config.max;
697 if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
698 obj.txtMax.attr({"text" : obj.txtMaximum});
699 setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
700 }
701
702 // overflow values
703 displayVal = val;
704 if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
705 if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
706
707 color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
708
709 if(obj.config.textRenderer) {
710 displayVal = obj.config.textRenderer(displayVal);
711 } else if( obj.config.humanFriendly ) {
712 displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
713 } else {
714 displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
715 }
716 obj.originalValue = displayVal;
717 obj.config.value = val * 1;
718
719 if(!obj.config.counter) {
720 obj.txtValue.attr({"text":displayVal});
721 setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
722 }
723
724 obj.level.animate({
725 pki: [
726 obj.config.value,
727 obj.config.min,
728 obj.config.max,
729 obj.params.widgetW,
730 obj.params.widgetH,
731 obj.params.dx,
732 obj.params.dy,
733 obj.config.gaugeWidthScale,
734 obj.config.donut
735 ],
736 "fill":color
737 }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
738
739 // var clear
740 obj, displayVal, color, max = null;
741};
742
743/** Generate shadow */
744JustGage.prototype.generateShadow = function(svg, defs) {
745
746 var obj = this;
747 var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
748
749 // FILTER
750 gaussFilter = document.createElementNS(svg,"filter");
751 gaussFilter.setAttribute("id","inner-shadow");
752 defs.appendChild(gaussFilter);
753
754 // offset
755 feOffset = document.createElementNS(svg,"feOffset");
756 feOffset.setAttribute("dx", 0);
757 feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
758 gaussFilter.appendChild(feOffset);
759
760 // blur
761 feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
762 feGaussianBlur.setAttribute("result","offset-blur");
763 feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
764 gaussFilter.appendChild(feGaussianBlur);
765
766 // composite 1
767 feComposite1 = document.createElementNS(svg,"feComposite");
768 feComposite1.setAttribute("operator","out");
769 feComposite1.setAttribute("in", "SourceGraphic");
770 feComposite1.setAttribute("in2","offset-blur");
771 feComposite1.setAttribute("result","inverse");
772 gaussFilter.appendChild(feComposite1);
773
774 // flood
775 feFlood = document.createElementNS(svg,"feFlood");
776 feFlood.setAttribute("flood-color","black");
777 feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
778 feFlood.setAttribute("result","color");
779 gaussFilter.appendChild(feFlood);
780
781 // composite 2
782 feComposite2 = document.createElementNS(svg,"feComposite");
783 feComposite2.setAttribute("operator","in");
784 feComposite2.setAttribute("in", "color");
785 feComposite2.setAttribute("in2","inverse");
786 feComposite2.setAttribute("result","shadow");
787 gaussFilter.appendChild(feComposite2);
788
789 // composite 3
790 feComposite3 = document.createElementNS(svg,"feComposite");
791 feComposite3.setAttribute("operator","over");
792 feComposite3.setAttribute("in", "shadow");
793 feComposite3.setAttribute("in2","SourceGraphic");
794 gaussFilter.appendChild(feComposite3);
795
796 // set shadow
797 if (!obj.config.hideInnerShadow) {
798 obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
799 obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
800 }
801
802 // var clear
803 gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
804
805};
806
807/** Get color for value */
808function getColor(val, pct, col, noGradient, custSec) {
809
810 var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
811 var noGradient = noGradient || custSec.length > 0;
812
813 if(custSec.length > 0) {
814 for(var i = 0; i < custSec.length; i++) {
815 if(val > custSec[i].lo && val <= custSec[i].hi) {
816 return custSec[i].color;
817 }
818 }
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: