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 ##############################################################################
24 from urllib import quote_plus
30 from osv import osv, fields
31 from osv import expression
32 from tools.translate import _
33 from tools.safe_eval import safe_eval
36 FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink')
37 READ_WRITE_ACCESS = ('perm_read', 'perm_write')
38 READ_ONLY_ACCESS = ('perm_read',)
41 # Pseudo-domain to represent an empty filter, constructed using
42 # osv.expression's DUMMY_LEAF
43 DOMAIN_ALL = [(1, '=', 1)]
45 # A good selection of easy to read password characters (e.g. no '0' vs 'O', etc.)
46 RANDOM_PASS_CHARACTERS = 'aaaabcdeeeefghjkmnpqrstuvwxyzAAAABCDEEEEFGHJKLMNPQRSTUVWXYZ23456789'
47 def generate_random_pass():
48 return ''.join(random.sample(RANDOM_PASS_CHARACTERS,10))
50 class share_wizard(osv.osv_memory):
51 _logger = logging.getLogger('share.wizard')
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 could not 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, 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 += '/web/webclient/login?db=%(dbname)s&login=%(login)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='')
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_extra_arguments=['key'],
154 share_url_template_hash_arguments=['action_id'])
155 user = this.result_line_ids[0]
156 data = dict(dbname=cr.dbname, login=user.login, key=user.password, action_id=this.action_id.id)
157 result[this.id] = this.share_url_template(context=ctx) % data
162 'action_id': fields.many2one('ir.actions.act_window', 'Action to share', required=True,
163 help="The action that opens the screen containing the data you wish to share."),
164 'view_type': fields.char('Current View Type', size=32, required=True),
165 'domain': fields.char('Domain', size=256, help="Optional domain for further data filtering"),
166 'user_type': fields.selection(lambda s, *a, **k: s._user_type_selection(*a, **k),'Sharing method', required=True,
167 help="Select the type of user(s) you would like to share data with."),
168 'new_users': fields.text("Emails"),
169 'access_mode': fields.selection([('readonly','Can view'),('readwrite','Can edit')],'Access Mode', required=True,
170 help="Access rights to be granted on the shared documents."),
171 'result_line_ids': fields.one2many('share.wizard.result.line', 'share_wizard_id', 'Summary', readonly=True),
172 'share_root_url': fields.function(_share_root_url, string='Share Access URL', type='char', size=512, readonly=True,
173 help='Main access page for users that are granted shared access'),
174 'name': fields.char('Share Title', size=64, required=True, help="Title for the share (displayed to users as menu and shortcut name)"),
175 'message': fields.text("Personal Message", help="An optional personal message, to be included in the e-mail notification."),
177 'embed_code': fields.function(_embed_code, type='text'),
178 'embed_option_title': fields.boolean("Display title"),
179 'embed_option_search': fields.boolean('Display search view'),
180 'embed_url': fields.function(_embed_url, string='Share URL', type='char', size=512, readonly=True),
184 'user_type' : 'embedded',
185 'domain': lambda self, cr, uid, context, *a: context.get('domain', '[]'),
186 'action_id': lambda self, cr, uid, context, *a: context.get('action_id'),
187 'access_mode': 'readonly',
188 'embed_option_title': True,
189 'embed_option_search': True,
192 def has_email(self, cr, uid, context=None):
193 return bool(self.pool.get('res.users').browse(cr, uid, uid, context=context).user_email)
195 def go_step_1(self, cr, uid, ids, context=None):
196 user_type = self.browse(cr,uid,ids,context)[0].user_type
197 if user_type == 'emails' and not self.has_email(cr, uid, context=context):
198 raise osv.except_osv(_('No e-mail address configured'),
199 _('You must configure your e-mail address in the user preferences before using the Share button.'))
200 model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'action_share_wizard_step1')
201 action = self.pool.get(model).read(cr, uid, res_id, context=context)
202 action['res_id'] = ids[0]
203 action.pop('context', '')
206 def _create_share_group(self, cr, uid, wizard_data, context=None):
207 group_obj = self.pool.get('res.groups')
208 share_group_name = '%s: %s (%d-%s)' %('Shared', wizard_data.name, uid, time.time())
209 # create share group without putting admin in it
210 return group_obj.create(cr, UID_ROOT, {'name': share_group_name, 'share': True}, {'noadmin': True})
212 def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None):
213 """Create one new res.users record for each email address provided in
214 wizard_data.new_users, ignoring already existing users.
215 Populates wizard_data.result_line_ids with one new line for
216 each user (existing or not). New users will also have a value
217 for the password field, so they can receive it by email.
218 Returns the ids of the created users, and the ids of the
219 ignored, existing ones."""
220 user_obj = self.pool.get('res.users')
221 current_user = user_obj.browse(cr, UID_ROOT, uid, context=context)
222 # modify context to disable shortcuts when creating share users
223 context['noshortcut'] = True
226 if wizard_data.user_type == 'emails':
227 for new_user in (wizard_data.new_users or '').split('\n'):
229 new_user = new_user.strip()
230 if not new_user: continue
231 # Ignore the user if it already exists.
232 existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
233 existing_ids.extend(existing)
235 new_line = { 'user_id': existing[0],
236 'newly_created': False}
237 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
239 new_pass = generate_random_pass()
240 user_id = user_obj.create(cr, UID_ROOT, {
242 'password': new_pass,
244 'user_email': new_user,
245 'groups_id': [(6,0,[group_id])],
247 'company_id': current_user.company_id.id
249 new_line = { 'user_id': user_id,
250 'password': new_pass,
251 'newly_created': True}
252 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
253 created_ids.append(user_id)
255 elif wizard_data.user_type == 'embedded':
256 new_login = 'embedded-%s' % (uuid.uuid4().hex,)
257 new_pass = generate_random_pass()
258 user_id = user_obj.create(cr, UID_ROOT, {
260 'password': new_pass,
262 'groups_id': [(6,0,[group_id])],
265 'company_id': current_user.company_id.id
267 new_line = { 'user_id': user_id,
268 'password': new_pass,
269 'newly_created': True}
270 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
271 created_ids.append(user_id)
273 return created_ids, existing_ids
275 def _create_shortcut(self, cr, uid, values, context=None):
278 new_context = context.copy()
280 if key.startswith('default_'):
283 dataobj = self.pool.get('ir.model.data')
284 menu_id = dataobj._get_id(cr, uid, 'base', 'menu_administration_shortcut')
285 shortcut_menu_id = int(dataobj.read(cr, uid, menu_id, ['res_id'], new_context)['res_id'])
286 action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, values, new_context)
287 menu_data = {'name': values['name'],
289 'action': 'ir.actions.act_window,'+str(action_id),
290 'parent_id': shortcut_menu_id,
291 'icon': 'STOCK_JUSTIFY_FILL'}
292 menu_obj = self.pool.get('ir.ui.menu')
293 menu_id = menu_obj.create(cr, UID_ROOT, menu_data)
294 sc_data = {'name': values['name'], 'sequence': UID_ROOT,'res_id': menu_id }
295 self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, new_context)
298 user_groups = set(self.pool.get('res.users').read(cr, UID_ROOT, uid, ['groups_id'])['groups_id'])
299 key = (cr.dbname, shortcut_menu_id, tuple(user_groups))
300 menu_obj._cache[key] = True
303 def _cleanup_action_context(self, context_str, user_id):
304 """Returns a dict representing the context_str evaluated (safe_eval) as
305 a dict where items that are not useful for shared actions
306 have been removed. If the evaluation of context_str as a
307 dict fails, context_str is returned unaltered.
309 :param user_id: the integer uid to be passed as 'uid' in the
315 context = safe_eval(context_str, tools.UnquoteEvalContext(), nocopy=True)
316 result = dict(context)
318 # Remove all context keys that seem to toggle default
319 # filters based on the current user, as it makes no sense
320 # for shared users, who would not see any data by default.
321 if key and key.startswith('search_default_') and 'user_id' in key:
324 # Note: must catch all exceptions, as UnquoteEvalContext may cause many
325 # different exceptions, as it shadows builtins.
326 self._logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
330 def _shared_action_def(self, cr, uid, wizard_data, context=None):
331 copied_action = wizard_data.action_id
333 if wizard_data.access_mode == 'readonly':
334 view_mode = wizard_data.view_type
335 view_id = copied_action.view_id.id if copied_action.view_id.type == wizard_data.view_type else False
337 view_mode = copied_action.view_mode
338 view_id = copied_action.view_id.id
342 'name': wizard_data.name,
343 'domain': copied_action.domain,
344 'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
345 'res_model': copied_action.res_model,
346 'view_mode': view_mode,
347 'view_type': copied_action.view_type,
348 'search_view_id': copied_action.search_view_id.id if wizard_data.access_mode != 'readonly' else False,
352 if copied_action.view_ids:
353 action_def['view_ids'] = [(0,0,{'sequence': x.sequence,
354 'view_mode': x.view_mode,
355 'view_id': x.view_id.id })
356 for x in copied_action.view_ids
357 if (wizard_data.access_mode != 'readonly' or x.view_mode == wizard_data.view_type)
361 def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, make_home, context=None):
362 """Create a shortcut to reach the shared data, as well as the corresponding action, for
363 each user in ``user_ids``, and assign it as their home action if ``make_home`` is True.
364 Meant to be overridden for special cases.
366 values = self._shared_action_def(cr, uid, wizard_data, context=None)
367 user_obj = self.pool.get('res.users')
368 for user_id in user_ids:
369 action_id = self._create_shortcut(cr, user_id, values)
371 # We do this only for new share users, as existing ones already have their initial home
372 # action. Resetting to the default menu does not work well as the menu is rather empty
373 # and does not contain the shortcuts in most cases.
374 user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
376 def _get_recursive_relations(self, cr, uid, model, ttypes, relation_fields=None, suffix=None, context=None):
377 """Returns list of tuples representing recursive relationships of type ``ttypes`` starting from
378 model with ID ``model_id``.
380 :param model: browsable model to start loading relationships from
381 :param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
382 :param relation_fields: list of previously followed relationship tuples - to avoid duplicates
384 :param suffix: optional suffix to append to the field path to reach the main object
386 if relation_fields is None:
388 local_rel_fields = []
389 models = [x[1].model for x in relation_fields]
390 model_obj = self.pool.get('ir.model')
391 model_osv = self.pool.get(model.model)
392 for colinfo in model_osv._all_columns.itervalues():
393 coldef = colinfo.column
394 coltype = coldef._type
395 relation_field = None
396 if coltype in ttypes and colinfo.column._obj not in models:
397 relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',coldef._obj)])[0]
398 relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
399 relation_osv = self.pool.get(coldef._obj)
400 if coltype == 'one2many':
401 # don't record reverse path if it's not a real m2o (that happens, but rarely)
402 dest_model_ci = relation_osv._all_columns
403 reverse_rel = coldef._fields_id
404 if reverse_rel in dest_model_ci and dest_model_ci[reverse_rel].column._type == 'many2one':
405 relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
406 local_rel_fields.append((relation_field, relation_model_browse))
407 for parent in relation_osv._inherits:
408 if parent not in models:
409 parent_model = self.pool.get(parent)
410 parent_colinfos = parent_model._all_columns
411 parent_model_browse = model_obj.browse(cr, UID_ROOT,
412 model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
413 if relation_field and coldef._fields_id in parent_colinfos:
414 # inverse relationship is available in the parent
415 local_rel_fields.append((relation_field, parent_model_browse))
417 # TODO: can we setup a proper rule to restrict inherited models
418 # in case the parent does not contain the reverse m2o?
419 local_rel_fields.append((None, parent_model_browse))
420 if relation_model_id != model.id and coltype in ['one2many', 'many2many']:
421 local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
422 [coltype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
423 return local_rel_fields
425 def _get_relationship_classes(self, cr, uid, model, context=None):
426 """Computes the *relationship classes* reachable from the given
427 model. The 4 relationship classes are:
428 - [obj0]: the given model itself (and its parents via _inherits, if any)
429 - [obj1]: obj0 and all other models recursively accessible from
430 obj0 via one2many relationships
431 - [obj2]: obj0 and all other models recursively accessible from
432 obj0 via one2many and many2many relationships
433 - [obj3]: all models recursively accessible from obj1 via many2one
436 Each class is returned as a list of pairs [(field,model_browse)], where
437 ``model`` is the browse_record of a reachable ir.model, and ``field`` is
438 the dot-notation reverse relationship path coming from that model to obj0,
439 or None if there is no reverse path.
441 :return: ([obj0], [obj1], [obj2], [obj3])
443 # obj0 class and its parents
444 obj0 = [(None, model)]
445 model_obj = self.pool.get(model.model)
446 ir_model_obj = self.pool.get('ir.model')
447 for parent in model_obj._inherits:
448 parent_model_browse = ir_model_obj.browse(cr, UID_ROOT,
449 ir_model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
450 obj0 += [(None, parent_model_browse)]
452 obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], relation_fields=obj0, context=context)
453 obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], relation_fields=obj0, context=context)
454 obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
455 for dummy, model in obj1:
456 obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
457 return obj0, obj1, obj2, obj3
459 def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None):
460 model_access_obj = self.pool.get('ir.model.access')
461 user_right_ids = model_access_obj.search(cr, uid,
462 [('group_id', 'in', group_ids), ('model_id', 'in', model_ids)],
464 user_access_matrix = {}
466 for access_right in model_access_obj.browse(cr, uid, user_right_ids, context=context):
467 access_line = user_access_matrix.setdefault(access_right.model_id.model, set())
468 for perm in FULL_ACCESS:
469 if getattr(access_right, perm, 0):
470 access_line.add(perm)
471 return user_access_matrix
473 def _add_access_rights_for_share_group(self, cr, uid, group_id, mode, fields_relations, context=None):
474 """Adds access rights to group_id on object models referenced in ``fields_relations``,
475 intersecting with access rights of current user to avoid granting too much rights
477 model_access_obj = self.pool.get('ir.model.access')
478 user_obj = self.pool.get('res.users')
479 target_model_ids = [x[1].id for x in fields_relations]
480 perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or READ_WRITE_ACCESS
481 current_user = user_obj.browse(cr, uid, uid, context=context)
483 current_user_access_map = self._get_access_map_for_groups_and_models(cr, uid,
484 [x.id for x in current_user.groups_id], target_model_ids, context=context)
485 group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
486 [group_id], target_model_ids, context=context)
487 self._logger.debug("Current user access matrix: %r", current_user_access_map)
488 self._logger.debug("New group current access matrix: %r", group_access_map)
490 # Create required rights if allowed by current user rights and not
492 for dummy, model in fields_relations:
494 'name': _('Copied access for sharing'),
495 'group_id': group_id,
496 'model_id': model.id,
498 current_user_access_line = current_user_access_map.get(model.model,set())
499 existing_group_access_line = group_access_map.get(model.model,set())
500 need_creation = False
501 for perm in perms_to_add:
502 if perm in current_user_access_line \
503 and perm not in existing_group_access_line:
504 values.update({perm:True})
505 group_access_map.setdefault(model.model, set()).add(perm)
508 model_access_obj.create(cr, UID_ROOT, values)
509 self._logger.debug("Creating access right for model %s with values: %r", model.model, values)
511 def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
512 rule_obj = self.pool.get('ir.rule')
514 for group in current_user.groups_id:
515 for dummy, model in fields_relations:
516 for rule in group.rule_groups:
517 if rule.id in rules_done:
519 rules_done.add(rule.id)
520 if rule.model_id.id == model.id:
521 if 'user.' in rule.domain_force:
522 # Above pattern means there is likely a condition
523 # specific to current user, so we must copy the rule using
524 # the evaluated version of the domain.
525 # And it's better to copy one time too much than too few
526 rule_obj.copy(cr, UID_ROOT, rule.id, default={
527 'name': '%s %s' %(rule.name, _('(Copy for sharing)')),
528 'groups': [(6,0,[group_id])],
529 'domain_force': rule.domain, # evaluated version!
531 self._logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
533 # otherwise we can simply link the rule to keep it dynamic
534 rule_obj.write(cr, 1, [rule.id], {
535 'groups': [(4,group_id)]
537 self._logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
539 def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
540 """Verifies that the given rule only belongs to the given group_id, otherwise
541 duplicate it for the current group, and unlink the previous one.
542 The duplicated rule has the original domain copied verbatim, without
544 Returns the final rule to use (browse_record), either the original one if it
545 only belongs to this group, or the copy."""
546 if len(rule.groups) == 1:
548 # duplicate it first:
549 rule_obj = self.pool.get('ir.rule')
550 new_id = rule_obj.copy(cr, UID_ROOT, rule.id,
552 'name': '%s %s' %(rule.name, _('(Duplicated for modified sharing permissions)')),
553 'groups': [(6,0,[group_id])],
554 'domain_force': rule.domain_force, # non evaluated!
556 self._logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
557 # then disconnect from group_id:
558 rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
559 return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
561 def _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, restrict=False, rule_name=None, context=None):
562 """Add a new ir.rule entry for model_id and domain on the target group_id.
563 If ``restrict`` is True, instead of adding a rule, the domain is
564 combined with AND operator with all existing rules in the group, to implement
565 an additional restriction (as of 6.1, multiple rules in the same group are
566 OR'ed by default, so a restriction must alter all existing rules)
568 This is necessary because the personal rules of the user that is sharing
569 are first copied to the new share group. Afterwards the filters used for
570 sharing are applied as an additional layer of rules, which are likely to
571 apply to the same model. The default rule algorithm would OR them (as of 6.1),
572 which would result in a combined set of permission that could be larger
573 than those of the user that is sharing! Hence we must forcefully AND the
575 One possibly undesirable effect can appear when sharing with a
576 pre-existing group, in which case altering pre-existing rules would not
577 be desired. This is addressed in the portal module.
579 if rule_name is None:
580 rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
581 (current_user.name, current_user.login, group_id)
582 rule_obj = self.pool.get('ir.rule')
583 rule_ids = rule_obj.search(cr, UID_ROOT, [('groups', 'in', group_id), ('model_id', '=', model_id)])
585 for rule in rule_obj.browse(cr, UID_ROOT, rule_ids, context=context):
586 if rule.domain_force == domain:
587 # don't create it twice!
591 self._logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
594 # restricting existing rules is done by adding the clause
595 # with an AND, but we can't alter the rule if it belongs to
596 # other groups, so we duplicate if needed
597 rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context)
598 eval_ctx = rule_obj._eval_context_for_combinations()
599 org_domain = expression.normalize(eval(rule.domain_force, eval_ctx))
600 new_clause = expression.normalize(eval(domain, eval_ctx))
601 combined_domain = expression.AND([new_clause, org_domain])
602 rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
603 self._logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
605 # Adding the new rule in the group is ok for normal cases, because rules
606 # in the same group and for the same model will be combined with OR
607 # (as of v6.1), so the desired effect is achieved.
608 rule_obj.create(cr, UID_ROOT, {
610 'model_id': model_id,
611 'domain_force': domain,
612 'groups': [(4,group_id)]
614 self._logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
616 def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
617 rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
618 (current_user.name, current_user.login, group_id)
620 domain = safe_eval(wizard_data.domain)
622 for rel_field, model in fields_relations:
624 if not rel_field: continue
625 for element in domain:
626 if expression.is_leaf(element):
627 left, operator, right = element
628 left = '%s.%s'%(rel_field, left)
629 element = left, operator, right
630 related_domain.append(element)
631 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
632 group_id, model_id=model.id, domain=str(related_domain),
633 rule_name=rule_name, restrict=True, context=context)
635 self._logger.exception('Failed to create share access')
636 raise osv.except_osv(_('Sharing access could not be created'),
637 _('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.'))
639 def _check_preconditions(self, cr, uid, wizard_data, context=None):
640 self._assert(wizard_data.action_id and wizard_data.access_mode,
641 _('Action and Access Mode are required to create a shared access'),
643 self._assert(self.has_share(cr, uid, context=context),
644 _('You must be a member of the Share/User group to use the share wizard'),
646 if wizard_data.user_type == 'emails':
647 self._assert(wizard_data.new_users,
648 _('Please indicate the emails of the persons to share with, one per line'),
651 def _create_share_users_group(self, cr, uid, wizard_data, context=None):
652 """Creates the appropriate share group and share users, and populates
653 result_line_ids of wizard_data with one line for each user.
655 :return: the new group id (to which the shared access should be granted)
657 group_id = self._create_share_group(cr, uid, wizard_data, context=context)
658 # First create any missing user, based on the email addresses provided
659 new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
660 # Finally, setup the new action and shortcut for the users.
662 # existing users still need to join the new group
663 self.pool.get('res.users').write(cr, UID_ROOT, existing_ids, {
664 'groups_id': [(4,group_id)],
666 # existing user don't need their home action replaced, only a new shortcut
667 self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
669 # new users need a new shortcut AND a home action
670 self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
673 def go_step_2(self, cr, uid, ids, context=None):
674 wizard_data = self.browse(cr, uid, ids[0], context=context)
675 self._check_preconditions(cr, uid, wizard_data, context=context)
677 # Create shared group and users
678 group_id = self._create_share_users_group(cr, uid, wizard_data, context=context)
680 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
682 model_obj = self.pool.get('ir.model')
683 model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0]
684 model = model_obj.browse(cr, uid, model_id, context=context)
687 # We have several classes of objects that should receive different access rights:
689 # - [obj0] be the target model itself (and its parents via _inherits, if any)
690 # - [obj1] be the target model and all other models recursively accessible from
691 # obj0 via one2many relationships
692 # - [obj2] be the target model and all other models recursively accessible from
693 # obj0 via one2many and many2many relationships
694 # - [obj3] be all models recursively accessible from obj1 via many2one relationships
695 # (currently not used)
696 obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
697 mode = wizard_data.access_mode
699 # Add access to [obj0] and [obj1] according to chosen mode
700 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
701 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
703 # Add read-only access (always) to [obj2]
704 self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
707 # A. On [obj0], [obj1], [obj2]: add all rules from all groups of
708 # the user that is sharing
709 # Warning: rules must be copied instead of linked if they contain a reference
710 # to uid or if the rule is shared with other groups (and it must be replaced correctly)
711 # B. On [obj0]: 1 rule with domain of shared action
712 # C. For each model in [obj1]: 1 rule in the form:
713 # many2one_rel.domain_of_obj0
714 # where many2one_rel is the many2one used in the definition of the
715 # one2many, and domain_of_obj0 is the sharing domain
716 # For example if [obj0] is project.project with a domain of
717 # ['id', 'in', [1,2]]
718 # then we will have project.task in [obj1] and we need to create this
719 # ir.rule on project.task:
720 # ['project_id.id', 'in', [1,2]]
723 all_relations = obj0 + obj1 + obj2
724 self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
726 main_domain = wizard_data.domain if wizard_data.domain != '[]' else DOMAIN_ALL
727 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
728 group_id, model_id=model.id, domain=main_domain,
729 restrict=True, context=context)
731 self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
733 # refresh wizard_data
734 wizard_data = self.browse(cr, uid, ids[0], context=context)
736 # send the confirmation emails:
737 self.send_emails(cr, uid, wizard_data, context=context)
739 dummy, step2_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step2_form')
741 'name': _('Shared access created!'),
744 'res_model': 'share.wizard',
747 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
748 'type': 'ir.actions.act_window',
752 def send_emails(self, cr, uid, wizard_data, context=None):
753 self._logger.info('Sending share notifications by email...')
754 mail_message = self.pool.get('mail.message')
755 user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
756 if not user.user_email:
757 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.'))
759 # TODO: also send an HTML version of this mail
761 for result_line in wizard_data.result_line_ids:
762 email_to = result_line.user_id.user_email
765 subject = wizard_data.name
768 body += _("I've shared %s with you!") % wizard_data.name
770 body += _("The documents are not attached, you can view them online directly on my OpenERP server at:")
771 body += "\n " + result_line.share_url
773 if wizard_data.message:
774 body += wizard_data.message
776 if result_line.newly_created:
777 body += _("These are your credentials to access this protected area:\n")
778 body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
779 body += "%s: %s" % (_("Password"), result_line.password) + "\n"
780 body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
782 body += _("The documents have been automatically added to your current OpenERP documents.\n")
783 body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
785 body += (user.signature or '')
788 body += _("OpenERP is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
789 "It is open source and can be found on http://www.openerp.com.")
790 msg_ids.append(mail_message.schedule_with_attach(cr, uid,
795 model='share.wizard',
797 # force direct delivery, as users expect instant notification
798 mail_message.send(cr, uid, msg_ids, context=context)
799 self._logger.info('%d share notification(s) sent.', len(msg_ids))
801 def onchange_embed_options(self, cr, uid, ids, opt_title, opt_search, context=None):
802 wizard = self.browse(cr, uid, ids[0], context)
803 options = dict(title=opt_title, search=opt_search)
804 return {'value': {'embed_code': self._generate_embedded_code(wizard, options)}}
808 class share_result_line(osv.osv_memory):
809 _name = 'share.wizard.result.line'
810 _rec_name = 'user_id'
813 def _share_url(self, cr, uid, ids, _fieldname, _args, context=None):
814 result = dict.fromkeys(ids, '')
815 for this in self.browse(cr, uid, ids, context=context):
816 data = dict(dbname=cr.dbname, login=this.login)
817 result[this.id] = this.share_wizard_id.share_url_template() % data
821 'user_id': fields.many2one('res.users', required=True, readonly=True),
822 'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
823 'password': fields.char('Password', size=64, readonly=True),
824 'share_url': fields.function(_share_url, string='Share URL', type='char', size=512),
825 'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True),
826 'newly_created': fields.boolean('Newly created', readonly=True),
829 'newly_created': True,
832 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: