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
27 from osv import osv, fields
28 from osv import expression
29 from tools.translate import _
30 from tools.safe_eval import safe_eval
32 FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink')
33 READ_WRITE_ACCESS = ('perm_read', 'perm_write')
34 READ_ONLY_ACCESS = ('perm_read',)
37 # Pseudo-domain to represent an empty filter, constructed using
38 # osv.expression's DUMMY_LEAF
39 DOMAIN_ALL = [(1, '=', 1)]
41 # A good selection of easy to read password characters (e.g. no '0' vs 'O', etc.)
42 RANDOM_PASS_CHARACTERS = 'aaaabcdeeeefghjkmnpqrstuvwxyzAAAABCDEEEEFGHJKLMNPQRSTUVWXYZ23456789'
43 def generate_random_pass():
44 return ''.join(random.sample(RANDOM_PASS_CHARACTERS,10))
47 # Utils for introspecting the ORM - could be moved to server someday
48 class column_info(object):
49 """Struct containing details about an osv column, either one local to
50 its model, or one inherited via _inherits.
52 :attr name: name of the column
53 :attr column: column instance, subclass of osv.fields.column
54 :attr parent_model: if the column is inherited, name of the model
55 that contains it, None for local columns.
56 :attr parent_column: the name of the column containing the m2o
57 relationship to the parent model that contains
58 this column, None for local columns.
60 def __init__(self, name, column, parent_model=None, parent_column=None):
63 self.parent_model = parent_model
64 self.parent_column = parent_column
66 def get_column_infos(osv_model):
67 """Returns a dict mapping all fields names (direct fields and
68 inherited field via _inherits) to a ``column_info`` struct
69 giving detailed columns """
71 for k, (parent,m2o,col) in osv_model._inherit_fields.iteritems():
72 result[k] = column_info(k, col, parent, m2o)
73 for k, v in osv_model._columns.iteritems():
74 result[k] = column_info(k,v)
78 class share_wizard(osv.osv_memory):
79 _logger = logging.getLogger('share.wizard')
80 _name = 'share.wizard'
81 _description = 'Share Wizard'
83 def _assert(self, condition, error_message, context=None):
84 """Raise a user error with the given message if condition is not met.
85 The error_message should have been translated with _().
88 raise osv.except_osv(_('Sharing access could not be created'), error_message)
90 def has_group(self, cr, uid, module, group_xml_id, context=None):
91 """Returns True if current user is a member of the group identified by the module, group_xml_id pair."""
92 # if the group was deleted or does not exist, we say NO (better safe than sorry)
94 model, group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module, group_xml_id)
97 return group_id in self.pool.get('res.users').read(cr, uid, uid, ['groups_id'], context=context)['groups_id']
99 def has_share(self, cr, uid, context=None):
100 return self.has_group(cr, uid, module='share', group_xml_id='group_share_user', context=context)
102 def has_email(self, cr, uid, context=None):
103 return bool(self.pool.get('res.users').browse(cr, uid, uid, context=context).user_email)
105 def view_init(self, cr, uid, fields_list, context=None):
106 if not self.has_email(cr, uid, context=context):
107 raise osv.except_osv(_('No e-mail address configured'),
108 _('You must configure your e-mail address in the user preferences before using the Share button.'))
109 return super(share_wizard, self).view_init(cr, uid, fields_list, context=context)
111 def _user_type_selection(self, cr, uid, context=None):
112 """Selection values may be easily overridden/extended via inheritance"""
113 return [('emails','List of emails')]
115 """Override of create() to auto-compute the action name"""
116 def create(self, cr, uid, values, context=None):
117 if 'action_id' in values and not 'name' in values:
118 action = self.pool.get('ir.actions.actions').browse(cr, uid, values['action_id'], context=context)
119 values['name'] = action.name
120 return super(share_wizard,self).create(cr, uid, values, context=context)
122 def share_url_template(self, cr, uid, _ids, context=None):
123 # NOTE: take _ids in parameter to allow usage through browse_record objects
124 base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
126 base_url += '/?db=%(dbname)s&login=%(login)s'
129 def _share_root_url(self, cr, uid, ids, _fieldname, _args, context=None):
130 result = dict.fromkeys(ids, '')
131 data = dict(dbname=cr.dbname, login='')
132 for this in self.browse(cr, uid, ids, context=context):
133 result[this.id] = this.share_url_template() % data
137 'action_id': fields.many2one('ir.actions.act_window', 'Action to share', required=True,
138 help="The action that opens the screen containing the data you wish to share."),
139 'domain': fields.char('Domain', size=256, help="Optional domain for further data filtering"),
140 'user_type': fields.selection(lambda s, *a, **k: s._user_type_selection(*a, **k),'Users to share with', required=True,
141 help="Select the type of user(s) you would like to share data with."),
142 'new_users': fields.text("Emails"),
143 'access_mode': fields.selection([('readonly','Can view'),('readwrite','Can edit')],'Access Mode', required=True,
144 help="Access rights to be granted on the shared documents."),
145 'result_line_ids': fields.one2many('share.wizard.result.line', 'share_wizard_id', 'Summary', readonly=True),
146 'share_root_url': fields.function(_share_root_url, string='Share Access URL', type='char', size=512, readonly=True,
147 help='Main access page for users that are granted shared access'),
148 'name': fields.char('Share Title', size=64, required=True, help="Title for the share (displayed to users as menu and shortcut name)"),
149 'message': fields.text("Personal Message", help="An optional personal message, to be included in the e-mail notification."),
152 'user_type' : 'emails',
153 'domain': lambda self, cr, uid, context, *a: context.get('domain', '[]'),
154 'action_id': lambda self, cr, uid, context, *a: context.get('action_id'),
155 'access_mode': 'readonly',
158 def go_step_1(self, cr, uid, ids, context=None):
159 dummy, step1_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step1_form')
161 'name': _('Grant instant access to your documents'),
164 'res_model': 'share.wizard',
167 'views': [(step1_form_view_id, 'form')],
168 'type': 'ir.actions.act_window',
172 def _create_share_group(self, cr, uid, wizard_data, context=None):
173 group_obj = self.pool.get('res.groups')
174 share_group_name = '%s: %s (%d-%s)' %('Shared', wizard_data.name, uid, time.time())
175 # create share group without putting admin in it
176 return group_obj.create(cr, UID_ROOT, {'name': share_group_name, 'share': True}, {'noadmin': True})
178 def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None):
179 """Create one new res.users record for each email address provided in
180 wizard_data.new_users, ignoring already existing users.
181 Populates wizard_data.result_line_ids with one new line for
182 each user (existing or not). New users will also have a value
183 for the password field, so they can receive it by email.
184 Returns the ids of the created users, and the ids of the
185 ignored, existing ones."""
186 user_obj = self.pool.get('res.users')
187 current_user = user_obj.browse(cr, UID_ROOT, uid, context=context)
190 for new_user in (wizard_data.new_users or '').split('\n'):
192 new_user = new_user.strip()
193 if not new_user: continue
194 # Ignore the user if it already exists.
195 existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
196 existing_ids.extend(existing)
198 new_line = { 'user_id': existing[0],
199 'newly_created': False}
200 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
202 new_pass = generate_random_pass()
203 user_id = user_obj.create(cr, UID_ROOT, {
205 'password': new_pass,
207 'user_email': new_user,
208 'groups_id': [(6,0,[group_id])],
210 'company_id': current_user.company_id.id
212 new_line = { 'user_id': user_id,
213 'password': new_pass,
214 'newly_created': True}
215 wizard_data.write({'result_line_ids': [(0,0,new_line)]})
216 created_ids.append(user_id)
217 return created_ids, existing_ids
219 def _create_shortcut(self, cr, uid, values, context=None):
222 new_context = context.copy()
224 if key.startswith('default_'):
227 dataobj = self.pool.get('ir.model.data')
228 menu_id = dataobj._get_id(cr, uid, 'base', 'menu_administration_shortcut')
229 shortcut_menu_id = int(dataobj.read(cr, uid, menu_id, ['res_id'], new_context)['res_id'])
230 action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, values, new_context)
231 menu_data = {'name': values['name'],
233 'action': 'ir.actions.act_window,'+str(action_id),
234 'parent_id': shortcut_menu_id,
235 'icon': 'STOCK_JUSTIFY_FILL'}
236 menu_obj = self.pool.get('ir.ui.menu')
237 menu_id = menu_obj.create(cr, UID_ROOT, menu_data)
238 sc_data = {'name': values['name'], 'sequence': UID_ROOT,'res_id': menu_id }
239 self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, new_context)
242 user_groups = set(self.pool.get('res.users').read(cr, UID_ROOT, uid, ['groups_id'])['groups_id'])
243 key = (cr.dbname, shortcut_menu_id, tuple(user_groups))
244 menu_obj._cache[key] = True
247 def _cleanup_action_context(self, context_str, user_id):
248 """Returns a dict representing the context_str evaluated (safe_eval) as
249 a dict where items that are not useful for shared actions
250 have been removed. If the evaluation of context_str as a
251 dict fails, context_str is returned unaltered.
253 :param user_id: the integer uid to be passed as 'uid' in the
259 context = safe_eval(context_str, tools.UnquoteEvalContext(), nocopy=True)
260 result = dict(context)
262 # Remove all context keys that seem to toggle default
263 # filters based on the current user, as it makes no sense
264 # for shared users, who would not see any data by default.
265 if key and key.startswith('search_default_') and 'user_id' in key:
268 # Note: must catch all exceptions, as UnquoteEvalContext may cause many
269 # different exceptions, as it shadows builtins.
270 self._logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
274 def _shared_action_def(self, cr, uid, wizard_data, context=None):
275 copied_action = wizard_data.action_id
277 'name': wizard_data.name,
278 'domain': copied_action.domain,
279 'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
280 'res_model': copied_action.res_model,
281 'view_mode': copied_action.view_mode,
282 'view_type': copied_action.view_type,
283 'search_view_id': copied_action.search_view_id.id,
284 'view_id': copied_action.view_id.id,
286 if copied_action.view_ids:
287 action_def['view_ids'] = [(0,0,{'sequence': x.sequence,
288 'view_mode': x.view_mode,
289 'view_id': x.view_id.id })
290 for x in copied_action.view_ids]
293 def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, make_home, context=None):
294 """Create a shortcut to reach the shared data, as well as the corresponding action, for
295 each user in ``user_ids``, and assign it as their home action if ``make_home`` is True.
296 Meant to be overridden for special cases.
298 values = self._shared_action_def(cr, uid, wizard_data, context=None)
299 user_obj = self.pool.get('res.users')
300 for user_id in user_ids:
301 action_id = self._create_shortcut(cr, user_id, values)
303 # We do this only for new share users, as existing ones already have their initial home
304 # action. Resetting to the default menu does not work well as the menu is rather empty
305 # and does not contain the shortcuts in most cases.
306 user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
308 def _get_recursive_relations(self, cr, uid, model, ttypes, relation_fields=None, suffix=None, context=None):
309 """Returns list of tuples representing recursive relationships of type ``ttypes`` starting from
310 model with ID ``model_id``.
312 :param model: browsable model to start loading relationships from
313 :param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
314 :param relation_fields: list of previously followed relationship tuples - to avoid duplicates
316 :param suffix: optional suffix to append to the field path to reach the main object
318 if relation_fields is None:
320 local_rel_fields = []
321 models = [x[1].model for x in relation_fields]
322 model_obj = self.pool.get('ir.model')
323 model_osv = self.pool.get(model.model)
324 for colinfo in get_column_infos(model_osv).itervalues():
325 coldef = colinfo.column
326 coltype = coldef._type
327 relation_field = None
328 if coltype in ttypes and colinfo.column._obj not in models:
329 relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',coldef._obj)])[0]
330 relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
331 relation_osv = self.pool.get(coldef._obj)
332 if coltype == 'one2many':
333 # don't record reverse path if it's not a real m2o (that happens, but rarely)
334 dest_model_ci = get_column_infos(relation_osv)
335 reverse_rel = coldef._fields_id
336 if reverse_rel in dest_model_ci and dest_model_ci[reverse_rel].column._type == 'many2one':
337 relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
338 local_rel_fields.append((relation_field, relation_model_browse))
339 for parent in relation_osv._inherits:
340 if parent not in models:
341 parent_model = self.pool.get(parent)
342 parent_colinfos = get_column_infos(parent_model)
343 parent_model_browse = model_obj.browse(cr, UID_ROOT,
344 model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
345 if relation_field and coldef._fields_id in parent_colinfos:
346 # inverse relationship is available in the parent
347 local_rel_fields.append((relation_field, parent_model_browse))
349 # TODO: can we setup a proper rule to restrict inherited models
350 # in case the parent does not contain the reverse m2o?
351 local_rel_fields.append((None, parent_model_browse))
352 if relation_model_id != model.id and coltype in ['one2many', 'many2many']:
353 local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
354 [coltype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
355 return local_rel_fields
357 def _get_relationship_classes(self, cr, uid, model, context=None):
358 """Computes the *relationship classes* reachable from the given
359 model. The 4 relationship classes are:
360 - [obj0]: the given model itself (and its parents via _inherits, if any)
361 - [obj1]: obj0 and all other models recursively accessible from
362 obj0 via one2many relationships
363 - [obj2]: obj0 and all other models recursively accessible from
364 obj0 via one2many and many2many relationships
365 - [obj3]: all models recursively accessible from obj1 via many2one
368 Each class is returned as a list of pairs [(field,model_browse)], where
369 ``model`` is the browse_record of a reachable ir.model, and ``field`` is
370 the dot-notation reverse relationship path coming from that model to obj0,
371 or None if there is no reverse path.
373 :return: ([obj0], [obj1], [obj2], [obj3])
375 # obj0 class and its parents
376 obj0 = [(None, model)]
377 model_obj = self.pool.get(model.model)
378 ir_model_obj = self.pool.get('ir.model')
379 for parent in model_obj._inherits:
380 parent_model_browse = ir_model_obj.browse(cr, UID_ROOT,
381 ir_model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
382 obj0 += [(None, parent_model_browse)]
384 obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], relation_fields=obj0, context=context)
385 obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], relation_fields=obj0, context=context)
386 obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
387 for dummy, model in obj1:
388 obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
389 return obj0, obj1, obj2, obj3
391 def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None):
392 model_access_obj = self.pool.get('ir.model.access')
393 user_right_ids = model_access_obj.search(cr, uid,
394 [('group_id', 'in', group_ids), ('model_id', 'in', model_ids)],
396 user_access_matrix = {}
398 for access_right in model_access_obj.browse(cr, uid, user_right_ids, context=context):
399 access_line = user_access_matrix.setdefault(access_right.model_id.model, set())
400 for perm in FULL_ACCESS:
401 if getattr(access_right, perm, 0):
402 access_line.add(perm)
403 return user_access_matrix
405 def _add_access_rights_for_share_group(self, cr, uid, group_id, mode, fields_relations, context=None):
406 """Adds access rights to group_id on object models referenced in ``fields_relations``,
407 intersecting with access rights of current user to avoid granting too much rights
409 model_access_obj = self.pool.get('ir.model.access')
410 user_obj = self.pool.get('res.users')
411 target_model_ids = [x[1].id for x in fields_relations]
412 perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or READ_WRITE_ACCESS
413 current_user = user_obj.browse(cr, uid, uid, context=context)
415 current_user_access_map = self._get_access_map_for_groups_and_models(cr, uid,
416 [x.id for x in current_user.groups_id], target_model_ids, context=context)
417 group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
418 [group_id], target_model_ids, context=context)
419 self._logger.debug("Current user access matrix: %r", current_user_access_map)
420 self._logger.debug("New group current access matrix: %r", group_access_map)
422 # Create required rights if allowed by current user rights and not
424 for dummy, model in fields_relations:
426 'name': _('Copied access for sharing'),
427 'group_id': group_id,
428 'model_id': model.id,
430 current_user_access_line = current_user_access_map.get(model.model,set())
431 existing_group_access_line = group_access_map.get(model.model,set())
432 need_creation = False
433 for perm in perms_to_add:
434 if perm in current_user_access_line \
435 and perm not in existing_group_access_line:
436 values.update({perm:True})
437 group_access_map.setdefault(model.model, set()).add(perm)
440 model_access_obj.create(cr, UID_ROOT, values)
441 self._logger.debug("Creating access right for model %s with values: %r", model.model, values)
443 def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
444 rule_obj = self.pool.get('ir.rule')
446 for group in current_user.groups_id:
447 for dummy, model in fields_relations:
448 for rule in group.rule_groups:
449 if rule.id in rules_done:
451 rules_done.add(rule.id)
452 if rule.model_id.id == model.id:
453 if 'user.' in rule.domain_force:
454 # Above pattern means there is likely a condition
455 # specific to current user, so we must copy the rule using
456 # the evaluated version of the domain.
457 # And it's better to copy one time too much than too few
458 rule_obj.copy(cr, UID_ROOT, rule.id, default={
459 'name': '%s %s' %(rule.name, _('(Copy for sharing)')),
460 'groups': [(6,0,[group_id])],
461 'domain_force': rule.domain, # evaluated version!
463 self._logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
465 # otherwise we can simply link the rule to keep it dynamic
466 rule_obj.write(cr, 1, [rule.id], {
467 'groups': [(4,group_id)]
469 self._logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
471 def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
472 """Verifies that the given rule only belongs to the given group_id, otherwise
473 duplicate it for the current group, and unlink the previous one.
474 The duplicated rule has the original domain copied verbatim, without
476 Returns the final rule to use (browse_record), either the original one if it
477 only belongs to this group, or the copy."""
478 if len(rule.groups) == 1:
480 # duplicate it first:
481 rule_obj = self.pool.get('ir.rule')
482 new_id = rule_obj.copy(cr, UID_ROOT, rule.id,
484 'name': '%s %s' %(rule.name, _('(Duplicated for modified sharing permissions)')),
485 'groups': [(6,0,[group_id])],
486 'domain_force': rule.domain_force, # non evaluated!
488 self._logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
489 # then disconnect from group_id:
490 rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
491 return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
493 def _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, restrict=False, rule_name=None, context=None):
494 """Add a new ir.rule entry for model_id and domain on the target group_id.
495 If ``restrict`` is True, instead of adding a rule, the domain is
496 combined with AND operator with all existing rules in the group, to implement
497 an additional restriction (as of 6.1, multiple rules in the same group are
498 OR'ed by default, so a restriction must alter all existing rules)
500 This is necessary because the personal rules of the user that is sharing
501 are first copied to the new share group. Afterwards the filters used for
502 sharing are applied as an additional layer of rules, which are likely to
503 apply to the same model. The default rule algorithm would OR them (as of 6.1),
504 which would result in a combined set of permission that could be larger
505 than those of the user that is sharing! Hence we must forcefully AND the
507 One possibly undesirable effect can appear when sharing with a
508 pre-existing group, in which case altering pre-existing rules would not
509 be desired. This is addressed in the portal module.
511 if rule_name is None:
512 rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
513 (current_user.name, current_user.login, group_id)
514 rule_obj = self.pool.get('ir.rule')
515 rule_ids = rule_obj.search(cr, UID_ROOT, [('groups', 'in', group_id), ('model_id', '=', model_id)])
517 for rule in rule_obj.browse(cr, UID_ROOT, rule_ids, context=context):
518 if rule.domain_force == domain:
519 # don't create it twice!
523 self._logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
526 # restricting existing rules is done by adding the clause
527 # with an AND, but we can't alter the rule if it belongs to
528 # other groups, so we duplicate if needed
529 rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context)
530 eval_ctx = rule_obj._eval_context_for_combinations()
531 org_domain = expression.normalize(eval(rule.domain_force, eval_ctx))
532 new_clause = expression.normalize(eval(domain, eval_ctx))
533 combined_domain = expression.AND([new_clause, org_domain])
534 rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
535 self._logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
537 # Adding the new rule in the group is ok for normal cases, because rules
538 # in the same group and for the same model will be combined with OR
539 # (as of v6.1), so the desired effect is achieved.
540 rule_obj.create(cr, UID_ROOT, {
542 'model_id': model_id,
543 'domain_force': domain,
544 'groups': [(4,group_id)]
546 self._logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
548 def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
549 rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
550 (current_user.name, current_user.login, group_id)
552 domain = safe_eval(wizard_data.domain)
554 for rel_field, model in fields_relations:
556 if not rel_field: continue
557 for element in domain:
558 if expression.is_leaf(element):
559 left, operator, right = element
560 left = '%s.%s'%(rel_field, left)
561 element = left, operator, right
562 related_domain.append(element)
563 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
564 group_id, model_id=model.id, domain=str(related_domain),
565 rule_name=rule_name, restrict=True, context=context)
567 self._logger.exception('Failed to create share access')
568 raise osv.except_osv(_('Sharing access could not be created'),
569 _('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.'))
571 def _check_preconditions(self, cr, uid, wizard_data, context=None):
572 self._assert(wizard_data.action_id and wizard_data.access_mode,
573 _('Action and Access Mode are required to create a shared access'),
575 self._assert(self.has_share(cr, uid, context=context),
576 _('You must be a member of the Share/User group to use the share wizard'),
578 if wizard_data.user_type == 'emails':
579 self._assert(wizard_data.new_users,
580 _('Please indicate the emails of the persons to share with, one per line'),
583 def _create_share_users_group(self, cr, uid, wizard_data, context=None):
584 """Creates the appropriate share group and share users, and populates
585 result_line_ids of wizard_data with one line for each user.
587 :return: the new group id (to which the shared access should be granted)
589 group_id = self._create_share_group(cr, uid, wizard_data, context=context)
590 # First create any missing user, based on the email addresses provided
591 new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
592 # Finally, setup the new action and shortcut for the users.
594 # existing users still need to join the new group
595 self.pool.get('res.users').write(cr, UID_ROOT, existing_ids, {
596 'groups_id': [(4,group_id)],
598 # existing user don't need their home action replaced, only a new shortcut
599 self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
601 # new users need a new shortcut AND a home action
602 self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
605 def go_step_2(self, cr, uid, ids, context=None):
606 wizard_data = self.browse(cr, uid, ids[0], context=context)
607 self._check_preconditions(cr, uid, wizard_data, context=context)
609 # Create shared group and users
610 group_id = self._create_share_users_group(cr, uid, wizard_data, context=context)
612 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
614 model_obj = self.pool.get('ir.model')
615 model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0]
616 model = model_obj.browse(cr, uid, model_id, context=context)
619 # We have several classes of objects that should receive different access rights:
621 # - [obj0] be the target model itself (and its parents via _inherits, if any)
622 # - [obj1] be the target model and all other models recursively accessible from
623 # obj0 via one2many relationships
624 # - [obj2] be the target model and all other models recursively accessible from
625 # obj0 via one2many and many2many relationships
626 # - [obj3] be all models recursively accessible from obj1 via many2one relationships
627 # (currently not used)
628 obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
629 mode = wizard_data.access_mode
631 # Add access to [obj0] and [obj1] according to chosen mode
632 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
633 self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
635 # Add read-only access (always) to [obj2]
636 self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
639 # A. On [obj0], [obj1], [obj2]: add all rules from all groups of
640 # the user that is sharing
641 # Warning: rules must be copied instead of linked if they contain a reference
642 # to uid or if the rule is shared with other groups (and it must be replaced correctly)
643 # B. On [obj0]: 1 rule with domain of shared action
644 # C. For each model in [obj1]: 1 rule in the form:
645 # many2one_rel.domain_of_obj0
646 # where many2one_rel is the many2one used in the definition of the
647 # one2many, and domain_of_obj0 is the sharing domain
648 # For example if [obj0] is project.project with a domain of
649 # ['id', 'in', [1,2]]
650 # then we will have project.task in [obj1] and we need to create this
651 # ir.rule on project.task:
652 # ['project_id.id', 'in', [1,2]]
655 all_relations = obj0 + obj1 + obj2
656 self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
658 main_domain = wizard_data.domain if wizard_data.domain != '[]' else DOMAIN_ALL
659 self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
660 group_id, model_id=model.id, domain=main_domain,
661 restrict=True, context=context)
663 self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
665 # refresh wizard_data
666 wizard_data = self.browse(cr, uid, ids[0], context=context)
668 # send the confirmation emails:
669 self.send_emails(cr, uid, wizard_data, context=context)
671 dummy, step2_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step2_form')
673 'name': _('Shared access created!'),
676 'res_model': 'share.wizard',
679 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
680 'type': 'ir.actions.act_window',
684 def send_emails(self, cr, uid, wizard_data, context=None):
685 self._logger.info('Sending share notifications by email...')
686 mail_message = self.pool.get('mail.message')
687 user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
688 if not user.user_email:
689 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.'))
691 # TODO: also send an HTML version of this mail
693 for result_line in wizard_data.result_line_ids:
694 email_to = result_line.user_id.user_email
695 subject = wizard_data.name
698 body += _("I've shared %s with you!") % wizard_data.name
700 body += _("The documents are not attached, you can view them online directly on my OpenERP server at:")
701 body += "\n " + result_line.share_url
703 if wizard_data.message:
704 body += wizard_data.message
706 if result_line.newly_created:
707 body += _("These are your credentials to access this protected area:\n")
708 body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
709 body += "%s: %s" % (_("Password"), result_line.password) + "\n"
710 body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
712 body += _("The documents have been automatically added to your current OpenERP documents.\n")
713 body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
715 body += user.signature
718 body += _("OpenERP is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
719 "It is open source and can be found on http://www.openerp.com.")
721 if mail_message.schedule_with_attach(cr, uid,
726 model='share.wizard'):
729 self._logger.warning('Failed to send share notification from %s to %s, ignored', user.user_email, email_to)
730 self._logger.info('%s share notification(s) successfully sent.', emails_sent)
733 class share_result_line(osv.osv_memory):
734 _name = 'share.wizard.result.line'
735 _rec_name = 'user_id'
738 def _share_url(self, cr, uid, ids, _fieldname, _args, context=None):
739 result = dict.fromkeys(ids, '')
740 for this in self.browse(cr, uid, ids, context=context):
741 data = dict(dbname=cr.dbname, login=this.login)
742 result[this.id] = this.share_wizard_id.share_url_template() % data
746 'user_id': fields.many2one('res.users', required=True, readonly=True),
747 'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
748 'password': fields.char('Password', size=64, readonly=True),
749 'share_url': fields.function(_share_url, string='Share URL', type='char', size=512),
750 'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True),
751 'newly_created': fields.boolean('Newly created', readonly=True),
754 'newly_created': True,
758 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: