From e50bc120020d606962c1d9b687e53b1bdb34c048 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Wed, 18 Aug 2021 11:01:46 +0200 Subject: [PATCH 01/33] [IMP] project: add Project Sharing feature Purpose: ======= This commit adds the project sharing feature. This feature consists to share the backend views about project.task to portal users. About the access rights: - The project manager can share a project with project sharing feature. To do this, he need to give access to portal users that we want their access in project sharing views. He can choose between 3 differents accesses: 1. Readonly: the portal with this access right will only have read access to the project sharing views of the project shared. 2. Comment: the portal with this access right will can use the chatter in task form view. 3. Edit: the portal with this access right will can edit some fields of `project.task` model. The difference between the classic backend views and the project sharing views is in the project sharing views, we list the fields that the user can read/write, so the portal users can see only the fields in our list. Moreover, the actions and the contextual menu (actions dropdown) are not available in the project sharing views and the stat buttons are visible but the click on these buttons are disabled. Implementation details: ====================== This commit is realized in many steps: - create the webclient and routes. - create own qweb bundle. - add project sharing views. - use the session to define the action and active_id - create public fields for project sharing: in this step, we lists all field names of the `project.task` model that we want to display for the portal user. Two lists are created, one for only readable field names and the other one for the writable field names. Moreover, two properties are defined in the `project.task` model to use the both lists. - use `access_token` and check model in project sharing route: in this step we check if the model is `project.project` and the `access_token` is the one set in the project since we have the id of the project in the params url. - allow only `GET` method to enter project_sharing routes: the `/my/project//project_sharing` route must only be called with a `GET` method because this route is only used to have the template to render the project sharing webclient. task-2379518 closes #73341 --- addons/project/__manifest__.py | 17 ++ addons/project/controllers/portal.py | 49 +++- addons/project/models/project.py | 63 ++++ .../static/src/project_sharing/main.js | 5 + .../src/project_sharing/project_sharing.js | 75 +++++ .../src/project_sharing/project_sharing.xml | 14 + .../views/project_sharing_templates.xml | 26 ++ .../project/views/project_sharing_views.xml | 273 ++++++++++++++++++ 8 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 addons/project/static/src/project_sharing/main.js create mode 100644 addons/project/static/src/project_sharing/project_sharing.js create mode 100644 addons/project/static/src/project_sharing/project_sharing.xml create mode 100644 addons/project/views/project_sharing_templates.xml create mode 100644 addons/project/views/project_sharing_views.xml diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 625ee834690..c4970f054f6 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -36,8 +36,10 @@ 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', 'views/mail_activity_views.xml', + 'views/project_sharing_views.xml', 'views/project_portal_templates.xml', 'views/project_task_templates.xml', + 'views/project_sharing_templates.xml', 'data/ir_cron_data.xml', 'data/mail_data.xml', 'data/mail_template_data.xml', @@ -87,6 +89,21 @@ 'web.assets_tests': [ 'project/static/tests/tours/**/*', ], + 'project.assets_qweb': [ + ('include', 'web.assets_qweb'), + 'project/static/src/project_sharing/**/*.xml', + ], + 'project.webclient': [ + ('include', 'web.assets_backend'), + ('remove', 'web/static/src/webclient/menu_service.js'), + ('remove', 'mail/static/src/components/dialog_manager/dialog_manager.js'), + ('remove', 'mail/static/src/services/dialog_service/dialog_service.js'), + ('remove', 'mail/static/src/components/chat_window_manager/chat_window_manager.js'), + ('remove', 'mail/static/src/services/chat_window_service/chat_window_service.js'), + 'project/static/src/project_sharing/**/*.js', + 'web/static/src/start.js', + 'web/static/src/legacy/legacy_setup.js', + ], }, 'license': 'LGPL-3', } diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index f5bb7cc91c9..cbdb6e87f01 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -6,11 +6,12 @@ from operator import itemgetter from markupsafe import Markup -from odoo import http, _ +from odoo import conf, http, _ from odoo.exceptions import AccessError, MissingError from odoo.http import request +from odoo.addons.web.controllers.main import HomeStaticTemplateHelpers from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager -from odoo.tools import groupby as groupbyelem +from odoo.tools import consteq, groupby as groupbyelem from odoo.osv.expression import OR @@ -89,6 +90,50 @@ class ProjectCustomerPortal(CustomerPortal): values = self._project_get_page_view_values(project_sudo, access_token, **kw) return request.render("project.portal_my_project", values) + @http.route("/project_sharing/", type='http', auth="user", methods=['GET']) + def get_project_sharing(self, project_id, model='project.project', access_token=None): + if not model or model != 'project.project' or not access_token: + return request.not_found() + project = request.env[model].sudo().browse(project_id) + project_validation = project and project.exists() and consteq(project.access_token, access_token) and project.privacy_visibility == 'portal' + if not project_validation: + return request.not_found() + return request.render('project.project_sharing', {'project_id': project.id}) + + @http.route("/embed/project/", type="http", auth="user", methods=['GET']) + def render_project_backend_view(self, project_id): + project = request.env['project.project'].sudo().browse(project_id) + + if not project.exists(): + return request.not_found() + + session_info = request.env['ir.http'].session_info() + user_context = request.session.get_context() if request.session.uid else {} + mods = conf.server_wide_modules or [] + qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="project.assets_qweb") + lang = user_context.get("lang") + translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang) + cache_hashes = { + "qweb": qweb_checksum, + "translations": translation_hash, + } + + project_company = project.company_id + session_info.update(cache_hashes=cache_hashes, action_name='project.project_sharing_project_task_action', project_id=project.id, user_companies={ + 'current_company': project_company.id, + 'allowed_companies': { + project_company.id: { + 'id': project_company.id, + 'name': project_company.name, + }, + }, + }) + + return request.render( + 'project.project_sharing_embed', + {'session_info': session_info}, + ) + # ------------------------------------------------------------ # My Task # ------------------------------------------------------------ diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 5dedfe41179..0eb864dd6fe 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -14,6 +14,45 @@ from odoo.osv.expression import OR from .project_task_recurrence import DAYS, WEEKS from .project_update import STATUS_COLOR + +PROJECT_TASK_READABLE_FIELDS = [ + 'id', + 'active', + 'description', + 'priority', + 'kanban_state_label', + 'project_id', + 'display_project_id', + 'color', + 'partner_is_company', + 'commercial_partner_id', + 'allow_subtasks', + 'child_text', + 'is_closed', + 'email_from', + 'create_date', + 'write_date', + 'company_id', + 'displayed_image_id', + 'display_name', + 'priority', +] + +PROJECT_TASK_WRITABLE_FIELDS = [ + 'name', + 'partner_id', + 'partner_email', + 'user_id', + 'date_deadline', + 'tag_ids', + 'sequence', + 'stage_id', + 'kanban_state', + 'child_ids', + 'parent_id', +] + + class ProjectTaskType(models.Model): _name = 'project.task.type' _description = 'Task Stage' @@ -899,6 +938,14 @@ class Task(models.Model): repeat_show_week = fields.Boolean(compute='_compute_repeat_visibility') repeat_show_month = fields.Boolean(compute='_compute_repeat_visibility') + @property + def SELF_READABLE_FIELDS(self): + return PROJECT_TASK_READABLE_FIELDS | self.SELF_WRITABLE_FIELDS + + @property + def SELF_WRITABLE_FIELDS(self): + return PROJECT_TASK_WRITABLE_FIELDS + @api.constrains('depend_on_ids') def _check_no_cyclic_dependencies(self): if not self._check_m2m_recursion('depend_on_ids'): @@ -1185,6 +1232,22 @@ class Task(models.Model): # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ + @api.model + def fields_get(self, allfields=None, attributes=None): + fields = super().fields_get(allfields=allfields, attributes=attributes) + if not self.env.user.has_group('base.group_portal'): + return fields + readable_fields = self.SELF_READABLE_FIELDS + public_fields = {field_name: description for field_name, description in fields.items() if field_name in readable_fields} + + writable_fields = self.SELF_WRITABLE_FIELDS + for field_name, description in public_fields.items(): + if field_name not in writable_fields and not description.get('readonly', False): + # If the field is not in Writable fields and it is not readonly then we force the readonly to True + description['readonly'] = True + + return public_fields + @api.model def default_get(self, default_fields): vals = super(Task, self).default_get(default_fields) diff --git a/addons/project/static/src/project_sharing/main.js b/addons/project/static/src/project_sharing/main.js new file mode 100644 index 00000000000..61458f6992c --- /dev/null +++ b/addons/project/static/src/project_sharing/main.js @@ -0,0 +1,5 @@ +/** @odoo-module **/ +import { startWebClient } from '@web/start'; +import { ProjectSharingWebClient } from './project_sharing'; + +startWebClient(ProjectSharingWebClient); diff --git a/addons/project/static/src/project_sharing/project_sharing.js b/addons/project/static/src/project_sharing/project_sharing.js new file mode 100644 index 00000000000..a8a0503130d --- /dev/null +++ b/addons/project/static/src/project_sharing/project_sharing.js @@ -0,0 +1,75 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; +import { useBus, useEffect, useService } from '@web/core/utils/hooks'; +import { ActionContainer } from '@web/webclient/actions/action_container'; +import { MainComponentsContainer } from "@web/core/main_components_container"; +import { useOwnDebugContext } from "@web/core/debug/debug_context"; +import { DebugMenu } from "@web/core/debug/debug_menu"; +import { ErrorHandler, NotUpdatable } from "@web/core/utils/components"; +import { session } from '@web/session'; + +const { Component } = owl; + +export class ProjectSharingWebClient extends Component { + setup() { + window.parent.document.body.style.margin = "0"; // remove the margin in the parent body + this.actionService = useService('action'); + this.user = useService("user"); + useService("legacy_service_provider"); + useOwnDebugContext({ categories: ["default"] }); + if (this.env.debug) { + registry.category("systray").add( + "web.debug_mode_menu", + { + Component: DebugMenu, + }, + { sequence: 100 } + ); + } + useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", (mode) => { + if (mode !== "new") { + this.el.classList.toggle("o_fullscreen", mode === "fullscreen"); + } + }); + useEffect( + () => { + this._showView(); + }, + () => [] + ); + } + + mounted() { + this.env.bus.trigger("WEB_CLIENT_READY"); + } + + handleComponentError(error, C) { + // remove the faulty component + this.Components.splice(this.Components.indexOf(C), 1); + /** + * we rethrow the error to notify the user something bad happened. + * We do it after a tick to make sure owl can properly finish its + * rendering + */ + Promise.resolve().then(() => { + throw error; + }); + } + + async _showView() { + const { action_name, project_id } = session; + await this.actionService.doAction( + action_name, + { + clearBreadcrumbs: true, + additionalContext: { + active_id: project_id, + } + } + ); + } +} + +ProjectSharingWebClient.components = { ActionContainer, ErrorHandler, NotUpdatable, MainComponentsContainer }; +ProjectSharingWebClient.template = 'project.ProjectSharingWebClient'; diff --git a/addons/project/static/src/project_sharing/project_sharing.xml b/addons/project/static/src/project_sharing/project_sharing.xml new file mode 100644 index 00000000000..9f021f9530d --- /dev/null +++ b/addons/project/static/src/project_sharing/project_sharing.xml @@ -0,0 +1,14 @@ + + + + + + + + +
+ + + + + diff --git a/addons/project/views/project_sharing_templates.xml b/addons/project/views/project_sharing_templates.xml new file mode 100644 index 00000000000..2717036f7bc --- /dev/null +++ b/addons/project/views/project_sharing_templates.xml @@ -0,0 +1,26 @@ + + + +