[REF] gamification: split goal and rename views

bzr revid: mat@openerp.com-20130228135329-uxvvewraskm3jlfi
This commit is contained in:
Martin Trigaux 2013-02-28 14:53:29 +01:00
parent 81d917d1a5
commit 4928357194
8 changed files with 576 additions and 596 deletions

View File

@ -1,2 +1,3 @@
import goal
import plan
import res_users

View File

@ -27,10 +27,8 @@
'description': """Gamification of goals""",
'data': [
'view/type.xml',
'view/goal.xml',
'view/plan.xml',
'view/menu.xml',
'goal_view.xml',
'plan_view.xml',
'cron.xml',
],
'installable': True,

View File

@ -22,78 +22,9 @@
from openerp.osv import fields, osv
from openerp.tools.safe_eval import safe_eval
from templates import TemplateHelper
from datetime import date, datetime, timedelta
from mako.template import Template as MakoTemplate
from urllib import urlencode, quote as quote
import calendar
import itertools
import logging
import os.path
_logger = logging.getLogger(__name__)
GAMIFICATION_PATH = os.path.dirname(os.path.abspath(__file__))
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
# arbitrary Python statements are not accepted, and not all expressions are
# allowed: only "public" attributes (not starting with '_') of objects may
# be accessed.
# This is done on purpose: it prevents incidental or malicious execution of
# Python code that may break the security of the server.
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import FileSystemLoader
mako_template_env = SandboxedEnvironment(
loader=FileSystemLoader(os.path.join(GAMIFICATION_PATH,'templates/')),
block_start_string="<%",
block_end_string="%>",
variable_start_string="${",
variable_end_string="}",
comment_start_string="<%doc>",
comment_end_string="</%doc>",
line_statement_prefix="%",
line_comment_prefix="##",
trim_blocks=True, # do not output newline after blocks
autoescape=True, # XML/HTML automatic escaping
)
mako_template_env.globals.update({
'str': str,
'quote': quote,
'urlencode': urlencode,
})
except ImportError:
_logger.warning("jinja2 not available, templating features will not work!")
def start_end_date_for_period(period):
"""Return the start and end date for a goal period based on today
:return (start_date, end_date), datetime.date objects, False if the period is
not defined or unknown"""
today = date.today()
if period == 'daily':
start_date = today
end_date = start_date # ? + timedelta(days=1)
elif period == 'weekly':
delta = timedelta(days=today.weekday())
start_date = today - delta
end_date = start_date + timedelta(days=7)
elif period == 'monthly':
month_range = calendar.monthrange(today.year, today.month)
start_date = today.replace(day=month_range[0])
end_date = today.replace(day=month_range[1])
elif period == 'yearly':
start_date = today.replace(month=1, day=1)
end_date = today.replace(month=12, day=31)
else: # period == 'once':
start_date = False # for manual goal, start each time
end_date = False
return (start_date, end_date)
class gamification_goal_type(osv.Model):
"""Goal type definition
@ -264,7 +195,8 @@ class gamification_goal(osv.Model):
towrite['state'] = 'inprogress_update'
# generate a remind report
body_html = mako_template_env.get_template('reminder.mako').render({'object':goal})
template_env = TemplateHelper()
body_html = template_env.get_template('reminder.mako').render({'object':goal})
self.message_post(cr, uid, goal.id,
body=body_html,
partner_ids=[goal.user_id.partner_id.id],
@ -340,428 +272,3 @@ class gamification_goal(osv.Model):
plan_obj.report_progress(cr, uid, [goal.planline_id.plan_id.id], users=[goal.user_id], context=context)
return super(gamification_goal, self).write(cr, uid, ids, vals, context=context)
class gamification_goal_plan(osv.Model):
"""Gamification goal plan
Set of predifined goals to be able to automate goal settings or
quickly apply several goals manually to a group of users
If 'user_ids' is defined and 'period' is different than 'one', the set will
be assigned to the users for each period (eg: every 1st of each month if
'monthly' is selected)
"""
_name = 'gamification.goal.plan'
_description = 'Gamification goal plan'
_inherit = 'mail.thread'
_columns = {
'name' : fields.char('Plan Name', required=True),
'user_ids' : fields.many2many('res.users',
string='Users',
help="list of users to which the goal will be set"),
'planline_ids' : fields.one2many('gamification.goal.planline',
'plan_id',
string='Planline',
help="list of goals that will be set",
required=True),
'autojoin_group_id' : fields.many2one('res.groups',
string='Auto-join Group',
help='Group of users whose members will automatically be added to the users'),
'period' : fields.selection([
('once', 'No Periodicity'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')
],
string='Periodicity',
help='Period of automatic goal assigment, will be done manually if none is selected',
required=True),
'state': fields.selection([
('draft', 'Draft'),
('inprogress', 'In progress'),
('done', 'Done'),
],
string='State',
required=True),
'visibility_mode':fields.selection([
('board','Leader board'),
('progressbar','Personal progressbar')
],
string="Visibility",
help='How are displayed the results, shared or in a single progressbar',
required=True),
'report_message_frequency':fields.selection([
('never','Never'),
('onchange','On change'),
('daily','Daily'),
('weekly','Weekly'),
('monthly','Monthly'),
('yearly', 'Yearly')
],
string="Frequency",
required=True),
'report_message_group_id' : fields.many2one('mail.group',
string='Send a copy to',
help='Group that will receive a copy of the report in addition to the user'),
'report_header' : fields.text('Report Header'),
'remind_update_delay' : fields.integer('Remind delay',
help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified.")
}
_defaults = {
'period': 'once',
'state': 'draft',
'visibility_mode' : 'progressbar',
'report_message_frequency' : 'onchange',
}
def _check_nonzero_planline(self, cr, uid, ids, context=None):
"""checks that there is at least one planline set"""
for plan in self.browse(cr, uid, ids, context):
if len(plan.planline_ids) < 1:
return False
return True
def _check_nonzero_users(self, cr, uid, ids, context=None):
"""checks that there is at least one user set"""
for plan in self.browse(cr, uid, ids, context):
if len(plan.user_ids) < 1 and plan.state != 'draft':
return False
return True
_constraints = [
(_check_nonzero_planline, "At least one planline is required to create a goal plan", ['planline_ids']),
(_check_nonzero_users, "At least one user is required to create a non-draft goal plan", ['user_ids']),
]
def write(self, cr, uid, ids, vals, context=None):
"""Overwrite the write method to add the user of groups"""
write_res = super(gamification_goal_plan, self).write(cr, uid, ids, vals, context=context)
# add users when change the group auto-subscription
if 'autojoin_group_id' in vals:
new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
self.plan_subscribe_users(cr, uid, ids, [user.id for user in new_group.users], context=context)
return write_res
def _update_all(self, cr, uid, ids=False, context=None):
"""Update the plans
Create the goals for planlines not linked to goals (eg: modified the
plan to add planlines)
:param list(int) ids: the ids of the plans to update, if False will
update every goal in progress"""
if not ids:
ids = self.search(cr, uid, [('state', '=', 'inprogress')])
goal_obj = self.pool.get('gamification.goal')
planline_obj = self.pool.get('gamification.goal.planline')
self.generate_goals_from_plan(cr, uid, ids, context=context)
for plan in self.browse(cr, uid, ids, context):
for planline in plan.planline_ids:
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)] , context=context)
goal_obj.update(cr, uid, goal_ids, context=context)
# useless, goals removed in cascade
# current_planlines = [planline.id for planline in plan.planline_ids]
# print(current_planlines)
# related_planlines = planline_obj.search(cr, uid, [('plan_id','=',plan.id)])
# print(related_planlines)
# # the list of planlines linked to the plan but not in plan.planline_ids
# excluded_planlines = [plid for plid in related_planlines if plid not in current_planlines ]
# print(excluded_planlines)
# excluded_goals = goal_obj.search(cr, uid, [('planline_id', 'in', excluded_planlines)], context=context)
# print(excluded_goals)
# goal_obj.write(cr, uid, excluded_goals, {'state': 'canceled'}, context=context)
def action_start(self, cr, uid, ids, context=None):
"""Start a draft goal plan
Change the state of the plan to in progress"""
self.generate_goals_from_plan(cr, uid, ids, context=context)
return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
def action_check(self, cr, uid, ids, context=None):
"""Check a goal plan
Create goals that haven't been created yet (eg: if added users of planlines)
Recompute the current value for each goal related"""
return self._update_all(cr, uid, ids, context=context)
def action_close(self, cr, uid, ids, context=None):
"""Close a plan in progress
Change the state of the plan to in done
Does NOT close the related goals, this is handled by the goal itself"""
return self.write(cr, uid, ids, {'state': 'done'}, context=context)
def action_reset(self, cr, uid, ids, context=None):
"""Reset a closed goal plan
Change the state of the plan to in progress
Closing a plan does not affect the goals so neither does reset"""
return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
def action_cancel(self, cr, uid, ids, context=None):
"""Cancel a plan in progress
Change the state of the plan to draft
Cancel the related goals"""
self.write(cr, uid, ids, {'state': 'draft'}, context=context)
for plan in self.browse(cr, uid, ids, context):
for planline in plan.planline_ids:
goal_obj = self.pool.get('gamification.goal')
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)], context=context)
goal_obj.write(cr, uid, goal_ids, {'state': 'canceled'}, context=context)
return True
def action_show_related_goals(self, cr, uid, ids, context=None):
""" This opens goal view with a restriction to the list of goals from this plan only
@return: the goal view
"""
# get ids of related goals
goal_obj = self.pool.get('gamification.goal')
related_goal_ids = []
for plan in self.browse(cr, uid, ids, context=context):
for planline in plan.planline_ids:
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)], context=context)
related_goal_ids.extend(goal_ids)
# process the new view
if context is None:
context = {}
res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'gamification','goals_from_plan_act', context=context)
res['context'] = context
res['context'].update({
'default_id': related_goal_ids
})
res['domain'] = [('id','in', related_goal_ids)]
return res
def generate_goals_from_plan(self, cr, uid, ids, context=None):
"""Generate the lsit of goals fron a plan"""
for plan in self.browse(cr, uid, ids, context):
(start_date, end_date) = start_end_date_for_period(plan.period)
for planline in plan.planline_ids:
for user in plan.user_ids:
#self.create_goal_from_plan(cr, uid, ids, planline.id, user.id, start_date, context=context)
goal_obj = self.pool.get('gamification.goal')
domain = [('planline_id', '=', planline.id),
('user_id', '=', user.id)]
if start_date:
domain.append(('start_date', '=', start_date))
# goal existing for this planline ?
if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
# resume canceled goals
domain.append(('state', '=', 'canceled'))
canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
goal_obj.update(cr, uid, canceled_goal_ids, context=context)
# skip to next user
continue
values = {
'type_id':planline.type_id.id,
'planline_id':planline.id,
'user_id':user.id,
'target_goal':planline.target_goal,
'state':'inprogress',
}
if start_date:
values['start_date'] = start_date.isoformat()
if end_date:
values['end_date'] = end_date.isoformat()
if planline.plan_id.remind_update_delay:
values['remind_update_delay'] = planline.plan_id.remind_update_delay
new_goal_id = goal_obj.create(cr, uid, values, context)
goal_obj.update(cr, uid, [new_goal_id], context=context)
return True
def plan_subscribe_users(self, cr, uid, ids, new_user_ids, context=None):
""" Add the following users to plans
:param ids: ids of plans to which the users will be added
:param user_ids: ids of the users to add"""
for plan in self.browse(cr,uid, ids, context):
subscription = [user.id for user in plan.user_ids]
subscription.extend(new_user_ids)
unified_subscription = list(set(subscription))
self.write(cr, uid, ids, {'user_ids': [(4, uid) for uid in unified_subscription]}, context=context)
return True
def report_progress(self, cr, uid, ids, users=False, context=None):
"""Post report about the progress of the goals
:param list(int) ids: the list of plan ids that need to be reported
:param list(res.users) users: the list of users that are concerned by
the report. If False, will send the report to every user concerned
(goal users and group that recieves a copy). Only used for plan with
a visibility mode set to 'personal'."""
context = context or {}
goal_obj = self.pool.get('gamification.goal')
for plan in self.browse(cr, uid, ids, context=context):
if not plan.report_message_group_id:
# no report group, skipping
continue
if plan.visibility_mode == 'board':
# generate a shared report
planlines_boards = []
for planline in plan.planline_ids:
(start_date, end_date) = start_end_date_for_period(plan.period)
domain = [
('planline_id', '=', planline.id),
('state', 'in', ('inprogress', 'inprogress_update',
'reached', 'failed')),
]
if start_date:
domain.append(('start_date', '=', start_date.isoformat()))
board_goals = []
goal_ids = goal_obj.search(cr, uid, domain, context=context)
for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
board_goals.append({
'user': goal.user_id,
'current':goal.current,
'target_goal':goal.target_goal,
'completeness':goal.completeness,
})
# most complete first, current if same percentage (eg: if several 100%)
sorted_board = enumerate(sorted(board_goals, key=lambda k: (k['completeness'], k['current']), reverse=True))
planlines_boards.append({'goal_type':planline.type_id.name, 'board_goals':sorted_board})
body_html = mako_template_env.get_template('group_progress.mako').render({'object':plan, 'planlines_boards':planlines_boards})
self.message_post(cr, uid, plan.id,
body=body_html,
partner_ids=[user.partner_id.id for user in plan.user_ids],
context=context,
subtype='mail.mt_comment')
self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
else:
# generate individual reports
for user in users or plan.user_ids:
goal_ids = self.get_current_related_goals(cr, uid, plan.id, user.id, context=context)
if len(goal_ids) == 0:
continue
variables = {
'object':plan,
'user':user,
'goals':goal_obj.browse(cr, uid, goal_ids, context=context)
}
body_html = mako_template_env.get_template('personal_progress.mako').render(variables)
self.message_post(cr, uid, plan.id,
body=body_html,
partner_ids=[user.partner_id.id],
context=context,
subtype='mail.mt_comment')
self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
return True
def get_current_related_goals(self, cr, uid, plan_id, user_id, context=None):
"""Get the ids of goals linked to a plan for the current instance
If several goals are linked to the same planline and user, only the
latest instance of the plan is checked (eg: if the plan is monthly,
return the goals started the 1st of this month).
"""
plan = self.browse(cr, uid, plan_id, context=context)
(start_date, end_date) = start_end_date_for_period(plan.period)
goal_obj = self.pool.get('gamification.goal')
related_goal_ids = []
for planline in plan.planline_ids:
domain = [('planline_id', '=', planline.id),
('user_id', '=', user_id),
('state','in',('inprogress','inprogress_update','reached'))]
if start_date:
domain.append(('start_date', '=', start_date.isoformat()))
goal_ids = goal_obj.search(cr, uid, domain, context=context)
related_goal_ids.extend(goal_ids)
return related_goal_ids
class gamification_goal_planline(osv.Model):
"""Gamification goal planline
Predifined goal for 'gamification_goal_plan'
These are generic list of goals with only the target goal defined
Should only be created for the gamification_goal_plan object
"""
_name = 'gamification.goal.planline'
_description = 'Gamification generic goal for plan'
_order = "sequence_type"
def _get_planline_types(self, cr, uid, ids, context=None):
"""Return the ids of planline items related to the gamification.goal.type
objects in 'ids (used to update the value of 'sequence_type')'"""
result = {}
for goal_type in self.pool.get('gamification.goal.type').browse(cr, uid, ids, context=context):
domain = [('type_id', '=', goal_type.id)]
planline_ids = self.pool.get('gamification.goal.planline').search(cr, uid, domain, context=context)
for p_id in planline_ids:
result[p_id] = True
return result.keys()
_columns = {
'plan_id' : fields.many2one('gamification.goal.plan',
string='Plan',
ondelete="cascade"),
'type_id' : fields.many2one('gamification.goal.type',
string='Goal Type',
required=True,
ondelete="cascade"),
'target_goal' : fields.float('Target Value to Reach',
required=True),
'sequence_type' : fields.related('type_id','sequence',
type='integer',
string='Sequence',
readonly=True,
store={
'gamification.goal.type': (_get_planline_types, ['sequence'], 10),
}),
}

View File

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration"/>
<!-- Goals -->
<menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
<!-- Goal plans -->
<menuitem id="gamification_plan_menu" parent="gamification_menu" action="goal_plan_list_action" sequence="10"/>
<!-- Goal types -->
<menuitem id="gamification_type_menu" parent="gamification_menu" action="goal_type_list_action" sequence="20"/>
<!-- Goal views -->
<record id="goal_list_action" model="ir.actions.act_window">
<field name="name">Goals</field>
<field name="res_model">gamification.goal</field>
@ -123,5 +133,84 @@
</field>
</record>
<!-- Goal types view -->
<record id="goal_type_list_action" model="ir.actions.act_window">
<field name="name">Goal Types</field>
<field name="res_model">gamification.goal.type</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal type.
</p>
<p>
A goal type is a technical model of goal defining a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_type_list_view" model="ir.ui.view">
<field name="name">Goal Types List</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<tree string="Goal types">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_type_form_view" model="ir.ui.view">
<field name="name">Goal Types Form</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<form string="Goal types" version="7.0">
<sheet>
<h1>
<div class="oe_edit_only">
<label for="name"/>
</div>
<field name="name"/>
</h1>
<group>
<group string="General">
<field name="description"/>
<field name="condition"/>
</group>
<group string="Computation">
<field name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" attrs="{'invisible':[('computation_mode','=','manually')], 'required':[('computation_mode','!=','manually')]}"/>
<field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" />
<field name="field_date_id" attrs="{'invisible':[('computation_mode','=','manually')]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]"/>
<field name="domain" attrs="{'invisible':[('computation_mode','=','manually')], 'required':[('computation_mode','!=','manually')]}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_type_search_view" model="ir.ui.view">
<field name="name">Goal Type Search</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<search string="Search Goal Types">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By...">
<filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</search>
</field>
</record>
</data>
</openerp>

480
addons/gamification/plan.py Normal file
View File

@ -0,0 +1,480 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import fields, osv
from templates import TemplateHelper
from datetime import date, datetime, timedelta
import calendar
def start_end_date_for_period(period):
"""Return the start and end date for a goal period based on today
:return (start_date, end_date), datetime.date objects, False if the period is
not defined or unknown"""
today = date.today()
if period == 'daily':
start_date = today
end_date = start_date # ? + timedelta(days=1)
elif period == 'weekly':
delta = timedelta(days=today.weekday())
start_date = today - delta
end_date = start_date + timedelta(days=7)
elif period == 'monthly':
month_range = calendar.monthrange(today.year, today.month)
start_date = today.replace(day=month_range[0])
end_date = today.replace(day=month_range[1])
elif period == 'yearly':
start_date = today.replace(month=1, day=1)
end_date = today.replace(month=12, day=31)
else: # period == 'once':
start_date = False # for manual goal, start each time
end_date = False
return (start_date, end_date)
class gamification_goal_plan(osv.Model):
"""Gamification goal plan
Set of predifined goals to be able to automate goal settings or
quickly apply several goals manually to a group of users
If 'user_ids' is defined and 'period' is different than 'one', the set will
be assigned to the users for each period (eg: every 1st of each month if
'monthly' is selected)
"""
_name = 'gamification.goal.plan'
_description = 'Gamification goal plan'
_inherit = 'mail.thread'
_columns = {
'name' : fields.char('Plan Name', required=True),
'user_ids' : fields.many2many('res.users',
string='Users',
help="list of users to which the goal will be set"),
'planline_ids' : fields.one2many('gamification.goal.planline',
'plan_id',
string='Planline',
help="list of goals that will be set",
required=True),
'autojoin_group_id' : fields.many2one('res.groups',
string='Auto-join Group',
help='Group of users whose members will automatically be added to the users'),
'period' : fields.selection([
('once', 'No Periodicity'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly')
],
string='Periodicity',
help='Period of automatic goal assigment, will be done manually if none is selected',
required=True),
'state': fields.selection([
('draft', 'Draft'),
('inprogress', 'In progress'),
('done', 'Done'),
],
string='State',
required=True),
'visibility_mode':fields.selection([
('board','Leader board'),
('progressbar','Personal progressbar')
],
string="Visibility",
help='How are displayed the results, shared or in a single progressbar',
required=True),
'report_message_frequency':fields.selection([
('never','Never'),
('onchange','On change'),
('daily','Daily'),
('weekly','Weekly'),
('monthly','Monthly'),
('yearly', 'Yearly')
],
string="Frequency",
required=True),
'report_message_group_id' : fields.many2one('mail.group',
string='Send a copy to',
help='Group that will receive a copy of the report in addition to the user'),
'report_header' : fields.text('Report Header'),
'remind_update_delay' : fields.integer('Remind delay',
help="The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified.")
}
_defaults = {
'period': 'once',
'state': 'draft',
'visibility_mode' : 'progressbar',
'report_message_frequency' : 'onchange',
}
def _check_nonzero_planline(self, cr, uid, ids, context=None):
"""checks that there is at least one planline set"""
for plan in self.browse(cr, uid, ids, context):
if len(plan.planline_ids) < 1:
return False
return True
def _check_nonzero_users(self, cr, uid, ids, context=None):
"""checks that there is at least one user set"""
for plan in self.browse(cr, uid, ids, context):
if len(plan.user_ids) < 1 and plan.state != 'draft':
return False
return True
_constraints = [
(_check_nonzero_planline, "At least one planline is required to create a goal plan", ['planline_ids']),
(_check_nonzero_users, "At least one user is required to create a non-draft goal plan", ['user_ids']),
]
def write(self, cr, uid, ids, vals, context=None):
"""Overwrite the write method to add the user of groups"""
write_res = super(gamification_goal_plan, self).write(cr, uid, ids, vals, context=context)
# add users when change the group auto-subscription
if 'autojoin_group_id' in vals:
new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context)
self.plan_subscribe_users(cr, uid, ids, [user.id for user in new_group.users], context=context)
return write_res
def _update_all(self, cr, uid, ids=False, context=None):
"""Update the plans
Create the goals for planlines not linked to goals (eg: modified the
plan to add planlines)
:param list(int) ids: the ids of the plans to update, if False will
update every goal in progress"""
if not ids:
ids = self.search(cr, uid, [('state', '=', 'inprogress')])
goal_obj = self.pool.get('gamification.goal')
planline_obj = self.pool.get('gamification.goal.planline')
self.generate_goals_from_plan(cr, uid, ids, context=context)
for plan in self.browse(cr, uid, ids, context):
for planline in plan.planline_ids:
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)] , context=context)
goal_obj.update(cr, uid, goal_ids, context=context)
# useless, goals removed in cascade
# current_planlines = [planline.id for planline in plan.planline_ids]
# print(current_planlines)
# related_planlines = planline_obj.search(cr, uid, [('plan_id','=',plan.id)])
# print(related_planlines)
# # the list of planlines linked to the plan but not in plan.planline_ids
# excluded_planlines = [plid for plid in related_planlines if plid not in current_planlines ]
# print(excluded_planlines)
# excluded_goals = goal_obj.search(cr, uid, [('planline_id', 'in', excluded_planlines)], context=context)
# print(excluded_goals)
# goal_obj.write(cr, uid, excluded_goals, {'state': 'canceled'}, context=context)
def action_start(self, cr, uid, ids, context=None):
"""Start a draft goal plan
Change the state of the plan to in progress"""
self.generate_goals_from_plan(cr, uid, ids, context=context)
return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
def action_check(self, cr, uid, ids, context=None):
"""Check a goal plan
Create goals that haven't been created yet (eg: if added users of planlines)
Recompute the current value for each goal related"""
return self._update_all(cr, uid, ids, context=context)
def action_close(self, cr, uid, ids, context=None):
"""Close a plan in progress
Change the state of the plan to in done
Does NOT close the related goals, this is handled by the goal itself"""
return self.write(cr, uid, ids, {'state': 'done'}, context=context)
def action_reset(self, cr, uid, ids, context=None):
"""Reset a closed goal plan
Change the state of the plan to in progress
Closing a plan does not affect the goals so neither does reset"""
return self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
def action_cancel(self, cr, uid, ids, context=None):
"""Cancel a plan in progress
Change the state of the plan to draft
Cancel the related goals"""
self.write(cr, uid, ids, {'state': 'draft'}, context=context)
for plan in self.browse(cr, uid, ids, context):
for planline in plan.planline_ids:
goal_obj = self.pool.get('gamification.goal')
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)], context=context)
goal_obj.write(cr, uid, goal_ids, {'state': 'canceled'}, context=context)
return True
def action_show_related_goals(self, cr, uid, ids, context=None):
""" This opens goal view with a restriction to the list of goals from this plan only
@return: the goal view
"""
# get ids of related goals
goal_obj = self.pool.get('gamification.goal')
related_goal_ids = []
for plan in self.browse(cr, uid, ids, context=context):
for planline in plan.planline_ids:
goal_ids = goal_obj.search(cr, uid, [('planline_id', '=', planline.id)], context=context)
related_goal_ids.extend(goal_ids)
# process the new view
if context is None:
context = {}
res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid ,'gamification','goals_from_plan_act', context=context)
res['context'] = context
res['context'].update({
'default_id': related_goal_ids
})
res['domain'] = [('id','in', related_goal_ids)]
return res
def generate_goals_from_plan(self, cr, uid, ids, context=None):
"""Generate the lsit of goals fron a plan"""
for plan in self.browse(cr, uid, ids, context):
(start_date, end_date) = start_end_date_for_period(plan.period)
for planline in plan.planline_ids:
for user in plan.user_ids:
#self.create_goal_from_plan(cr, uid, ids, planline.id, user.id, start_date, context=context)
goal_obj = self.pool.get('gamification.goal')
domain = [('planline_id', '=', planline.id),
('user_id', '=', user.id)]
if start_date:
domain.append(('start_date', '=', start_date))
# goal existing for this planline ?
if len(goal_obj.search(cr, uid, domain, context=context)) > 0:
# resume canceled goals
domain.append(('state', '=', 'canceled'))
canceled_goal_ids = goal_obj.search(cr, uid, domain, context=context)
goal_obj.write(cr, uid, canceled_goal_ids, {'state': 'inprogress'}, context=context)
goal_obj.update(cr, uid, canceled_goal_ids, context=context)
# skip to next user
continue
values = {
'type_id':planline.type_id.id,
'planline_id':planline.id,
'user_id':user.id,
'target_goal':planline.target_goal,
'state':'inprogress',
}
if start_date:
values['start_date'] = start_date.isoformat()
if end_date:
values['end_date'] = end_date.isoformat()
if planline.plan_id.remind_update_delay:
values['remind_update_delay'] = planline.plan_id.remind_update_delay
new_goal_id = goal_obj.create(cr, uid, values, context)
goal_obj.update(cr, uid, [new_goal_id], context=context)
return True
def plan_subscribe_users(self, cr, uid, ids, new_user_ids, context=None):
""" Add the following users to plans
:param ids: ids of plans to which the users will be added
:param user_ids: ids of the users to add"""
for plan in self.browse(cr,uid, ids, context):
subscription = [user.id for user in plan.user_ids]
subscription.extend(new_user_ids)
unified_subscription = list(set(subscription))
self.write(cr, uid, ids, {'user_ids': [(4, uid) for uid in unified_subscription]}, context=context)
return True
def report_progress(self, cr, uid, ids, context=None, users=False):
"""Post report about the progress of the goals
:param list(int) ids: the list of plan ids that need to be reported
:param list(res.users) users: the list of users that are concerned by
the report. If False, will send the report to every user concerned
(goal users and group that recieves a copy). Only used for plan with
a visibility mode set to 'personal'."""
context = context or {}
goal_obj = self.pool.get('gamification.goal')
template_env = TemplateHelper()
for plan in self.browse(cr, uid, ids, context=context):
if not plan.report_message_group_id:
# no report group, skipping
continue
if plan.visibility_mode == 'board':
# generate a shared report
planlines_boards = []
for planline in plan.planline_ids:
(start_date, end_date) = start_end_date_for_period(plan.period)
domain = [
('planline_id', '=', planline.id),
('state', 'in', ('inprogress', 'inprogress_update',
'reached', 'failed')),
]
if start_date:
domain.append(('start_date', '=', start_date.isoformat()))
board_goals = []
goal_ids = goal_obj.search(cr, uid, domain, context=context)
for goal in goal_obj.browse(cr, uid, goal_ids, context=context):
board_goals.append({
'user': goal.user_id,
'current':goal.current,
'target_goal':goal.target_goal,
'completeness':goal.completeness,
})
# most complete first, current if same percentage (eg: if several 100%)
sorted_board = enumerate(sorted(board_goals, key=lambda k: (k['completeness'], k['current']), reverse=True))
planlines_boards.append({'goal_type':planline.type_id.name, 'board_goals':sorted_board})
body_html = template_env.get_template('group_progress.mako').render({'object':plan, 'planlines_boards':planlines_boards})
self.message_post(cr, uid, plan.id,
body=body_html,
partner_ids=[user.partner_id.id for user in plan.user_ids],
context=context,
subtype='mail.mt_comment')
self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
else:
# generate individual reports
for user in users or plan.user_ids:
goal_ids = self.get_current_related_goals(cr, uid, plan.id, user.id, context=context)
if len(goal_ids) == 0:
continue
variables = {
'object':plan,
'user':user,
'goals':goal_obj.browse(cr, uid, goal_ids, context=context)
}
body_html = template_env.get_template('personal_progress.mako').render(variables)
self.message_post(cr, uid, plan.id,
body=body_html,
partner_ids=[user.partner_id.id],
context=context,
subtype='mail.mt_comment')
self.pool.get('mail.group').message_post(cr, uid, plan.report_message_group_id.id,
body=body_html,
context=context,
subtype='mail.mt_comment')
return True
def get_current_related_goals(self, cr, uid, plan_id, user_id, context=None):
"""Get the ids of goals linked to a plan for the current instance
If several goals are linked to the same planline and user, only the
latest instance of the plan is checked (eg: if the plan is monthly,
return the goals started the 1st of this month).
"""
plan = self.browse(cr, uid, plan_id, context=context)
(start_date, end_date) = start_end_date_for_period(plan.period)
goal_obj = self.pool.get('gamification.goal')
related_goal_ids = []
for planline in plan.planline_ids:
domain = [('planline_id', '=', planline.id),
('user_id', '=', user_id),
('state','in',('inprogress','inprogress_update','reached'))]
if start_date:
domain.append(('start_date', '=', start_date.isoformat()))
goal_ids = goal_obj.search(cr, uid, domain, context=context)
related_goal_ids.extend(goal_ids)
return related_goal_ids
class gamification_goal_planline(osv.Model):
"""Gamification goal planline
Predifined goal for 'gamification_goal_plan'
These are generic list of goals with only the target goal defined
Should only be created for the gamification_goal_plan object
"""
_name = 'gamification.goal.planline'
_description = 'Gamification generic goal for plan'
_order = "sequence_type"
def _get_planline_types(self, cr, uid, ids, context=None):
"""Return the ids of planline items related to the gamification.goal.type
objects in 'ids (used to update the value of 'sequence_type')'"""
result = {}
for goal_type in self.pool.get('gamification.goal.type').browse(cr, uid, ids, context=context):
domain = [('type_id', '=', goal_type.id)]
planline_ids = self.pool.get('gamification.goal.planline').search(cr, uid, domain, context=context)
for p_id in planline_ids:
result[p_id] = True
return result.keys()
_columns = {
'plan_id' : fields.many2one('gamification.goal.plan',
string='Plan',
ondelete="cascade"),
'type_id' : fields.many2one('gamification.goal.type',
string='Goal Type',
required=True,
ondelete="cascade"),
'target_goal' : fields.float('Target Value to Reach',
required=True),
'sequence_type' : fields.related('type_id','sequence',
type='integer',
string='Sequence',
readonly=True,
store={
'gamification.goal.type': (_get_planline_types, ['sequence'], 10),
}),
}

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- <menuitem id="root_menu" name="Gamification"/> -->
<menuitem id="gamification_menu" name="Gamification Tools" parent="base.menu_administration"/>
<!-- Goals -->
<menuitem id="gamification_goal_menu" parent="gamification_menu" action="goal_list_action" sequence="0"/>
<!-- Goal plans -->
<menuitem id="gamification_plan_menu" parent="gamification_menu" action="goal_plan_list_action" sequence="10"/>
<!-- Goal types -->
<menuitem id="gamification_type_menu" parent="gamification_menu" action="goal_type_list_action" sequence="20"/>
</data>
</openerp>

View File

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="goal_type_list_action" model="ir.actions.act_window">
<field name="name">Goal Types</field>
<field name="res_model">gamification.goal.type</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a goal type.
</p>
<p>
A goal type is a technical model of goal defining a condition to reach.
The dates, values to reach or users are defined in goal instance.
</p>
</field>
</record>
<record id="goal_type_list_view" model="ir.ui.view">
<field name="name">Goal Types List</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<tree string="Goal types">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="computation_mode"/>
</tree>
</field>
</record>
<record id="goal_type_form_view" model="ir.ui.view">
<field name="name">Goal Types Form</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<form string="Goal types" version="7.0">
<sheet>
<h1>
<div class="oe_edit_only">
<label for="name"/>
</div>
<field name="name"/>
</h1>
<group>
<group string="General">
<field name="description"/>
<field name="condition"/>
</group>
<group string="Computation">
<field name="computation_mode"/>
<!-- Hide the fields below if manually -->
<field name="model_id" attrs="{'invisible':[('computation_mode','=','manually')], 'required':[('computation_mode','!=','manually')]}"/>
<field name="field_id" attrs="{'invisible':[('computation_mode','!=','sum')], 'required':[('computation_mode','=','sum')]}" domain="[('model_id','=',model_id)]" />
<field name="field_date_id" attrs="{'invisible':[('computation_mode','=','manually')]}" domain="[('ttype', 'in', ('date', 'datetime')), ('model_id','=',model_id)]"/>
<field name="domain" attrs="{'invisible':[('computation_mode','=','manually')], 'required':[('computation_mode','!=','manually')]}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="goal_type_search_view" model="ir.ui.view">
<field name="name">Goal Type Search</field>
<field name="model">gamification.goal.type</field>
<field name="arch" type="xml">
<search string="Search Goal Types">
<field name="name"/>
<field name="model_id"/>
<field name="field_id"/>
<group expand="0" string="Group By...">
<filter string="Model" domain="[]" context="{'group_by':'model_id'}"/>
<filter string="Computation Mode" domain="[]" context="{'group_by':'computation_mode'}"/>
</group>
</search>
</field>
</record>
</data>
</openerp>