[IMP] share: improved shared wizard

New features: can share to groups if user has "Share / User (Extended)"
group. Warning message if user does not have email and cannot share by
email. Names of sharing groups are now hidden in ORM access denied messages.
Better error reporting.
UI: more user-friendly labels, titles. More visibible button in web client.
Bugfixes: better handling of sharing objects with _inherits, can combine
ir.rules if already exist for same (object,group). Button is now hidden
in web client if user is not in "Share/User" group.

bzr revid: odo@openerp.com-20110408133608-vmdrzfeo0dz28wer
This commit is contained in:
Olivier Dony 2011-04-08 15:36:08 +02:00
parent 15e686e1b0
commit 6922668a77
7 changed files with 448 additions and 154 deletions

View File

@ -19,5 +19,6 @@
#
##############################################################################
import ir_model
import res_users
import wizard

46
addons/share/ir_model.py Normal file
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()

View File

@ -3,7 +3,16 @@
<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>
<record id="group_share_user_extended" model="res.groups">
<field name="name">Sharing / User (Extended)</field>
<field name="comment">
Members of this groups have access to the sharing wizard in extended mode, so they can not only share their documents with external users, but they can also share their documents with other groups.
This could result in an alteration of the system's security policy, so should be granted only to trustworthy users who know the security policy very well.
This group should not be deleted.</field>
</record>
</data>
</openerp>
</openerp>

View File

@ -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')

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(
'</h1>',
output.index(self.MAIN_FORM_BODY)) - 1
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))

View File

