summaryrefslogtreecommitdiff
path: root/addons/share/wizard
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2015-07-09 13:35:24 +0200
committerAntony Lesuisse <al@openerp.com>2015-07-09 13:50:28 +0200
commitd2a66f171e64a871c74bb58adeda281fd3c945a2 (patch)
tree23bb3e9eea8711c7196bfc24dd8a8207d17b597b /addons/share/wizard
parentcf63d4d277ef1ba02ff4ebcdae8583332a1775b1 (diff)
[REM] share
share is now deprecated by the website /my portal.
Diffstat (limited to 'addons/share/wizard')
-rw-r--r--addons/share/wizard/__init__.py4
-rw-r--r--addons/share/wizard/share_wizard.py898
-rw-r--r--addons/share/wizard/share_wizard_view.xml138
3 files changed, 0 insertions, 1040 deletions
diff --git a/addons/share/wizard/__init__.py b/addons/share/wizard/__init__.py
deleted file mode 100644
index 0e3ca7a3297..00000000000
--- a/addons/share/wizard/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import share_wizard
diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py
deleted file mode 100644
index b80c60f9ba7..00000000000
--- a/addons/share/wizard/share_wizard.py
+++ /dev/null
@@ -1,898 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import logging
-import random
-import time
-import uuid
-from openerp import SUPERUSER_ID
-
-import simplejson
-
-from openerp import api
-from openerp import tools
-from openerp.osv import fields, osv
-from openerp.osv import expression
-from openerp.tools.translate import _
-from openerp.tools.safe_eval import safe_eval
-import openerp
-from openerp.exceptions import UserError
-_logger = logging.getLogger(__name__)
-
-FULL_ACCESS = ('perm_read', 'perm_write', 'perm_create', 'perm_unlink')
-READ_WRITE_ACCESS = ('perm_read', 'perm_write')
-READ_ONLY_ACCESS = ('perm_read',)
-UID_ROOT = 1
-
-# Pseudo-domain to represent an empty filter, constructed using
-# osv.expression's DUMMY_LEAF
-DOMAIN_ALL = [(1, '=', 1)]
-
-# A good selection of easy to read password characters (e.g. no '0' vs 'O', etc.)
-RANDOM_PASS_CHARACTERS = 'aaaabcdeeeefghjkmnpqrstuvwxyzAAAABCDEEEEFGHJKLMNPQRSTUVWXYZ23456789'
-def generate_random_pass():
- return ''.join(random.sample(RANDOM_PASS_CHARACTERS,10))
-
-class share_wizard(osv.TransientModel):
- _name = 'share.wizard'
- _description = 'Share Wizard'
-
- def _assert(self, condition, error_message, context=None):
- """Raise a user error with the given message if condition is not met.
- The error_message should have been translated with _().
- """
- if not condition:
- raise UserError(error_message)
-
- def has_group(self, cr, uid, module, group_xml_id, context=None):
- """Returns True if current user is a member of the group identified by the module, group_xml_id pair."""
- # if the group was deleted or does not exist, we say NO (better safe than sorry)
- try:
- model, group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module, group_xml_id)
- except ValueError:
- return False
- return group_id in self.pool.get('res.users').read(cr, uid, [uid], ['groups_id'], context=context)[0]['groups_id']
-
- def has_share(self, cr, uid, unused_param, context=None):
- return self.has_group(cr, uid, module='base', group_xml_id='group_no_one', context=context)
-
- def _user_type_selection(self, cr, uid, context=None):
- """Selection values may be easily overridden/extended via inheritance"""
- return [('embedded', _('Direct link or embed code')), ('emails',_('Emails')), ]
-
- """Override of create() to auto-compute the action name"""
- def create(self, cr, uid, values, context=None):
- if 'action_id' in values and not 'name' in values:
- action = self.pool.get('ir.actions.actions').browse(cr, uid, values['action_id'], context=context)
- values['name'] = action.name
- return super(share_wizard,self).create(cr, uid, values, context=context)
-
- @api.cr_uid_ids_context
- def share_url_template(self, cr, uid, _ids, context=None):
- # NOTE: take _ids in parameter to allow usage through browse_record objects
- base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
- if base_url:
- base_url += '/login?db=%(dbname)s&login=%(login)s&key=%(password)s'
- extra = context and context.get('share_url_template_extra_arguments')
- if extra:
- base_url += '&' + '&'.join('%s=%%(%s)s' % (x,x) for x in extra)
- hash_ = context and context.get('share_url_template_hash_arguments')
- if hash_:
- base_url += '#' + '&'.join('%s=%%(%s)s' % (x,x) for x in hash_)
- return base_url
-
- def _share_root_url(self, cr, uid, ids, _fieldname, _args, context=None):
- result = dict.fromkeys(ids, '')
- data = dict(dbname=cr.dbname, login='', password='')
- for this in self.browse(cr, uid, ids, context=context):
- result[this.id] = this.share_url_template() % data
- return result
-
- def _generate_embedded_code(self, wizard, options=None):
- cr, uid, context = wizard.env.args
- if options is None:
- options = {}
-
- js_options = {}
- title = options['title'] if 'title' in options else wizard.embed_option_title
- search = (options['search'] if 'search' in options else wizard.embed_option_search) if wizard.access_mode != 'readonly' else False
-
- if not title:
- js_options['display_title'] = False
- if search:
- js_options['search_view'] = True
-
- js_options_str = (', ' + simplejson.dumps(js_options)) if js_options else ''
-
- base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default=None, context=context)
- user = wizard.result_line_ids[0]
-
- return """
-<script type="text/javascript" src="%(base_url)s/web/webclient/js"></script>
-<script type="text/javascript">
- new openerp.init(%(init)s).web.embed(%(server)s, %(dbname)s, %(login)s, %(password)s,%(action)d%(options)s);
-</script> """ % {
- 'init': simplejson.dumps(openerp.conf.server_wide_modules),
- 'base_url': base_url or '',
- 'server': simplejson.dumps(base_url),
- 'dbname': simplejson.dumps(cr.dbname),
- 'login': simplejson.dumps(user.login),
- 'password': simplejson.dumps(user.password),
- 'action': user.user_id.action_id.id,
- 'options': js_options_str,
- }
-
- def _embed_code(self, cr, uid, ids, _fn, _args, context=None):
- result = dict.fromkeys(ids, '')
- for this in self.browse(cr, uid, ids, context=context):
- result[this.id] = self._generate_embedded_code(this)
- return result
-
- def _embed_url(self, cr, uid, ids, _fn, _args, context=None):
- if context is None:
- context = {}
- result = dict.fromkeys(ids, '')
- for this in self.browse(cr, uid, ids, context=context):
- if this.result_line_ids:
- ctx = dict(context, share_url_template_hash_arguments=['action'])
- user = this.result_line_ids[0]
- data = dict(dbname=cr.dbname, login=user.login, password=user.password, action=this.action_id.id)
- result[this.id] = this.share_url_template(context=ctx) % data
- return result
-
-
- _columns = {
- 'action_id': fields.many2one('ir.actions.act_window', 'Action to share', required=True,
- help="The action that opens the screen containing the data you wish to share."),
- 'view_type': fields.char('Current View Type', required=True),
- 'domain': fields.char('Domain', help="Optional domain for further data filtering"),
- 'user_type': fields.selection(lambda s, *a, **k: s._user_type_selection(*a, **k),'Sharing method', required=True,
- help="Select the type of user(s) you would like to share data with."),
- 'new_users': fields.text("Emails"),
- 'email_1': fields.char('New user email', size=64),
- 'email_2': fields.char('New user email', size=64),
- 'email_3': fields.char('New user email', size=64),
- 'invite': fields.boolean('Invite users to OpenSocial record'),
- 'access_mode': fields.selection([('readonly','Can view'),('readwrite','Can edit')],'Access Mode', required=True,
- help="Access rights to be granted on the shared documents."),
- 'result_line_ids': fields.one2many('share.wizard.result.line', 'share_wizard_id', 'Summary', readonly=True),
- 'share_root_url': fields.function(_share_root_url, string='Share Access URL', type='char', readonly=True,
- help='Main access page for users that are granted shared access'),
- 'name': fields.char('Share Title', required=True, help="Title for the share (displayed to users as menu and shortcut name)"),
- 'record_name': fields.char('Record name', help="Name of the shared record, if sharing a precise record"),
- 'message': fields.text("Personal Message", help="An optional personal message, to be included in the email notification."),
- 'embed_code': fields.function(_embed_code, type='text', string='Code',
- help="Embed this code in your documents to provide a link to the "\
- "shared document."),
- 'embed_option_title': fields.boolean('Display title'),
- 'embed_option_search': fields.boolean('Display search view'),
- 'embed_url': fields.function(_embed_url, string='Share URL', size=512, type='char', readonly=True),
- }
- _defaults = {
- 'view_type': 'page',
- 'user_type' : 'embedded',
- 'invite': False,
- 'domain': lambda self, cr, uid, context, *a: context.get('domain', '[]'),
- 'action_id': lambda self, cr, uid, context, *a: context.get('action_id'),
- 'access_mode': 'readwrite',
- 'embed_option_title': True,
- 'embed_option_search': True,
- }
-
- def has_email(self, cr, uid, context=None):
- return bool(self.pool.get('res.users').browse(cr, uid, uid, context=context).email)
-
- def go_step_1(self, cr, uid, ids, context=None):
- wizard_data = self.browse(cr,uid,ids,context)[0]
- if wizard_data.user_type == 'emails' and not self.has_email(cr, uid, context=context):
- raise UserError(_('You must configure your email address in the user preferences before using the Share button.'))
- model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'action_share_wizard_step1')
- action = self.pool[model].read(cr, uid, [res_id], context=context)[0]
- action['res_id'] = ids[0]
- action.pop('context', '')
- return action
-
- def _create_share_group(self, cr, uid, wizard_data, context=None):
- group_obj = self.pool.get('res.groups')
- share_group_name = '%s: %s (%d-%s)' %('Shared', wizard_data.name, uid, time.time())
- values = {'name': share_group_name, 'share': True}
- try:
- implied_group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'group_shared')[1]
- except ValueError:
- implied_group_id = None
- if implied_group_id:
- values['implied_ids'] = [(4, implied_group_id)]
- # create share group without putting admin in it
- return group_obj.create(cr, UID_ROOT, values, {'noadmin': True})
-
- def _create_new_share_users(self, cr, uid, wizard_data, group_id, context=None):
- """Create one new res.users record for each email address provided in
- wizard_data.new_users, ignoring already existing users.
- Populates wizard_data.result_line_ids with one new line for
- each user (existing or not). New users will also have a value
- for the password field, so they can receive it by email.
- Returns the ids of the created users, and the ids of the
- ignored, existing ones."""
- context = dict(context or {})
- user_obj = self.pool.get('res.users')
- current_user = user_obj.browse(cr, UID_ROOT, uid, context=context)
- # modify context to disable shortcuts when creating share users
- context['noshortcut'] = True
- context['no_reset_password'] = True
- created_ids = []
- existing_ids = []
- if wizard_data.user_type == 'emails':
- # get new user list from email data
- new_users = (wizard_data.new_users or '').split('\n')
- new_users += [wizard_data.email_1 or '', wizard_data.email_2 or '', wizard_data.email_3 or '']
- for new_user in new_users:
- # Ignore blank lines
- new_user = new_user.strip()
- if not new_user: continue
- # Ignore the user if it already exists.
- if not wizard_data.invite:
- existing = user_obj.search(cr, UID_ROOT, [('login', '=', new_user)])
- else:
- existing = user_obj.search(cr, UID_ROOT, [('email', '=', new_user)])
- existing_ids.extend(existing)
- if existing:
- new_line = { 'user_id': existing[0],
- 'newly_created': False}
- wizard_data.write({'result_line_ids': [(0,0,new_line)]})
- continue
- new_pass = generate_random_pass()
- user_id = user_obj.create(cr, UID_ROOT, {
- 'login': new_user,
- 'password': new_pass,
- 'name': new_user,
- 'email': new_user,
- 'groups_id': [(6,0,[group_id])],
- 'company_id': current_user.company_id.id,
- 'company_ids': [(6, 0, [current_user.company_id.id])],
- }, context)
- new_line = { 'user_id': user_id,
- 'password': new_pass,
- 'newly_created': True}
- wizard_data.write({'result_line_ids': [(0,0,new_line)]})
- created_ids.append(user_id)
-
- elif wizard_data.user_type == 'embedded':
- new_login = 'embedded-%s' % (uuid.uuid4().hex,)
- new_pass = generate_random_pass()
- user_id = user_obj.create(cr, UID_ROOT, {
- 'login': new_login,
- 'password': new_pass,
- 'name': new_login,
- 'groups_id': [(6,0,[group_id])],
- 'company_id': current_user.company_id.id,
- 'company_ids': [(6, 0, [current_user.company_id.id])],
- }, context)
- new_line = { 'user_id': user_id,
- 'password': new_pass,
- 'newly_created': True}
- wizard_data.write({'result_line_ids': [(0,0,new_line)]})
- created_ids.append(user_id)
-
- return created_ids, existing_ids
-
- def _create_action(self, cr, uid, values, context=None):
- if context is None:
- context = {}
- new_context = context.copy()
- for key in context:
- if key.startswith('default_'):
- del new_context[key]
- action_id = self.pool.get('ir.actions.act_window').create(cr, UID_ROOT, values, new_context)
- return action_id
-
- def _cleanup_action_context(self, context_str, user_id):
- """Returns a dict representing the context_str evaluated (safe_eval) as
- a dict where items that are not useful for shared actions
- have been removed. If the evaluation of context_str as a
- dict fails, context_str is returned unaltered.
-
- :param user_id: the integer uid to be passed as 'uid' in the
- evaluation context
- """
- result = False
- if context_str:
- try:
- context = safe_eval(context_str, tools.UnquoteEvalContext(), nocopy=True)
- result = dict(context)
- for key in context:
- # Remove all context keys that seem to toggle default
- # filters based on the current user, as it makes no sense
- # for shared users, who would not see any data by default.
- if key and key.startswith('search_default_') and 'user_id' in key:
- result.pop(key)
- except Exception:
- # Note: must catch all exceptions, as UnquoteEvalContext may cause many
- # different exceptions, as it shadows builtins.
- _logger.debug("Failed to cleanup action context as it does not parse server-side", exc_info=True)
- result = context_str
- return result
-
- def _shared_action_def(self, cr, uid, wizard_data, context=None):
- copied_action = wizard_data.action_id
-
- if wizard_data.access_mode == 'readonly':
- view_mode = wizard_data.view_type
- view_id = copied_action.view_id.id if copied_action.view_id.type == wizard_data.view_type else False
- else:
- view_mode = copied_action.view_mode
- view_id = copied_action.view_id.id
-
-
- action_def = {
- 'name': wizard_data.name,
- 'domain': copied_action.domain,
- 'context': self._cleanup_action_context(wizard_data.action_id.context, uid),
- 'res_model': copied_action.res_model,
- 'view_mode': view_mode,
- 'view_type': copied_action.view_type,
- 'search_view_id': copied_action.search_view_id.id if wizard_data.access_mode != 'readonly' else False,
- 'view_id': view_id,
- 'auto_search': True,
- }
- if copied_action.view_ids:
- action_def['view_ids'] = [(0,0,{'sequence': x.sequence,
- 'view_mode': x.view_mode,
- 'view_id': x.view_id.id })
- for x in copied_action.view_ids
- if (wizard_data.access_mode != 'readonly' or x.view_mode == wizard_data.view_type)
- ]
- return action_def
-
- def _setup_action_and_shortcut(self, cr, uid, wizard_data, user_ids, make_home, context=None):
- """Create a shortcut to reach the shared data, as well as the corresponding action, for
- each user in ``user_ids``, and assign it as their home action if ``make_home`` is True.
- Meant to be overridden for special cases.
- """
- values = self._shared_action_def(cr, uid, wizard_data, context=None)
- user_obj = self.pool.get('res.users')
- for user_id in user_ids:
- action_id = self._create_action(cr, user_id, values)
- if make_home:
- # We do this only for new share users, as existing ones already have their initial home
- # action. Resetting to the default menu does not work well as the menu is rather empty
- # and does not contain the shortcuts in most cases.
- user_obj.write(cr, UID_ROOT, [user_id], {'action_id': action_id})
-
- def _get_recursive_relations(self, cr, uid, model, ttypes, relation_fields=None, suffix=None, context=None):
- """Returns list of tuples representing recursive relationships of type ``ttypes`` starting from
- model with ID ``model_id``.
-
- :param model: browsable model to start loading relationships from
- :param ttypes: list of relationship types to follow (e.g: ['one2many','many2many'])
- :param relation_fields: list of previously followed relationship tuples - to avoid duplicates
- during recursion
- :param suffix: optional suffix to append to the field path to reach the main object
- """
- if relation_fields is None:
- relation_fields = []
- local_rel_fields = []
- models = [x[1].model for x in relation_fields]
- model_obj = self.pool.get('ir.model')
- model_osv = self.pool[model.model]
- for field in model_osv._fields.itervalues():
- ftype = field.type
- relation_field = None
- if ftype in ttypes and field.comodel_name not in models:
- relation_model_id = model_obj.search(cr, UID_ROOT, [('model','=',field.comodel_name)])[0]
- relation_model_browse = model_obj.browse(cr, UID_ROOT, relation_model_id, context=context)
- relation_osv = self.pool[field.comodel_name]
- #skip virtual one2many fields (related, ...) as there is no reverse relationship
- if ftype == 'one2many' and field.inverse_name:
- # don't record reverse path if it's not a real m2o (that happens, but rarely)
- dest_fields = relation_osv._fields
- reverse_rel = field.inverse_name
- if reverse_rel in dest_fields and dest_fields[reverse_rel].type == 'many2one':
- relation_field = ('%s.%s'%(reverse_rel, suffix)) if suffix else reverse_rel
- local_rel_fields.append((relation_field, relation_model_browse))
- for parent in relation_osv._inherits:
- if parent not in models:
- parent_model = self.pool[parent]
- parent_fields = parent_model._fields
- parent_model_browse = model_obj.browse(cr, UID_ROOT,
- model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
- if relation_field and field.inverse_name in parent_fields:
- # inverse relationship is available in the parent
- local_rel_fields.append((relation_field, parent_model_browse))
- else:
- # TODO: can we setup a proper rule to restrict inherited models
- # in case the parent does not contain the reverse m2o?
- local_rel_fields.append((None, parent_model_browse))
- if relation_model_id != model.id and ftype in ['one2many', 'many2many']:
- local_rel_fields += self._get_recursive_relations(cr, uid, relation_model_browse,
- [ftype], relation_fields + local_rel_fields, suffix=relation_field, context=context)
- return local_rel_fields
-
- def _get_relationship_classes(self, cr, uid, model, context=None):
- """Computes the *relationship classes* reachable from the given
- model. The 4 relationship classes are:
- - [obj0]: the given model itself (and its parents via _inherits, if any)
- - [obj1]: obj0 and all other models recursively accessible from
- obj0 via one2many relationships
- - [obj2]: obj0 and all other models recursively accessible from
- obj0 via one2many and many2many relationships
- - [obj3]: all models recursively accessible from obj1 via many2one
- relationships
-
- Each class is returned as a list of pairs [(field,model_browse)], where
- ``model`` is the browse_record of a reachable ir.model, and ``field`` is
- the dot-notation reverse relationship path coming from that model to obj0,
- or None if there is no reverse path.
-
- :return: ([obj0], [obj1], [obj2], [obj3])
- """
- # obj0 class and its parents
- obj0 = [(None, model)]
- model_obj = self.pool[model.model]
- ir_model_obj = self.pool.get('ir.model')
- for parent in model_obj._inherits:
- parent_model_browse = ir_model_obj.browse(cr, UID_ROOT,
- ir_model_obj.search(cr, UID_ROOT, [('model','=',parent)]))[0]
- obj0 += [(None, parent_model_browse)]
-
- obj1 = self._get_recursive_relations(cr, uid, model, ['one2many'], relation_fields=obj0, context=context)
- obj2 = self._get_recursive_relations(cr, uid, model, ['one2many', 'many2many'], relation_fields=obj0, context=context)
- obj3 = self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
- for dummy, model in obj1:
- obj3 += self._get_recursive_relations(cr, uid, model, ['many2one'], relation_fields=obj0, context=context)
- return obj0, obj1, obj2, obj3
-
- def _get_access_map_for_groups_and_models(self, cr, uid, group_ids, model_ids, context=None):
- model_access_obj = self.pool.get('ir.model.access')
- user_right_ids = model_access_obj.search(cr, uid,
- [('group_id', 'in', group_ids), ('model_id', 'in', model_ids)],
- context=context)
- user_access_matrix = {}
- if user_right_ids:
- for access_right in model_access_obj.browse(cr, uid, user_right_ids, context=context):
- access_line = user_access_matrix.setdefault(access_right.model_id.model, set())
- for perm in FULL_ACCESS:
- if getattr(access_right, perm, 0):
- access_line.add(perm)
- return user_access_matrix
-
- def _add_access_rights_for_share_group(self, cr, uid, group_id, mode, fields_relations, context=None):
- """Adds access rights to group_id on object models referenced in ``fields_relations``,
- intersecting with access rights of current user to avoid granting too much rights
- """
- model_access_obj = self.pool.get('ir.model.access')
- user_obj = self.pool.get('res.users')
- target_model_ids = [x[1].id for x in fields_relations]
- perms_to_add = (mode == 'readonly') and READ_ONLY_ACCESS or READ_WRITE_ACCESS
- current_user = user_obj.browse(cr, uid, uid, context=context)
-
- current_user_access_map = self._get_access_map_for_groups_and_models(cr, uid,
- [x.id for x in current_user.groups_id], target_model_ids, context=context)
- group_access_map = self._get_access_map_for_groups_and_models(cr, uid,
- [group_id], target_model_ids, context=context)
- _logger.debug("Current user access matrix: %r", current_user_access_map)
- _logger.debug("New group current access matrix: %r", group_access_map)
-
- # Create required rights if allowed by current user rights and not
- # already granted
- for dummy, model in fields_relations:
- # mail.message is transversal: it should not received directly the access rights
- if model.model in ['mail.message', 'mail.notification', 'res.company']: continue
- values = {
- 'name': _('Copied access for sharing'),
- 'group_id': group_id,
- 'model_id': model.id,
- }
- current_user_access_line = current_user_access_map.get(model.model,set())
- existing_group_access_line = group_access_map.get(model.model,set())
- need_creation = False
- for perm in perms_to_add:
- if perm in current_user_access_line \
- and perm not in existing_group_access_line:
- values.update({perm:True})
- group_access_map.setdefault(model.model, set()).add(perm)
- need_creation = True
- if need_creation:
- model_access_obj.create(cr, UID_ROOT, values)
- _logger.debug("Creating access right for model %s with values: %r", model.model, values)
-
- def _link_or_copy_current_user_rules(self, cr, current_user, group_id, fields_relations, context=None):
- rule_obj = self.pool.get('ir.rule')
- rules_done = set()
- for group in current_user.groups_id:
- for dummy, model in fields_relations:
- for rule in group.rule_groups:
- if rule.id in rules_done:
- continue
- rules_done.add(rule.id)
- if rule.model_id.id == model.id:
- if 'user.' in rule.domain_force:
- # Above pattern means there is likely a condition
- # specific to current user, so we must copy the rule using
- # the evaluated version of the domain.
- # And it's better to copy one time too much than too few
- rule_obj.copy(cr, UID_ROOT, rule.id, default={
- 'name': '%s %s' %(rule.name, _('(Copy for sharing)')),
- 'groups': [(6,0,[group_id])],
- 'domain_force': rule.domain, # evaluated version!
- })
- _logger.debug("Copying rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
- else:
- # otherwise we can simply link the rule to keep it dynamic
- rule_obj.write(cr, SUPERUSER_ID, [rule.id], {
- 'groups': [(4,group_id)]
- })
- _logger.debug("Linking rule %s (%s) on model %s with domain: %s", rule.name, rule.id, model.model, rule.domain_force)
-
- def _check_personal_rule_or_duplicate(self, cr, group_id, rule, context=None):
- """Verifies that the given rule only belongs to the given group_id, otherwise
- duplicate it for the current group, and unlink the previous one.
- The duplicated rule has the original domain copied verbatim, without
- any evaluation.
- Returns the final rule to use (browse_record), either the original one if it
- only belongs to this group, or the copy."""
- if len(rule.groups) == 1:
- return rule
- # duplicate it first:
- rule_obj = self.pool.get('ir.rule')
- new_id = rule_obj.copy(cr, UID_ROOT, rule.id,
- default={
- 'name': '%s %s' %(rule.name, _('(Duplicated for modified sharing permissions)')),
- 'groups': [(6,0,[group_id])],
- 'domain_force': rule.domain_force, # non evaluated!
- })
- _logger.debug("Duplicating rule %s (%s) (domain: %s) for modified access ", rule.name, rule.id, rule.domain_force)
- # then disconnect from group_id:
- rule.write({'groups':[(3,group_id)]}) # disconnects, does not delete!
- return rule_obj.browse(cr, UID_ROOT, new_id, context=context)
-
- def _create_or_combine_sharing_rule(self, cr, current_user, wizard_data, group_id, model_id, domain, restrict=False, rule_name=None, context=None):
- """Add a new ir.rule entry for model_id and domain on the target group_id.
- If ``restrict`` is True, instead of adding a rule, the domain is
- combined with AND operator with all existing rules in the group, to implement
- an additional restriction (as of 6.1, multiple rules in the same group are
- OR'ed by default, so a restriction must alter all existing rules)
-
- This is necessary because the personal rules of the user that is sharing
- are first copied to the new share group. Afterwards the filters used for
- sharing are applied as an additional layer of rules, which are likely to
- apply to the same model. The default rule algorithm would OR them (as of 6.1),
- which would result in a combined set of permission that could be larger
- than those of the user that is sharing! Hence we must forcefully AND the
- rules at this stage.
- One possibly undesirable effect can appear when sharing with a
- pre-existing group, in which case altering pre-existing rules would not
- be desired. This is addressed in the portal module.
- """
- if rule_name is None:
- rule_name = _('Sharing filter created by user %s (%s) for group %s') % \
- (current_user.name, current_user.login, group_id)
- rule_obj = self.pool.get('ir.rule')
- rule_ids = rule_obj.search(cr, UID_ROOT, [('groups', 'in', group_id), ('model_id', '=', model_id)])
- if rule_ids:
- for rule in rule_obj.browse(cr, UID_ROOT, rule_ids, context=context):
- if rule.domain_force == domain:
- # don't create it twice!
- if restrict:
- continue
- else:
- _logger.debug("Ignoring sharing rule on model %s with domain: %s the same rule exists already", model_id, domain)
- return
- if restrict:
- # restricting existing rules is done by adding the clause
- # with an AND, but we can't alter the rule if it belongs to
- # other groups, so we duplicate if needed
- rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context)
- eval_ctx = rule_obj._eval_context_for_combinations()
- org_domain = expression.normalize_domain(safe_eval(rule.domain_force, eval_ctx))
- new_clause = expression.normalize_domain(safe_eval(domain, eval_ctx))
- combined_domain = expression.AND([new_clause, org_domain])
- rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')})
- _logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain)
- if not rule_ids or not restrict:
- # Adding the new rule in the group is ok for normal cases, because rules
- # in the same group and for the same model will be combined with OR
- # (as of v6.1), so the desired effect is achieved.
- rule_obj.create(cr, UID_ROOT, {
- 'name': rule_name,
- 'model_id': model_id,
- 'domain_force': domain,
- 'groups': [(4,group_id)]
- })
- _logger.debug("Created sharing rule on model %s with domain: %s", model_id, domain)
-
- def _create_indirect_sharing_rules(self, cr, current_user, wizard_data, group_id, fields_relations, context=None):
- rule_name = _('Indirect sharing filter created by user %s (%s) for group %s') % \
- (current_user.name, current_user.login, group_id)
- try:
- domain = safe_eval(wizard_data.domain)
- if domain:
- for rel_field, model in fields_relations:
- # mail.message is transversal: it should not received directly the access rights
- if model.model in ['mail.message', 'mail.notification', 'res.company']: continue
- related_domain = []
- if not rel_field: continue
- for element in domain:
- if expression.is_leaf(element):
- left, operator, right = element
- left = '%s.%s'%(rel_field, left)
- element = left, operator, right
- related_domain.append(element)
- self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
- group_id, model_id=model.id, domain=str(related_domain),
- rule_name=rule_name, restrict=True, context=context)
- except Exception:
- _logger.info('Failed to create share access', exc_info=True)
- raise UserError(_('Sorry, the current screen and filter you are trying to share are not supported at the moment.\nYou may want to try a simpler filter.'))
-
- def _check_preconditions(self, cr, uid, wizard_data, context=None):
- self._assert(wizard_data.action_id and wizard_data.access_mode,
- _('Action and Access Mode are required to create a shared access.'),
- context=context)
- self._assert(self.has_share(cr, uid, wizard_data, context=context),
- _('You must be a member of the Technical group to use the share wizard.'),
- context=context)
- if wizard_data.user_type == 'emails':
- self._assert((wizard_data.new_users or wizard_data.email_1 or wizard_data.email_2 or wizard_data.email_3),
- _('Please indicate the emails of the persons to share with, one per line.'),
- context=context)
-
- def _create_share_users_group(self, cr, uid, wizard_data, context=None):
- """Creates the appropriate share group and share users, and populates
- result_line_ids of wizard_data with one line for each user.
-
- :return: a tuple composed of the new group id (to which the shared access should be granted),
- the ids of the new share users that have been created and the ids of the existing share users
- """
- group_id = self._create_share_group(cr, uid, wizard_data, context=context)
- # First create any missing user, based on the email addresses provided
- new_ids, existing_ids = self._create_new_share_users(cr, uid, wizard_data, group_id, context=context)
- # Finally, setup the new action and shortcut for the users.
- if existing_ids:
- # existing users still need to join the new group
- self.pool.get('res.users').write(cr, UID_ROOT, existing_ids, {
- 'groups_id': [(4,group_id)],
- })
- # existing user don't need their home action replaced, only a new shortcut
- self._setup_action_and_shortcut(cr, uid, wizard_data, existing_ids, make_home=False, context=context)
- if new_ids:
- # new users need a new shortcut AND a home action
- self._setup_action_and_shortcut(cr, uid, wizard_data, new_ids, make_home=True, context=context)
- return group_id, new_ids, existing_ids
-
- def go_step_2(self, cr, uid, ids, context=None):
- wizard_data = self.browse(cr, uid, ids[0], context=context)
- self._check_preconditions(cr, uid, wizard_data, context=context)
-
- # Create shared group and users
- group_id, new_ids, existing_ids = self._create_share_users_group(cr, uid, wizard_data, context=context)
-
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
-
- model_obj = self.pool.get('ir.model')
- model_id = model_obj.search(cr, uid, [('model','=', wizard_data.action_id.res_model)])[0]
- model = model_obj.browse(cr, uid, model_id, context=context)
-
- # ACCESS RIGHTS
- # We have several classes of objects that should receive different access rights:
- # Let:
- # - [obj0] be the target model itself (and its parents via _inherits, if any)
- # - [obj1] be the target model and all other models recursively accessible from
- # obj0 via one2many relationships
- # - [obj2] be the target model and all other models recursively accessible from
- # obj0 via one2many and many2many relationships
- # - [obj3] be all models recursively accessible from obj1 via many2one relationships
- # (currently not used)
- obj0, obj1, obj2, obj3 = self._get_relationship_classes(cr, uid, model, context=context)
- mode = wizard_data.access_mode
-
- # Add access to [obj0] and [obj1] according to chosen mode
- self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj0, context=context)
- self._add_access_rights_for_share_group(cr, uid, group_id, mode, obj1, context=context)
-
- # Add read-only access (always) to [obj2]
- self._add_access_rights_for_share_group(cr, uid, group_id, 'readonly', obj2, context=context)
-
- # IR.RULES
- # A. On [obj0], [obj1], [obj2]: add all rules from all groups of
- # the user that is sharing
- # Warning: rules must be copied instead of linked if they contain a reference
- # to uid or if the rule is shared with other groups (and it must be replaced correctly)
- # B. On [obj0]: 1 rule with domain of shared action
- # C. For each model in [obj1]: 1 rule in the form:
- # many2one_rel.domain_of_obj0
- # where many2one_rel is the many2one used in the definition of the
- # one2many, and domain_of_obj0 is the sharing domain
- # For example if [obj0] is project.project with a domain of
- # ['id', 'in', [1,2]]
- # then we will have project.task in [obj1] and we need to create this
- # ir.rule on project.task:
- # ['project_id.id', 'in', [1,2]]
-
- # A.
- all_relations = obj0 + obj1 + obj2
- self._link_or_copy_current_user_rules(cr, current_user, group_id, all_relations, context=context)
- # B.
- main_domain = wizard_data.domain if wizard_data.domain != '[]' else str(DOMAIN_ALL)
- self._create_or_combine_sharing_rule(cr, current_user, wizard_data,
- group_id, model_id=model.id, domain=main_domain,
- restrict=True, context=context)
- # C.
- self._create_indirect_sharing_rules(cr, current_user, wizard_data, group_id, obj1, context=context)
-
- # refresh wizard_data
- wizard_data = self.browse(cr, uid, ids[0], context=context)
-
- # EMAILS AND NOTIFICATIONS
- # A. Not invite: as before
- # -> send emails to destination users
- # B. Invite (OpenSocial)
- # -> subscribe all users (existing and new) to the record
- # -> send a notification with a summary to the current record
- # -> send a notification to all users; users allowing to receive
- # emails in preferences will receive it
- # new users by default receive all notifications by email
-
- # A.
- if not wizard_data.invite:
- self.send_emails(cr, uid, wizard_data, context=context)
- # B.
- else:
- # Invite (OpenSocial): automatically subscribe users to the record
- res_id = 0
- for cond in safe_eval(main_domain):
- if cond[0] == 'id':
- res_id = cond[2]
- # Record id not found: issue
- if res_id <= 0:
- raise UserError(_('The share engine has not been able to fetch a record_id for your invitation.'))
- self.pool[model.model].message_subscribe(cr, uid, [res_id], new_ids + existing_ids, context=context)
- # self.send_invite_email(cr, uid, wizard_data, context=context)
- # self.send_invite_note(cr, uid, model.model, res_id, wizard_data, context=context)
-
- # CLOSE
- # A. Not invite: as before
- # B. Invite: skip summary screen, get back to the record
-
- # A.
- if not wizard_data.invite:
- dummy, step2_form_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'share', 'share_step2_form')
- return {
- 'name': _('Shared access created!'),
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'share.wizard',
- 'view_id': False,
- 'res_id': ids[0],
- 'views': [(step2_form_view_id, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
- 'type': 'ir.actions.act_window',
- 'target': 'new'
- }
- # B.
- else:
- return {
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': model.model,
- 'view_id': False,
- 'res_id': res_id,
- 'views': [(False, 'form'), (False, 'tree'), (False, 'calendar'), (False, 'graph')],
- 'type': 'ir.actions.act_window',
- }
-
-
- def send_invite_note(self, cr, uid, model_name, res_id, wizard_data, context=None):
- subject = _('Invitation')
- body = 'has been <b>shared</b> with'
- tmp_idx = 0
- for result_line in wizard_data.result_line_ids:
- body += ' @%s' % (result_line.user_id.login)
- if tmp_idx < len(wizard_data.result_line_ids)-2:
- body += ','
- elif tmp_idx == len(wizard_data.result_line_ids)-2:
- body += ' and'
- body += '.'
- return self.pool[model_name].message_post(cr, uid, [res_id], body=body, context=context)
-
- def send_invite_email(self, cr, uid, wizard_data, context=None):
- # TDE Note: not updated because will disappear
- message_obj = self.pool.get('mail.message')
- notification_obj = self.pool.get('mail.notification')
- user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
- if not user.email:
- raise UserError(_('The current user must have an email address configured in User Preferences to be able to send outgoing emails.'))
-
- # TODO: also send an HTML version of this mail
- for result_line in wizard_data.result_line_ids:
- email_to = result_line.user_id.email
- if not email_to:
- continue
- subject = _('Invitation to collaborate about %s') % (wizard_data.record_name)
- body = _("Hello,\n\n")
- body += _("I have shared %s (%s) with you!\n\n") % (wizard_data.record_name, wizard_data.name)
- if wizard_data.message:
- body += "%s\n\n" % (wizard_data.message)
- if result_line.newly_created:
- body += _("The documents are not attached, you can view them online directly on my Odoo server at:\n %s\n\n") % (result_line.share_url)
- body += _("These are your credentials to access this protected area:\n")
- body += "%s: %s" % (_("Username"), result_line.user_id.login) + "\n"
- body += "%s: %s" % (_("Password"), result_line.password) + "\n"
- body += "%s: %s" % (_("Database"), cr.dbname) + "\n"
- body += _("The documents have been automatically added to your subscriptions.\n\n")
- body += '%s\n\n' % ((user.signature or ''))
- body += "--\n"
- body += _("Odoo is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
- "It is open source and can be found on https://www.odoo.com.")
- msg_id = message_obj.schedule_with_attach(cr, uid, user.email, [email_to], subject, body, model='', context=context)
- notification_obj.create(cr, uid, {'user_id': result_line.user_id.id, 'message_id': msg_id}, context=context)
-
- def send_emails(self, cr, uid, wizard_data, context=None):
- _logger.info('Sending share notifications by email...')
- mail_mail = self.pool.get('mail.mail')
- user = self.pool.get('res.users').browse(cr, UID_ROOT, uid)
- if not user.email:
- raise UserError(_('The current user must have an email address configured in User Preferences to be able to send outgoing emails.'))
-
- # TODO: also send an HTML version of this mail
- mail_ids = []
- for result_line in wizard_data.result_line_ids:
- email_to = result_line.user_id.email
- if not email_to:
- continue
- subject = wizard_data.name
- body = _("Hello,\n\n")
- body += _("I've shared %s with you!\n\n") % wizard_data.name
- body += _("The documents are not attached, you can view them online directly on my Odoo server at:\n %s\n\n") % (result_line.share_url)
- if wizard_data.message:
- body += '%s\n\n' % (wizard_data.message)
- if result_line.newly_created:
- body += _("These are your credentials to access this protected area:\n")
- body += "%s: %s\n" % (_("Username"), result_line.user_id.login)
- body += "%s: %s\n" % (_("Password"), result_line.password)
- body += "%s: %s\n" % (_("Database"), cr.dbname)
- else:
- body += _("The documents have been automatically added to your current Odoo documents.\n")
- body += _("You may use your current login (%s) and password to view them.\n") % result_line.user_id.login
- body += "\n\n%s\n\n" % ( (user.signature or '') )
- body += "--\n"
- body += _("Odoo is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n"
- "It is open source and can be found on https://www.odoo.com.")
- mail_ids.append(mail_mail.create(cr, uid, {
- 'email_from': user.email,
- 'email_to': email_to,
- 'subject': subject,
- 'body_html': '<pre>%s</pre>' % body}, context=context))
- # force direct delivery, as users expect instant notification
- mail_mail.send(cr, uid, mail_ids, context=context)
- _logger.info('%d share notification(s) sent.', len(mail_ids))
-
- def onchange_embed_options(self, cr, uid, ids, opt_title, opt_search, context=None):
- wizard = self.browse(cr, uid, ids[0], context)
- options = dict(title=opt_title, search=opt_search)
- return {'value': {'embed_code': self._generate_embedded_code(wizard, options)}}
-
-
-class share_result_line(osv.osv_memory):
- _name = 'share.wizard.result.line'
- _rec_name = 'user_id'
-
-
- def _share_url(self, cr, uid, ids, _fieldname, _args, context=None):
- result = dict.fromkeys(ids, '')
- for this in self.browse(cr, uid, ids, context=context):
- data = dict(dbname=cr.dbname, login=this.login, password=this.password)
- if this.share_wizard_id and this.share_wizard_id.action_id:
- data['action'] = this.share_wizard_id.action_id.id
- this = this.with_context(share_url_template_hash_arguments=['action'])
- result[this.id] = this.share_wizard_id.share_url_template() % data
- return result
-
- _columns = {
- 'user_id': fields.many2one('res.users', required=True, readonly=True),
- 'login': fields.related('user_id', 'login', string='Login', type='char', size=64, required=True, readonly=True),
- 'password': fields.char('Password', size=64, readonly=True),
- 'share_url': fields.function(_share_url, string='Share URL', type='char', size=512),
- 'share_wizard_id': fields.many2one('share.wizard', 'Share Wizard', required=True, ondelete='cascade'),
- 'newly_created': fields.boolean('Newly created', readonly=True),
- }
- _defaults = {
- 'newly_created': True,
- }
diff --git a/addons/share/wizard/share_wizard_view.xml b/addons/share/wizard/share_wizard_view.xml
deleted file mode 100644
index 81caaa4a109..00000000000
--- a/addons/share/wizard/share_wizard_view.xml
+++ /dev/null
@@ -1,138 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
-
- <record id="share_step0_form" model="ir.ui.view">
- <field name="name">share.step0.form</field>
- <field name="model">share.wizard</field>
- <field name="arch" type="xml">
- <form string="Sharing: preparation">
- <separator
- string="Please select the action that opens the screen containing the data you want to share."/>
- <group>
- <field name="action_id"/>
- <label colspan="2"
- string="Optionally, you may specify an additional domain restriction that will be applied to the shared data."/>
- <field name="domain"/>
- </group>
- <footer>
- <button name="go_step_1" string="Next" type="object" class="oe_highlight"/>
- or
- <button string="Cancel" class="oe_link" special="cancel" />
- </footer>
- </form>
- </field>
- </record>
-
- <record id="share_step1_form" model="ir.ui.view">
- <field name="name">share.step1.form</field>
- <field name="model">share.wizard</field>
- <field name="arch" type="xml">
- <form string="Configuration">
- <group>
- <field name="user_type" invisible="1"/>
- <field name="invite" invisible="1"/>
- </group>
- <group>
- <group colspan="4" name="emails_group" attrs="{'invisible':['|', ('user_type', '!=', 'emails'), ('invite', '=', True)]}"
- string="Share with these People (one email per line)">
- <field nolabel="1" name="new_users" attrs="{'required':[('user_type','=','emails'), ('invite', '!=', True)]}"/>
- </group>
- <group colspan="4" name="email_lines" attrs="{'invisible':['|', ('invite', '!=', True), ('user_type', '!=', 'emails')]}"
- string="Share with these People (one email per line)">
- <field name="email_1"/>
- <field name="email_2"/>
- <field name="email_3"/>
- </group>
- <group colspan="4" attrs="{'invisible':[('user_type', '=', 'embedded')]}"
- string="Include an Optional Personal Message">
- <field name="message" colspan="2" nolabel="1"/>
- </group>
- <group attrs="{'invisible':[('invite', '=', True)]}" string="Sharing Options">
- <field name="name"/>
- <field name="access_mode"/>
- </group>
- </group>
- <footer>
- <button name="go_step_2" string="Share" type="object" class="oe_highlight"/>
- or
- <button string="Cancel" class="oe_link" special="cancel" />
- </footer>
- </form>
- </field>
- </record>
-
- <record id="share_step2_form" model="ir.ui.view">
- <field name="name">share.step2.form</field>
- <field name="model">share.wizard</field>
- <field name="arch" type="xml">
- <form string="Access granted!">
- <group attrs="{'invisible':[('user_type','=','embedded')]}"
- string="An email notification with instructions has been sent to the following people:">
- <field name="result_line_ids" nolabel="1" mode="tree">
- <tree string="Summary">
- <field name="login"/>
- </tree>
- <form string="Access info">
- <group>
- <field name="login"/>
- </group>
- </form>
- </field>
- </group>
- <group colspan="4" col="1" attrs="{'invisible':[('user_type','!=','embedded')]}">
- <group string="Use this link">
- <field name="embed_url"/>
- </group>
- <group string="Or insert the following code where you want to embed your documents">
- <field name="embed_code"/>
- </group>
- <group string="Embedded code options">
- <field name="embed_option_title" on_change="onchange_embed_options(embed_option_title, embed_option_search)"/>
- <field name="embed_option_search" on_change="onchange_embed_options(embed_option_title, embed_option_search)"
- attrs="{'invisible':[('access_mode','=','readonly')]}"/>
- </group>
- </group>
- <footer>
- <field name="user_type" invisible="1"/>
- <field name="access_mode" invisible="1"/>
- <button string="Close" class="oe_highlight" special="cancel" />
- </footer>
-
- </form>
- </field>
- </record>
-
- <!-- action for manual launch from menuitem. context may contain:
- - 'action_id' (id of action)
- - 'domain' (string expression for full domain to apply as sent to server,
- with dynamic data like 'uid' replaced by actual value (i.e. after eval)!)
- -->
- <record id="action_share_wizard" model="ir.actions.act_window">
- <field name="name">Share Wizard</field>
- <field name="type">ir.actions.act_window</field>
- <field name="res_model">share.wizard</field>
- <field name="view_type">form</field>
- <field name="view_mode">form</field>
- <field name="view_id" ref="share_step0_form"/>
- <field name="target">new</field>
- </record>
-
- <!-- action for direct launch from client widget with context providing:
- - 'action_id' (id of action)
- - 'domain' (string expression for full domain to apply as sent to server,
- with dynamic data like 'uid' replaced by actual value (i.e. after eval)!)
- -->
- <record id="action_share_wizard_step1" model="ir.actions.act_window">
- <field name="name">Share your documents</field>
- <field name="type">ir.actions.act_window</field>
- <field name="res_model">share.wizard</field>
- <field name="view_type">form</field>
- <field name="view_mode">form</field>
- <field name="view_id" ref="share_step1_form"/>
- <field name="target">new</field>
- <field name="context">False</field>
- </record>
-
- </data>
-</openerp>