[MERGE] features: new/updated portal and share modules
bzr revid: odo@openerp.com-20110624141403-5bemanedpsyw4xeb
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import portal
|
||||
import wizard
|
||||
import ir_ui_menu
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
{
|
||||
"name" : "Portal",
|
||||
"version" : "0.3",
|
||||
"depends" : ["base", "share"],
|
||||
"author" : "OpenERP SA",
|
||||
"category": 'Tools',
|
||||
"description": """
|
||||
This module defines 'portals' to customize the access to your OpenERP database
|
||||
for external users.
|
||||
|
||||
A portal defines customized user menu and access rights for a group of users
|
||||
(the ones associated to that portal). It also associates user groups to the
|
||||
portal users (adding a group in the portal automatically adds it to the portal
|
||||
users, etc). That feature is very handy when used in combination with the
|
||||
module 'share'.
|
||||
""",
|
||||
'website': 'http://www.openerp.com',
|
||||
'data': ['security/portal_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'portal_view.xml',
|
||||
'wizard_view.xml',
|
||||
'wizard/share_wizard_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'certificate' : '',
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv, fields
|
||||
|
||||
class portal_menu(osv.osv):
|
||||
"""Inherited menu class to customized the login search for menus,
|
||||
as web client 6.0 does not support the menu action properly yet"""
|
||||
|
||||
_inherit = 'ir.ui.menu'
|
||||
|
||||
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
|
||||
if context is None:
|
||||
context = {}
|
||||
# if the current user belongs to a portal, we have to
|
||||
# rewrite any search on the top menus to be under the
|
||||
# portal's root menu:
|
||||
if not context.get('ir.ui.menu.full_list') and uid != 1 and \
|
||||
args and len(args) == 1 and \
|
||||
len(args[0]) == 3 and \
|
||||
(args[0][0] == 'parent_id' \
|
||||
and args[0][1] == '=' \
|
||||
and args[0][2] == False):
|
||||
Portals = self.pool.get('res.portal')
|
||||
portal_id = Portals.search(cr, uid, [('group_id.users', 'in', uid)])
|
||||
if portal_id:
|
||||
assert len(portal_id) == 1, "Users may only belong to one portal at a time!"
|
||||
portal_data = Portals.read(cr, uid, portal_id[0], ['parent_menu_id'])
|
||||
menu_id_pair = portal_data.get('parent_menu_id') # (ID, Name)
|
||||
if menu_id_pair:
|
||||
args = [('parent_id', '=', menu_id_pair[0])]
|
||||
ids = super(portal_menu, self).search(cr, uid, args, offset=0,
|
||||
limit=None, order=order, context=context, count=False)
|
||||
return len(ids) if count else ids
|
||||
portal_menu()
|
||||
@@ -0,0 +1,246 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv, fields
|
||||
from tools.translate import _
|
||||
|
||||
class portal(osv.osv):
|
||||
_name = 'res.portal'
|
||||
_description = 'Portal'
|
||||
_rec_name = 'group_id'
|
||||
_columns = {
|
||||
'group_id': fields.many2one('res.groups', required=True,
|
||||
string='Portal Group',
|
||||
help=_('This group defines the users associated to this portal')),
|
||||
'user_ids': fields.related('group_id', 'users',
|
||||
type='many2many', relation='res.users', store=False,
|
||||
string='Portal Users'),
|
||||
'menu_action_id': fields.many2one('ir.actions.actions', readonly=True,
|
||||
string='Menu Action',
|
||||
help=_("What replaces the standard menu for the portal's users")),
|
||||
'parent_menu_id': fields.many2one('ir.ui.menu',
|
||||
string='Parent Menu',
|
||||
help=_('The menu action opens the submenus of this menu item')),
|
||||
'widget_ids': fields.one2many('res.portal.widget', 'portal_id',
|
||||
string='Widgets',
|
||||
help=_('Widgets assigned to portal users')),
|
||||
}
|
||||
_sql_constraints = [
|
||||
('unique_group', 'UNIQUE(group_id)', _('Portals must have distinct groups.'))
|
||||
]
|
||||
|
||||
def copy(self, cr, uid, id, default={}, context=None):
|
||||
""" override copy(): group_id and menu_action_id must be different """
|
||||
# copy the former group_id
|
||||
groups_obj = self.pool.get('res.groups')
|
||||
group_id = self.browse(cr, uid, id, context).group_id.id
|
||||
default['group_id'] = groups_obj.copy(cr, uid, group_id, {}, context)
|
||||
default['menu_action_id'] = None
|
||||
return super(portal, self).copy(cr, uid, id, default, context)
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
""" extend create() to assign the portal group and menu to users """
|
||||
# first create the 'menu_action_id'
|
||||
assert not values.get('menu_action_id')
|
||||
values['menu_action_id'] = self._create_menu_action(cr, uid, values, context)
|
||||
|
||||
if 'user_ids' in values:
|
||||
# set menu action of users
|
||||
user_values = {'menu_id': values['menu_action_id']}
|
||||
# values['user_ids'] should match [(6, 0, IDs)]
|
||||
for id in get_many2many(values['user_ids']):
|
||||
values['user_ids'].append((1, id, user_values))
|
||||
|
||||
# create portal
|
||||
portal_id = super(portal, self).create(cr, uid, values, context)
|
||||
|
||||
# assign widgets to users
|
||||
if 'user_ids' in values:
|
||||
self._assign_widgets_to_users(cr, uid, [portal_id], context)
|
||||
|
||||
return portal_id
|
||||
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
portals = self.browse(cr, uid, ids, context)
|
||||
return [(p.id, p.group_id.name) for p in portals]
|
||||
|
||||
def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100):
|
||||
# first search for group names that match
|
||||
groups_obj = self.pool.get('res.groups')
|
||||
group_names = groups_obj.name_search(cr, uid, name, args, operator, context, limit)
|
||||
# then search for portals that match the groups found so far
|
||||
domain = [('group_id', 'in', [gn[0] for gn in group_names])]
|
||||
ids = self.search(cr, uid, domain, context=context)
|
||||
return self.name_get(cr, uid, ids, context)
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
""" extend write() to reflect menu and groups changes on users """
|
||||
# first apply portal changes
|
||||
super(portal, self).write(cr, uid, ids, values, context)
|
||||
portals = self.browse(cr, uid, ids, context)
|
||||
|
||||
# if 'menu_action_id' has changed, set menu_id on users
|
||||
if 'menu_action_id' in values:
|
||||
user_values = {'menu_id': values['menu_action_id']}
|
||||
user_ids = [u.id for p in portals for u in p.user_ids if u.id != 1]
|
||||
self.pool.get('res.users').write(cr, uid, user_ids, user_values, context)
|
||||
|
||||
# if parent_menu_id has changed, apply the change on menu_action_id
|
||||
if 'parent_menu_id' in values:
|
||||
act_window_obj = self.pool.get('ir.actions.act_window')
|
||||
action_ids = [p.menu_action_id.id for p in portals]
|
||||
action_values = {'domain': [('parent_id', '=', values['parent_menu_id'])]}
|
||||
act_window_obj.write(cr, uid, action_ids, action_values, context)
|
||||
|
||||
# assign portal widgets to users, if widgets or users changed
|
||||
if ('user_ids' in values) or ('widget_ids' in values):
|
||||
self._assign_widgets_to_users(cr, uid, ids, context)
|
||||
|
||||
return True
|
||||
|
||||
def _create_menu_action(self, cr, uid, values, context=None):
|
||||
# create a menu action that opens the menu items below parent_menu_id
|
||||
groups_obj = self.pool.get('res.groups')
|
||||
group_name = groups_obj.browse(cr, uid, values['group_id'], context).name
|
||||
actions_obj = self.pool.get('ir.actions.act_window')
|
||||
action_values = {
|
||||
'name': group_name + ' Menu',
|
||||
'type': 'ir.actions.act_window',
|
||||
'usage': 'menu',
|
||||
'res_model': 'ir.ui.menu',
|
||||
'view_type': 'tree',
|
||||
'view_id': self._res_xml_id(cr, uid, 'base', 'view_menu'),
|
||||
'domain': [('parent_id', '=', values.get('parent_menu_id', False))],
|
||||
}
|
||||
return actions_obj.create(cr, uid, action_values, context)
|
||||
|
||||
def do_create_menu(self, cr, uid, ids, context=None):
|
||||
""" create a parent menu for the given portals """
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
menu_root = self._res_xml_id(cr, uid, 'portal', 'portal_menu')
|
||||
|
||||
for p in self.browse(cr, uid, ids, context):
|
||||
# create a menuitem under 'portal.portal_menu'
|
||||
menu_values = {
|
||||
'name': p.group_id.name + ' Menu',
|
||||
'parent_id': menu_root,
|
||||
'groups_id': [(6, 0, [p.group_id.id])],
|
||||
}
|
||||
menu_id = menu_obj.create(cr, uid, menu_values, context)
|
||||
# set the parent_menu_id to item_id
|
||||
self.write(cr, uid, [p.id], {'parent_menu_id': menu_id}, context)
|
||||
|
||||
return True
|
||||
|
||||
def _assign_widgets_to_users(self, cr, uid, ids, context=None):
|
||||
""" assign portal widgets to users for the given portal ids """
|
||||
widget_user_obj = self.pool.get('res.widget.user')
|
||||
portals = self.browse(cr, uid, ids, context)
|
||||
for p in portals:
|
||||
for w in p.widget_ids:
|
||||
values = {'sequence': w.sequence, 'widget_id': w.widget_id.id}
|
||||
for u in p.user_ids:
|
||||
if u.id == 1: continue
|
||||
values['user_id'] = u.id
|
||||
widget_user_obj.create(cr, uid, values, context)
|
||||
|
||||
def onchange_group(self, cr, uid, ids, group_id, context=None):
|
||||
""" update the users list when the group changes """
|
||||
user_ids = False
|
||||
if group_id:
|
||||
group = self.pool.get('res.groups').browse(cr, uid, group_id, context)
|
||||
user_ids = [u.id for u in group.users]
|
||||
return {
|
||||
'value': {'user_ids': user_ids}
|
||||
}
|
||||
|
||||
def _res_xml_id(self, cr, uid, module, xml_id):
|
||||
""" return the resource id associated to the given xml_id """
|
||||
data_obj = self.pool.get('ir.model.data')
|
||||
data_id = data_obj._get_id(cr, uid, module, xml_id)
|
||||
return data_obj.browse(cr, uid, data_id).res_id
|
||||
|
||||
portal()
|
||||
|
||||
|
||||
|
||||
class users(osv.osv):
|
||||
_name = 'res.users'
|
||||
_inherit = 'res.users'
|
||||
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
""" override default value of menu_id for portal users """
|
||||
defs = super(users, self).default_get(cr, uid, fields, context)
|
||||
|
||||
# the value of 'menu_id' is passed in context by the portal form view
|
||||
if ('menu_id' in context) and ('menu_id' in fields):
|
||||
defs['menu_id'] = context['menu_id']
|
||||
|
||||
return defs
|
||||
|
||||
users()
|
||||
|
||||
|
||||
|
||||
class portal_widget(osv.osv):
|
||||
"""
|
||||
Similar to res.widget.user (res_widget.py), but with a portal instead.
|
||||
New users in a portal are assigned the portal's widgets.
|
||||
"""
|
||||
_name='res.portal.widget'
|
||||
_description = 'Portal Widgets'
|
||||
_order = 'sequence'
|
||||
_columns = {
|
||||
'sequence': fields.integer('Sequence'),
|
||||
'portal_id': fields.many2one('res.portal', select=1,
|
||||
string='Portal'),
|
||||
'widget_id': fields.many2one('res.widget', required=True, ondelete='cascade',
|
||||
string='Widget'),
|
||||
}
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
domain = [('portal_id', '=', values.get('portal_id')),
|
||||
('widget_id', '=', values.get('widget_id'))]
|
||||
existing = self.search(cr, uid, domain, context=context)
|
||||
if existing:
|
||||
res = existing[0]
|
||||
else:
|
||||
res = super(portal_widget, self).create(cr, uid, values, context=context)
|
||||
return res
|
||||
|
||||
portal_widget()
|
||||
|
||||
|
||||
|
||||
# utils
|
||||
def get_browse_id(obj):
|
||||
""" return the id of a browse() object, or None """
|
||||
return (obj and obj.id or None)
|
||||
|
||||
def get_browse_ids(objs):
|
||||
""" return the ids of a list of browse() objects """
|
||||
return map(get_browse_id, objs)
|
||||
|
||||
def get_many2many(arg):
|
||||
""" get the list of ids from a many2many 'values' field """
|
||||
assert len(arg) == 1 and arg[0][0] == 6 # arg = [(6, _, IDs)]
|
||||
return arg[0][2]
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- menu Administration/Portals -->
|
||||
<menuitem name="Portals" id="portal_menu" parent="base.menu_administration" groups="group_portal_manager"/>
|
||||
|
||||
<!-- menu Administration/Portals/Portals -->
|
||||
<record id="portal_list_action" model="ir.actions.act_window">
|
||||
<field name="name">Portals</field>
|
||||
<field name="res_model">res.portal</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help">
|
||||
A portal helps defining specific views and rules for a group of users (the
|
||||
portal group). A portal is associated with a menu; when changed, the portal menu
|
||||
is assigned to the portal's users. Groups can be added to or removed from the
|
||||
portal's users.
|
||||
</field>
|
||||
</record>
|
||||
<menuitem name="Portals" id="portal_list_menu"
|
||||
parent="portal_menu" sequence="1" action="portal_list_action"/>
|
||||
|
||||
|
||||
<!-- portal list view -->
|
||||
<record id="portal_list" model="ir.ui.view">
|
||||
<field name="name">Portal List</field>
|
||||
<field name="model">res.portal</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Portal List">
|
||||
<field name="group_id"/>
|
||||
<field name="user_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- portal search view -->
|
||||
<record id="portal_search" model="ir.ui.view">
|
||||
<field name="name">Portal Search</field>
|
||||
<field name="model">res.portal</field>
|
||||
<field name="type">search</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Portal Search">
|
||||
<field name="group_id"/>
|
||||
<field name="user_ids"/>
|
||||
<field name="menu_action_id"/>
|
||||
<field name="parent_menu_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- portal form view -->
|
||||
<record id="portal_form" model="ir.ui.view">
|
||||
<field name="name">Portal Form</field>
|
||||
<field name="model">res.portal</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Portal Form">
|
||||
<field name="group_id" on_change="onchange_group(group_id)"/>
|
||||
<notebook>
|
||||
<page string="Users">
|
||||
<field name="user_ids" nolabel="1" colspan="3"
|
||||
context="{'menu_id': menu_action_id}"/>
|
||||
<group name="user_panel" colspan="1"/>
|
||||
<!-- wizards insert buttons inside the group 'user_panel' -->
|
||||
</page>
|
||||
<page string="Menu & Widgets">
|
||||
<group colspan="2" col="2">
|
||||
<separator string="Portal Menu" colspan="2"/>
|
||||
<group colspan="2" col="2">
|
||||
<field name="menu_action_id"/>
|
||||
<field name="parent_menu_id"
|
||||
context="{'ir.ui.menu.full_list': True}"/>
|
||||
</group>
|
||||
<button name="do_create_menu" type="object"
|
||||
string="Create Parent Menu"/>
|
||||
</group>
|
||||
<group colspan="2" col="2">
|
||||
<separator string="Portal Widgets" colspan="2"/>
|
||||
<field name="widget_ids" nolabel="1" rowspan="7">
|
||||
<tree string="Widgets" editable="bottom">
|
||||
<field name="sequence"/>
|
||||
<field name="widget_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,5 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_portal_manager","access.portal.manager","model_res_portal","group_portal_manager",1,1,1,1
|
||||
"access_portal_all","access.portal.all","model_res_portal",,1,0,0,0
|
||||
"access_portal_widget_all","access.portal.widget.all","model_res_portal_widget",,1,0,0,0
|
||||
"access_portal_widget_manager","access.portal.widget.manager","model_res_portal_widget",group_portal_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="group_portal_manager" model="res.groups">
|
||||
<field name="name">Portal / Manager</field>
|
||||
<field name="comment">
|
||||
Members of this groups have access to the portal definitions, and can easily manage the users, access right and menus of portal users.
|
||||
Members of this group who are also members of the Sharing/User group get additional options in the sharing wizard:
|
||||
- they may select a list of existing users to share with, without having to give a list of emails
|
||||
- they may choose to share with a Portal group instead of sharing with users, effectively extending the Portal content
|
||||
These features are useful for setting up Portals, but this could result in an alteration of the system's security policy.
|
||||
Therefore this should be granted only to trustworthy users who know the security policy very well.
|
||||
This group must not be deleted.</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import share_wizard
|
||||
import groups_wizard
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv, fields
|
||||
from tools.translate import _
|
||||
|
||||
class groups_wizard(osv.osv_memory):
|
||||
_name = 'res.portal.groups'
|
||||
_description = 'Portal Groups Wizard'
|
||||
_columns = {
|
||||
'portal_id': fields.many2one('res.portal', required=True, readonly=True,
|
||||
string='Portal'),
|
||||
'add_group_ids': fields.many2many('res.groups',
|
||||
'portal_groups_add', 'portal_groups_id', 'group_ids',
|
||||
string='Add Groups'),
|
||||
'remove_group_ids': fields.many2many('res.groups',
|
||||
'portal_groups_remove', 'portal_groups_id', 'group_ids',
|
||||
string='Remove Groups'),
|
||||
}
|
||||
_defaults = {
|
||||
'portal_id': (lambda self,cr,uid,context: context and context.get('active_id'))
|
||||
}
|
||||
|
||||
def do_apply(self, cr, uid, ids, context=None, *args):
|
||||
assert len(ids) == 1
|
||||
wizard = self.browse(cr, uid, ids[0], context)
|
||||
|
||||
# select all portal users except admin
|
||||
user_ids = [u.id for u in wizard.portal_id.user_ids if u.id != 1]
|
||||
|
||||
# apply group adds and removals to portal users
|
||||
add_gids = [(4, g.id) for g in wizard.add_group_ids]
|
||||
rem_gids = [(3, g.id) for g in wizard.remove_group_ids]
|
||||
user_values = {'groups_id': add_gids + rem_gids}
|
||||
self.pool.get('res.users').write(cr, uid, user_ids, user_values, context)
|
||||
|
||||
# return an empty dictionary to close the wizard window
|
||||
return {}
|
||||
|
||||
groups_wizard()
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv, fields
|
||||
from tools.translate import _
|
||||
|
||||
UID_ROOT = 1
|
||||
SHARED_DOCS_MENU = "Documents"
|
||||
SHARED_DOCS_CHILD_MENU = "Shared Documents"
|
||||
|
||||
class share_wizard_portal(osv.osv_memory):
|
||||
"""Inherited share wizard to automatically create appropriate
|
||||
menus in the selected portal upon sharing with a portal group."""
|
||||
_inherit = "share.wizard"
|
||||
|
||||
def _user_type_selection(self, cr, uid, context=None):
|
||||
selection = super(share_wizard_portal, self)._user_type_selection(cr, uid, context=context)
|
||||
selection.extend([('existing','Users you already shared with'),
|
||||
('groups','Existing Groups (e.g Portal Groups)')])
|
||||
return selection
|
||||
|
||||
_columns = {
|
||||
'user_ids': fields.many2many('res.users', 'share_wizard_res_user_rel', 'share_id', 'user_id', 'Existing users', domain=[('share', '=', True)]),
|
||||
'group_ids': fields.many2many('res.groups', 'share_wizard_res_group_rel', 'share_id', 'group_id', 'Existing groups', domain=[('share', '=', False)]),
|
||||
|
||||
# no clean way to extend selection yet, copy the field.
|
||||
'user_type': fields.selection(_user_type_selection,'Users to share with',
|
||||
help="Select the type of user(s) you would like to share data with."),
|
||||
}
|
||||
|
||||
def is_portal_manager(self, cr, uid, context=None):
|
||||
return self.has_group(cr, uid, module='portal', group_xml_id='group_portal_manager', context=context)
|
||||
|
||||
def _check_preconditions(self, cr, uid, wizard_data, context=None):
|
||||
if wizard_data.user_type == 'existing':
|
||||
self._assert(wizard_data.user_ids,
|
||||
_('Please select at least one user to share with'),
|
||||
context=context)
|
||||
elif wizard_data.user_type == 'groups':
|
||||
self._assert(wizard_data.group_ids,
|
||||
_('Please select at least one group to share with'),
|
||||
context=context)
|
||||
return super(share_wizard_portal, self)._check_preconditions(cr, uid, wizard_data, context=context)
|
||||
|
||||
def _create_or_get_submenu_named(self, cr, uid, parent_menu_id, menu_name, context=None):
|
||||
if not parent_menu_id:
|
||||
return
|
||||
Menus = self.pool.get('ir.ui.menu')
|
||||
parent_menu = Menus.browse(cr, uid, parent_menu_id) # No context
|
||||
menu_id = None
|
||||
max_seq = 10
|
||||
for child_menu in parent_menu.child_id:
|
||||
max_seq = max(max_seq, child_menu.sequence)
|
||||
if child_menu.name == menu_name:
|
||||
menu_id = child_menu.id
|
||||
break
|
||||
if not menu_id:
|
||||
# not found, create it
|
||||
menu_id = Menus.create(cr, UID_ROOT,
|
||||
{'name': menu_name,
|
||||
'parent_id': parent_menu.id,
|
||||
'sequence': max_seq + 10, # at the bottom
|
||||
})
|
||||
return menu_id
|
||||
|
||||
def _sharing_root_menu_id(self, cr, uid, portal, context=None):
|
||||
"""Create or retrieve root ID of sharing menu in portal menu
|
||||
|
||||
:param portal: browse_record of portal, constructed with a context WITHOUT language
|
||||
"""
|
||||
parent_menu_id = self._create_or_get_submenu_named(cr, uid, portal.parent_menu_id.id, SHARED_DOCS_MENU, context=context)
|
||||
if parent_menu_id:
|
||||
child_menu_id = self._create_or_get_submenu_named(cr, uid, parent_menu_id, SHARED_DOCS_CHILD_MENU, context=context)
|
||||
return child_menu_id
|
||||
|
||||
def _create_shared_data_menu(self, cr, uid, wizard_data, portal, context=None):
|
||||
"""Create sharing menus in portal menu according to share wizard options.
|
||||
|
||||
:param wizard_data: browse_record of share.wizard
|
||||
:param portal: browse_record of portal, constructed with a context WITHOUT language
|
||||
"""
|
||||
root_menu_id = self._sharing_root_menu_id(cr, uid, portal, context=context)
|
||||
if not root_menu_id:
|
||||
# no specific parent menu, cannot create the sharing menu at all.
|
||||
return
|
||||
# Create the shared action and menu
|
||||
action_def = self._shared_action_def(cr, uid, wizard_data, context=None)
|
||||
action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, action_def)
|
||||
menu_data = {'name': action_def['name'],
|
||||
'sequence': 10,
|
||||
'action': 'ir.actions.act_window,'+str(action_id),
|
||||
'parent_id': root_menu_id,
|
||||
'icon': 'STOCK_JUSTIFY_FILL'}
|
||||
menu_id = self.pool.get('ir.ui.menu').create(cr, UID_ROOT, menu_data)
|
||||
return menu_id
|
||||
|
||||
def _create_share_users_group(self, cr, uid, wizard_data, context=None):
|
||||
# Override of super() to handle the possibly selected "existing users"
|
||||
# and "existing groups".
|
||||
# In both cases, we call super() to create the share group, but when
|
||||
# sharing with existing groups, we will later delete it, and copy its
|
||||
# access rights and rules to the selected groups.
|
||||
group_id = super(share_wizard_portal,self)._create_share_users_group(cr, uid, wizard_data, context=context)
|
||||
|
||||
# For sharing with existing groups, we don't create a share group, instead we'll
|
||||
# alter the rules of the groups so they can see the shared data
|
||||
if wizard_data.group_ids:
|
||||
# get the list of portals and the related groups to install their menus.
|
||||
Portals = self.pool.get('res.portal')
|
||||
all_portals = Portals.browse(cr, UID_ROOT, Portals.search(cr, UID_ROOT, [])) #no context!
|
||||
all_portal_group_ids = [p.group_id.id for p in all_portals]
|
||||
|
||||
# populate result lines with the users of each group and
|
||||
# setup the menu for portal groups
|
||||
for group in wizard_data.group_ids:
|
||||
if group.id in all_portal_group_ids:
|
||||
portal = all_portals[all_portal_group_ids.index(group.id)]
|
||||
self._create_shared_data_menu(cr, uid, wizard_data, portal, context=context)
|
||||
|
||||
for user in group.users:
|
||||
new_line = {'user_id': user.id,
|
||||
'newly_created': False}
|
||||
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
|
||||
|
||||
elif wizard_data.user_ids:
|
||||
# must take care of existing users, by adding them to the new group, which is group_ids[0],
|
||||
# and adding the shortcut
|
||||
selected_user_ids = [x.id for x in wizard_data.user_ids]
|
||||
self.pool.get('res.users').write(cr, UID_ROOT, selected_user_ids, {'groups_id': [(4,group_id)]})
|
||||
self._setup_action_and_shortcut(cr, uid, wizard_data, selected_user_ids, make_home=False, context=context)
|
||||
# populate the result lines for existing users too
|
||||
for user in wizard_data.user_ids:
|
||||
new_line = { 'user_id': user.id,
|
||||
'newly_created': False}
|
||||
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
|
||||
|
||||
return group_id
|
||||
|
||||
def copy_share_group_access_and_delete(self, cr, wizard_data, share_group_id, context=None):
|
||||
# In the case of sharing with existing groups, the strategy is to copy
|
||||
# access rights and rules from the share group, so that we can
|
||||
if not wizard_data.group_ids: return
|
||||
Groups = self.pool.get('res.groups')
|
||||
Rules = self.pool.get('ir.rule')
|
||||
Rights = self.pool.get('ir.model.access')
|
||||
share_group = Groups.browse(cr, UID_ROOT, share_group_id)
|
||||
share_rule_ids = [r.id for r in share_group.rule_groups]
|
||||
for target_group in wizard_data.group_ids:
|
||||
# Link the rules to the group. This is appropriate because as of
|
||||
# v6.1, the algorithm for combining them will OR the rules, hence
|
||||
# extending the visible data.
|
||||
Rules.write(cr, UID_ROOT, share_rule_ids, {'groups': [(4,target_group.id)]})
|
||||
self._logger.debug("Linked sharing rules from temporary sharing group to group %s", target_group)
|
||||
|
||||
# Copy the access rights. This is appropriate too because
|
||||
# groups have the UNION of all permissions granted by their
|
||||
# access right lines.
|
||||
for access_line in share_group.model_access:
|
||||
Rights.copy(cr, UID_ROOT, access_line.id, default={'group_id': target_group.id})
|
||||
self._logger.debug("Copied access rights from temporary sharing group to group %s", target_group)
|
||||
|
||||
# finally, delete it after removing its users
|
||||
Groups.write(cr, UID_ROOT, [share_group_id], {'users': [(6,0,[])]})
|
||||
Groups.unlink(cr, UID_ROOT, [share_group_id])
|
||||
self._logger.debug("Deleted temporary sharing group %s", share_group_id)
|
||||
|
||||
def _finish_result_lines(self, cr, uid, wizard_data, share_group_id, context=None):
|
||||
super(share_wizard_portal,self)._finish_result_lines(cr, uid, wizard_data, share_group_id, context=context)
|
||||
self.copy_share_group_access_and_delete(cr, wizard_data, share_group_id, context=context)
|
||||
|
||||
share_wizard_portal()
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="share_step1_form_portal" model="ir.ui.view">
|
||||
<field name="name">share.step1.form.portal</field>
|
||||
<field name="model">share.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="share.share_step1_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/group[@name='emails_group']" position="before">
|
||||
<group name="groups_group" colspan="4" col="4" groups="portal.group_portal_manager">
|
||||
<separator colspan="4" string="Who do you want to share with?"/>
|
||||
<field name="user_type" />
|
||||
</group>
|
||||
<group name="user_group" colspan="4" attrs="{'invisible':[('user_type','!=','existing')]}">
|
||||
<field colspan="4" nolabel="1" name="user_ids" mode="tree">
|
||||
<tree string="Existing users">
|
||||
<field name="login"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<group colspan="4" attrs="{'invisible':[('user_type','!=','groups')]}">
|
||||
<field colspan="4" nolabel="1" name="group_ids" mode="tree">
|
||||
<tree string="Existing groups">
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="share_step2_form_portal" model="ir.ui.view">
|
||||
<field name="name">share.step2.form.portal</field>
|
||||
<field name="model">share.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="share.share_step2_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/field[@name='result_line_ids']/tree/field[@name='login']" position="after">
|
||||
<field name="share_url" groups="portal.group_portal_manager"/>
|
||||
</xpath>
|
||||
<xpath expr="/form/field[@name='result_line_ids']/form/field[@name='login']" position="after">
|
||||
<field name="share_url" colspan="4" groups="portal.group_portal_manager"/>
|
||||
</xpath>
|
||||
<xpath expr="/form/field[@name='result_line_ids']" position="after">
|
||||
<field colspan="4" name="share_root_url" groups="portal.group_portal_manager"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- groups wizard action -->
|
||||
<record id="groups_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Add/Remove Groups</field>
|
||||
<field name="res_model">res.portal.groups</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<!-- groups wizard form -->
|
||||
<record id="groups_wizard_form" model="ir.ui.view">
|
||||
<field name="name">Portal Groups Wizard</field>
|
||||
<field name="model">res.portal.groups</field>
|
||||
<field name="type">form</field>
|
||||
<field name="priority" eval="16"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Portal Groups Wizard">
|
||||
<group colspan="4">
|
||||
<field name="portal_id"/>
|
||||
</group>
|
||||
<separator string="Add Groups" colspan="2"/>
|
||||
<separator string="Remove Groups" colspan="2"/>
|
||||
<field name="add_group_ids" nolabel="1" colspan="2"/>
|
||||
<field name="remove_group_ids" nolabel="1" colspan="2"/>
|
||||
<separator colspan="4"/>
|
||||
<label colspan="2"/>
|
||||
<button special="cancel"
|
||||
string="Cancel" icon="gtk-cancel"/>
|
||||
<button name="do_apply" type="object"
|
||||
string="Apply" icon="gtk-apply"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- add a button in the portal form view -->
|
||||
<record id="portal_form_with_groups_wizard" model="ir.ui.view">
|
||||
<field name="name">Portal Form</field>
|
||||
<field name="model">res.portal</field>
|
||||
<field name="inherit_id" ref="portal_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='user_panel']" position="inside">
|
||||
<button name="%(groups_wizard_action)d" type="action"
|
||||
string="Add/Remove Groups"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -19,5 +19,6 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import ir_model
|
||||
import res_users
|
||||
import wizard
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
{
|
||||
"name" : "Sharing Tools",
|
||||
"version" : "1.3",
|
||||
"version" : "1.5",
|
||||
"depends" : ["base"],
|
||||
"author" : "OpenERP SA",
|
||||
"category": 'Tools',
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A. (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from osv import fields, osv
|
||||
|
||||
class ir_model_access(osv.osv):
|
||||
_inherit = 'ir.model.access'
|
||||
|
||||
# overload group_names_with_access() to avoid returning sharing groups
|
||||
# by filtering out groups with share=true.
|
||||
def group_names_with_access(self, cr, model_name, access_mode):
|
||||
"""Returns the names of visible groups which have been granted ``access_mode`` on
|
||||
the model ``model_name``.
|
||||
:rtype: list
|
||||
"""
|
||||
assert access_mode in ['read','write','create','unlink'], 'Invalid access mode: %s' % access_mode
|
||||
cr.execute('''SELECT
|
||||
g.name
|
||||
FROM
|
||||
ir_model_access a
|
||||
JOIN ir_model m ON (a.model_id=m.id)
|
||||
JOIN res_groups g ON (a.group_id=g.id)
|
||||
WHERE
|
||||
m.model=%s AND
|
||||
(g.share IS NULL or g.share IS false) AND
|
||||
a.perm_''' + access_mode, (model_name,))
|
||||
return [x[0] for x in cr.fetchall()]
|
||||
|
||||
ir_model_access()
|
||||
@@ -10,16 +10,29 @@
|
||||
<xpath expr="/search/field[@name='company_ids']" position="after">
|
||||
<field name="share">
|
||||
<filter name="no_share" string="Regular users only (no share user)" icon="terp-partner"
|
||||
domain="[('share','=',False)]"/>
|
||||
domain="[('share','=',False)]" default="1"/>
|
||||
</field>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Hide share users/groups by default -->
|
||||
<record model="ir.actions.act_window" id="base.action_res_users">
|
||||
<field name="name">Users</field>
|
||||
<field name="context">{'search_default_no_share': 1}</field>
|
||||
</record>
|
||||
<record id="view_users_form_share" model="ir.ui.view">
|
||||
<field name="name">res.users.form</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/notebook/page//field[@name='groups_id']" position="attributes">
|
||||
<attribute name="context">{'search_default_no_share':1}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="ir.ui.view" id="res_groups_search_sharing">
|
||||
<field name="name">res.groups.search.share</field>
|
||||
@@ -31,16 +44,27 @@
|
||||
<field name="name"/>
|
||||
<field name="share">
|
||||
<filter name="no_share" string="Regular groups only (no share groups" icon="terp-partner"
|
||||
domain="[('share','=',False)]"/>
|
||||
domain="[('share','=',False)]" default="1"/>
|
||||
</field>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Hide share users/groups by default -->
|
||||
<record model="ir.actions.act_window" id="base.action_res_groups">
|
||||
<field name="name">Groups</field>
|
||||
<field name="context">{'search_default_no_share': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="view_groups_form_share" model="ir.ui.view">
|
||||
<field name="name">res.groups.form</field>
|
||||
<field name="model">res.groups</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="base.view_groups_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/notebook/page//field[@name='users']" position="attributes">
|
||||
<attribute name="context">{'search_default_no_share':1}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -3,7 +3,8 @@
|
||||
<data noupdate="1">
|
||||
<record id="group_share_user" model="res.groups">
|
||||
<field name="name">Sharing / User</field>
|
||||
<field name="comment">
|
||||
Members of this groups have access to the sharing wizard, which allows them to invite external users to view or edit some of their documents.</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</openerp>
|
||||
@@ -12,7 +12,7 @@ class ShareWizardController(openerp.controllers.SecuredController):
|
||||
_cp_path = "/share"
|
||||
|
||||
@expose()
|
||||
def index(self, domain, search_domain, context, view_id, action_id=None):
|
||||
def index(self, domain, context, view_id, search_domain='[]', action_id=None):
|
||||
context = ast.literal_eval(context)
|
||||
|
||||
if not action_id:
|
||||
@@ -34,11 +34,9 @@ class ShareWizardController(openerp.controllers.SecuredController):
|
||||
scheme, netloc, '/openerp/login',
|
||||
'db=%(dbname)s&user=%(login)s&password=%(password)s', ''))
|
||||
|
||||
share_wiz_id = rpc.RPCProxy('ir.ui.menu').search(
|
||||
[('name','=', 'Share Wizard')])
|
||||
context.update(
|
||||
active_ids=share_wiz_id,
|
||||
active_id=share_wiz_id[0],
|
||||
#active_ids=share_wiz_id,
|
||||
#active_id=share_wiz_id[0],
|
||||
_terp_view_name='Share Wizard',
|
||||
share_root_url=share_root_url)
|
||||
Share = rpc.RPCProxy('share.wizard')
|
||||
|
||||
+81
-28
@@ -1,41 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import openobject.templating
|
||||
|
||||
class ShareActionEditor(openobject.templating.TemplateEditor):
|
||||
class FormEditor(openobject.templating.TemplateEditor):
|
||||
templates = ['/openerp/controllers/templates/form.mako']
|
||||
MAIN_FORM_BODY = u'id="main_form_body"'
|
||||
|
||||
def insert_share_button(self, output):
|
||||
# Insert the share button on the form title H1 line, at the very end,
|
||||
# but only if the user is a member of the sharing group
|
||||
share_opener_insertion = output.index('% endif', output.index(
|
||||
'/view_diagram/process',
|
||||
output.index(self.MAIN_FORM_BODY))) + len('% endif')
|
||||
return output[:share_opener_insertion] + '''
|
||||
<%
|
||||
if 'has_share' not in cp.session:
|
||||
cp.session['has_share'] = rpc.RPCProxy('share.wizard').has_share()
|
||||
%>
|
||||
% if cp.session['has_share'] and buttons.toolbar and not is_dashboard:
|
||||
<a id="share-opener" href="#share" title="${_('Share this in 2 clicks...')}">
|
||||
<img id="share-opener-img" src="/share/static/images/share.png"/>
|
||||
</a>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
jQuery("#share-opener").click(function() {
|
||||
jQuery(this).attr(
|
||||
"href",
|
||||
openobject.http.getURL('/share', {
|
||||
context: jQuery("#_terp_context").val(),
|
||||
domain: jQuery("#_terp_domain").val(),
|
||||
view_id: jQuery("#_terp_view_id").val(),
|
||||
action_id: jQuery("#_terp_action_id").val(),
|
||||
search_domain: jQuery("#_terp_view_type").val() == "form" ?
|
||||
("[('id','=',"+jQuery("#_terp_id").val()+")]") :
|
||||
jQuery("#_terp_search_domain").val(),
|
||||
}));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
\n
|
||||
% endif
|
||||
''' + output[share_opener_insertion:]
|
||||
|
||||
def edit(self, template, template_text):
|
||||
return self.insert_share_button(
|
||||
super(FormEditor, self).edit(template, template_text))
|
||||
|
||||
|
||||
class SidebarEditor(openobject.templating.TemplateEditor):
|
||||
templates = ['/openerp/widgets/templates/sidebar.mako']
|
||||
ADD_SHARE_SECTION = u'id="sidebar"'
|
||||
|
||||
def insert_share_link(self, output):
|
||||
# Insert the link on the line right after the link to open the
|
||||
# attachment form
|
||||
# Insert the link on the line, right after the link to open the
|
||||
# attachment form, but only if the user is a member of the sharing group
|
||||
share_opener_insertion = output.index(
|
||||
'\n',
|
||||
output.index(self.ADD_SHARE_SECTION)) + 1
|
||||
return output[:share_opener_insertion] + \
|
||||
'''<div id="share-wizard" class="sideheader-a"><h2>${_("Sharing")}</h2></div>
|
||||
<ul class="clean-a">
|
||||
<li>
|
||||
<a id="sharing" href="#share">${_("Share")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
jQuery("#sharing").click(function() {
|
||||
jQuery(this).attr(
|
||||
"href",
|
||||
openobject.http.getURL('/share', {
|
||||
context: jQuery("#_terp_context").val(),
|
||||
domain: jQuery("#_terp_domain").val(),
|
||||
view_id: jQuery("#_terp_view_id").val(),
|
||||
action_id: jQuery("#_terp_action_id").val(),
|
||||
search_domain: jQuery("#_terp_search_domain").val(),
|
||||
}));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
\n''' + \
|
||||
output[share_opener_insertion:]
|
||||
return output[:share_opener_insertion] + '''
|
||||
<%
|
||||
if 'has_share' not in cp.session:
|
||||
cp.session['has_share'] = rpc.RPCProxy('share.wizard').has_share()
|
||||
%>
|
||||
% if cp.session['has_share']:
|
||||
<div id="share-wizard" class="sideheader-a"><h2>${_("Sharing")}</h2></div>
|
||||
<ul class="clean-a">
|
||||
<li>
|
||||
<a id="sharing" href="#share">${_("Share")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function() {
|
||||
jQuery("#sharing").click(function() {
|
||||
jQuery(this).attr(
|
||||
"href",
|
||||
openobject.http.getURL('/share', {
|
||||
context: jQuery("#_terp_context").val(),
|
||||
domain: jQuery("#_terp_domain").val(),
|
||||
view_id: jQuery("#_terp_view_id").val(),
|
||||
action_id: jQuery("#_terp_action_id").val(),
|
||||
search_domain: jQuery("#_terp_view_type").val() == "form" ?
|
||||
("[('id','=',"+jQuery("#_terp_id").val()+")]") :
|
||||
jQuery("#_terp_search_domain").val(),
|
||||
}));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
\n
|
||||
% endif
|
||||
''' + output[share_opener_insertion:]
|
||||
|
||||
def edit(self, template, template_text):
|
||||
return self.insert_share_link(
|
||||
super(ShareActionEditor, self).edit(template, template_text))
|
||||
super(SidebarEditor, self).edit(template, template_text))
|
||||
|
||||
+490
-227
@@ -21,97 +21,186 @@
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from urllib import quote_plus
|
||||
|
||||
import tools
|
||||
from osv import osv, fields
|
||||
from osv.expression import expression
|
||||
from osv import expression
|
||||
from tools.translate import _
|
||||
from tools.safe_eval import safe_eval
|
||||
|
||||
FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink')
|
||||
READ_WRITE_ACCESS = ('perm_read', 'perm_write')
|
||||
READ_ONLY_ACCESS = ('perm_read',)
|
||||
UID_ROOT = 1
|
||||
|
||||
# Pseudo-domain to represent an empty filter, constructed using
|
||||
# osv.expression's DUMMY_LEAF
|
||||
DOMAIN_ALL = [(1, '=', 1)]
|
||||
|
||||
RANDOM_PASS_CHARACTERS = [chr(x) for x in range(48, 58) + range(97, 123) + range(65, 91)]
|
||||
RANDOM_PASS_CHARACTERS.remove('l') #lowercase l, easily mistaken as one or capital i
|
||||
RANDOM_PASS_CHARACTERS.remove('I') #uppercase i, easily mistaken as one or lowercase L
|
||||
RANDOM_PASS_CHARACTERS.remove('O') #uppercase o, mistaken with zero
|
||||
RANDOM_PASS_CHARACTERS.remove('o') #lowercase o, mistaken with zero
|
||||
RANDOM_PASS_CHARACTERS.remove('0') #zero, mistaken with o-letter
|
||||
RANDOM_PASS_CHARACTERS.remove('1') #one, mistaken with lowercase-L or capital i
|
||||
# A good selection of easy to read password characters (e.g. no '0' vs 'O', etc.)
|
||||
RANDOM_PASS_CHARACTERS = 'aaaabcdeeeefghjkmnpqrstuvwxyzAAAABCDEEEEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
def generate_random_pass():
|
||||
pass_chars = RANDOM_PASS_CHARACTERS[:]
|
||||
random.shuffle(pass_chars)
|
||||
return ''.join(pass_chars[0:10])
|
||||
return ''.join(random.sample(RANDOM_PASS_CHARACTERS,10))
|
||||
|
||||
|
||||
class share_create(osv.osv_memory):
|
||||
__logger = logging.getLogger('share.wizard')
|
||||
# Utils for introspecting the ORM - could be moved to server someday
|
||||
class column_info(object):
|
||||
"""Struct containing details about an osv column, either one local to
|
||||
its model, or one inherited via _inherits.
|
||||
|
||||
:attr name: name of the column
|
||||
:attr column: column instance, subclass of osv.fields.column
|
||||
:attr parent_model: if the column is inherited, name of the model
|
||||
that contains it, None for local columns.
|
||||
:attr parent_column: the name of the column containing the m2o
|
||||
relationship to the parent model that contains
|
||||
this column, None for local columns.
|
||||
"""
|
||||
def __init__(self, name, column, parent_model=None, parent_column=None):
|
||||
self.name = name
|
||||
self.column = column
|
||||
self.parent_model = parent_model
|
||||
self.parent_column = parent_column
|
||||
|
||||
def get_column_infos(osv_model):
|
||||
"""Returns a dict mapping all fields names (direct fields and
|
||||
inherited field via _inherits) to a ``column_info`` struct
|
||||
giving detailed columns """
|
||||
result = {}
|
||||
for k, (parent,m2o,col) in osv_model._inherit_fields.iteritems():
|
||||
result[k] = column_info(k, col, parent, m2o)
|
||||
for k, v in osv_model._columns.iteritems():
|
||||
result[k] = column_info(k,v)
|
||||
return result
|
||||
|
||||
|
||||
class share_wizard(osv.osv_memory):
|
||||
_logger = logging.getLogger('share.wizard')
|
||||
_name = 'share.wizard'
|
||||
_description = 'Share Wizard'
|
||||
|
||||
def _assert(self, condition, error_message, context=None):
|
||||
"""Raise a user error with the given message if condition is not met.
|
||||
The error_message should have been translated with _().
|
||||
"""
|
||||
if not condition:
|
||||
raise osv.except_osv(_('Sharing access could not be created'), error_message)
|
||||
|
||||
def has_group(self, cr, uid, module, group_xml_id, context=None):
|
||||
"""Returns True if current user is a member of the group identified by the module, group_xml_id pair."""
|
||||
# if the group was deleted or does not exist, we say NO (better safe than sorry)
|
||||
try:
|
||||
model, group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module, group_xml_id)
|
||||
except ValueError:
|
||||
return False
|
||||
return group_id in self.pool.get('res.users').read(cr, uid, uid, ['groups_id'], context=context)['groups_id']
|
||||
|
||||
def has_share(self, cr, uid, context=None):
|
||||
return self.has_group(cr, uid, module='share', group_xml_id='group_share_user', context=context)
|
||||
|
||||
def has_email(self, cr, uid, context=None):
|
||||
return bool(self.pool.get('res.users').browse(cr, uid, uid, context=context).user_email)
|
||||
|
||||
def view_init(self, cr, uid, fields_list, context=None):
|
||||
if not self.has_email(cr, uid, context=context):
|
||||
raise osv.except_osv(_('No e-mail address configured'),
|
||||
_('You must configure your e-mail address in the user preferences before using the Share button.'))
|
||||
return super(share_wizard, self).view_init(cr, uid, fields_list, context=context)
|
||||
|
||||
def _user_type_selection(self, cr, uid, context=None):
|
||||
"""Selection values may be easily overridden/extended via inheritance"""
|
||||
return [('emails','List of emails')]
|
||||
|
||||
"""Override of create() to auto-compute the action name"""
|
||||
def create(self, cr, uid, values, context=None):
|
||||
if 'action_id' in values and not 'name' in values:
|
||||
action = self.pool.get('ir.actions.actions').browse(cr, uid, values['action_id'], context=context)
|
||||
values['name'] = action.name
|
||||
return super(share_wizard,self).create(cr, uid, values, context=context)
|
||||
|
||||
_columns = {
|
||||
'action_id': fields.many2one('ir.actions.act_window', 'Action to share', required=True,
|
||||
help="The action that opens the screen containing the data you wish to share."),
|
||||
'domain': fields.char('Domain', size=256, help="Optional domain for further data filtering"),
|
||||
'user_type': fields.selection([('existing','Existing external users'),('new','New users (emails required)')],'Users to share with',
|
||||
help="Select the type of user(s) you would like to share data with."),
|
||||
'user_ids': fields.many2many('res.users', 'share_wizard_res_user_rel', 'share_id', 'user_id', 'Existing users', domain=[('share', '=', True)]),
|
||||
'new_users': fields.text("New users"),
|
||||
'access_mode': fields.selection([('readwrite','Read & Write'),('readonly','Read-only')],'Access Mode'),
|
||||
'user_type': fields.selection(_user_type_selection,'Users to share with',
|
||||
help="Select the type of user(s) you would like to share data with."),
|
||||
'new_users': fields.text("Emails"),
|
||||
'access_mode': fields.selection([('readonly','Can view'),('readwrite','Can edit')],'Access Mode', required=True,
|
||||
help="Access rights to be granted on the shared documents."),
|
||||
'result_line_ids': fields.one2many('share.wizard.result.line', 'share_wizard_id', 'Summary', readonly=True),
|
||||
'share_root_url': fields.char('Generic Share Access URL', size=512, readonly=True, tooltip='Main access page for users that are granted shared access')
|
||||
'share_root_url': fields.char('Share Access URL', size=512, readonly=True, tooltip='Main access page for users that are granted shared access'),
|
||||
'name': fields.char('Share Title', size=64, required=True, help="Title for the share (displayed to users as menu and shortcut name)"),
|
||||
'message': fields.text("Personal Message", help="An optional personal message, to be included in the e-mail notification."),
|
||||
}
|
||||
_defaults = {
|
||||
'user_type' : lambda self, cr, uid, *a: 'existing' if self.pool.get('res.users').search(cr, uid, [('share', '=', True)]) else 'new',
|
||||
'user_type' : 'new',
|
||||
'domain': lambda self, cr, uid, context, *a: context.get('domain', '[]'),
|
||||
'share_root_url': lambda self, cr, uid, context, *a: context.get('share_root_url') or _('Please specify "share_root_url" in context'),
|
||||
'action_id': lambda self, cr, uid, context, *a: context.get('action_id'),
|
||||
'access_mode': 'readonly'
|
||||
'access_mode': 'readonly',
|
||||
}
|
||||
|
||||
def go_step_1(self, cr, uid, ids, context=None):
|
||||
dummy, step1_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step1_form')
|
||||
return {
|
||||
'name': _('Sharing Wizard - Step 1'),
|
||||
'name': _('Grant instant access to your documents'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'share.wizard',
|
||||
'view_id': False,
|
||||
'res_id': ids[0],
|
||||
'views': [(step1_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
|
||||
'views': [(step1_form_view_id, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new'
|
||||
}
|
||||
|
||||
def _create_share_group(self, cr, uid, wizard_data, context=None):
|
||||
group_obj = self.pool.get('res.groups')
|
||||
share_group_name = '%s: %s (%d-%s)' %('Sharing', wizard_data.action_id.res_model, uid, time.time())
|
||||
share_group_name = '%s: %s (%d-%s)' %('Shared', wizard_data.name, uid, time.time())
|
||||
# create share group without putting admin in it
|
||||
return group_obj.create(cr, 1, {'name': share_group_name, 'share': True}, {'noadmin': True})
|
||||
return group_obj.create(cr, UID_ROOT, {'name': share_group_name, 'share': True}, {'noadmin': True})
|
||||
|
||||
def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None):
|
||||
"""Create one new res.users record for each email address provided in
|
||||
wizard_data.new_users, ignoring already existing users.
|
||||
Populates wizard_data.result_line_ids with one new line for
|
||||
each user (existing or not). New users will also have a value
|
||||
for the password field, so they can receive it by email.
|
||||
Returns the ids of the created users, and the ids of the
|
||||
ignored, existing ones."""
|
||||
user_obj = self.pool.get('res.users')
|
||||
current_user = user_obj.browse(cr, uid, uid, context=context)
|
||||
user_ids = []
|
||||
if wizard_data.user_type == 'new':
|
||||
for new_user in wizard_data.new_users.split('\n'):
|
||||
# attempt to show more user-friendly msg than default constraint error
|
||||
existing = user_obj.search(cr, 1, [('login', '=', new_user)])
|
||||
if existing:
|
||||
raise osv.except_osv(_('User already exists'),
|
||||
_('This username (%s) already exists, perhaps data has already been shared with this person.\nYou may want to try selecting existing shared users instead.') % new_user)
|
||||
user_id = user_obj.create(cr, 1, {
|
||||
'login': new_user,
|
||||
'password': generate_random_pass(),
|
||||
'name': new_user,
|
||||
'user_email': new_user,
|
||||
'groups_id': [(6,0,[group_id])],
|
||||
'share': True,
|
||||
'company_id': current_user.company_id and current_user.company_id.id
|
||||
})
|
||||
user_ids.append(user_id)
|
||||
return user_ids
|
||||
current_user = user_obj.browse(cr, UID_ROOT, uid, context=context)
|
||||
created_ids = []
|
||||
existing_ids = []
|
||||
for new_user in (wizard_data.new_users or '').split('\n'):
|
||||
# Ignore blank lines
|
||||
new_user = new_user.strip()
|
||||
if not new_user: continue
|
||||
# Ignore the user if it already exists.
|
||||
existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
|
||||
existing_ids.extend(existing)
|
||||
if existing:
|
||||
new_line = { 'user_id': existing[0],
|
||||
'newly_created': False}
|
||||
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
|
||||
continue
|
||||
new_pass = generate_random_pass()
|
||||
user_id = user_obj.create(cr, UID_ROOT, {
|
||||
'login': new_user,
|
||||
'password': new_pass,
|
||||
'name': new_user,
|
||||
'user_email': new_user,
|
||||
'groups_id': [(6,0,[group_id])],
|
||||
'share': True,
|
||||
'company_id': current_user.company_id.id
|
||||
})
|
||||
new_line = { 'user_id': user_id,
|
||||
'password': new_pass,
|
||||
'newly_created': True}
|
||||
wizard_data.write({'result_line_ids': [(0,0,new_line)]})
|
||||
created_ids.append(user_id)
|
||||
return created_ids, existing_ids
|
||||
|
||||
def _create_shortcut(self, cr, uid, values, context=None):
|
||||
if context is None:
|
||||
@@ -124,81 +213,165 @@ class share_create(osv.osv_memory):
|
||||
dataobj = self.pool.get('ir.model.data')
|
||||
menu_id = dataobj._get_id(cr, uid, 'base', 'menu_administration_shortcut', new_context)
|
||||
shortcut_menu_id = int(dataobj.read(cr, uid, menu_id, ['res_id'], new_context)['res_id'])
|
||||
action_id = self.pool.get('ir.actions.act_window').create(cr, 1, values, new_context)
|
||||
action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, values, new_context)
|
||||
menu_data = {'name': values['name'],
|
||||
'sequence': 10,
|
||||
'action': 'ir.actions.act_window,'+str(action_id),
|
||||
'parent_id': shortcut_menu_id,
|
||||
'icon': 'STOCK_JUSTIFY_FILL'}
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
menu_id = menu_obj.create(cr, 1, menu_data)
|
||||
sc_data= {'name': values['name'], 'sequence': 1,'res_id': menu_id }
|
||||
sc_menu_id = self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, new_context)
|
||||
menu_id = menu_obj.create(cr, UID_ROOT, menu_data)
|
||||
sc_data = {'name': values['name'], 'sequence': UID_ROOT,'res_id': menu_id }
|
||||
self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, new_context)
|
||||
|
||||
# update menu cache
|
||||
user_groups = set(self.pool.get('res.users').read(cr, 1, uid, ['groups_id'])['groups_id'])
|
||||
user_groups = set(self.pool.get('res.users').read(cr, UID_ROOT, uid, ['groups_id'])['groups_id'])
|
||||
key = (cr.dbname, shortcut_menu_id, tuple(user_groups))
|
||||
menu_obj._cache[key] = True
|
||||
return action_id
|
||||
|
||||
def _cleanup_action_context(self, context_str, user_id):
|
||||
"""Returns a dict representing the context_str evaluated (safe_eval) as
|
||||
a dict where items that are not useful for shared actions
|
||||
have been removed. If the evaluation of context_str as a
|
||||
dict fails, context_str is returned unaltered.
|
||||
|
||||
def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, new_users, context=None):
|
||||
user_obj = self.pool.get('res.users')
|
||||
menu_action_id = user_obj._get_menu(cr, uid, context=context)
|
||||
values = {
|
||||
'name': (_('%s (Shared)') % wizard_data.action_id.name)[:64],
|
||||
'domain': wizard_data.domain,
|
||||
'context': wizard_data.action_id.context,
|
||||
'res_model': wizard_data.action_id.res_model,
|
||||
'view_mode': wizard_data.action_id.view_mode,
|
||||
'view_type': wizard_data.action_id.view_type,
|
||||
'search_view_id': wizard_data.action_id.search_view_id.id,
|
||||
:param user_id: the integer uid to be passed as 'uid' in the
|
||||
evaluation context
|
||||
"""
|
||||
result = False
|
||||
if context_str:
|
||||
try:
|
||||
context = safe_eval(context_str, tools.UnquoteEvalContext(), nocopy=True)
|
||||
result = dict(context)
|
||||
for key in context:
|
||||
# Remove all context keys that seem to toggle default
|
||||
# filters based on the current user, as it makes no sense
|
||||
# for shared users, who would not see any data by default.
|
||||
if key and key.startswith('search_default_') and 'user_id' in key:
|
||||
result.pop(key)
|
||||
except Exception:
|
||||
# Note: must catch all exceptions, as UnquoteEvalContext may cause many
|
||||
# different exceptions, as it shadows builtins.
|
||||
self._logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
|
||||
result = context_str
|
||||
return result
|
||||
|
||||
def _shared_action_def(self, cr, uid, wizard_data, context=None):
|
||||
copied_action = wizard_data.action_id
|
||||
action_def = {
|
||||
'name': wizard_data.name,
|
||||
'domain': copied_action.domain,
|
||||
'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
|
||||
'res_model': copied_action.res_model,
|
||||
'view_mode': copied_action.view_mode,
|
||||
'view_type': copied_action.view_type,
|
||||
'search_view_id': copied_action.search_view_id.id,
|
||||
'view_id': copied_action.view_id.id,
|
||||
}
|
||||
if copied_action.view_ids:
|
||||
action_def['view_ids'] = [(0,0,{'sequence': x.sequence,
|
||||
'view_mode': x.view_mode,
|
||||
'view_id': x.view_id.id })
|
||||
for x in copied_action.view_ids]
|
||||
return action_def
|
||||
|
||||
def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, make_home, context=None):
|
||||
"""Create a shortcut to reach the shared data, as well as the corresponding action, for
|
||||
each user in ``user_ids``, and assign it as their home action if ``make_home`` is True.
|
||||
Meant to be overridden for special cases.
|
||||
"""
|
||||
values = self._shared_action_def(cr, uid, wizard_data, context=None)
|
||||
user_obj = self.pool.get('res.users')
|
||||
for user_id in user_ids:
|
||||
action_id = self._create_shortcut(cr, user_id, values)
|
||||
if new_users:
|
||||
user_obj.write(cr, 1, [user_id], {'action_id': action_id})
|
||||
else:
|
||||
user_obj.write(cr, 1, [user_id], {'action_id': menu_action_id})
|
||||
if make_home:
|
||||
# We do this only for new share users, as existing ones already have their initial home
|
||||
# action. Resetting to the default menu does not work well as the menu is rather empty
|
||||
# and does not contain the shortcuts in most cases.
|
||||
user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
|
||||
|
||||
def _get_recursive_relations(self, cr, uid, model, ttypes, relation_fields=None, suffix=None, context=None):
|
||||
"""Returns list of tuples representing recursive relationships of type ``ttypes`` starting from
|
||||
model with ID ``model_id``.
|
||||
|
||||
@param model: browsable model to start loading relationships from
|
||||
@param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
|
||||
@param relation_fields: list of previously followed relationship tuples - to avoid duplicates
|
||||
:param model: browsable model to start loading relationships from
|
||||
:param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
|
||||
:param relation_fields: list of previously followed relationship tuples - to avoid duplicates
|
||||
during recursion
|
||||
@param suffix: optional suffix to append to the field path to reach the main object
|
||||
:param suffix: optional suffix to append to the field path to reach the main object
|
||||
"""
|
||||
|
||||
if relation_fields is None:
|
||||
relation_fields = []
|
||||
local_rel_fields = []
|
||||
models = [x[1].model for x in relation_fields]
|
||||
model_obj = self.pool.get('ir.model')
|
||||
model_osv = self.pool.get(model.model)
|
||||
for field in model_osv._columns.values() + [x[2] for x in model_osv._inherit_fields.itervalues()]:
|
||||
if field._type in ttypes and field._obj not in models:
|
||||
relation_model_id = model_obj.search(cr, uid, [('model','=',field._obj)])[0]
|
||||
if field._type == 'one2many':
|
||||
relation_field = '%s.%s'%(field._fields_id, suffix) if suffix else field._fields_id
|
||||
else:
|
||||
relation_field = None # TODO: add some filtering for m2m and m2o - not always possible...
|
||||
model_browse = model_obj.browse(cr, uid, relation_model_id, context=context)
|
||||
local_rel_fields.append((relation_field, model_browse))
|
||||
if relation_model_id != model.id and field._type in ['one2many', 'many2many']:
|
||||
local_rel_fields += self._get_recursive_relations(cr, uid, model_browse,
|
||||
[field._type], relation_fields + local_rel_fields, suffix=relation_field, context=context)
|
||||
for colinfo in get_column_infos(model_osv).itervalues():
|
||||
coldef = colinfo.column
|
||||
coltype = coldef._type
|
||||
relation_field = None
|
||||
if coltype in ttypes and colinfo.column._obj not in models:
|
||||
relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',coldef._obj)])[0]
|
||||
relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
|
||||
relation_osv = self.pool.get(coldef._obj)
|
||||
if coltype == 'one2many':
|
||||
# don't record reverse path if it's not a real m2o (that happens, but rarely)
|
||||
dest_model_ci = get_column_infos(relation_osv)
|
||||
reverse_rel = coldef._fields_id
|
||||
if reverse_rel in dest_model_ci and dest_model_ci[reverse_rel].column._type == 'many2one':
|
||||
relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
|
||||
local_rel_fields.append((relation_field, relation_model_browse))
|
||||
for parent in relation_osv._inherits:
|
||||
if parent not in models:
|
||||
parent_model = self.pool.get(parent)
|
||||
parent_colinfos = get_column_infos(parent_model)
|
||||
parent_model_browse = model_obj.browse(cr, UID_ROOT,
|
||||
model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
|
||||
if relation_field and coldef._fields_id in parent_colinfos:
|
||||
# inverse relationship is available in the parent
|
||||
local_rel_fields.append((relation_field, parent_model_browse))
|
||||
else:
|
||||
# TODO: can we setup a proper rule to restrict inherited models
|
||||
# in case the parent does not contain the reverse m2o?
|
||||
local_rel_fields.append((None, parent_model_browse))
|
||||
if relation_model_id != model.id and coltype in ['one2many', 'many2many']:
|
||||
local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
|
||||
[coltype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
|
||||
return local_rel_fields
|
||||
|
||||
def _get_relationship_classes(self, cr, uid, model, context=None):
|
||||
"""Computes the *relationship classes* reachable from the given
|
||||
model. The 4 relationship classes are:
|
||||
- [obj0]: the given model itself (and its parents via _inherits, if any)
|
||||
- [obj1]: obj0 and all other models recursively accessible from
|
||||
obj0 via one2many relationships
|
||||
- [obj2]: obj0 and all other models recursively accessible from
|
||||
obj0 via one2many and many2many relationships
|
||||
- [obj3]: all models recursively accessible from obj1 via many2one
|
||||
relationships
|
||||
|
||||
Each class is returned as a list of pairs [(field,model_browse)], where
|
||||
``model`` is the browse_record of a reachable ir.model, and ``field`` is
|
||||
the dot-notation reverse relationship path coming from that model to obj0,
|
||||
or None if there is no reverse path.
|
||||
|
||||
:return: ([obj0], [obj1], [obj2], [obj3])
|
||||
"""
|
||||
# obj0 class and its parents
|
||||
obj0 = [(None, model)]
|
||||
obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], context=context)
|
||||
obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], context=context)
|
||||
obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], context=context)
|
||||
model_obj = self.pool.get(model.model)
|
||||
ir_model_obj = self.pool.get('ir.model')
|
||||
for parent in model_obj._inherits:
|
||||
parent_model_browse = ir_model_obj.browse(cr, UID_ROOT,
|
||||
ir_model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
|
||||
obj0 += [(None, parent_model_browse)]
|
||||
|
||||
obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], relation_fields=obj0, context=context)
|
||||
obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], relation_fields=obj0, context=context)
|
||||
obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
|
||||
for dummy, model in obj1:
|
||||
obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], context=context)
|
||||
obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
|
||||
return obj0, obj1, obj2, obj3
|
||||
|
||||
def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None):
|
||||
@@ -215,23 +388,22 @@ class share_create(osv.osv_memory):
|
||||
access_line.add(perm)
|
||||
return user_access_matrix
|
||||
|
||||
def _add_access_rights_for_share_group(self, cr, uid, group_id, mode,
|
||||
fields_relations, context=None):
|
||||
def _add_access_rights_for_share_group(self, cr, uid, group_id, mode, fields_relations, context=None):
|
||||
"""Adds access rights to group_id on object models referenced in ``fields_relations``,
|
||||
intersecting with access rights of current user to avoid granting too much rights
|
||||
"""
|
||||
model_access_obj = self.pool.get('ir.model.access')
|
||||
user_obj = self.pool.get('res.users')
|
||||
target_model_ids = [x[1].id for x in fields_relations]
|
||||
perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or FULL_ACCESS
|
||||
target_model_ids = [x[1].id for x in fields_relations]
|
||||
perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or READ_WRITE_ACCESS
|
||||
current_user = user_obj.browse(cr, uid, uid, context=context)
|
||||
|
||||
current_user_access_map = self._get_access_map_for_groups_and_models(cr, uid,
|
||||
[x.id for x in current_user.groups_id], target_model_ids, context=context)
|
||||
group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
|
||||
[group_id], target_model_ids, context=context)
|
||||
self.__logger.debug("Current user access matrix: %r", current_user_access_map)
|
||||
self.__logger.debug("New group current access matrix: %r", group_access_map)
|
||||
self._logger.debug("Current user access matrix: %r", current_user_access_map)
|
||||
self._logger.debug("New group current access matrix: %r", group_access_map)
|
||||
|
||||
# Create required rights if allowed by current user rights and not
|
||||
# already granted
|
||||
@@ -251,126 +423,193 @@ class share_create(osv.osv_memory):
|
||||
group_access_map.setdefault(model.model, set()).add(perm)
|
||||
need_creation = True
|
||||
if need_creation:
|
||||
model_access_obj.create(cr, 1, values)
|
||||
self.__logger.debug("Creating access right for model %s with values: %r", model.model, values)
|
||||
model_access_obj.create(cr, UID_ROOT, values)
|
||||
self._logger.debug("Creating access right for model %s with values: %r", model.model, values)
|
||||
|
||||
def _link_or_copy_current_user_rules(self, cr, uid, group_id, fields_relations, context=None):
|
||||
user_obj = self.pool.get('res.users')
|
||||
def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
|
||||
rule_obj = self.pool.get('ir.rule')
|
||||
current_user = user_obj.browse(cr, uid, uid, context=context)
|
||||
completed_models = set()
|
||||
rules_done = set()
|
||||
for group in current_user.groups_id:
|
||||
for dummy, model in fields_relations:
|
||||
if model.id in completed_models:
|
||||
continue
|
||||
completed_models.add(model.id)
|
||||
for rule in group.rule_groups:
|
||||
if rule.model_id == model.id:
|
||||
if rule.id in rules_done:
|
||||
continue
|
||||
rules_done.add(rule.id)
|
||||
if rule.model_id.id == model.id:
|
||||
if 'user.' in rule.domain_force:
|
||||
# Above pattern means there is likely a condition
|
||||
# specific to current user, so we must copy the rule using
|
||||
# the evaluated version of the domain.
|
||||
# And it's better to copy one time too much than too few
|
||||
rule_obj.copy(cr, 1, rule.id, default={
|
||||
'name': '%s (%s)' %(rule.name, _('(Copy for sharing)')),
|
||||
rule_obj.copy(cr, UID_ROOT, rule.id, default={
|
||||
'name': '%s %s' %(rule.name, _('(Copy for sharing)')),
|
||||
'groups': [(6,0,[group_id])],
|
||||
'domain_force': rule.domain, # evaluated version!
|
||||
})
|
||||
self.__logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
|
||||
self._logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
|
||||
else:
|
||||
# otherwise we can simply link the rule to keep it dynamic
|
||||
rule_obj.write(cr, 1, [rule.id], {
|
||||
'groups': [(4,group_id)]
|
||||
})
|
||||
self.__logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
|
||||
self._logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
|
||||
|
||||
def _create_indirect_sharing_rules(self, cr, uid, wizard_data, group_id, fields_relations, context=None):
|
||||
user_obj = self.pool.get('res.users')
|
||||
current_user = user_obj.browse(cr, uid, uid, context=context)
|
||||
def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
|
||||
"""Verifies that the given rule only belongs to the given group_id, otherwise
|
||||
duplicate it for the current group, and unlink the previous one.
|
||||
The duplicated rule has the original domain copied verbatim, without
|
||||
any evaluation.
|
||||
Returns the final rule to use (browse_record), either the original one if it
|
||||
only belongs to this group, or the copy."""
|
||||
if len(rule.groups) == 1:
|
||||
return rule
|
||||
# duplicate it first:
|
||||
rule_obj = self.pool.get('ir.rule')
|
||||
new_id = rule_obj.copy(cr, UID_ROOT, rule.id,
|
||||
default={
|
||||
'name': '%s %s' %(rule.name, _('(Duplicated for modified sharing permissions)')),
|
||||
'groups': [(6,0,[group_id])],
|
||||
'domain_force': rule.domain_force, # non evaluated!
|
||||
})
|
||||
self._logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
|
||||
# then disconnect from group_id:
|
||||
rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
|
||||
return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
|
||||
|
||||
def _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, restrict=False, rule_name=None, context=None):
|
||||
"""Add a new ir.rule entry for model_id and domain on the target group_id.
|
||||
If ``restrict`` is True, instead of adding a rule, the domain is
|
||||
combined with AND operator with all existing rules in the group, to implement
|
||||
an additional restriction (as of 6.1, multiple rules in the same group are
|
||||
OR'ed by default, so a restriction must alter all existing rules)
|
||||
|
||||
This is necessary because the personal rules of the user that is sharing
|
||||
are first copied to the new share group. Afterwards the filters used for
|
||||
sharing are applied as an additional layer of rules, which are likely to
|
||||
apply to the same model. The default rule algorithm would OR them (as of 6.1),
|
||||
which would result in a combined set of permission that could be larger
|
||||
than those of the user that is sharing! Hence we must forcefully AND the
|
||||
rules at this stage.
|
||||
One possibly undesirable effect can appear when sharing with a
|
||||
pre-existing group, in which case altering pre-existing rules would not
|
||||
be desired. This is addressed in the portal module.
|
||||
"""
|
||||
if rule_name is None:
|
||||
rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
|
||||
(current_user.name, current_user.login, group_id)
|
||||
rule_obj = self.pool.get('ir.rule')
|
||||
rule_ids = rule_obj.search(cr, UID_ROOT, [('groups', 'in', group_id), ('model_id', '=', model_id)])
|
||||
if rule_ids:
|
||||
for rule in rule_obj.browse(cr, UID_ROOT, rule_ids, context=context):
|
||||
if rule.domain_force == domain:
|
||||
# don't create it twice!
|
||||
if restrict:
|
||||
continue
|
||||
else:
|
||||
self._logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
|
||||
return
|
||||
if restrict:
|
||||
# restricting existing rules is done by adding the clause
|
||||
# with an AND, but we can't alter the rule if it belongs to
|
||||
# other groups, so we duplicate if needed
|
||||
rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context)
|
||||
eval_ctx = rule_obj._eval_context_for_combinations()
|
||||
org_domain = expression.normalize(eval(rule.domain_force, eval_ctx))
|
||||
new_clause = expression.normalize(eval(domain, eval_ctx))
|
||||
combined_domain = expression.AND([new_clause, org_domain])
|
||||
rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
|
||||
self._logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
|
||||
if not restrict:
|
||||
# Adding the new rule in the group is ok for normal cases, because rules
|
||||
# in the same group and for the same model will be combined with OR
|
||||
# (as of v6.1), so the desired effect is achieved.
|
||||
rule_obj.create(cr, UID_ROOT, {
|
||||
'name': rule_name,
|
||||
'model_id': model_id,
|
||||
'domain_force': domain,
|
||||
'groups': [(4,group_id)]
|
||||
})
|
||||
self._logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
|
||||
|
||||
def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
|
||||
rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
|
||||
(current_user.name, current_user.login, group_id)
|
||||
try:
|
||||
domain = safe_eval(wizard_data.domain)
|
||||
if domain:
|
||||
domain_expr = expression(domain)
|
||||
domain_expr = expression.expression(domain)
|
||||
for rel_field, model in fields_relations:
|
||||
related_domain = []
|
||||
if not rel_field: continue
|
||||
for element in domain:
|
||||
if domain_expr._is_leaf(element):
|
||||
left, operator, right = element
|
||||
left, operator, right = element
|
||||
left = '%s.%s'%(rel_field, left)
|
||||
element = left, operator, right
|
||||
related_domain.append(element)
|
||||
rule_obj.create(cr, 1, {
|
||||
'name': _('Indirect sharing filter created by user %s (%s) for group %s') % \
|
||||
(current_user.name, current_user.login, group_id),
|
||||
'model_id': model.id,
|
||||
'domain_force': str(related_domain),
|
||||
'groups': [(4,group_id)]
|
||||
})
|
||||
self.__logger.debug("Created indirect rule on model %s with domain: %s", model.model, repr(related_domain))
|
||||
self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
|
||||
group_id, model_id=model.id, domain=str(related_domain),
|
||||
rule_name=rule_name, restrict=True, context=context)
|
||||
except Exception:
|
||||
self.__logger.exception('Failed to create share access')
|
||||
raise osv.except_osv(_('Sharing access could not be setup'),
|
||||
self._logger.exception('Failed to create share access')
|
||||
raise osv.except_osv(_('Sharing access could not be created'),
|
||||
_('Sorry, the current screen and filter you are trying to share are not supported at the moment.\nYou may want to try a simpler filter.'))
|
||||
|
||||
def _create_result_lines(self, cr, uid, wizard_data, context=None):
|
||||
user_obj = self.pool.get('res.users')
|
||||
result_obj = self.pool.get('share.wizard.result.line')
|
||||
def _finish_result_lines(self, cr, uid, wizard_data, share_group_id, context=None):
|
||||
"""Perform finalization of the share, and populate the summary"""
|
||||
share_root_url = wizard_data.share_root_url
|
||||
format_url = '%(login)s' in share_root_url\
|
||||
and '%(password)s' in share_root_url\
|
||||
and '%(dbname)s' in share_root_url
|
||||
existing_passwd_str = _('*usual password*')
|
||||
if wizard_data.user_type == 'new':
|
||||
for email in wizard_data.new_users.split('\n'):
|
||||
user_id = user_obj.search(cr, 1, [('login', '=', email)], context=context)
|
||||
password = user_obj.read(cr, 1, user_id[0], ['password'])['password']
|
||||
share_url = share_root_url % \
|
||||
{'login': email,
|
||||
'password': password,
|
||||
'dbname': cr.dbname} if format_url else share_root_url
|
||||
result_obj.create(cr, uid, {
|
||||
'share_wizard_id': wizard_data.id,
|
||||
'login': email,
|
||||
'password': password,
|
||||
'share_url': share_url,
|
||||
}, context=context)
|
||||
else:
|
||||
# existing users
|
||||
for user in wizard_data.user_ids:
|
||||
share_url = share_root_url % \
|
||||
{'login': user.login,
|
||||
'password': '',
|
||||
'dbname': cr.dbname} if format_url else share_root_url
|
||||
result_obj.create(cr, uid, {
|
||||
'share_wizard_id': wizard_data.id,
|
||||
'login': user.login,
|
||||
'password': existing_passwd_str,
|
||||
'share_url': share_url,
|
||||
'newly_created': False,
|
||||
}, context=context)
|
||||
format_url = '%(dbname)s' in share_root_url
|
||||
for result in wizard_data.result_line_ids:
|
||||
# share_root_url may contain placeholders for dbname, login and password
|
||||
share_url = (share_root_url % \
|
||||
{'login': quote_plus(result.user_id.login),
|
||||
'password': '', # kept empty for security reasons
|
||||
'dbname': quote_plus(cr.dbname)}) \
|
||||
if format_url else share_root_url
|
||||
result.write({'share_url': share_url}, context=context)
|
||||
|
||||
def _check_preconditions(self, cr, uid, wizard_data, context=None):
|
||||
self._assert(wizard_data.action_id and wizard_data.access_mode,
|
||||
_('Action and Access Mode are required to create a shared access'),
|
||||
context=context)
|
||||
self._assert(self.has_share(cr, uid, context=context),
|
||||
_('You must be a member of the Share/User group to use the share wizard'),
|
||||
context=context)
|
||||
if wizard_data.user_type == 'emails':
|
||||
self._assert(wizard_data.new_users,
|
||||
_('Please indicate the emails of the persons to share with, one per line'),
|
||||
context=context)
|
||||
|
||||
def _create_share_users_group(self, cr, uid, wizard_data, context=None):
|
||||
"""Creates the appropriate share group and share users, and populates
|
||||
result_line_ids of wizard_data with one line for each user.
|
||||
|
||||
:return: the new group id (to which the shared access should be granted)
|
||||
"""
|
||||
group_id = self._create_share_group(cr, uid, wizard_data, context=context)
|
||||
# First create any missing user, based on the email addresses provided
|
||||
new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
|
||||
# Finally, setup the new action and shortcut for the users.
|
||||
if existing_ids:
|
||||
# existing users still need to join the new group
|
||||
self.pool.get('res.users').write(cr, UID_ROOT, existing_ids, {
|
||||
'groups_id': [(4,group_id)],
|
||||
})
|
||||
# existing user don't need their home action replaced, only a new shortcut
|
||||
self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
|
||||
if new_ids:
|
||||
# new users need a new shortcut AND a home action
|
||||
self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
|
||||
return group_id
|
||||
|
||||
def go_step_2(self, cr, uid, ids, context=None):
|
||||
wizard_data = self.browse(cr, uid, ids and ids[0], context=context)
|
||||
assert wizard_data.action_id and wizard_data.access_mode and \
|
||||
((wizard_data.user_type == 'new' and wizard_data.new_users) or \
|
||||
(wizard_data.user_type == 'existing' and wizard_data.user_ids))
|
||||
wizard_data = self.browse(cr, uid, ids[0], context=context)
|
||||
self._check_preconditions(cr, uid, wizard_data, context=context)
|
||||
|
||||
# Create shared group and users
|
||||
group_id = self._create_share_group(cr, uid, wizard_data, context=context)
|
||||
user_obj = self.pool.get('res.users')
|
||||
current_user = user_obj.browse(cr, uid, uid, context=context)
|
||||
if wizard_data.user_type == 'new':
|
||||
user_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
|
||||
else:
|
||||
user_ids = [x.id for x in wizard_data.user_ids]
|
||||
# reset home action to regular menu as user needs access to multiple items
|
||||
user_obj.write(cr, 1, user_ids, {
|
||||
'groups_id': [(4,group_id)],
|
||||
})
|
||||
self._setup_action_and_shortcut(cr, uid, wizard_data, user_ids,
|
||||
(wizard_data.user_type == 'new'), context=context)
|
||||
group_id = self._create_share_users_group(cr, uid, wizard_data, context=context)
|
||||
|
||||
current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
|
||||
model_obj = self.pool.get('ir.model')
|
||||
model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0]
|
||||
@@ -379,59 +618,68 @@ class share_create(osv.osv_memory):
|
||||
# ACCESS RIGHTS
|
||||
# We have several classes of objects that should receive different access rights:
|
||||
# Let:
|
||||
# - [obj0] be the target model itself
|
||||
# - [obj0] be the target model itself (and its parents via _inherits, if any)
|
||||
# - [obj1] be the target model and all other models recursively accessible from
|
||||
# obj0 via one2many relationships
|
||||
# - [obj2] be the target model and all other models recursively accessible from
|
||||
# obj0 via one2many and many2many relationships
|
||||
# - [obj3] be all models recursively accessible from obj1 via many2one relationships
|
||||
# (currently not used)
|
||||
obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
|
||||
mode = wizard_data.access_mode
|
||||
|
||||
# Add access to [obj0] and [obj1] according to chosen mode
|
||||
# Add access to [obj0] and [obj1] according to chosen mode
|
||||
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
|
||||
self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
|
||||
|
||||
# Add read-only access (always) to [obj2] and [obj3]
|
||||
# Add read-only access (always) to [obj2]
|
||||
self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
|
||||
self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj3, context=context)
|
||||
|
||||
|
||||
# IR.RULES
|
||||
# A. On [obj0]: 1 rule with domain of shared action
|
||||
# B. For each model in [obj1]: 1 rule in the form:
|
||||
# A. On [obj0], [obj1], [obj2]: add all rules from all groups of
|
||||
# the user that is sharing
|
||||
# Warning: rules must be copied instead of linked if they contain a reference
|
||||
# to uid or if the rule is shared with other groups (and it must be replaced correctly)
|
||||
# B. On [obj0]: 1 rule with domain of shared action
|
||||
# C. For each model in [obj1]: 1 rule in the form:
|
||||
# many2one_rel.domain_of_obj0
|
||||
# where many2one_rel is the many2one used in the definition of the
|
||||
# one2many, and domain_of_obj0 is the sharing domain
|
||||
# For example if [obj0] is project.project with a domain of
|
||||
# For example if [obj0] is project.project with a domain of
|
||||
# ['id', 'in', [1,2]]
|
||||
# then we will have project.task in [obj1] and we need to create this
|
||||
# then we will have project.task in [obj1] and we need to create this
|
||||
# ir.rule on project.task:
|
||||
# ['project_id.id', 'in', [1,2]]
|
||||
# C. And on [obj0], [obj1], [obj2], [obj3]: add all rules from all groups of
|
||||
# the user that is sharing
|
||||
# (Warning: rules must be copied instead of linked if they contain a reference
|
||||
# to uid, and it must be replaced correctly)
|
||||
rule_obj = self.pool.get('ir.rule')
|
||||
|
||||
# A.
|
||||
rule_obj.create(cr, 1, {
|
||||
'name': _('Sharing filter created by user %s (%s) for group %s') % \
|
||||
(current_user.name, current_user.login, group_id),
|
||||
'model_id': model.id,
|
||||
'domain_force': wizard_data.domain,
|
||||
'groups': [(4,group_id)]
|
||||
})
|
||||
all_relations = obj0 + obj1 + obj2
|
||||
self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
|
||||
# B.
|
||||
self._create_indirect_sharing_rules(cr, uid, wizard_data, group_id, obj1, context=context)
|
||||
main_domain = wizard_data.domain if wizard_data.domain != '[]' else DOMAIN_ALL
|
||||
self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
|
||||
group_id, model_id=model.id, domain=main_domain,
|
||||
restrict=True, context=context)
|
||||
# C.
|
||||
all_relations = obj0 + obj1 + obj2 + obj3
|
||||
self._link_or_copy_current_user_rules(cr, uid, group_id, all_relations, context=context)
|
||||
# so far, so good -> populate summary results and return them
|
||||
self._create_result_lines(cr, uid, wizard_data, context=context)
|
||||
self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
|
||||
|
||||
# so far, so good -> check summary results and return them
|
||||
self._finish_result_lines(cr, uid, wizard_data, group_id, context=context)
|
||||
|
||||
# update share URL to make it generic
|
||||
generic_share_url = wizard_data.share_root_url % {'dbname': cr.dbname,
|
||||
'login': '',
|
||||
'password': ''}
|
||||
wizard_data.write({'share_root_url': generic_share_url}, context=context)
|
||||
|
||||
# refresh wizard_data
|
||||
wizard_data = self.browse(cr, uid, ids[0], context=context)
|
||||
|
||||
# send the confirmation emails:
|
||||
self.send_emails(cr, uid, wizard_data, context=context)
|
||||
|
||||
dummy, step2_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step2_form')
|
||||
return {
|
||||
'name': _('Sharing Wizard - Step 2'),
|
||||
'name': _('Shared access created!'),
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'share.wizard',
|
||||
@@ -442,40 +690,55 @@ class share_create(osv.osv_memory):
|
||||
'target': 'new'
|
||||
}
|
||||
|
||||
def send_emails(self, cr, uid, ids, context=None):
|
||||
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
def send_emails(self, cr, uid, wizard_data, context=None):
|
||||
self._logger.info('Sending share notifications by email...')
|
||||
user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
|
||||
if not user.user_email:
|
||||
raise osv.except_osv(_('Email required'), _('The current user must have an email address configured in User Preferences to be able to send outgoing emails.'))
|
||||
for wizard_data in self.browse(cr, uid, ids, context=context):
|
||||
for result_line in wizard_data.result_line_ids:
|
||||
email_to = result_line.login
|
||||
subject = _('%s has shared OpenERP %s information with you') % (user.name, wizard_data.action_id.name)
|
||||
body = _("Dear,\n\n%s\n\n") % subject
|
||||
body += _("To access it, you can go to the following URL:\n %s") % result_line.share_url
|
||||
body += "\n\n"
|
||||
if result_line.newly_created:
|
||||
body += _("You may use the following login and password to get access to this protected area:\n")
|
||||
body += "%s: %s" % (_("Username"), result_line.login) + "\n"
|
||||
body += "%s: %s" % (_("Password"), result_line.password) + "\n"
|
||||
body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
|
||||
else:
|
||||
body += _("This additional data has been automatically added to your current access.\n")
|
||||
body += _("You may use your existing login and password to view it. As a reminder, your login is %s.\n") % result_line.login
|
||||
|
||||
if not tools.email_send(
|
||||
user.user_email,
|
||||
[email_to],
|
||||
subject,
|
||||
body):
|
||||
self.__logger.warning('Failed to send sharing email from %s to %s', user.user_email, email_to)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
share_create()
|
||||
# TODO: also send an HTML version of this mail
|
||||
emails_sent = 0
|
||||
for result_line in wizard_data.result_line_ids:
|
||||
email_to = result_line.user_id.user_email
|
||||
subject = wizard_data.name
|
||||
body = _("Hello,")
|
||||
body += "\n\n"
|
||||
body += _("I've shared %s with you!") % wizard_data.name
|
||||
body += "\n\n"
|
||||
body += _("The documents are not attached, you can view them online directly on my OpenERP server at:")
|
||||
body += "\n " + result_line.share_url
|
||||
body += "\n\n"
|
||||
if wizard_data.message:
|
||||
body += wizard_data.message
|
||||
body += "\n\n"
|
||||
if result_line.newly_created:
|
||||
body += _("These are your credentials to access this protected area:\n")
|
||||
body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
|
||||
body += "%s: %s" % (_("Password"), result_line.password) + "\n"
|
||||
body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
|
||||
else:
|
||||
body += _("The documents have been automatically added to your current OpenERP documents.\n")
|
||||
body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
|
||||
body += "\n\n"
|
||||
body += user.signature
|
||||
body += "\n\n"
|
||||
body += "--\n"
|
||||
body += _("OpenERP is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
|
||||
"It is open source and can be found on http://www.openerp.com.")
|
||||
|
||||
if tools.email_send(user.user_email, [email_to], subject, body):
|
||||
emails_sent += 1
|
||||
else:
|
||||
self._logger.warning('Failed to send share notification from %s to %s, ignored', user.user_email, email_to)
|
||||
self._logger.info('%s share notification(s) successfully sent.', emails_sent)
|
||||
share_wizard()
|
||||
|
||||
class share_result_line(osv.osv_memory):
|
||||
_name = 'share.wizard.result.line'
|
||||
_rec_name = 'login'
|
||||
_rec_name = 'user_id'
|
||||
_columns = {
|
||||
'login': fields.char('Username', size=64, required=True, readonly=True),
|
||||
'user_id': fields.many2one('res.users', required=True, readonly=True),
|
||||
'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
|
||||
'password': fields.char('Password', size=64, readonly=True),
|
||||
'share_url': fields.char('Share URL', size=512, required=True),
|
||||
'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="model">share.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Share wizard: step 0">
|
||||
<form string="Sharing: preparation">
|
||||
<separator colspan="4"
|
||||
string="Please select the action that opens the screen containing the data you want to share."/>
|
||||
<field name="action_id" colspan="4"/>
|
||||
@@ -28,25 +28,24 @@
|
||||
<field name="model">share.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Share wizard: step 1">
|
||||
<separator colspan="4" string="Who would you want to share this data with?"/>
|
||||
<field name="user_type"/>
|
||||
<group colspan="4" attrs="{'invisible':[('user_type','!=','existing')]}">
|
||||
<separator colspan="4" string="Existing External Users"/>
|
||||
<field colspan="4" nolabel="1" name="user_ids" mode="tree"/>
|
||||
<form string="Grant instant access to your documents">
|
||||
<group colspan="4" name="emails_group" attrs="{'invisible':[('user_type','!=','emails')]}">
|
||||
<separator colspan="4" string="Share with these people (one e-mail per line)"/>
|
||||
<field colspan="4" nolabel="1" name="new_users" attrs="{'required':[('user_type','=','emails')]}"/>
|
||||
</group>
|
||||
<group colspan="4" attrs="{'invisible':[('user_type','!=','new')]}">
|
||||
<separator colspan="4" string="New Users (please provide one e-mail address per line below)"/>
|
||||
<field colspan="4" nolabel="1" name="new_users"/>
|
||||
<separator colspan="4" string="Optional: include a personal message"/>
|
||||
<group colspan="4" col="4">
|
||||
<field name="message" colspan="4" nolabel="1"/>
|
||||
</group>
|
||||
<separator colspan="4" string="Select the desired shared access mode:"/>
|
||||
<group colspan="4">
|
||||
<field name="access_mode"/>
|
||||
<separator colspan="4" string="Extra Options"/>
|
||||
<group colspan="4" col="4">
|
||||
<field name="name" colspan="4"/>
|
||||
<field name="access_mode" colspan="4"/>
|
||||
</group>
|
||||
<separator colspan="4"/>
|
||||
<group colspan="4">
|
||||
<button special="cancel" string="Cancel" icon='gtk-cancel'/>
|
||||
<button name="go_step_2" string="Finish" colspan="1" type="object" icon="gtk-go-forward"/>
|
||||
<button name="go_step_2" string="Share" colspan="1" type="object" icon="gtk-go-forward"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
@@ -57,26 +56,19 @@
|
||||
<field name="model">share.wizard</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Share wizard: step 2">
|
||||
<separator colspan="4" string="Congratulations, you have successfully setup a new shared access!"/>
|
||||
<label colspan="4" string="Here is a summary of the access points you have just created:"/>
|
||||
<form string="Access granted!">
|
||||
<separator colspan="4" string="An e-mail notification with instructions has been sent to the following people:"/>
|
||||
<field name="result_line_ids" nolabel="1" colspan="4" mode="tree">
|
||||
<tree string="Summary">
|
||||
<field name="login"/>
|
||||
<field name="password"/>
|
||||
<field name="share_url"/>
|
||||
</tree>
|
||||
<form string="Access info">
|
||||
<field name="login"/>
|
||||
<field name="password"/>
|
||||
<field name="share_url" colspan="4"/>
|
||||
</form>
|
||||
</field>
|
||||
<field colspan="4" name="share_root_url"/>
|
||||
<separator colspan="4"/>
|
||||
<group colspan="4">
|
||||
<button special="cancel" string="Close" icon='gtk-ok'/>
|
||||
<button name="send_emails" string="Send Email Notification(s)" colspan="1" type="object" icon="gtk-go-forward"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
@@ -114,8 +106,5 @@
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<!-- temporarily under Low-Level-Actions -->
|
||||
<menuitem action="action_share_wizard" id="menu_action_share_wizard" parent="base.next_id_4" groups="group_share_user" icon="terp-rating-rated"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
Reference in New Issue
Block a user