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
1=== added directory 'gamification'
2=== added file 'gamification/__init__.py'
3--- gamification/__init__.py 1970-01-01 00:00:00 +0000
4+++ gamification/__init__.py 2013-06-28 14:51:48 +0000
5@@ -0,0 +1,5 @@
6+import goal
7+import goal_type_data
8+import plan
9+import res_users
10+import badge
11
12=== added file 'gamification/__openerp__.py'
13--- gamification/__openerp__.py 1970-01-01 00:00:00 +0000
14+++ gamification/__openerp__.py 2013-06-28 14:51:48 +0000
15@@ -0,0 +1,62 @@
16+# -*- coding: utf-8 -*-
17+##############################################################################
18+#
19+# OpenERP, Open Source Management Solution
20+# Copyright (C) 2004-2013 Tiny SPRL (<http://openerp.com>).
21+#
22+# This program is free software: you can redistribute it and/or modify
23+# it under the terms of the GNU Affero General Public License as
24+# published by the Free Software Foundation, either version 3 of the
25+# License, or (at your option) any later version.
26+#
27+# This program is distributed in the hope that it will be useful,
28+# but WITHOUT ANY WARRANTY; without even the implied warranty of
29+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30+# GNU Affero General Public License for more details.
31+#
32+# You should have received a copy of the GNU Affero General Public License
33+# along with this program. If not, see <http://www.gnu.org/licenses/>.
34+#
35+##############################################################################
36+{
37+ 'name': 'Gamification',
38+ 'version': '1.0',
39+ 'author': 'OpenERP SA',
40+ 'category': 'Human Ressources',
41+ 'depends': ['mail', 'email_template'],
42+ 'description': """
43+Gamification process
44+====================
45+The Gamification module provides ways to evaluate and motivate the users of OpenERP.
46+
47+The users can be evaluated using goals and numerical objectives to reach.
48+**Goals** are assigned through **plans** to evaluate and compare members of a team with each others and through time.
49+
50+For non-numerical achievements, **badges** can be granted to users. From a simple "thank you" to an exceptional achievement, a badge is an easy way to exprimate gratitude to a user for their good work.
51+
52+Both goals and badges are flexibles and can be adapted to a large range of modules and actions. When installed, this module creates easy goals to help new users to discover OpenERP and configure their user profile.
53+""",
54+
55+ 'data': [
56+ 'plan_view.xml',
57+ 'badge_view.xml',
58+ 'goal_view.xml',
59+ 'cron.xml',
60+ 'security/gamification_security.xml',
61+ 'security/ir.model.access.csv',
62+ 'goal_base_data.xml',
63+ 'badge_data.xml',
64+ ],
65+ 'test': [
66+ 'test/goal_demo.yml'
67+ ],
68+ 'installable': True,
69+ 'application': True,
70+ 'auto_install': True,
71+ 'css': ['static/src/css/gamification.css'],
72+ 'js': [
73+ 'static/src/js/gamification.js',
74+ 'static/lib/justgage/justgage.js',
75+ ],
76+ 'qweb': ['static/src/xml/gamification.xml'],
77+}
78
79=== added file 'gamification/badge.py'
80--- gamification/badge.py 1970-01-01 00:00:00 +0000
81+++ gamification/badge.py 2013-06-28 14:51:48 +0000
82@@ -0,0 +1,299 @@
83+# -*- coding: utf-8 -*-
84+##############################################################################
85+#
86+# OpenERP, Open Source Management Solution
87+# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
88+#
89+# This program is free software: you can redistribute it and/or modify
90+# it under the terms of the GNU General Public License as published by
91+# the Free Software Foundation, either version 3 of the License, or
92+# (at your option) any later version.
93+#
94+# This program is distributed in the hope that it will be useful,
95+# but WITHOUT ANY WARRANTY; without even the implied warranty of
96+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
97+# GNU General Public License for more details.
98+#
99+# You should have received a copy of the GNU General Public License
100+# along with this program. If not, see <http://www.gnu.org/licenses/>
101+#
102+##############################################################################
103+
104+from openerp import SUPERUSER_ID
105+from openerp.osv import fields, osv
106+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
107+from openerp.tools.translate import _
108+
109+# from templates import TemplateHelper
110+from datetime import date
111+import logging
112+
113+_logger = logging.getLogger(__name__)
114+
115+
116+class gamification_badge_user(osv.Model):
117+ """User having received a badge"""
118+
119+ _name = 'gamification.badge.user'
120+ _description = 'Gamification user badge'
121+ _order = "create_date desc"
122+
123+ _columns = {
124+ 'user_id': fields.many2one('res.users', string="User", required=True),
125+ 'badge_id': fields.many2one('gamification.badge', string='Badge', required=True),
126+ 'comment': fields.text('Comment'),
127+ 'badge_name': fields.related('badge_id', 'name', type="char", string="Badge Name"),
128+ 'create_date': fields.datetime('Created', readonly=True),
129+ 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
130+ }
131+
132+
133+class gamification_badge(osv.Model):
134+ """Badge object that users can send and receive"""
135+
136+ _name = 'gamification.badge'
137+ _description = 'Gamification badge'
138+ _inherit = ['mail.thread']
139+
140+ def _get_owners_info(self, cr, uid, ids, name, args, context=None):
141+ """Return:
142+ the list of unique res.users ids having received this badge
143+ the total number of time this badge was granted
144+ the total number of users this badge was granted to
145+ """
146+ result = dict.fromkeys(ids, False)
147+ for obj in self.browse(cr, uid, ids, context=context):
148+ res = set()
149+ for owner in obj.owner_ids:
150+ res.add(owner.user_id.id)
151+ res = list(res)
152+ result[obj.id] = {
153+ 'unique_owner_ids': res,
154+ 'stat_count': len(obj.owner_ids),
155+ 'stat_count_distinct': len(res)
156+ }
157+ return result
158+
159+ def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
160+ """Return stats related to badge users"""
161+ result = dict.fromkeys(ids, False)
162+ badge_user_obj = self.pool.get('gamification.badge.user')
163+ first_month_day = date.today().replace(day=1).strftime(DF)
164+ for obj in self.browse(cr, uid, ids, context=context):
165+ result[obj.id] = {
166+ 'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid)], context=context, count=True),
167+ 'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_date', '>=', first_month_day)], context=context, count=True),
168+ 'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True),
169+ 'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', obj.id), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True)
170+ }
171+ return result
172+
173+ def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
174+ """Computes the number of badges remaining the user can send
175+
176+ 0 if not allowed or no remaining
177+ integer if limited sending
178+ -1 if infinite (should not be displayed)
179+ """
180+ result = dict.fromkeys(ids, False)
181+ for badge in self.browse(cr, uid, ids, context=context):
182+ if self._can_grant_badge(cr, uid, uid, badge.id, context) != 1:
183+ #if the user cannot grant this badge at all, result is 0
184+ result[badge.id] = 0
185+ elif not badge.rule_max:
186+ #if there is no limitation, -1 is returned which mean 'infinite'
187+ result[badge.id] = -1
188+ else:
189+ result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
190+ return result
191+
192+ _columns = {
193+ 'name': fields.char('Badge', required=True, translate=True),
194+ 'description': fields.text('Description'),
195+ 'image': fields.binary("Image", help="This field holds the image used for the badge, limited to 256x256"),
196+ # image_select: selection with a on_change to fill image with predefined picts
197+ 'rule_auth': fields.selection([
198+ ('everyone', 'Everyone'),
199+ ('users', 'A selected list of users'),
200+ ('having', 'People having some badges'),
201+ ('nobody', 'No one, assigned through challenges'),
202+ ],
203+ string="Allowance to Grant",
204+ help="Who can grant this badge",
205+ required=True),
206+ 'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
207+ string='Authorized Users',
208+ help="Only these people can give this badge"),
209+ 'rule_auth_badge_ids': fields.many2many('gamification.badge',
210+ 'rel_badge_badge', 'badge1_id', 'badge2_id',
211+ string='Required Badges',
212+ help="Only the people having these badges can give this badge"),
213+
214+ 'rule_max': fields.boolean('Monthly Limited Sending',
215+ help="Check to set a monthly limit per person of sending this badge"),
216+ 'rule_max_number': fields.integer('Limitation Number',
217+ help="The maximum number of time this badge can be sent per month per person."),
218+ 'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
219+ type="integer",
220+ string='My Monthly Sending Total',
221+ multi='badge_users',
222+ help="The number of time the current user has sent this badge this month."),
223+ 'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
224+ string='Remaining Sending Allowed', help="If a maxium is set"),
225+
226+ 'plan_ids': fields.one2many('gamification.goal.plan', 'reward_id',
227+ string="Reward of Challenges"),
228+
229+ 'goal_type_ids': fields.many2many('gamification.goal.type',
230+ string='Goals Linked',
231+ help="The users that have succeeded theses goals will receive automatically the badge."),
232+
233+ 'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
234+ string='Owners', help='The list of instances of this badge granted to users'),
235+ 'unique_owner_ids': fields.function(_get_owners_info,
236+ string='Unique Owners',
237+ help="The list of unique users having received this badge.",
238+ multi='unique_users',
239+ type="many2many", relation="res.users"),
240+
241+ 'stat_count': fields.function(_get_owners_info, string='Total',
242+ type="integer",
243+ multi='unique_users',
244+ help="The number of time this badge has been received."),
245+ 'stat_count_distinct': fields.function(_get_owners_info,
246+ type="integer",
247+ string='Number of users',
248+ multi='unique_users',
249+ help="The number of time this badge has been received by unique users."),
250+ 'stat_this_month': fields.function(_get_badge_user_stats,
251+ type="integer",
252+ string='Monthly total',
253+ multi='badge_users',
254+ help="The number of time this badge has been received this month."),
255+ 'stat_my': fields.function(_get_badge_user_stats, string='My Total',
256+ type="integer",
257+ multi='badge_users',
258+ help="The number of time the current user has received this badge."),
259+ 'stat_my_this_month': fields.function(_get_badge_user_stats,
260+ type="integer",
261+ string='My Monthly Total',
262+ multi='badge_users',
263+ help="The number of time the current user has received this badge this month."),
264+ }
265+
266+ _defaults = {
267+ 'rule_auth': 'everyone',
268+ }
269+
270+ def send_badge(self, cr, uid, badge_id, badge_user_ids, user_from=False, context=None):
271+ """Send a notification to a user for receiving a badge
272+
273+ Does NOT verify constrains on badge granting.
274+ The users are added to the owner_ids (create badge_user if needed)
275+ The stats counters are incremented
276+ :param badge_id: id of the badge to deserve
277+ :param badge_user_ids: list(int) of badge users that will receive the badge
278+ :param user_from: optional id of the res.users object that has sent the badge
279+ """
280+ badge = self.browse(cr, uid, badge_id, context=context)
281+ # template_env = TemplateHelper()
282+
283+ res = None
284+ temp_obj = self.pool.get('email.template')
285+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context)
286+ ctx = context.copy()
287+ for badge_user in self.pool.get('gamification.badge.user').browse(cr, uid, badge_user_ids, context=context):
288+
289+ ctx.update({'user_from': self.pool.get('res.users').browse(cr, uid, user_from).name})
290+
291+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=ctx)
292+
293+ # as SUPERUSER as normal user don't have write access on a badge
294+ res = self.message_post(cr, SUPERUSER_ID, badge.id, partner_ids=[badge_user.user_id.partner_id.id], body=body_html, type='comment', subtype='mt_comment', context=context)
295+ return res
296+
297+ def check_granting(self, cr, uid, user_from_id, badge_id, context=None):
298+ """Check the user 'user_from_id' can grant the badge 'badge_id' and raise the appropriate exception
299+ if not"""
300+ status_code = self._can_grant_badge(cr, uid, user_from_id, badge_id, context=context)
301+ if status_code == 1:
302+ return True
303+ elif status_code == 2:
304+ raise osv.except_osv(_('Warning!'), _('This badge can not be sent by users.'))
305+ elif status_code == 3:
306+ raise osv.except_osv(_('Warning!'), _('You are not in the user allowed list.'))
307+ elif status_code == 4:
308+ raise osv.except_osv(_('Warning!'), _('You do not have the required badges.'))
309+ elif status_code == 5:
310+ raise osv.except_osv(_('Warning!'), _('You have already sent this badge too many time this month.'))
311+ else:
312+ _logger.exception("Unknown badge status code: %d" % int(status_code))
313+ return False
314+
315+ def _can_grant_badge(self, cr, uid, user_from_id, badge_id, context=None):
316+ """Check if a user can grant a badge to another user
317+
318+ :param user_from_id: the id of the res.users trying to send the badge
319+ :param badge_id: the granted badge id
320+ :return: integer representing the permission.
321+ 1: can grant
322+ 2: nobody can send
323+ 3: user not in the allowed list
324+ 4: don't have the required badges
325+ 5: user's monthly limit reached
326+ """
327+ badge = self.browse(cr, uid, badge_id, context=context)
328+
329+ if badge.rule_auth == 'nobody':
330+ return 2
331+
332+ elif badge.rule_auth == 'users' and user_from_id not in [user.id for user in badge.rule_auth_user_ids]:
333+ return 3
334+
335+ elif badge.rule_auth == 'having':
336+ all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', user_from_id)], context=context)
337+ for required_badge in badge.rule_auth_badge_ids:
338+ if required_badge.id not in all_user_badges:
339+ return 4
340+
341+ if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
342+ return 5
343+
344+ # badge.rule_auth == 'everyone' -> no check
345+ return 1
346+
347+
348+class grant_badge_wizard(osv.TransientModel):
349+ """ Wizard allowing to grant a badge to a user"""
350+
351+ _name = 'gamification.badge.user.wizard'
352+ _columns = {
353+ 'user_id': fields.many2one("res.users", string='User', required=True),
354+ 'badge_id': fields.many2one("gamification.badge", string='Badge', required=True),
355+ 'comment': fields.text('Comment'),
356+ }
357+
358+ def action_grant_badge(self, cr, uid, ids, context=None):
359+ """Wizard action for sending a badge to a chosen user"""
360+
361+ badge_obj = self.pool.get('gamification.badge')
362+ badge_user_obj = self.pool.get('gamification.badge.user')
363+
364+ for wiz in self.browse(cr, uid, ids, context=context):
365+ if uid == wiz.user_id.id:
366+ raise osv.except_osv(_('Warning!'), _('You can not grant a badge to yourself'))
367+
368+ #check if the badge granting is legitimate
369+ if badge_obj.check_granting(cr, uid, user_from_id=uid, badge_id=wiz.badge_id.id, context=context):
370+ #create the badge
371+ values = {
372+ 'user_id': wiz.user_id.id,
373+ 'badge_id': wiz.badge_id.id,
374+ 'comment': wiz.comment,
375+ }
376+ badge_user = badge_user_obj.create(cr, uid, values, context=context)
377+ #notify the user
378+ result = badge_obj.send_badge(cr, uid, wiz.badge_id.id, [badge_user], user_from=uid, context=context)
379+
380+ return result
381+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
382
383=== added file 'gamification/badge_data.xml'
384--- gamification/badge_data.xml 1970-01-01 00:00:00 +0000
385+++ gamification/badge_data.xml 2013-06-28 14:51:48 +0000
386@@ -0,0 +1,906 @@
387+<?xml version="1.0" encoding="utf-8"?>
388+<openerp>
389+ <data noupdate="1">
390+ <record id="badge_good_job" model="gamification.badge">
391+ <field name="name">Good Job</field>
392+ <field name="description">You did great at your job.</field>
393+ <field name="rule_auth">everyone</field>
394+ <field name="image">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
604+ RU5ErkJggg==</field>
605+ </record>
606+
607+ <record id="badge_problem_solver" model="gamification.badge">
608+ <field name="name">Problem Solver</field>
609+ <field name="description">No one can solve challenges like you do.</field>
610+ <field name="rule_auth">everyone</field>
611+ <field name="image">iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QAVgB7ADQ3APIQAAAACXBI
612+ WXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QMdCQASe88aBAAAIABJREFUeNrsvXeYZld15vtbe59z
613+ vlj1Va7OUa1Wq6UWykIJJBAKgEDkYMCAB2xsj8O1fa/TjO0Zj+1nDL4YX3DAeEyOJuNLECAJhFBA
614+ qdXq3K1OFbvyF8/Ze80f+1SpwQotITFg6zxPPWp1f/WdsNde4V3vuw48czxzPHM8czxzPHM8c/xH
615+ POQ/wH090XvUR/nzMwbwU34fcsKfzQl/Zx7h7350wRXwj/Bnf4IR6L9Hg5Cf8QUXwJ7w3+gRforA
616+ OmADsAyo5Z/3wHT+MwbsAeaBFMge4cfnP+4EY9BnDOAnv+jmhMWOgSRf2HOBrcVKdHqpVthQrSUr
617+ 40JUTUqWUldCsRKRlCOiyICAKqQdT9Z2dFoZjZkOrUZKu55O12fSQ/WZ5oEs1Z3AA8C9wCzQBjon
618+ GIn7EYN4xgCe5kWPgQIwDKyPi/ZlSdG+vGdZpbbilB5Wbu4hKVk11oiqoqo/siy6dMv6SDcvYERA
619+ wGdKfbbN0V0zjB2YY26yOdJuZl/O2v5rwH5gAmieYBQ/k8YgP8XXdeKiF4Ey8ELgD1Zt7lmx+dnL
620+ qS0rSWQNXj0iBpGHF1qRJ3VzP2oYi4YkCFmqTB1d0J23jsjYwfmHgD8Gbs9DRwNonWAM/mfBEOSn
621+ dOEXXXsZuAL4g00XDK1fc3p/qTZUIiqYsMI/1lI/eatQlKzlmRqpc+DuifqhHdM7gT8FdgBzuTG0
622+ fyR3eMYATmLhI6AEVIBfG1xbfeOarf3D688awMZG1as8ivP+yR4nXIIItOsZe++a4PCDU/tnx5uf
623+ AD4GzAALuVdIf1oNQX4Kzi8nuPlhhDf2Lav8/vnXr6NnuKzeeQkP+qc5XcktQuDYrhnuvfFIOn+8
624+ 9WfAF4HxPEQ0TzAEfcYAHt7xBaALeHvvstLvX/KqU225O1Z+hjEK7z0LUx1u+dhu6rOdP8wN4fgJ
625+ hpD9tHgD+T90TpPH+ArwvEpP8p6Lbtg42Lei/PCH5Gcbo1pMHicfWuD2Lx0Ya8ym/wX4Tm4ICz+S
626+ LP6HMQBzAjjTJ4b3nnnFqmu3PHu5Kiqq+jO/8D9qBADqYeeto2y/6ehNwH8BjuQAVOOEsPDv2gAW
627+ Y/3Sru8ZLn/i8tdtolCJQJ/+Hb+4GP9HvIsqKpA2Hd/+yK5sZrT528A38/xg7gRv8BPPDexPaPFt
628+ vut7gT8/+6o1f3rBi9dplBgR5GlfkFKS0FUuY40lzRxelRPs4ek3iHCXmEjYeM6giWN7zdiBuS3A
629+ D/i3EPO/Kw8gJ5R2vVHRfOt5b9yyvjZUelqTPFWlXCiyeXk3Z69sI+khxian6R/ogsIm9k2U2DPS
630+ YnxmGqcOEeFEG3h6DSIY38xYk6//445JlJ8n9CEmgfpPOiQ8nXe6GO8rwAWDayqfueQVmypxKfRu
631+ nupnrKqUkiKnDCWcNrhANRqh1ZhmoNZg9XJLuWRpdtpMTgsjY0Wc7afau5bjjS72jkYcHG8zNT8F
632+ oj9kEE+HMSzmOp1mxi0f39M8frT+u3lIGDshJPxEIGV5mhe/Cly/cnPvP138io3AU5fkLcb0UiFm
633+ dW/Eqt4OZw6P01McR+IGw8MJSVKhXK1iaAMWVfCuydycMjreYnauzeyc0OrUGBhex1R7GYdnejgw
634+ 1uLo5Bipaz9t3kEVRBSM8L1P7+Pwjun/CnwJGMlBpM5PAjOQp3Hxu4G3nnLOwJ+fc906VVR+XMhW
635+ VTFGSKKEJEtY11/jpdd0qEX3kDUaZFnG0LKMalcFKGN9AUhQ6mAtaBPR7vB3foxme47I1nCuyPxC
636+ k6l6xvDQuTywdw3v//AemiWHFlKKhQKtrJXv3KfWGFQVMcIDNx/TB2469k7gU3mVMEOAk59WI5Cn
637+ afFrwO+ceeWq39pyyTJVVRGVJ3y2xV1eq9RY1V9hTf8CjfkRztq8gve+az/funmMYtHxc68Z5JU3
638+ 1Dh9a0Q4TYbxLQwpnjLGRRAJSorQRn0XqhYkxRlBIjh6MOFzX5jlgx8b574HFsgyZevpPfzJX1xE
639+ dzKNLwyyZ6TKHbuOMd9cQAxPWahQ9YCw785xfvDVwx8C3gsczkvFp9UI5Gna+e/YduXKP95y6Qr1
640+ zssTeTiLiz7Y08+m5cKmoZSiO0izNQs6Bz5j29aEoaH1zM5V2HF/zBe/Msmtt82g4ti2zfLSG8pc
641+ eVkPpbIhcwFzEQxCglOwJkJNxIH9wle+vMBnvjDBHXcu0Gx5Tj2lyA0v7ueF1/RwyqkZ6CgP7V9g
642+ ctLT6NRo+X7i7jUcnirw4NE6c436DxnDkzUEVY8xlh3fPcb93zz6UeBvgEO5EbSersRQnuLFrwKv
643+ OuX8ofedc80aVa8ntfiqSmRjhvtqbBiOWVNr0W0P02g8RGRbFEvQ21tm7coS3V01VDxqPOILiDUI
644+ BWYnq+y43/Ltmxvc+O3jLLTmuPTSIs+5vJvzLzAMDTjIIg6PxNz47Tof++QYt3x3nnZb2bC2wHUv
645+ 6OZl1/dx4UVFytUM71rg2xjKOB2jXm/TaiQcn1rgoSMeT4lGOsx0Z5iHposcmcqYbzYxBsTIkwoT
646+ qmCssP2mozxw07H3Ah86wQjaT4cRyFP0HYuL//w1W/s/edHL1p/04leKZS44fZgty+botweYmT7I
647+ zEKHdWsNa9eWSQolvAiJ7Vp6SIhiTAZqwBvA4cWBxKivMjtR4747DD+4u83uPRmj03N0988xO9fh
648+ 5u/OMTfvWL2ywGWXVHjbGwY4//wC5a4U71rh+60gqggZeAXTBC0jkqC0Ua3TqHc4cMTx4K6MYpRQ
649+ SAY5OLeC3RMRY7MZrayDmCceIlQVY4V7vn5Ed31v9PeAb+ThYPaExPCnxgAWQZ4ysGrFqbV7Ln3V
650+ pkj10Rf/RESu4npI6gv84a8uYKLjNJrTdHUplSSmUi2iUkQV1CbgTsjGFYzxQIqqRYlRLxgDXh0Q
651+ g9ZYmC/yN++pc89dRXbtP8b2vQeJowgB/uyPT+XVr41ZNtwCGqiLsSrAHGoaGN8N+fnFpOAtiEFt
652+ AHbUNyFzZC5lcnqBxoKlsaB0dVXoJJfyP99/nAULqTQwRp6QV1hkMt388T1+bP/cbwA354nhHE9x
653+ I0l+zN+VHOQZqNSSu65++9b+KDH/5mtVlThKWFaLqOoMAyXH4WNl3vtXD7Jl/XKq1Yy1G+d54xur
654+ XHZxhThSnOugpg1eUCIMEpqukhuAGFCLmhSPA1FsXGB+PuFb30j55w8d5+bvzNJViXjlDX1cdVWZ
655+ NatqPHSgw9e/MccnPzfG1GyHyy/p5k2v7+faa4p09TTx7ZAsQoqogG3hXQOrfQgR3ipiLepb4C1K
656+ CbEeYxPGxwyf/VyDf/7INLfdOUfPYDe//gcXUy3PMlP3HJ6xzDeaIVd4HINYZCF99e+3N2YnWj9P
657+ 4CYey8Gi7KlKCuXHjPtJ3tT56At/5cznlLsLP3QDxThmsJqwoqvDYDKBcaMkcZMNaz2rVw+zd/8Q
658+ n/78LPfc7RHfg888pe7jXPk8uP4lNTZujBDTQtseMTn7RyVnASlYg7ElZo4XuPe+Op/+7Dxf/PI0
659+ 9QXH857bx+tfPcjllws9/R3QedTHiCYgXUxNOP7/Gxf44Idn+c73pqnVDC950QCvemWVs8+2dNeU
660+ zLWw3iAaI74FongjwQNgEImZq8MPfuD4+CcW+OwXppiczOitWZ5/ZS+veWUf5523wK23jqFOUNPL
661+ eLOX6ayHo7OW2UYHJZS2j2QMqkprIeUr771/j0v114Ddef+g8VRVBvJj/F5M6OO/+eKXb/yfK0/r
662+ ASC2lu6CMFhKWd09w7LqMTppm0oX9A0YNm2I6KqGMkxMRGS7qM+V+OqNC3z0E7PUZ3tJbDfHxic4
663+ 8+wOb3htiXPOi6hWM9AsfzAJrU7Ent3Cpz41xyc+PcnIWIcLzi3zljcs47prKwwMp3htodrGqWBN
664+ gpKiZIgPHkVcAjbi4MGIj3x0mk/+yxQP7m6xYX2BN79pkOtfUmX9GqFUaKJpB68CJiJzBUZGHF/6
665+ cof3/sM4D+5qkcTCWVsrvOU1K7nhhgqDqzs4NwdZC6OORqvOjj0djh0Tmk1har5KpX8lD013c3ha
666+ WGileHWP6BmOPDjNrZ/Z90XgL4F9T2Vl8GQNwAKVQqFwbv/a0jef85rN2leOpD9qsbp7ksGucQb6
667+ 2ywbhq6Kpae7SJTEYD3iS0CSO3SDWgHv8cbifZHJ8W4+/PEJbvx6B/HdlIvdjM8e5LoXRrz4+gpe
668+ M776lQ4f/tgE2x9scubpFd742kFe8+oaQ0MNxLSQyOG9xWAxAs4r6hVMeF6iBmsVdYo6IErwPibL
669+ yux+0PLBj47z6c9NcPhYk4vPr/HG1/dz/YvKlLo63Hpryvv+boav3jhDu60M9lle84oB3v6Wfk7Z
670+ DMY0Qn6iBvEpIhHeK8TgXBOvbRbm6hw67KnPG+YWLKOTXZT6VnBsvo9dx5SphYWlakJVsdZw99cO
671+ +d3fH/8V4Pt5ZTCX9w30J20AhsDiGXzWOWu/c9UNPavX9Y2zsm+Oaneb/l6l1iVUS0K1FlFMBgDJ
672+ kz+PYEEsoiFuE6WoGlRKYYd6xUTC/Gw39+0QvvetEvfcbVk21McXvnEbR8aO027DxrUVPvy/TueM
673+ s1qUSnOI95BZsBokPVGKkBL5Ek4FvKIm9KCMgrEKHtR3wLaAPpymGGMQV+L48TI7HlQ+9ckGd92d
674+ 0WwvMDM/z+FjTYpJwiUX9vKOd/Tz7HOFwcE2ahzeO/ANrJTzZamDlPE+PDUjBscsise4Ah3fZHS0
675+ zcI8zMx6jh+P8FLFxz3sGO3lvkMeTICMnYOvvOfe+XbTL+YDI3k+4H6S7eBF19+dJIUPX3h28Vm/
676+ /saY512e0T/UZt16YfXKMr09vZQrvdioGvRYCiqAGCRvBHk8geKZgnRALEqKsRFCienphF17Gnz1
677+ xgk+9YWdLDQn2HJ6zJpVZZp1ZaB7kLvvmWJ2JmOgv0jfoEOMR8WFpBDBaPAA6h2YkMWHLFIwi9dj
678+ QhWrGuHJQARroV537Ny/wDe+Oc/efU3GpxpMz3ZIM+W0dYOUyoahIRjqE3r7lKhgcF5BXM5SFgyK
679+ 9zboDSCUlpogWgFbIrIlemuW/iFl9apulg2n1GqO09Z6zlrf5si+Ijv2NihWE0wkDK3vKhy4+7gA
680+ u/LF/7H7BfIkdn9l3Zq1Vx0bPfaZ3loPq4cjXvC8mFe/pocNm9qUCi1EWhgKqJqwMwGPIhq+QkTx
681+ qggmXLs1OCIazSL332P44MdH+OwXZpifz7jyOb286pU9vPTFJWo9GU6Fo/ti/uVf5th+bzeHR1IO
682+ jU5y0cWWX3hzjW3bYsqVOkbboBaMomrwasPV5wZpDUsNN00N3ljanQK7dls+8YlJPvix44yMZtQq
683+ EavXRrzxNcNcfU2No8eatCb7+NbNs9y9vc79u0c584yEl99Q4SUvqjG8rEOh0EbUos6DBmMTJMDU
684+ alAUbzuIjxAnKB6MwdmEudkqN369wbveM8r3fzBPXLBc/vpTqfYkRInhO5/Yx9iBuXcAtwEPEXiG
685+ T7oqkCf42QToL5erNxeLPRvPv/AVHHnoewzUmvR0GyI7wdvfVOI5zzPYYhvN2iF718UTOUSqoA5v
686+ HWgRXInd+yzv/8Aon/n8cQ4fydi0scgvv3Ulr3xViaGhFmgHI2FXe/EYFTyWZqvKzbekfPJjLZpz
687+ /RybmGeqPsKb3zzAm95Qpq+/BepQv7gfOzgzR+QHEbV400EkYWG+xFf+tcE7332M7ds7lIoFFMfz
688+ Lq/x//zmCrad7Yjj3NuKgI9pt2L274Uf3J5w330Rt/5gku17Rth6esIv/PwA17+oTG9fG9GFYPha
689+ QnLxkGoBF7XBtBBXRqXExFiJD35ohne/b4TJ445iHLG2ELO5VuDYeT3UhkoUqzGo8Lm/uns7nt8l
690+ 6BBGf5yE8IkYwGJv/9Ktl6790vjuiG3nvpUoUjrtY3QaD2GzUTYur9B045x1zjRvfG2JFSsN3mUg
691+ DjUgYhGbsHMnfOYzTT72yUl27W0x2Bdzw4v7eNMbapy2Vah0tXHZNIko4rvxaEgiF01dFRWPmJhW
692+ s4tD++Hzn23xrZtSitEQI1NjrN/U5u1v7+XSSyJM1AAfqgijCV4i7rnb8/4PLPDFr8zQbhTwXtmw
693+ zvK6V/fzwhd1sWbDPNbOIi7GugAgLQZN5wVvBXzKwmw3h/eV+do329x1Z5sde5ocn5tm82bDL7xp
694+ mKuujhlY1iRrepzpIALWg5Uebv+e8L5/mOLL35jFtxMS47mgnPDWVcu5uFzj62P7+bPVCf3LK1T7
695+ CsQly87vjrHjlmN/BHyVIFObebIJoTzBmn+gWLFfuvo/nbntpg+OybZtbySuDCA2T+rcDDOj99Jd
696+ nqGvFuGzSc4/t8HV13o2nJowOVHgm99u8ZGPj3HLrXMkBcNlF1d58+uHuep5ZfqH5yBr47Bhx7oM
697+ Y9JwoTbKrzaYgDqLugDUCAY0QiKYnShx83cjvvUt4eDBiEPH5hkYqnPDSxKufkEZawy3fS/lfe8f
698+ 57Y7GzRbypqVZa58Tj+vfGWZKy5TSpUWmrkQIDQQd41GIbaHOIaX8O+icYCmxeGzChNjlvvuTbjj
699+ dsMt31/g3gcnKVVSLrxQeMNr+jjvrDJCzK23Nvn/3j/Od29vIFiWlxOu7CnxhqEBzqlUSawny+B/
700+ 7HmQT6/ppm9FhdpQiUI5QoHPv/PuCe/4RWB7DhA1nowXkCe4+1+69fIV/+uU84a45Z+Psvm0G6jW
701+ VuM1QkxCppaYDupnmJ/eRdmMsWVjH43mJLsO7OSBXbPM1R0rhhN+45fW8vKXR6w9JcNnDQwO9R4r
702+ BVTqiKQ5zOBCCLE2uN+cTKa+AdpCTAW0AE5R48B71JTwpot77utw89cTDu8dZPf+KW67bxedLAWf
703+ cMraAfr6Olz3woSXvayHNesVzzw2yzCief4iQXhmUvBR/njzR2Z8qDg0xHcvDsnyvSIlMl/ivnsb
704+ 7Ly/zD13W/bsM9yx/RDezGLEsDBnWDfUi01bXNtd5qX9/WwrFLGiiDX4rIM04bcO7uKr67rpX1Wh
705+ NlSmWI2JYuHgvVPc/sUDvw98G9j7ZL1A9ASaPV3Fcvyrp1+2HJcptf4uOpngpBgyaq8410BMEWQ5
706+ 5d4BimaBPaPH2LtnO9PTTeYbnlVDvVx92QBXXVFk9YomtJtYAUUwRlFpoT5kyhp1UJ9iKD7cBBAg
707+ M0AFNeUQ47WNWIPTKBAwvQfv6Epidu6Z5qOf2sXKoX46GSzUlU1rejk8OssrX7aSN78pptY9B2kL
708+ G0oDFIuKh6iJuBh1cV6+ehAX4Om8CSU2Q8WgPg7cAtpg5jFSZ9vZhjPO9rzitcv4nd8+zsGxKvfv
709+ PM7ZpyznzOVFnusjruvvp0dBJMJYUCyoYgWOTY2wx5pQvBhZKmJUYeVpPfBF/i9g5wl8wuypNgA5
710+ oe7fvPG8wXMXN4BKB/Whz24AbyISm6AuXIETQ116iAtlOv5WZuaaAPTUVjA6WeEf/r6LKJnggmd3
711+ eNlLixS75snSCCFCxONpIM4hvojSCgmjVZAUFYeQYLxFRYECHrCR0pjr5XOfb/C3HzjMnXfXWTM8
712+ yCuuOZsdeybp6etQ7ephZsrwS2+4gZvveIC/u3AnF18Y8eafr3HlFWWsaQVswAiqRVQUiTyp1jF4
713+ jBTzx9YGWw+OMTOI8YjxGG9wpoXYHprTFf7pozN86IPb2bG7RWIjvMLeg2P81blbuahawOCX6mTN
714+ KwUVIdaYoraZsSbvKubs6fz5RwXDlkuX9z34nZGteQiYejJcwugksYIK8Ocbzx2EnBbltEOW1fHq
715+ sGKDYjbLMBIvKfBVFKeWatcwBjBRzPDa65hp17np7rvorSjHRpbxzRvbnH1Bh2uvS1m/waGpR1yM
716+ aBEsqLGoZuAjjI9zA6wjtoDXKmgXO3c6Pv7J43z4E3sZHYFtp67g+ktOwxYarFw3wS/8UokLzt1I
717+ FKUcPdrhi/96C8NDPVxVu5jpuQV++7eOUuka4SXXF3j5y0ps3GDAdFAfdqV4xZhKaDmb4CXEFRFV
718+ iBzGB4hZTJX9e6u8/x/rfOLTI4xPKKet6uM1Zy6n1Ia/vW8vG0pFnlUuE7uckYSASh7hDFhBZ47j
719+ mm3SOMZYg7E/wk1E2HLxMvbcPnZt1vH3ntAjcE+lASyWfrUNzxrYWqrGAdYUZcWa5YwdGKF3MIPY
720+ YiT0sZdgLxTvU4SIJOkK6Jz3tGyCVntZ3nUKks5y566vsHrI0ums45avT3PWuVO86lVFVq9OsEmd
721+ djaB8ZZCVAVJ8QLqgpxwbt5y112O9/79Ub7y1TmSuMDZp67jktNrlHtnOfuCMV7+0m6Ghkr4bAHD
722+ HA7D8jUp73iHJW20+erX9/ClL3tKcT+V8jo+9y8TvPdvR7jkYuGtb6ly3rmWardHbETHdxBtYqgE
723+ uY9xdHxKJFU6rsKuB5T3vG+aT35uikgKnL52kEuXJVygcG1vkVnp5/337yOylqKRQGVQg2rY3epd
724+ wAokgmKBDKUZCcaGHzEPsypVIUosPcPlZ08eXhjmYbXRE+IMRCfR6y8Brzjl/KHSD4kp4g7OdRAR
725+ jIlxmuJ9RmQKeOcRY4giH3IlUwjIvwjigste8ClZKizb+HKMbfK9B27EpLMsGzyL3/udSboHDvG2
726+ txfZduZqoqgJDtS28FSYme3h45+Y4d3vO8b+Ax02rOzjRZdtQ6XBmo1zvO3tlg2nhAdsdBrnM0wU
727+ k7kFvGZYamSZYpIFrn0hXPMiy+yk8M8ffogdBzpccf6Z9Bdr/M5vPshM6yhvfkON17++j1WrFWQG
728+ oYP4GOdjvFa45TsJ/+PPx/j6t6ZZ0V/jxedsoqvd5pxOzPV9w/TSIfLKnYTKIRaDEcGrQTRCjGJE
729+ cD539Rl4SdAUOlUoWwmcgsA5yxtFYR3OeO5Kvv2hXdcAB3Ja+RPyAtFJ/HvZWPm/uwdKS2QOATLf
730+ xmUZWdZAkq6w23P5vljFYvDOoF6xURJKJ6+gHcQK+ITExjjjaWkXy9dej3Fz3LxzF9lCg9OytXzi
731+ 77p4X3qA619S4DmXJOw9EPH3/1jn0188DK7AWaeuYNNFBTZtznjBtfNcdEFM72AZzCzeeSL1ofFk
732+ IlTB2m6seiQPT0oKRhCf0js4zm/+RoH/9NYyd9wxyic/fZC+ngrnrXw2O3+Qcu1HdjG8rM5/fvsw
733+ L7gmyBs/9/kF/vLdozy4u8npa1bxuovXsKzd4NlNuKRrkL6eCKsdvIQ+yGinhVNlKBasCN5H4DJU
734+ LF4gdIDAp02sROz2Hh+ZsPj2kdlFw+u7KNfi8xuz6afysmn2iVQD0ePU/jFw6mkXDVfE5nBmbgTV
735+ Wmh4aN6iBSWyCYsmoouzHDDEhcrSZ9rtWZLCSqy1iKSkzoXdYGJ81Eu55yzswLM4ML2T7bffyTmn
736+ 93HbN3v57d/9Dg+N1Fk5NMCF2zbRV1MuuDDj+S9osWVrjNgO+BYiwfiNkZBQ5lCwMeRuO4ejFyeL
737+ qA8gEwqmQ7WWcsVVliueX2bvXvjyl/bz3Vsizt+6ge7Kct71/z7Ab/7+QbwKxyeE8zau5ucuLrKy
738+ vsDz246zegfpNj40n0QwIkuQ8LF2A6/K8lKCLtazvoNQDM0Sn6E2ApfiM+UYiubuf5FVJI8w5G7l
739+ qb2r99wxPpizsSeeCH/w8QygCFy86cJl+kOnFqHaW6bZPE7q6qhXxMSoBow/JIBmaXxLsdBFHCd4
740+ l6FpGwM4DeFBxIRYCAgxeKHV6VConkNP9Tz2j93Nt2/7MiMT9VD+DPUwMj7Oe969nk1b5vDZPF6B
741+ rJLzBH0+q8GjOITSw1cuPzI/MtcIhvlCectYwcscqGX9BsN//rWEX/nFCv/6lTa/+8ffx2M4dDSl
742+ XEr4oxvOY/mRadaljnMGholshPiULO0QxUWMki+0BZS5dgC1+k2cA0qCJgUWh0uItRgjaKWKm5rl
743+ qA/CkcX4/2j0sRWn9rDnjvFrCRKzMkF+LifjBaLHWHwLFCu15PVJycqJJAVVJfMtnGvhsnYgMohd
744+ snjnHE4zxC7SuOIlAEW9Ip6H6V1eUe/BxogakIjEWYQFGtKNqTyLpPIgfmwCAYpxhdPPWM9b3rad
745+ q66Ct7+tRv9gFkpFWsHlq8HjMRI/whNbvJT82RiT/68GoxEQV8VLipo2WafG7bd1+IcPj3J0pEk+
746+ sIRWs0P3/SO8bOUwBQymExYLSfASh+RODLg2aKgc9s8tBO9pDKgPYJPkTaLcOFUEdUJ7YZYJCQtv
747+ bB4G5JEMQBha1wVwPkF8W8k990mFAfMY/xYDA7Xh8qYf5ayJgIkEMQrqMEbxmoFAlnVQARvZnMKr
748+ GBNhRDDGkLWbix1ZnHcIhsgWsKqgaSBwRJ6mxHSilLado1gaJI4sxWKJsfke7jmQ4nWAQ7u38LrX
749+ Km972yy7d5QQ0x1IJmoQVwCN8kENWbgWJQA5uRCDXKvk1eWhzCGimCTFtfr41IcHueDyQ7z6TceI
750+ 6it47rZ1rOzuRhB6IsPz+7oo2hRvHEqH1Dfw4rE5aBMsPZTITgzTWfDKqxKDX0R1FsUlfrFbGvol
751+ bZ8xZswPVQCPZAEiYK1h7Rn9XbkBVPPKzZwsxv9Y8f+5a7b0PaIhFYoJSSSUkkE62sSQgXd4n2Kw
752+ iDdENgmdcQvWhkSw0R5b6qgYUwDjESVIto0E0qV4ImMwYrFU6HhHmjlMFNGz4nyioRfiK8/nth1N
753+ Ml8lbWzlN369xGtf0+BrXyvSSosQGzIXCB/qGmgGmnp8x+Ayj/p2aA6pxzshVQ/GMn60h79+F5xz
754+ 8V7+6E8W2NB1KpesW8GKyTq/5cr8/PrhJQFIEseIEywKSUwUxQgO12ng0zbq0gDsRB6PsJCnS4O1
755+ Ih7B+zY+A592cmMgbw1HNEU4GkeYJSCIRxXUeq+sPr1vcX5iVw7cmZOB+qPHQf8uWrml9xG/x0Qe
756+ Ci1cOkdkevGahgWzxTzxcqjL3RyWKI4RItqE9xjCAAAgAElEQVStBRC/RAxZJHly4nSQxZ3pw++b
757+ 3HjCxyypi6GwmvKaZTQaI3x/x10MVhOWmbP5b39ymNpfT/H618ZcfU2Z/p4walC9yVHDJkYCHc1L
758+ MDbjEw7t6+cjH5/hHz98kGLUzQWbzsG2UkoTh/mVoSE29VexdWVHx6GqJEaoGLuk/V8KcSKYQink
759+ FAjqPUaVJjBWbwBQii1GDWot4sFGFbzxeBvCoaFNXdss5ACQXfIAj76Q3YNFgMuAu/K1sycDDT+W
760+ Byh0D5a2RMmjJB/Gs2xtlWOHvkPiHZHUQAVrQF1w5d5neZKVEEdFMAbVxWaLD+7Zh0RRc3lUEGBa
761+ RAyaa/cfzhIFkSjg8D5GXRlN1tK/5jqoXcftO5Xjcynrh87hC/+yjkuvGOed71LGxmtkRvA2QzEB
762+ qvaCa3ez874Kb/nFNs+6bCdf/lfhhgvPYlUpZsPRI/xp0uav16zmtLgPXIy3RQ53Qi1fNYYExUeS
763+ kzwkVykJqENMPsJYwmLXvXK42SIW6CLgAOJdKATyIRniFaMRDsGV+0mt+SEP8Kg6ApRSNQZ4Vu4B
764+ yifL9jKP0fwp9gyXNj6a2YkIq7fVkOQ4x8e/i/UjGGlhjMPYCJeBkRhjLSIxIhaXZTjnQnVgFxfU
765+ Li20IDif4VyG4nMj4AQPoLmGTlE3j+tM4DWirTVaZpDu5dfSveZNfGtnidt3HGbLhjPY/cDpvPCl
766+ 8/zKrzY4emAAa4cxbi03fq2P5187yvNfOEZ7ZIgXXrCVWiPj9D1TfGB4Jb+/YiXDNkHEgJvOc5SI
767+ dho2VNkIBWPwXoJCKcvCZ41BjMlrsLwoVktTlbb3lKyhZgXxEihpQmg8eYeK0Egs36u3ed2+MSYf
768+ AQV8JA3B4vSR2mCxF+jJq7foyYaARfy/XO0plNTrIy4+Enbu6MgY0+M3sa4xxorVZ2HjlXR8D9bE
769+ eO/DZ21OBBXBZWnAz9WjPhAprYnzWjwIN51zGInw2kGJAhcgv1UjPoSQpIzREs7XcZknkhKZLUJk
770+ KA1eTHfvs9g9toMH9j/AKctXc3zUcvVLdlKpNnGpod2scsa60xja0kKOTXNlXORlq1fSExdwqhgc
771+ QoTDYOMEkQwfeWazEMh7rUWtxeSVA2JxGhI57x2SZYixeBuBOGaaynwnpSexWFPIK6cEtaBkeDXs
772+ SR3v3neID+8dIekv01OOsNFjJ4FLa2KEofU1Zidag7kHiPMN/picwegxhJ6X1YbKj35CERbmmsS2
773+ Rnd3mePTh5g8foi1KzfT3bcNjVaSRTFOY9Rb4rhKlmV0OgsYVVyWYUWIogST8/XEBNAkihLIQiiR
774+ qIwY828IDKEPb4lsoFqJgBEfZN9YsqhIafgSapzDvmPfZWbvA7TqLWb3N9m2aSUXbBpiYc9Rfq6n
775+ xpVr11JWJU5dQCtdB4ksYoQIDzZCXWAxexf2dm8hBmuJspC8QtATqkjgLuQl7yJzad4pHlgRJ1SI
776+ 8VEH0YgMw5gzfGbqOH93cIT9s3VS5+jNHGID3m+jRy8DfwgQ2lxjz+1j/Tl8Hz8ZDyAnhIBtld6E
777+ RxvNKgKFUsyKFWdy+uZLOHjoNlzrGEeO7aJ5YDtr12ylf+hcSPqwUqNS7mESaLUW8K5NFBUwPiR6
778+ gShqcN7ngEyGkQhrLc4rRuyShFpVl8xZREjTFiI2TPnOk0mDwbk2KglNqpQGn09t2fmM7P4ks/P3
779+ 89CBUf57Tz9Xr12HlQxpO4jAJxbjNexml2ElWrpZRXAY2i6wsAvGIt6HnawGn7YQazBJIU9oTVAw
780+ a0gGF/KrXlFJqMQBjh4xwjdm5viHg6PsWqjTSjM29VR4aV+N0QJ8N+4QJQYbB07A4x3dA2WA/jwE
781+ xCdUAvpEq4AIOK3aW4DHGOwQJTETx/exkF7JyjUvppMepb3/mxQ7k0wdP8CRI3vYsPl8enpPJbYp
782+ hSQi8w6RDEsJ1GBtEOAiHu87iIakx7sT6N0/BOiYYDAaLioyBVQ9LutgTLJ0V0KE+A4FSelIEU83
783+ g8s3cfjw/bxsqJery11EKB0gEY9KjMOE/rxmWGvyvoYJsAGGFJhtBTSvZKPQ2NKgN7ClYkhkBTR1
784+ SCRop43EJYSIehb4EKcVCsxh+cLCHB8/fITbR6coFWO6I8NbVwzxjjXLWGkj3rZ7D6a7EHa/PblJ
785+ alEiRIlZlXX8iQbwhD3AYghYnpSixywikkKMV0c7bYI1xPEwGza/gsnJB5gev4f+UoNjD93B5Nh+
786+ sk6D7mqVmfl2EFaaboyxeLIAIfs8+zcWwYfWsiRkqWKj+GE71hxNlEC3VhHEGFKf4+5K/l0GZwrU
787+ nWDFE3nF5xKKZZKQoKTWkWQGrAYVUWrAWjxprgD2GGuWMCSPMJ+GHKBiBREfwpZA+PLgqjWKwtQP
788+ GyBgzWC0UQfgoe4CfzE5w4d2HUSsZd1AN9f0dPHaFQNsK5cQiZgeHWVPq4OYwsNEkMcXlmOtxUZm
789+ 8McxgCUYuNgVFxfpR4+eeDgMgs+aqCvgJcET09t3Dr29pzIxcg9edpFEytjcPO12SidtUZ+5h+7+
790+ s8h0MI+dYDUghU7beVs0QtRiTZvUkDNmQts0KIOFDAc+w2KJJM67bjlp1OcoJREYUJ8hefxWUVQc
791+ NhMwimiMZinEESKCX2ghhfDwvY0CQkgQfIynHQD67KI9Bu2DChhyuRkmVxwJXjPacczOzGNEeHC+
792+ w11jx1jW08Xm2PKL/T1c2t8XYrwLooWHZqeYjwNMfIKW5XGpGzlC25svfvRkDQAgqnQl8ZIW+1EO
793+ j8PRIk0bVLqGA21bQZ1gpMLg8gvx2VZGR77PihVFZmfHmJmb54EdN3HGaTPYwnri8iYw3TiTgBoK
794+ HrwoGYr6UPrYqBgSqxOGNJkcB8fGucQrDIwQk1uttVjMkuGoBug5lBoS2sCYANcaH1jI6lHnsZUq
795+ ixpk8R6PCQifBuE4QE9kUdVQMXiPGEtmY0QdIikuivFeGLFtvulafOnIEeLIsmV1H4P1Ji8wEdcN
796+ D1IKhL+QaRmLV8dcp03DCgUTuoAnNVMvt30b2Qqk8Y9TBgpgbGLt47WTjDVUeyz1+Ql6etYhESgO
797+ ryGmemJcVGZw9ZVMjHyHfQfuCieViJ27t9NJ72FoeDWrlp9PoXs9mRlAJQkqIknz7DqoiCXvfPnF
798+ fr7mVYMP4I7YCK8On6XYKEaBtF0nisrBA6jS7rTC+eMYMWYpS2dRupb3LsiyPPNPIY5zoih4K3m3
799+ E8pRFHanBxNFgdSqGZgMpzDhhZvm5vmnY2PcNjZFvZ2xvKfCixopL+pfxlDs8aZDRyNiLGLzcNhp
800+ MZk6FmJDyT7cApbHGyyxuDGslH7EA8iTCQEmjo05CaeDOnI2rQ08FCMYa3Euf2cPgpMK3bWNFApF
801+ vHdsPOUCksQh1Ln/ge2Mjx5hxbL19A5todq7DWe68wEJBp+3axct0drgwk1OEVckRxgVIxHg8YHI
802+ h40KSyHBiFCvz4fN4jzqXM5ZcHlCKXk3T5DIIOqxuddZLPNSJOj/gHIU5y5a8sokzAOuu5hvLyzw
803+ 3sNHuWVsmmV93azqr7FrZIr5+SbPqfUwGNnAKSSikDnECt7neY+NmPNgYnncRtAjbkoRy6O/Iu+k
804+ Q4CIOYkzBqSTdjtkuGmaQhRitLEG7z3WW5ZmbqQdoqhAb/9FtBw05+/g4gvOZ2R8hL379nLo6B42
805+ rjtKz7KziaorcZQQFYwmGGNx3pP5NKh881lBsgQKyFJ71BhLloXyMPQUFukfOdi0SMTN9f6ILiEm
806+ kpeZmnWQqBA4jhrQ6NQpjTwJ7DEW5wmNIBEaXrmr2eYDYxN8/tAYXZUqp61axgZSzlq+nP82Ps1g
807+ FNEXGYxooIM5jzYXkHJPKH/TDJNlHO0ENHWJCPJEBXwByDspKDh6zO39eOcy0DvYhZsLOy8qBK/j
808+ neZoX4YSI5Jio4Q4KeCdI9MYCv3U4qtoNA9SKCZccflGHjq0j117b6c8soPhZafQt/JybHF1qKeN
809+ ybN+xUvI7NX70GrNOpgowZpcUaRKHBUC7KwZkGAwZK65aN14FbwRDB6fhcEM4hUii6jgo8LSg1Dn
810+ 8AitxDCbppSMsDpWoszR0Zg7tcP7jh7mq0em8bHlrC3rsdML3FAs8vq169mVKv9dhSSyRIsbVFM0
811+ c0ixHHQGFiS20GhywHui+MQ+wBMwAv0hvEyeTAgI2rfMnxSlqFyLOD65gHNt4qQYYmQu4TJhfwQl
812+ sAY37l3onWMyUl9EC6dRqKxhqnmISlfGFZetYP+hA+zbfxej4/s5ZcMZdJeHKEQG58CSBOGYaNi9
813+ edfQZW2IEozEeO8wEsAlgQAwLcqCA2MgZOzeB2FJs4mplJHI5rx8wCmu2cSUy2AtRj0NMTiU7jjC
814+ VivsUc/HJo/z/gOHmEkzzj51LfX5OhfOzPHL65azIi5g05TZTkbqPYNJRCx2KeHDK66+gHQn4DPE
815+ CVmaMqosoX+hHH7Cms8fmxauLlN/Mt7G2xad9vwSncqQExs10MfVhVGoUZRgbRwSMXUYcahp03ZN
816+ yArEyRkUo3XMNe+l1t3kOZeu4OjRER7YcSsrlw/R1VVkZrYJro01oVNoc3hM7cNJryy+E5RgdIuq
817+ ZGPIKesQn0BWgQDiIIJXcFkn4A4oEgXPhguziTouhNbBgW7ePz/PTYeOs2uuzhmnrGaV96xdmOeX
818+ VwxyQbGY8/2ULFL2LrRBlY3FArF3wQO50ACLq91LfRCXZnSyDlP5+Fhj5XE7gY9aoJ3k202jR/ll
819+ nzaz7DExAAmFUldPFyOaoZrlbj8kLEaUVrtJZJMcyjV5MhcyeesK4GMKohhbINMWHWuIus8lKp/K
820+ 6PgP6O+3rFqxij0H9jI1PUdkYtqtMcSuAl8I/f18urfmOMJio2AxYzYSY0VR38LnPTrJ4zYaMv6g
821+ /wsj4MTGwUtlikkCocVHjtRYZrNgWCOzLT40e4xl/TWeu2EF2egUb6h289p1QxCH2B5IPj50Atuh
822+ /OwpF5buPxCSwjsP8T4Xnhrqs/NQiJd2v5wcCPDw4jnf5t++9/gJGYACvrnQSR8TBcox8u7eLrzO
823+ 03Z1JIspmD7EQ0YKpoiIQ3Oun827eqoNvBiMUYQYpyCSod7jNcGZbnqWXY1hnKMj32fN8tUsHxjg
824+ 7gd20p7fTtSuU6qdhWOADEuqc8SUA59OMzAu9waSzxHMgqzftXMcPydhZooag2hgMImAa82H5LXY
825+ Dc7iNKMdO/Y14a8PHaLjPGt6K7zq4jPYvvsI50wt8Mvr19FnE7ztoFmGmpD4knugyVYoP4vYwPzO
826+ cxcVxXfaQV0tEe3ZEXa4Jq0kOqECOMkCIMcBskwbPPxa2ydNCs1a9ay9SJ17bCPIcK6NtWWgiENR
827+ bZL5BpHtCSRRldwDLHbJ0rxz5/JdKyBxmBziM9TVEdOPMz30D7+AycNfZdfurwDw4J6dDPdP0jj0
828+ ffqHt9E3+Hx81EVbGhRdQAUzSfLy0YfETjyqnjQLKJ74Rdn3Ig+/hSdGsojIdOefz/Cx56G28KGx
829+ Oh84+BBHGu28ZS1kP9jLHw8PcOpgP8VcoIoLRBARQbOg8nUoh3ID2FhYJInkzaJOB4kLIJY2ENcG
830+ YHKOpihxTgQ15uRyAJUQplzmFodJZifjBcyjhAAHHG03s8f8dRGwRc/QxpT2wiiluBz6+FiggHNC
831+ u93A+wCPFnJ9gHdp6OnLYnkWWql4T2RiYlNkUafrTZFK36YlYmlXtcaGDatZPlDl2MHvsvO+dzFz
832+ 9AuUNSUjoi0xXpJAWPXNpd0v4vMpotBlzMNkTAemUAEruLhDWk5plT2HUP7iyHGec+e9vOfIMa69
833+ 6jLO37wJgPrIFL8+tIqz4gJF7znxZeZGQJ0L3TufkWGY6ITScVUU54hlyEfEWsRnQVdIinq4s61k
834+ sVnKAU42ARACluAzXRSJZk/GAyzGjgzYtTDVurSwqvLYbsA6lm2o8OCN32Z5/xBGhlAp4qMExaNE
835+ KIo1EYWkFNxLWqeUv0gpS9tEtpCTRPMhUrYQwkg6DXENooD+GWvZvPUaJptKoVji9M01RsdHmTh8
836+ G/PT+xhYcT7V3i046UU0HwsjoR+vLpSNoUgOE8JEDcQJimC9ARsx2nF84fg4f/PQBPvSjOdvO40L
837+ 1i9nfmyETnMeBNZWSwxHijMKObPH5Ewg0cABDJQocMQcbTSJBGpxPp9QwLsANDlVvGQYb9jjlQ9m
838+ HkqFnAhiFsVCJ9UQStuerOPGcmFIyklMEDOPEk0yYHt9pvO4MUBE2H//GEdH9nHPPZ8mbe3DykKY
839+ /EUWrNyEBDBJAsHEuWwRiSGKkyU6mPpA+QocfSU25cAwlgIqAYXz8XKKtYuJe6+iLhsplfs4a8tp
840+ aDrFkV1f5NiDHyObuQd8HWeSXFkcoCiXh4BEBMWFWECQq42r8rHZOi++bw+/tusovetW8HtvvIZa
841+ 1uKs+w/xW6bIhigBhYKAxIL1GUYzrDpEU9RleK/4TgvttMF5Uq/MdxzVyDJYjgIL2Yck0OXJZisz
842+ fCPNuO6eXcx2FUJ7OTbY6If1gI93tBsZqiwawEl5APMIi79oALfNjDdPCnmam2gRRzHOzXLr9z/C
843+ 5OjNRH42H4oUwCEVu0QND7E4NECytJnHsAD2YAyZS3G+jdhyPmItW+LMx2RYHD7qptB7CV2rX8Fk
844+ Zzm9PcvYsvlU0s4kI/u/yPi+TyLtHVhdAA0l4iK7LbEx6gt4D4224wuNBi/esYc33f0AZqCH//qG
845+ a1hdLlD49nb+stzL1ctqFEWZb4ZWUBe5gsgLTj0aCaJBJyHGI5GFpIQhZiELOETVGHqxmDSXJXTa
846+ +EzZF8E7p2d53W0PcHShzdREk2IxIUosJjInXwYKHNszQ64NbHGSI+QerQzMgFZ9ulU3RireP7Yh
847+ dVeHOPu0qzlw8PsU2gcYPXwnhw/tYMOpF1Ht3kamXaixIIW8CnBL3Po4Koey0IZ3+jh1QUzqQhFi
848+ jC6JOELjZrH0I+cN9lEdugLX9Symp26nq3uByLSZnDrAwe1H6O3fRO/ghSTFwsNQcGRoaMatbcdf
849+ HzrM1ycnWLd8iD+85iJGRqZofed+frenm63LBzBkpOrpYJjPPcjaYiF/ywd5kimhAdZqhjYycQg5
850+ KJPO0cgc5dggVslCv4jppMBn5uZ5572HmMJiixHXlrvorRS4SyCKTE4HP0nkR5WJg/PkgyJO+oWU
851+ j1YFOKA1O948qOjWxzU+KzjtZuvWV3D00HeZHL+PWOrcd8+XWbt+jOXLz8LaYbrKfXkISHNNgAb9
852+ nBhcLimXMFIzzBnM36jnchrWwyOCAmxr8Tg0vPqtWKN7+QtJG/uZHLmFvv4SPkupz+7FZiNEhSrW
853+ t6gVi9zUavLp2Uk+e2iCWm8Xr77qIgbjIofv2MFrqyWuHBrEh7Tsf7d35sFyXmeZ/51zvq27by93
854+ k2QtluQFL4pjJ7ZDEg9DIGFSM0NCGEjCNhRbFTVTMwzDwBTFTBXDAMWwD2FYhhASKAhLyGKCk5AM
855+ TsCOE29ClqVr7fvddNe+t28v33eW+eOcvrelSLJkx8amdKq61KW+0u3u8573vMvzPC+xkCRdh04l
856+ Cz2fBYxkCdLZQGKJoAhCVknm8RHWeJkaKTivHB1r2RkllHLoRpIDpuAXj5zj75fXGBsZ4QFr+IFG
857+ iW8YrvHfz57FiSwwi64y/w9+e+bkiubCiaQvuA5ggO7KQvdQ0bV7VCyv+F6y4R4L88fIhu5ly+5v
858+ YGzrXTyz72OU0jlWFw4xN32Yr7ntTZSyTkiRtP8C++lLIEc6Z3GD/HcpMMaRRHHg0wewh/DlX59y
859+ ge36XoBxbVTlFrbdejNF+xiTJz/HSCOh216hKObYtnmMns75w3PTaODd33A/b7hxlIcfPcDdpZjv
860+ 2DJObGO09GyfzIQAMpJ0raMVGkHVPuYgVDyF1kgV4fIc40DGiZ9lYS3ndRtjLbU4YlZF/PHyPO8/
861+ MY0ol7h1rMYbXY+f2raVkbKlM7fIYlcHQsy1NYC6LY2z7hgbI+v1C70C+h4gB/aee27p23bdPXLF
862+ YFBG0Ou1EFbhhIJ4E3ff933MzRzk+OG/4YaRMmdPPkqaVtg8XGet3fQ5Px6IYZzACihMhziqIAxo
863+ V4D0BqGF8p/EOdZb+MpLv1pjkFHJzwEix5GjhUKUd7N9z/dRtE6iZ/cRy0WOnD5KUWje9JqbeNcD
864+ d3PwSxNUzpzn98dHqIoY6QBpiUJK6iIBhQGlaFtHN0DCG07hjPPdQCmRUYrJC2zeQmVV3zJ2fmdm
865+ V30NYDbL+E+TszzbbVMfH2bTapufG6lzXxwhIgm5Zmp1jrW40i+mXoSDvvJaW+oBPIFXDu0Xg3ih
866+ WYANkeQXzk4sXvFNCCDJEqwtvM6NcwjrMEYxvOm13PuGH8Yku+gWEEWCnjYsN08g8jPEJkI4nxp5
867+ ZawIiUEFfr8NCKC+uki/8WOFxxpY40uoMpSYlUqQMvIpo5Q4UrLyHWze9a9xpZvRhb9Kusst6o8d
868+ 4ufSGt+9rUZNyjCBJOTvAo8BsJ6y7ZyjZT2xA6CqfENHBWqZdRYpJSoe86RPelg6NDPBgba/Huac
869+ YW2kzKhz/EiS8LHdO7ivFGNSgZMCWVhWdMFCJNaVwq+2CiikYPp40+BFI5sDHuAFs4Nt8ADLzfnO
870+ qSulgAhBVokpig7W9DC6h9Fdj+JxCU7dwPZb38WNt307C01DuVKhtbrM0UMP0Ws9Dm7GS7wLATIj
871+ Nzo0R6Rn3IQ6fd8I+2QTzxxyoaKHT6+KwuffATSqpO/7ddUwldF7KJXqAHxvXOaHag22SkPRavp6
872+ fKCKO+dwWveHE3megBW0EeQhGB5NIu8hjMNqizDG1/jpYYRGakdblPirnuZTUzOMD9e5+4ZR9swu
873+ 8Zc3bOEHhqsMKYvQmjT3dQPnHDNasCY90HRDEubqAsDT++fbeAn5Vji8jhdJD9fAWruZf7TXvrI3
874+ SUqKvGgTxwlJkhFFKQoZih45Bkll6A5uvvXtLDVXUVKysDTJ8tLjnDz0UfLWPhIWiaQmikogIi8t
875+ I1VoIIkwNFLirFnf3HUZeiEQQqKEQgm5XjQRAqTMkcKiBngSr8tSrOhh45SSHPHYQcB2ex4KLmOw
876+ EbbwSBBnYc16TKACKioCNMb6/9sY3wyTLqewEU+7Ej969jz/4annaBaa1dYa45Pn+d/j4+x0SajQ
877+ CJSNMCLyAWNeMKcFupRs4ADk1bn/lbkO7dXidAgAV4MBmBd6BVwQCAKPH31i9sr/Sewo8rbXCxKh
878+ oid8bqyEIMYgBWRDw2AdSkXcuO0OilwxVhfs3/txzh19iKh7jtSuefKoUD5vd35zpZRYa7DOw8iL
879+ vIcxXpjKN8zCiQmCE845dFGAU0TGoozFBq1gnCdsSqspnMb2ev7qilRQId2InVzAIK7moYgElGVg
880+ 8Rpw2l9DGstClPKhlTbv2LefR63m3W+6h9dsGUE6wZs3DWMjTaE0iQZlNbpYQbrCi2dbx5y1FDhU
881+ JEIR6OoigLMTSwCfCwbQChkAXy0DOHzkidmOte6CiV+D/YCslCKkRRtLYQ2F8+ROK3KskJ6a7TSC
882+ hCKQQ7ds/ybGdvxLFlYlu3ftoCimeGbvB1maeRRhmwhhPd6sT7gUfZGpnjeqqOT/jLOADQwKHSqI
883+ OheFnwJmIoyAnJxcd1BAVPY6EcLmSItn84TPImwgrqJR0iJcAcKwGhDFiZRULGAShIrQ0mKE48nC
884+ 8N5Dp/jpU+e4597b2ZJFvG1xgTfVh2jrgpIWKBOAZ8qLacis6gNZ50fan3IOEcsNPaCrwAIKCdPH
885+ mk28WOQS16gYKp8nuyyAVZ3b968udC9bj44zBbKg0J3wLXp2rAgRlXXFel8AIbHW0XMgSzdx8+3f
886+ jxW3gojZurXG6dNf4NRzf4ZZeZLELnrUr0yQMvZj4axdn77dH8diwzXgQpoohPKDJ4TwmQQbp9pD
887+ CYPkfK/rCzm9ArvWgxxcuweF8f18E+jtzjLb9d4jltKXgq2gsJJppfjluWW+89lTzI8Pc8+duxk6
888+ PcX7qhXekwxBwAIkzoIRREKANuG76X/LBdY5VoTCSJBRgIMJnmcaqaO12GNxaq0vFzs4S+irYgA6
889+ WNRHTuybyy/rj6T2AZDpIqSfBqJ14XF8ASWMiEAlYdMMrtdECY12itEdb2HrLd9JV29l966dOOZ5
890+ dv+DzE99nlifR5mej+yFXBeBcgMNEt/391WjDSaNwNjcx5GENnOAhgkCj0AEjoBzyCgOHUkXxgJ4
891+ kqefOOJoBQ/QkJJECFZTyxN5h+89cJpfn13kdW/Yw4jRfNt8kz/csol7XEQuJOfXugigoixOBWGo
892+ viCV0b4rGJfIez2mhKBwegAMKq4Y+DkHBz4/CfAlvDrYytVWAK/FALrA4tHHz5/VPXPJa0BGgrQi
893+ Wesu0+21kVIRxQnGGoxxGKPD2LeIOPF6g+1uM7TGjdfajbax/aZvRZXux7khbrtlF1OTT3L66Eew
894+ q08xPjyMkjKkhkGPELFxkgBddC/wECpKPTpJbaCkPDLBE1jE+ikbQBLFgYdofDorw78qQgawo1Rm
895+ Li7xSzPzfMvBo2S7t/KNd95M7fAJPliu8AP1KpH06XBHCo61u5QFbBUWJwo/V6gPGJEy0MocXV3g
896+ 4ggl5QAf8HkQWQ5mTjbP4CeJLVxL/n81BtA3gl6wrJ8/sXf+MlYpSCqG1ZVZYjmEsDHW5nR7TT+x
897+ Oxw7h+f/B+gSxkZoHFZ3URQ4ociG9rD79u+hnW+hUi5RynLOnNmLdZpSlgEdrNVokaOt59U7G4FV
898+ qCgDvICEMcW6uLhFBVUuUEJQQmClh2FbB9rk9DnHutXCmi7WWUyeo12OLRw6xNQH8i7fNnGIj2rL
899+ v3rgfrrzS7x9aZnfu2Eb24UkMRLR01hhIW+zUuSkSlKWIDRIp3HtVR90aoMzjnZvhYPdJi2pkLEI
900+ bOAr3//OOc4eXKTXNg/jFUKXgA7XqLi8t7QAABa8SURBVBV8Na0GE66Bfcf3zZ3kEnxBIeCGWxos
901+ zZ+g15nCFAvgII43+UlgzmGsd31pVh3oB3gpNZkkgeGrcQKMqLBp+1vZuuNdtHoZm28YpzFSZWVt
902+ jbmZw6hiichIL0wtOhR2ASP83D1rte+iwfqcQeGEV/gIcLChMEZOJZmHhHnJMt8oKpdRJNg+XEzD
903+ +TTicMf3AXSieMPX3sXtmxoMHTvBHzQafF+phHBtrO3gbI6SEmMNbeE4nxcoAZEAYQTCSoxM0NaG
904+ 7qSj1Ms5lzuWSmpdE+jKHsBhjeOpT53q4QdGnA+HNOca5eKvxgPYcA0src53/8R7gQuNQAgYvbFM
905+ eVOTgwcepNNa9A0RoQNXW67P0epjArTuhZROBC1+iaVf6hUUroKs7+HmPd/NWmcrM7PeqCbPHWbu
906+ 7GcQ7QPEboEIRRYNBz5CjhCSXrcdRtPEgb9v14dXRUJ41XnrcLlGGYdUMYFWDEBOD5xFFIrDLuVX
907+ Fps8Mr8AwL237aJ3dprvWF7lN8Yb7BQFwhREPUNUFDi9hrQ9hHA0pYeEVYWgagPL2DgoNCLvIYsu
908+ 6B5fXsv5PZOiyrHnAzyvIIRg7vQqpnAfwY+Pm2NjhNxX1QAGY4EV4BOHHpue93esu6AaKKXjtjeP
909+ 09i1zN69H+LcyYfB9oJokvQdPSfJ0iEA2p1FX/IMxEjrbCDhCJyIQDkKvUqnqLBl+zsRYsTDzJ0l
910+ 757m0MRfcPK5DyM6Z4m0IbGCKM4QQvlrJjSXCLQtbcK8IHxtwoRKIlZ7CJdzHjjiHMpKOlbxx0XO
911+ vzh0mAfX2j7VBR5/eoIfi1K+vTJEbARYhbUKJROkSpFpCtoHaGfzHGMdw7EkdRKHwcmCOJJIa5m3
912+ jt86v8J7l9ocH1aYyBGniiiWXk/5EtRwF6qGj3/iZBt4JhjA8kD1j5fCAPo1gcXWUu8XTu9f/AoP
913+ ICNBnCm23Fxn1911Zmf3cnTio3TWZpFCIYWXRZfKB1m9vBWict/ONQhUFCNU5NGyuiC1ZZQQ5LJg
914+ qLoFpSRjI1tp1Lfzmq+5lUZFs+8fPsDs9F+h7Hmk9YYp+pi//jgXB1boPncuCDn05xb6drQNBtMF
915+ nrCO9547y385d5Y33b+H+8arvH6kjgN2ZQk3CbxiuPEAZCmsJ7tYhyusxzkawYx2GGAsjkmEQgtH
916+ 4QTaCiaM5Ieml/iZXhs9mpBUYkrVhGwoJk7VZa8AIQTHn5qj19EPApPB/bd4gVPFrxJusF4TaAJf
917+ ePKhk+d1bi94U1IK4lRRriWM7sjY+foKbXOYw/s/zOL5g0ExE6I4WSdoCh0DXi1bSOnRNdbTtqVU
918+ OBWuZifJnSWSiihrUNv6TtrchqPEnbfuoLW8n4ln/4D24uNEbnldzMG5vhagwNpAC3NgnURqn3+b
919+ /sdzcF5KfqnZ5FuOnGBqpM4Dd9+OPTnFf5YRX5t5PONmKUiVQOgwmkZYTzoRAm16KCfQUiBkj6Wg
920+ KDamEo+N6vWQ1vHB5TXeMXmeR1NHWk8p1xKGhlOGRlJK1YQ48wZwqcDPGsfEo1PTofM3GQAgPV7g
921+ 2LhrMQATosx5Z/mhw1+esbBRHRTSR69pOWJoJKWxOWPnaxtUt3U5cvhPmT7xSZTNKaXDPgi0XZAF
922+ hghrPJ5OEtTDjPUnWElUCNKUkBTagEvo5mPI+v0M7Xwn00sJw40xdmypcur4Jzl16BO41hkS0wVR
923+ YLCYvrYgEEtB6gxWOHpZhrIJPSx/qwu++cRJfnNhibe88fWMpjF7Tk7xR8Oj3IulW/ggsCYFCh2K
924+ W73AezB+jE2WetUz5+Ve9696YchdyiFsj6NS8h9nlvjJtVXaIzHlakylkVAby6hvKlEdzShVY6JE
925+ XqIO4A35yb8+RXdNPwicDdH/ixonH13Dz9rgBVaAkxOPTD+x/baRNza2lAa8AIhEIqRntqhIEsWS
926+ ykjEzLG/p3Vgki2bd/pAK18DVYAuhRzR4/SFdBg8ZVsEkSYhBJHyX64VGhfnaCRSbGPLTe+F/Ayn
927+ j3+GHVvGkbLJs3t/l10772fTlgcg2owRYl1oqiwkkZJEWiOs4HDi+I3zTT48NcueO27i60eqtJ87
928+ yq9s3sRrxmpIUeAKFbgNsDVLSJwJo/LA9tq+npGkvosYysldK1kKSKYsTfi0gR+bnmeloshqCWkp
929+ olRNKDcShhoppVpCWo785it5iXRb0G7mnDmw8ASwj40JIS9qinh0jT/f9wILwE88/emTf/22H7yz
930+ bo3bqMBJULEgEZ7domJJnCrSUkRzcp7nDh1fL9rEARSCEEhiT/VyFiUUVvQRfIECHoo1gkAocT7g
931+ tXIIUbqVG/dso7t4gPmZR7l991YWV45y8NkT7Nr1Jsojd9G0Xql7S7WEiCVNkfCxdpOfPnICVxvi
932+ 7V93L2dPneNtnR7ftWmURpgOZoUkTxR58HTb4xRhQmvagYhKvgcRvKC1BQ5HU8ChIA37OwurlHSP
933+ 1XpMkimyoZhyLaHSSKjUU7KhmKSkLisI5Zyfcv7/PjhhnOOT+JGxg3e/e7kMoJ8RrAIzC5PtX3v2
934+ 4cmf2fP1Wwfq8yF46bNbI0mUSOJMEWc51dESpyfO01peZW52H9Xh+wGJdl2USEOs4DH8QiikDEJL
935+ rt+d8wpgHlatPXMGiaNCafiNVOt3Mjf5RYw7QJq0OXHib9iRz5EIQaQUHW34MhF/MjvNx+eX+Of3
936+ 3Alo9OET/PpwnXulQkiLRflp884ibMFKQAOVrfVzDYQnhAjrW9TG9ZAixtkcS8xZm7EiHJvGhlCV
937+ mLXIUMpiStWYcj2l0kgoh1O/HvTJS2++kIJDX5yi29IfwM8InAyRf86LHB+vXsC/cQP1gan5c63t
938+ u187flucqY20cACpsn4VJJIoVojYUm5EZGXF5Olnaa9MM1QZJ1J1UAXdooOUGUrJdVFGHHRbp1iY
939+ P0J5aIT6+OtwQqBU0k/dg9iDxcgylZGbqTd2sLy0RCK69DozWN1kvFFjWRf8+eQMk0nEWx94LYtn
940+ p3lru+B/bN7EzaYgCtO/pSGkkAIdKT64ssqJVofvGa1xl8pwLvcgkDCcoiha2DjGanhGC347Lyjt
941+ GOG8XUOmglLVB3rVsRK1kYxKIx2I+OUlN7//fc6fXeXxB089B3wCPxRikhc5NPqFeoDBq6AdAsJf
942+ /Mz7D3zjO37k7kqcqgvRQiHMVANyJyr2cUGcRpTrKUszJ3j2H45xx9d8F+Xh3SAFVhRYzXo+79E2
943+ 4RcbE3QFgzSLLTDOEkWpHx0vDMZJovgmbrx9NzY/yqkjD9JqztNqt8kLzVtfv8eXXJ86zP/dPMwt
944+ ThJ3OlibY1WMyzUySvzvtRYnFEuFRgkYipT/+AJ0r4NKI5wVyChjzTp+dbXDJ4zD1mOWl1coDSX+
945+ 1NcSyvWEUnXg1EdXbvo457Da8cWPHJsHPogfDDU10PR5UZv/Qj3AYIXQAMYa91hzdu3dO+8alb4z
946+ Ky4whP61IEKZM0p8XBCnimwoISk7Th57glbzLOPjdyFlKUi8eSVucHRap1mcO0xaatDY9AbfGcQh
947+ pELKyD+3CuVE0EcJeP2kzuimPSRxhdmZCcAR93r8V5Hy49VhxmJLpDQI61VHZYySymv24LGBhZR8
948+ oNlioZvzg5sb7JICJyVKRl5NREQ8U8CPzi/zxSxitWTpKR1OfUZtzEf4lXpCVvGnfkP+9dKbb51F
949+ ScXDf3TIrS703hdKvsdD1a97tYifr1YaeLmsoBci0UPTx1d/dv/Dk+uiSZdyZVIKokSRliMq9ZTa
950+ WInG5hKbdtW4+d5RbDTFs0/9JmuL+9eHObkwWbPfrLFGB1aZC9Vbs07SQGovSm2UvxqcRhYF2tWo
951+ D99OtexT0H+TJryjqlBJQZxL3400kjgdgnYnALbk+ufQzpJbixBQw2GFXSe85kLye601vn+5yfHh
952+ jKU4R2SCSiOlNu4/X20so9JISIdi4tRTvp7v5AsET3/6FAvn1j4UNv9k2PzOV2vzX8wVMGgEnfDG
953+ Pn7osZmbh4azf3vT68bWg8KvAJHigvV76NO6N0gUlWqF8ydXOTTxR2zb8mY27/wmbJSG9nI2EAT2
954+ tfv6uEDr5wgEb+GUbxMLaxEqQTqJRZIbD9FuxArpOkgXexRvr4MslT2+P45xhQ7AFYEyhk4S0zG+
955+ hVxzEmctGsGkdvzqcovPK0uvoeiKnFIloVyLqTRSKg1f5EnKkS/vrk/+uHKXDwHnDi5z7Km5TwKP
956+ hpM/82Jz/pfKAHRIR2aA33jqoVNJqRq954ZbGuLyRhBigzAPTyofKKq4YExkVEdv5MSzTzC/9wh3
957+ vuYHkeVRYlkJ4F/txadcEsAhLiCBNgys33o2uFBPsOsy7n0ouww3qBACkaSBaCI8mllrT+0KqWfb
958+ Olq5JlMQxQJyxd/mmp+YW4CxEkto4lhRGfL3fKWRenc/FJNk0fPe9Rfj7M9NLPGlj5/YCzw0EPSt
959+ cI1TQV/qK+DiAtFqCFB+/ZE/O/Y3MyeaHsV7GYpLPza44FpoJNTHMipjsPvuURrbNPv2/hoLk18g
960+ i7QXfDJdHH00j8PoTiCEBIHKQDrtZyFCep0/bYoNl64FykQIKzyVu98JNNbTvKzzM4OtwWHoCGgb
961+ TSolvSjmf7bW+MnOGmtjKctKUxqKGRrx1bzGpjL1sRKVekpajlCxvKrN7zd5Jg8t86WPnTgAfAA4
962+ Apx5Mc2el9oDXBwPNEOJ8mf+/sNHq//sPbc8sO32Bv1C0eUNwdcN4v6YtJA2JiVFuRYzc+JvOXMq
963+ NHGsRoYSrwBUmoH1U8uEUH5olYz81A5XeOFpBFZYTEAFp9IzkTz4QK/PKkBbL97k+tKtDiOhLUM7
964+ 1Dj+28IKB5WhmVpkLChX0q8s6mTKb/xVqnz3c/2pw8s89pfHJ4DfxY+FPx1q/deE83s5soDnuxJ6
965+ wONnDi7e1Bgv7a6OZc/7RfSFkfvXQRQrXztIBbWRMlIKWs02wgnGb7gPIRuelaNb+Il/vqcQR0lQ
966+ 7rQoFWRpBFAsM3vuSzjn+ObhGvclXqbNij4NJcDD8JpBRvh2sQEe05a/aq4yPDLEVGJoR4akFFGp
967+ J1RHMmpjGdURX8dPSlHQ+Lt6lW+AmaMrPPoXxyaA3wqbfyoAPa8Z5fOPaQAXG8GTZyeWFPC68Z3V
968+ rygUXc4byNBYiiLlefLKEZcE9dEhFGVWFibJhrYSx1WEi5Eiw5kVP4hJKj+8Snn2UF/xUhaLTJ97
969+ Aucs72rUuKckEYVASOPl7SyYwqA7bWQSUWCQQjFRaH7HGCpbR1imi0wlpZpP76qjGbXRC4s6KpJX
970+ LfHeJ7tMPDLFUw+d/jzwB8DhsPkLL/Xmv1QGcLERPDd3prXYWuh93fY7hte7Wlf2BGJdI0fFkihR
971+ ASkDScXR6zSZOv401VKDNN1EbhZJowaFLhCR5yQ54TBFBykTf7KLZWYmH8day7uHa9yVRCir/Ddg
972+ DYXpYnVBkpXJRYG0ko91DD+8tMRkBoumQzbk45TBU1+uJSSl6AKXf7Wbj4DHPnqcY0/N/SXw0QG3
973+ /7Js/lczBrgcjKxfLPqzMxOLs62l7s+/+dtvqZTqcQh4r2wIfizfRk9BJZI4LVAxINocPvCHjIzu
974+ Ycuud+ASi5AG5bwSl3COKCr5vrr1TSc3qH0ZZhIa4183OqcUV9GFYEYofqG5wqeFoTccE6dQGQr3
975+ fEjt0kpMnKkLELzXsvHdluaRPz3SXT7feT/w5VDjnwoBX+eluvNfLg8waAhFMIaZTqv4zJEnZ9+y
976+ eVe9Xq6lrMs9iMsbAYKNMnIUvEEiSasR5WrK8twMizP7SaOIUmX7urR8kbdDAzFoExaLzEw+iXOO
977+ b61VeW3qB1IgBNJKRKSwTvDXRZcfX1nlSymYIY9vKNcThsKpHxrOKFUTkpJav+uvNsrvr9mTq3zu
978+ 9yfmumv6Z4G9A6lef/Pdy7H5L7UBXFwy7oYP99lTz8yvrcx1vnb77Y3nPTmDpeT17mIsiSJJlErK
979+ wwmJijhz/GnWlo5Sr2xBqgoqzfwUTxVk6Ippzk/vw1jLdwzXuCON/evOYp1jgYL/s7LGz3Y6zNUk
980+ KlOUhjxgozqahWpe6k/+Nd/1/SeCL/7FcQ783eRDwO8Az+Ep3dNskDpfts1/OQzgUjFBBziyMt99
981+ +uS++ddn1aTW2FzaqPI9jzfoT9P2haOgo5MYasNl8k6L08e/jKNHaehGokgiXYSwCnrnmJ7ci3WO
982+ 94yOcgcOVYDBsk8b/t38Mp+KHaaqSLKIci2hOuK7d9XRjHI9IS3H13zX+36F4OzBJf7uT4/MLs+0
983+ 3wd8diDHn+MaNH1erQbAgCfIwwc+r3P72clDSyzNtPfUN5VU5sefXtYQ1r2B5ALEUZT4ADGuCJIy
984+ LEwfZXn2AFlWIimPIkSCyWeZn3kW6wzfPV7jVjTGSt63usxPr61xuiKRZUVW8WXc6mhGdbQU0rvE
985+ 5/bR1Rd1+gbbnOvy5IOnikOPzXzW5Pa38Uje42yQOa9azOHVbgCXigvWgP2rC91PHX967uZOq9g6
986+ fENFREmARF3GEr7iSkj6sYE/vZVGStFtMX16H/naOeqNTQjXZGF2gkJr3jNap+4EP7fS4n26Q16L
987+ iUsRpaqv6FXHMmqjJZ/eVaL14Y1XU8fvv95dLXjmc2fNUw+dPtha6v0v4OFw1/dP/Wo4DIZ/xCX+
988+ EX9vfzxdGagBm4DtwE9tu23kvvu/eSdJSV1F+dSTN3VhKbqGXrugs1rQXslpN3Pmz7XQa2WSKGNh
989+ 4RzOOX7ipu18caXJocQSZx6uloX7fj3Kfx6kzsV3fP/lvKN56qHTnDu8NIHjQyGnn8JDuJaD0ecv
990+ R4r3SjaAwV6ExOsulIA6MAbcJqV49+iOoXfc8eYb2HJr7UL1e3EJQ7AeMt03hG6rYG0lp7OSs7rY
991+ Ze70Kt01jXCCRiODsiBJFWnlInzeVVbz/Ka7dc3h2ROrTDwyxdy51uedcQ+Hgs5MOO1LAxuvX+5A
992+ 75VsAP33EKZOEQMV/Aj0EWAceHdtPHvb9tuGd+y4c4T65hLrA74usob+AEqrHUXPkHc03Zb3CJ1W
993+ TnulwHYt2llUJEhLcejeJRu5/eWQOm5giGq4ClbmOkweWeLUMwtzK/Pdx4FPhpN+PhRzlkO803ul
994+ bfwryQAu9gh9Q8iAIfw49BFgK/Dvq6Pp3be9cUt5fGdNlqrxhp5uP9J01msKWYcprDeE4BE6rYK8
995+ XWC01+LNKj7a7+f1Kg4unz4I+SLj0o7OasH85Ko79uRcZ3Fq7Tjw58HNz4VN78u0tAdcvX2lbfwr
996+ 1QAuvhriAa9QCVeE9wyCt0vBO8e2Vxs77x5l6y0NkpJaF1ZavxYsGG3RwSMUPYM1DqkESSkiySLP
997+ xIk2tI3WT7xz9DqauTMtTu9fYOZEs2WNewSvxzMXOnXL4bEa3HyXDaVu90rd+Fe6AQxeDYPXQxo8
998+ QyV4hyqwGdgN3JKWo9elpejWtBLXh2+oUB1NqY6mlGupp4wLh9F+jpGnZlucgd5aQXvFB46tpR6t
999+ xS55x8zlXX2219YToT5/lg0a9urAhnfYEGcePO2v6I1/NRjAlYwhCoFjP3gshWyiFIwjA3aErGIc
1000+ GA6vJwOf2Qyko6t4LMNcCNz6Yov9Rzs8Bv+uH9CZV8tpfzUbwJWMYTBuGHwkA8+jAcNZlwkaKE65
1001+ gc0swvN84Hkx8HqfhWsHClyv2iV49a/BsVryoocaMJTBnxsci+wucttm4O/sJV53/BNagn9aS1zj
1002+ c3eZ58/32vV1fV1f19f1dX1dX9fX9XV9XV/X1/V1fb0a1/8He8q//0YPCp0AAAAASUVORK5CYII=</field>
1003+ </record>
1004+
1005+ <record id="badge_idea" model="gamification.badge">
1006+ <field name="name">Brilliant</field>
1007+ <field name="description">With your brilliant ideas, you are an inspiration to others.</field>
1008+ <field name="rule_auth">everyone</field>
1009+ <field name="rule_max">True</field>
1010+ <field name="rule_max_number">2</field>
1011+ <field name="image">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
1270+ AABJRU5ErkJggg==</field>
1271+ </record>
1272+ </data>
1273+
1274+
1275+ <data noupdate="0">
1276+
1277+ <record id="email_template_badge_received" model="email.template">
1278+ <field name="name">Received Badge</field>
1279+ <field name="body_html"><![CDATA[
1280+ <p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> !
1281+ % if ctx["user_from"]
1282+ This badge was granted by <strong>${ctx["user_from"]}</strong>.
1283+ % endif
1284+ </p>
1285+
1286+ % if object.comment
1287+ <p><em>${object.comment}</em></p>
1288+ % endif
1289+ ]]></field>
1290+ </record>
1291+ </data>
1292+</openerp>
1293
1294=== added file 'gamification/badge_view.xml'
1295--- gamification/badge_view.xml 1970-01-01 00:00:00 +0000
1296+++ gamification/badge_view.xml 2013-06-28 14:51:48 +0000
1297@@ -0,0 +1,219 @@
1298+<?xml version="1.0" encoding="UTF-8"?>
1299+<openerp>
1300+ <data>
1301+
1302+ <!-- Badge views -->
1303+ <record id="badge_list_action" model="ir.actions.act_window">
1304+ <field name="name">Badges</field>
1305+ <field name="res_model">gamification.badge</field>
1306+ <field name="view_mode">kanban,tree,form</field>
1307+ <field name="help" type="html">
1308+ <p class="oe_view_nocontent_create">
1309+ Click to create a badge.
1310+ </p>
1311+ <p>
1312+ A badge is a symbolic token granted to a user as a sign of reward.
1313+ It can be deserved automatically when some conditions are met or manually by users.
1314+ Some badges are harder than others to get with specific conditions.
1315+ </p>
1316+ </field>
1317+ </record>
1318+
1319+
1320+ <record id="view_badge_wizard_grant" model="ir.ui.view">
1321+ <field name="name">Grant Badge User Form</field>
1322+ <field name="model">gamification.badge.user.wizard</field>
1323+ <field name="arch" type="xml">
1324+ <form string="Grant Badge To" version="7.0">
1325+ Who would you like to reward?
1326+ <group>
1327+ <field name="user_id" nolabel="1" />
1328+ <field name="badge_id" invisible="1"/>
1329+ <field name="comment" nolabel="1" placeholder="Describe what they did and why it matters (will be public)" class="oe_no_padding" />
1330+ </group>
1331+ <footer>
1332+ <button string="Grant Badge" type="object" name="action_grant_badge" class="oe_highlight" /> or
1333+ <button string="Cancel" special="cancel" class="oe_link"/>
1334+ </footer>
1335+ </form>
1336+ </field>
1337+ </record>
1338+
1339+ <act_window domain="[]" id="action_grant_wizard"
1340+ name="Grant Badge"
1341+ target="new"
1342+ res_model="gamification.badge.user.wizard"
1343+ context="{'default_badge_id': active_id, 'badge_id': active_id}"
1344+ view_type="form" view_mode="form"
1345+ view_id="gamification.view_badge_wizard_grant" />
1346+
1347+ <record id="badge_list_view" model="ir.ui.view">
1348+ <field name="name">Badge List</field>
1349+ <field name="model">gamification.badge</field>
1350+ <field name="arch" type="xml">
1351+ <tree string="Badge List">
1352+ <field name="name"/>
1353+ <field name="stat_count"/>
1354+ <field name="stat_this_month"/>
1355+ <field name="stat_my"/>
1356+ <field name="rule_auth"/>
1357+ </tree>
1358+ </field>
1359+ </record>
1360+
1361+ <record id="badge_form_view" model="ir.ui.view">
1362+ <field name="name">Badge Form</field>
1363+ <field name="model">gamification.badge</field>
1364+ <field name="arch" type="xml">
1365+ <form string="Badge" version="7.0">
1366+ <header>
1367+ <button string="Grant this Badge" type="action" name="%(action_grant_wizard)d" class="oe_highlight" attrs="{'invisible': [('remaining_sending','=',0)]}" />
1368+ <button string="Check Badge" type="object" name="check_automatic" groups="base.group_no_one" />
1369+ </header>
1370+ <sheet>
1371+ <div class="oe_right oe_button_box">
1372+ </div>
1373+ <field name="image" widget='image' class="oe_left oe_avatar"/>
1374+ <div class="oe_title">
1375+ <label for="name" class="oe_edit_only"/>
1376+ <h1>
1377+ <field name="name"/>
1378+ </h1>
1379+ </div>
1380+ <group>
1381+ <field name="description" nolabel="1" placeholder="Badge Description" />
1382+ </group>
1383+ <group string="Granting">
1384+ <field name="rule_auth" widget="radio" />
1385+ <field name="rule_auth_user_ids" attrs="{'invisible': [('rule_auth','!=','users')]}" widget="many2many_tags" />
1386+ <field name="rule_auth_badge_ids" attrs="{'invisible': [('rule_auth','!=','having')]}" widget="many2many_tags" />
1387+ <field name="rule_max" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
1388+ <field name="rule_max_number" attrs="{'invisible': ['|',('rule_max','=',False),('rule_auth','=','nobody')]}"/>
1389+ <label for="stat_my_monthly_sending"/>
1390+ <div>
1391+ <field name="stat_my_monthly_sending" attrs="{'invisible': [('rule_auth','=','nobody')]}" />
1392+ <div attrs="{'invisible': [('remaining_sending','=',-1)]}" class="oe_grey">
1393+ You can still grant <field name="remaining_sending" class="oe_inline"/> badges this month
1394+ </div>
1395+ <div attrs="{'invisible': [('remaining_sending','!=',-1)]}" class="oe_grey">
1396+ No monthly sending limit
1397+ </div>
1398+ </div>
1399+ </group>
1400+ <group string="Rewards for challenges">
1401+ <field name="plan_ids" widget="many2many_kanban" nolabel="1" />
1402+ </group>
1403+ <group string="Statistics">
1404+ <group>
1405+ <field name="stat_count"/>
1406+ <field name="stat_this_month"/>
1407+ <field name="stat_count_distinct"/>
1408+ </group>
1409+ <group>
1410+ <field name="stat_my"/>
1411+ <field name="stat_my_this_month"/>
1412+ </group>
1413+ </group>
1414+ </sheet>
1415+ </form>
1416+ </field>
1417+ </record>
1418+
1419+
1420+ <record id="badge_kanban_view" model="ir.ui.view" >
1421+ <field name="name">Badge Kanban View</field>
1422+ <field name="model">gamification.badge</field>
1423+ <field name="arch" type="xml">
1424+ <kanban version="7.0" class="oe_background_grey">
1425+ <field name="name"/>
1426+ <field name="description"/>
1427+ <field name="image"/>
1428+ <field name="stat_my"/>
1429+ <field name="stat_count"/>
1430+ <field name="stat_this_month"/>
1431+ <field name="unique_owner_ids"/>
1432+ <field name="stat_my_monthly_sending"/>
1433+ <field name="remaining_sending" />
1434+ <field name="rule_max_number" />
1435+ <templates>
1436+ <t t-name="kanban-box">
1437+ <div t-attf-class="#{record.stat_my.raw_value ? 'oe_kanban_color_5' : 'oe_kanban_color_white'} oe_kanban_card oe_kanban_global_click oe_kanban_badge">
1438+ <div class="oe_kanban_content">
1439+ <div class="oe_kanban_left">
1440+ <a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.image.raw_value)" t-att-title="record.name.value" width="90" height="90" /></a>
1441+ </div>
1442+ <div class="oe_no_overflow">
1443+ <h4><field name="name"/></h4>
1444+ <t t-if="record.remaining_sending.value != 0">
1445+ <button type="action" name="%(action_grant_wizard)d" class="oe_highlight">Grant</button>
1446+ <span class="oe_grey">
1447+ <t t-if="record.remaining_sending.value != -1">
1448+ <t t-esc="record.stat_my_monthly_sending.value"/>/<t t-esc="record.rule_max_number.value"/>
1449+ </t>
1450+ <t t-if="record.remaining_sending.value == -1">
1451+ <t t-esc="record.stat_my_monthly_sending.value"/>/∞
1452+ </t>
1453+ </span>
1454+ </t>
1455+ <t t-if="record.remaining_sending.value == 0">
1456+ <div class="oe_grey">Can not grant</div>
1457+ </t>
1458+ <p>
1459+ <strong><t t-esc="record.stat_count.raw_value"/></strong> granted,<br/>
1460+ <strong><t t-esc="record.stat_this_month.raw_value"/></strong> this month
1461+ </p>
1462+ </div>
1463+ <div class="oe_kanban_badge_avatars">
1464+ <t t-if="record.description.value">
1465+ <p><em><field name="description"/></em></p>
1466+ </t>
1467+ <a type="object" name="get_granted_employees">
1468+ <t t-foreach="record.unique_owner_ids.raw_value.slice(0,11)" t-as="owner">
1469+ <img t-att-src="kanban_image('res.users', 'image_small', owner)" t-att-data-member_id="owner"/>
1470+ </t>
1471+ </a>
1472+ </div>
1473+ </div>
1474+ </div>
1475+ </t>
1476+ </templates>
1477+ </kanban>
1478+ </field>
1479+ </record>
1480+
1481+
1482+ <!-- Badge user viewss -->
1483+
1484+ <record id="badge_user_kanban_view" model="ir.ui.view" >
1485+ <field name="name">Badge User Kanban View</field>
1486+ <field name="model">gamification.badge.user</field>
1487+ <field name="arch" type="xml">
1488+ <kanban version="7.0" class="oe_background_grey">
1489+ <field name="badge_name"/>
1490+ <field name="badge_id"/>
1491+ <field name="user_id"/>
1492+ <field name="comment"/>
1493+ <field name="create_date"/>
1494+ <templates>
1495+ <t t-name="kanban-box">
1496+ <div class="oe_kanban_card oe_kanban_global_click oe_kanban_badge oe_kanban_color_white">
1497+ <div class="oe_kanban_content">
1498+ <div class="oe_kanban_left">
1499+ <a type="open"><img t-att-src="kanban_image('gamification.badge', 'image', record.badge_id.raw_value)" t-att-title="record.badge_name.value" width="24" height="24" /></a>
1500+ </div>
1501+ <h4>
1502+ <a type="open"><t t-esc="record.badge_name.raw_value" /></a>
1503+ </h4>
1504+ <t t-if="record.comment.raw_value">
1505+ <p><em><field name="comment"/></em></p>
1506+ </t>
1507+ <p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="record.create_date.raw_value.toString(Date.CultureInfo.formatPatterns.shortDate)" /></p>
1508+ </div>
1509+ </div>
1510+ </t>
1511+ </templates>
1512+ </kanban>
1513+ </field>
1514+ </record>
1515+ </data>
1516+</openerp>
1517
1518=== added file 'gamification/cron.xml'
1519--- gamification/cron.xml 1970-01-01 00:00:00 +0000
1520+++ gamification/cron.xml 2013-06-28 14:51:48 +0000
1521@@ -0,0 +1,16 @@
1522+<?xml version="1.0" encoding="UTF-8"?>
1523+<openerp>
1524+ <data>
1525+ <record forcecreate="True" id="ir_cron_check_plan"
1526+ model="ir.cron">
1527+ <field name="name">Run Goal Plan Checker</field>
1528+ <field name="interval_number">1</field>
1529+ <field name="interval_type">days</field>
1530+ <field name="numbercall">-1</field>
1531+ <field eval="False" name="doall" />
1532+ <field name="model">gamification.goal.plan</field>
1533+ <field name="function">_cron_update</field>
1534+ <field name="args">()</field>
1535+ </record>
1536+ </data>
1537+</openerp>
1538\ No newline at end of file
1539
1540=== added directory 'gamification/doc'
1541=== added file 'gamification/doc/gamification_plan_howto.rst'
1542--- gamification/doc/gamification_plan_howto.rst 1970-01-01 00:00:00 +0000
1543+++ gamification/doc/gamification_plan_howto.rst 2013-06-28 14:51:48 +0000
1544@@ -0,0 +1,107 @@
1545+How to create new challenge for my addon
1546+========================================
1547+
1548+Running example
1549++++++++++++++++
1550+
1551+A module to create and manage groceries lists has been developped. To motivate users to use it, a challenge (gamification.goal.plan) is developped. This how to will explain the creation of a dedicated module and the XML file for the required data.
1552+
1553+Module
1554+++++++
1555+
1556+The challenge for my addon will consist of an auto-installed module containing only the definition of goals. Goal type are quite technical to create and should not be seen or modified through the web interface.
1557+
1558+If our groceries module is called ``groceries``, the structure will be consisted of three addons :
1559+
1560+::
1561+
1562+ addons/
1563+ ...
1564+ gamification/
1565+ groceries/
1566+ groceries_gamification/
1567+ __openerp__.py
1568+ groceries_goals.xml
1569+
1570+The ``__openerp__.py`` file containing the following information :
1571+
1572+::
1573+
1574+ {
1575+ ...
1576+ 'depends': ['gamification','groceries'],
1577+ 'data': ['groceries_goals.xml'],
1578+ 'auto_install': True,
1579+ }
1580+
1581+
1582+Goal type definition
1583++++++++++++++++++++++
1584+
1585+For our groceries module, we would like to evaluate the total number of items written on lists during a month. The evaluated value being a number of line in the database, the goal type computation mode will be ``count``. The XML data file will contain the following information :
1586+
1587+::
1588+
1589+ <record model="gamification.goal.type" id="type_groceries_nbr_items">
1590+ <field name="name">Number of items</field>
1591+ <field name="computation_mode">count</field>
1592+ ...
1593+ </record>
1594+
1595+To be able to compute the number of lines, the model containing groceries is required. To be able to refine the computation on a time period, a reference date field should be mention. This field date must be a field of the selected model. In our example, we will use the ``gorceries_item`` model and the ``shopping_day`` field.
1596+
1597+::
1598+
1599+ <record model="gamification.goal.type" id="type_groceries_nbr_items">
1600+ ...
1601+ <field name="model_id" eval="ref('groceries.model_groceries_item')" />
1602+ <field name="field_date_id" eval="ref('groceries.field_groceries_item_shopping_day')" />
1603+ </record>
1604+
1605+As we do not want to count every recorded item, we will use a domain to restrict the selection on the user (display the count only for the items the users has bought) and the state (only the items whose list is confirmed are included). The user restriction is made with the keyword ``user_id`` in the domain and should correspond to a many2one field with the relation ``res.users``. During the evaluation, it is replaced by the user ID of the linked goal.
1606+
1607+::
1608+
1609+ <record model="gamification.goal.type" id="type_groceries_nbr_items">
1610+ ...
1611+ <field name="domain">[('shopper_id', '=', user_id), ('list_id.state', '=', 'confirmed')]</field>
1612+ </record>
1613+
1614+An action can also be defined to help users to quickly reach the screen where they will be able to modify their current value. This is done by adding the XML ID of the ir.action we want to call. In our example, we would like to open the grocery list form view owned by the user.
1615+
1616+If we do not specify a res_id to the action, only the list of records can be displayed. The restriction is done with the field ``res_id_field`` containing the field name of the user profile containing the required id. In our example, we assume the res.users model has been extended with a many2one field ``groceries_list`` to the model ``groceries.list``.
1617+
1618+::
1619+
1620+ <record model="gamification.goal.type" id="type_groceries_nbr_items">
1621+ ...
1622+ <field name="action_id">groceries.action_groceries_list_form</field>
1623+ <field name="res_id_field">groceries_list.id</field>
1624+ </record>
1625+
1626+
1627+Plan definition
1628+++++++++++++++++
1629+
1630+Once all the goal types are defined, a challenge (or goal plan) can be created. In our example, we would like to create a plan "Discover the Groceries Module" with simple tasks applied to every new user in the group ``groceries.shoppers_group``. This goal plan should only be applied once by user and with no ending period, no dates or peridocity is then selected. The goal will be started manually but specifying a value to ``start_date`` can make it start automatically.
1631+
1632+::
1633+
1634+ <record model="gamification.goal.plan" id="plan_groceries_discover">
1635+ <field name="name">Discover the Groceries Module</field>
1636+ <field name="period">once</field>
1637+ <field name="visibility_mode">progressbar</field>
1638+ <field name="report_message_frequency">never</field>
1639+ <field name="planline_ids" eval="[(4, ref('planline_groceries_discover1'))]"/>
1640+ <field name="autojoin_group_id" eval="ref('groceries.shoppers_group')" />
1641+ </record>
1642+
1643+To add goal types to a plan, planlines must be created. The value to reach is defined for a planline, this will be the ``target_goal`` for each goal generated.
1644+
1645+::
1646+
1647+ <record model="gamification.goal.planline" id="planline_groceries_discover1">
1648+ <field name="type_id" eval="ref('type_groceries_nbr_items')" />
1649+ <field name="target_goal">3</field>
1650+ <field name="plan_id" eval="ref('plan_groceries_discover')" />
1651+ </record>
1652\ No newline at end of file
1653
1654=== added file 'gamification/doc/goal.rst'
1655--- gamification/doc/goal.rst 1970-01-01 00:00:00 +0000
1656+++ gamification/doc/goal.rst 2013-06-28 14:51:48 +0000
1657@@ -0,0 +1,46 @@
1658+.. _gamification_goal:
1659+
1660+gamification.goal
1661+=================
1662+
1663+Models
1664+++++++
1665+
1666+``gamification.goal`` for the generated goals from plans
1667+
1668+.. versionchanged:: 7.0
1669+
1670+Fields
1671+++++++
1672+
1673+ - ``type_id`` : The related gamification.goal.type object.
1674+ - ``user_id`` : The user responsible for the goal. Goal type domain filtering on the user id will use that value.
1675+ - ``planline_id`` : if the goal is generated from a plan, the planline used to generate this goal
1676+ - ``plan_id`` : if the goal is generated from a plan, related link from planline_id.plan_id
1677+ - ``start_date`` : the starting date for goal evaluation. If a goal is evaluated using a date field (eg: creation date of an invoice), this field will be used in the domain. Without starting date, every past recorded data will be considered in the goal type computation.
1678+ - ``end_date`` : the end date for goal evaluation, similar to start_date. When an end_date is passed, the goal update method will change to status to reached or failed depending of the current value.
1679+ - ``target_goal`` : the numerical value to reach (higher or lower depending of goal type) to reach a goal
1680+ - ``current`` : current computed value of the goal
1681+ - ``completeness`` : percentage of completion of a goal
1682+ - ``state`` :
1683+ - ``draft`` : goal not active and displayed in user's goal list. Only present for manual creation of goal as plans generate goals in progress.
1684+ - ``inprogress`` : a goal is started and is not closed yet
1685+ - ``inprogress_update`` : in case of manual goal, a number of day can be specified. If the last manual update from the user is older than this number of days, a reminder will be sent and the state changed to this.
1686+ - ``reached`` : the goal is succeeded
1687+ - ``failed`` : the goal is failed
1688+ - ``canceled`` : state if the goal plan is canceled
1689+ - ``remind_update_delay`` : the number of day before an inprogress goal is set to inprogress_update and a reminder is sent.
1690+ - ``last_update`` : the date of the last modification of this goal
1691+ - ``computation_mode`` : related field from the linked goal type
1692+ - ``type_description`` : related field from the linked goal type
1693+ - ``type_suffix`` : related field from the linked goal type
1694+ - ``type_condition`` : related field from the linked goal type
1695+
1696+
1697+Methods
1698++++++++
1699+
1700+ - ``update`` :
1701+ Compute the current value of goal and change states accordingly and send reminder if needed.
1702+ - ``get_action`` :
1703+ Returns the action description specified for the goal modification. If an action XML ID is specified in the goal type definition it will be used. If a goal is manual, the action is a simple wizard to input a new value.
1704
1705=== added file 'gamification/doc/index.rst'
1706--- gamification/doc/index.rst 1970-01-01 00:00:00 +0000
1707+++ gamification/doc/index.rst 2013-06-28 14:51:48 +0000
1708@@ -0,0 +1,17 @@
1709+Gamification module documentation
1710+=================================
1711+
1712+The Gamification module holds all the module and logic related to goals, plans and badges. The computation is mainly done in this module, other modules inherating from it should only add data and tests.
1713+
1714+Goals
1715+-----
1716+A **Goal** is an objective applied to an user with a numerical target to reach. It can have a starting and end date. Users usually do not create goals but relies on goal plans.
1717+
1718+A **Goal Type** is a generic objective that can be applied to any structure stored in the database and use numerical value. The creation of goal types is quite technical and should rarely be done. Once a generic goal is created, it can be associated to several goal plans with different numerical targets.
1719+
1720+A **Goal Plan** is a a set of goal types with a target value to reach applied to a group of users. It can be periodic to create and evaluate easily the performances of a team.
1721+
1722+Badges
1723+------
1724+A **Badge** is a symbolic token granted to a user as a sign of reward. It can be offered by a user to another or automatically offered when some conditions are met. The conditions can either be a list of goal types succeeded or a user definied python code executed.
1725+
1726
1727=== added file 'gamification/goal.py'
1728--- gamification/goal.py 1970-01-01 00:00:00 +0000
1729+++ gamification/goal.py 2013-06-28 14:51:48 +0000
1730@@ -0,0 +1,404 @@
1731+# -*- coding: utf-8 -*-
1732+##############################################################################
1733+#
1734+# OpenERP, Open Source Management Solution
1735+# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
1736+#
1737+# This program is free software: you can redistribute it and/or modify
1738+# it under the terms of the GNU General Public License as published by
1739+# the Free Software Foundation, either version 3 of the License, or
1740+# (at your option) any later version.
1741+#
1742+# This program is distributed in the hope that it will be useful,
1743+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1744+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1745+# GNU General Public License for more details.
1746+#
1747+# You should have received a copy of the GNU General Public License
1748+# along with this program. If not, see <http://www.gnu.org/licenses/>
1749+#
1750+##############################################################################
1751+
1752+from openerp import SUPERUSER_ID
1753+from openerp.osv import fields, osv
1754+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
1755+from openerp.tools.safe_eval import safe_eval
1756+from openerp.tools.translate import _
1757+
1758+from datetime import date, datetime, timedelta
1759+
1760+import logging
1761+
1762+_logger = logging.getLogger(__name__)
1763+
1764+
1765+class gamification_goal_type(osv.Model):
1766+ """Goal type definition
1767+
1768+ A goal type defining a way to set an objective and evaluate it
1769+ Each module wanting to be able to set goals to the users needs to create
1770+ a new gamification_goal_type
1771+ """
1772+ _name = 'gamification.goal.type'
1773+ _description = 'Gamification goal type'
1774+ _order = 'sequence'
1775+
1776+ def _get_suffix(self, cr, uid, ids, field_name, arg, context=None):
1777+ res = dict.fromkeys(ids, '')
1778+ for goal in self.browse(cr, uid, ids, context=context):
1779+ if goal.suffix and not goal.monetary:
1780+ res[goal.id] = goal.suffix
1781+ elif goal.monetary:
1782+ # use the current user's company currency
1783+ user = self.pool.get('res.users').browse(cr, uid, uid, context)
1784+ if goal.suffix:
1785+ res[goal.id] = "%s %s" % (user.company_id.currency_id.symbol, goal.suffix)
1786+ else:
1787+ res[goal.id] = user.company_id.currency_id.symbol
1788+ else:
1789+ res[goal.id] = ""
1790+ return res
1791+
1792+ _columns = {
1793+ 'name': fields.char('Goal Type', required=True, translate=True),
1794+ 'description': fields.text('Goal Description'),
1795+ 'monetary': fields.boolean('Monetary Value', help="The target and current value are defined in the company currency."),
1796+ 'suffix': fields.char('Suffix', help="The unit of the target and current values", translate=True),
1797+ 'full_suffix': fields.function(_get_suffix, type="char", string="Full Suffix", help="The currency and suffix field"),
1798+ 'computation_mode': fields.selection([
1799+ ('manually', 'Recorded manually'),
1800+ ('count', 'Automatic: number of records'),
1801+ ('sum', 'Automatic: sum on a field'),
1802+ ('python', 'Automatic: execute a specific Python code'),
1803+ ],
1804+ string="Computation Mode",
1805+ help="Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'.",
1806+ required=True),
1807+ 'display_mode': fields.selection([
1808+ ('progress', 'Progressive (using numerical values)'),
1809+ ('checkbox', 'Checkbox (done or not-done)'),
1810+ ],
1811+ string="Displayed as", required=True),
1812+ 'model_id': fields.many2one('ir.model',
1813+ string='Model',
1814+ help='The model object for the field to evaluate'),
1815+ 'field_id': fields.many2one('ir.model.fields',
1816+ string='Field to Sum',
1817+ help='The field containing the value to evaluate'),
1818+ 'field_date_id': fields.many2one('ir.model.fields',
1819+ string='Date Field',
1820+ help='The date to use for the time period evaluated'),
1821+ #TODO: actually it would be better to use 'user.id' in the domain definition, because it means user is a browse record and it's more flexible (i can do '[(country_id,=,user.partner_id.country_id.id)])
1822+
1823+ 'domain': fields.char("Filter Domain",
1824+ help="Technical filters rules to apply. Use 'user.id' (without marks) to limit the search to the evaluated user.",
1825+ required=True),
1826+ 'compute_code': fields.char('Compute Code',
1827+ help="The name of the python method that will be executed to compute the current value. See the file gamification/goal_type_data.py for examples."),
1828+ 'condition': fields.selection([
1829+ ('higher', 'The higher the better'),
1830+ ('lower', 'The lower the better')
1831+ ],
1832+ string='Goal Performance',
1833+ help='A goal is considered as completed when the current value is compared to the value to reach',
1834+ required=True),
1835+ 'sequence': fields.integer('Sequence', help='Sequence number for ordering', required=True),
1836+ 'action_id': fields.many2one('ir.actions.act_window', string="Action",
1837+ help="The action that will be called to update the goal value."),
1838+ 'res_id_field': fields.char("ID Field of user",
1839+ help="The field name on the user profile (res.users) containing the value for res_id for action.")
1840+ }
1841+
1842+ _defaults = {
1843+ 'sequence': 1,
1844+ 'condition': 'higher',
1845+ 'computation_mode': 'manually',
1846+ 'domain': "[]",
1847+ 'monetary': False,
1848+ 'display_mode': 'progress',
1849+ }
1850+
1851+
1852+class gamification_goal(osv.Model):
1853+ """Goal instance for a user
1854+
1855+ An individual goal for a user on a specified time period"""
1856+
1857+ _name = 'gamification.goal'
1858+ _description = 'Gamification goal instance'
1859+ _inherit = 'mail.thread'
1860+
1861+ def _get_completeness(self, cr, uid, ids, field_name, arg, context=None):
1862+ """Return the percentage of completeness of the goal, between 0 and 100"""
1863+ res = dict.fromkeys(ids, 0.0)
1864+ for goal in self.browse(cr, uid, ids, context=context):
1865+ if goal.type_condition == 'higher' and goal.current > 0:
1866+ res[goal.id] = min(100, round(100.0 * goal.current / goal.target_goal, 2))
1867+ elif goal.current < goal.target_goal:
1868+ # a goal 'lower than' has only two values possible: 0 or 100%
1869+ res[goal.id] = 100.0
1870+ return res
1871+
1872+ def on_change_type_id(self, cr, uid, ids, type_id=False, context=None):
1873+ goal_type = self.pool.get('gamification.goal.type')
1874+ if not type_id:
1875+ return {'value': {'type_id': False}}
1876+ goal_type = goal_type.browse(cr, uid, type_id, context=context)
1877+ return {'value': {'computation_mode': goal_type.computation_mode, 'type_condition': goal_type.condition}}
1878+
1879+ _columns = {
1880+ 'type_id': fields.many2one('gamification.goal.type', string='Goal Type', required=True, ondelete="cascade"),
1881+ 'user_id': fields.many2one('res.users', string='User', required=True),
1882+ 'planline_id': fields.many2one('gamification.goal.planline', string='Goal Planline', ondelete="cascade"),
1883+ 'plan_id': fields.related('planline_id', 'plan_id',
1884+ string="Plan",
1885+ type='many2one',
1886+ relation='gamification.goal.plan',
1887+ store=True),
1888+ 'start_date': fields.date('Start Date'),
1889+ 'end_date': fields.date('End Date'), # no start and end = always active
1890+ 'target_goal': fields.float('To Reach',
1891+ required=True,
1892+ track_visibility='always'), # no goal = global index
1893+ 'current': fields.float('Current Value', required=True, track_visibility='always'),
1894+ 'completeness': fields.function(_get_completeness, type='float', string='Completeness'),
1895+ 'state': fields.selection([
1896+ ('draft', 'Draft'),
1897+ ('inprogress', 'In progress'),
1898+ ('inprogress_update', 'In progress (to update)'),
1899+ ('reached', 'Reached'),
1900+ ('failed', 'Failed'),
1901+ ('canceled', 'Canceled'),
1902+ ],
1903+ string='State',
1904+ required=True,
1905+ track_visibility='always'),
1906+
1907+ 'computation_mode': fields.related('type_id', 'computation_mode', type='char', string="Type computation mode"),
1908+ 'remind_update_delay': fields.integer('Remind delay',
1909+ help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified."),
1910+ 'last_update': fields.date('Last Update',
1911+ help="In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in goal plan). Ignored in case of non-manual goal or goal not linked to a plan."),
1912+
1913+ 'type_description': fields.related('type_id', 'description', type='char', string='Type Description', readonly=True),
1914+ 'type_suffix': fields.related('type_id', 'suffix', type='char', string='Type Description', readonly=True),
1915+ 'type_condition': fields.related('type_id', 'condition', type='char', string='Type Condition', readonly=True),
1916+ 'type_suffix': fields.related('type_id', 'full_suffix', type="char", string="Suffix", readonly=True),
1917+ 'type_display': fields.related('type_id', 'display_mode', type="char", string="Display Mode", readonly=True),
1918+ }
1919+
1920+ _defaults = {
1921+ 'current': 0,
1922+ 'state': 'draft',
1923+ 'start_date': fields.date.today,
1924+ }
1925+ _order = 'create_date desc, end_date desc, type_id, id'
1926+
1927+ def _check_remind_delay(self, goal, context=None):
1928+ """Verify if a goal has not been updated for some time and send a
1929+ reminder message of needed.
1930+
1931+ :return: data to write on the goal object
1932+ """
1933+ if goal.remind_update_delay and goal.last_update:
1934+ delta_max = timedelta(days=goal.remind_update_delay)
1935+ last_update = datetime.strptime(goal.last_update, DF).date()
1936+ if date.today() - last_update > delta_max and goal.state == 'inprogress':
1937+ # generate a remind report
1938+ temp_obj = self.pool.get('email.template')
1939+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_reminder', context)
1940+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal', goal.id, context=context)
1941+
1942+ self.message_post(cr, uid, goal.id, body=body_html, partner_ids=[goal.user_id.partner_id.id], context=context, subtype='mail.mt_comment')
1943+ return {'state': 'inprogress_update'}
1944+ return {}
1945+
1946+ def update(self, cr, uid, ids, context=None):
1947+ """Update the goals to recomputes values and change of states
1948+
1949+ If a manual goal is not updated for enough time, the user will be
1950+ reminded to do so (done only once, in 'inprogress' state).
1951+ If a goal reaches the target value, the status is set to reached
1952+ If the end date is passed (at least +1 day, time not considered) without
1953+ the target value being reached, the goal is set as failed."""
1954+
1955+ for goal in self.browse(cr, uid, ids, context=context):
1956+ #TODO: towrite may be falsy, to avoid useless write on the object. Please check the whole thing is still working
1957+ towrite = {}
1958+ if goal.state in ('draft', 'canceled'):
1959+ # skip if goal draft or canceled
1960+ continue
1961+
1962+ if goal.type_id.computation_mode == 'manually':
1963+ towrite.update(self._check_remind_delay(goal, context))
1964+
1965+ elif goal.type_id.computation_mode == 'python':
1966+ # execute the chosen method
1967+ values = {'cr': cr, 'uid': goal.user_id.id, 'context': context, 'self': self.pool.get('gamification.goal.type')}
1968+ result = safe_eval(goal.type_id.compute_code, values, {})
1969+
1970+ if type(result) in (float, int, long) and result != goal.current:
1971+ towrite['current'] = result
1972+ else:
1973+ _logger.exception(_('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code)))
1974+ # raise osv.except_osv(_('Error!'), _('Unvalid return content from the evaluation of %s' % str(goal.type_id.compute_code)))
1975+
1976+ else: # count or sum
1977+ obj = self.pool.get(goal.type_id.model_id.model)
1978+ field_date_name = goal.type_id.field_date_id.name
1979+
1980+ # eval the domain with user_id replaced by goal user
1981+ domain = safe_eval(goal.type_id.domain, {'user': goal.user_id})
1982+
1983+ #add temporal clause(s) to the domain if fields are filled on the goal
1984+ if goal.start_date and field_date_name:
1985+ domain.append((field_date_name, '>=', goal.start_date))
1986+ if goal.end_date and field_date_name:
1987+ domain.append((field_date_name, '<=', goal.end_date))
1988+
1989+ if goal.type_id.computation_mode == 'sum':
1990+ field_name = goal.type_id.field_id.name
1991+ res = obj.read_group(cr, uid, domain, [field_name], [''], context=context)
1992+ new_value = res and res[0][field_name] or 0.0
1993+
1994+ else: # computation mode = count
1995+ new_value = obj.search(cr, uid, domain, context=context, count=True)
1996+
1997+ #avoid useless write if the new value is the same as the old one
1998+ if new_value != goal.current:
1999+ towrite['current'] = new_value
2000+
2001+ # check goal target reached
2002+ #TODO: reached condition is wrong because it should check time constraints.
2003+ if (goal.type_id.condition == 'higher' and towrite.get('current', goal.current) >= goal.target_goal) or (goal.type_id.condition == 'lower' and towrite.get('current', goal.current) <= goal.target_goal):
2004+ towrite['state'] = 'reached'
2005+
2006+ # check goal failure
2007+ elif goal.end_date and fields.date.today() > goal.end_date:
2008+ towrite['state'] = 'failed'
2009+ if towrite:
2010+ self.write(cr, uid, [goal.id], towrite, context=context)
2011+ return True
2012+
2013+ def action_start(self, cr, uid, ids, context=None):
2014+ """Mark a goal as started.
2015+
2016+ This should only be used when creating goals manually (in draft state)"""
2017+ self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
2018+ return self.update(cr, uid, ids, context=context)
2019+
2020+ def action_reach(self, cr, uid, ids, context=None):
2021+ """Mark a goal as reached.
2022+
2023+ If the target goal condition is not met, the state will be reset to In
2024+ Progress at the next goal update until the end date."""
2025+ return self.write(cr, uid, ids, {'state': 'reached'}, context=context)
2026+
2027+ def action_fail(self, cr, uid, ids, context=None):
2028+ """Set the state of the goal to failed.
2029+
2030+ A failed goal will be ignored in future checks."""
2031+ return self.write(cr, uid, ids, {'state': 'failed'}, context=context)
2032+
2033+ def action_cancel(self, cr, uid, ids, context=None):
2034+ """Reset the completion after setting a goal as reached or failed.
2035+
2036+ This is only the current state, if the date and/or target criterias
2037+ match the conditions for a change of state, this will be applied at the
2038+ next goal update."""
2039+ return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
2040+
2041+ def create(self, cr, uid, vals, context=None):
2042+ """Overwrite the create method to add a 'no_remind_goal' field to True"""
2043+ context = context or {}
2044+ context['no_remind_goal'] = True
2045+ return super(gamification_goal, self).create(cr, uid, vals, context=context)
2046+
2047+ def write(self, cr, uid, ids, vals, context=None):
2048+ """Overwrite the write method to update the last_update field to today
2049+
2050+ If the current value is changed and the report frequency is set to On
2051+ change, a report is generated
2052+ """
2053+ vals['last_update'] = fields.date.today()
2054+ result = super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
2055+ for goal in self.browse(cr, uid, ids, context=context):
2056+ if goal.state != "draft" and ('type_id' in vals or 'user_id' in vals):
2057+ # avoid drag&drop in kanban view
2058+ raise osv.except_osv(_('Error!'), _('Can not modify the configuration of a started goal'))
2059+
2060+ if vals.get('current'):
2061+ if 'no_remind_goal' in context:
2062+ # new goals should not be reported
2063+ continue
2064+
2065+ if goal.plan_id and goal.plan_id.report_message_frequency == 'onchange':
2066+ self.pool.get('gamification.goal.plan').report_progress(cr, SUPERUSER_ID, goal.plan_id, users=[goal.user_id], context=context)
2067+ return result
2068+
2069+ def get_action(self, cr, uid, goal_id, context=None):
2070+ """Get the ir.action related to update the goal
2071+
2072+ In case of a manual goal, should return a wizard to update the value
2073+ :return: action description in a dictionnary
2074+ """
2075+ goal = self.browse(cr, uid, goal_id, context=context)
2076+ if goal.type_id.action_id:
2077+ #open a the action linked on the goal
2078+ action = goal.type_id.action_id.read()[0]
2079+
2080+ if goal.type_id.res_id_field:
2081+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
2082+ # this loop manages the cases where res_id_field is a browse record path (eg : company_id.currency_id.id)
2083+ field_names = goal.type_id.res_id_field.split('.')
2084+ res = current_user
2085+ for field_name in field_names[:]:
2086+ res = res.__getitem__(field_name)
2087+ action['res_id'] = res
2088+
2089+ # if one element to display, should see it in form mode if possible
2090+ views = action['views']
2091+ for (view_id, mode) in action['views']:
2092+ if mode == "form":
2093+ views = [(view_id, mode)]
2094+ break
2095+ action['views'] = views
2096+ return action
2097+
2098+ if goal.computation_mode == 'manually':
2099+ #open a wizard window to update the value manually
2100+ action = {
2101+ 'name': _("Update %s") % goal.type_id.name,
2102+ 'id': goal_id,
2103+ 'type': 'ir.actions.act_window',
2104+ 'views': [[False, 'form']],
2105+ 'target': 'new',
2106+ }
2107+ action['context'] = {'default_goal_id': goal_id, 'default_current': goal.current}
2108+ action['res_model'] = 'gamification.goal.wizard'
2109+ return action
2110+ return False
2111+
2112+
2113+class goal_manual_wizard(osv.TransientModel):
2114+ """Wizard type to update a manual goal"""
2115+ _name = 'gamification.goal.wizard'
2116+ _columns = {
2117+ 'goal_id': fields.many2one("gamification.goal", string='Goal', required=True),
2118+ 'current': fields.float('Current'),
2119+ }
2120+
2121+ def action_update_current(self, cr, uid, ids, context=None):
2122+ """Wizard action for updating the current value"""
2123+
2124+ goal_obj = self.pool.get('gamification.goal')
2125+
2126+ for wiz in self.browse(cr, uid, ids, context=context):
2127+ towrite = {
2128+ 'current': wiz.current,
2129+ 'goal_id': wiz.goal_id.id,
2130+ }
2131+ goal_obj.write(cr, uid, [wiz.goal_id.id], towrite, context=context)
2132+ goal_obj.update(cr, uid, [wiz.goal_id.id], context=context)
2133+ return {}
2134+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
2135
2136=== added file 'gamification/goal_base_data.xml'
2137--- gamification/goal_base_data.xml 1970-01-01 00:00:00 +0000
2138+++ gamification/goal_base_data.xml 2013-06-28 14:51:48 +0000
2139@@ -0,0 +1,232 @@
2140+<?xml version="1.0"?>
2141+<openerp>
2142+ <data>
2143+
2144+ <!-- goal types -->
2145+ <record model="gamification.goal.type" id="type_base_timezone">
2146+ <field name="name">Set your Timezone</field>
2147+ <field name="description">Configure your profile and specify your timezone</field>
2148+ <field name="computation_mode">count</field>
2149+ <field name="display_mode">checkbox</field>
2150+ <field name="model_id" eval="ref('base.model_res_users')" />
2151+ <field name="domain">[('id','=',user.id),('partner_id.tz', '!=', False)]</field>
2152+ <field name="action_id" eval="ref('base.action_res_users_my')" />
2153+ <field name="res_id_field">id</field>
2154+ </record>
2155+
2156+ <record model="gamification.goal.type" id="type_base_avatar">
2157+ <field name="name">Set your Avatar</field>
2158+ <field name="description">In your user preference</field>
2159+ <field name="computation_mode">manually</field>
2160+ <field name="display_mode">checkbox</field>
2161+ <!-- problem : default avatar != False -> manually + check in write function -->
2162+ <field name="action_id" eval="ref('base.action_res_users_my')" />
2163+ <field name="res_id_field">id</field>
2164+ </record>
2165+
2166+
2167+ <record model="gamification.goal.type" id="type_base_company_data">
2168+ <field name="name">Set your Company Data</field>
2169+ <field name="description">Write some information about your company (specify at least a name)</field>
2170+ <field name="computation_mode">count</field>
2171+ <field name="display_mode">checkbox</field>
2172+ <field name="model_id" eval="ref('base.model_res_company')" />
2173+ <field name="domain">[('user_ids', 'in', user.id), ('name', '!=', 'Your Company')]</field>
2174+ <field name="action_id" eval="ref('base.action_res_company_form')" />
2175+ <field name="res_id_field">company_id.id</field>
2176+ </record>
2177+
2178+ <record model="gamification.goal.type" id="type_base_company_logo">
2179+ <field name="name">Set your Company Logo</field>
2180+ <field name="computation_mode">count</field>
2181+ <field name="display_mode">checkbox</field>
2182+ <field name="model_id" eval="ref('base.model_res_company')" />
2183+ <field name="domain">[('user_ids', 'in', user.id),('logo', '!=', False)]</field>
2184+ <field name="action_id" eval="ref('base.action_res_company_form')" />
2185+ <field name="res_id_field">company_id.id</field>
2186+ </record>
2187+
2188+ <record id="action_new_simplified_res_users" model="ir.actions.act_window">
2189+ <field name="name">Create User</field>
2190+ <field name="type">ir.actions.act_window</field>
2191+ <field name="res_model">res.users</field>
2192+ <field name="view_type">form</field>
2193+ <field name="target">current</field>
2194+ <field name="view_id" ref="base.view_users_simple_form"/>
2195+ <field name="context">{'default_groups_ref': ['base.group_user']}</field>
2196+ <field name="help">Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.</field>
2197+ </record>
2198+
2199+ <record model="gamification.goal.type" id="type_base_invite">
2200+ <field name="name">Invite new Users</field>
2201+ <field name="description">Create at least another user</field>
2202+ <field name="display_mode">checkbox</field>
2203+ <field name="computation_mode">count</field>
2204+ <field name="model_id" eval="ref('base.model_res_users')" />
2205+ <field name="domain">[('id', '!=', user.id)]</field>
2206+ <field name="action_id" eval="ref('action_new_simplified_res_users')" />
2207+ </record>
2208+
2209+ <record model="gamification.goal.type" id="type_nbr_following">
2210+ <field name="name">Mail Group Following</field>
2211+ <field name="description">Follow mail groups to receive news</field>
2212+ <field name="computation_mode">python</field>
2213+ <field name="compute_code">self.number_following(cr, uid, 'mail.group')</field>
2214+ <field name="action_id" eval="ref('mail.action_view_groups')" />
2215+ </record>
2216+
2217+
2218+ <!-- plans -->
2219+ <record model="gamification.goal.plan" id="plan_base_discover">
2220+ <field name="name">Complete your Profile</field>
2221+ <field name="period">once</field>
2222+ <field name="visibility_mode">progressbar</field>
2223+ <field name="report_message_frequency">never</field>
2224+ <field name="autojoin_group_id" eval="ref('base.group_user')" />
2225+ <field name="state">inprogress</field>
2226+ <field name="category">other</field>
2227+ </record>
2228+
2229+ <record model="gamification.goal.plan" id="plan_base_configure">
2230+ <field name="name">Setup your Company</field>
2231+ <field name="period">once</field>
2232+ <field name="visibility_mode">progressbar</field>
2233+ <field name="report_message_frequency">never</field>
2234+ <field name="user_ids" eval="[(4, ref('base.user_root'))]" />
2235+ <field name="state">inprogress</field>
2236+ <field name="category">other</field>
2237+ </record>
2238+
2239+ <!-- planlines -->
2240+ <record model="gamification.goal.planline" id="planline_base_discover1">
2241+ <field name="type_id" eval="ref('type_base_timezone')" />
2242+ <field name="target_goal">1</field>
2243+ <field name="plan_id" eval="ref('plan_base_discover')" />
2244+ </record>
2245+ <record model="gamification.goal.planline" id="planline_base_discover2">
2246+ <field name="type_id" eval="ref('type_base_avatar')" />
2247+ <field name="target_goal">1</field>
2248+ <field name="plan_id" eval="ref('plan_base_discover')" />
2249+ </record>
2250+
2251+ <record model="gamification.goal.planline" id="planline_base_admin2">
2252+ <field name="type_id" eval="ref('type_base_company_logo')" />
2253+ <field name="target_goal">1</field>
2254+ <field name="plan_id" eval="ref('plan_base_configure')" />
2255+ </record>
2256+ <record model="gamification.goal.planline" id="planline_base_admin1">
2257+ <field name="type_id" eval="ref('type_base_company_data')" />
2258+ <field name="target_goal">1</field>
2259+ <field name="plan_id" eval="ref('plan_base_configure')" />
2260+ </record>
2261+ <record model="gamification.goal.planline" id="planline_base_admin3">
2262+ <field name="type_id" eval="ref('type_base_invite')" />
2263+ <field name="target_goal">1</field>
2264+ <field name="plan_id" eval="ref('plan_base_configure')" />
2265+ </record>
2266+ </data>
2267+
2268+ <!-- Mail template is done in a NOUPDATE block
2269+ so users can freely customize/delete them -->
2270+ <data noupdate="0">
2271+ <!--Email template -->
2272+
2273+ <record id="email_template_goal_reminder" model="email.template">
2274+ <field name="name">Reminder for Goal Update</field>
2275+ <field name="body_html"><![CDATA[
2276+ <header>
2277+ <strong>Reminder ${object.name}</strong>
2278+ </header>
2279+
2280+ <p class="oe_grey">${object.report_header or ''}</p>
2281+
2282+ <p>You have not updated your progress for the goal ${object.type_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.</p>
2283+
2284+ <p>If you have not changed your score yet, you can use the button "The current value is up to date" to indicate so.</p>
2285+ ]]></field>
2286+ </record>
2287+
2288+ <record id="email_template_goal_progress_perso" model="email.template">
2289+ <field name="name">Personal Goal Progress</field>
2290+ <field name="body_html"><![CDATA[
2291+ <header>
2292+ <strong>${object.name}</strong>
2293+ </header>
2294+ <p class="oe_grey">${object.report_header or ''}</p>
2295+
2296+ <table width="100%" border="1">
2297+ <tr>
2298+ <th>Goal</th>
2299+ <th>Target</th>
2300+ <th>Current</th>
2301+ <th>Completeness</th>
2302+ </tr>
2303+ % for goal in ctx["goals"]:
2304+ <tr
2305+ % if goal.completeness >= 100:
2306+ style="font-weight:bold;"
2307+ % endif
2308+ >
2309+ <td>${goal.type_id.name}</td>
2310+ <td>${goal.target_goal}
2311+ % if goal.type_suffix:
2312+ ${goal.type_suffix}
2313+ % endif
2314+ </td>
2315+ <td>${goal.current}
2316+ % if goal.type_suffix:
2317+ ${goal.type_suffix}
2318+ % endif
2319+ </td>
2320+ <td>${goal.completeness} %</td>
2321+ </tr>
2322+ % endfor
2323+ </table>]]></field>
2324+ </record>
2325+
2326+ <record id="email_template_goal_progress_group" model="email.template">
2327+ <field name="name">Group Goal Progress</field>
2328+ <field name="body_html"><![CDATA[
2329+ <header>
2330+ <strong>${object.name}</strong>
2331+ </header>
2332+ <p class="oe_grey">${object.report_header or ''}</p>
2333+
2334+ % for planline in ctx['planlines_boards']:
2335+ <table width="100%" border="1">
2336+ <tr>
2337+ <th colspan="4">${planline.goal_type.name}</th>
2338+ </tr>
2339+ <tr>
2340+ <th>#</th>
2341+ <th>Person</th>
2342+ <th>Completeness</th>
2343+ <th>Current</th>
2344+ </tr>
2345+ % for idx, goal in planline.board_goals:
2346+ % if idx < 3 or goal.user_id.id == user.id:
2347+ <tr
2348+ % if goal.completeness >= 100:
2349+ style="font-weight:bold;"
2350+ % endif
2351+ >
2352+ <td>${idx+1}</td>
2353+ <td>${goal.user_id.name}</td>
2354+ <td>${goal.completeness}%</td>
2355+ <td>${goal.current}/${goal.target_goal}
2356+ % if goal.type_suffix:
2357+ ${goal.type_suffix}
2358+ % endif
2359+ </td>
2360+ </tr>
2361+ % endif
2362+ % endfor
2363+ </table>
2364+
2365+ <br/><br/>
2366+
2367+ % endfor
2368+]]></field>
2369+ </record>
2370+ </data>
2371+</openerp>
2372
2373=== added file 'gamification/goal_type_data.py'
2374--- gamification/goal_type_data.py 1970-01-01 00:00:00 +0000
2375+++ gamification/goal_type_data.py 2013-06-28 14:51:48 +0000
2376@@ -0,0 +1,41 @@
2377+# -*- coding: utf-8 -*-
2378+##############################################################################
2379+#
2380+# OpenERP, Open Source Management Solution
2381+# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
2382+#
2383+# This program is free software: you can redistribute it and/or modify
2384+# it under the terms of the GNU General Public License as published by
2385+# the Free Software Foundation, either version 3 of the License, or
2386+# (at your option) any later version.
2387+#
2388+# This program is distributed in the hope that it will be useful,
2389+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2390+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2391+# GNU General Public License for more details.
2392+#
2393+# You should have received a copy of the GNU General Public License
2394+# along with this program. If not, see <http://www.gnu.org/licenses/>
2395+#
2396+##############################################################################
2397+
2398+from openerp.osv import osv
2399+
2400+class gamification_goal_type_data(osv.Model):
2401+ """Goal type data
2402+
2403+ Methods for more complex goals not possible with the 'sum' and 'count' mode.
2404+ Each method should return the value that will be set in the 'current' field
2405+ of a user's goal. The return type must be a float or integer.
2406+ """
2407+ _inherit = 'gamification.goal.type'
2408+
2409+#TODO: is it usefull to have this method in a standalone file? why not directly in the computation field of the related goal type?
2410+ def number_following(self, cr, uid, xml_id="mail.thread", context=None):
2411+ """Return the number of 'xml_id' objects the user is following
2412+
2413+ The model specified in 'xml_id' must inherit from mail.thread
2414+ """
2415+ ref_obj = self.pool.get(xml_id)
2416+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
2417+ return ref_obj.search(cr, uid, [('message_follower_ids', '=', user.partner_id.id)], count=True, context=context)
2418
2419=== added file 'gamification/goal_view.xml'
2420--- gamification/goal_view.xml 1970-01-01 00:00:00 +0000
2421+++ gamification/goal_view.xml 2013-06-28 14:51:48 +0000
2422@@ -0,0 +1,310 @@
2423+<?xml version="1.0" encoding="UTF-8"?>
2424+<openerp>
2425+ <data>
2426+
2427+ <!-- Goal views -->
2428+ <record id="goal_list_action" model="ir.actions.act_window">
2429+ <field name="name">Goals</field>
2430+ <field name="res_model">gamification.goal</field>
2431+ <field name="view_mode">tree,form,kanban</field>
2432+ <field name="context">{'search_default_group_by_user': True, 'search_default_group_by_type': True}</field>
2433+ <field name="help" type="html">
2434+ <p class="oe_view_nocontent_create">
2435+ Click to create a goal.
2436+ </p>
2437+ <p>
2438+ A goal is defined by a user and a goal type.
2439+ Goals can be created automatically by using goal plans.
2440+ </p>
2441+ </field>
2442+ </record>
2443+
2444+ <record id="goal_list_view" model="ir.ui.view">
2445+ <field name="name">Goal List</field>
2446+ <field name="model">gamification.goal</field>
2447+ <field name="arch" type="xml">
2448+ <tree string="Goal List" colors="red:state == 'failed';green:state == 'reached';grey:state == 'canceled'">
2449+ <field name="type_id" invisible="1" />
2450+ <field name="user_id" invisible="1" />
2451+ <field name="start_date"/>
2452+ <field name="end_date"/>
2453+ <field name="current"/>
2454+ <field name="target_goal"/>
2455+ <field name="completeness" widget="progressbar"/>
2456+ <field name="state" invisible="1"/>
2457+ <field name="planline_id" invisible="1"/>
2458+ </tree>
2459+ </field>
2460+ </record>
2461+
2462+ <record id="goal_form_view" model="ir.ui.view">
2463+ <field name="name">Goal Form</field>
2464+ <field name="model">gamification.goal</field>
2465+ <field name="arch" type="xml">
2466+ <form string="Goal" version="7.0">
2467+ <header>
2468+ <button string="Start goal" type="object" name="action_start" states="draft" class="oe_highlight"/>
2469+
2470+ <button string="Goal Reached" type="object" name="action_reach" states="inprogress,inprogress_update" />
2471+ <button string="Goal Failed" type="object" name="action_fail" states="inprogress,inprogress_update"/>
2472+ <button string="Reset Completion" type="object" name="action_cancel" states="failed,reached" groups="base.group_no_one" />
2473+ <field name="state" widget="statusbar" statusbar_visible="draft,inprogress,reached" />
2474+ </header>
2475+ <sheet>
2476+ <group>
2477+ <group string="Reference">
2478+ <field name="type_id" on_change="on_change_type_id(type_id)" attrs="{'readonly':[('state','!=','draft')]}"/>
2479+ <field name="user_id" attrs="{'readonly':[('state','!=','draft')]}"/>
2480+ <field name="plan_id" attrs="{'readonly':[('state','!=','draft')]}"/>
2481+ </group>
2482+ <group string="Schedule">
2483+ <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
2484+ <field name="end_date" />
2485+ <field name="computation_mode" invisible="1"/>
2486+
2487+ <label for="remind_update_delay" attrs="{'invisible':[('computation_mode','!=', 'manually')]}"/>
2488+ <div attrs="{'invisible':[('computation_mode','!=', 'manually')]}">
2489+ <field name="remind_update_delay" class="oe_inline"/>
2490+ days
2491+ </div>
2492+ <field name="last_update" groups="base.group_no_one"/>
2493+ </group>
2494+ <group string="Data" colspan="4">
2495+ <label for="target_goal" />
2496+ <div>
2497+ <field name="target_goal" attrs="{'readonly':[('state','!=','draft')]}" class="oe_inline"/>
2498+ <field name="type_suffix" class="oe_inline"/>
2499+ </div>
2500+ <label for="current" />
2501+ <div>
2502+ <field name="current" class="oe_inline"/>
2503+ <button string="refresh" type="object" name="update" class="oe_link" attrs="{'invisible':['|',('computation_mode', '=', 'manually'),('state', '=', 'draft')]}" />
2504+ <div class="oe_grey" attrs="{'invisible':[('type_id', '=', False)]}">
2505+ Reached when current value is <strong><field name="type_condition" class="oe_inline"/></strong> than the target.
2506+ </div>
2507+ </div>
2508+ </group>
2509+ </group>
2510+ </sheet>
2511+ <div class="oe_chatter">
2512+ <field name="message_follower_ids" widget="mail_followers"/>
2513+ <field name="message_ids" widget="mail_thread"/>
2514+ </div>
2515+ </form>
2516+ </field>
2517+ </record>
2518+
2519+ <record id="goal_search_view" model="ir.ui.view">
2520+ <field name="name">Goal Search</field>
2521+ <field name="model">gamification.goal</field>
2522+ <field name="arch" type="xml">
2523+ <search string="Search Goals">
2524+ <filter name="my" string="My Goals" domain="[('user_id', '=', uid)]"/>
2525+ <separator/>
2526+ <filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
2527+ <filter name="inprogress" string="Current"
2528+ domain="[
2529+ '|',
2530+ ('state', 'in', ('inprogress', 'inprogress_update')),
2531+ ('end_date', '>=', context_today().strftime('%%Y-%%m-%%d'))
2532+ ]"/>
2533+ <filter name="closed" string="Passed" domain="[('state', 'in', ('reached', 'failed'))]"/>
2534+ <separator/>
2535+
2536+ <field name="user_id"/>
2537+ <field name="type_id"/>
2538+ <field name="plan_id"/>
2539+ <group expand="0" string="Group By...">
2540+ <filter name="group_by_user" string="User" domain="[]" context="{'group_by':'user_id'}"/>
2541+ <filter name="group_by_type" string="Goal Type" domain="[]" context="{'group_by':'type_id'}"/>
2542+ <filter string="State" domain="[]" context="{'group_by':'state'}"/>
2543+ <filter string="End Date" domain="[]" context="{'group_by':'end_date'}"/>
2544+ </group>
2545+ </search>
2546+ </field>
2547+ </record>
2548+
2549+ <record id="goal_kanban_view" model="ir.ui.view" >
2550+ <field name="name">Goal Kanban View</field>
2551+ <field name="model">gamification.goal</field>
2552+ <field name="arch" type="xml">
2553+ <kanban version="7.0" class="oe_background_grey">
2554+ <field name="type_id"/>
2555+ <field name="user_id"/>
2556+ <field name="current"/>
2557+ <field name="completeness"/>
2558+ <field name="state"/>
2559+ <field name="target_goal"/>
2560+ <field name="type_condition"/>
2561+ <field name="type_suffix"/>
2562+ <field name="type_display"/>
2563+ <field name="start_date"/>
2564+ <field name="end_date"/>
2565+ <field name="last_update"/>
2566+ <templates>
2567+ <t t-name="kanban-tooltip">
2568+ <field name="type_description"/>
2569+ </t>
2570+ <t t-name="kanban-box">
2571+ <div t-attf-class="oe_kanban_card oe_gamification_goal oe_kanban_goal #{record.end_date.raw_value &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' : ''}">
2572+ <div class="oe_kanban_content">
2573+ <p><h4 class="oe_goal_name" tooltip="kanban-tooltip"><field name="type_id" /></h4></p>
2574+ <div class="oe_kanban_left">
2575+ <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" />
2576+ </div>
2577+ <field name="user_id" />
2578+ <div class="oe_goal_state_block">
2579+ <t t-if="record.type_display.raw_value == 'checkbox'">
2580+ <div class="oe_goal_state oe_e">
2581+ <t t-if="record.state.raw_value=='reached'"><span class="oe_green" title="Goal Reached">W</span></t>
2582+ <t t-if="record.state.raw_value=='inprogress' || record.state.raw_value=='inprogress_update'"><span title="Goal in Progress">N</span></t>
2583+ <t t-if="record.state.raw_value=='failed'"><span class="oe_red" title="Goal Failed">X</span></t>
2584+ </div>
2585+ </t>
2586+ <t t-if="record.type_display.raw_value == 'progress'">
2587+ <t t-if="record.type_condition.raw_value =='higher'">
2588+ <field name="current" widget="goal" options="{'max_field': 'target_goal', 'label_field': 'type_suffix'}"/>
2589+ </t>
2590+ <t t-if="record.type_condition.raw_value != 'higher'">
2591+ <div t-attf-class="oe_goal_state #{record.current.raw_value == record.target_goal.raw_value+1 ? 'oe_orange' : record.current.raw_value &gt; record.target_goal.raw_value ? 'oe_red' : 'oe_green'}">
2592+ <t t-esc="record.current.raw_value" />
2593+ </div>
2594+ <em>Target: less than <t t-esc="record.target_goal.raw_value" /></em>
2595+ </t>
2596+ </t>
2597+
2598+ </div>
2599+ <p>
2600+ <t t-if="record.start_date.value">
2601+ From <t t-esc="record.start_date.value" />
2602+ </t>
2603+ <t t-if="record.end_date.value">
2604+ To <t t-esc="record.end_date.value" />
2605+ </t>
2606+ </p>
2607+ </div>
2608+ </div>
2609+ </t>
2610+ </templates>
2611+ </kanban>
2612+ </field>
2613+ </record>
2614+
2615+
2616+ <!-- Goal types view -->
2617+
2618+ <record id="goal_type_list_action" model="ir.actions.act_window">
2619+ <field name="name">Goal Types</field>
2620+ <field name="res_model">gamification.goal.type</field>
2621+ <field name="view_mode">tree,form</field>
2622+ <field name="help" type="html">
2623+ <p class="oe_view_nocontent_create">
2624+ Click to create a goal type.
2625+ </p>
2626+ <p>
2627+ A goal type is a technical model of goal defining a condition to reach.
2628+ The dates, values to reach or users are defined in goal instance.
2629+ </p>
2630+ </field>
2631+ </record>
2632+
2633+ <record id="goal_type_list_view" model="ir.ui.view">
2634+ <field name="name">Goal Types List</field>
2635+ <field name="model">gamification.goal.type</field>
2636+ <field name="arch" type="xml">
2637+ <tree string="Goal types">
2638+ <field name="sequence" widget="handle"/>
2639+ <field name="name"/>
2640+ <field name="computation_mode"/>
2641+ </tree>
2642+ </field>
2643+ </record>
2644+
2645+
2646+ <record id="goal_type_form_view" model="ir.ui.view">
2647+ <field name="name">Goal Types Form</field>
2648+ <field name="model">gamification.goal.type</field>
2649+ <field name="arch" type="xml">
2650+ <form string="Goal types" version="7.0">
2651+ <sheet>
2652+ <label for="name" class="oe_edit_only"/>
2653+ <h1>
2654+ <field name="name" class="oe_inline"/>
2655+ </h1>
2656+ <label for="description" class="oe_edit_only"/>
2657+ <div>
2658+ <field name="description" class="oe_inline"/>
2659+ </div>
2660+
2661+ <group string="How to compute the goal?">
2662+
2663+ <field widget="radio" name="computation_mode"/>
2664+
2665+ <!-- Hide the fields below if manually -->
2666+ <field name="model_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
2667+ <field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" class="oe_inline"/>
2668+ <field name="field_date_id" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]" class="oe_inline"/>
2669+ <field name="domain" attrs="{'invisible':[('computation_mode','not in',('sum', 'count'))], 'required':[('computation_mode','in',('sum', 'count'))]}" class="oe_inline"/>
2670+ <field name="compute_code" attrs="{'invisible':[('computation_mode','!=','python')], 'required':[('computation_mode','=','python')]}" placeholder="e.g. self.my_method(cr, uid)"/>
2671+ <field name="condition" widget="radio"/>
2672+ </group>
2673+ <group string="Formating Options">
2674+ <field name="display_mode" widget="radio" />
2675+ <field name="suffix" placeholder="e.g. days"/>
2676+ <field name="monetary"/>
2677+ </group>
2678+ <group string="Clickable Goals">
2679+ <field name="action_id" class="oe_inline"/>
2680+ <field name="res_id_field" attrs="{'invisible': [('action_id', '=', False)]}" class="oe_inline"/>
2681+ </group>
2682+
2683+ </sheet>
2684+ </form>
2685+ </field>
2686+ </record>
2687+
2688+ <record id="goal_type_search_view" model="ir.ui.view">
2689+ <field name="name">Goal Type Search</field>
2690+ <field name="model">gamification.goal.type</field>
2691+ <field name="arch" type="xml">
2692+ <search string="Search Goal Types">
2693+ <field name="name"/>
2694+ <field name="model_id"/>
2695+ <field name="field_id"/>
2696+ <group expand="0" string="Group By...">
2697+ <filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
2698+ <filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
2699+ </group>
2700+ </search>
2701+ </field>
2702+ </record>
2703+
2704+
2705+ <record id="view_goal_wizard_update_current" model="ir.ui.view">
2706+ <field name="name">Update the current value of the Goal</field>
2707+ <field name="model">gamification.goal.wizard</field>
2708+ <field name="arch" type="xml">
2709+ <form string="Grant Badge To" version="7.0">
2710+ Set the current value you have reached for this goal
2711+ <group>
2712+ <field name="goal_id" invisible="1"/>
2713+ <field name="current" />
2714+ </group>
2715+ <footer>
2716+ <button string="Update" type="object" name="action_update_current" class="oe_highlight" /> or
2717+ <button string="Cancel" special="cancel" class="oe_link"/>
2718+ </footer>
2719+ </form>
2720+ </field>
2721+ </record>
2722+
2723+
2724+ <!-- menus in settings - technical feature required -->
2725+ <menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration" groups="base.group_no_one" />
2726+ <menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
2727+ <menuitem id="gamification_plan_menu" parent="gamification_menu" action="goal_plan_list_action" sequence="10"/>
2728+ <menuitem id="gamification_type_menu" parent="gamification_menu" action="goal_type_list_action" sequence="20"/>
2729+ <menuitem id="gamification_badge_menu" parent="gamification_menu" action="badge_list_action" sequence="30"/>
2730+
2731+ </data>
2732+</openerp>
2733
2734=== added directory 'gamification/html'
2735=== added file 'gamification/html/index.html'
2736--- gamification/html/index.html 1970-01-01 00:00:00 +0000
2737+++ gamification/html/index.html 2013-06-28 14:51:48 +0000
2738@@ -0,0 +1,86 @@
2739+<section class="oe_container">
2740+ <div class="oe_row oe_spaced">
2741+ <div class="oe_span12">
2742+ <h2 class="oe_slogan">Drive Engagement with Gamification</h2>
2743+ <h3 class="oe_slogan">Leverage natural desire for competition</h3>
2744+ <p class="oe_mt32">
2745+ Reinforce good habits and improve win rates with real-time recognition and rewards inspired by <a href="http://en.wikipedia.org/wiki/Gamification">game mechanics</a>. Align teams around clear business objectives with challenges, personal objectives and team leader boards.
2746+ </p>
2747+ <div class="oe_span4 oe_centered">
2748+ <h3>Leaderboards</h3>
2749+ <div class="oe_row_img oe_centered">
2750+ <img class="oe_picture" src="crm_game_01.png">
2751+ </div>
2752+ <p>
2753+ Promote leaders and competition amongst sales team with performance ratios.
2754+ </p>
2755+ </div>
2756+ <div class="oe_span4 oe_centered">
2757+ <h3>Personnal Objectives</h3>
2758+ <div class="oe_row_img">
2759+ <img class="oe_picture" src="crm_game_02.png">
2760+ </div>
2761+ <p>
2762+ Assign clear goals to users to align them with the company objectives.
2763+ </p>
2764+ </div>
2765+ <div class="oe_span4 oe_centered">
2766+ <h3>Visual Information</h3>
2767+ <div class="oe_row_img oe_centered">
2768+ <img class="oe_picture" src="crm_game_03.png">
2769+ </div>
2770+ <p>
2771+ See in an glance the progress of each user.
2772+ </p>
2773+ </div>
2774+ </div>
2775+</section>
2776+
2777+
2778+<section class="oe_container oe_dark">
2779+ <div class="oe_row oe_spaced">
2780+ <h2 class="oe_slogan">Create custom Challenges</h2>
2781+ <div class="oe_span6">
2782+ <p class="oe_mt32">
2783+Use predefined goals to generate easily your own challenges. Assign it to a team or individual users. Receive feedback as often as needed: daily, weekly... Repeat it automatically to compare progresses through time.
2784+ </p>
2785+ </div>
2786+ <div class="oe_span6">
2787+ <div class="oe_row_img oe_centered">
2788+ <img class="oe_picture oe_screenshot" src="crm_sc_05.png">
2789+ </div>
2790+ </div>
2791+ </div>
2792+</section>
2793+
2794+<section class="oe_container">
2795+ <div class="oe_row oe_spaced">
2796+ <h2 class="oe_slogan">Motivate with Badges</h2>
2797+ <div class="oe_span6">
2798+ <div class="oe_row_img oe_centered">
2799+ <img class="oe_picture" src="crm_linkedin.png">
2800+ </div>
2801+ </div>
2802+ <div class="oe_span6">
2803+ <p class="oe_mt32">
2804+Inspire achievement with recognition of coworker's good work by rewarding badges. These can be deserved manually or upon completion of challenges. Add fun to the competition with rare badges.
2805+ </p>
2806+ </div>
2807+ </div>
2808+</section>
2809+
2810+<section class="oe_container oe_dark">
2811+ <div class="oe_row oe_spaced">
2812+ <h2 class="oe_slogan">Adapt to any module</h2>
2813+ <div class="oe_span6">
2814+ <p class="oe_mt32">
2815+Create goals linked to any module. The evaluation system is very flexible and can be used for many different tasks : sales evaluation, creation of events, project completion or even helping new users to complete their profile.
2816+ </p>
2817+ </div>
2818+ <div class="oe_span6">
2819+ <div class="oe_row_img oe_centered">
2820+ <img class="oe_picture oe_screenshot" src="crm_sc_02.png">
2821+ </div>
2822+ </div>
2823+ </div>
2824+</section>
2825
2826=== added file 'gamification/plan.py'
2827--- gamification/plan.py 1970-01-01 00:00:00 +0000
2828+++ gamification/plan.py 2013-06-28 14:51:48 +0000
2829@@ -0,0 +1,804 @@
2830+# -*- coding: utf-8 -*-
2831+##############################################################################
2832+#
2833+# OpenERP, Open Source Management Solution
2834+# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
2835+#
2836+# This program is free software: you can redistribute it and/or modify
2837+# it under the terms of the GNU General Public License as published by
2838+# the Free Software Foundation, either version 3 of the License, or
2839+# (at your option) any later version.
2840+#
2841+# This program is distributed in the hope that it will be useful,
2842+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2843+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2844+# GNU General Public License for more details.
2845+#
2846+# You should have received a copy of the GNU General Public License
2847+# along with this program. If not, see <http://www.gnu.org/licenses/>
2848+#
2849+##############################################################################
2850+
2851+from openerp.osv import fields, osv
2852+from openerp.tools.translate import _
2853+
2854+# from templates import TemplateHelper
2855+
2856+from datetime import date, datetime, timedelta
2857+import calendar
2858+import logging
2859+_logger = logging.getLogger(__name__)
2860+
2861+
2862+def start_end_date_for_period(period, default_start_date=False, default_end_date=False):
2863+ """Return the start and end date for a goal period based on today
2864+
2865+ :return: (start_date, end_date), datetime.date objects, False if the period is
2866+ not defined or unknown"""
2867+ today = date.today()
2868+ if period == 'daily':
2869+ start_date = today
2870+ end_date = start_date
2871+ elif period == 'weekly':
2872+ delta = timedelta(days=today.weekday())
2873+ start_date = today - delta
2874+ end_date = start_date + timedelta(days=7)
2875+ elif period == 'monthly':
2876+ month_range = calendar.monthrange(today.year, today.month)
2877+ start_date = today.replace(day=1)
2878+ end_date = today.replace(day=month_range[1])
2879+ elif period == 'yearly':
2880+ start_date = today.replace(month=1, day=1)
2881+ end_date = today.replace(month=12, day=31)
2882+ else: # period == 'once':
2883+ start_date = default_start_date # for manual goal, start each time
2884+ end_date = default_end_date
2885+
2886+ if start_date and end_date:
2887+ return (start_date.isoformat(), end_date.isoformat())
2888+ else:
2889+ return (start_date, end_date)
2890+
2891+
2892+class gamification_goal_plan(osv.Model):
2893+ """Gamification goal plan
2894+
2895+ Set of predifined goals to be able to automate goal settings or
2896+ quickly apply several goals manually to a group of users
2897+
2898+ If 'user_ids' is defined and 'period' is different than 'one', the set will
2899+ be assigned to the users for each period (eg: every 1st of each month if
2900+ 'monthly' is selected)
2901+ """
2902+
2903+ _name = 'gamification.goal.plan'
2904+ _description = 'Gamification goal plan'
2905+ _inherit = 'mail.thread'
2906+
2907+ def _get_next_report_date(self, cr, uid, ids, field_name, arg, context=None):
2908+ """Return the next report date based on the last report date and report
2909+ period.
2910+
2911+ :return: a string in isoformat representing the date"""
2912+ res = {}
2913+ for plan in self.browse(cr, uid, ids, context):
2914+ last = datetime.strptime(plan.last_report_date, '%Y-%m-%d').date()
2915+ if plan.report_message_frequency == 'daily':
2916+ next = last + timedelta(days=1)
2917+ res[plan.id] = next.isoformat()
2918+ elif plan.report_message_frequency == 'weekly':
2919+ next = last + timedelta(days=7)
2920+ res[plan.id] = next.isoformat()
2921+ elif plan.report_message_frequency == 'monthly':
2922+ month_range = calendar.monthrange(last.year, last.month)
2923+ next = last.replace(day=month_range[1]) + timedelta(days=1)
2924+ res[plan.id] = next.isoformat()
2925+ elif plan.report_message_frequency == 'yearly':
2926+ res[plan.id] = last.replace(year=last.year + 1).isoformat()
2927+ # frequency == 'once', reported when closed only
2928+ else:
2929+ res[plan.id] = False
2930+
2931+ return res
2932+
2933+ def _planline_count(self, cr, uid, ids, field_name, arg, context=None):
2934+ res = dict.fromkeys(ids, 0)
2935+ for plan in self.browse(cr, uid, ids, context):
2936+ res[plan.id] = len(plan.planline_ids)
2937+ return res
2938+
2939+ _columns = {
2940+ 'name': fields.char('Challenge Name', required=True, translate=True),
2941+ 'description': fields.text('Description', translate=True),
2942+ 'state': fields.selection([
2943+ ('draft', 'Draft'),
2944+ ('inprogress', 'In Progress'),
2945+ ('done', 'Done'),
2946+ ],
2947+ string='State',
2948+ required=True),
2949+ 'manager_id': fields.many2one('res.users',
2950+ string='Responsible', help="The user responsible for the challenge."),
2951+
2952+ 'user_ids': fields.many2many('res.users', 'user_ids',
2953+ string='Users',
2954+ help="List of users to which the goal will be set"),
2955+ 'autojoin_group_id': fields.many2one('res.groups',
2956+ string='Auto-subscription Group',
2957+ help='Group of users whose members will automatically be added to the users'),
2958+
2959+ 'period': fields.selection([
2960+ ('once', 'Non recurring'),
2961+ ('daily', 'Daily'),
2962+ ('weekly', 'Weekly'),
2963+ ('monthly', 'Monthly'),
2964+ ('yearly', 'Yearly')
2965+ ],
2966+ string='Periodicity',
2967+ help='Period of automatic goal assigment. If none is selected, should be launched manually.',
2968+ required=True),
2969+ 'start_date': fields.date('Start Date',
2970+ help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date."),
2971+ 'end_date': fields.date('End Date',
2972+ help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date."),
2973+
2974+ 'proposed_user_ids': fields.many2many('res.users', 'proposed_user_ids',
2975+ string="Suggest to users"),
2976+
2977+ 'planline_ids': fields.one2many('gamification.goal.planline', 'plan_id',
2978+ string='Planline',
2979+ help="List of goals that will be set",
2980+ required=True),
2981+ 'planline_count': fields.function(_planline_count, type='integer', string="Planlines"),
2982+
2983+ 'reward_id': fields.many2one('gamification.badge', string="For Every Succeding User"),
2984+ 'reward_first_id': fields.many2one('gamification.badge', string="For 1st user"),
2985+ 'reward_second_id': fields.many2one('gamification.badge', string="For 2nd user"),
2986+ 'reward_third_id': fields.many2one('gamification.badge', string="For 3rd user"),
2987+ 'reward_failure': fields.boolean('Reward Bests if not Succeeded?'),
2988+
2989+ 'visibility_mode': fields.selection([
2990+ ('progressbar', 'Individual Goals'),
2991+ ('board', 'Leader Board (Group Ranking)'),
2992+ ],
2993+ string="Display Mode", required=True),
2994+ 'report_message_frequency': fields.selection([
2995+ ('never', 'Never'),
2996+ ('onchange', 'On change'),
2997+ ('daily', 'Daily'),
2998+ ('weekly', 'Weekly'),
2999+ ('monthly', 'Monthly'),
3000+ ('yearly', 'Yearly')
3001+ ],
3002+ string="Report Frequency", required=True),
3003+ 'report_message_group_id': fields.many2one('mail.group',
3004+ string='Send a copy to',
3005+ help='Group that will receive a copy of the report in addition to the user'),
3006+ 'report_header': fields.text('Report Header'),
3007+ 'remind_update_delay': fields.integer('Non-updated manual goals will be reminded after',
3008+ help="Never reminded if no value or zero is specified."),
3009+ 'last_report_date': fields.date('Last Report Date'),
3010+ 'next_report_date': fields.function(_get_next_report_date,
3011+ type='date',
3012+ string='Next Report Date'),
3013+
3014+ 'category': fields.selection([
3015+ ('hr', 'Human Ressources / Engagement'),
3016+ ('other', 'Settings / Gamification Tools'),
3017+ ],
3018+ string="Appears in", help="Define the visibility of the challenge through menus", required=True),
3019+ }
3020+
3021+ _defaults = {
3022+ 'period': 'once',
3023+ 'state': 'draft',
3024+ 'visibility_mode' : 'progressbar',
3025+ 'report_message_frequency' : 'onchange',
3026+ 'last_report_date': fields.date.today,
3027+ 'start_date': fields.date.today,
3028+ 'manager_id': lambda s, cr, uid, c: uid,
3029+ 'category': 'hr',
3030+ 'reward_failure': False,
3031+ }
3032+
3033+ _sort = 'end_date, start_date, name'
3034+
3035+ def write(self, cr, uid, ids, vals, context=None):
3036+ """Overwrite the write method to add the user of groups"""
3037+ context = context or {}
3038+ if not ids:
3039+ return True
3040+
3041+ # unsubscribe removed users from the plan
3042+ # users are not able to manually unsubscribe to challenges so should
3043+ # do it for them when not concerned anymore
3044+ if vals.get('user_ids'):
3045+ for action_tuple in vals['user_ids']:
3046+ if action_tuple[0] == 3:
3047+ # form (3, ID), remove one
3048+ self.message_unsubscribe_users(cr, uid, ids, [action_tuple[1]], context=context)
3049+ if action_tuple[0] == 5:
3050+ # form (5,), remove all
3051+ for plan in self.browse(cr, uid, ids, context=context):
3052+ self.message_unsubscribe_users(cr, uid, [plan.id], [user.id for user in plan.user_ids], context=context)
3053+ if action_tuple[0] == 6:
3054+ # form (6, False, [IDS]), replace by IDS
3055+ for plan in self.browse(cr, uid, ids, context=context):
3056+ removed_users = set([user.id for user in plan.user_ids]) - set(action_tuple[2])
3057+ self.message_unsubscribe_users(cr, uid, [plan.id], list(removed_users), context=context)
3058+
3059+ write_res = super(gamification_goal_plan, self).write(cr, uid, ids, vals, context=context)
3060+
3061+ # add users when change the group auto-subscription
3062+ if 'autojoin_group_id' in vals:
3063+ new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
3064+ group_user_ids = [user.id for user in new_group.users]
3065+ for plan in self.browse(cr, uid, ids, context=context):
3066+ self.write(cr, uid, [plan.id], {'user_ids': [(4, user) for user in group_user_ids]}, context=context)
3067+
3068+ # subscribe new users to the plan
3069+ if 'user_ids' in vals:
3070+ for plan in self.browse(cr, uid, ids, context=context):
3071+ self.message_subscribe_users(cr, uid, ids, [user.id for user in plan.user_ids], context=context)
3072+ return write_res
3073+
3074+ ##### Update #####
3075+
3076+ def _cron_update(self, cr, uid, context=None, ids=False):
3077+ """Daily cron check.
3078+
3079+ Start planned plans (in draft and with start_date = today)
3080+ Create the goals for planlines not linked to goals (eg: modified the
3081+ plan to add planlines)
3082+ Update every plan running
3083+ """
3084+ if not context: context = {}
3085+
3086+ # start planned plans
3087+ planned_plan_ids = self.search(cr, uid, [
3088+ ('state', '=', 'draft'),
3089+ ('start_date', '<=', fields.date.today())])
3090+ self.action_start(cr, uid, planned_plan_ids, context=context)
3091+
3092+ # close planned plans
3093+ planned_plan_ids = self.search(cr, uid, [
3094+ ('state', '=', 'inprogress'),
3095+ ('end_date', '>=', fields.date.today())])
3096+ self.action_close(cr, uid, planned_plan_ids, context=context)
3097+
3098+ if not ids:
3099+ ids = self.search(cr, uid, [('state', '=', 'inprogress')], context=context)
3100+
3101+ return self._update_all(cr, uid, ids, context=context)
3102+
3103+ def _update_all(self, cr, uid, ids, context=None):
3104+ """Update the plans and related goals
3105+
3106+ :param list(int) ids: the ids of the plans to update, if False will
3107+ update only plans in progress."""
3108+ if not context: context = {}
3109+ goal_obj = self.pool.get('gamification.goal')
3110+
3111+ # we use yesterday to update the goals that just ended
3112+ yesterday = date.today() - timedelta(days=1)
3113+ goal_ids = goal_obj.search(cr, uid, [
3114+ ('plan_id', 'in', ids),
3115+ '|',
3116+ ('state', 'in', ('inprogress', 'inprogress_update')),
3117+ '&',
3118+ ('state', 'in', ('reached', 'failed')),
3119+ '|',
3120+ ('end_date', '>=', yesterday.isoformat()),
3121+ ('end_date', '=', False)
3122+ ], context=context)
3123+ # update every running goal already generated linked to selected plans
3124+ goal_obj.update(cr, uid, goal_ids, context=context)
3125+
3126+ for plan in self.browse(cr, uid, ids, context=context):
3127+ if plan.autojoin_group_id:
3128+ # check in case of new users in plan, this happens if manager removed users in plan manually
3129+ self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context)
3130+ self.generate_goals_from_plan(cr, uid, [plan.id], context=context)
3131+
3132+ # goals closed but still opened at the last report date
3133+ closed_goals_to_report = goal_obj.search(cr, uid, [
3134+ ('plan_id', '=', plan.id),
3135+ ('start_date', '>=', plan.last_report_date),
3136+ ('end_date', '<=', plan.last_report_date)
3137+ ])
3138+
3139+ if len(closed_goals_to_report) > 0:
3140+ # some goals need a final report
3141+ self.report_progress(cr, uid, plan, subset_goal_ids=closed_goals_to_report, context=context)
3142+
3143+ if fields.date.today() == plan.next_report_date:
3144+ self.report_progress(cr, uid, plan, context=context)
3145+
3146+ self.check_challenge_reward(cr, uid, ids, context=context)
3147+ return True
3148+
3149+ def quick_update(self, cr, uid, plan_id, context=None):
3150+ """Update all the goals of a plan, no generation of new goals"""
3151+ if not context: context = {}
3152+ plan = self.browse(cr, uid, plan_id, context=context)
3153+ goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', '=', plan_id)], context=context)
3154+ self.pool.get('gamification.goal').update(cr, uid, goal_ids, context=context)
3155+ return True
3156+
3157+ ##### User actions #####
3158+
3159+ def action_start(self, cr, uid, ids, context=None):
3160+ """Start a draft goal plan
3161+
3162+ Change the state of the plan to in progress and generate related goals
3163+ """
3164+ # subscribe users if autojoin group
3165+ for plan in self.browse(cr, uid, ids, context=context):
3166+ if plan.autojoin_group_id:
3167+ self.write(cr, uid, [plan.id], {'user_ids': [(4, user.id) for user in plan.autojoin_group_id.users]}, context=context)
3168+
3169+ self.write(cr, uid, plan.id, {'state': 'inprogress'}, context=context)
3170+ self.message_post(cr, uid, plan.id, body="New challenge started.", context=context)
3171+ return self.generate_goals_from_plan(cr, uid, ids, context=context)
3172+
3173+ def action_check(self, cr, uid, ids, context=None):
3174+ """Check a goal plan
3175+
3176+ Create goals that haven't been created yet (eg: if added users of planlines)
3177+ Recompute the current value for each goal related"""
3178+ return self._update_all(cr, uid, ids=ids, context=context)
3179+
3180+ def action_close(self, cr, uid, ids, context=None):
3181+ """Close a plan in progress
3182+
3183+ Change the state of the plan to in done
3184+ Does NOT close the related goals, this is handled by the goal itself"""
3185+ self.check_challenge_reward(cr, uid, ids, force=True, context=context)
3186+ return self.write(cr, uid, ids, {'state': 'done'}, context=context)
3187+
3188+ def action_reset(self, cr, uid, ids, context=None):
3189+ """Reset a closed goal plan
3190+
3191+ Change the state of the plan to in progress
3192+ Closing a plan does not affect the goals so neither does reset"""
3193+ return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
3194+
3195+ def action_cancel(self, cr, uid, ids, context=None):
3196+ """Cancel a plan in progress
3197+
3198+ Change the state of the plan to draft
3199+ Cancel the related goals"""
3200+ self.write(cr, uid, ids, {'state': 'draft'}, context=context)
3201+ goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('plan_id', 'in', ids)], context=context)
3202+ self.pool.get('gamification.goal').write(cr, uid, goal_ids, {'state': 'canceled'}, context=context)
3203+
3204+ return True
3205+
3206+ def action_report_progress(self, cr, uid, ids, context=None):
3207+ """Manual report of a goal, does not influence automatic report frequency"""
3208+ for plan in self.browse(cr, uid, ids, context):
3209+ self.report_progress(cr, uid, plan, context=context)
3210+ return True
3211+
3212+ ##### Automatic actions #####
3213+
3214+ def generate_goals_from_plan(self, cr, uid, ids, context=None):
3215+ """Generate the list of goals linked to a plan.
3216+
3217+ If goals already exist for this planline, the planline is skipped. This
3218+ can be called after each change in the user or planline list.
3219+ :param list(int) ids: the list of plan concerned"""
3220+
3221+ for plan in self.browse(cr, uid, ids, context):
3222+ (start_date, end_date) = start_end_date_for_period(plan.period)
3223+
3224+ # if no periodicity, use plan dates
3225+ if not start_date and plan.start_date:
3226+ start_date = plan.start_date
3227+ if not end_date and plan.end_date:
3228+ end_date = plan.end_date
3229+
3230+ for planline in plan.planline_ids:
3231+ for user in plan.user_ids:
3232+
3233+ goal_obj = self.pool.get('gamification.goal')
3234+ domain = [('planline_id', '=', planline.id), ('user_id', '=', user.id)]
3235+ if start_date:
3236+ domain.append(('start_date', '=', start_date))
3237+
3238+ # goal already existing for this planline ?
3239+ if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
3240+
3241+ # resume canceled goals
3242+ domain.append(('state', '=', 'canceled'))
3243+ canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
3244+ goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
3245+ goal_obj.update(cr, uid, canceled_goal_ids, context=context)
3246+
3247+ # skip to next user
3248+ continue
3249+
3250+ values = {
3251+ 'type_id': planline.type_id.id,
3252+ 'planline_id': planline.id,
3253+ 'user_id': user.id,
3254+ 'target_goal': planline.target_goal,
3255+ 'state': 'inprogress',
3256+ }
3257+
3258+ if start_date:
3259+ values['start_date'] = start_date
3260+ if end_date:
3261+ values['end_date'] = end_date
3262+
3263+ if planline.plan_id.remind_update_delay:
3264+ values['remind_update_delay'] = planline.plan_id.remind_update_delay
3265+
3266+ new_goal_id = goal_obj.create(cr, uid, values, context)
3267+
3268+ goal_obj.update(cr, uid, [new_goal_id], context=context)
3269+
3270+ return True
3271+
3272+ ##### JS utilities #####
3273+
3274+ def get_board_goal_info(self, cr, uid, plan, subset_goal_ids=False, context=None):
3275+ """Get the list of latest goals for a plan, sorted by user ranking for each planline"""
3276+
3277+ goal_obj = self.pool.get('gamification.goal')
3278+ planlines_boards = []
3279+ (start_date, end_date) = start_end_date_for_period(plan.period)
3280+
3281+ for planline in plan.planline_ids:
3282+
3283+ domain = [
3284+ ('planline_id', '=', planline.id),
3285+ ('state', 'in', ('inprogress', 'inprogress_update',
3286+ 'reached', 'failed')),
3287+ ]
3288+
3289+ if subset_goal_ids:
3290+ goal_ids = goal_obj.search(cr, uid, domain, context=context)
3291+ common_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids]
3292+ else:
3293+ # if no subset goals, use the dates for restriction
3294+ if start_date:
3295+ domain.append(('start_date', '=', start_date))
3296+ if end_date:
3297+ domain.append(('end_date', '=', end_date))
3298+ common_goal_ids = goal_obj.search(cr, uid, domain, context=context)
3299+
3300+ board_goals = [goal for goal in goal_obj.browse(cr, uid, common_goal_ids, context=context)]
3301+
3302+ if len(board_goals) == 0:
3303+ # planline has no generated goals
3304+ continue
3305+
3306+ # most complete first, current if same percentage (eg: if several 100%)
3307+ sorted_board = enumerate(sorted(board_goals, key=lambda k: (k.completeness, k.current), reverse=True))
3308+ planlines_boards.append({'goal_type': planline.type_id, 'board_goals': sorted_board, 'target_goal': planline.target_goal})
3309+ return planlines_boards
3310+
3311+ def get_indivual_goal_info(self, cr, uid, user_id, plan, subset_goal_ids=False, context=None):
3312+ """Get the list of latest goals of a user for a plan"""
3313+ domain = [
3314+ ('plan_id', '=', plan.id),
3315+ ('user_id', '=', user_id),
3316+ ('state', 'in', ('inprogress', 'inprogress_update',
3317+ 'reached', 'failed')),
3318+ ]
3319+ goal_obj = self.pool.get('gamification.goal')
3320+ (start_date, end_date) = start_end_date_for_period(plan.period)
3321+
3322+ if subset_goal_ids:
3323+ # use the domain for safety, don't want irrelevant report if wrong argument
3324+ goal_ids = goal_obj.search(cr, uid, domain, context=context)
3325+ related_goal_ids = [goal for goal in goal_ids if goal in subset_goal_ids]
3326+ else:
3327+ # if no subset goals, use the dates for restriction
3328+ if start_date:
3329+ domain.append(('start_date', '=', start_date))
3330+ if end_date:
3331+ domain.append(('end_date', '=', end_date))
3332+ related_goal_ids = goal_obj.search(cr, uid, domain, context=context)
3333+
3334+ if len(related_goal_ids) == 0:
3335+ return False
3336+
3337+ goals = []
3338+ all_done = True
3339+ for goal in goal_obj.browse(cr, uid, related_goal_ids, context=context):
3340+ if goal.end_date:
3341+ if goal.end_date < fields.date.today():
3342+ # do not include goals of previous plan run
3343+ continue
3344+ else:
3345+ all_done = False
3346+ else:
3347+ if goal.state == 'inprogress' or goal.state == 'inprogress_update':
3348+ all_done = False
3349+
3350+ goals.append(goal)
3351+
3352+ if all_done:
3353+ # skip plans where all goal are done or failed
3354+ return False
3355+ else:
3356+ return goals
3357+
3358+ ##### Reporting #####
3359+
3360+ def report_progress(self, cr, uid, plan, context=None, users=False, subset_goal_ids=False):
3361+ """Post report about the progress of the goals
3362+
3363+ :param plan: the plan object that need to be reported
3364+ :param users: the list(res.users) of users that are concerned by
3365+ the report. If False, will send the report to every user concerned
3366+ (goal users and group that receive a copy). Only used for plan with
3367+ a visibility mode set to 'personal'.
3368+ :param goal_ids: the list(int) of goal ids linked to the plan for
3369+ the report. If not specified, use the goals for the current plan
3370+ period. This parameter can be used to produce report for previous plan
3371+ periods.
3372+ :param subset_goal_ids: a list(int) of goal ids to restrict the report
3373+ """
3374+
3375+ context = context or {}
3376+ goal_obj = self.pool.get('gamification.goal')
3377+ # template_env = TemplateHelper()
3378+ temp_obj = self.pool.get('email.template')
3379+ ctx = context.copy()
3380+ if plan.visibility_mode == 'board':
3381+ planlines_boards = self.get_board_goal_info(cr, uid, plan, subset_goal_ids, context)
3382+
3383+ ctx.update({'planlines_boards': planlines_boards})
3384+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_group', context)
3385+ body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.goal.plan', plan.id, context=context)
3386+
3387+ # body_html = template_env.get_template('group_progress.mako').render({'object': plan, 'planlines_boards': planlines_boards, 'uid': uid})
3388+
3389+ # send to every follower of the plan
3390+ self.message_post(cr, uid, plan.id,
3391+ body=body_html,
3392+ context=context,
3393+ subtype='mail.mt_comment')
3394+ if plan.report_message_group_id:
3395+ self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
3396+ body=body_html,
3397+ context=context,
3398+ subtype='mail.mt_comment')
3399+
3400+ else:
3401+ # generate individual reports
3402+ for user in users or plan.user_ids:
3403+ goals = self.get_indivual_goal_info(cr, uid, user.id, plan, subset_goal_ids, context=context)
3404+ if not goals:
3405+ continue
3406+
3407+ ctx.update({'goals': goals})
3408+ template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_goal_progress_perso', context)
3409+ body_html = temp_obj.render_template(cr, user.id, template_id.body_html, 'gamification.goal.plan', plan.id, context=context)
3410+ # send message only to users
3411+ self.message_post(cr, uid, 0,
3412+ body=body_html,
3413+ partner_ids=[(4, user.partner_id.id)],
3414+ context=context,
3415+ subtype='mail.mt_comment')
3416+ if plan.report_message_group_id:
3417+ self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
3418+ body=body_html,
3419+ context=context,
3420+ subtype='mail.mt_comment')
3421+ return self.write(cr, uid, plan.id, {'last_report_date': fields.date.today()}, context=context)
3422+
3423+ ##### Challenges #####
3424+
3425+ def accept_challenge(self, cr, uid, plan_ids, context=None, user_id=None):
3426+ """The user accept the suggested challenge"""
3427+ context = context or {}
3428+ user_id = user_id or uid
3429+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
3430+ message = "%s has joined the challenge" % user.name
3431+ self.message_post(cr, uid, plan_ids, body=message, context=context)
3432+ self.write(cr, uid, plan_ids, {'proposed_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context)
3433+ return self.generate_goals_from_plan(cr, uid, plan_ids, context=context)
3434+
3435+ def discard_challenge(self, cr, uid, plan_ids, context=None, user_id=None):
3436+ """The user discard the suggested challenge"""
3437+ context = context or {}
3438+ user_id = user_id or uid
3439+ user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
3440+ message = "%s has refused the challenge" % user.name
3441+ self.message_post(cr, uid, plan_ids, body=message, context=context)
3442+ return self.write(cr, uid, plan_ids, {'proposed_user_ids': (3, user_id)}, context=context)
3443+
3444+ def reply_challenge_wizard(self, cr, uid, plan_id, context=None):
3445+ context = context or {}
3446+ mod_obj = self.pool.get('ir.model.data')
3447+ act_obj = self.pool.get('ir.actions.act_window')
3448+ result = mod_obj.get_object_reference(cr, uid, 'gamification', 'challenge_wizard')
3449+ id = result and result[1] or False
3450+ result = act_obj.read(cr, uid, [id], context=context)[0]
3451+ result['res_id'] = plan_id
3452+ return result
3453+
3454+ def check_challenge_reward(self, cr, uid, plan_ids, force=False, context=None):
3455+ """Actions for the end of a challenge
3456+
3457+ If a reward was selected, grant it to the correct users.
3458+ Rewards granted at:
3459+ - the end date for a challenge with no periodicity
3460+ - the end of a period for challenge with periodicity
3461+ - when a challenge is manually closed
3462+ (if no end date, a running challenge is never rewarded)
3463+ """
3464+ context = context or {}
3465+ for plan in self.browse(cr, uid, plan_ids, context=context):
3466+ (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date)
3467+ yesterday = date.today() - timedelta(days=1)
3468+ if end_date == yesterday.isoformat() or force:
3469+ # open chatter message
3470+ message_body = _("The challenge %s is finished." % plan.name)
3471+
3472+ # reward for everybody succeeding
3473+ rewarded_users = []
3474+ if plan.reward_id:
3475+ for user in plan.user_ids:
3476+ reached_goal_ids = self.pool.get('gamification.goal').search(cr, uid, [
3477+ ('plan_id', '=', plan.id),
3478+ ('user_id', '=', user.id),
3479+ ('start_date', '=', start_date),
3480+ ('end_date', '=', end_date),
3481+ ('state', '=', 'reached')
3482+ ], context=context)
3483+ if len(reached_goal_ids) == len(plan.planline_ids):
3484+ self.reward_user(cr, uid, user.id, plan.reward_id.id, context)
3485+ rewarded_users.append(user)
3486+
3487+ if rewarded_users:
3488+ message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s." % (plan.reward_id.name, ", ".join([user.name for user in rewarded_users])))
3489+ else:
3490+ message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge.")
3491+
3492+ # reward bests
3493+ if plan.reward_first_id:
3494+ (first_user, second_user, third_user) = self.get_top3_users(cr, uid, plan, context)
3495+ if first_user:
3496+ self.reward_user(cr, uid, first_user.id, plan.reward_first_id.id, context)
3497+ message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
3498+ message_body += "<br/> 1. %s - %s" % (first_user.name, plan.reward_first_id.name)
3499+ else:
3500+ message_body += _("Nobody reached the required conditions to receive special badges.")
3501+
3502+ if second_user and plan.reward_second_id:
3503+ self.reward_user(cr, uid, second_user.id, plan.reward_second_id.id, context)
3504+ message_body += "<br/> 2. %s - %s" % (second_user.name, plan.reward_second_id.name)
3505+ if third_user and plan.reward_third_id:
3506+ self.reward_user(cr, uid, third_user.id, plan.reward_second_id.id, context)
3507+ message_body += "<br/> 3. %s - %s" % (third_user.name, plan.reward_third_id.name)
3508+
3509+ self.message_post(cr, uid, plan.id, body=message_body, context=context)
3510+ return True
3511+
3512+ def get_top3_users(self, cr, uid, plan, context=None):
3513+ """Get the top 3 users for a defined plan
3514+
3515+ Ranking criterias:
3516+ 1. succeed every goal of the challenge
3517+ 2. total completeness of each goal (can be over 100)
3518+ Top 3 is computed only for users succeeding every goal of the challenge,
3519+ except if reward_failure is True, in which case every user is
3520+ considered.
3521+ :return: ('first', 'second', 'third'), tuple containing the res.users
3522+ objects of the top 3 users. If no user meets the criterias for a rank,
3523+ it is set to False. Nobody can receive a rank is noone receives the
3524+ higher one (eg: if 'second' == False, 'third' will be False)
3525+ """
3526+ goal_obj = self.pool.get('gamification.goal')
3527+ (start_date, end_date) = start_end_date_for_period(plan.period, plan.start_date, plan.end_date)
3528+ challengers = []
3529+ for user in plan.user_ids:
3530+ all_reached = True
3531+ total_completness = 0
3532+ # every goal of the user for the running period
3533+ goal_ids = goal_obj.search(cr, uid, [
3534+ ('plan_id', '=', plan.id),
3535+ ('user_id', '=', user.id),
3536+ ('start_date', '=', start_date),
3537+ ('end_date', '=', end_date)
3538+ ], context=context)
3539+ for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
3540+ if goal.state != 'reached':
3541+ all_reached = False
3542+ if goal.type_condition == 'higher':
3543+ # can be over 100
3544+ total_completness += 100.0 * goal.current / goal.target_goal
3545+ elif goal.state == 'reached':
3546+ # for lower goals, can not get percentage so 0 or 100
3547+ total_completness += 100
3548+
3549+ challengers.append({'user': user, 'all_reached': all_reached, 'total_completness': total_completness})
3550+ sorted_challengers = sorted(challengers, key=lambda k: (k['all_reached'], k['total_completness']), reverse=True)
3551+
3552+ if len(sorted_challengers) == 0 or (not plan.reward_failure and not sorted_challengers[0]['all_reached']):
3553+ # nobody succeeded
3554+ return (False, False, False)
3555+ if len(sorted_challengers) == 1 or (not plan.reward_failure and not sorted_challengers[1]['all_reached']):
3556+ # only one user succeeded
3557+ return (sorted_challengers[0]['user'], False, False)
3558+ if len(sorted_challengers) == 2 or (not plan.reward_failure and not sorted_challengers[2]['all_reached']):
3559+ # only one user succeeded
3560+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], False)
3561+ return (sorted_challengers[0]['user'], sorted_challengers[1]['user'], sorted_challengers[2]['user'])
3562+
3563+ def reward_user(self, cr, uid, user_id, badge_id, context=None):
3564+ """Create a badge user and send the badge to him"""
3565+ user_badge_id = self.pool.get('gamification.badge.user').create(cr, uid, {'user_id': user_id, 'badge_id': badge_id}, context=context)
3566+ return self.pool.get('gamification.badge').send_badge(cr, uid, badge_id, [user_badge_id], user_from=None, context=context)
3567+
3568+
3569+class gamification_goal_planline(osv.Model):
3570+ """Gamification goal planline
3571+
3572+ Predifined goal for 'gamification_goal_plan'
3573+ These are generic list of goals with only the target goal defined
3574+ Should only be created for the gamification_goal_plan object
3575+ """
3576+
3577+ _name = 'gamification.goal.planline'
3578+ _description = 'Gamification generic goal for plan'
3579+ _order = "sequence, sequence_type, id"
3580+
3581+ def _get_planline_types(self, cr, uid, ids, context=None):
3582+ """Return the ids of planline items related to the gamification.goal.type
3583+ objects in 'ids (used to update the value of 'sequence_type')'"""
3584+
3585+ result = {}
3586+ for goal_type in self.pool.get('gamification.goal.type').browse(cr, uid, ids, context=context):
3587+ domain = [('type_id', '=', goal_type.id)]
3588+ planline_ids = self.pool.get('gamification.goal.planline').search(cr, uid, domain, context=context)
3589+ for p_id in planline_ids:
3590+ result[p_id] = True
3591+ return result.keys()
3592+
3593+ def on_change_type_id(self, cr, uid, ids, type_id=False, context=None):
3594+ goal_type = self.pool.get('gamification.goal.type')
3595+ if not type_id:
3596+ return {'value': {'type_id': False}}
3597+ goal_type = goal_type.browse(cr, uid, type_id, context=context)
3598+ ret = {'value': {
3599+ 'type_condition': goal_type.condition,
3600+ 'type_full_suffix': goal_type.full_suffix}}
3601+ return ret
3602+
3603+ _columns = {
3604+ 'name': fields.related('type_id', 'name', string="Name"),
3605+ 'plan_id': fields.many2one('gamification.goal.plan',
3606+ string='Plan',
3607+ required=True,
3608+ ondelete="cascade"),
3609+ 'type_id': fields.many2one('gamification.goal.type',
3610+ string='Goal Type',
3611+ required=True,
3612+ ondelete="cascade"),
3613+ 'target_goal': fields.float('Target Value to Reach',
3614+ required=True),
3615+ 'sequence': fields.integer('Sequence',
3616+ help='Sequence number for ordering'),
3617+ 'sequence_type': fields.related('type_id', 'sequence',
3618+ type='integer',
3619+ string='Sequence',
3620+ readonly=True,
3621+ store={
3622+ 'gamification.goal.type': (_get_planline_types, ['sequence'], 10),
3623+ }),
3624+ 'type_condition': fields.related('type_id', 'condition', type="selection",
3625+ readonly=True, string="Condition", selection=[('lower', '<='), ('higher', '>=')]),
3626+ 'type_suffix': fields.related('type_id', 'suffix', type="char", readonly=True, string="Unit"),
3627+ 'type_monetary': fields.related('type_id', 'monetary', type="boolean", readonly=True, string="Monetary"),
3628+ 'type_full_suffix': fields.related('type_id', 'full_suffix', type="char", readonly=True, string="Suffix"),
3629+ }
3630+
3631+ _default = {
3632+ 'sequence': 1,
3633+ }
3634
3635=== added file 'gamification/plan_view.xml'
3636--- gamification/plan_view.xml 1970-01-01 00:00:00 +0000
3637+++ gamification/plan_view.xml 2013-06-28 14:51:48 +0000
3638@@ -0,0 +1,291 @@
3639+<?xml version="1.0" encoding="UTF-8"?>
3640+<openerp>
3641+ <data>
3642+
3643+ <record id="goal_plan_list_view" model="ir.ui.view">
3644+ <field name="name">Challenges List</field>
3645+ <field name="model">gamification.goal.plan</field>
3646+ <field name="arch" type="xml">
3647+ <tree string="Goal types" colors="blue:state == 'draft';grey:state == 'done'">
3648+ <field name="name"/>
3649+ <field name="period"/>
3650+ <field name="manager_id"/>
3651+ <field name="state"/>
3652+ </tree>
3653+ </field>
3654+ </record>
3655+
3656+ <record id="goals_from_plan_act" model="ir.actions.act_window">
3657+ <field name="res_model">gamification.goal</field>
3658+ <field name="name">Related Goals</field>
3659+ <field name="view_mode">kanban,tree</field>
3660+ <field name="context">{'search_default_group_by_type': True, 'search_default_inprogress': True, 'search_default_plan_id': active_id, 'default_plan_id': active_id}</field>
3661+ <field name="help" type="html">
3662+ <p>
3663+ There is no goals associated to this challenge matching your search.
3664+ Make sure that your challenge is active and assigned to at least one user.
3665+ </p>
3666+ </field>
3667+ </record>
3668+
3669+ <record id="goal_plan_form_view" model="ir.ui.view">
3670+ <field name="name">Challenge Form</field>
3671+ <field name="model">gamification.goal.plan</field>
3672+ <field name="arch" type="xml">
3673+ <form string="Goal types" version="7.0">
3674+ <header>
3675+ <button string="Start Now" type="object" name="action_start" states="draft" class="oe_highlight"/>
3676+ <button string="Refresh Challenge" type="object" name="action_check" states="inprogress"/>
3677+ <button string="Close Challenge" type="object" name="action_close" states="inprogress" class="oe_highlight"/>
3678+ <button string="Reset to Draft" type="object" name="action_cancel" states="inprogress"/>
3679+ <button string="Reset Completion" type="object" name="action_reset" states="done"/>
3680+ <button string="Report Progress" type="object" name="action_report_progress" states="inprogress,done" groups="base.group_no_one"/>
3681+ <field name="state" widget="statusbar"/>
3682+ </header>
3683+ <sheet>
3684+
3685+ <div class="oe_title">
3686+ <label for="name" class="oe_edit_only"/>
3687+ <h1>
3688+ <field name="name" placeholder="e.g. Monthly Sales Objectives"/>
3689+ </h1>
3690+ <label for="user_ids" class="oe_edit_only" string="Assign Challenge To"/>
3691+ <div>
3692+ <field name="user_ids" widget="many2many_tags" />
3693+ </div>
3694+ </div>
3695+
3696+ <!-- action buttons -->
3697+ <div class="oe_right oe_button_box">
3698+ <button type="action" name="%(goals_from_plan_act)d" string="Related Goals" attrs="{'invisible': [('state','=','draft')]}" />
3699+ </div>
3700+ <group>
3701+ <group>
3702+ <field name="period" attrs="{'readonly':[('state','!=','draft')]}"/>
3703+ <field name="visibility_mode" widget="radio" colspan="1" />
3704+ </group>
3705+ <group>
3706+ <field name="manager_id"/>
3707+ <field name="start_date" attrs="{'readonly':[('state','!=','draft')]}"/>
3708+ <field name="end_date" attrs="{'readonly':[('state','!=','draft')]}"/>
3709+ </group>
3710+ </group>
3711+ <notebook>
3712+ <page string="Goals">
3713+ <field name="planline_ids" nolabel="1" colspan="4">
3714+ <tree string="Planline List" version="7.0" editable="bottom" >
3715+ <field name="sequence" widget="handle"/>
3716+ <field name="type_id" on_change="on_change_type_id(type_id)" />
3717+ <field name="type_condition"/>
3718+ <field name="target_goal"/>
3719+ <field name="type_full_suffix"/>
3720+ </tree>
3721+ </field>
3722+ <field name="description" placeholder="Describe the challenge: what is does, who it targets, why it matters..."/>
3723+ </page>
3724+ <page string="Reward">
3725+ <group>
3726+ <field name="reward_id"/>
3727+ <field name="reward_first_id" />
3728+ <field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" />
3729+ <field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" />
3730+ <field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" />
3731+ </group>
3732+ <div class="oe_grey">
3733+ <p>Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.</p>
3734+ </div>
3735+ </page>
3736+ <page string="Advanced Options">
3737+ <group string="Subscriptions">
3738+ <field name="autojoin_group_id" />
3739+ <field name="proposed_user_ids" widget="many2many_tags" />
3740+ </group>
3741+ <group string="Notification Messages">
3742+ <field name="report_message_frequency" />
3743+ <field name="report_header" placeholder="e.g. The following message contains the current progress of the sale team..." attrs="{'invisible': [('report_message_frequency','=','never')]}" />
3744+ <field name="report_message_group_id" attrs="{'invisible': [('report_message_frequency','=','never')]}" />
3745+ </group>
3746+ <group string="Reminders for Manual Goals">
3747+ <label for="remind_update_delay" />
3748+ <div>
3749+ <field name="remind_update_delay" class="oe_inline"/> days
3750+ </div>
3751+ </group>
3752+ <group string="Category" groups="base.group_no_one">
3753+ <field name="category" widget="radio" />
3754+ </group>
3755+ </page>
3756+ </notebook>
3757+
3758+ </sheet>
3759+ <div class="oe_chatter">
3760+ <field name="message_follower_ids" widget="mail_followers"/>
3761+ <field name="message_ids" widget="mail_thread"/>
3762+ </div>
3763+ </form>
3764+ </field>
3765+ </record>
3766+
3767+ <record model="ir.ui.view" id="view_goal_plan_kanban">
3768+ <field name="name">Challenge Kanban</field>
3769+ <field name="model">gamification.goal.plan</field>
3770+ <field name="arch" type="xml">
3771+ <kanban version="7.0" class="oe_background_grey">
3772+ <field name="planline_ids"/>
3773+ <field name="planline_count"/>
3774+ <field name="user_ids"/>
3775+ <templates>
3776+ <t t-name="kanban-box">
3777+ <div t-attf-class="oe_kanban_card oe_kanban_goal oe_kanban_global_click">
3778+ <div class="oe_dropdown_toggle oe_dropdown_kanban">
3779+ <span class="oe_e">í</span>
3780+ <ul class="oe_dropdown_menu">
3781+ <li><a type="edit">Configure Challenge</a></li>
3782+ </ul>
3783+ </div>
3784+ <div class="oe_kanban_content">
3785+
3786+ <h4><field name="name"/></h4>
3787+ <div class="oe_kanban_project_list">
3788+ <a type="action" name="%(goals_from_plan_act)d" style="margin-right: 10px">
3789+ <span t-if="record.planline_count.raw_value gt 1"><field name="planline_count"/> Goals</span>
3790+ <span t-if="record.planline_count.raw_value lt 2"><field name="planline_count"/> Goal</span>
3791+ </a>
3792+ </div>
3793+ <div class="oe_kanban_badge_avatars">
3794+ <t t-foreach="record.user_ids.raw_value.slice(0,11)" t-as="member">
3795+ <img t-att-src="kanban_image('res.users', 'image_small', member)" t-att-data-member_id="member"/>
3796+ </t>
3797+ </div>
3798+ </div>
3799+ </div>
3800+ </t>
3801+ </templates>
3802+ </kanban>
3803+ </field>
3804+ </record>
3805+
3806+ <record id="goal_plan_list_action" model="ir.actions.act_window">
3807+ <field name="name">Challenges</field>
3808+ <field name="res_model">gamification.goal.plan</field>
3809+ <field name="view_mode">kanban,tree,form</field>
3810+ <field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
3811+ <field name="help" type="html">
3812+ <p class="oe_view_nocontent_create">
3813+ Click to create a challenge.
3814+ </p>
3815+ <p>
3816+ Assign a list of goals to chosen users to evaluate them.
3817+ The challenge can use a period (weekly, monthly...) for automatic creation of goals.
3818+ The goals are created for the specified users or member of the group.
3819+ </p>
3820+ </field>
3821+ </record>
3822+ <!-- Specify form view ID to avoid selecting view_challenge_wizard -->
3823+ <record id="goal_plan_list_action_view1" model="ir.actions.act_window.view">
3824+ <field eval="1" name="sequence"/>
3825+ <field name="view_mode">kanban</field>
3826+ <field name="act_window_id" ref="goal_plan_list_action"/>
3827+ <field name="view_id" ref="view_goal_plan_kanban"/>
3828+ </record>
3829+ <record id="goal_plan_list_action_view2" model="ir.actions.act_window.view">
3830+ <field eval="10" name="sequence"/>
3831+ <field name="view_mode">form</field>
3832+ <field name="act_window_id" ref="goal_plan_list_action"/>
3833+ <field name="view_id" ref="goal_plan_form_view"/>
3834+ </record>
3835+
3836+ <!-- Planline -->
3837+ <record id="goal_planline_list_view" model="ir.ui.view">
3838+ <field name="name">Goal planline list</field>
3839+ <field name="model">gamification.goal.planline</field>
3840+ <field name="arch" type="xml">
3841+ <tree string="planline list" >
3842+ <field name="type_id"/>
3843+ <field name="target_goal"/>
3844+ </tree>
3845+ </field>
3846+ </record>
3847+
3848+
3849+ <record id="goal_plan_search_view" model="ir.ui.view">
3850+ <field name="name">Challenge Search</field>
3851+ <field name="model">gamification.goal.plan</field>
3852+ <field name="arch" type="xml">
3853+ <search string="Search Challenges">
3854+ <filter name="inprogress" string="Running Challenges"
3855+ domain="[('state', '=', 'inprogress')]"/>
3856+ <filter name="hr_plans" string="HR Challenges"
3857+ domain="[('category', '=', 'hr')]"/>
3858+ <field name="name"/>
3859+ <group expand="0" string="Group By...">
3860+ <filter string="State" domain="[]" context="{'group_by':'state'}"/>
3861+ <filter string="Period" domain="[]" context="{'group_by':'period'}"/>
3862+ </group>
3863+ </search>
3864+ </field>
3865+ </record>
3866+
3867+
3868+ <record id="view_challenge_wizard" model="ir.ui.view">
3869+ <field name="name">Challenge Wizard</field>
3870+ <field name="model">gamification.goal.plan</field>
3871+ <field name="arch" type="xml">
3872+ <form string="Challenge" version="7.0">
3873+ <field name="reward_failure" invisible="1"/>
3874+ <div class="oe_title">
3875+ <h1><field name="name" nolabel="1" readonly="1"/></h1>
3876+ </div>
3877+ <field name="description" nolabel="1" readonly="1" />
3878+ <group>
3879+ <field name="start_date" readonly="1" />
3880+ <field name="end_date" readonly="1" />
3881+ <field name="user_ids" string="Participating" readonly="1" widget="many2many_tags" />
3882+ <field name="proposed_user_ids" string="Invited" readonly="1" widget="many2many_tags" />
3883+ </group>
3884+ <group string="Goals">
3885+ <field name="planline_ids" nolabel="1" readonly="1" colspan="4">
3886+ <tree string="Planline List" version="7.0" editable="bottom" >
3887+ <field name="sequence" widget="handle"/>
3888+ <field name="type_id"/>
3889+ <field name="type_condition"/>
3890+ <field name="target_goal"/>
3891+ <field name="type_full_suffix"/>
3892+ </tree>
3893+ </field>
3894+ </group>
3895+ <group string="Reward">
3896+ <div class="oe_grey" attrs="{'invisible': ['|',('reward_id','!=',False),('reward_first_id','!=',False)]}">
3897+ There is no reward upon completion of this challenge.
3898+ </div>
3899+ <group attrs="{'invisible': [('reward_id','=',False),('reward_first_id','=',False)]}">
3900+ <field name="reward_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
3901+ <field name="reward_first_id" readonly="1" attrs="{'invisible': [('reward_first_id','=', False)]}" />
3902+ <field name="reward_second_id" readonly="1" attrs="{'invisible': [('reward_second_id','=', False)]}" />
3903+ <field name="reward_third_id" readonly="1" attrs="{'invisible': [('reward_third_id','=', False)]}" />
3904+ </group>
3905+ <div class="oe_grey" attrs="{'invisible': [('reward_failure','=',False)]}">
3906+ Even if the challenge is failed, best challengers will be rewarded
3907+ </div>
3908+ </group>
3909+ <footer>
3910+ <center>
3911+ <button string="Accept" type="object" name="accept_challenge" class="oe_highlight" />
3912+ <button string="Reject" type="object" name="discard_challenge"/> or
3913+ <button string="reply later" special="cancel" class="oe_link"/>
3914+ </center>
3915+ </footer>
3916+ </form>
3917+ </field>
3918+ </record>
3919+
3920+ <record id="challenge_wizard" model="ir.actions.act_window">
3921+ <field name="name">Challenge Description</field>
3922+ <field name="res_model">gamification.goal.plan</field>
3923+ <field name="view_type">form</field>
3924+ <field name="view_id" ref="view_challenge_wizard"/>
3925+ <field name="target">new</field>
3926+ </record>
3927+
3928+ </data>
3929+</openerp>
3930\ No newline at end of file
3931
3932=== added file 'gamification/res_users.py'
3933--- gamification/res_users.py 1970-01-01 00:00:00 +0000
3934+++ gamification/res_users.py 2013-06-28 14:51:48 +0000
3935@@ -0,0 +1,177 @@
3936+# -*- coding: utf-8 -*-
3937+##############################################################################
3938+#
3939+# OpenERP, Open Source Management Solution
3940+# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
3941+#
3942+# This program is free software: you can redistribute it and/or modify
3943+# it under the terms of the GNU General Public License as published by
3944+# the Free Software Foundation, either version 3 of the License, or
3945+# (at your option) any later version.
3946+#
3947+# This program is distributed in the hope that it will be useful,
3948+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3949+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3950+# GNU General Public License for more details.
3951+#
3952+# You should have received a copy of the GNU General Public License
3953+# along with this program. If not, see <http://www.gnu.org/licenses/>
3954+#
3955+##############################################################################
3956+
3957+from openerp.osv import osv
3958+
3959+
3960+class res_users_gamification_group(osv.Model):
3961+ """ Update of res.users class
3962+ - if adding groups to an user, check gamification.goal.plan linked to
3963+ this group, and the user. This is done by overriding the write method.
3964+ """
3965+ _name = 'res.users'
3966+ _inherit = ['res.users']
3967+
3968+ def write(self, cr, uid, ids, vals, context=None):
3969+ write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context)
3970+ if vals.get('groups_id'):
3971+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
3972+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
3973+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
3974+
3975+ goal_plan_obj = self.pool.get('gamification.goal.plan')
3976+ plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context)
3977+ if plan_ids:
3978+ goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context)
3979+
3980+ if vals.get('image'):
3981+ goal_type_id = self.pool.get('ir.model.data').get_object(cr, uid, 'gamification', 'type_base_avatar', context)
3982+ goal_ids = self.pool.get('gamification.goal').search(cr, uid, [('type_id', '=', goal_type_id.id), ('user_id', 'in', ids)], context=context)
3983+ values = {'state': 'reached', 'current': 1}
3984+ self.pool.get('gamification.goal').write(cr, uid, goal_ids, values, context=context)
3985+ return write_res
3986+
3987+ def get_goals_todo_info(self, cr, uid, context=None):
3988+ """Return the list of goals assigned to the user, grouped by plan
3989+
3990+ This method intends to return processable data in javascript in the
3991+ goal_list_to_do template. The output format is not constant as the
3992+ required information is different between individual and board goal
3993+ types
3994+ :return: list of dictionnaries for each goal to display
3995+ """
3996+ all_goals_info = []
3997+ plan_obj = self.pool.get('gamification.goal.plan')
3998+
3999+ plan_ids = plan_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
4000+ for plan in plan_obj.browse(cr, uid, plan_ids, context=context):
4001+ # serialize goals info to be able to use it in javascript
4002+ serialized_goals_info = {
4003+ 'id': plan.id,
4004+ 'name': plan.name,
4005+ 'visibility_mode': plan.visibility_mode,
4006+ }
4007+ user = self.browse(cr, uid, uid, context=context)
4008+ serialized_goals_info['currency'] = user.company_id.currency_id.id
4009+
4010+ if plan.visibility_mode == 'board':
4011+ # board report should be grouped by planline for all users
4012+ goals_info = plan_obj.get_board_goal_info(cr, uid, plan, subset_goal_ids=False, context=context)
4013+
4014+ if len(goals_info) == 0:
4015+ # plan with no valid planlines
4016+ continue
4017+
4018+ serialized_goals_info['planlines'] = []
4019+ for planline_board in goals_info:
4020+ vals = {'type_name': planline_board['goal_type'].name,
4021+ 'type_description': planline_board['goal_type'].description,
4022+ 'type_condition': planline_board['goal_type'].condition,
4023+ 'type_computation_mode': planline_board['goal_type'].computation_mode,
4024+ 'type_monetary': planline_board['goal_type'].monetary,
4025+ 'type_suffix': planline_board['goal_type'].suffix,
4026+ 'type_action': True if planline_board['goal_type'].action_id else False,
4027+ 'type_display': planline_board['goal_type'].display_mode,
4028+ 'target_goal': planline_board['target_goal'],
4029+ 'goals': []}
4030+ for goal in planline_board['board_goals']:
4031+ # Keep only the Top 3 and the current user
4032+ if goal[0] > 2 and goal[1].user_id.id != uid:
4033+ continue
4034+
4035+ vals['goals'].append({
4036+ 'rank': goal[0] + 1,
4037+ 'id': goal[1].id,
4038+ 'user_id': goal[1].user_id.id,
4039+ 'user_name': goal[1].user_id.name,
4040+ 'state': goal[1].state,
4041+ 'completeness': goal[1].completeness,
4042+ 'current': goal[1].current,
4043+ 'target_goal': goal[1].target_goal,
4044+ })
4045+ if uid == goal[1].user_id.id:
4046+ vals['own_goal_id'] = goal[1].id
4047+ serialized_goals_info['planlines'].append(vals)
4048+
4049+ else:
4050+ # individual report are simply a list of goal
4051+ goals_info = plan_obj.get_indivual_goal_info(cr, uid, uid, plan, subset_goal_ids=False, context=context)
4052+
4053+ if not goals_info:
4054+ continue
4055+
4056+ serialized_goals_info['goals'] = []
4057+ for goal in goals_info:
4058+ serialized_goals_info['goals'].append({
4059+ 'id': goal.id,
4060+ 'type_name': goal.type_id.name,
4061+ 'type_description': goal.type_description,
4062+ 'type_condition': goal.type_id.condition,
4063+ 'type_monetary': goal.type_id.monetary,
4064+ 'type_suffix': goal.type_id.suffix,
4065+ 'type_action': True if goal.type_id.action_id else False,
4066+ 'type_display': goal.type_id.display_mode,
4067+ 'state': goal.state,
4068+ 'completeness': goal.completeness,
4069+ 'computation_mode': goal.computation_mode,
4070+ 'current': goal.current,
4071+ 'target_goal': goal.target_goal,
4072+ })
4073+
4074+ all_goals_info.append(serialized_goals_info)
4075+ return all_goals_info
4076+
4077+ def get_challenge_suggestions(self, cr, uid, context=None):
4078+ """Return the list of goal plans suggested to the user"""
4079+ plan_info = []
4080+ goal_plan_obj = self.pool.get('gamification.goal.plan')
4081+ plan_ids = goal_plan_obj.search(cr, uid, [('proposed_user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context)
4082+ for plan in goal_plan_obj.browse(cr, uid, plan_ids, context=context):
4083+ values = {
4084+ 'id': plan.id,
4085+ 'name': plan.name,
4086+ 'description': plan.description,
4087+ }
4088+ plan_info.append(values)
4089+ return plan_info
4090+
4091+
4092+class res_groups_gamification_group(osv.Model):
4093+ """ Update of res.groups class
4094+ - if adding users from a group, check gamification.goal.plan linked to
4095+ this group, and the user. This is done by overriding the write method.
4096+ """
4097+ _name = 'res.groups'
4098+ _inherit = 'res.groups'
4099+
4100+ def write(self, cr, uid, ids, vals, context=None):
4101+ write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context)
4102+ if vals.get('users'):
4103+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
4104+ user_ids = [command[1] for command in vals['users'] if command[0] == 4]
4105+ user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
4106+
4107+ goal_plan_obj = self.pool.get('gamification.goal.plan')
4108+ plan_ids = goal_plan_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context)
4109+ if plan_ids:
4110+ goal_plan_obj.write(cr, uid, plan_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context)
4111+ return write_res
4112+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
4113
4114=== added directory 'gamification/security'
4115=== added file 'gamification/security/gamification_security.xml'
4116--- gamification/security/gamification_security.xml 1970-01-01 00:00:00 +0000
4117+++ gamification/security/gamification_security.xml 2013-06-28 14:51:48 +0000
4118@@ -0,0 +1,31 @@
4119+<?xml version="1.0" ?>
4120+<openerp>
4121+ <data noupdate="1">
4122+ <record model="ir.module.category" id="module_goal_category">
4123+ <field name="name">Gamification</field>
4124+ <field name="description"></field>
4125+ <field name="sequence">17</field>
4126+ </record>
4127+ <record id="group_goal_manager" model="res.groups">
4128+ <field name="name">Manager</field>
4129+ <field name="category_id" ref="module_goal_category"/>
4130+ <field name="users" eval="[(4, ref('base.user_root'))]"/>
4131+ </record>
4132+
4133+ <record id="goal_user_visibility" model="ir.rule">
4134+ <field name="name">User can only see his/her goals or goal from the same plan in board visibility</field>
4135+ <field name="model_id" ref="model_gamification_goal"/>
4136+ <field name="groups" eval="[(4, ref('base.group_user'))]"/>
4137+ <field name="perm_read" eval="True"/>
4138+ <field name="perm_write" eval="True"/>
4139+ <field name="perm_create" eval="False"/>
4140+ <field name="perm_unlink" eval="False"/>
4141+ <field name="domain_force">[
4142+ '|',
4143+ ('user_id','=',user.id),
4144+ '&amp;',
4145+ ('plan_id.user_ids','in',user.id),
4146+ ('plan_id.visibility_mode','=','board')]</field>
4147+ </record>
4148+ </data>
4149+</openerp>
4150
4151=== added file 'gamification/security/ir.model.access.csv'
4152--- gamification/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
4153+++ gamification/security/ir.model.access.csv 2013-06-28 14:51:48 +0000
4154@@ -0,0 +1,20 @@
4155+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
4156+
4157+goal_anybody,"Goal Anybody",model_gamification_goal,,1,1,0,0
4158+goal_manager,"Goal Manager",model_gamification_goal,group_goal_manager,1,1,1,1
4159+
4160+goal_type_anybody,"Goal Type Anybody",model_gamification_goal_type,,1,0,0,0
4161+goal_type_manager,"Goal Type Manager",model_gamification_goal_type,group_goal_manager,1,1,1,1
4162+
4163+plan_anybody,"Goal Plan Anybody",model_gamification_goal_plan,,1,0,0,0
4164+plan_manager,"Goal Plan Manager",model_gamification_goal_plan,group_goal_manager,1,1,1,1
4165+
4166+planline_anybody,"Goal Planline Anybody",model_gamification_goal_planline,,1,0,0,0
4167+planline_manager,"Goal Planline Manager",model_gamification_goal_planline,group_goal_manager,1,1,1,1
4168+
4169+badge_anybody,"Badge Anybody",model_gamification_badge,,1,0,0,0
4170+badge_manager,"Badge Manager",model_gamification_badge,group_goal_manager,1,1,1,1
4171+
4172+badge_user_anybody,"Badge-user Anybody",model_gamification_badge_user,,1,0,0,0
4173+badge_user_user,"Badge-user User",model_gamification_badge_user,base.group_user,1,1,1,0
4174+badge_user_manager,"Badge-user Manager",model_gamification_badge_user,group_goal_manager,1,1,1,1
4175
4176=== added directory 'gamification/static'
4177=== added directory 'gamification/static/lib'
4178=== added directory 'gamification/static/lib/justgage'
4179=== added file 'gamification/static/lib/justgage/justgage.js'
4180--- gamification/static/lib/justgage/justgage.js 1970-01-01 00:00:00 +0000
4181+++ gamification/static/lib/justgage/justgage.js 2013-06-28 14:51:48 +0000
4182@@ -0,0 +1,946 @@
4183+/**
4184+ * JustGage - this is work-in-progress, unreleased, unofficial code, so it might not work top-notch :)
4185+ * Check http://www.justgage.com for official releases
4186+ * Licensed under MIT.
4187+ * @author Bojan Djuricic (@Toorshia)
4188+ *
4189+ * LATEST UPDATES
4190+
4191+ * -----------------------------
4192+ * April 18, 2013.
4193+ * -----------------------------
4194+ * parentNode - use this instead of id, to attach gauge to node which is outside of DOM tree - https://github.com/toorshia/justgage/issues/48
4195+ * width - force gauge width
4196+ * height - force gauge height
4197+
4198+ * -----------------------------
4199+ * April 17, 2013.
4200+ * -----------------------------
4201+ * fix - https://github.com/toorshia/justgage/issues/49
4202+
4203+ * -----------------------------
4204+ * April 01, 2013.
4205+ * -----------------------------
4206+ * fix - https://github.com/toorshia/justgage/issues/46
4207+
4208+ * -----------------------------
4209+ * March 26, 2013.
4210+ * -----------------------------
4211+ * customSectors - define specific color for value range (0-10 : red, 10-30 : blue etc.)
4212+
4213+ * -----------------------------
4214+ * March 23, 2013.
4215+ * -----------------------------
4216+ * counter - option to animate value in counting fashion
4217+ * fix - https://github.com/toorshia/justgage/issues/45
4218+
4219+ * -----------------------------
4220+ * March 13, 2013.
4221+ * -----------------------------
4222+ * refresh method - added optional 'max' parameter to use when you need to update max value
4223+
4224+ * -----------------------------
4225+ * February 26, 2013.
4226+ * -----------------------------
4227+ * decimals - option to define/limit number of decimals when not using humanFriendly or customRenderer to display value
4228+ * fixed a missing parameters bug when calling generateShadow() for IE < 9
4229+
4230+ * -----------------------------
4231+ * December 31, 2012.
4232+ * -----------------------------
4233+ * fixed text y-position for hidden divs - workaround for Raphael <tspan> 'dy' bug - https://github.com/DmitryBaranovskiy/raphael/issues/491
4234+ * 'show' parameters, like showMinMax are now 'hide' because I am lame developer - please update these in your setups
4235+ * Min and Max labels are now auto-off when in donut mode
4236+ * Start angle in donut mode is now 90
4237+ * donutStartAngle - option to define start angle for donut
4238+
4239+ * -----------------------------
4240+ * November 25, 2012.
4241+ * -----------------------------
4242+ * Option to define custom rendering function for displayed value
4243+
4244+ * -----------------------------
4245+ * November 19, 2012.
4246+ * -----------------------------
4247+ * Config.value is now updated after gauge refresh
4248+
4249+ * -----------------------------
4250+ * November 13, 2012.
4251+ * -----------------------------
4252+ * Donut display mode added
4253+ * Option to hide value label
4254+ * Option to enable responsive gauge size
4255+ * Removed default title attribute
4256+ * Option to accept min and max defined as string values
4257+ * Option to configure value symbol
4258+ * Fixed bad aspect ratio calculations
4259+ * Option to configure minimum font size for all texts
4260+ * Option to show shorthand big numbers (human friendly)
4261+ */
4262+
4263+ JustGage = function(config) {
4264+
4265+ // if (!config.id) {alert("Missing id parameter for gauge!"); return false;}
4266+ // if (!document.getElementById(config.id)) {alert("No element with id: \""+config.id+"\" found!"); return false;}
4267+
4268+ var obj = this;
4269+
4270+ // configurable parameters
4271+ obj.config =
4272+ {
4273+ // id : string
4274+ // this is container element id
4275+ id : config.id,
4276+
4277+ // parentNode : node object
4278+ // this is container element
4279+ parentNode : (config.parentNode) ? config.parentNode : null,
4280+
4281+ // width : int
4282+ // gauge width
4283+ width : (config.width) ? config.width : null,
4284+
4285+ // height : int
4286+ // gauge height
4287+ height : (config.height) ? config.height : null,
4288+
4289+ // title : string
4290+ // gauge title
4291+ title : (config.title) ? config.title : "",
4292+
4293+ // titleFontColor : string
4294+ // color of gauge title
4295+ titleFontColor : (config.titleFontColor) ? config.titleFontColor : "#999999",
4296+
4297+ // value : int
4298+ // value gauge is showing
4299+ value : (config.value) ? config.value : 0,
4300+
4301+ // valueFontColor : string
4302+ // color of label showing current value
4303+ valueFontColor : (config.valueFontColor) ? config.valueFontColor : "#010101",
4304+
4305+ // symbol : string
4306+ // special symbol to show next to value
4307+ symbol : (config.symbol) ? config.symbol : "",
4308+
4309+ // min : int
4310+ // min value
4311+ min : (config.min) ? parseFloat(config.min) : 0,
4312+
4313+ // max : int
4314+ // max value
4315+ max : (config.max) ? parseFloat(config.max) : 100,
4316+
4317+ // humanFriendlyDecimal : int
4318+ // number of decimal places for our human friendly number to contain
4319+ humanFriendlyDecimal : (config.humanFriendlyDecimal) ? config.humanFriendlyDecimal : 0,
4320+
4321+ // textRenderer: func
4322+ // function applied before rendering text
4323+ textRenderer : (config.textRenderer) ? config.textRenderer : null,
4324+
4325+ // gaugeWidthScale : float
4326+ // width of the gauge element
4327+ gaugeWidthScale : (config.gaugeWidthScale) ? config.gaugeWidthScale : 1.0,
4328+
4329+ // gaugeColor : string
4330+ // background color of gauge element
4331+ gaugeColor : (config.gaugeColor) ? config.gaugeColor : "#edebeb",
4332+
4333+ // label : string
4334+ // text to show below value
4335+ label : (config.label) ? config.label : "",
4336+
4337+ // labelFontColor : string
4338+ // color of label showing label under value
4339+ labelFontColor : (config.labelFontColor) ? config.labelFontColor : "#b3b3b3",
4340+
4341+ // shadowOpacity : int
4342+ // 0 ~ 1
4343+ shadowOpacity : (config.shadowOpacity) ? config.shadowOpacity : 0.2,
4344+
4345+ // shadowSize: int
4346+ // inner shadow size
4347+ shadowSize : (config.shadowSize) ? config.shadowSize : 5,
4348+
4349+ // shadowVerticalOffset : int
4350+ // how much shadow is offset from top
4351+ shadowVerticalOffset : (config.shadowVerticalOffset) ? config.shadowVerticalOffset : 3,
4352+
4353+ // levelColors : string[]
4354+ // colors of indicator, from lower to upper, in RGB format
4355+ levelColors : (config.levelColors) ? config.levelColors : [
4356+ "#a9d70b",
4357+ "#f9c802",
4358+ "#ff0000"
4359+ ],
4360+
4361+ // startAnimationTime : int
4362+ // length of initial animation
4363+ startAnimationTime : (config.startAnimationTime) ? config.startAnimationTime : 700,
4364+
4365+ // startAnimationType : string
4366+ // type of initial animation (linear, >, <, <>, bounce)
4367+ startAnimationType : (config.startAnimationType) ? config.startAnimationType : ">",
4368+
4369+ // refreshAnimationTime : int
4370+ // length of refresh animation
4371+ refreshAnimationTime : (config.refreshAnimationTime) ? config.refreshAnimationTime : 700,
4372+
4373+ // refreshAnimationType : string
4374+ // type of refresh animation (linear, >, <, <>, bounce)
4375+ refreshAnimationType : (config.refreshAnimationType) ? config.refreshAnimationType : ">",
4376+
4377+ // donutStartAngle : int
4378+ // angle to start from when in donut mode
4379+ donutStartAngle : (config.donutStartAngle) ? config.donutStartAngle : 90,
4380+
4381+ // valueMinFontSize : int
4382+ // absolute minimum font size for the value
4383+ valueMinFontSize : config.valueMinFontSize || 16,
4384+
4385+ // titleMinFontSize
4386+ // absolute minimum font size for the title
4387+ titleMinFontSize : config.titleMinFontSize || 10,
4388+
4389+ // labelMinFontSize
4390+ // absolute minimum font size for the label
4391+ labelMinFontSize : config.labelMinFontSize || 10,
4392+
4393+ // minLabelMinFontSize
4394+ // absolute minimum font size for the minimum label
4395+ minLabelMinFontSize : config.minLabelMinFontSize || 10,
4396+
4397+ // maxLabelMinFontSize
4398+ // absolute minimum font size for the maximum label
4399+ maxLabelMinFontSize : config.maxLabelMinFontSize || 10,
4400+
4401+ // hideValue : bool
4402+ // hide value text
4403+ hideValue : (config.hideValue) ? config.hideValue : false,
4404+
4405+ // hideMinMax : bool
4406+ // hide min and max values
4407+ hideMinMax : (config.hideMinMax) ? config.hideMinMax : false,
4408+
4409+ // hideInnerShadow : bool
4410+ // hide inner shadow
4411+ hideInnerShadow : (config.hideInnerShadow) ? config.hideInnerShadow : false,
4412+
4413+ // humanFriendly : bool
4414+ // convert large numbers for min, max, value to human friendly (e.g. 1234567 -> 1.23M)
4415+ humanFriendly : (config.humanFriendly) ? config.humanFriendly : false,
4416+
4417+ // noGradient : bool
4418+ // whether to use gradual color change for value, or sector-based
4419+ noGradient : (config.noGradient) ? config.noGradient : false,
4420+
4421+ // donut : bool
4422+ // show full donut gauge
4423+ donut : (config.donut) ? config.donut : false,
4424+
4425+ // relativeGaugeSize : bool
4426+ // whether gauge size should follow changes in container element size
4427+ relativeGaugeSize : (config.relativeGaugeSize) ? config.relativeGaugeSize : false,
4428+
4429+ // counter : bool
4430+ // animate level number change
4431+ counter : (config.counter) ? config.counter : false,
4432+
4433+ // decimals : int
4434+ // number of digits after floating point
4435+ decimals : (config.decimals) ? config.decimals : 0,
4436+
4437+ // customSectors : [] of objects
4438+ // number of digits after floating point
4439+ customSectors : (config.customSectors) ? config.customSectors : []
4440+ };
4441+
4442+ // variables
4443+ var
4444+ canvasW,
4445+ canvasH,
4446+ widgetW,
4447+ widgetH,
4448+ aspect,
4449+ dx,
4450+ dy,
4451+ titleFontSize,
4452+ titleX,
4453+ titleY,
4454+ valueFontSize,
4455+ valueX,
4456+ valueY,
4457+ labelFontSize,
4458+ labelX,
4459+ labelY,
4460+ minFontSize,
4461+ minX,
4462+ minY,
4463+ maxFontSize,
4464+ maxX,
4465+ maxY;
4466+
4467+ // overflow values
4468+ if (obj.config.value > obj.config.max) obj.config.value = obj.config.max;
4469+ if (obj.config.value < obj.config.min) obj.config.value = obj.config.min;
4470+ obj.originalValue = config.value;
4471+
4472+ // create canvas
4473+ if (obj.config.id !== null && (document.getElementById(obj.config.id)) !== null) {
4474+ obj.canvas = Raphael(obj.config.id, "100%", "100%");
4475+ } else if (obj.config.parentNode !== null) {
4476+ obj.canvas = Raphael(obj.config.parentNode, "100%", "100%");
4477+ }
4478+
4479+ if (obj.config.relativeGaugeSize === true) {
4480+ obj.canvas.setViewBox(0, 0, 200, 150, true);
4481+ }
4482+
4483+ // canvas dimensions
4484+ if (obj.config.relativeGaugeSize === true) {
4485+ canvasW = 200;
4486+ canvasH = 150;
4487+ } else if (obj.config.width !== null && obj.config.height !== null) {
4488+ canvasW = obj.config.width;
4489+ canvasH = obj.config.height;
4490+ } else if (obj.config.parentNode !== null) {
4491+ obj.canvas.setViewBox(0, 0, 200, 150, true);
4492+ canvasW = 200;
4493+ canvasH = 150;
4494+ } else {
4495+ canvasW = getStyle(document.getElementById(obj.config.id), "width").slice(0, -2) * 1;
4496+ canvasH = getStyle(document.getElementById(obj.config.id), "height").slice(0, -2) * 1;
4497+ }
4498+
4499+ // widget dimensions
4500+ if (obj.config.donut === true) {
4501+
4502+ // DONUT *******************************
4503+
4504+ // width more than height
4505+ if(canvasW > canvasH) {
4506+ widgetH = canvasH;
4507+ widgetW = widgetH;
4508+ // width less than height
4509+ } else if (canvasW < canvasH) {
4510+ widgetW = canvasW;
4511+ widgetH = widgetW;
4512+ // if height don't fit, rescale both
4513+ if(widgetH > canvasH) {
4514+ aspect = widgetH / canvasH;
4515+ widgetH = widgetH / aspect;
4516+ widgetW = widgetH / aspect;
4517+ }
4518+ // equal
4519+ } else {
4520+ widgetW = canvasW;
4521+ widgetH = widgetW;
4522+ }
4523+
4524+ // delta
4525+ dx = (canvasW - widgetW)/2;
4526+ dy = (canvasH - widgetH)/2;
4527+
4528+ // title
4529+ titleFontSize = ((widgetH / 8) > 10) ? (widgetH / 10) : 10;
4530+ titleX = dx + widgetW / 2;
4531+ titleY = dy + widgetH / 11;
4532+
4533+ // value
4534+ valueFontSize = ((widgetH / 6.4) > 16) ? (widgetH / 5.4) : 18;
4535+ valueX = dx + widgetW / 2;
4536+ if(obj.config.label !== '') {
4537+ valueY = dy + widgetH / 1.85;
4538+ } else {
4539+ valueY = dy + widgetH / 1.7;
4540+ }
4541+
4542+ // label
4543+ labelFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
4544+ labelX = dx + widgetW / 2;
4545+ labelY = valueY + labelFontSize;
4546+
4547+ // min
4548+ minFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
4549+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
4550+ minY = labelY;
4551+
4552+ // max
4553+ maxFontSize = ((widgetH / 16) > 10) ? (widgetH / 16) : 10;
4554+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
4555+ maxY = labelY;
4556+
4557+ } else {
4558+ // HALF *******************************
4559+
4560+ // width more than height
4561+ if(canvasW > canvasH) {
4562+ widgetH = canvasH;
4563+ widgetW = widgetH * 1.25;
4564+ //if width doesn't fit, rescale both
4565+ if(widgetW > canvasW) {
4566+ aspect = widgetW / canvasW;
4567+ widgetW = widgetW / aspect;
4568+ widgetH = widgetH / aspect;
4569+ }
4570+ // width less than height
4571+ } else if (canvasW < canvasH) {
4572+ widgetW = canvasW;
4573+ widgetH = widgetW / 1.25;
4574+ // if height don't fit, rescale both
4575+ if(widgetH > canvasH) {
4576+ aspect = widgetH / canvasH;
4577+ widgetH = widgetH / aspect;
4578+ widgetW = widgetH / aspect;
4579+ }
4580+ // equal
4581+ } else {
4582+ widgetW = canvasW;
4583+ widgetH = widgetW * 0.75;
4584+ }
4585+
4586+ // delta
4587+ dx = (canvasW - widgetW)/2;
4588+ dy = (canvasH - widgetH)/2;
4589+
4590+ // title
4591+ titleFontSize = ((widgetH / 8) > obj.config.titleMinFontSize) ? (widgetH / 10) : obj.config.titleMinFontSize;
4592+ titleX = dx + widgetW / 2;
4593+ titleY = dy + widgetH / 6.4;
4594+
4595+ // value
4596+ valueFontSize = ((widgetH / 6.5) > obj.config.valueMinFontSize) ? (widgetH / 6.5) : obj.config.valueMinFontSize;
4597+ valueX = dx + widgetW / 2;
4598+ valueY = dy + widgetH / 1.275;
4599+
4600+ // label
4601+ labelFontSize = ((widgetH / 16) > obj.config.labelMinFontSize) ? (widgetH / 16) : obj.config.labelMinFontSize;
4602+ labelX = dx + widgetW / 2;
4603+ labelY = valueY + valueFontSize / 2 + 5;
4604+
4605+ // min
4606+ minFontSize = ((widgetH / 16) > obj.config.minLabelMinFontSize) ? (widgetH / 16) : obj.config.minLabelMinFontSize;
4607+ minX = dx + (widgetW / 10) + (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
4608+ minY = labelY;
4609+
4610+ // max
4611+ maxFontSize = ((widgetH / 16) > obj.config.maxLabelMinFontSize) ? (widgetH / 16) : obj.config.maxLabelMinFontSize;
4612+ maxX = dx + widgetW - (widgetW / 10) - (widgetW / 6.666666666666667 * obj.config.gaugeWidthScale) / 2 ;
4613+ maxY = labelY;
4614+ }
4615+
4616+ // parameters
4617+ obj.params = {
4618+ canvasW : canvasW,
4619+ canvasH : canvasH,
4620+ widgetW : widgetW,
4621+ widgetH : widgetH,
4622+ dx : dx,
4623+ dy : dy,
4624+ titleFontSize : titleFontSize,
4625+ titleX : titleX,
4626+ titleY : titleY,
4627+ valueFontSize : valueFontSize,
4628+ valueX : valueX,
4629+ valueY : valueY,
4630+ labelFontSize : labelFontSize,
4631+ labelX : labelX,
4632+ labelY : labelY,
4633+ minFontSize : minFontSize,
4634+ minX : minX,
4635+ minY : minY,
4636+ maxFontSize : maxFontSize,
4637+ maxX : maxX,
4638+ maxY : maxY
4639+ };
4640+
4641+ // var clear
4642+ canvasW, canvasH, widgetW, widgetH, aspect, dx, dy, titleFontSize, titleX, titleY, valueFontSize, valueX, valueY, labelFontSize, labelX, labelY, minFontSize, minX, minY, maxFontSize, maxX, maxY = null
4643+
4644+ // pki - custom attribute for generating gauge paths
4645+ obj.canvas.customAttributes.pki = function (value, min, max, w, h, dx, dy, gws, donut) {
4646+
4647+ var alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path;
4648+
4649+ if (donut) {
4650+ alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
4651+ Ro = w / 2 - w / 7;
4652+ Ri = Ro - w / 6.666666666666667 * gws;
4653+
4654+ Cx = w / 2 + dx;
4655+ Cy = h / 1.95 + dy;
4656+
4657+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
4658+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
4659+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
4660+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
4661+
4662+ path += "M" + (Cx - Ri) + "," + Cy + " ";
4663+ path += "L" + (Cx - Ro) + "," + Cy + " ";
4664+ if (value > ((max - min) / 2)) {
4665+ path += "A" + Ro + "," + Ro + " 0 0 1 " + (Cx + Ro) + "," + Cy + " ";
4666+ }
4667+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
4668+ path += "L" + Xi + "," + Yi + " ";
4669+ if (value > ((max - min) / 2)) {
4670+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx + Ri) + "," + Cy + " ";
4671+ }
4672+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
4673+ path += "Z ";
4674+
4675+ return { path: path };
4676+
4677+ } else {
4678+ alpha = (1 - (value - min) / (max - min)) * Math.PI;
4679+ Ro = w / 2 - w / 10;
4680+ Ri = Ro - w / 6.666666666666667 * gws;
4681+
4682+ Cx = w / 2 + dx;
4683+ Cy = h / 1.25 + dy;
4684+
4685+ Xo = w / 2 + dx + Ro * Math.cos(alpha);
4686+ Yo = h - (h - Cy) + 0 - Ro * Math.sin(alpha);
4687+ Xi = w / 2 + dx + Ri * Math.cos(alpha);
4688+ Yi = h - (h - Cy) + 0 - Ri * Math.sin(alpha);
4689+
4690+ path += "M" + (Cx - Ri) + "," + Cy + " ";
4691+ path += "L" + (Cx - Ro) + "," + Cy + " ";
4692+ path += "A" + Ro + "," + Ro + " 0 0 1 " + Xo + "," + Yo + " ";
4693+ path += "L" + Xi + "," + Yi + " ";
4694+ path += "A" + Ri + "," + Ri + " 0 0 0 " + (Cx - Ri) + "," + Cy + " ";
4695+ path += "Z ";
4696+
4697+ return { path: path };
4698+ }
4699+
4700+ // var clear
4701+ alpha, Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi, path = null;
4702+ };
4703+
4704+ // gauge
4705+ obj.gauge = obj.canvas.path().attr({
4706+ "stroke": "none",
4707+ "fill": obj.config.gaugeColor,
4708+ pki: [
4709+ obj.config.max,
4710+ obj.config.min,
4711+ obj.config.max,
4712+ obj.params.widgetW,
4713+ obj.params.widgetH,
4714+ obj.params.dx,
4715+ obj.params.dy,
4716+ obj.config.gaugeWidthScale,
4717+ obj.config.donut
4718+ ]
4719+ });
4720+
4721+ // level
4722+ obj.level = obj.canvas.path().attr({
4723+ "stroke": "none",
4724+ "fill": getColor(obj.config.value, (obj.config.value - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors),
4725+ pki: [
4726+ obj.config.min,
4727+ obj.config.min,
4728+ obj.config.max,
4729+ obj.params.widgetW,
4730+ obj.params.widgetH,
4731+ obj.params.dx,
4732+ obj.params.dy,
4733+ obj.config.gaugeWidthScale,
4734+ obj.config.donut
4735+ ]
4736+ });
4737+ if(obj.config.donut) {
4738+ obj.level.transform("r" + obj.config.donutStartAngle + ", " + (obj.params.widgetW/2 + obj.params.dx) + ", " + (obj.params.widgetH/1.95 + obj.params.dy));
4739+ }
4740+
4741+ // title
4742+ obj.txtTitle = obj.canvas.text(obj.params.titleX, obj.params.titleY, obj.config.title);
4743+ obj.txtTitle.attr({
4744+ "font-size":obj.params.titleFontSize,
4745+ "font-weight":"bold",
4746+ "font-family":"Arial",
4747+ "fill":obj.config.titleFontColor,
4748+ "fill-opacity":"1"
4749+ });
4750+ setDy(obj.txtTitle, obj.params.titleFontSize, obj.params.titleY);
4751+
4752+ // value
4753+ obj.txtValue = obj.canvas.text(obj.params.valueX, obj.params.valueY, 0);
4754+ obj.txtValue.attr({
4755+ "font-size":obj.params.valueFontSize,
4756+ "font-weight":"bold",
4757+ "font-family":"Arial",
4758+ "fill":obj.config.valueFontColor,
4759+ "fill-opacity":"0"
4760+ });
4761+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
4762+
4763+ // label
4764+ obj.txtLabel = obj.canvas.text(obj.params.labelX, obj.params.labelY, obj.config.label);
4765+ obj.txtLabel.attr({
4766+ "font-size":obj.params.labelFontSize,
4767+ "font-weight":"normal",
4768+ "font-family":"Arial",
4769+ "fill":obj.config.labelFontColor,
4770+ "fill-opacity":"0"
4771+ });
4772+ setDy(obj.txtLabel, obj.params.labelFontSize, obj.params.labelY);
4773+
4774+ // min
4775+ obj.txtMinimum = obj.config.min;
4776+ if( obj.config.humanFriendly ) obj.txtMinimum = humanFriendlyNumber( obj.config.min, obj.config.humanFriendlyDecimal );
4777+ obj.txtMin = obj.canvas.text(obj.params.minX, obj.params.minY, obj.txtMinimum);
4778+ obj.txtMin.attr({
4779+ "font-size":obj.params.minFontSize,
4780+ "font-weight":"normal",
4781+ "font-family":"Arial",
4782+ "fill":obj.config.labelFontColor,
4783+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
4784+ });
4785+ setDy(obj.txtMin, obj.params.minFontSize, obj.params.minY);
4786+
4787+ // max
4788+ obj.txtMaximum = obj.config.max;
4789+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
4790+ obj.txtMax = obj.canvas.text(obj.params.maxX, obj.params.maxY, obj.txtMaximum);
4791+ obj.txtMax.attr({
4792+ "font-size":obj.params.maxFontSize,
4793+ "font-weight":"normal",
4794+ "font-family":"Arial",
4795+ "fill":obj.config.labelFontColor,
4796+ "fill-opacity": (obj.config.hideMinMax || obj.config.donut)? "0" : "1"
4797+ });
4798+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
4799+
4800+ var defs = obj.canvas.canvas.childNodes[1];
4801+ var svg = "http://www.w3.org/2000/svg";
4802+
4803+ if (ie < 9) {
4804+ onCreateElementNsReady(function() {
4805+ obj.generateShadow(svg, defs);
4806+ });
4807+ } else {
4808+ obj.generateShadow(svg, defs);
4809+ }
4810+
4811+ // var clear
4812+ defs, svg = null;
4813+
4814+ // set value to display
4815+ if(obj.config.textRenderer) {
4816+ obj.originalValue = obj.config.textRenderer(obj.originalValue);
4817+ } else if(obj.config.humanFriendly) {
4818+ obj.originalValue = humanFriendlyNumber( obj.originalValue, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
4819+ } else {
4820+ obj.originalValue = (obj.originalValue * 1).toFixed(obj.config.decimals) + obj.config.symbol;
4821+ }
4822+
4823+ if(obj.config.counter === true) {
4824+ //on each animation frame
4825+ eve.on("raphael.anim.frame." + (obj.level.id), function() {
4826+ var currentValue = obj.level.attr("pki");
4827+ if(obj.config.textRenderer) {
4828+ obj.txtValue.attr("text", obj.config.textRenderer(Math.floor(currentValue[0])));
4829+ } else if(obj.config.humanFriendly) {
4830+ obj.txtValue.attr("text", humanFriendlyNumber( Math.floor(currentValue[0]), obj.config.humanFriendlyDecimal ) + obj.config.symbol);
4831+ } else {
4832+ obj.txtValue.attr("text", (currentValue[0] * 1).toFixed(obj.config.decimals) + obj.config.symbol);
4833+ }
4834+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
4835+ currentValue = null;
4836+ });
4837+ //on animation end
4838+ eve.on("raphael.anim.finish." + (obj.level.id), function() {
4839+ obj.txtValue.attr({"text" : obj.originalValue});
4840+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
4841+ });
4842+ } else {
4843+ //on animation start
4844+ eve.on("raphael.anim.start." + (obj.level.id), function() {
4845+ obj.txtValue.attr({"text" : obj.originalValue});
4846+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
4847+ });
4848+ }
4849+
4850+ // animate gauge level, value & label
4851+ obj.level.animate({
4852+ pki: [
4853+ obj.config.value,
4854+ obj.config.min,
4855+ obj.config.max,
4856+ obj.params.widgetW,
4857+ obj.params.widgetH,
4858+ obj.params.dx,
4859+ obj.params.dy,
4860+ obj.config.gaugeWidthScale,
4861+ obj.config.donut
4862+ ]
4863+ }, obj.config.startAnimationTime, obj.config.startAnimationType);
4864+ obj.txtValue.animate({"fill-opacity":(obj.config.hideValue)?"0":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
4865+ obj.txtLabel.animate({"fill-opacity":"1"}, obj.config.startAnimationTime, obj.config.startAnimationType);
4866+};
4867+
4868+/** Refresh gauge level */
4869+JustGage.prototype.refresh = function(val, max) {
4870+
4871+ var obj = this;
4872+ var displayVal, color, max = max || null;
4873+
4874+ // set new max
4875+ if(max !== null) {
4876+ obj.config.max = max;
4877+
4878+ obj.txtMaximum = obj.config.max;
4879+ if( obj.config.humanFriendly ) obj.txtMaximum = humanFriendlyNumber( obj.config.max, obj.config.humanFriendlyDecimal );
4880+ obj.txtMax.attr({"text" : obj.txtMaximum});
4881+ setDy(obj.txtMax, obj.params.maxFontSize, obj.params.maxY);
4882+ }
4883+
4884+ // overflow values
4885+ displayVal = val;
4886+ if ((val * 1) > (obj.config.max * 1)) {val = (obj.config.max * 1);}
4887+ if ((val * 1) < (obj.config.min * 1)) {val = (obj.config.min * 1);}
4888+
4889+ color = getColor(val, (val - obj.config.min) / (obj.config.max - obj.config.min), obj.config.levelColors, obj.config.noGradient, obj.config.customSectors);
4890+
4891+ if(obj.config.textRenderer) {
4892+ displayVal = obj.config.textRenderer(displayVal);
4893+ } else if( obj.config.humanFriendly ) {
4894+ displayVal = humanFriendlyNumber( displayVal, obj.config.humanFriendlyDecimal ) + obj.config.symbol;
4895+ } else {
4896+ displayVal = (displayVal * 1).toFixed(obj.config.decimals) + obj.config.symbol;
4897+ }
4898+ obj.originalValue = displayVal;
4899+ obj.config.value = val * 1;
4900+
4901+ if(!obj.config.counter) {
4902+ obj.txtValue.attr({"text":displayVal});
4903+ setDy(obj.txtValue, obj.params.valueFontSize, obj.params.valueY);
4904+ }
4905+
4906+ obj.level.animate({
4907+ pki: [
4908+ obj.config.value,
4909+ obj.config.min,
4910+ obj.config.max,
4911+ obj.params.widgetW,
4912+ obj.params.widgetH,
4913+ obj.params.dx,
4914+ obj.params.dy,
4915+ obj.config.gaugeWidthScale,
4916+ obj.config.donut
4917+ ],
4918+ "fill":color
4919+ }, obj.config.refreshAnimationTime, obj.config.refreshAnimationType);
4920+
4921+ // var clear
4922+ obj, displayVal, color, max = null;
4923+};
4924+
4925+/** Generate shadow */
4926+JustGage.prototype.generateShadow = function(svg, defs) {
4927+
4928+ var obj = this;
4929+ var gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3;
4930+
4931+ // FILTER
4932+ gaussFilter = document.createElementNS(svg,"filter");
4933+ gaussFilter.setAttribute("id","inner-shadow");
4934+ defs.appendChild(gaussFilter);
4935+
4936+ // offset
4937+ feOffset = document.createElementNS(svg,"feOffset");
4938+ feOffset.setAttribute("dx", 0);
4939+ feOffset.setAttribute("dy", obj.config.shadowVerticalOffset);
4940+ gaussFilter.appendChild(feOffset);
4941+
4942+ // blur
4943+ feGaussianBlur = document.createElementNS(svg,"feGaussianBlur");
4944+ feGaussianBlur.setAttribute("result","offset-blur");
4945+ feGaussianBlur.setAttribute("stdDeviation", obj.config.shadowSize);
4946+ gaussFilter.appendChild(feGaussianBlur);
4947+
4948+ // composite 1
4949+ feComposite1 = document.createElementNS(svg,"feComposite");
4950+ feComposite1.setAttribute("operator","out");
4951+ feComposite1.setAttribute("in", "SourceGraphic");
4952+ feComposite1.setAttribute("in2","offset-blur");
4953+ feComposite1.setAttribute("result","inverse");
4954+ gaussFilter.appendChild(feComposite1);
4955+
4956+ // flood
4957+ feFlood = document.createElementNS(svg,"feFlood");
4958+ feFlood.setAttribute("flood-color","black");
4959+ feFlood.setAttribute("flood-opacity", obj.config.shadowOpacity);
4960+ feFlood.setAttribute("result","color");
4961+ gaussFilter.appendChild(feFlood);
4962+
4963+ // composite 2
4964+ feComposite2 = document.createElementNS(svg,"feComposite");
4965+ feComposite2.setAttribute("operator","in");
4966+ feComposite2.setAttribute("in", "color");
4967+ feComposite2.setAttribute("in2","inverse");
4968+ feComposite2.setAttribute("result","shadow");
4969+ gaussFilter.appendChild(feComposite2);
4970+
4971+ // composite 3
4972+ feComposite3 = document.createElementNS(svg,"feComposite");
4973+ feComposite3.setAttribute("operator","over");
4974+ feComposite3.setAttribute("in", "shadow");
4975+ feComposite3.setAttribute("in2","SourceGraphic");
4976+ gaussFilter.appendChild(feComposite3);
4977+
4978+ // set shadow
4979+ if (!obj.config.hideInnerShadow) {
4980+ obj.canvas.canvas.childNodes[2].setAttribute("filter", "url(#inner-shadow)");
4981+ obj.canvas.canvas.childNodes[3].setAttribute("filter", "url(#inner-shadow)");
4982+ }
4983+
4984+ // var clear
4985+ gaussFilter, feOffset, feGaussianBlur, feComposite1, feFlood, feComposite2, feComposite3 = null;
4986+
4987+};
4988+
4989+/** Get color for value */
4990+function getColor(val, pct, col, noGradient, custSec) {
4991+
4992+ var no, inc, colors, percentage, rval, gval, bval, lower, upper, range, rangePct, pctLower, pctUpper, color;
4993+ var noGradient = noGradient || custSec.length > 0;
4994+
4995+ if(custSec.length > 0) {
4996+ for(var i = 0; i < custSec.length; i++) {
4997+ if(val > custSec[i].lo && val <= custSec[i].hi) {
4998+ return custSec[i].color;
4999+ }
5000+ }
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: