[MERGE] features: new/updated portal and share modules

bzr revid: odo@openerp.com-20110624141403-5bemanedpsyw4xeb
This commit is contained in:
Olivier Dony
2011-06-24 16:14:03 +02:00
21 changed files with 1529 additions and 292 deletions
+24
View File
@@ -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
+50
View File
@@ -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:
+53
View File
@@ -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()
+246
View File
@@ -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]
+93
View File
@@ -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 &amp; 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_portal_manager access.portal.manager model_res_portal group_portal_manager 1 1 1 1
3 access_portal_all access.portal.all model_res_portal 1 0 0 0
4 access_portal_widget_all access.portal.widget.all model_res_portal_widget 1 0 0 0
5 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>
+23
View File
@@ -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
+59
View File
@@ -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()
+189
View File
@@ -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>
+51
View File
@@ -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>
+1
View File
@@ -19,5 +19,6 @@
#
##############################################################################
import ir_model
import res_users
import wizard
+1 -1
View File
@@ -22,7 +22,7 @@
{
"name" : "Sharing Tools",
"version" : "1.3",
"version" : "1.5",
"depends" : ["base"],
"author" : "OpenERP SA",
"category": 'Tools',
+46
View File
@@ -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()
+27 -3
View File
@@ -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 -2
View File
@@ -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>
+3 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+15 -26
View File
@@ -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>