1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
25 from openerp import SUPERUSER_ID
29 from openerp import tools
30 from openerp.osv import fields, osv
31 from openerp.osv import expression
32 from openerp.tools.translate import _
33 from openerp.tools.safe_eval import safe_eval
35 _logger = logging.getLogger(__name__)
37 FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink')
38 READ_WRITE_ACCESS = ('perm_read', 'perm_write')
39 READ_ONLY_ACCESS = ('perm_read',)
42 # Pseudo-domain to represent an empty filter, constructed using
43 # osv.expression's DUMMY_LEAF
44 DOMAIN_ALL = [(1, '=', 1)]
46 # A good selection of easy to read password characters (e.g. no '0' vs 'O', etc.)
47 RANDOM_PASS_CHARACTERS = 'aaaabcdeeeefghjkmnpqrstuvwxyzAAAABCDEEEEFGHJKLMNPQRSTUVWXYZ23456789'
48 def generate_random_pass():
49 return ''.join(random.sample(RANDOM_PASS_CHARACTERS,10))
51 class share_wizard(osv.TransientModel):
52 _name = 'share.wizard'
53 _description = 'Share Wizard'
55 def _assert(self, condition, error_message, context=None):
56 """Raise a user error with the given message if condition is not met.
57 The error_message should have been translated with _().
60 raise osv.except_osv(_('Sharing access cannot be created.'), error_message)
62 def has_group(self, cr, uid, module, group_xml_id, context=None):
63 """Returns True if current user is a member of the group identified by the module, group_xml_id pair."""
64 # if the group was deleted or does not exist, we say NO (better safe than sorry)
66 model, group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module, group_xml_id)
69 return group_id in self.pool.get('res.users').read(cr, uid, uid, ['groups_id'], context=context)['groups_id']
71 def has_share(self, cr, uid, unused_param, context=None):
72 return self.has_group(cr, uid, module='share', group_xml_id='group_share_user', context=context)
74 def _user_type_selection(self, cr, uid, context=None):
75 """Selection values may be easily overridden/extended via inheritance"""
76 return [('embedded', _('Direct link or embed code')), ('emails',_('Emails')), ]
78 """Override of create() to auto-compute the action name"""
79 def create(self, cr, uid, values, context=None):
80 if 'action_id' in values and not 'name' in values:
81 action = self.pool.get('ir.actions.actions').browse(cr, uid, values['action_id'], context=context)
82 values['name'] = action.name
83 return super(share_wizard,self).create(cr, uid, values, context=context)
85 def share_url_template(self, cr, uid, _ids, context=None):
86 # NOTE: take _ids in parameter to allow usage through browse_record objects
87 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
89 base_url += '/login?db=%(dbname)s&login=%(login)s&key=%(password)s'
90 extra = context and context.get('share_url_template_extra_arguments')
92 base_url += '&' + '&'.join('%s=%%(%s)s' % (x,x) for x in extra)
93 hash_ = context and context.get('share_url_template_hash_arguments')
95 base_url += '#' + '&'.join('%s=%%(%s)s' % (x,x) for x in hash_)
98 def _share_root_url(self, cr, uid, ids, _fieldname, _args, context=None):
99 result = dict.fromkeys(ids, '')
100 data = dict(dbname=cr.dbname, login='', password='')
101 for this in self.browse(cr, uid, ids, context=context):
102 result[this.id] = this.share_url_template() % data
105 def _generate_embedded_code(self, wizard, options=None):
108 context = wizard._context
113 title = options['title'] if 'title' in options else wizard.embed_option_title
114 search = (options['search'] if 'search' in options else wizard.embed_option_search) if wizard.access_mode != 'readonly' else False
117 js_options['display_title'] = False
119 js_options['search_view'] = True
121 js_options_str = (', ' + simplejson.dumps(js_options)) if js_options else ''
123 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default=None, context=context)
124 user = wizard.result_line_ids[0]
127 <script type="text/javascript" src="%(base_url)s/web/webclient/js"></script>
128 <script type="text/javascript">
129 new openerp.init(%(init)s).web.embed(%(server)s, %(dbname)s, %(login)s, %(password)s,%(action)d%(options)s);
131 'init': simplejson.dumps(openerp.conf.server_wide_modules),
132 'base_url': base_url or '',
133 'server': simplejson.dumps(base_url),
134 'dbname': simplejson.dumps(cr.dbname),
135 'login': simplejson.dumps(user.login),
136 'password': simplejson.dumps(user.password),
137 'action': user.user_id.action_id.id,
138 'options': js_options_str,
141 def _embed_code(self, cr, uid, ids, _fn, _args, context=None):
142 result = dict.fromkeys(ids, '')
143 for this in self.browse(cr, uid, ids, context=context):
144 result[this.id] = self._generate_embedded_code(this)
147 def _embed_url(self, cr, uid, ids, _fn, _args, context=None):
150 result = dict.fromkeys(ids, '')
151 for this in self.browse(cr, uid, ids, context=context):
152 if this.result_line_ids:
153 ctx = dict(context, share_url_template_hash_arguments=['action'])
154 user = this.result_line_ids[0]
155 data = dict(dbname=cr.dbname, login=user.login, password=user.password, action=this.action_id.id)
156 result[this.id] = this.share_url_template(context=ctx) % data
161 'action_id': fields.many2one('ir.actions.act_window', 'Action to share', required=True,
162 help="The action that opens the screen containing the data you wish to share."),
163 'view_type': fields.char('Current View Type', size=32, required=True),
164 'domain': fields.char('Domain', size=256, help="Optional domain for further data filtering"),
165 'user_type': fields.selection(lambda s, *a, **k: s._user_type_selection(*a, **k),'Sharing method', required=True,
166 help="Select the type of user(s) you would like to share data with."),
167 'new_users': fields.text("Emails"),
168 'email_1': fields.char('New user email', size=64),
169 'email_2': fields.char('New user email', size=64),
170 'email_3': fields.char('New user email', size=64),
171 'invite': fields.boolean('Invite users to OpenSocial record'),
172 'access_mode': fields.selection([('readonly','Can view'),('readwrite','Can edit')],'Access Mode', required=True,
173 help="Access rights to be granted on the shared documents."),
174 'result_line_ids': fields.one2many('share.wizard.result.line', 'share_wizard_id', 'Summary', readonly=True),
175 'share_root_url': fields.function(_share_root_url, string='Share Access URL', type='char', size=512, readonly=True,
176 help='Main access page for users that are granted shared access'),
177 'name': fields.char('Share Title', size=64, required=True, help="Title for the share (displayed to users as menu and shortcut name)"),
178 'record_name': fields.char('Record name', size=128, help="Name of the shared record, if sharing a precise record"),
179 'message': fields.text("Personal Message", help="An optional personal message, to be included in the email notification."),
180 'embed_code': fields.function(_embed_code, type='text', string='Code',
181 help="Embed this code in your documents to provide a link to the "\
183 'embed_option_title': fields.boolean('Display title'),
184 'embed_option_search': fields.boolean('Display search view'),
185 'embed_url': fields.function(_embed_url, string='Share URL', type='char', size=512, readonly=True),
189 'user_type' : 'embedded',
191 'domain': lambda self, cr, uid, context, *a: context.get('domain', '[]'),
192 'action_id': lambda self, cr, uid, context, *a: context.get('action_id'),
193 'access_mode': 'readwrite',
194 'embed_option_title': True,
195 'embed_option_search': True,
198 def has_email(self, cr, uid, context=None):
199 return bool(self.pool.get('res.users').browse(cr, uid, uid, context=context).email)
201 def go_step_1(self, cr, uid, ids, context=None):
202 wizard_data = self.browse(cr,uid,ids,context)[0]
203 if wizard_data.user_type == 'emails' and not self.has_email(cr, uid, context=context):
204 raise osv.except_osv(_('No email address configured'),
205 _('You must configure your email address in the user preferences before using the Share button.'))
206 model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'action_share_wizard_step1')
207 action = self.pool[model].read(cr, uid, res_id, context=context)
208 action['res_id'] = ids[0]
209 action.pop('context', '')
212 def _create_share_group(self, cr, uid, wizard_data, context=None):
213 group_obj = self.pool.get('res.groups')
214 share_group_name = '%s: %s (%d-%s)' %('Shared', wizard_data.name, uid, time.time())
215 # create share group without putting admin in it
216 return group_obj.create(cr, UID_ROOT, {'name': share_group_name, 'share': True}, {'noadmin': True})
218 def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None):
219 """Create one new res.users record for each email address provided in
220 wizard_data.new_users, ignoring already existing users.
221 Populates wizard_data.result_line_ids with one new line for
222 each user (existing or not). New users will also have a value
223 for the password field, so they can receive it by email.
224 Returns the ids of the created users, and the ids of the
225 ignored, existing ones."""
228 user_obj = self.pool.get('res.users')
229 current_user = user_obj.browse(cr, UID_ROOT, uid, context=context)
230 # modify context to disable shortcuts when creating share users
231 context['noshortcut'] = True
232 context['no_reset_password'] = True
235 if wizard_data.user_type == 'emails':
236 # get new user list from email data
237 new_users = (wizard_data.new_users or '').split('\n')
238 new_users += [wizard_data.email_1 or '', wizard_data.email_2 or '', wizard_data.email_3 or '']
239 for new_user in new_users:
241 new_user = new_user.strip()
242 if not new_user: continue
243 # Ignore the user if it already exists.
244 if not wizard_data.invite:
245 existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
247 existing = user_obj.search(cr, UID_ROOT, [('email', '=', new_user)])
248 existing_ids.extend(existing)
250 new_line = { 'user_id': existing[0],
251 'newly_created': False}
252 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
254 new_pass = generate_random_pass()
255 user_id = user_obj.create(cr, UID_ROOT, {
257 'password': new_pass,
260 'groups_id': [(6,0,[group_id])],
261 'company_id': current_user.company_id.id,
262 'company_ids': [(6, 0, [current_user.company_id.id])],
264 new_line = { 'user_id': user_id,
265 'password': new_pass,
266 'newly_created': True}
267 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
268 created_ids.append(user_id)
270 elif wizard_data.user_type == 'embedded':
271 new_login = 'embedded-%s' % (uuid.uuid4().hex,)
272 new_pass = generate_random_pass()
273 user_id = user_obj.create(cr, UID_ROOT, {
275 'password': new_pass,
277 'groups_id': [(6,0,[group_id])],
278 'company_id': current_user.company_id.id,
279 'company_ids': [(6, 0, [current_user.company_id.id])],
281 new_line = { 'user_id': user_id,
282 'password': new_pass,
283 'newly_created': True}
284 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
285 created_ids.append(user_id)
287 return created_ids, existing_ids
289 def _create_action(self, cr, uid, values, context=None):
292 new_context = context.copy()
294 if key.startswith('default_'):
296 action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, values, new_context)
299 def _cleanup_action_context(self, context_str, user_id):
300 """Returns a dict representing the context_str evaluated (safe_eval) as
301 a dict where items that are not useful for shared actions
302 have been removed. If the evaluation of context_str as a
303 dict fails, context_str is returned unaltered.
305 :param user_id: the integer uid to be passed as 'uid' in the
311 context = safe_eval(context_str, tools.UnquoteEvalContext(), nocopy=True)
312 result = dict(context)
314 # Remove all context keys that seem to toggle default
315 # filters based on the current user, as it makes no sense
316 # for shared users, who would not see any data by default.
317 if key and key.startswith('search_default_') and 'user_id' in key:
320 # Note: must catch all exceptions, as UnquoteEvalContext may cause many
321 # different exceptions, as it shadows builtins.
322 _logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
326 def _shared_action_def(self, cr, uid, wizard_data, context=None):
327 copied_action = wizard_data.action_id
329 if wizard_data.access_mode == 'readonly':
330 view_mode = wizard_data.view_type
331 view_id = copied_action.view_id.id if copied_action.view_id.type == wizard_data.view_type else False
333 view_mode = copied_action.view_mode
334 view_id = copied_action.view_id.id
338 'name': wizard_data.name,
339 'domain': copied_action.domain,
340 'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
341 'res_model': copied_action.res_model,
342 'view_mode': view_mode,
343 'view_type': copied_action.view_type,
344 'search_view_id': copied_action.search_view_id.id if wizard_data.access_mode != 'readonly' else False,
348 if copied_action.view_ids:
349 action_def['view_ids'] = [(0,0,{'sequence': x.sequence,
350 'view_mode': x.view_mode,
351 'view_id': x.view_id.id })
352 for x in copied_action.view_ids
353 if (wizard_data.access_mode != 'readonly' or x.view_mode == wizard_data.view_type)
357 def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, make_home, context=None):
358 """Create a shortcut to reach the shared data, as well as the corresponding action, for
359 each user in ``user_ids``, and assign it as their home action if ``make_home`` is True.
360 Meant to be overridden for special cases.
362 values = self._shared_action_def(cr, uid, wizard_data, context=None)
363 user_obj = self.pool.get('res.users')
364 for user_id in user_ids:
365 action_id = self._create_action(cr, user_id, values)
367 # We do this only for new share users, as existing ones already have their initial home
368 # action. Resetting to the default menu does not work well as the menu is rather empty
369 # and does not contain the shortcuts in most cases.
370 user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
372 def _get_recursive_relations(self, cr, uid, model, ttypes, relation_fields=None, suffix=None, context=None):
373 """Returns list of tuples representing recursive relationships of type ``ttypes`` starting from
374 model with ID ``model_id``.
376 :param model: browsable model to start loading relationships from
377 :param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
378 :param relation_fields: list of previously followed relationship tuples - to avoid duplicates
380 :param suffix: optional suffix to append to the field path to reach the main object
382 if relation_fields is None:
384 local_rel_fields = []
385 models = [x[1].model for x in relation_fields]
386 model_obj = self.pool.get('ir.model')
387 model_osv = self.pool[model.model]
388 for colinfo in model_osv._all_columns.itervalues():
389 coldef = colinfo.column
390 coltype = coldef._type
391 relation_field = None
392 if coltype in ttypes and colinfo.column._obj not in models:
393 relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',coldef._obj)])[0]
394 relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
395 relation_osv = self.pool[coldef._obj]
396 #skip virtual one2many fields (related, ...) as there is no reverse relationship
397 if coltype == 'one2many' and hasattr(coldef, '_fields_id'):
398 # don't record reverse path if it's not a real m2o (that happens, but rarely)
399 dest_model_ci = relation_osv._all_columns
400 reverse_rel = coldef._fields_id
401 if reverse_rel in dest_model_ci and dest_model_ci[reverse_rel].column._type == 'many2one':
402 relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
403 local_rel_fields.append((relation_field, relation_model_browse))
404 for parent in relation_osv._inherits:
405 if parent not in models:
406 parent_model = self.pool[parent]
407 parent_colinfos = parent_model._all_columns
408 parent_model_browse = model_obj.browse(cr, UID_ROOT,
409 model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
410 if relation_field and coldef._fields_id in parent_colinfos:
411 # inverse relationship is available in the parent
412 local_rel_fields.append((relation_field, parent_model_browse))
414 # TODO: can we setup a proper rule to restrict inherited models
415 # in case the parent does not contain the reverse m2o?
416 local_rel_fields.append((None, parent_model_browse))
417 if relation_model_id != model.id and coltype in ['one2many', 'many2many']:
418 local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
419 [coltype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
420 return local_rel_fields
422 def _get_relationship_classes(self, cr, uid, model, context=None):
423 """Computes the *relationship classes* reachable from the given
424 model. The 4 relationship classes are:
425 - [obj0]: the given model itself (and its parents via _inherits, if any)
426 - [obj1]: obj0 and all other models recursively accessible from
427 obj0 via one2many relationships
428 - [obj2]: obj0 and all other models recursively accessible from
429 obj0 via one2many and many2many relationships
430 - [obj3]: all models recursively accessible from obj1 via many2one
433 Each class is returned as a list of pairs [(field,model_browse)], where
434 ``model`` is the browse_record of a reachable ir.model, and ``field`` is
435 the dot-notation reverse relationship path coming from that model to obj0,
436 or None if there is no reverse path.
438 :return: ([obj0], [obj1], [obj2], [obj3])
440 # obj0 class and its parents
441 obj0 = [(None, model)]
442 model_obj = self.pool[model.model]
443 ir_model_obj = self.pool.get('ir.model')
444 for parent in model_obj._inherits:
445 parent_model_browse = ir_model_obj.browse(cr, UID_ROOT,
446 ir_model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
447 obj0 += [(None, parent_model_browse)]
449 obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], relation_fields=obj0, context=context)
450 obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], relation_fields=obj0, context=context)
451 obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
452 for dummy, model in obj1:
453 obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
454 return obj0, obj1, obj2, obj3
456 def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None):
457 model_access_obj = self.pool.get('ir.model.access')
458 user_right_ids = model_access_obj.search(cr, uid,
459 [('group_id', 'in', group_ids), ('model_id', 'in', model_ids)],
461 user_access_matrix = {}
463 for access_right in model_access_obj.browse(cr, uid, user_right_ids, context=context):
464 access_line = user_access_matrix.setdefault(access_right.model_id.model, set())
465 for perm in FULL_ACCESS:
466 if getattr(access_right, perm, 0):
467 access_line.add(perm)
468 return user_access_matrix
470 def _add_access_rights_for_share_group(self, cr, uid, group_id, mode, fields_relations, context=None):
471 """Adds access rights to group_id on object models referenced in ``fields_relations``,
472 intersecting with access rights of current user to avoid granting too much rights
474 model_access_obj = self.pool.get('ir.model.access')
475 user_obj = self.pool.get('res.users')
476 target_model_ids = [x[1].id for x in fields_relations]
477 perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or READ_WRITE_ACCESS
478 current_user = user_obj.browse(cr, uid, uid, context=context)
480 current_user_access_map = self._get_access_map_for_groups_and_models(cr, uid,
481 [x.id for x in current_user.groups_id], target_model_ids, context=context)
482 group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
483 [group_id], target_model_ids, context=context)
484 _logger.debug("Current user access matrix: %r", current_user_access_map)
485 _logger.debug("New group current access matrix: %r", group_access_map)
487 # Create required rights if allowed by current user rights and not
489 for dummy, model in fields_relations:
490 # mail.message is transversal: it should not received directly the access rights
491 if model.model in ['mail.message']: continue
493 'name': _('Copied access for sharing'),
494 'group_id': group_id,
495 'model_id': model.id,
497 current_user_access_line = current_user_access_map.get(model.model,set())
498 existing_group_access_line = group_access_map.get(model.model,set())
499 need_creation = False
500 for perm in perms_to_add:
501 if perm in current_user_access_line \
502 and perm not in existing_group_access_line:
503 values.update({perm:True})
504 group_access_map.setdefault(model.model, set()).add(perm)
507 model_access_obj.create(cr, UID_ROOT, values)
508 _logger.debug("Creating access right for model %s with values: %r", model.model, values)
510 def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
511 rule_obj = self.pool.get('ir.rule')
513 for group in current_user.groups_id:
514 for dummy, model in fields_relations:
515 for rule in group.rule_groups:
516 if rule.id in rules_done:
518 rules_done.add(rule.id)
519 if rule.model_id.id == model.id:
520 if 'user.' in rule.domain_force:
521 # Above pattern means there is likely a condition
522 # specific to current user, so we must copy the rule using
523 # the evaluated version of the domain.
524 # And it's better to copy one time too much than too few
525 rule_obj.copy(cr, UID_ROOT, rule.id, default={
526 'name': '%s %s' %(rule.name, _('(Copy for sharing)')),
527 'groups': [(6,0,[group_id])],
528 'domain_force': rule.domain, # evaluated version!
530 _logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
532 # otherwise we can simply link the rule to keep it dynamic
533 rule_obj.write(cr, SUPERUSER_ID, [rule.id], {
534 'groups': [(4,group_id)]
536 _logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
538 def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
539 """Verifies that the given rule only belongs to the given group_id, otherwise
540 duplicate it for the current group, and unlink the previous one.
541 The duplicated rule has the original domain copied verbatim, without
543 Returns the final rule to use (browse_record), either the original one if it
544 only belongs to this group, or the copy."""
545 if len(rule.groups) == 1:
547 # duplicate it first:
548 rule_obj = self.pool.get('ir.rule')
549 new_id = rule_obj.copy(cr, UID_ROOT, rule.id,
551 'name': '%s %s' %(rule.name, _('(Duplicated for modified sharing permissions)')),
552 'groups': [(6,0,[group_id])],
553 'domain_force': rule.domain_force, # non evaluated!
555 _logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
556 # then disconnect from group_id:
557 rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
558 return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
560 def _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, restrict=False, rule_name=None, context=None):
561 """Add a new ir.rule entry for model_id and domain on the target group_id.
562 If ``restrict`` is True, instead of adding a rule, the domain is
563 combined with AND operator with all existing rules in the group, to implement
564 an additional restriction (as of 6.1, multiple rules in the same group are
565 OR'ed by default, so a restriction must alter all existing rules)
567 This is necessary because the personal rules of the user that is sharing
568 are first copied to the new share group. Afterwards the filters used for
569 sharing are applied as an additional layer of rules, which are likely to
570 apply to the same model. The default rule algorithm would OR them (as of 6.1),
571 which would result in a combined set of permission that could be larger
572 than those of the user that is sharing! Hence we must forcefully AND the
574 One possibly undesirable effect can appear when sharing with a
575 pre-existing group, in which case altering pre-existing rules would not
576 be desired. This is addressed in the portal module.
578 if rule_name is None:
579 rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
580 (current_user.name, current_user.login, group_id)
581 rule_obj = self.pool.get('ir.rule')
582 rule_ids = rule_obj.search(cr, UID_ROOT, [('groups', 'in', group_id), ('model_id', '=', model_id)])
584 for rule in rule_obj.browse(cr, UID_ROOT, rule_ids, context=context):
585 if rule.domain_force == domain:
586 # don't create it twice!
590 _logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
593 # restricting existing rules is done by adding the clause
594 # with an AND, but we can't alter the rule if it belongs to
595 # other groups, so we duplicate if needed
596 rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context)
597 eval_ctx = rule_obj._eval_context_for_combinations()
598 org_domain = expression.normalize_domain(eval(rule.domain_force, eval_ctx))
599 new_clause = expression.normalize_domain(eval(domain, eval_ctx))
600 combined_domain = expression.AND([new_clause, org_domain])
601 rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
602 _logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
603 if not rule_ids or not restrict:
604 # Adding the new rule in the group is ok for normal cases, because rules
605 # in the same group and for the same model will be combined with OR
606 # (as of v6.1), so the desired effect is achieved.
607 rule_obj.create(cr, UID_ROOT, {
609 'model_id': model_id,
610 'domain_force': domain,
611 'groups': [(4,group_id)]
613 _logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
615 def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
616 rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
617 (current_user.name, current_user.login, group_id)
619 domain = safe_eval(wizard_data.domain)
621 for rel_field, model in fields_relations:
622 # mail.message is transversal: it should not received directly the access rights
623 if model.model in ['mail.message']: continue
625 if not rel_field: continue
626 for element in domain:
627 if expression.is_leaf(element):
628 left, operator, right = element
629 left = '%s.%s'%(rel_field, left)
630 element = left, operator, right
631 related_domain.append(element)
632 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
633 group_id, model_id=model.id, domain=str(related_domain),
634 rule_name=rule_name, restrict=True, context=context)
636 _logger.exception('Failed to create share access')
637 raise osv.except_osv(_('Sharing access cannot be created.'),
638 _('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.'))
640 def _check_preconditions(self, cr, uid, wizard_data, context=None):
641 self._assert(wizard_data.action_id and wizard_data.access_mode,
642 _('Action and Access Mode are required to create a shared access.'),
644 self._assert(self.has_share(cr, uid, wizard_data, context=context),
645 _('You must be a member of the Share/User group to use the share wizard.'),
647 if wizard_data.user_type == 'emails':
648 self._assert((wizard_data.new_users or wizard_data.email_1 or wizard_data.email_2 or wizard_data.email_3),
649 _('Please indicate the emails of the persons to share with, one per line.'),
652 def _create_share_users_group(self, cr, uid, wizard_data, context=None):
653 """Creates the appropriate share group and share users, and populates
654 result_line_ids of wizard_data with one line for each user.
656 :return: a tuple composed of the new group id (to which the shared access should be granted),
657 the ids of the new share users that have been created and the ids of the existing share users
659 group_id = self._create_share_group(cr, uid, wizard_data, context=context)
660 # First create any missing user, based on the email addresses provided
661 new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
662 # Finally, setup the new action and shortcut for the users.
664 # existing users still need to join the new group
665 self.pool.get('res.users').write(cr, UID_ROOT, existing_ids, {
666 'groups_id': [(4,group_id)],
668 # existing user don't need their home action replaced, only a new shortcut
669 self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
671 # new users need a new shortcut AND a home action
672 self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
673 return group_id, new_ids, existing_ids
675 def go_step_2(self, cr, uid, ids, context=None):
676 wizard_data = self.browse(cr, uid, ids[0], context=context)
677 self._check_preconditions(cr, uid, wizard_data, context=context)
679 # Create shared group and users
680 group_id, new_ids, existing_ids = self._create_share_users_group(cr, uid, wizard_data, context=context)
682 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
684 model_obj = self.pool.get('ir.model')
685 model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0]
686 model = model_obj.browse(cr, uid, model_id, context=context)
689 # We have several classes of objects that should receive different access rights:
691 # - [obj0] be the target model itself (and its parents via _inherits, if any)
692 # - [obj1] be the target model and all other models recursively accessible from
693 # obj0 via one2many relationships
694 # - [obj2] be the target model and all other models recursively accessible from
695 # obj0 via one2many and many2many relationships
696 # - [obj3] be all models recursively accessible from obj1 via many2one relationships
697 # (currently not used)
698 obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
699 mode = wizard_data.access_mode
701 # Add access to [obj0] and [obj1] according to chosen mode
702 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
703 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
705 # Add read-only access (always) to [obj2]
706 self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
709 # A. On [obj0], [obj1], [obj2]: add all rules from all groups of
710 # the user that is sharing
711 # Warning: rules must be copied instead of linked if they contain a reference
712 # to uid or if the rule is shared with other groups (and it must be replaced correctly)
713 # B. On [obj0]: 1 rule with domain of shared action
714 # C. For each model in [obj1]: 1 rule in the form:
715 # many2one_rel.domain_of_obj0
716 # where many2one_rel is the many2one used in the definition of the
717 # one2many, and domain_of_obj0 is the sharing domain
718 # For example if [obj0] is project.project with a domain of
719 # ['id', 'in', [1,2]]
720 # then we will have project.task in [obj1] and we need to create this
721 # ir.rule on project.task:
722 # ['project_id.id', 'in', [1,2]]
725 all_relations = obj0 + obj1 + obj2
726 self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
728 main_domain = wizard_data.domain if wizard_data.domain != '[]' else str(DOMAIN_ALL)
729 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
730 group_id, model_id=model.id, domain=main_domain,
731 restrict=True, context=context)
733 self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
735 # refresh wizard_data
736 wizard_data = self.browse(cr, uid, ids[0], context=context)
738 # EMAILS AND NOTIFICATIONS
739 # A. Not invite: as before
740 # -> send emails to destination users
741 # B. Invite (OpenSocial)
742 # -> subscribe all users (existing and new) to the record
743 # -> send a notification with a summary to the current record
744 # -> send a notification to all users; users allowing to receive
745 # emails in preferences will receive it
746 # new users by default receive all notifications by email
749 if not wizard_data.invite:
750 self.send_emails(cr, uid, wizard_data, context=context)
753 # Invite (OpenSocial): automatically subscribe users to the record
755 for cond in safe_eval(main_domain):
758 # Record id not found: issue
760 raise osv.except_osv(_('Record id not found'), _('The share engine has not been able to fetch a record_id for your invitation.'))
761 self.pool[model.model].message_subscribe(cr, uid, [res_id], new_ids + existing_ids, context=context)
762 # self.send_invite_email(cr, uid, wizard_data, context=context)
763 # self.send_invite_note(cr, uid, model.model, res_id, wizard_data, context=context)
766 # A. Not invite: as before
767 # B. Invite: skip summary screen, get back to the record
770 if not wizard_data.invite:
771 dummy, step2_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step2_form')
773 'name': _('Shared access created!'),
776 'res_model': 'share.wizard',
779 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
780 'type': 'ir.actions.act_window',
788 'res_model': model.model,
791 'views': [(False, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
792 'type': 'ir.actions.act_window',
796 def send_invite_note(self, cr, uid, model_name, res_id, wizard_data, context=None):
797 subject = _('Invitation')
798 body = 'has been <b>shared</b> with'
800 for result_line in wizard_data.result_line_ids:
801 body += ' @%s' % (result_line.user_id.login)
802 if tmp_idx < len(wizard_data.result_line_ids)-2:
804 elif tmp_idx == len(wizard_data.result_line_ids)-2:
807 return self.pool[model_name].message_post(cr, uid, [res_id], body=body, context=context)
809 def send_invite_email(self, cr, uid, wizard_data, context=None):
810 # TDE Note: not updated because will disappear
811 message_obj = self.pool.get('mail.message')
812 notification_obj = self.pool.get('mail.notification')
813 user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
815 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.'))
817 # TODO: also send an HTML version of this mail
818 for result_line in wizard_data.result_line_ids:
819 email_to = result_line.user_id.email
822 subject = _('Invitation to collaborate about %s') % (wizard_data.record_name)
823 body = _("Hello,\n\n")
824 body += _("I have shared %s (%s) with you!\n\n") % (wizard_data.record_name, wizard_data.name)
825 if wizard_data.message:
826 body += "%s\n\n" % (wizard_data.message)
827 if result_line.newly_created:
828 body += _("The documents are not attached, you can view them online directly on my OpenERP server at:\n %s\n\n") % (result_line.share_url)
829 body += _("These are your credentials to access this protected area:\n")
830 body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
831 body += "%s: %s" % (_("Password"), result_line.password) + "\n"
832 body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
833 body += _("The documents have been automatically added to your subscriptions.\n\n")
834 body += '%s\n\n' % ((user.signature or ''))
836 body += _("OpenERP is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
837 "It is open source and can be found on http://www.openerp.com.")
838 msg_id = message_obj.schedule_with_attach(cr, uid, user.email, [email_to], subject, body, model='', context=context)
839 notification_obj.create(cr, uid, {'user_id': result_line.user_id.id, 'message_id': msg_id}, context=context)
841 def send_emails(self, cr, uid, wizard_data, context=None):
842 _logger.info('Sending share notifications by email...')
843 mail_mail = self.pool.get('mail.mail')
844 user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
846 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.'))
848 # TODO: also send an HTML version of this mail
850 for result_line in wizard_data.result_line_ids:
851 email_to = result_line.user_id.email
854 subject = wizard_data.name
855 body = _("Hello,\n\n")
856 body += _("I've shared %s with you!\n\n") % wizard_data.name
857 body += _("The documents are not attached, you can view them online directly on my OpenERP server at:\n %s\n\n") % (result_line.share_url)
858 if wizard_data.message:
859 body += '%s\n\n' % (wizard_data.message)
860 if result_line.newly_created:
861 body += _("These are your credentials to access this protected area:\n")
862 body += "%s: %s\n" % (_("Username"), result_line.user_id.login)
863 body += "%s: %s\n" % (_("Password"), result_line.password)
864 body += "%s: %s\n" % (_("Database"), cr.dbname)
866 body += _("The documents have been automatically added to your current OpenERP documents.\n")
867 body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
868 body += "\n\n%s\n\n" % ( (user.signature or '') )
870 body += _("OpenERP is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
871 "It is open source and can be found on http://www.openerp.com.")
872 mail_ids.append(mail_mail.create(cr, uid, {
873 'email_from': user.email,
874 'email_to': email_to,
876 'body_html': '<pre>%s</pre>' % body}, context=context))
877 # force direct delivery, as users expect instant notification
878 mail_mail.send(cr, uid, mail_ids, context=context)
879 _logger.info('%d share notification(s) sent.', len(mail_ids))
881 def onchange_embed_options(self, cr, uid, ids, opt_title, opt_search, context=None):
882 wizard = self.browse(cr, uid, ids[0], context)
883 options = dict(title=opt_title, search=opt_search)
884 return {'value': {'embed_code': self._generate_embedded_code(wizard, options)}}
887 class share_result_line(osv.osv_memory):
888 _name = 'share.wizard.result.line'
889 _rec_name = 'user_id'
892 def _share_url(self, cr, uid, ids, _fieldname, _args, context=None):
893 result = dict.fromkeys(ids, '')
894 for this in self.browse(cr, uid, ids, context=context):
895 data = dict(dbname=cr.dbname, login=this.login, password=this.password)
896 if this.share_wizard_id and this.share_wizard_id.action_id:
897 data['action_id'] = this.share_wizard_id.action_id.id
898 ctx = dict(context, share_url_template_hash_arguments=['action_id'])
899 result[this.id] = this.share_wizard_id.share_url_template(context=ctx) % data
903 'user_id': fields.many2one('res.users', required=True, readonly=True),
904 'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
905 'password': fields.char('Password', size=64, readonly=True),
906 'share_url': fields.function(_share_url, string='Share URL', type='char', size=512),
907 'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True, ondelete='cascade'),
908 'newly_created': fields.boolean('Newly created', readonly=True),
911 'newly_created': True,
914 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: