From ad15fa33857184189b03ba69295f135318de1608 Mon Sep 17 00:00:00 2001 From: Olivier Dony <odo@openerp.com> Date: Sun, 17 Oct 2010 07:13:19 +0200 Subject: [PATCH] [IMP] share: much more complete implementation of share wizard, pending some improvements + web widget creation bzr revid: odo@openerp.com-20101017051319-p4xi0epaqmgvwdmr --- addons/share/__init__.py | 3 +- addons/share/__openerp__.py | 30 +- addons/share/{share.py => res_users.py} | 9 +- addons/share/res_users_view.xml | 44 ++ addons/share/security/ir.model.access.csv | 2 + addons/share/security/share_security.xml | 8 + addons/share/share_view.xml | 4 +- addons/share/wizard/__init__.py | 1 + addons/share/wizard/share_wizard.py | 820 +++++++++++----------- addons/share/wizard/share_wizard_view.xml | 167 +++-- 10 files changed, 572 insertions(+), 516 deletions(-) rename addons/share/{share.py => res_users.py} (74%) create mode 100644 addons/share/res_users_view.xml create mode 100644 addons/share/security/ir.model.access.csv create mode 100644 addons/share/security/share_security.xml diff --git a/addons/share/__init__.py b/addons/share/__init__.py index eff88be137d9..9c0c7bf7da24 100644 --- a/addons/share/__init__.py +++ b/addons/share/__init__.py @@ -18,5 +18,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## -import share + +import res_users import wizard diff --git a/addons/share/__openerp__.py b/addons/share/__openerp__.py index 9dc2a6658d82..4f2eafb4e52f 100644 --- a/addons/share/__openerp__.py +++ b/addons/share/__openerp__.py @@ -21,24 +21,34 @@ { - "name" : "Share Management", + "name" : "Sharing Tools", "version" : "1.1", "depends" : ["base"], "author" : "OpenERP SA", "category": 'Generic Modules', - "description": """The goal is to implement a generic sharing mechanism, where user of OpenERP -can share data from OpenERP to their colleagues, customers, or friends. -The system will work by creating new users and groups on the fly, and by -combining the appropriate access rights and ir.rules to ensure that the /shared -users/ will only have access to the correct data. + "description": """ + + This module adds generic sharing tools to your current OpenERP database, + and specifically a 'share' button that is available in the Web client to + share any kind of OpenERP data with colleagues, customers, friends, etc. + + The system will work by creating new users and groups on the fly, and by + combining the appropriate access rights and ir.rules to ensure that the + shared users only have access to the data that has been shared with them. + + This is extremely useful for collaborative work, knowledge sharing, + synchronization with other companies, etc. + """, 'website': 'http://www.openerp.com', - 'init_xml': [], - 'update_xml': [ + 'data': [ + 'security/share_security.xml', + 'security/ir.model.access.csv', 'share_view.xml', + 'res_users_view.xml', 'wizard/share_wizard_view.xml' - ], + ], 'installable': True, - 'active': False, } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/share/share.py b/addons/share/res_users.py similarity index 74% rename from addons/share/share.py rename to addons/share/res_users.py index 6e3c4932a1e0..816830662203 100644 --- a/addons/share/share.py +++ b/addons/share/res_users.py @@ -18,12 +18,14 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## -from osv import fields, osv, orm +from osv import fields, osv + class res_groups(osv.osv): _name = "res.groups" _inherit = 'res.groups' _columns = { - 'share': fields.boolean('Share') + 'share': fields.boolean('Share Group', groups='share.group_share', readonly=True, + help="Group created to set access rights for sharing data with some users.") } res_groups() @@ -31,6 +33,7 @@ class res_users(osv.osv): _name = 'res.users' _inherit = 'res.users' _columns = { - 'share': fields.boolean('Share') + 'share': fields.boolean('Share User', groups='share.group_share', readonly=True, + help="External user with limited access, created only for the purpose of sharing data.") } res_users() diff --git a/addons/share/res_users_view.xml b/addons/share/res_users_view.xml new file mode 100644 index 000000000000..09733e81a01f --- /dev/null +++ b/addons/share/res_users_view.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <record model="ir.ui.view" id="res_users_search_sharing"> + <field name="name">res.users.search.share</field> + <field name="model">res.users</field> + <field name="type">search</field> + <field name="inherit_id" ref="base.view_users_search"/> + <field name="arch" type="xml"> + <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)]"/> + </field> + </xpath> + </field> + </record> + + <record model="ir.actions.act_window" id="base.action_res_users"> + <field name="context">{'search_default_no_share': 1}</field> + </record> + + <record model="ir.ui.view" id="res_groups_search_sharing"> + <field name="name">res.groups.search.share</field> + <field name="model">res.groups</field> + <field name="type">search</field> + <field name="priority" eval="8"/> + <field name="arch" type="xml"> + <search string="Groups"> + <field name="name"/> + <field name="share"> + <filter name="no_share" string="Regular groups only (no share groups" icon="terp-partner" + domain="[('share','=',False)]"/> + </field> + </search> + </field> + </record> + + <record model="ir.actions.act_window" id="base.action_res_groups"> + <field name="context">{'search_default_no_share': 1}</field> + </record> + + </data> +</openerp> \ No newline at end of file diff --git a/addons/share/security/ir.model.access.csv b/addons/share/security/ir.model.access.csv new file mode 100644 index 000000000000..a8f37d300c69 --- /dev/null +++ b/addons/share/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_share_wizard_user","access_share_wizard_user","model_share_wizard","share.group_share_user",1,1,1,1 \ No newline at end of file diff --git a/addons/share/security/share_security.xml b/addons/share/security/share_security.xml new file mode 100644 index 000000000000..0b2e8c4c1fdb --- /dev/null +++ b/addons/share/security/share_security.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data noupdate="1"> + <record id="group_share_user" model="res.groups"> + <field name="name">Sharing / User</field> + </record> + </data> +</openerp> diff --git a/addons/share/share_view.xml b/addons/share/share_view.xml index 24736b3d53a3..863da6ea747c 100644 --- a/addons/share/share_view.xml +++ b/addons/share/share_view.xml @@ -6,7 +6,7 @@ <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"> + <field name="arch" type="xml"> <field name="name" position="after"> <field name="share"/> </field> @@ -17,7 +17,7 @@ <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"> + <field name="arch" type="xml"> <field name="password" position="after"> <field name="share"/> </field> diff --git a/addons/share/wizard/__init__.py b/addons/share/wizard/__init__.py index b906abf5e56d..2034dfe2fc60 100644 --- a/addons/share/wizard/__init__.py +++ b/addons/share/wizard/__init__.py @@ -20,3 +20,4 @@ ############################################################################## import share_wizard + diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py index add9c4f9352c..2d1fd252ecf3 100644 --- a/addons/share/wizard/share_wizard.py +++ b/addons/share/wizard/share_wizard.py @@ -18,480 +18,448 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## +import logging +import random +import time + +import tools from osv import osv, fields +from osv.expression import expression from tools.translate import _ -import tools +from tools.safe_eval import safe_eval + +FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink') +READ_ONLY_ACCESS = ('perm_read',) + + +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 +def generate_random_pass(): + pass_chars = RANDOM_PASS_CHARACTERS[:] + random.shuffle(pass_chars) + return ''.join(pass_chars[0:10]) -def _generate_random_number(): - import random - 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 - def generate_random_pass(): - pass_chars = RANDOM_PASS_CHARACTERS[:] - random.shuffle(pass_chars) - return ''.join(pass_chars[0:10]) - return generate_random_pass() class share_create(osv.osv_memory): - _name = 'share.create' - _description = 'Create share' - - def _access(self, cr, uid, ids, field_name, arg, context=None): - if context is None: - context = {} - res = {} - action_id = context.get('action_id', False) - access_obj = self.pool.get('ir.model.access') - action_obj = self.pool.get('ir.actions.act_window') - model_obj = self.pool.get('ir.model') - user_obj = self.pool.get('res.users') - current_user = user_obj.browse(cr, uid, uid) - access_ids = [] - if action_id: - action = action_obj.browse(cr, uid, action_id, context=context) - active_model_ids = model_obj.search(cr, uid, [('model','=',action.res_model)]) - active_model_id = active_model_ids and active_model_ids[0] or False - access_ids = access_obj.search(cr, uid, [ - ('group_id','in',map(lambda x:x.id, current_user.groups_id)), - ('model_id','',active_model_id)]) - for rec_id in ids: - write_access = False - read_access = False - for access in access_obj.browse(cr, uid, access_ids, context=context): - if access.perm_write: - write_access = True - if access.perm_read: - read_access = True - res[rec_id]['write_access'] = write_access - res[rec_id]['read_access'] = read_access - return res + __logger = logging.getLogger('share.wizard') + _name = 'share.wizard' + _description = 'Share Wizard' _columns = { - 'action_id': fields.many2one('ir.actions.act_window', 'Action', required=True), - 'domain': fields.char('Domain', size=64), - 'user_type': fields.selection( [ ('existing','Existing'),('new','New')],'User Type'), - 'user_ids': fields.many2many('res.users', 'share_user_rel', 'share_id','user_id', 'Share Users'), - 'new_user': fields.text("New user"), - 'access_mode': fields.selection([('readwrite','READ & WRITE'),('readonly','READ ONLY')],'Access Mode'), - 'write_access': fields.function(_access, method=True, string='Write Access',type='boolean', multi='write_access'), - 'read_access': fields.function(_access, method=True, string='Write Access',type='boolean', multi='read_access'), + '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.one2many('share.wizard.user', 'share_wizard_id', 'Users'), + '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'), + 'share_root_url': fields.char('Generic Share Access URL', size=512, tooltip='Main access page for users that are granted shared access') } _defaults = { - 'user_type' : 'existing', - 'domain': '[]', + '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', + _('Please specify "share_root_url" in server configuration or in context')), + 'action_id': lambda self, cr, uid, context, *a: context.get('action_id'), 'access_mode': 'readonly' - } - def default_get(self, cr, uid, fields, context=None): - """ - To get default values for the object. - """ - - res = super(share_create, self).default_get(cr, uid, fields, context=context) - if not context: - context={} - action_id = context.get('action_id', False) - domain = context.get('domain', '[]') - - - if 'action_id' in fields: - res['action_id'] = action_id - if 'domain' in fields: - res['domain'] = domain - return res - - def do_step_1(self, cr, uid, ids, context=None): - """ - This action to excute step 1 - - """ - if not context: - context = {} - - data_obj = self.pool.get('ir.model.data') - - step1_form_view = data_obj._get_id(cr, uid, 'share', 'share_step1_form') - - if step1_form_view: - step1_form_view_id = data_obj.browse(cr, uid, step1_form_view, context=context).res_id - - step1_id = False - for this in self.browse(cr, uid, ids, context=context): - vals ={ - 'domain': this.domain, - 'action_id': this.action_id and this.action_id.id or False, - } - step1_id = this.id - - context.update(vals) - value = { - 'name': _('Step:2 Sharing Wizard'), + 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'), 'view_type': 'form', 'view_mode': 'form', - 'res_model': 'share.create', + 'res_model': 'share.wizard', 'view_id': False, - 'res_id': step1_id, + 'res_id': ids[0], 'views': [(step1_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')], 'type': 'ir.actions.act_window', - 'context': context, - 'target': 'new' - } - return value - - - def do_step_2(self, cr, uid, ids, context=None): - """ - This action to excute step 2 - - """ - if not context: - context = {} - - data_obj = self.pool.get('ir.model.data') - - step2_form_view = data_obj._get_id(cr, uid, 'share', 'share_step2_form') - - if step2_form_view: - step2_form_view_id = data_obj.browse(cr, uid, step2_form_view, context=context).res_id - - step1_id = False - for this in self.browse(cr, uid, ids, context=context): - vals = { - 'user_type': this.user_type, - 'existing_user_ids': map(lambda x:x.id, this.user_ids), - 'new_user': this.new_user, - } - - context.update(vals) - value = { - 'name': _('Step:3 Sharing Wizard'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'share.create', - 'view_id': False, - 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')], - 'type': 'ir.actions.act_window', - 'context': context, 'target': 'new' } - return value - - - def do_step_3(self, cr, uid, ids, context=None): - """ - This action to excute step 3 - - """ - if not context: - context = {} - - for this in self.browse(cr, uid, ids, context=context): - vals = { - 'access_mode': this.access_mode, - } - - context.update(vals) + def _create_share_group(self, cr, uid, wizard_data, context=None): group_obj = self.pool.get('res.groups') - user_obj = self.pool.get('res.users') - fields_obj = self.pool.get('ir.model.fields') - model_access_obj = self.pool.get('ir.model.access') - model_obj = self.pool.get('ir.model') - rule_obj = self.pool.get('ir.rule') - action_obj = self.pool.get('ir.actions.act_window') - - new_users = context.get('new_user', False) - action_id = context.get('action_id', False) - user_type = context.get('user_type', False) - access_mode = context.get('access_mode', False) - action = action_obj.browse(cr, uid, action_id, context=context) - active_model = action.res_model - - active_id = False #TODO: Pass record id of res_model of action - existing_user_ids = context.get('existing_user_ids', False) - domain = eval(context.get('domain', '[]')) - - # Create Share Group - share_group_name = '%s: %s' %('Sharing', active_model) - group_ids = group_obj.search(cr, uid, [('name','=',share_group_name)]) - group_id = group_ids and group_ids[0] or False - if not group_id: - group_id = group_obj.create(cr, uid, {'name': share_group_name, 'share': True}) - else: - group = group_obj.browse(cr, uid, group_id, context=context) - if not group.share: - raise osv.except_osv(_('Error'), _("Share Group is exits without sharing !")) - - # Create new user + 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}) + def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None): + user_obj = self.pool.get('res.users') current_user = user_obj.browse(cr, uid, uid) user_ids = [] - if user_type == 'new' and new_users: - for new_user in new_users.split('\n'): - password = _generate_random_number() - user_id = user_obj.create(cr, uid, { + 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.')) + user_id = user_obj.create(cr, 1, { 'login': new_user, - 'password': password, + 'password': generate_random_pass(), 'name': new_user, 'user_email': new_user, 'groups_id': [(6,0,[group_id])], - 'action_id': action_id, 'share': True, 'company_id': current_user.company_id and current_user.company_id.id }) user_ids.append(user_id) - context['new_user_ids'] = user_ids - - # Modify existing user - if user_type == 'existing': - user_obj.write(cr, uid, existing_user_ids , { - 'groups_id': [(4,group_id)], - 'action_id': action_id - }) - - - #ACCESS RIGHTS / IR.RULES COMPUTATION - - active_model_ids = model_obj.search(cr, uid, [('model','=',active_model)]) - active_model_id = active_model_ids and active_model_ids[0] or False - - def _get_relation(model_id, ttypes, new_obj=[]): - obj = [] - models = map(lambda x:x[1].model, new_obj) - field_ids = fields_obj.search(cr, uid, [('model_id','=',model_id),('ttype','in', ttypes)]) - for field in fields_obj.browse(cr, uid, field_ids, context=context): - if field.relation not in models: - relation_model_ids = model_obj.search(cr, uid, [('model','=',field.relation)]) - relation_model_id = relation_model_ids and relation_model_ids[0] or False - relation_model = model_obj.browse(cr, uid, relation_model_id, context=context) - obj.append((field.relation_field, relation_model)) - - if relation_model_id != model_id and field.ttype in ['one2many', 'many2many']: - obj += _get_relation(relation_model_id, [field.ttype], obj) - - return obj - - active_model = model_obj.browse(cr, uid, active_model_id, context=context) - obj0 = [(None, active_model)] - obj1 = _get_relation(active_model_id, ['one2many']) - obj2 = _get_relation(active_model_id, ['one2many', 'many2many']) - obj3 = _get_relation(active_model_id, ['many2one']) - for rel_field, model in obj1: - obj3 += _get_relation(model.id, ['many2one']) + return user_ids + def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, new_users, context=None): + menu_obj = self.pool.get('ir.ui.menu') + 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: + action_id = menu_obj.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}) + + 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 + during recursion + @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]: + 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], local_rel_fields, suffix=relation_field, context=context) + return local_rel_fields + + def _get_relationship_classes(self, cr, uid, model, context=None): + 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) + for dummy, model in obj1: + obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], context=context) + return obj0, obj1, obj2, obj3 + + def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None): + model_access_obj = self.pool.get('ir.model.access') + user_right_ids = model_access_obj.search(cr, uid, + [('group_id', 'in', group_ids), ('model_id', 'in', model_ids)], + context=context) + user_access_matrix = {} + if user_right_ids: + for access_right in model_access_obj.browse(cr, uid, user_right_ids, context=context): + access_line = user_access_matrix.setdefault(access_right.model_id.model, set()) + for perm in FULL_ACCESS: + if getattr(access_right, perm, 0): + 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): + """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 current_user = user_obj.browse(cr, uid, uid, context=context) - if access_mode == 'readonly': - res = [] - # intersect with read access rights of user running the - # wizard, to avoid adding more access than current - for group in current_user.groups_id: - for access_control in group.model_access: - if access_control.model_id.id in res: - continue - if access_control.perm_read: - res.append(access_control.model_id.id) - model_access_obj.create(cr, uid, { - 'name': 'Read Access of group %s on %s model'%(share_group_name, access_control.model_id.name), - 'model_id' : access_control.model_id.id, - 'group_id' : group_id, - 'perm_read' : True - }) - res = [] - for rel_field, model in obj0+obj1+obj2+obj3: - if model.id in res: - continue - res.append(model.id) - model_access_obj.create(cr, uid, { - 'name': 'Read Access of group %s on %s model'%(share_group_name, model.name), - 'model_id' : model.id, - 'group_id' : group_id, - 'perm_read' : True - }) - if access_mode == 'readwrite': - res = [] - for rel_field, model in obj0+obj1: - if model.id in res: - continue - res.append(model.id) - model_access_obj.create(cr, uid, { - 'name': 'Write Access of group %s on %s model'%(share_group_name, model.name), - 'model_id' : model.id, - 'group_id' : group_id, - 'perm_read' : True, - 'perm_write' : True, - 'perm_unlink' : True, - 'perm_create' : True, - }) - # intersect with access rights of user - # running the wizard, to avoid adding more access than current - - for group in current_user.groups_id: - for access_control in group.model_access: - if access_control.model_id.id in res: - continue - if access_control.perm_read: - res.append(access_control.model_id.id) - model_access_obj.create(cr, uid, { - 'name': 'Read Access of group %s on %s model'%(share_group_name, access_control.model_id.name), - 'model_id' : access_control.model_id.id, - 'group_id' : group_id, - 'perm_read' : True - }) - for rel_field, model in obj2+obj3: - if model.id in res: - continue - res.append(model.id) - model_access_obj.create(cr, uid, { - 'name': 'Read Access of group %s on %s model'%(share_group_name, model.name), - 'model_id' : model.id, - 'group_id' : group_id, - 'perm_read' : True - }) - # - # And on OBJ0, OBJ1, OBJ2, OBJ3: add all rules from groups of the user - # that is sharing in the many2many of the rules on the new group - # (rule must be copied instead of adding it if it contains a reference to uid - # or user.xxx so it can be replaced correctly) + 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) + + # Create required rights if allowed by current user rights and not + # already granted + for dummy, model in fields_relations: + values = { + 'name': _('Copied access for sharing'), + 'group_id': group_id, + 'model_id': model.id, + } + current_user_access_line = current_user_access_map.get(model.model,set()) + existing_group_access_line = group_access_map.get(model.model,set()) + need_creation = False + for perm in perms_to_add: + if perm in current_user_access_line \ + and perm not in existing_group_access_line: + values.update({perm:True}) + 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) + + def _link_or_copy_current_user_rules(self, cr, uid, group_id, fields_relations, context=None): + user_obj = self.pool.get('res.users') + 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: - res = [] - for rel_field, model in obj0+obj1+obj2+obj3: - if model.id in res: + for dummy, model in fields_relations: + if model.id in completed_models: continue - res.append(model.id) + completed_models.add(model.id) for rule in group.rule_groups: if rule.model_id == model.id: - rule_obj.copy(cr, uid, rule.id, default={ - 'name': '%s-%s'%(share_group_name, model.model), - 'groups': [(6,0,[group_id])] - }, context=context) - - rule_obj.create(cr, uid, { - 'name': '%s-%s'%(share_group_name, active_model.model), - 'model_id': active_model.id, - 'domain_force': domain, - 'groups': [(6,0,[group_id])] - }) - for rel_field, model in obj1: - obj1_domain = [] - for opr1, opt, opr2 in domain: - new_opr1 = '%s.%s'%(rel_field, opr1) - obj1_domain.append((new_opr1, opt, opr2)) - - rule_obj.create(cr, uid, { - 'name': '%s-%s'%(share_group_name, model.model), - 'model_id': model.id, - 'domain_force': obj1_domain, - 'groups': [(6,0,[group_id])] - }) - context['share_model'] = active_model.model - context['share_rec_id'] = active_id - - - data_obj = self.pool.get('ir.model.data') - - form_view = data_obj._get_id(cr, uid, 'share', 'share_result_form') - form_view_id = False - if form_view: - form_view_id = data_obj.browse(cr, uid, form_view, context=context).res_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)')), + '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) + 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) + + 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) + rule_obj = self.pool.get('ir.rule') + try: + domain = safe_eval(wizard_data.domain) + if domain: + domain_expr = expression(domain) + for rel_field, model in fields_relations: + related_domain = [] + for element in domain: + if domain_expr._is_leaf(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)) + except Exception: + self.__logger.exception('Failed to create share access') + raise osv.except_osv(_('Sharing access could not be setup'), + _('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') + share_root_url = wizard_data.share_root_url + format_url = '%(login)' in share_root_url and '%(password)' 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} 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': email, + 'password': ''} if format_url else share_root_url + result_obj.create(cr, uid, { + 'share_wizard_id': wizard_data.id, + 'login': user.user_id.login, + 'password': existing_passwd_str, + 'share_url': share_url, + 'newly_created': False, + }, context=context) + + 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)) + + # 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.user_id.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) - value = { - 'name': _('Step:4 Share Users Detail'), + model_obj = self.pool.get('ir.model') + model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0] + model = model_obj.browse(cr, uid, model_id, context=context) + + # ACCESS RIGHTS + # We have several classes of objects that should receive different access rights: + # Let: + # - [obj0] be the target model itself + # - [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 + 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) + + # 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'), 'view_type': 'form', 'view_mode': 'form', - 'res_model': 'share.result', + 'res_model': 'share.wizard', 'view_id': False, - 'views': [(form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')], + 'res_id': ids[0], + 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')], 'type': 'ir.actions.act_window', - 'context': context, 'target': 'new' } - return value -share_create() + def send_emails(self, cr, uid, ids, context=None): + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + 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") + subject + "\n\n" + body += _("To access it, you can go to the following URL:\n %s") % wizard_data.share_root_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" + 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() -class share_result(osv.osv_memory): - _name = "share.result" +class share_user_ref(osv.osv_memory): + _name = 'share.wizard.user' + _rec_name = 'user_id' _columns = { - 'users': fields.text("Users", readonly=True), - } - - - - def do_send_email(self, cr, uid, ids, context=None): - user_obj = self.pool.get('res.users') - if not context: - context={} - existing_user_ids = context.get('existing_user_ids', []) - new_user_ids = context.get('new_user_ids', []) - share_url = tools.config.get('share_root_url', False) - user = user_obj.browse(cr, uid, uid, context=context) - for share_user in user_obj.browse(cr, uid, new_user_ids+existing_user_ids, context=context): - email_to = share_user.user_email - subject = '%s wants to share private data with you' %(user.name) - body = """ - Dear, - - %s wants to share private data from OpenERP with you! - """%(user.name) - if share_url: - body += """ - To view it, you can access the following URL: - %s - """%(user.name, share_url) - if share_user.id in new_user_ids: - body += """ - You may use the following login and password to get access to this - protected area: - login: %s - password: %s - """%(user.login, user.password) - elif share_user.id in existing_user_ids: - body += """ - You may use your existing login and password to get access to this - additional data. As a reminder, your login is %s. - """%(user.name) - - flag = tools.email_send( - user.user_email, - email_to, - subject, - body - ) - return flag - - - def default_get(self, cr, uid, fields, context=None): - """ - To get default values for the object. - """ + 'user_id': fields.many2one('res.users', 'Users', required=True, domain=[('share', '=', True)]), + 'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True), + } +share_user_ref() - res = super(share_result, self).default_get(cr, uid, fields, context=context) - user_obj = self.pool.get('res.users') - if not context: - context={} - existing_user_ids = context.get('existing_user_ids', []) - new_user_ids = context.get('new_user_ids', []) - share_url = tools.config.get('share_root_url', False) - if 'users' in fields: - users = [] - for user in user_obj.browse(cr, uid, new_user_ids): - txt = 'Login: %s Password: %s' %(user.login, user.password) - if share_url: - txt += ' Share URL: %s' %(share_url) - users.append(txt) - for user in user_obj.browse(cr, uid, existing_user_ids): - txt = 'Login: %s' %(user.login) - if share_url: - txt += ' Share URL: %s' %(share_url) - users.append(txt) - res['users'] = '\n'.join(users) - return res - -share_result() +class share_result_line(osv.osv_memory): + _name = 'share.wizard.result.line' + _rec_name = 'login' + _columns = { + 'login': fields.char('Username', 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), + 'newly_created': fields.boolean('Newly created', readonly=True), + } + _defaults = { + 'newly_created': True, + } +share_result_line() diff --git a/addons/share/wizard/share_wizard_view.xml b/addons/share/wizard/share_wizard_view.xml index 159583eecbb8..e6cade91c841 100644 --- a/addons/share/wizard/share_wizard_view.xml +++ b/addons/share/wizard/share_wizard_view.xml @@ -1,106 +1,125 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> - <data> + <data> - <record id="share_step0_form" model="ir.ui.view"> + <record id="share_step0_form" model="ir.ui.view"> <field name="name">share.step0.form</field> - <field name="model">share.create</field> + <field name="model">share.wizard</field> <field name="type">form</field> <field name="arch" type="xml"> - <form string="Share"> - <separator colspan="4" string="Step 1: Chose the action and the additional domain"/> - <field name="action_id"/> - <field name="domain"/> - <separator colspan="4"/> - <group col="2" colspan="4"> - <button special="cancel" string="Cancel" icon='gtk-cancel'/> - <button name="do_step_1" string="Next" colspan="1" type="object" icon="gtk-go-forward"/> - </group> - </form> + <form string="Share wizard: step 0"> + <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"/> + <separator colspan="4" + string="Optionally, you may specify an additional domain restriction that will be applied to the shared data."/> + <field name="domain"/> + <separator colspan="4"/> + <group colspan="4"> + <button special="cancel" string="Cancel" icon='gtk-cancel'/> + <button name="go_step_1" string="Next" colspan="1" type="object" icon="gtk-go-forward"/> + </group> + </form> </field> </record> <record id="share_step1_form" model="ir.ui.view"> <field name="name">share.step1.form</field> - <field name="model">share.create</field> + <field name="model">share.wizard</field> <field name="type">form</field> <field name="arch" type="xml"> - <form string="Share"> - <separator colspan="4" string="Step 2: Chose the User type"/> - <field name="user_type"/> - <newline/> - <group colspan="4" attrs="{'invisible':[('user_type','!=','existing')]}"> - <separator colspan="4" string="Exising Users"/> - <field colspan="4" nolabel="1" name="user_ids" domain="[('share_user','=',True)]"/> - </group> - <group colspan="4" attrs="{'invisible':[('user_type','!=','new')]}"> - <separator colspan="4" string="New Users"/> - <field colspan="4" nolabel="1" name="new_user"/> - </group> - <separator colspan="4"/> - <group col="3" colspan="4"> - <button special="cancel" string="Cancel" icon='gtk-cancel'/> - <button name="do_step_2" string="Next" colspan="1" type="object" icon="gtk-go-forward"/> - </group> - </form> + <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"> + <tree editable="bottom" string=""> + <field name="user_id"/> + </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> + <separator colspan="4" string="Select the desired shared access mode:"/> + <group colspan="4"> + <field name="access_mode"/> + </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"/> + </group> + </form> </field> </record> <record id="share_step2_form" model="ir.ui.view"> <field name="name">share.step2.form</field> - <field name="model">share.create</field> + <field name="model">share.wizard</field> <field name="type">form</field> <field name="arch" type="xml"> - <form string="Share"> - <separator colspan="4" string="Step 3: Chose Access Mode"/> - <group colspan="4"> - <field name="access_mode"/> - </group> - <separator colspan="4"/> - <group col="3" colspan="4"> - <button special="cancel" string="Cancel" icon='gtk-cancel'/> - <button name="do_step_3" string="Next" colspan="1" type="object" icon="gtk-go-forward"/> - </group> - </form> + <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:"/> + <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> </record> - - - <record id="action_share_wizard" model="ir.actions.act_window"> - <field name="name">Share Access Rules</field> + <!-- action for manual launch from menuitem. context may contain: + - 'action_id' (id of action) + - 'domain' (string expression for full domain to apply as sent to server, + with dynamic data like 'uid' replaced by actual value (i.e. after eval)!) + - 'share_root_url' : URL for direct access to share page (may include %(login) and %(password) placeholders) + --> + <record id="action_share_wizard" model="ir.actions.act_window"> + <field name="name">Share Wizard</field> <field name="type">ir.actions.act_window</field> - <field name="res_model">share.create</field> + <field name="res_model">share.wizard</field> <field name="view_type">form</field> <field name="view_mode">form</field> - <field name="view_id" ref="share_step0_form"/> + <field name="view_id" ref="share_step0_form"/> <field name="target">new</field> - </record> - - <menuitem action="action_share_wizard" id="menu_action_share_wizard" parent="base.menu_security"/> - - - <record id="share_result_form" model="ir.ui.view"> - <field name="name">share.result.form</field> - <field name="model">share.result</field> - <field name="type">form</field> - <field name="arch" type="xml"> - <form string="Share"> - <separator colspan="4" string="Step 4: Share Users Detail"/> - <field name="users" colspan="4" nolabel="1"/> - <separator colspan="4"/> - <group col="2" colspan="4"> - <button special="cancel" string="Cancel" icon='gtk-cancel'/> - <button name="do_send_email" string="Send Email" type="object" icon="gtk-apply"/> - </group> - </form> - </field> </record> - - + <!-- action for direct launch from client widget with context providing: + - 'action_id' (id of action) + - 'domain' (string expression for full domain to apply as sent to server, + with dynamic data like 'uid' replaced by actual value (i.e. after eval)!) + - 'share_root_url' : URL for direct access to share page (may include %(login) and %(password) placeholders) + --> + <record id="action_share_wizard_step1" model="ir.actions.act_window"> + <field name="name">Share Wizard</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">share.wizard</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="view_id" ref="share_step1_form"/> + <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> + </data> </openerp> -- GitLab