@ -29,7 +29,9 @@ 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
RANDOM_PASS_CHARACTERS = [chr(x) for x in range(48, 58) + range(97, 123) + range(65, 91)]
@ -50,30 +52,68 @@ class share_create(osv.osv_memory):
_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_extended_share(self, cr, uid, context=None):
return self.has_group(cr, uid, module='share', group_xml_id='group_share_user_extended', context=context)
def _has_email(self, cr, uid, ids, name, arg, context=None):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
has_email = bool(user.user_email)
return dict([(id,has_email) for id in ids])
def _user_type_selection(self, cr, uid, context=None):
result = [('new','New users (emails required)'),
('existing','Existing external users')]
if self.has_extended_share(cr, uid, context=context):
result.append(('groups','Existing groups of users'))
return result
_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_type': fields.selection(_user_type_selection,'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)]),
'group_ids': fields.many2many('res.groups', 'share_wizard_res_group_rel', 'share_id', 'group_id', 'Existing groups', domain=[('share', '=', False)]),
'new_users': fields.text("New users"),
'access_mode': fields.selection([('readwrite','Read & Write'),('readonly','Read-only')],'Access Mode'),
'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('Generic Share Access URL', size=512, readonly=True, tooltip='Main access page for users that are granted shared access'),
# used to display a warning message at first step
'has_user_email': fields.function(_has_email, string='Has email', method=True, type="boolean"),
}
_defaults = {
'user_type' : lambda self, cr, uid, *a: 'existing' if self.pool.get('res.users').search(cr, uid, [('share', '=', True)]) else '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': _('Configure shared access'),
'view_type': 'form',
'view_mode': 'form',
'res_model': 'share.wizard',
@ -88,7 +128,7 @@ class share_create(osv.osv_memory):
group_obj = self.pool.get('res.groups')
share_group_name = '%s: %s (%d-%s)' %('Sharing', wizard_data.action_id.res_model, 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):
user_obj = self.pool.get('res.users')
@ -97,11 +137,11 @@ class share_create(osv.osv_memory):
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, {
existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
self._assert(not existing,
_('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,
context=context)
user_id = user_obj.create(cr, UID_ROOT, {
'login': new_user,
'password': generate_random_pass(),
'name': new_user,
@ -124,54 +164,81 @@ 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 }
menu_id = menu_obj.create(cr, UID_ROOT, menu_data)
sc_data= {'name': values['name'], 'sequence': UID_ROOT,'res_id': menu_id }
sc_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 (literal_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.
: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, {'uid': user_id})
result = dict(context)
for key in context:
# Remove all context keys that seem to toggle default
# filters based on the current user, which make no sense
# for shared users
if key and key.startswith('search_default_') and 'user_id' in key:
result.pop(key)
except (NameError, ValueError):
self.__logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
result = context_str
return result
def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, new_users, 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."""
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,
}
for user_id in user_ids:
values = {
'name': (_('%s (Shared)') % wizard_data.action_id.name)[:64],
'domain': wizard_data.domain,
'context': self._cleanup_action_context(wizard_data.action_id.context, user_id),
'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,
}
action_id = self._create_shortcut(cr, user_id, values)
if new_users:
user_obj.write(cr, 1, [user_id], {'action_id': action_id})
user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
else:
user_obj.write(cr, 1, [user_id], {'action_id': menu_action_id})
user_obj.write(cr, UID_ROOT, [user_id], {'action_id': menu_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 = []
@ -180,20 +247,41 @@ class share_create(osv.osv_memory):
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]
relation_model_id = model_obj.search(cr, UID_ROOT, [('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)
# TODO: add some filtering for m2m and m2o - not always possible...
relation_field = None
model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
local_rel_fields.append((relation_field, model_browse))
for parent in self.pool.get(model_browse.model)._inherits:
if parent not in models:
parent_model = self.pool.get(parent)
parent_model_browse = model_obj.browse(cr, UID_ROOT,
model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
if relation_field and (field._fields_id in parent_model._columns or \
field._fields_id in parent_model_inherit_fields):
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 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)
return local_rel_fields
def _get_relationship_classes(self, cr, uid, model, context=None):
# obj0 class and its parents
obj0 = [(None, model)]
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'], 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)
@ -215,15 +303,14 @@ 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,
@ -251,13 +338,11 @@ 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)
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()
for group in current_user.groups_id:
for dummy, model in fields_relations:
@ -271,7 +356,7 @@ class share_create(osv.osv_memory):
# 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={
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!
@ -284,10 +369,46 @@ class share_create(osv.osv_memory):
})
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 _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, rule_name=None, context=None):
rule_obj = self.pool.get('ir.rule')
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)
# if the target group already has one or more rules for the given model,
# we should instead add the new domain to each rule with OR operator to
# achieve the desired effect, otherwise they would be AND'ed as happens
# for any pair of rules on the same group for the same model.
# Indeed, A v (B /\ C) == (A v B) /\ (A v C)
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:
# skip identical ones!
continue
# sanity check: the rule we are about to modify must not be used by another group
self._assert(len(rule.groups) == 1,
_('Sorry, the selected group(s) currently have security rules in conflict with '\
'the access point you are adding, and these rules cannot be altered because they are used '\
'by other groups as well. Please correct it and make sure each group does not share any '\
'security rule with other groups (global rules are fine).'), context=context)
# combine both domains with 'OR'
combined_domain = rule_obj.domain_disjunction(cr, UID_ROOT, rule.domain_force, domain)
rule.write({'domain_force': combined_domain}, context=context)
self.__logger.debug("Combined new sharing rule on model %s with domain: %s with existing one(s): %r", model_id, domain, combined_domain)
else:
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_obj = self.pool.get('ir.rule')
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:
@ -296,21 +417,16 @@ class share_create(osv.osv_memory):
related_domain = []
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, context=context)
except Exception:
self.__logger.exception('Failed to create share access')
raise osv.except_osv(_('Sharing access could not be setup'),
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):
@ -322,9 +438,10 @@ class share_create(osv.osv_memory):
and '%(dbname)s' in share_root_url
existing_passwd_str = _('*usual password*')
if wizard_data.user_type == 'new':
# new users
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']
user_id = user_obj.search(cr, UID_ROOT, [('login', '=', email)], context=context)
password = user_obj.read(cr, UID_ROOT, user_id[0], ['password'])['password']
share_url = share_root_url % \
{'login': email,
'password': password,
@ -335,7 +452,7 @@ class share_create(osv.osv_memory):
'password': password,
'share_url': share_url,
}, context=context)
else:
elif wizard_data.user_type == 'existing':
# existing users
for user in wizard_data.user_ids:
share_url = share_root_url % \
@ -349,28 +466,75 @@ class share_create(osv.osv_memory):
'share_url': share_url,
'newly_created': False,
}, context=context)
else:
# existing groups
for group in wizard_data.group_ids:
for user in group.users:
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)
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 point'),
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 == 'new':
self._assert(wizard_data.new_users,
_('Please indicate the emails of the persons to share with, one per line'),
context=context)
elif 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)
def _create_share_users_groups(self, cr, uid, wizard_data, context=None):
"""Create the appropriate shared users and groups, and return the new group_ids)
to which the shared access should be granted.
"""
group_ids = []
user_ids = []
if wizard_data.user_type == 'groups':
group_id = None
group_ids.extend([g.id for g in wizard_data.group_ids])
else:
group_id = self._create_share_group(cr, uid, wizard_data, context=context)
group_ids.append(group_id)
if wizard_data.user_type == 'new':
user_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
elif wizard_data.user_type == 'existing':
user_ids = [x.id for x in wizard_data.user_ids]
# reset home action to regular menu as user needs access to multiple items
self.pool.get('res.users').write(cr, UID_ROOT, user_ids, {
'groups_id': [(4,group_id)],
})
if user_ids:
self._setup_action_and_shortcut(cr, uid, wizard_data, user_ids,
(wizard_data.user_type == 'new'), context=context)
return group_ids
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))
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_ids = self._create_share_users_groups(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,7 +543,7 @@ 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
@ -388,50 +552,47 @@ class share_create(osv.osv_memory):
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
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]
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:
# 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
# ['id', 'in', [1,2]]
# 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)]
})
# B.
self._create_indirect_sharing_rules(cr, uid, wizard_data, group_id, obj1, context=context)
# C.
all_relations = obj0 + obj1 + obj2 + obj3
self._link_or_copy_current_user_rules(cr, uid, group_id, all_relations, context=context)
for group_id in group_ids:
# 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]
self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, 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:
# 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
# ['id', 'in', [1,2]]
# 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]: 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)
# A.
self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
group_id, model_id=model.id, domain=wizard_data.domain,
context=context)
# B.
self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
# C.
all_relations = obj0 + obj1 + obj2
self._link_or_copy_current_user_rules(cr, current_user, 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)
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',

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="Share wizard: 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,41 @@
<field name="model">share.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Share wizard: step 1">
<form string="Setup shared access">
<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"/>
<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','!=','new')]}">
<separator colspan="4" string="New Users (please provide one e-mail address per line below)"/>
<field colspan="4" nolabel="1" name="new_users"/>
</group>
<group colspan="4" attrs="{'invisible':[('user_type','!=','groups')]}">
<separator colspan="4" string="Existing Groups"/>
<field colspan="4" nolabel="1" name="group_ids" mode="tree">
<tree string="Existing groups">
<field name="name"/>
</tree>
</field>
</group>
<separator colspan="4" string="Select the desired shared access mode:"/>
<group colspan="4">
<field name="access_mode"/>
</group>
<group colspan="4" attrs="{'invisible':[('has_user_email','!=',False)]}">
<label colspan="4" string="/!\ Sharing via e-mail is disabled because you don't have an email address in your preferences"/>
<field name="has_user_email" invisible="1"/>
</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="Confirm" colspan="1" type="object" icon="gtk-go-forward"/>
</group>
</form>
</field>
@ -57,7 +73,7 @@
<field name="model">share.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Share wizard: step 2">
<form string="Shared access created!">
<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:"/>
<field name="result_line_ids" nolabel="1" colspan="4" mode="tree">
@ -73,10 +89,19 @@
</form>
</field>
<field colspan="4" name="share_root_url"/>
<group colspan="4" attrs="{'invisible':[('has_user_email','!=',False)]}">
<label colspan="4" string="/!\ Sharing via e-mail is disabled because you don't have an email address in your preferences"/>
<field name="has_user_email" invisible="1"/>
</group>
<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"/>
<button name="send_emails" string="Send Email Notification(s)"
colspan="1" type="object"
icon="gtk-go-forward"
help="Email notifications can only be sent if you don't have an email address in your preferences"
attrs="{'readonly':[('has_user_email','!=',True)]}"
/>
</group>
</form>
</field>
@ -115,7 +140,8 @@
</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"/>
<menuitem action="action_share_wizard" id="menu_action_share_wizard" parent="base.next_id_4"
groups="group_share_user,group_share_user_extended" icon="terp-rating-rated"/>
</data>
</openerp>