diff options
| author | Mitali Patel <mpa@odoo.com> | 2017-12-15 14:38:02 +0530 |
|---|---|---|
| committer | David Beguin <dbe@odoo.com> | 2018-08-03 15:20:42 +0200 |
| commit | 84f528bcff12e39b5ed172a4789aab27c9f89a19 (patch) | |
| tree | 747f84c680e2a1a122ac190792d4a2d7b07612ef /addons/project | |
| parent | 766a1025b1ba25ef40897418fac662eaa42d3a0f (diff) | |
[IMP] Portal - Share link : Easily share the url of a document
Purpose
=======
- Quickly share the url to someone else (a client, a colleague,...)
- Ensure that the recipient can access at least
the portal view of the shared record.
- Typically used when a client cannot retrieve the mail to access his order.
The share link can be used in this case.
Specifications
==============
For any object inheriting form portal.mixin:
- Add a button SHARE (not visible in edit mode)
- When clicking on this button, a popup opens with :
- A warning message for tasks and projects only (see below)
- the link (like in gmail) that can be copied
- Recipients
- mail composer (with preselected template) ==> see below
- button [Send Link] [Copy Link] Discard
- After sharing document, put internal note like
"Document shared to xyz,...." with template message
- Anyone with the link, even anonymous user (not logged in) can have access
to the document with the access token provided in the url.
Impacted models:
- account.invoice (Community)
- project.project (Community)
- project.task (Community)
- purchase.order (Community)
- sale.order (Community)
- helpdesk.ticket (Enterprise)
Warning messages and access rules:
Allowed :
- SO canceled or draft will be accessible with the link
with access_token
- If the customer account is B2B (signup not enabled), the recipient
will anyway see the document as the user specifically wants the
recipient to see the document.
Restrictions :
- For Project and Task, if the privacy is not public, then, there is a
contradiction between the access_token mechanism
and the privacy of the document.
- A warning message will be displayed in the share wizard to inform the
user if the document cannot be visible by the recipients and to
ask him to set the privacy to 'Visible by following customer'.
The send button will, in that case, be hidden.
- To avoid to block the share for a new project, default privacy value
is now set to 'Visible by followong customer'
Technical implementation
========================
- Move the access_token mechanism (field + methods + mail controller)
to the portal.mixin to be able to use it in a generic way for each object
inheriting the portal.mixin
- Generalise a part of the _*model*_get_page_view_values method
into a single one in portal
- Generalize the _*model*_check_access into the portal controller of the
portal module
- Remove the init_column + default value for the access_token
> old records have an access_token,
> new one won't but it will be generated on demand via the get_access_token
Done for performance reasons
- Add share button into action menu separately. + kanban view context menu
(except for task and project where button not in action menu but 'simple'
button for task and project because other modules already provide action
to send documents by email, which is not the case for project and task.)
- Add a sign_token used to authentify the recipient in the portal view chatter,
if any. The message will be posted as if the user was logged in.
- Set the _get_share_url as private for security reason
- Add a redirect parameter to _get_share_url to get
If false : The direct portal view url
If True : The redirect url (mail/view/?)
- Cleaning up unnecessary code
- Bug fix :
- Before, if user was not logged and record had partner_id,
if partner id was null, post message was done as admin.
Now, the post message is done as public user.
- If the user had an uid but had no access_token, he could be able
to gain the access token of the record.
check_access_rights was missing in the get_access_action.
Task ID : 30985
Closes #25629
Diffstat (limited to 'addons/project')
| -rw-r--r-- | addons/project/controllers/portal.py | 58 | ||||
| -rw-r--r-- | addons/project/models/project.py | 74 | ||||
| -rw-r--r-- | addons/project/views/project_portal_templates.xml | 3 | ||||
| -rw-r--r-- | addons/project/views/project_views.xml | 9 |
4 files changed, 69 insertions, 75 deletions
diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 06dd82e00ac..5d7d65ddb14 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -5,6 +5,7 @@ from collections import OrderedDict from operator import itemgetter from odoo import http, _ +from odoo.exceptions import AccessError from odoo.http import request from odoo.addons.portal.controllers.portal import get_records_pager, CustomerPortal, pager as portal_pager from odoo.tools import groupby as groupbyelem @@ -24,6 +25,16 @@ class CustomerPortal(CustomerPortal): values['task_count'] = Task.search_count([('project_id', 'in', projects.ids)]) return values + # ------------------------------------------------------------ + # My Project + # ------------------------------------------------------------ + def _project_get_page_view_values(self, project, access_token, **kwargs): + values = { + 'page_name': 'project', + 'project': project, + } + return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs) + @http.route(['/my/projects', '/my/projects/page/<int:page>'], type='http', auth="user", website=True) def portal_my_projects(self, page=1, date_begin=None, date_end=None, sortby=None, **kw): values = self._prepare_portal_layout_values() @@ -70,13 +81,26 @@ class CustomerPortal(CustomerPortal): }) return request.render("project.portal_my_projects", values) - @http.route(['/my/project/<int:project_id>'], type='http', auth="user", website=True) - def portal_my_project(self, project_id=None, **kw): - project = request.env['project.project'].browse(project_id) - vals = {'project': project} - history = request.session.get('my_projects_history', []) - vals.update(get_records_pager(history, project)) - return request.render("project.portal_my_project", vals) + @http.route(['/my/project/<int:project_id>'], type='http', auth="public", website=True) + def portal_my_project(self, project_id=None, access_token=None, **kw): + try: + project_sudo = self._document_check_access('project.project', project_id, access_token) + except AccessError: + return request.redirect('/my') + + values = self._project_get_page_view_values(project_sudo, access_token, **kw) + return request.render("project.portal_my_project", values) + + # ------------------------------------------------------------ + # My Task + # ------------------------------------------------------------ + def _task_get_page_view_values(self, task, access_token, **kwargs): + values = { + 'page_name': 'task', + 'task': task, + 'user': request.env.user + } + return self._get_page_view_values(task, access_token, values, 'my_tasks_history', False, **kwargs) @http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True) def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby='project', **kw): @@ -177,16 +201,12 @@ class CustomerPortal(CustomerPortal): }) return request.render("project.portal_my_tasks", values) - @http.route(['/my/task/<int:task_id>'], type='http', auth="user", website=True) - def portal_my_task(self, task_id=None, **kw): - task = request.env['project.task'].browse(task_id) - task.check_access_rights('read') - task.check_access_rule('read') + @http.route(['/my/task/<int:task_id>'], type='http', auth="public", website=True) + def portal_my_task(self, task_id, access_token=None, **kw): + try: + task_sudo = self._document_check_access('project.task', task_id, access_token) + except AccessError: + return request.redirect('/my') - vals = { - 'task': task, - 'user': request.env.user - } - history = request.session.get('my_tasks_history', []) - vals.update(get_records_pager(history, task)) - return request.render("project.portal_my_task", vals) + values = self._task_get_page_view_values(task_sudo, access_token, **kw) + return request.render("project.portal_my_task", values) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 08a473a4c5e..373e6e0eee7 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -208,7 +208,7 @@ class Project(models.Model): ('portal', _('Visible by following customers')), ], string='Privacy', required=True, - default='employees', + default='portal', help="Holds visibility of the tasks or issues that belong to the current project:\n" "- On invitation only: Employees may only see the followed project, tasks or issues\n" "- Visible by all employees: Employees may see all project, tasks or issues\n" @@ -242,10 +242,16 @@ class Project(models.Model): ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') ] - def _compute_portal_url(self): - super(Project, self)._compute_portal_url() + def _compute_access_url(self): + super(Project, self)._compute_access_url() for project in self: - project.portal_url = '/my/project/%s' % project.id + project.access_url = '/my/project/%s' % project.id + + def _compute_access_warning(self): + super(Project, self)._compute_access_warning() + for project in self.filtered(lambda x: x.privacy_visibility != 'portal'): + project.access_warning = _( + "The project cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy to 'Visible by following customers' in order to make it accessible by the recipient(s).") @api.depends('percentage_satisfaction_task') def _compute_percentage_satisfaction_project(self): @@ -320,30 +326,6 @@ class Project(models.Model): return res @api.multi - def get_access_action(self, access_uid=None): - """ Instead of the classic form view, redirect to website for portal users - that can read the project. """ - self.ensure_one() - user, record = self.env.user, self - if access_uid: - user = self.env['res.users'].sudo().browse(access_uid) - record = self.sudo(user) - - if user.share: - try: - record.check_access_rule('read') - except AccessError: - pass - else: - return { - 'type': 'ir.actions.act_url', - 'url': '/my/project/%s' % self.id, - 'target': 'self', - 'res_id': self.id, - } - return super(Project, self).get_access_action(access_uid) - - @api.multi def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): """ Subscribe to all existing active tasks when subscribing to a project """ res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) @@ -568,10 +550,16 @@ class Task(models.Model): else: task.kanban_state_label = task.legend_done - def _compute_portal_url(self): - super(Task, self)._compute_portal_url() + def _compute_access_url(self): + super(Task, self)._compute_access_url() for task in self: - task.portal_url = '/my/task/%s' % task.id + task.access_url = '/my/task/%s' % task.id + + def _compute_access_warning(self): + super(Task, self)._compute_access_warning() + for task in self.filtered(lambda x: x.project_id.privacy_visibility != 'portal'): + task.access_warning = _( + "The task cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy of the project to 'Visible by following customers' in order to make it accessible by the recipient(s).") @api.depends('child_ids.planned_hours') def _compute_subtask_planned_hours(self): @@ -771,30 +759,6 @@ class Task(models.Model): return {'date_end': fields.Datetime.now()} return {'date_end': False} - @api.multi - def get_access_action(self, access_uid=None): - """ Instead of the classic form view, redirect to website for portal users - that can read the task. """ - self.ensure_one() - user, record = self.env.user, self - if access_uid: - user = self.env['res.users'].sudo().browse(access_uid) - record = self.sudo(user) - - if user.share: - try: - record.check_access_rule('read') - except AccessError: - pass - else: - return { - 'type': 'ir.actions.act_url', - 'url': '/my/task/%s' % self.id, - 'target': 'self', - 'res_id': self.id, - } - return super(Task, self).get_access_action(access_uid) - # --------------------------------------------------- # Subtasks # --------------------------------------------------- diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index 2750f0734e4..521a4a86140 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -234,6 +234,9 @@ <div class="col-lg-10 offset-lg-1 mt16"> <t t-call="portal.message_thread"> <t t-set="object" t-value="task"/> + <t t-set="token" t-value="task.access_token"/> + <t t-set="pid" t-value="pid"/> + <t t-set="hash" t-value="hash"/> </t> </div> </div> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index c900b426c6d..73f5cd4767e 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -92,6 +92,9 @@ <field name="model">project.project</field> <field name="arch" type="xml"> <form string="Project"> + <header> + <button name="%(portal.portal_share_action)d" string="Share" type="action" class="oe_highlight oe_read_only"/> + </header> <sheet string="Project"> <div class="oe_button_box" name="button_box" groups="base.group_user"> <button class="oe_stat_button" name="attachment_tree_view" type="object" icon="fa-files-o"> @@ -328,6 +331,9 @@ <div class="o_kanban_card_manage_pane dropdown-menu" groups="project.group_project_manager" role="menu"> <div class="o_kanban_card_manage_section o_kanban_manage_reports"> <div role="menuitem"> + <a name="%(portal.portal_share_action)d" type="action">Share</a> + </div> + <div role="menuitem"> <a type="edit">Edit</a> </div> <div role="menuitem" t-if="record.rating_status.raw_value != 'no'"> @@ -412,7 +418,7 @@ <header> <button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight" attrs="{'invisible' : [('user_id', '!=', False)]}"/> - + <button name="%(portal.portal_share_action)d" string="Share" type="action" class="oe_highlight oe_read_only"/> <field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/> </header> <sheet string="Task"> @@ -557,6 +563,7 @@ </a> <div class="dropdown-menu" role="menu"> <a t-if="widget.editable" role="menuitem" type="set_cover" class="dropdown-item">Set Cover Image</a> + <a name="%(portal.portal_share_action)d" role="menuitem" type="action" class="dropdown-item">Share</a> <a t-if="widget.editable" role="menuitem" type="edit" class="dropdown-item">Edit Task</a> <a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a> <div role="separator" class="dropdown-divider"></div> |
