From 3305e2d1d4eafc3a8fc94d33ba86b32a41d9f6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 5 Jul 2022 08:04:54 +0000 Subject: [PATCH] [REM][MOV] knowledge: move to enterprise Due to business decisions knowledge is moved to enterprise repository. As some other moves are planned, notably coupon or the complete responsible UI and support that are coming to community Odoo feels like knowledge belongs to enterprise applications. Planned move from enterprise to community are * mass mailing themes; * mobile UI; * complete coupon and loyalty cards support; * spreadsheet library; For any questions please refer to decision makers. Task-2900765 X-original-commit: 9ba76621dc66fcf672ce514a47cc933bd9462f38 Part-of: odoo/odoo#95556 --- .tx/config | 10 - addons/knowledge/__init__.py | 13 - addons/knowledge/__manifest__.py | 95 - addons/knowledge/controllers/__init__.py | 4 - addons/knowledge/controllers/main.py | 349 --- addons/knowledge/data/article_templates.xml | 350 --- addons/knowledge/data/ir_actions_data.xml | 11 - addons/knowledge/data/knowledge_data.xml | 513 ----- addons/knowledge/data/mail_templates.xml | 27 - addons/knowledge/i18n/knowledge.pot | 1885 ----------------- addons/knowledge/models/__init__.py | 8 - addons/knowledge/models/knowledge_article.py | 1686 --------------- .../models/knowledge_article_favorite.py | 52 - .../models/knowledge_article_member.py | 110 - addons/knowledge/models/res_partner.py | 16 - addons/knowledge/models/res_users.py | 49 - addons/knowledge/security/ir.model.access.csv | 16 - addons/knowledge/security/ir_rule.xml | 79 - addons/knowledge/static/description/icon.png | Bin 27374 -> 0 bytes .../components/emoji_picker/emoji_list.xml | 13 - .../components/emoji_picker/emoji_picker.js | 60 - .../knowledge_documents_selector.js | 70 - .../permission_panel/permission_panel.js | 327 --- .../permission_panel/permission_panel.scss | 39 - .../permission_panel/permission_panel.xml | 84 - .../knowledge/static/src/img/fleet_icon.png | Bin 5702 -> 0 bytes addons/knowledge/static/src/js/field_html.js | 85 - .../static/src/js/form_controller.js | 192 -- .../knowledge/static/src/js/form_renderer.js | 77 - .../static/src/js/knowledge_behaviors.js | 166 -- .../src/js/knowledge_clipboard_whitelist.js | 8 - .../static/src/js/knowledge_controller.js | 446 ---- .../src/js/knowledge_field_html_injector.js | 317 --- .../static/src/js/knowledge_frontend.js | 89 - .../static/src/js/knowledge_macros.js | 321 --- .../static/src/js/knowledge_plugin.js | 26 - .../static/src/js/knowledge_renderers.js | 444 ---- .../static/src/js/knowledge_service.js | 151 -- .../static/src/js/knowledge_toolbars.js | 339 --- .../static/src/js/knowledge_toolbars_edit.js | 36 - .../static/src/js/knowledge_views.js | 34 - .../static/src/js/tools/tree_panel_mixin.js | 130 -- .../src/js/widgets/knowledge_dialogs.js | 182 -- .../src/js/widgets/knowledge_emoji_picker.js | 59 - .../js/widgets/knowledge_permission_panel.js | 40 - addons/knowledge/static/src/js/wysiwyg.js | 182 -- .../src/js/wysiwyg/knowledge_article_link.js | 127 -- .../static/src/models/activity/chatter.js | 12 - .../static/src/scss/knowledge_blocks.scss | 42 - .../static/src/scss/knowledge_editor.scss | 116 - .../static/src/scss/knowledge_frontend.scss | 110 - .../static/src/scss/knowledge_views.scss | 283 --- .../webclient/commands/command_category.js | 9 - .../webclient/commands/knowledge_providers.js | 124 -- .../static/src/xml/chatter_topbar.xml | 10 - .../src/xml/knowledge_command_palette.xml | 47 - .../static/src/xml/knowledge_editor.xml | 35 - .../static/src/xml/knowledge_templates.xml | 23 - .../static/src/xml/knowledge_toolbars.xml | 33 - .../knowledge/static/tests/test_services.js | 15 - .../tests/tours/knowledge_main_flow_tour.js | 185 -- .../tests/tours/knowledge_pick_emoji_tour.js | 48 - addons/knowledge/tests/__init__.py | 12 - addons/knowledge/tests/common.py | 422 ---- .../tests/test_knowledge_article_business.py | 896 -------- .../test_knowledge_article_constraints.py | 413 ---- .../tests/test_knowledge_article_internals.py | 124 -- .../test_knowledge_article_permissions.py | 494 ----- .../tests/test_knowledge_article_sequence.py | 177 -- .../knowledge/tests/test_knowledge_form_ui.py | 74 - .../tests/test_knowledge_performance.py | 95 - .../tests/test_knowledge_security.py | 204 -- addons/knowledge/tests/test_res_users.py | 53 - .../knowledge_article_favorite_views.xml | 61 - .../views/knowledge_article_member_views.xml | 64 - .../views/knowledge_article_views.xml | 349 --- addons/knowledge/views/knowledge_menus.xml | 43 - .../knowledge/views/knowledge_templates.xml | 54 - .../views/knowledge_templates_common.xml | 71 - .../views/knowledge_templates_frontend.xml | 102 - addons/knowledge/wizard/__init__.py | 4 - addons/knowledge/wizard/knowledge_invite.py | 26 - .../wizard/knowledge_invite_views.xml | 41 - addons/website_knowledge/__init__.py | 5 - addons/website_knowledge/__manifest__.py | 25 - .../website_knowledge/controllers/__init__.py | 4 - addons/website_knowledge/controllers/main.py | 21 - .../i18n/website_knowledge.pot | 76 - addons/website_knowledge/models/__init__.py | 4 - .../models/knowledge_article.py | 17 - .../security/ir.model.access.csv | 2 - addons/website_knowledge/security/ir_rule.xml | 52 - .../website_preview/website_preview.js | 12 - .../static/src/js/knowledge_controller.js | 13 - addons/website_knowledge/tests/__init__.py | 4 - .../tests/test_knowledge_security.py | 64 - .../views/knowledge_templates_frontend.xml | 36 - .../views/knowledge_views.xml | 37 - 98 files changed, 14560 deletions(-) delete mode 100644 addons/knowledge/__init__.py delete mode 100644 addons/knowledge/__manifest__.py delete mode 100644 addons/knowledge/controllers/__init__.py delete mode 100644 addons/knowledge/controllers/main.py delete mode 100644 addons/knowledge/data/article_templates.xml delete mode 100644 addons/knowledge/data/ir_actions_data.xml delete mode 100644 addons/knowledge/data/knowledge_data.xml delete mode 100644 addons/knowledge/data/mail_templates.xml delete mode 100644 addons/knowledge/i18n/knowledge.pot delete mode 100644 addons/knowledge/models/__init__.py delete mode 100644 addons/knowledge/models/knowledge_article.py delete mode 100644 addons/knowledge/models/knowledge_article_favorite.py delete mode 100644 addons/knowledge/models/knowledge_article_member.py delete mode 100644 addons/knowledge/models/res_partner.py delete mode 100644 addons/knowledge/models/res_users.py delete mode 100644 addons/knowledge/security/ir.model.access.csv delete mode 100644 addons/knowledge/security/ir_rule.xml delete mode 100644 addons/knowledge/static/description/icon.png delete mode 100644 addons/knowledge/static/src/components/emoji_picker/emoji_list.xml delete mode 100644 addons/knowledge/static/src/components/emoji_picker/emoji_picker.js delete mode 100644 addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js delete mode 100644 addons/knowledge/static/src/components/permission_panel/permission_panel.js delete mode 100644 addons/knowledge/static/src/components/permission_panel/permission_panel.scss delete mode 100644 addons/knowledge/static/src/components/permission_panel/permission_panel.xml delete mode 100644 addons/knowledge/static/src/img/fleet_icon.png delete mode 100644 addons/knowledge/static/src/js/field_html.js delete mode 100644 addons/knowledge/static/src/js/form_controller.js delete mode 100644 addons/knowledge/static/src/js/form_renderer.js delete mode 100644 addons/knowledge/static/src/js/knowledge_behaviors.js delete mode 100644 addons/knowledge/static/src/js/knowledge_clipboard_whitelist.js delete mode 100644 addons/knowledge/static/src/js/knowledge_controller.js delete mode 100644 addons/knowledge/static/src/js/knowledge_field_html_injector.js delete mode 100644 addons/knowledge/static/src/js/knowledge_frontend.js delete mode 100644 addons/knowledge/static/src/js/knowledge_macros.js delete mode 100644 addons/knowledge/static/src/js/knowledge_plugin.js delete mode 100644 addons/knowledge/static/src/js/knowledge_renderers.js delete mode 100644 addons/knowledge/static/src/js/knowledge_service.js delete mode 100644 addons/knowledge/static/src/js/knowledge_toolbars.js delete mode 100644 addons/knowledge/static/src/js/knowledge_toolbars_edit.js delete mode 100644 addons/knowledge/static/src/js/knowledge_views.js delete mode 100644 addons/knowledge/static/src/js/tools/tree_panel_mixin.js delete mode 100644 addons/knowledge/static/src/js/widgets/knowledge_dialogs.js delete mode 100644 addons/knowledge/static/src/js/widgets/knowledge_emoji_picker.js delete mode 100644 addons/knowledge/static/src/js/widgets/knowledge_permission_panel.js delete mode 100644 addons/knowledge/static/src/js/wysiwyg.js delete mode 100644 addons/knowledge/static/src/js/wysiwyg/knowledge_article_link.js delete mode 100644 addons/knowledge/static/src/models/activity/chatter.js delete mode 100644 addons/knowledge/static/src/scss/knowledge_blocks.scss delete mode 100644 addons/knowledge/static/src/scss/knowledge_editor.scss delete mode 100644 addons/knowledge/static/src/scss/knowledge_frontend.scss delete mode 100644 addons/knowledge/static/src/scss/knowledge_views.scss delete mode 100644 addons/knowledge/static/src/webclient/commands/command_category.js delete mode 100644 addons/knowledge/static/src/webclient/commands/knowledge_providers.js delete mode 100644 addons/knowledge/static/src/xml/chatter_topbar.xml delete mode 100644 addons/knowledge/static/src/xml/knowledge_command_palette.xml delete mode 100644 addons/knowledge/static/src/xml/knowledge_editor.xml delete mode 100644 addons/knowledge/static/src/xml/knowledge_templates.xml delete mode 100644 addons/knowledge/static/src/xml/knowledge_toolbars.xml delete mode 100644 addons/knowledge/static/tests/test_services.js delete mode 100644 addons/knowledge/static/tests/tours/knowledge_main_flow_tour.js delete mode 100644 addons/knowledge/static/tests/tours/knowledge_pick_emoji_tour.js delete mode 100644 addons/knowledge/tests/__init__.py delete mode 100644 addons/knowledge/tests/common.py delete mode 100644 addons/knowledge/tests/test_knowledge_article_business.py delete mode 100644 addons/knowledge/tests/test_knowledge_article_constraints.py delete mode 100644 addons/knowledge/tests/test_knowledge_article_internals.py delete mode 100644 addons/knowledge/tests/test_knowledge_article_permissions.py delete mode 100644 addons/knowledge/tests/test_knowledge_article_sequence.py delete mode 100644 addons/knowledge/tests/test_knowledge_form_ui.py delete mode 100644 addons/knowledge/tests/test_knowledge_performance.py delete mode 100644 addons/knowledge/tests/test_knowledge_security.py delete mode 100644 addons/knowledge/tests/test_res_users.py delete mode 100644 addons/knowledge/views/knowledge_article_favorite_views.xml delete mode 100644 addons/knowledge/views/knowledge_article_member_views.xml delete mode 100644 addons/knowledge/views/knowledge_article_views.xml delete mode 100644 addons/knowledge/views/knowledge_menus.xml delete mode 100644 addons/knowledge/views/knowledge_templates.xml delete mode 100644 addons/knowledge/views/knowledge_templates_common.xml delete mode 100644 addons/knowledge/views/knowledge_templates_frontend.xml delete mode 100644 addons/knowledge/wizard/__init__.py delete mode 100644 addons/knowledge/wizard/knowledge_invite.py delete mode 100644 addons/knowledge/wizard/knowledge_invite_views.xml delete mode 100644 addons/website_knowledge/__init__.py delete mode 100644 addons/website_knowledge/__manifest__.py delete mode 100644 addons/website_knowledge/controllers/__init__.py delete mode 100644 addons/website_knowledge/controllers/main.py delete mode 100644 addons/website_knowledge/i18n/website_knowledge.pot delete mode 100644 addons/website_knowledge/models/__init__.py delete mode 100644 addons/website_knowledge/models/knowledge_article.py delete mode 100644 addons/website_knowledge/security/ir.model.access.csv delete mode 100644 addons/website_knowledge/security/ir_rule.xml delete mode 100644 addons/website_knowledge/static/src/client_actions/website_preview/website_preview.js delete mode 100644 addons/website_knowledge/static/src/js/knowledge_controller.js delete mode 100644 addons/website_knowledge/tests/__init__.py delete mode 100644 addons/website_knowledge/tests/test_knowledge_security.py delete mode 100644 addons/website_knowledge/views/knowledge_templates_frontend.xml delete mode 100644 addons/website_knowledge/views/knowledge_views.xml diff --git a/.tx/config b/.tx/config index 7cb2b4a92e2..dfb3702b832 100644 --- a/.tx/config +++ b/.tx/config @@ -452,11 +452,6 @@ file_filter = addons/im_livechat_mail_bot/i18n/.po source_file = addons/im_livechat_mail_bot/i18n/im_livechat_mail_bot.pot source_lang = en -[odoo-s15-3.knowledge] -file_filter = addons/knowledge/i18n/.po -source_file = addons/knowledge/i18n/knowledge.pot -source_lang = en - [odoo-s15-3.l10n_multilang] file_filter = addons/l10n_multilang/i18n/.po source_file = addons/l10n_multilang/i18n/l10n_multilang.pot @@ -1182,11 +1177,6 @@ file_filter = addons/website_jitsi/i18n/.po source_file = addons/website_jitsi/i18n/website_jitsi.pot source_lang = en -[odoo-s15-3.website_knowledge] -file_filter = addons/website_knowledge/i18n/.po -source_file = addons/website_knowledge/i18n/website_knowledge.pot -source_lang = en - [odoo-s15-3.website_links] file_filter = addons/website_links/i18n/.po source_file = addons/website_links/i18n/website_links.pot diff --git a/addons/knowledge/__init__.py b/addons/knowledge/__init__.py deleted file mode 100644 index d1d1d566d49..00000000000 --- a/addons/knowledge/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import controllers -from . import models -from . import wizard - -from odoo import api, SUPERUSER_ID - - -def _init_private_article_per_user(cr, registry): - env = api.Environment(cr, SUPERUSER_ID, {}) - env['res.users'].search([('partner_share', '=', False)])._generate_tutorial_articles() diff --git a/addons/knowledge/__manifest__.py b/addons/knowledge/__manifest__.py deleted file mode 100644 index 3ddaee40f4c..00000000000 --- a/addons/knowledge/__manifest__.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -{ - 'name': 'Knowledge', - 'summary': 'Centralize, manage, share and grow your knowledge library', - 'description': 'Centralize, manage, share and grow your knowledge library', - 'category': 'Productivity/Knowledge', - 'version': '0.1', - 'depends': [ - 'web', - 'web_editor', - 'mail', - 'portal' - ], - 'data': [ - 'data/article_templates.xml', - 'data/knowledge_data.xml', - 'data/ir_actions_data.xml', - 'data/mail_templates.xml', - 'wizard/knowledge_invite_views.xml', - 'views/knowledge_article_views.xml', - 'views/knowledge_article_favorite_views.xml', - 'views/knowledge_article_member_views.xml', - 'views/knowledge_templates.xml', - 'views/knowledge_templates_common.xml', - 'views/knowledge_templates_frontend.xml', - 'views/knowledge_menus.xml', - 'security/ir.model.access.csv', - 'security/ir_rule.xml', - ], - 'installable': True, - 'application': True, - 'auto_install': False, - 'license': 'LGPL-3', - 'post_init_hook': '_init_private_article_per_user', - 'assets': { - 'web.assets_backend': [ - 'web/static/fonts/fonts.scss', - 'knowledge/static/src/components/*/*.scss', - 'knowledge/static/src/components/*/*.js', - 'knowledge/static/src/scss/knowledge_views.scss', - 'knowledge/static/src/scss/knowledge_editor.scss', - 'knowledge/static/src/scss/knowledge_blocks.scss', - 'knowledge/static/src/components/*/*.js', - 'knowledge/static/src/js/knowledge_controller.js', - 'knowledge/static/src/js/knowledge_renderers.js', - 'knowledge/static/src/js/knowledge_views.js', - 'knowledge/static/src/js/widgets/knowledge_dialogs.js', - 'knowledge/static/src/js/tools/tree_panel_mixin.js', - 'knowledge/static/src/js/widgets/knowledge_permission_panel.js', - 'knowledge/static/src/js/widgets/knowledge_emoji_picker.js', - 'knowledge/static/src/webclient/commands/*.js', - 'knowledge/static/src/models/*/*.js', - 'knowledge/static/src/js/form_controller.js', - 'knowledge/static/src/js/form_renderer.js', - 'knowledge/static/src/js/knowledge_macros.js', - 'knowledge/static/src/js/knowledge_behaviors.js', - 'knowledge/static/src/js/knowledge_toolbars.js', - 'knowledge/static/src/js/knowledge_field_html_injector.js', - 'knowledge/static/src/js/knowledge_plugin.js', - 'knowledge/static/src/js/field_html.js', - 'knowledge/static/src/js/knowledge_service.js', - ], - 'web.assets_frontend': [ - 'knowledge/static/src/scss/knowledge_frontend.scss', - 'knowledge/static/src/scss/knowledge_blocks.scss', - 'knowledge/static/src/js/knowledge_frontend.js', - 'knowledge/static/src/js/tools/tree_panel_mixin.js', - ], - 'web_editor.assets_wysiwyg': [ - 'knowledge/static/src/js/wysiwyg/knowledge_article_link.js', - 'knowledge/static/src/js/wysiwyg.js', - 'knowledge/static/src/js/knowledge_toolbars_edit.js', - 'knowledge/static/src/js/knowledge_clipboard_whitelist.js' - ], - 'web.assets_qweb': [ - 'knowledge/static/src/components/*/*.xml', - 'knowledge/static/src/xml/knowledge_editor.xml', - 'knowledge/static/src/xml/knowledge_templates.xml', - 'knowledge/static/src/xml/chatter_topbar.xml', - 'knowledge/static/src/xml/knowledge_command_palette.xml', - 'knowledge/static/src/xml/knowledge_toolbars.xml', - ], - 'web.assets_tests': [ - 'knowledge/static/tests/tours/*.js', - ], - 'web.qunit_suite_tests': [ - 'knowledge/static/tests/test_services.js', - ], - 'web.qunit_mobile_suite_tests': [ - 'knowledge/static/tests/test_services.js', - ], - }, -} diff --git a/addons/knowledge/controllers/__init__.py b/addons/knowledge/controllers/__init__.py deleted file mode 100644 index 5d4b25db9c0..00000000000 --- a/addons/knowledge/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import main diff --git a/addons/knowledge/controllers/main.py b/addons/knowledge/controllers/main.py deleted file mode 100644 index 25bb4b02f78..00000000000 --- a/addons/knowledge/controllers/main.py +++ /dev/null @@ -1,349 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import werkzeug - -from odoo import http, tools, _ -from odoo.exceptions import AccessError, ValidationError -from odoo.http import request - - -class KnowledgeController(http.Controller): - - # ------------------------ - # Article Access Routes - # ------------------------ - - @http.route('/knowledge/home', type='http', auth='user') - def access_knowledge_home(self): - """ This route will redirect internal users to the backend view of the - article and the share users to the frontend view instead. """ - article = request.env["knowledge.article"]._get_first_accessible_article() - if request.env.user.has_group('base.group_user') and not article: - return self._redirect_to_backend_view(article) - if not article: - return self._redirect_to_portal_view(False, hide_side_bar=True) - return request.redirect("/knowledge/article/%s" % article.id) - - @http.route('/knowledge/article/', type='http', auth='user') - def redirect_to_article(self, article_id): - """ This route will redirect internal users to the backend view of the - article and the share users to the frontend view instead.""" - article = self._fetch_article(article_id) - if request.env.user.has_group('base.group_user'): - if not article: - return werkzeug.exceptions.Forbidden() - return self._redirect_to_backend_view(article) - return self._redirect_to_portal_view(article) - - @http.route('/knowledge/article/invite//', type='http', auth='public') - def article_invite(self, member_id, invitation_hash): - """ This route will check if the given parameter allows the client to access the article via the invite token. - Then, if the partner has not registered yet, we will redirect the client to the signup page to finally redirect - them to the article. - If the partner already has registrered, we redirect them directly to the article. - """ - member = request.env['knowledge.article.member'].sudo().browse(member_id).exists() - correct_token = member._get_invitation_hash() if member else False - if not correct_token or not tools.consteq(correct_token, invitation_hash): - raise werkzeug.exceptions.NotFound() - - partner = member.partner_id - article = member.article_id - - if not partner.user_ids: - # Force the signup even if not enabled (as we explicitly invited the member). - # They should still be able to create a user. - signup_allowed = request.env['res.users']._get_signup_invitation_scope() == 'b2c' - if not signup_allowed: - partner.signup_prepare() - partner.signup_get_auth_param() - signup_url = partner._get_signup_url_for_action(url='/knowledge/article/%s' % article.id)[partner.id] - return request.redirect(signup_url) - - return request.redirect('/web/login?redirect=/knowledge/article/%s' % article.id) - - def _fetch_article(self, article_id): - """ Check the access of the given article for the current user. """ - article = request.env['knowledge.article'].browse(int(article_id)).exists() - if not article: - return request.env['knowledge.article'] - try: - article.check_access_rights('read') - article.check_access_rule('read') - except AccessError: - return request.env['knowledge.article'] - return article - - def _redirect_to_backend_view(self, article): - return request.redirect("/web#id=%s&model=knowledge.article&action=%s&menu_id=%s" % ( - article.id if article else '', - request.env.ref("knowledge.knowledge_article_action_form").id, - request.env.ref('knowledge.knowledge_menu_root').id - )) - - def _redirect_to_portal_view(self, article, hide_side_bar=False): - return request.render('knowledge.knowledge_article_view_frontend', { - 'article': article, - 'portal_readonly_mode': True, # used to bypass access check (to speed up loading) - 'show_sidebar': not hide_side_bar and bool(self._get_root_articles(limit=1)) - }) - - # ------------------------ - # Articles tree generation - # ------------------------ - - def _get_root_articles(self, limit=None): - """ Meant to be overriden by website_knowledge to search in sudo with adapted domain.""" - return request.env["knowledge.article"].search([("parent_id", "=", False)], limit=limit, order='sequence, id') - - def _prepare_articles_tree_html(self, template, active_article, unfolded_articles=False): - """ Prepares all the info needed to render the article tree view side panel - and returns the rendered given template with those values. - - :param active_article: used to highlight the given - article_id in the template; - :param list unfolded_articles: List of IDs used to display the children - of the given article ids. Unfolded articles are saved into local storage. - When reloading/opening the article page, previously unfolded articles - nodes must be opened; - """ - unfolded_articles = set() if not unfolded_articles else set(unfolded_articles) - if active_article: - # root articles = starting point of the tree view : unfold only if root_article in (accessible) parents - ancestors = active_article._get_readable_ancetors() - if active_article.root_article_id in ancestors: - unfolded_articles |= set(ancestors.ids) - - root_articles = self._get_root_articles() - - values = { - "active_article": active_article, - "workspace_articles": root_articles.filtered(lambda article: article.category == 'workspace'), - "shared_articles": root_articles.filtered(lambda article: article.category == 'shared'), - "private_articles": root_articles.filtered( - lambda article: article.category == "private" and article.user_has_write_access), - "unfolded_articles": unfolded_articles, - 'portal_readonly_mode': not request.env.user.has_group('base.group_user'), - } - favorites = request.env['knowledge.article.favorite'] - if not request.env.user._is_public(): - favorites = request.env['knowledge.article.favorite'].search([ - ("user_id", "=", request.env.user.id), ('article_id.active', '=', True) - ]) - values["favorites"] = favorites - - return request.env['ir.qweb']._render(template, values) - - @http.route('/knowledge/tree_panel', type='json', auth='user') - def get_tree_panel_all(self, active_article_id=False, unfolded_articles=False): - return self._prepare_articles_tree_html( - 'knowledge.knowledge_article_tree', - self._fetch_article(active_article_id), - unfolded_articles=unfolded_articles - ) - - @http.route('/knowledge/tree_panel/portal', type='json', auth='public') - def get_tree_panel_portal(self, active_article_id=False, unfolded_articles=False): - """ Frontend access for left panel. """ - return self._prepare_articles_tree_html( - 'knowledge.knowledge_article_tree_frontend', - self._fetch_article(active_article_id), - unfolded_articles=unfolded_articles - ) - - @http.route('/knowledge/tree_panel/children', type='json', auth='user') - def get_tree_panel_children(self, parent_id): - parent = self._fetch_article(parent_id) - if not parent: - return werkzeug.exceptions.NotFound() - return request.env['ir.qweb']._render('knowledge.articles_template', { - 'articles': parent.child_ids, - 'portal_readonly_mode': not request.env.user.has_group('base.group_user'), # used to bypass access check (to speed up loading) - }) - - @http.route('/knowledge/tree_panel/favorites', type='json', auth='user') - def get_tree_panel_favorites(self, active_article_id=False): - favorite_articles = request.env['knowledge.article.favorite'].search([ - ("user_id", "=", request.env.user.id), ('article_id.active', '=', True) - ]) - return request.env['ir.qweb']._render('knowledge.knowledge_article_tree_favorites', { - 'favorites': favorite_articles, - "active_article": request.env['knowledge.article'].browse(active_article_id), - }) - - # ------------------------ - # Article permission panel - # ------------------------ - - @http.route('/knowledge/get_article_permission_panel_data', type='json', auth='user') - def get_article_permission_panel_data(self, article_id): - """ - Returns a dictionnary containing all values required to render the permission panel. - :param article_id: (int) article id - """ - article = self._fetch_article(article_id) - if not article: - return werkzeug.exceptions.Forbidden() - is_sync = not article.is_desynchronized - # Get member permission info - members_values = [] - members_permission = article._get_article_member_permissions(additional_fields={ - 'res.partner': [ - ('name', 'partner_name'), - ('email', 'partner_email'), - ('partner_share', 'partner_share'), - ], - 'knowledge.article': [ - ('icon', 'based_on_icon'), - ('name', 'based_on_name'), - ], - })[article.id] - for partner_id, member in members_permission.items(): - # empty member added by '_get_article_member_permissions', don't show it in the panel - if not member['member_id']: - continue - - # if share partner and permission = none, don't show it in the permission panel. - if member['permission'] == 'none' and member['partner_share']: - continue - - # if article is desyncronized, don't show members based on parent articles. - if not is_sync and member['based_on']: - continue - - member_values = { - 'id': member['member_id'], - 'partner_id': partner_id, - 'partner_name': member['partner_name'], - 'partner_email': member['partner_email'], - 'permission': member['permission'], - 'based_on': f'{member["based_on_icon"] or "📄"} {member["based_on_name"]}' if member['based_on_name'] else False, - 'based_on_id': member['based_on'], - 'partner_share': member['partner_share'], - 'is_unique_writer': member['permission'] == "write" and article.inherited_permission != "write" and not any( - other_member['permission'] == 'write' - for partner_id, other_member in members_permission.items() - if other_member['member_id'] != member['member_id'] - ), - } - members_values.append(member_values) - - internal_permission_field = request.env['knowledge.article']._fields['internal_permission'] - permission_field = request.env['knowledge.article.member']._fields['permission'] - user_is_admin = request.env.user._is_admin() - return { - 'internal_permission_options': internal_permission_field.get_description(request.env).get('selection', []), - 'internal_permission': article.inherited_permission, - 'parent_permission': article.parent_id.inherited_permission, - 'based_on': article.inherited_permission_parent_id.display_name, - 'based_on_id': article.inherited_permission_parent_id.id, - 'members_options': permission_field.get_description(request.env).get('selection', []), - 'members': members_values, - 'is_sync': is_sync, - 'parent_id': article.parent_id.id, - 'parent_name': article.parent_id.display_name, - 'user_is_admin': user_is_admin, - 'show_admin_tip': user_is_admin and article.user_permission != 'write', - } - - @http.route('/knowledge/article/set_member_permission', type='json', auth='user') - def article_set_member_permission(self, article_id, permission, member_id=False, inherited_member_id=False): - """ Sets the permission of the given member for the given article. - - The returned result can also include a `reload_tree` entry that tells the - caller that the aside block listing all articles should be reloaded. This - happens when the article moves from one section to another. - - **Note**: The user needs "write" permission to change the permission of a user. - - :param int article_id: target article id; - :param string permission: permission to set on member, one of 'none', - 'read' or 'write'; - :param int member_id: id of a member of the given article; - :param int inherited_member_id: id of a member from one of the article's - parent (indicates rights are inherited from parents); - """ - article = self._fetch_article(article_id) - if not article: - return werkzeug.exceptions.Forbidden() - member = request.env['knowledge.article.member'].browse(member_id or inherited_member_id).exists() - if not member: - return {'error': _("The selected member does not exists or has been already deleted.")} - - previous_category = article.category - - try: - article._set_member_permission(member, permission, bool(inherited_member_id)) - except (AccessError, ValidationError): - return {'error': _("You cannot change the permission if this member.")} - - if article.category != previous_category: - return {'reload_tree': True} - - return {} - - @http.route('/knowledge/article/remove_member', type='json', auth='user') - def article_remove_member(self, article_id, member_id=False, inherited_member_id=False): - """ Removes the given member from the given article. - - The returned result can also include a `reload_tree` entry that tells the - caller that the aside block listing all articles should be reloaded. This - happens when the article moves from one section to another. - - **Note**: The user needs "write" permission to remove another member from - the list. The user can always remove themselves from the list. - - :param int article_id: target article id; - :param int member_id: id of a member of the given article; - :param int inherited_member_id: id of a member from one of the article's - parent (indicates rights are inherited from parents); - """ - article = self._fetch_article(article_id) - if not article: - return werkzeug.exceptions.Forbidden() - member = request.env['knowledge.article.member'].browse(member_id or inherited_member_id).exists() - if not member: - return {'error': _("The selected member does not exists or has been already deleted.")} - - previous_category = article.category - - try: - article._remove_member(member) - except (AccessError, ValidationError) as e: - return {'error': e} - - if article.category != previous_category: - return {'reload_tree': True} - - return {} - - @http.route('/knowledge/article/set_internal_permission', type='json', auth='user') - def article_set_internal_permission(self, article_id, permission): - """ Sets the internal permission of the given article. - - The returned result can also include a `reload_tree` entry that tells the - caller that the aside block listing all articles should be reloaded. This - happens when the article moves from one section to another. - - **Note**: The user needs "write" permission to update the internal permission - of the article. - - :param int article_id: target article id; - :param string permission: permission to set on member, one of 'none', - 'read' or 'write'; - """ - article = self._fetch_article(article_id) - if not article: - return werkzeug.exceptions.Forbidden() - - previous_category = article.category - - try: - article._set_internal_permission(permission) - except (AccessError, ValidationError): - return {'error': _("You cannot change the internal permission of this article.")} - - if article.category != previous_category: - return {'reload_tree': True} - return {} diff --git a/addons/knowledge/data/article_templates.xml b/addons/knowledge/data/article_templates.xml deleted file mode 100644 index 50c6cfcedde..00000000000 --- a/addons/knowledge/data/article_templates.xml +++ /dev/null @@ -1,350 +0,0 @@ - - - - - - - diff --git a/addons/knowledge/data/ir_actions_data.xml b/addons/knowledge/data/ir_actions_data.xml deleted file mode 100644 index b6f519df3c0..00000000000 --- a/addons/knowledge/data/ir_actions_data.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Articles - - code - action = model.action_home_page() - - - diff --git a/addons/knowledge/data/knowledge_data.xml b/addons/knowledge/data/knowledge_data.xml deleted file mode 100644 index f0c25c9bc0c..00000000000 --- a/addons/knowledge/data/knowledge_data.xml +++ /dev/null @@ -1,513 +0,0 @@ - - - - - HR FAQ - ❓ - -

- - HR FAQ - -

-
-


-
- - Expense Policy - -
-
- Who do I need to address if I need office supplies? -
-

- - Instead of setting up complicated processes, - we prefer to let our employees buy whatever they need. - Just fill in an expense and we will reimburse you. - -

-


-
- Why has my expense not been reimbursed? It has been accepted. -
-

- The Accounting team handles all payments on Fridays afternoon. -
- If 2 weeks have passed and you are still waiting to be paid, get in touch with them. -

-


-
- - Car Policy - -
-
- How do I know which model I can order or not? -
-

- - Our catalog can be found here and is updated every 2 years. - If you do not manage to find the specific model you are looking for, - - - contact our Fleet Officer - -

-
-
- -
-
-
Car_Catalog.png
-
png
-
-
-
-


-
- Who can drive my car? -
-

- - Your car can be driven by - you, your spouse and by any person living under the same roof - as long as they have a valid permit. - -

-


-
- - Leaves & Time Off - -
-
- What do I have to do if I cannot work? -
-

- Please refer to the chart below. -

-


- - - - - - - - - - - - - - - -
- An hour or two (medical appointment, ...) - - Let your Team Leader know in advance. -
- A day or less - - Inform HR and your Team Leader. -
- Absent for more than a day - - See a doctor and send us the sick note. -
-


-
- - write -
- - Sales & Marketing - 🤩 - -

- - Sales & Marketing - -

-
-

We decided to gather all relevant documentation here to make sure all resources were easy to access.

-


- - - - - - - - - - - -
- - SALES - - - - MARKETING - -
PlaybookArticleBrandAssetsArticle
-


-
- - write -
- - Sales Playbook - 📘 - -

Sales Playbook

-
-

- Compiled below are tips & tricks collected among our veterans to help newcomers get started. We hope it will help you sign deals. 🔔 -

-


-

Prospection Templates 👌

-
-


-

- - - PRO TIP - - : From a lead, use the 📗 button in the chatter to find this article and autocomplete your email with this template. - -

-
-
-
-

Hey ProspectName,

-

- I was doing some research online and found your company. -
- Considering we just launched ProductName, I was thinking you would be interested. -

-

- Could you please let me know on which number I could reach you so that we could get in touch? -
- It should not take longer than 15 minutes. -

-

- Talk to you soon, -
- YourName -

-


-
-
-


-

- - PRO TIP: From a lead, use the - - 📗 button in the chatter to find this article and autocomplete your description with this qualification template. -

-
-
-
-

Prospect Qualification

-


-
    -
  1. - Company -
  2. -
  3. -
      -
    • Background
    • -
    • Size
    • -
    • Estimated Revenues
    • -
    • Pain Points
    • -
    -
  4. -
  5. - Contact -
  6. -
  7. -
      -
    • Job Position
    • -
    • Email
    • -
    • Phone
    • -
    -
  8. -
-


-
-
-


-

The 5 Commandments 🙏

-
-
    -
  1. - My Forecast will always be accurate and up-to-date -
  2. -
  3. - All my activities will always be encoded in our CRM -
  4. -
  5. - I will only sell a project if I am convinced it can be a success -
  6. -
  7. - I will not steal prospects from colleagues -
  8. -
  9. - I will not waste time and energy bad-mouthing competitors -
  10. -
-


-

Tips to close more deals 🤝

-
-
    -
  • - Be assertive but listen to what is being said -
  • -
  • - Identify the pain points and offer clear solutions -
  • -
  • - Prepare your demos in advance and integrate the prospect's use case into it -
  • -
-


-

Company Abbreviations 📚

-
-

- You may have heard those a million times and yet you are not entirely sure of what it means. -
- Here is a quick recap for you to shine during the next meeting. -

- - - - - - - - - - - - - - - - - - - -
- MRR - - - Monthly - Recurring - Revenues - (subscriptions, ...) - -
- NRR - - - Non-Recurring - Revenues - (consultancy services, ...) - -
- USP - - - Unique - Selling - Proposition: - Advantage that makes you stand out from the competition. - -
- Q1,2,3,4 - - Nth - Quarter of the fiscal year. -
- E.g. Q4 starts on Oct. 1 and ends on Dec. 31. -
-


-
- - write -
- - Brand Assets - 🎨 - -

Brand Assets

-
-

- Please submit a request for assistance to the Marketing Team if you fall into one of the following: -

-
    -
  • - Advertising that would appear online or on TV -
  • -
  • - SEO & SEA projects (betting on keywords, ...) -
  • -
  • - Booklets for a total price > $1000 -
  • -
-

- For all other categories, we simply require you to follow the rules listed below. -

- - - - - - -
- - Make sure to comply as - you will represent our brand. - -
- If in doubt, get in touch with us. -
-


-

Logo

-
-

- Here are logos that you can use at your own convenience. -
- They can also be shared with customers, journalists and resellers. -

-
-
- -
-
-
Odoo Logo
-
JPG
-
-
-
-


-

Usage

-
    -
  • - Do not recreate the Logo from scratch, use these ones -
  • -
  • - Logos should only be used in the colors provided -
  • -
  • - Always include sufficient clear space around the logo -
  • -
-


-

Brand Name Rules

-
-

- YourCompany is very proud of its brand image. -
- When representing the brand, we thus ask you to be very cautious in how you refer to the company. -

- - - - - - - -
-

- - Do - -

-
    -
  • - - Capitalize the word - YourCompany, - except if it's part of an URL e.g. website/company - -
  • -
  • - - Always type - YourCompany - in the same font size and style as the content of the text - -
  • -
  • - - Write it without any article - (the YourCompany, a YourCompany, ...) - -
  • -
-
-

- - Do Not - -

-
    -
  • - - Pluralize the trademark (e.g. - YourCompanies) - -
  • -
  • - Use the logo instead of the word inside sentences -
  • -
  • - Use the expression "YourCompanions" to refer to our community -
  • -
-
-


-

"About Us" Template

-
-

- Use this template whenever you have an announcement to make. -

-


-
-
-
-

- 250 Executive Park Blvd, Suite 3400 94134 -
- San Francisco California (US) -
- United States -
- info@yourcompany.com -

-


-

- YourCompany is proud to announce that... -

-


-

- About YourCompany: YourCompany is a team of passionate people whose goal is to improve everyone's life through disruptive products. - We build great products to solve your business problems. -
- Our products are designed for small to medium size companies willing to optimize their performance. -

-


-
-
-


-
- - write -
-
-
diff --git a/addons/knowledge/data/mail_templates.xml b/addons/knowledge/data/mail_templates.xml deleted file mode 100644 index 04c01d7f530..00000000000 --- a/addons/knowledge/data/mail_templates.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/addons/knowledge/i18n/knowledge.pot b/addons/knowledge/i18n/knowledge.pot deleted file mode 100644 index 2a6234dc259..00000000000 --- a/addons/knowledge/i18n/knowledge.pot +++ /dev/null @@ -1,1885 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * knowledge -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~15.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-09 14:05+0000\n" -"PO-Revision-Date: 2022-06-09 14:05+0000\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_templates.xml:0 -#, python-format -msgid "\" under:" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__favorite_count -msgid "#Is Favorite" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "%s (copy)" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_mail_invite -msgid ".

" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" or\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_frontend -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Move To" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Archive" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Unarchive" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Duplicate" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_kanban -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Lock" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -" Add cover" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.articles_template_name -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -" Add icon" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Unlock" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Invite" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Create" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid " Share" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -" External users cannot write on " -"Articles and will be granted read access instead." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "" -"" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -msgid "There are no Articles in your Workspace." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -msgid "You do not have any private Articles." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "-" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Add a checklist\n" -" (/checklist)\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Select text to\n" -" Highlight,\n" -" strikethrough\n" -" or \n" -" \n" -" style\n" -" \n" -" \n" -" it\n" -" \n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Blow this list, try\n" -" \n" -" commands\n" -" by \n" -" typing \"/\"\n" -" \n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Content —\n" -" Just click and \n" -" \n" -" start typing\n" -" (documentation, tips, reports, ...)\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Folders —\n" -" \n" -" \n" -" Nest other\n" -" Articles\n" -" \n" -" \n" -" under it \n" -" to \n" -" \n" -" \n" -" regroup them (per team, topic, project, ...).\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Use\n" -" /template\n" -" to write content meant to be pasted, or reuse as templates in emails\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Use\n" -" \n" -" /file\n" -" \n" -" to share documents that are frequently needed\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" \n" -" Articles\n" -" \n" -" are stored into different\n" -" \n" -" \n" -" Sections\n" -" \n" -" :" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" A good workflow is to write your drafts in Private and, once done, move it from\n" -" \n" -" Private \n" -" \n" -" to\n" -" Workspace \n" -" to share it with everyone.\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" And voilà, it is that simple.\n" -"
\n" -" 🚀\n" -"
" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Ready to give it a spin?\n" -"
\n" -" Try the following 👇\n" -"
" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Those are your\n" -" \n" -" Articles.\n" -" \n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" To change the way\n" -" \n" -" Articles \n" -" \n" -" are organized, you can simply\n" -" \n" -" Drag & Drop\n" -" \n" -" them.\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" 👈 See the\n" -" Menu \n" -" there, on the left?\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Add a separator" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"And again, to move an \n" -" Article \n" -" from a \n" -" Section\n" -" \n" -" to the other,\n" -" \n" -" \n" -" just\n" -" Drag & Drop\n" -" \n" -" \n" -" it." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"Check this box to indicate it's done" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"Click anywhere, and just start " -"typing" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Each of them can be used both as:" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"Got it? Now let's try advanced features " -"🧠" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Press Ctrl-Z to undo any change" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"This is where you and your team, can centralize your \n" -" \n" -" Knowledge\n" -" \n" -" and best practices! 🚀" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"This private page is for you to play around with it.\n" -"
" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"Use /heading to convert a text into " -"title" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Favorites\n" -" \n" -" — Those are \n" -" \n" -" shortcuts\n" -" \n" -" you create for yourself. Unstar ⭐ this page on the top, to remove it from favorites." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Private\n" -" \n" -" \n" -" \n" -" — This is\n" -" \n" -" your stuff,\n" -" the things you keep for yourself (Drafts, Todo lists, ...)\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Shared\n" -" \n" -" \n" -" — Those are where you\n" -" \n" -" have\n" -" \n" -" \n" -" \n" -" invited someone or been invited\n" -" " -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"\n" -" Workspace\n" -" \n" -" \n" -" \n" -" — Articles there can be accessed by\n" -" \n" -" \n" -" your team\n" -" " -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Access Restricted. May not be shared with everyone from" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_needaction -msgid "Action Needed" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__active -msgid "Active" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_ids -msgid "Activities" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_exception_decoration -msgid "Activity Exception Decoration" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_state -msgid "Activity State" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_type_icon -msgid "Activity Type Icon" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "Add to favorites" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Admin" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Administrator" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/webclient/commands/knowledge_providers.js:0 -#, python-format -msgid "Advanced Search" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "Archived" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "" -"Are you sure you want to leave this article? If you do, you will no longer " -"have access to the article." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "" -"Are you sure you want to move this article to the Workspace? It will be " -"shared with all internal users." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "" -"Are you sure you want to move this to private? Only you will be able to " -"access it." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "Are you sure you want to remove you own \"Write\" access ?" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "Are you sure you want to restore access?" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "" -"Are you sure you want to restrict this role and restrict access ? This " -"article will no longer inherit access settings from the parent page." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "" -"Are you sure you want to set the internal permission to \"none\" ? If you " -"do, you will no longer have access to the article." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "" -"Are you sure you want to set your permission to \"none\"? If you do, you " -"will no longer have access to the article." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__article_id -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__article_id -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__article_id -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_search -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_search -msgid "Article" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article_member.py:0 -#, python-format -msgid "" -"Article '%s' should always have a writer: inherit write permission, or have " -"a member with write access" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_knowledge_article_member -msgid "Article Member" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__article_url -msgid "Article URL" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#: model:ir.actions.act_window,name:knowledge.knowledge_article_action -#: model:ir.actions.act_window,name:knowledge.knowledge_article_action_form -#: model:ir.actions.server,name:knowledge.ir_actions_server_knowledge_home_page -#, python-format -msgid "Articles" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#: code:addons/knowledge/models/knowledge_article.py:0 -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "" -"Articles %s cannot be updated as this would create a recursive hierarchy." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "As an administrator, you can always modify this article." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_attachment_count -msgid "Attachment Count" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Based on" -msgstr "" - -#. module: knowledge -#: model_terms:ir.actions.act_window,help:knowledge.knowledge_article_action -msgid "Be the first one to unleash the power of Knowledge !" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__body -msgid "Body" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__user_has_write_access -msgid "Can Write" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article_member.py:0 -#, python-format -msgid "Can not update the article or partner of a member." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article_favorite.py:0 -#, python-format -msgid "Can not update the article or user of a favorite." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__inherited_permission__read -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__internal_permission__read -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article_member__permission__read -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_invite__permission__read -msgid "Can read" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__inherited_permission__write -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__internal_permission__write -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article_member__permission__write -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_invite__permission__write -msgid "Can write" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#: code:addons/knowledge/static/src/js/wysiwyg/knowledge_article_link.js:0 -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_invite_view_form -#, python-format -msgid "Cancel" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "" -"Cannot create an article under article %(parent_name)s which is a non-" -"private parent" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#, python-format -msgid "Categories" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__category -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "Category" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__child_ids -msgid "Child Articles" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_editor.xml:0 -#, python-format -msgid "Choose an Article" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_toolbars.js:0 -#, python-format -msgid "Close" -msgstr "" - -#. module: knowledge -#: model:ir.ui.menu,name:knowledge.knowledge_menu_configuration -msgid "Configuration" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_res_partner -msgid "Contact" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Copy to Clipboard" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__cover -msgid "Cover Image" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_command_palette.xml:0 -#, python-format -msgid "Create \"" -msgstr "" - -#. module: knowledge -#: model_terms:ir.actions.act_window,help:knowledge.knowledge_article_action -msgid "Create an article" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__create_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__create_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__create_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__create_uid -msgid "Created by" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "Created by:" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__create_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__create_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__create_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__create_date -msgid "Created on" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_mail_invite -msgid "Dear" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__internal_permission -msgid "" -"Default permission for all internal users. (External users can still have " -"access to this article if they are added to its members)" -msgstr "" - -#. module: knowledge -#: model:ir.model.constraint,message:knowledge.constraint_knowledge_article_check_permission_on_desync -msgid "Desynchronized articles must have internal permission." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__is_desynchronized -msgid "Desyncronized with parents" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "Discard" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__display_name -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__display_name -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__display_name -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__display_name -msgid "Display Name" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Diving in" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Download" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__icon -msgid "Emoji" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#: code:addons/knowledge/static/src/js/knowledge_behaviors.js:0 -#: code:addons/knowledge/static/src/js/knowledge_macros.js:0 -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_frontend -#, python-format -msgid "Error" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "External Guest" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Facebook" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_form -msgid "Favorite" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_knowledge_article_favorite -msgid "Favorite Article" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__favorite_ids -msgid "Favorite Articles" -msgstr "" - -#. module: knowledge -#: model:ir.actions.act_window,name:knowledge.knowledge_article_favorite_action -#: model:ir.ui.menu,name:knowledge.knowledge_article_favorite_menu -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_tree -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree_favorites -msgid "Favorites" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_follower_ids -msgid "Followers" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_partner_ids -msgid "Followers (Partners)" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__activity_type_icon -msgid "Font awesome icon e.g. fa-tasks" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__full_width -msgid "Full width" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_mail_invite -msgid "Go to Article" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_search -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_search -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "Group By" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Guest" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__user_has_access -msgid "Has Access" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__has_message -msgid "Has Message" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__have_share_partners -msgid "Have Share Partners" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Hello there" -msgstr "" - -#. module: knowledge -#: model:ir.ui.menu,name:knowledge.knowledge_menu_home -msgid "Home" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "" -"I'm a template 👋\n" -"
" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__id -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__id -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__id -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__id -msgid "ID" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_exception_icon -msgid "Icon" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__activity_exception_icon -msgid "Icon to indicate an exception activity." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__message_needaction -msgid "If checked, new messages require your attention." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__message_has_error -#: model:ir.model.fields,help:knowledge.field_knowledge_article__message_has_sms_error -msgid "If checked, some messages have a delivery error." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__is_desynchronized -msgid "" -"If set, this article won't inherit access rules from its parents anymore." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__inherited_permission -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__article_permission -msgid "Inherited Permission" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__inherited_permission_parent_id -msgid "Inherited Permission Parent Article" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/wysiwyg/knowledge_article_link.js:0 -#, python-format -msgid "Insert Link" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__internal_permission -#, python-format -msgid "Internal Permission" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Internal members" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "Invitation to access %s" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_invite_view_form -msgid "Invite" -msgstr "" - -#. module: knowledge -#: model:ir.actions.act_window,name:knowledge.knowledge_invite_action_from_article -msgid "Invite people" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_invite_view_form -msgid "Invite users (email address)..." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__is_user_favorite -msgid "Is Favorited" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_is_follower -msgid "Is Follower" -msgstr "" - -#. module: knowledge -#: model:ir.ui.menu,name:knowledge.knowledge_menu_root -msgid "Knowledge" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_knowledge_article -msgid "Knowledge Article" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_knowledge_invite -msgid "Knowledge Invite Wizard" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__last_edition_uid -msgid "Last Edited by" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "Last Edited by:" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__last_edition_date -msgid "Last Edited on" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article____last_update -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite____last_update -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member____last_update -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite____last_update -msgid "Last Modified on" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__write_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__write_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__write_uid -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__write_uid -msgid "Last Updated by" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__write_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__write_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__write_date -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__write_date -msgid "Last Updated on" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/wysiwyg/knowledge_article_link.js:0 -#, python-format -msgid "Link an Article" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__is_locked -msgid "Locked" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "" -"Logging changes from %(partner_name)s without write access on article " -"%(article_name)s due to hierarchy tree update" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_main_attachment_id -msgid "Main Attachment" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_form -msgid "Member" -msgstr "" - -#. module: knowledge -#: model:ir.actions.act_window,name:knowledge.knowledge_article_member_action -#: model:ir.ui.menu,name:knowledge.knowledge_article_member_menu -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_tree -msgid "Members" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__article_member_ids -msgid "Members Information" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_has_error -msgid "Message Delivery error" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_ids -msgid "Messages" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_toolbars.js:0 -#, python-format -msgid "Missing File" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_templates.xml:0 -#, python-format -msgid "Move \"" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#, python-format -msgid "Move an Article" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "Move to Workspace" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__my_activity_date_deadline -msgid "My Activity Deadline" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "My Favorites" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "My Private Articles" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_search -msgid "My Shared Articles" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Navigation Basics 🐣" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/models/knowledge_article.py:0 -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "New Article" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_calendar_event_id -msgid "Next Activity Calendar Event" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_date_deadline -msgid "Next Activity Deadline" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_summary -msgid "Next Activity Summary" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_type_id -msgid "Next Activity Type" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_command_palette.xml:0 -#, python-format -msgid "No Article found." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/webclient/commands/knowledge_providers.js:0 -#, python-format -msgid "No Article found. Create \"%s\"" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__inherited_permission__none -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__internal_permission__none -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article_member__permission__none -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_invite__permission__none -msgid "No access" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/webclient/commands/knowledge_providers.js:0 -#, python-format -msgid "No article found." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_frontend -msgid "Not found" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_needaction_counter -msgid "Number of Actions" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_has_error_counter -msgid "Number of errors" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__message_needaction_counter -msgid "Number of messages which requires an action" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__message_has_error_counter -msgid "Number of messages with delivery error" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#, python-format -msgid "Ok" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Onboarding.pdf" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_toolbars.js:0 -#, python-format -msgid "" -"Oops, the file %s could not be found. Please replace this file box by a new " -"one to re-upload the file." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__parent_id -msgid "Parent Article" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__partner_id -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_search -msgid "Partner" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_member__permission -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__permission -msgid "Permission" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__category__private -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -#, python-format -msgid "Private" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_invite__partner_ids -msgid "Recipients" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/emoji_picker/emoji_list.xml:0 -#: code:addons/knowledge/static/src/components/emoji_picker/emoji_list.xml:0 -#, python-format -msgid "Remove Icon" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Remove Member" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "Remove from favorites" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__activity_user_id -msgid "Responsible User" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "Restore" -msgstr "" - -#. module: knowledge -#: model:ir.model.constraint,message:knowledge.constraint_knowledge_article_check_desync_on_root -msgid "Root articles cannot be desynchronized." -msgstr "" - -#. module: knowledge -#: model:ir.model.constraint,message:knowledge.constraint_knowledge_article_check_permission_on_root -msgid "Root articles must have internal permission." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__message_has_sms_error -msgid "SMS Delivery error" -msgstr "" - -#. module: knowledge -#: model:ir.ui.menu,name:knowledge.knowledge_menu_article -msgid "Search" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_search -msgid "Search Favorites" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/chatter_topbar.xml:0 -#, python-format -msgid "Search Knowledge Articles" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_member_view_search -msgid "Search Members" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_templates.xml:0 -#, python-format -msgid "Search an Article to use as Parent" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_frontend -msgid "Search an Article..." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "Search an article..." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/webclient/commands/knowledge_providers.js:0 -#, python-format -msgid "Search for an article..." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Send as Message" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__sequence -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__sequence -msgid "Sequence" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_controller.js:0 -#, python-format -msgid "Set as Private" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__category__shared -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -msgid "Shared" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree_frontend -msgid "Shared with you" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__activity_state -msgid "" -"Status based on activities\n" -"Overdue: Due date is already passed\n" -"Today: Activity date is today\n" -"Planned: Future activities." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__root_article_id -msgid "Subject" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_toolbars.js:0 -#, python-format -msgid "Template copied to clipboard." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "The article '%s' needs at least one member with 'Write' access." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article_member.py:0 -#, python-format -msgid "" -"The external user %(user_name)s cannot have a 'write' permission on article " -"%(article_name)s" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_macros.js:0 -#, python-format -msgid "" -"The operation was interrupted because the page or the record changed. Please" -" try again later." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_frontend -msgid "The requested article does not exist or you do not have access to it." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/controllers/main.py:0 -#: code:addons/knowledge/controllers/main.py:0 -#, python-format -msgid "The selected member does not exists or has been already deleted." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__sequence -msgid "" -"The sequence is computed only among the articles that have the same parent." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__root_article_id -msgid "" -"The subject is the title of the highest parent in the article hierarchy." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/knowledge_behaviors.js:0 -#, python-format -msgid "This article was deleted or you don't have the rights to access it." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__name -msgid "Title" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "To be sure to stay updated, follow us on" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "Trying to remove wrong member." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Twitter" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__activity_exception_decoration -msgid "Type of the exception activity on record." -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Use as" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Use as Attachment" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/xml/knowledge_toolbars.xml:0 -#, python-format -msgid "Use as Description" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__category -msgid "" -"Used to categozie articles in UI, depending on their main permission " -"definitions." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article_favorite__user_id -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_favorite_view_search -msgid "User" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__user_favorite_sequence -msgid "User Favorite Sequence" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "User Permission" -msgstr "" - -#. module: knowledge -#: model:ir.model.constraint,message:knowledge.constraint_knowledge_article_favorite_unique_article_user -msgid "User already has this article in favorites." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__user_permission -msgid "User permission" -msgstr "" - -#. module: knowledge -#: model:ir.model,name:knowledge.model_res_users -msgid "Users" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,field_description:knowledge.field_knowledge_article__website_message_ids -msgid "Website Messages" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__website_message_ids -msgid "Website communication history" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/res_users.py:0 -#, python-format -msgid "Welcome %s" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__is_locked -msgid "" -"When locked, users cannot write on the body or change the title, even if " -"they have write access on the article." -msgstr "" - -#. module: knowledge -#: model:ir.model.fields,help:knowledge.field_knowledge_article__full_width -msgid "" -"When set, the article body will take the full width available on the article" -" page. Otherwise, the body will have large horizontal margins." -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "Who has access to what? 🕵️‍♂️" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/js/widgets/knowledge_dialogs.js:0 -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__category__workspace -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_tree -#, python-format -msgid "Workspace" -msgstr "" - -#. module: knowledge -#: model:ir.model.constraint,message:knowledge.constraint_knowledge_article_member_unique_article_partner -msgid "You already added this partner on this article." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "You are not allowed to move this article under article %(parent_name)s" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "You cannot add or remove this article to your favorites" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/controllers/main.py:0 -#, python-format -msgid "You cannot change the internal permission of this article." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/controllers/main.py:0 -#, python-format -msgid "You cannot change the permission if this member." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "You cannot create an article under articles on which you cannot write" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "You cannot give access to the article '%s' as you are not editor." -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "" -"You cannot move an article under %(parent_name)s as you cannot write on it" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "" -"You cannot remove the member %(member_name)s from article %(article_name)s" -msgstr "" - -#. module: knowledge -#: code:addons/knowledge/models/knowledge_article.py:0 -#, python-format -msgid "You cannot restore the article %(article_name)s" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "YouTube" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "[GIF_OF_DRAG_DROP.GIF]" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/webclient/commands/knowledge_providers.js:0 -#, python-format -msgid "articles" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.js:0 -#, python-format -msgid "confirm" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_mail_invite -msgid "invited you to access" -msgstr "" - -#. module: knowledge -#. openerp-web -#: code:addons/knowledge/static/src/components/permission_panel/permission_panel.xml:0 -#, python-format -msgid "loader" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__user_permission__none -msgid "none" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_view_form -msgid "options" -msgstr "" - -#. module: knowledge -#: model_terms:ir.ui.view,arch_db:knowledge.knowledge_article_user_onboarding -msgid "pdf" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__user_permission__read -msgid "read" -msgstr "" - -#. module: knowledge -#: model:ir.model.fields.selection,name:knowledge.selection__knowledge_article__user_permission__write -msgid "write" -msgstr "" diff --git a/addons/knowledge/models/__init__.py b/addons/knowledge/models/__init__.py deleted file mode 100644 index 0100fc90b42..00000000000 --- a/addons/knowledge/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import knowledge_article_favorite -from . import knowledge_article_member -from . import knowledge_article -from . import res_partner -from . import res_users diff --git a/addons/knowledge/models/knowledge_article.py b/addons/knowledge/models/knowledge_article.py deleted file mode 100644 index 46b2b508d87..00000000000 --- a/addons/knowledge/models/knowledge_article.py +++ /dev/null @@ -1,1686 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import ast - -from collections import defaultdict -from markupsafe import Markup -from werkzeug.urls import url_join - -from odoo import Command, fields, models, api, _ -from odoo.exceptions import AccessError, ValidationError, UserError -from odoo.osv import expression -from odoo.tools import get_lang - -ARTICLE_PERMISSION_LEVEL = {'none': 0, 'read': 1, 'write': 2} - - -class Article(models.Model): - _name = "knowledge.article" - _description = "Knowledge Article" - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = "favorite_count desc, write_date desc, id desc" - _mail_post_access = 'read' - - active = fields.Boolean(default=True) - name = fields.Char(string="Title", default=lambda self: _('New Article'), required=True, tracking=20) - body = fields.Html(string="Body") - icon = fields.Char(string='Emoji') - cover = fields.Binary(string='Cover Image') - is_locked = fields.Boolean( - string='Locked', - help="When locked, users cannot write on the body or change the title, " - "even if they have write access on the article.") - full_width = fields.Boolean( - string='Full width', - help="When set, the article body will take the full width available on the article page. " - "Otherwise, the body will have large horizontal margins.") - article_url = fields.Char('Article URL', compute='_compute_article_url', readonly=True) - # Hierarchy and sequence - parent_id = fields.Many2one("knowledge.article", string="Parent Article", tracking=30) - child_ids = fields.One2many("knowledge.article", "parent_id", string="Child Articles") - is_desynchronized = fields.Boolean( - string="Desyncronized with parents", - help="If set, this article won't inherit access rules from its parents anymore.") - sequence = fields.Integer( - string="Sequence", - default=0, # Set default=0 to avoid false values and messed up sequence order inside same parent - help="The sequence is computed only among the articles that have the same parent.") - root_article_id = fields.Many2one( - 'knowledge.article', string="Subject", recursive=True, - compute="_compute_root_article_id", store=True, compute_sudo=True, tracking=10, - help="The subject is the title of the highest parent in the article hierarchy.") - # Access rules and members + implied category - internal_permission = fields.Selection( - [('write', 'Can write'), ('read', 'Can read'), ('none', 'No access')], - string='Internal Permission', required=False, - help="Default permission for all internal users. " - "(External users can still have access to this article if they are added to its members)") - inherited_permission = fields.Selection( - [('write', 'Can write'), ('read', 'Can read'), ('none', 'No access')], - string='Inherited Permission', - compute="_compute_inherited_permission", compute_sudo=True, - store=True, recursive=True) - inherited_permission_parent_id = fields.Many2one( - "knowledge.article", string="Inherited Permission Parent Article", - compute="_compute_inherited_permission", compute_sudo=True, - store=True, recursive=True) - article_member_ids = fields.One2many( - 'knowledge.article.member', 'article_id', string='Members Information', - copy=True) - user_has_access = fields.Boolean( - string='Has Access', - compute="_compute_user_has_access", search="_search_user_has_access") - user_has_write_access = fields.Boolean( - string='Can Write', - compute="_compute_user_has_write_access", search="_search_user_has_write_access") - user_permission = fields.Selection( - [('write', 'write'), ('read', 'read'), ('none', 'none')], - string='User permission', - compute='_compute_user_permission') - # categories and ownership - category = fields.Selection( - [('workspace', 'Workspace'), ('private', 'Private'), ('shared', 'Shared')], - compute="_compute_category", compute_sudo=True, store=True, - help='Used to categozie articles in UI, depending on their main permission definitions.') - # Stored to improve performance when loading the article tree. (avoid looping through members if 'workspace') - # Same as write_uid/_date but limited to the body - last_edition_uid = fields.Many2one( - "res.users", string="Last Edited by", - compute='_compute_last_edition_data', store=True, - readonly=False, copy=False) - last_edition_date = fields.Datetime( - string="Last Edited on", - compute='_compute_last_edition_data', store=True, - readonly=False, copy=False) - # Favorite - is_user_favorite = fields.Boolean( - string="Is Favorited", - compute="_compute_is_user_favorite", - inverse="_inverse_is_user_favorite", - search="_search_is_user_favorite") - user_favorite_sequence = fields.Integer(string="User Favorite Sequence", compute="_compute_is_user_favorite") - favorite_ids = fields.One2many( - 'knowledge.article.favorite', 'article_id', - string='Favorite Articles', copy=False) - # Set default=0 to avoid false values and messed up order - favorite_count = fields.Integer( - string="#Is Favorite", - compute="_compute_favorite_count", store=True, copy=False, default=0) - - _sql_constraints = [ - ('check_permission_on_root', - 'check(parent_id IS NOT NULL OR internal_permission IS NOT NULL)', - 'Root articles must have internal permission.' - ), - ('check_permission_on_desync', - 'check(is_desynchronized IS NOT TRUE OR internal_permission IS NOT NULL)', - 'Desynchronized articles must have internal permission.' - ), - ('check_desync_on_root', - 'check(parent_id IS NOT NULL OR is_desynchronized IS NOT TRUE)', - 'Root articles cannot be desynchronized.' - ), - ] - - # ------------------------------------------------------------ - # CONSTRAINTS - # ------------------------------------------------------------ - - @api.constrains('internal_permission', 'article_member_ids') - def _check_is_writable(self): - """ Articles must always have at least one writer. This constraint is done - on article level, in coordination to the constraint on member model (see - ``_check_is_writable`` on ``knowledge.article.member``). - - If article has no member the internal_permission must be write. If article - has members validation is done in article.member model as we cannot trigger - the constraint depending on fields from related model. - - Ǹote: computation is done in Py instead of using optimized SQL queries - because value are not yet in DB at this point.""" - for article in self: - if article.inherited_permission != 'write' and not article._has_write_member(): - raise ValidationError(_("The article '%s' needs at least one member with 'Write' access.", article.display_name)) - - @api.constrains('parent_id') - def _check_parent_id_recursion(self): - if not self._check_recursion(): - raise ValidationError( - _('Articles %s cannot be updated as this would create a recursive hierarchy.', - ', '.join(self.mapped('name')) - ) - ) - - def name_get(self): - return [(rec.id, "%s %s" % (rec.icon or "📄", rec.name)) for rec in self] - - # ------------------------------------------------------------ - # COMPUTED FIELDS - # ------------------------------------------------------------ - - def _compute_article_url(self): - for article in self: - if not article.ids: - article.article_url = False - else: - article.article_url = url_join(article.get_base_url(), 'knowledge/article/%s' % article.id) - - @api.depends('parent_id', 'parent_id.root_article_id') - def _compute_root_article_id(self): - wparent = self.filtered('parent_id') - for article in self - wparent: - article.root_article_id = article - - if not wparent: - return - # group by parents to lessen number of computation - articles_byparent = defaultdict(lambda: self.env['knowledge.article']) - for article in wparent: - articles_byparent[article.parent_id] += article - - for parent, articles in articles_byparent.items(): - ancestors = self.env['knowledge.article'] - while parent: - if parent in ancestors: - raise ValidationError( - _('Articles %s cannot be updated as this would create a recursive hierarchy.', - ', '.join(articles.mapped('name')) - ) - ) - ancestors += parent - parent = parent.parent_id - articles.root_article_id = ancestors[-1:] - - @api.depends('parent_id', 'parent_id.inherited_permission_parent_id', 'internal_permission') - def _compute_inherited_permission(self): - """ Computed inherited internal permission. We go up ancestors until - finding an article with an internal permission set, or a root article - (without parent) or until finding a desynchronized article which - serves as permission ancestor. Desynchronized articles break the - permission tree finding. - - 'parent_id.inherited_permission_parent_id' needs to be in the trigger - as we will need to update this article's inherited permissions if our parent - changes itself from which article it's inheriting. - This allows cascading changes "downwards" when we modify the - internal_permission of an article in the chain. - - It is however not directly used as we optimize the batching and group all - articles by their parent_id.""" - self_inherit = self.filtered(lambda article: article.internal_permission) - for article in self_inherit: - article.inherited_permission = article.internal_permission - article.inherited_permission_parent_id = False - - remaining = self - self_inherit - if not remaining: - return - # group by parents to lessen number of computation - articles_byparent = defaultdict(lambda: self.env['knowledge.article']) - for article in remaining: - articles_byparent[article.parent_id] += article - - for parent, articles in articles_byparent.items(): - ancestors = self.env['knowledge.article'] - while parent: - if parent in ancestors: - raise ValidationError( - _('Articles %s cannot be updated as this would create a recursive hierarchy.', - ', '.join(articles.mapped('name')) - ) - ) - ancestors += parent - if parent.internal_permission or parent.is_desynchronized: - break - parent = parent.parent_id - articles.inherited_permission = ancestors[-1:].internal_permission - articles.inherited_permission_parent_id = ancestors[-1:] - - @api.depends_context('uid') - @api.depends('internal_permission', 'article_member_ids.partner_id', 'article_member_ids.permission') - def _compute_user_permission(self): - """ Compute permission for current user. Public users never have any - permission. Shared users have permission based only on members permission - as internal permission never apply to them. Internal users combine both - internal and members permissions, taking the highest one. """ - if self.env.user._is_public(): - self.user_permission = False - return - - # split transient due to direct SQL query to perform - transient = self.filtered(lambda article: not article.ids) - transient.user_permission = 'write' # not created yet, set default permission value - toupdate = self - transient - if not toupdate: - return - - articles_permissions = {} - if not self.env.user.share: - articles_permissions = self._get_internal_permission() - member_permissions = self._get_partner_member_permissions(self.env.user.partner_id) - for article in self: - article_id = article.ids[0] - if self.env.user.share: - article.user_permission = member_permissions.get(article_id, False) - else: - article.user_permission = member_permissions.get(article_id, False) \ - or articles_permissions[article_id] - - @api.depends_context('uid') - @api.depends('user_permission') - def _compute_user_has_access(self): - """ Compute if the current user has read access to the article based on - permissions and memberships. - - Note that admins have all access through ACLs by default but fields are - still using the permission-based computation. """ - for article in self: - article.user_has_access = article.user_permission != 'none' if article.user_permission else False - - def _search_user_has_access(self, operator, value): - """ This search method looks at article and members permissions to return - all the article the current user has access to. - - Heuristic is - - External users only have access to an article if they are r/w member - on that article; - - Internal users have access if: - - they are read or write member on the article - OR - - The article allow read or write access to all internal users AND the user - is not member with 'none' access - """ - if operator not in ('=', '!=') or not isinstance(value, bool): - raise NotImplementedError("Unsupported search operator") - - articles_with_access = {} - if not self.env.user.share: - articles_with_access = self._get_internal_permission(filter_domain=[('internal_permission', '!=', 'none')]) - member_permissions = self._get_partner_member_permissions(self.env.user.partner_id) - articles_with_no_member_access = [article_id for article_id, perm in member_permissions.items() if perm == 'none'] - articles_with_member_access = list(set(member_permissions.keys() - set(articles_with_no_member_access))) - - # If searching articles for which user has access. - if (value and operator == '=') or (not value and operator == '!='): - if self.env.user.share: - return [('id', 'in', articles_with_member_access)] - return ['|', - '&', ('id', 'in', list(articles_with_access.keys())), ('id', 'not in', articles_with_no_member_access), - ('id', 'in', articles_with_member_access)] - - # If searching articles for which user has NO access. - if self.env.user.share: - return [('id', 'not in', articles_with_member_access)] - return ['|', - '&', ('id', 'not in', list(articles_with_access.keys())), ('id', 'not in', articles_with_member_access), - ('id', 'in', articles_with_no_member_access)] - - @api.depends_context('uid') - @api.depends('user_permission') - def _compute_user_has_write_access(self): - """ Compute if the current user has read access to the article based on - permissions and memberships. - - Note that share user can never write and we therefore shorten the computation. - - Note that admins have all access through ACLs by default but fields are - still using the permission-based computation. """ - if self.env.user.share: - self.user_has_write_access = False - return - for article in self: - article.user_has_write_access = article.user_permission == 'write' - - def _search_user_has_write_access(self, operator, value): - if operator not in ('=', '!=') or not isinstance(value, bool): - raise NotImplementedError("Unsupported search operator") - - # share is never allowed to write - if self.env.user.share: - if (value and operator == '=') or (not value and operator == '!='): - return expression.FALSE_DOMAIN - return expression.TRUE_DOMAIN - - articles_with_access = self._get_internal_permission(filter_domain=[('internal_permission', '=', 'write')]) - member_permissions = self._get_partner_member_permissions(self.env.user.partner_id) - articles_with_member_access = [article_id for article_id, perm in member_permissions.items() if perm == 'write'] - articles_with_no_member_access = list(set(member_permissions.keys() - set(articles_with_member_access))) - - # If searching articles for which user has write access. - if (value and operator == '=') or (not value and operator == '!='): - return ['|', - '&', ('id', 'in', list(articles_with_access.keys())), ('id', 'not in', articles_with_no_member_access), - ('id', 'in', articles_with_member_access) - ] - # If searching articles for which user has NO write access. - return ['|', - '&', ('id', 'not in', list(articles_with_access.keys())), ('id', 'not in', articles_with_member_access), - ('id', 'in', articles_with_no_member_access) - ] - - @api.depends('root_article_id.internal_permission', 'root_article_id.article_member_ids.permission') - def _compute_category(self): - # compute workspace articles - workspace_articles = self.filtered(lambda a: a.root_article_id.internal_permission != 'none') - workspace_articles.category = 'workspace' - - remaining_articles = self - workspace_articles - if not remaining_articles: - return - - results = self.env['knowledge.article.member'].read_group([ - ('article_id', 'in', remaining_articles.root_article_id.ids), ('permission', '!=', 'none') - ], ['article_id'], ['article_id']) # each returned member is read on write. - access_member_per_root_article = dict.fromkeys(remaining_articles.root_article_id.ids, 0) - for result in results: - access_member_per_root_article[result['article_id'][0]] += result["article_id_count"] - - for article in remaining_articles: - # should never crash as non workspace articles always have at least one member with access. - if access_member_per_root_article[article.root_article_id.id] > 1: - article.category = 'shared' - else: - article.category = 'private' - - @api.depends('favorite_ids') - def _compute_favorite_count(self): - favorites = self.env['knowledge.article.favorite'].read_group( - [('article_id', 'in', self.ids)], ['article_id'], ['article_id'] - ) - favorites_count_by_article = { - favorite['article_id'][0]: favorite['article_id_count'] for favorite in favorites} - for article in self: - article.favorite_count = favorites_count_by_article.get(article.id, 0) - - @api.depends('body') - def _compute_last_edition_data(self): - """ Each change of body is considered as a content edition update. """ - self.last_edition_uid = self.env.uid - self.last_edition_date = self.env.cr.now() - - @api.depends_context('uid') - @api.depends('favorite_ids.user_id') - def _compute_is_user_favorite(self): - if self.env.user._is_public(): - self.is_user_favorite = False - return - for article in self: - favorite = article.favorite_ids.filtered(lambda f: f.user_id == self.env.user) - article.is_user_favorite = bool(favorite) - article.user_favorite_sequence = favorite.sequence if favorite else -1 - - def _inverse_is_user_favorite(self): - """ Read access is sufficient for toggling its own favorite status. """ - to_fav = self.filtered(lambda article: self.env.user not in article.favorite_ids.user_id) - to_unfav = self - to_fav - - if to_fav: - to_fav.favorite_ids = [(0, 0, {'user_id': self.env.uid})] - if to_unfav: - to_unfav.favorite_ids.filtered(lambda u: u.user_id == self.env.user).sudo().unlink() - - def _search_is_user_favorite(self, operator, value): - if operator not in ('=', '!='): - raise NotImplementedError("Unsupported search operation on favorite articles") - - if (value and operator == '=') or (not value and operator == '!='): - return [('favorite_ids', 'in', self.env['knowledge.article.favorite'].sudo()._search( - [('user_id', '=', self.env.uid)] - ))] - - # easier than a not in on a 2many field (hint: use sudo because of - # complicated ACL on favorite based on user access on article) - return [('favorite_ids', 'not in', self.env['knowledge.article.favorite'].sudo()._search( - [('user_id', '=', self.env.uid)] - ))] - - # ------------------------------------------------------------ - # CRUD - # ------------------------------------------------------------ - - @api.model - def search(self, args, offset=0, limit=None, order=None, count=False): - """ Override to support ordering on is_user_favorite. - - Ordering through web client calls search_read with an order parameter set. - Search_read then calls search. In this override we therefore override search - to intercept a search without count with an order on is_user_favorite. - In that case we do the search in two steps. - - First step: fill with current user's favorite results - - * Search articles that are favorite of the current user. - * Results of that search will be at the top of returned results. Use limit - None because we have to search all favorite articles. - * Finally take only a subset of those articles to fill with - results matching asked offset / limit. - - Second step: fill with other results. If first step does not gives results - enough to match offset and limit parameters we fill with a search on other - articles. We keep the asked domain and ordering while filtering out already - scanned articles to keep a coherent results. - - All other search and search_read are left untouched by this override to avoid - side effects. Search_count is not affected by this override. - """ - if count or not order or 'is_user_favorite' not in order: - return super(Article, self).search(args, offset=offset, limit=limit, order=order, count=count) - order_items = [order_item.strip().lower() for order_item in (order or self._order).split(',')] - favorite_asc = any('is_user_favorite asc' in item for item in order_items) - - # Search articles that are favorite of the current user. - my_articles_domain = expression.AND([[('favorite_ids.user_id', 'in', [self.env.uid])], args]) - my_articles_order = ', '.join(item for item in order_items if 'is_user_favorite' not in item) - articles_ids = super(Article, self).search(my_articles_domain, offset=0, limit=None, order=my_articles_order, count=count).ids - - # keep only requested window (offset + limit, or offset+) - my_articles_ids_keep = articles_ids[offset:(offset + limit)] if limit else articles_ids[offset:] - # keep list of already skipped article ids to exclude them from future search - my_articles_ids_skip = articles_ids[:(offset + limit)] if limit else articles_ids - - # do not go further if limit is achieved - if limit and len(my_articles_ids_keep) >= limit: - return self.browse(my_articles_ids_keep) - - # Fill with remaining articles. If a limit is given, simply remove count of - # already fetched. Otherwise keep none. If an offset is set we have to - # reduce it by already fetch results hereabove. Order is updated to exclude - # is_user_favorite when calling super() . - article_limit = (limit - len(my_articles_ids_keep)) if limit else None - if offset: - article_offset = max((offset - len(articles_ids), 0)) - else: - article_offset = 0 - article_order = ', '.join(item for item in order_items if 'is_user_favorite' not in item) - - other_article_res = super(Article, self).search( - expression.AND([[('id', 'not in', my_articles_ids_skip)], args]), - offset=article_offset, limit=article_limit, order=article_order, count=count - ) - if favorite_asc in order_items: - return other_article_res + self.browse(my_articles_ids_keep) - else: - return self.browse(my_articles_ids_keep) + other_article_res - - @api.model_create_multi - def create(self, vals_list): - """ Article permissions being quite strong, some custom behavior support - is necessary in order to let people create articles with a correct - configuration. - - Constraints - * creating an article under a parent requires to be able to write on - it. As anyway errors will raise we prevently raise a more user friendly - error; - * root articles without permission are forced to write to avoid issues - with constraints; - - Notably - * automatically organize articles to be the last of their parent - children, unless a specific sequence is given; - * allow creation of private articles for him- or her-self. As creation - rights on member model are not granted, we detect private article - creation and sudo the creation of those. This requires some data - manipulation to sudo only those and keep requested ordering based - on vals_list; - """ - defaults = self.default_get(['article_member_ids', 'internal_permission', 'parent_id']) - vals_by_parent_id = {} - vals_as_sudo = [] - - for vals in vals_list: - can_sudo = False - # get values from vals or defaults - member_ids = vals.get('article_member_ids') or defaults.get('article_member_ids') or False - internal_permission = vals.get('internal_permission') or defaults.get('internal_permission') or False - parent_id = vals.get('parent_id') or defaults.get('parent_id') or False - - # force write permission for workspace articles - if not parent_id and not internal_permission: - vals.update({'internal_permission': 'write', - 'parent_id': False, # just be sure we don't grant privileges - }) - - # allow private articles creation if given values are considered as safe - check_for_sudo = not self.env.su and \ - not any(fname in vals for fname in ['favorite_ids', 'child_ids']) and \ - not parent_id and internal_permission == 'none' and \ - member_ids and len(member_ids) == 1 - if check_for_sudo: - self_member = member_ids[0][0] == Command.CREATE and \ - member_ids[0][2].get('partner_id') == self.env.user.partner_id.id - if self_member: - can_sudo = True - - # if no sequence, parent will have to be checked - if not vals.get('sequence'): - vals_by_parent_id.setdefault(parent_id, []).append(vals) - vals_as_sudo.append(can_sudo) - - # compute all maximum sequences / parent - max_sequence_by_parent = {} - if vals_by_parent_id: - parent_ids = list(vals_by_parent_id.keys()) - try: - self.check_access_rights('write') - self.env['knowledge.article'].browse(parent_ids).check_access_rule('write') - except AccessError: - raise AccessError(_("You cannot create an article under articles on which you cannot write")) - max_sequence_by_parent = self._get_max_sequence_inside_parents(parent_ids) - - # update sequences - for parent_id, article_vals in vals_by_parent_id.items(): - current_sequence = 0 - if parent_id in max_sequence_by_parent: - current_sequence = max_sequence_by_parent[parent_id] + 1 - - for vals in article_vals: - if 'sequence' in vals: - current_sequence = vals.get('sequence') - else: - vals['sequence'] = current_sequence - current_sequence += 1 - - # sort by sudo / not sudo - notsudo_articles = iter(super(Article, self).create([ - vals for vals, can_sudo in zip(vals_list, vals_as_sudo) - if not can_sudo - ])) - sudo_articles = iter(super(Article, self.sudo()).create([ - vals for vals, can_sudo in zip(vals_list, vals_as_sudo) - if can_sudo - ]).with_env(self.env)) - articles = self.env['knowledge.article'] - for vals, is_sudo in zip(vals_list, vals_as_sudo): - if is_sudo: - articles += next(sudo_articles) - else: - articles += next(notsudo_articles) - - return articles - - def write(self, vals): - # Move under a parent is considered as a write on it (permissions, ...) - _resequence = False - if 'parent_id' in vals: - parent = self.env['knowledge.article'] - if vals.get('parent_id'): - parent = self.browse(vals['parent_id']) - try: - parent.check_access_rights('write') - parent.check_access_rule('write') - except AccessError: - raise AccessError(_("You cannot move an article under %(parent_name)s as you cannot write on it", - parent_name=parent.display_name)) - if 'sequence' not in vals: - max_sequence = self._get_max_sequence_inside_parents(parent.ids).get(parent.id, -1) - vals['sequence'] = max_sequence + 1 - else: - _resequence = True - - result = super(Article, self).write(vals) - - # resequence only if a sequence was not already computed based on current - # parent maximum to avoid unnecessary recomputation of sequences - if _resequence: - self._resequence() - - return result - - @api.returns('self', lambda value: value.id) - def copy(self, default=None): - defaults = dict( - {"name": _("%s (copy)", self.name)}, - **(default or {}) - ) - if not self.env.su and not self.user_has_write_access: - defaults.pop('article_member_ids', None) - defaults.pop('favorite_ids', None) - defaults.pop('child_ids', None) - if self.user_has_access: - defaults['article_member_ids'] = [ - (0, 0, {'partner_id': self.env.user.partner_id.id, - 'permission': 'write'}) - ] - return super().copy(default=defaults) - - def unlink(self): - for article in self: - # Make all the article's children be adopted by the parent's parent. - # Otherwise, we will have to manage an orphan house. - parent = article.parent_id - if parent: - article.child_ids.write({"parent_id": parent.id}) - return super(Article, self).unlink() - - def action_archive(self): - """ When archiving - - * archive the current article and all its writable descendants; - * unreachable descendants (none, read) are set as free articles without - root; - """ - # _detach_unwritable_descendants calls _filter_access_rules_python which returns - # a sudo-ed recordset - writable_descendants = self._detach_unwritable_descendants().with_env(self.env) - return super(Article, self + writable_descendants).action_archive() - - # ------------------------------------------------------------ - # ACTIONS - # ------------------------------------------------------------ - - def action_home_page(self): - res_id = self.env.context.get('res_id', False) - article = self.browse(res_id) if res_id else self._get_first_accessible_article() - mode = 'edit' if article.user_has_write_access else 'readonly' - action = self.env['ir.actions.act_window']._for_xml_id('knowledge.knowledge_article_action_form') - action['res_id'] = article.id - action['context'] = dict( - ast.literal_eval(action.get('context')), - form_view_initial_mode=mode, - ) - return action - - def action_set_lock(self): - self.is_locked = True - - def action_set_unlock(self): - self.is_locked = False - - def action_toggle_favorite(self): - """ Read access is sufficient for toggling its own favorite status. """ - try: - self.check_access_rights('read') - self.check_access_rule('read') - except AccessError: - # Return a meaningful error message as this may be called through UI - raise AccessError(_("You cannot add or remove this article to your favorites")) - - self.sudo()._inverse_is_user_favorite() - return self[0].is_user_favorite if self else False - - def action_article_archive(self): - """ Article specific archive: after archive, redirect to the home page - displaying accessible articles, instead of doing nothing. """ - self.action_archive() - return self.with_context(res_id=False).action_home_page() - - def action_unarchive(self): - res = super(Article, self).action_unarchive() - if len(self) == 1: - return self.with_context(res_id=self.id).action_home_page() - return res - - # ------------------------------------------------------------ - # SEQUENCE / ORDERING - # ------------------------------------------------------------ - - def move_to(self, parent_id=False, before_article_id=False, is_private=False): - """ Move an article in the tree. - - :param int parent_id: id of an article that will be the new parent; - :param int before_article_id: id of an article before which the article - should be moved. Otherwise it is put as last parent children; - :param bool is_private: set as private; - - :return: True - """ - self.ensure_one() - before_article = self.env['knowledge.article'].browse(before_article_id) if before_article_id else self.env['knowledge.article'] - parent = self.env['knowledge.article'].browse(parent_id) if parent_id else self.env['knowledge.article'] - - if is_private: - # making an article private requires a lot of extra-processing, use specific method - return self._move_and_make_private(parent=parent, before_article=before_article) - - values = {'parent_id': parent_id} - if before_article: - values['sequence'] = before_article.sequence - if not parent_id: - # be sure to have an internal permission on the article if moved outside - # of an hierarchy - values.update({ - 'internal_permission': 'write', - 'is_desynchronized': False - }) - - return self.write(values) - - def _resequence(self): - """ This method reorders the children of the same parent (brotherhood) if - needed. If an article have been moved from one parent to another we do not - need to resequence the children of the old parent as the order remains - unchanged. We only need to resequence the children of the new parent only if - the sequences of the children contains duplicates. - - When reordering an article, we assume that we always set the sequence - equals to the position we want it to be. When duplicates last modified - wins. We use write date, presence in self (indicating a write hence a - priority) and ID to differentiate new ordering between duplicates. - - e.g. if we want article D to be placed at 3rd position between A B et C - * set D.sequence = 2; - * but C was already 2; - * D is in self: it wins. Or D has newer write_date: it wins. Or D has - been created more recently: it wins. - """ - parent_ids = self.mapped("parent_id").ids - if any(not article.parent_id for article in self): - parent_ids.append(False) - - # fetch and sort all_chidren: sequence ASC, then modified, then write date DESC - all_children = self.search([("parent_id", 'in', parent_ids)]) - all_children = all_children.sorted( - lambda article: (-1 * article.sequence, - article in self, - article.write_date, - article.id - ), - reverse=True # due to date - ) - - article_to_update_by_sequence = defaultdict(self.env['knowledge.article'].browse) - for parent_id in parent_ids: - children = all_children.filtered(lambda a: a.parent_id.id == parent_id) - sequences = children.mapped('sequence') - # no need to resequence if no duplicates. - if len(sequences) == len(set(sequences)): - return - - # only need to resequence after duplicate: allow holes in the sequence but limit number of write operations. - duplicate_index = [idx for idx, item in enumerate(sequences) if item in sequences[:idx]][0] - start_sequence = sequences[duplicate_index] + 1 - for i, child in enumerate(children[duplicate_index:]): - article_to_update_by_sequence[i + start_sequence] |= child - - for sequence in article_to_update_by_sequence: - # call super to avoid loops in write - super(Article, article_to_update_by_sequence[sequence]).write({'sequence': sequence}) - - @api.model - def _get_max_sequence_inside_parents(self, parent_ids): - max_sequence_by_parent = {} - if parent_ids: - domain = [('parent_id', 'in', parent_ids)] - else: - domain = [('parent_id', '=', False)] - rg_results = self.env['knowledge.article'].sudo().read_group( - domain, - ['sequence:max'], - ['parent_id'] - ) - for rg_line in rg_results: - # beware name_get like returns either 0, either (id, 'name') - index = rg_line['parent_id'][0] if rg_line['parent_id'] else False - max_sequence_by_parent[index] = rg_line['sequence'] - return max_sequence_by_parent - - # ------------------------------------------------------------ - # HELPERS - # ------------------------------------------------------------ - - @api.model - @api.returns('knowledge.article', lambda article: article.id) - def article_create(self, title=False, parent_id=False, is_private=False): - """ Helper to create articles, allowing to pre-compute some configuration - values. - - :param str title: name of the article; - :param int parent_id: id of an existing article who will be the parent - of the newly created articled. Must be writable; - :param bool is_private: set current user as sole owner of the new article; - """ - parent = self.browse(parent_id) if parent_id else self.env['knowledge.article'] - values = {'parent_id': parent.id} - if title: - values.update({ - 'body': Markup('

%s

') % title, - 'name': title, - }) - else: - values['body'] = Markup('


') - - if parent: - if not is_private and parent.category == "private": - is_private = True - else: - # child do not have to setup an internal permission as it is inherited - values['internal_permission'] = 'none' if is_private else 'write' - - if is_private: - if parent and parent.category != "private": - raise ValidationError( - _("Cannot create an article under article %(parent_name)s which is a non-private parent", - parent_name=parent.display_name) - ) - if not parent: - values['article_member_ids'] = [(0, 0, { - 'partner_id': self.env.user.partner_id.id, - 'permission': 'write' - })] - return self.create(values) - - def get_user_sorted_articles(self, search_query, limit=10): - """ Called when using the Command palette to search for articles matching the search_query. - As the article should be sorted also in function of the current user's favorite sequence, a search_read rpc - won't be enough to returns the articles in the correct order. - This method returns a list of article proposal matching the search_query sorted by: - - is_user_favorite - by Favorite sequence - - Favorite count - and returned result mimic a search_read result structure. - """ - search_domain = ["|", ("name", "ilike", search_query), ("root_article_id.name", "ilike", search_query)] - articles = self.search( - expression.AND([search_domain, [("is_user_favorite", "=", True)]]), - limit=None, - ) - sorted_articles = articles.sorted( - key=lambda a: (-1 * a.user_favorite_sequence, - a.favorite_count, - a.write_date, - a.id), - reverse=True - ) - - # fill with not favorites articles - if len(sorted_articles) < limit: - articles = self.search( - expression.AND([search_domain, [("is_user_favorite", "=", False)]]), - limit=(limit-len(sorted_articles)), - order='favorite_count desc, write_date desc, id desc' - ) - sorted_articles += articles.sorted( - key=lambda a: (a.favorite_count, - a.write_date, - a.id), - reverse=True - ) - elif len(sorted_articles) > limit: - sorted_articles = sorted_articles[:limit] - - return sorted_articles.read(['id', 'name', 'is_user_favorite', - 'favorite_count', 'root_article_id', 'icon']) - - # ------------------------------------------------------------ - # PERMISSIONS / MEMBERS MANAGEMENT - # ------------------------------------------------------------ - - def restore_article_access(self): - """ Resets permissions based on ancestors. It removes all members except - members on the articles that are not on any ancestor or that have higher - permission than from ancestors. - - Security note: this method checks for write access on current article, - considering it as sufficient to restore access and members. - """ - self.ensure_one() - if not self.parent_id: - return False - if not self.env.su and not self.user_has_write_access: - raise AccessError( - _('You have to be editor on %(article_name)s to restore it.', - article_name=self.display_name)) - - member_permission = (self | self.parent_id)._get_article_member_permissions() - article_members_permission = member_permission[self.id] - parents_members_permission = member_permission[self.parent_id.id] - - members_values = [] - for partner, values in article_members_permission.items(): - permission = values['permission'] - if values["based_on"] or partner not in parents_members_permission \ - or ARTICLE_PERMISSION_LEVEL[permission] > ARTICLE_PERMISSION_LEVEL[parents_members_permission[partner]['permission']]: - continue - members_values.append((3, values['member_id'])) - - return self.sudo().write({ - 'internal_permission': False, - 'article_member_ids': members_values, - 'is_desynchronized': False - }) - - def invite_members(self, partners, permission): - """ Invite the given partners to the current article. Inviting to remove - access is straightforward (just set permission). Inviting with rights - requires to check for privilege escalation in descendants. - - :param Model partner_ids: recordset of invited partners; - :param string permission: permission of newly invited members, one of - 'none', 'read' or 'write'; - """ - self.ensure_one() - if permission == 'none': - self._add_members(partners, permission) - else: - # prevent the invited user to get access to children articles the current user has no access to - unreachable_children = self.sudo().child_ids.filtered(lambda c: not c.user_has_write_access) - for child in unreachable_children: - child._add_members(partners, 'none', force_update=False) - - share_partner_ids = partners.filtered(lambda partner: partner.partner_share) - self._add_members(share_partner_ids, 'read') - self._add_members(partners - share_partner_ids, permission) - - if permission != 'none': - self._send_invite_mail(partners) - - return True - - def _set_internal_permission(self, permission): - """ Set the internal permission of the article. - - Special cases: - * to ensure the user still has write access after modification, - add the user as write member if given permission != write; - * when downgrading internal permission on a child article, desync it - from parent to stop inherited rights transmission; - * if we set same permission as parent and the article has no specific - member: resync if on parent; - - :param str permission: internal permission to set, one of 'none', 'read' - or 'write'; - """ - self.ensure_one() - if self.user_has_write_access and permission != "write": - self._add_members(self.env.user.partner_id, 'write') - - downgrade = not self.is_desynchronized and self.parent_id and \ - ARTICLE_PERMISSION_LEVEL[self.parent_id.inherited_permission] > ARTICLE_PERMISSION_LEVEL[permission] - if downgrade: - # desync is done as sudo, explicitly check access - if not self.env.su and not self.user_has_write_access: - raise AccessError( - _('You have to be editor on %(article_name)s to change its internal permission.', - article_name=self.display_name)) - return self._desync_access_from_parents(force_internal_permission=permission) - - values = {'internal_permission': permission} - if permission == self.parent_id.inherited_permission and not self.article_member_ids: - values.update({ - 'internal_permission': False, - 'is_desynchronized': False - }) - return self.write(values) - - def _set_member_permission(self, member, permission, is_based_on=False): - """ Sets the given permission to the given member. - - If the member has rights based on membership: simply update it. - - If the member has rights based on a parent article (inherited rights) - If the new permission is downgrading the member's access - the article is desynchronized form its parent; - Else we add a new member with the higher permission; - - Security note: this method checks for write access on current article, - considering it as sufficient to modify members permissions. - - :param member: member whose permission - is to be updated. Can be a member of 'self' or one of its ancestors; - :param str permission: new permission, one of 'none', 'read' or 'write'; - :param bool is_based_on: whether rights are inherited or through membership; - """ - self.ensure_one() - if not self.env.su and not self.user_has_write_access: - raise AccessError( - _('You have to be editor on %(article_name)s to modify members permissions.', - article_name=self.display_name)) - - if is_based_on: - downgrade = ARTICLE_PERMISSION_LEVEL[member.permission] > ARTICLE_PERMISSION_LEVEL[permission] - if downgrade: - self._desync_access_from_parents(force_partners=member.partner_id, force_member_permission=permission) - else: - self._add_members(member.partner_id, permission) - else: - member.article_id.sudo().with_context(knowledge_member_skip_writable_check=True).write({ - 'article_member_ids': [(1, member.id, {'permission': permission})] - }) - - def _remove_member(self, member): - """ Removes a member from the article. If the member was based on a - parent article, the current article will be desynchronized form its parent. - We also ensure the partner to remove is removed after the desynchronization - if was copied from parent. - - Security note - * when removing themselves: users need only read access on the article - (automatically checked by access on self); - * when removing someone else: write access is required on the article - (explicitly checked); - - :param member: member to remove - """ - self.ensure_one() - if not member: - raise ValueError(_('Trying to remove wrong member.')) - - # belongs to current article members - current_membership = self.article_member_ids.filtered(lambda m: m == member) - current_user_partner = self.env.user.partner_id - - # If remove self member, set member permission to 'none' (= leave article) to hide the article for the user. - if member.partner_id == current_user_partner: - if current_membership: - members_command = [(1, current_membership.id, {'permission': 'none'})] - else: - members_command = [(0, 0, {'partner_id': current_user_partner.id, 'permission': 'none'})] - self.sudo().write({'article_member_ids': members_command}) - else: - if not self.env.su and not self.user_has_write_access: - raise AccessError( - _("You have to be editor on %(article_name)s to remove or exclude member %(member_name)s.", - article_name=self.display_name, - member_name=member.display_name)) - # member is on current article: remove member - if current_membership: - self.sudo().write({'article_member_ids': [(2, current_membership.id)]}) - # inherited rights from parent: desync and remove member - else: - self._desync_access_from_parents(force_partners=self.article_member_ids.partner_id) - current_membership = self.article_member_ids.filtered(lambda m: m.partner_id == member.partner_id) - if current_membership: - self.sudo().write({'article_member_ids': [(2, current_membership.id)]}) - - def _add_members(self, partners, permission, force_update=True): - """ Adds new members to the current article with the given permission. - If a given partner is already member permission is updated instead. - - Security note: this method checks for write access on current article, - considering it as sufficient to add new members. - - :param partners: recordset of res.partner for which - new members are added; - :param string permission: member permission, one of 'none', 'read' or 'write'; - :param boolean force_update: if already existing, force the new permission; - this can be used to create default members and left existing one untouched; - """ - self.ensure_one() - if not self.env.su and not self.user_has_write_access: - raise AccessError( - _("You have to be editor on %(article_name)s to add members.", - article_name=self.display_name)) - - members_to_update = self.article_member_ids.filtered_domain([('partner_id', 'in', partners.ids)]) - partners_to_create = partners - members_to_update.mapped('partner_id') - - members_command = [ - (0, 0, {'partner_id': partner.id, - 'permission': permission}) - for partner in partners_to_create - ] - if force_update: - members_command += [ - (1, member.id, {'permission': permission}) - for member in members_to_update - ] - - return self.sudo().write({'article_member_ids': members_command}) - - def _desync_access_from_parents(self, force_internal_permission=False, - force_partners=False, force_member_permission=False): - """ Copies all inherited accesses from parents on the article and desynchronize - the article from its parent, allowing custom access management. We allow - to force permission of given partners. - - Security note: this method does not check accesses. Caller has to ensure - access is granted, depending on the business flow. - - :param string force_internal_permission: force a new internal permission - for the article. Otherwise fallback on inherited computed internal - permission; - :param force_partners: force permission of new members - related to those partners; - :param string force_member_permission: used with force_partners to specify - the custom permission to give. One of 'none', 'read', 'write'; - """ - self.ensure_one() - new_internal_permission = force_internal_permission or self.inherited_permission - members_commands = self._copy_access_from_parents_commands( - force_partners=force_partners, - force_member_permission=force_member_permission - ) - - return self.sudo().write({ - 'article_member_ids': members_commands, - 'internal_permission': new_internal_permission, - 'is_desynchronized': True, - }) - - def _copy_access_from_parents_commands(self, force_partners=False, force_member_permission=False): - """ Prepares commands for all inherited accesses from parents on the given - article. It allows to de-synchronize the article from its parent and - allows custom access management. We allow to force permission of given - partners, bypassing inherited ones. - - :param force_partners: force permission of new members - related to those partners; - :param str force_member_permission: force a new permission to partners - given by force_partners. Otherwise fallback on inherited computed - internal permission; - - :return list member_commands: commands to be applied on 'article_member_ids' - field; - """ - self.ensure_one() - members_permission = self._get_article_member_permissions()[self.id] - - members_commands = [] - for partner_id, values in members_permission.items(): - # if member already on self, do not add it. - if not values['based_on'] or values['based_on'] == self.id: - continue - if force_partners and force_member_permission and partner_id in force_partners.ids: - new_member_permission = force_member_permission - else: - new_member_permission = values['permission'] - - members_commands.append( - (0, 0, {'partner_id': partner_id, - 'permission': new_member_permission, - })) - return members_commands - - def _detach_unwritable_descendants(self): - """ When taking specific actions on an article like archiving or making - it private, we want to be able to 'detach' the unaccessible children and - set them as free (root) articles. Indeed in those business flows you - should not change the current access level of children articles. - - This method takes care of correctly detaching those children and returns - a subset of children to which the user effectively has write access to. - - As the children are moved to "root" articles we also reset their desync - status. Indeed, a root article cannot be desyncronized as stated by SQL - constraints. - - Note: this might produce funny results when involving a hierarchy with - invisible nodes in it (A-B-C where B is not achievable). You might - archive / privatize articles and break hierarchy without knowing it. - - Security note: this method does not check accesses. Caller has to ensure - access is granted, depending on the business flow. - - :return children: the children articles which were - not detached, meaning that current user has write access on them """ - all_descendants_sudo = self.sudo()._get_descendants() - writable_descendants_sudo = all_descendants_sudo.with_env(self.env)._filter_access_rules_python('write') - other_descendants_sudo = all_descendants_sudo - writable_descendants_sudo - - # copy rights to allow breaking the hierarchy while keeping access for members - # do this on synchronized articles as desynchronized one do not inherit from parent - for article_sudo in other_descendants_sudo.filtered(lambda article: not article.is_desynchronized): - article_sudo.write({ - 'article_member_ids': article_sudo._copy_access_from_parents_commands() - }) - - # create new root articles and reset desync: direct children of these articles + - # the writable descendants. Indeed they are going to be modified the same way - # as "self" (archived / moved to private) -> all their children should be detached - new_roots_woperm_sudo = other_descendants_sudo.filtered( - lambda article: article.parent_id in (self + writable_descendants_sudo) and not article.internal_permission) - new_roots_wperm_sudo = other_descendants_sudo.filtered( - lambda article: article.parent_id in (self + writable_descendants_sudo) and article.internal_permission) - if new_roots_wperm_sudo: - new_roots_wperm_sudo.write({ - 'is_desynchronized': False, - 'parent_id': False - }) - for new_root_sudo in new_roots_woperm_sudo: - new_root_sudo.write({ - 'is_desynchronized': False, - 'internal_permission': new_root_sudo.inherited_permission, - 'parent_id': False, - }) - - return writable_descendants_sudo - - def _move_and_make_private(self, parent=False, before_article=False): - """ Set as private: remove members, ensure current user is the only member - with write access. Requires a sudo to bypass member ACLs after checking - write access on the article. - - Children articles to which the user also has a write access to are made - private as well. Other articles are detached, see '_detach_unwritable_descendants' - for details. - - :param parent: an optional parent to move the article under; - :param before_article: article before which the article - should be moved. Otherwise it is put as last parent children; - - :return: True - """ - self.ensure_one() - parent = parent if parent is not False else self.env['knowledge.article'] - before_article = before_article if before_article is not False else self.env['knowledge.article'] - - try: - self.check_access_rights('write') - (self + parent).check_access_rule('write') - except (AccessError, UserError): - if parent: - raise AccessError( - _("You are not allowed to move '%(article_name)s' under '%(parent_name)s'.", - article_name=self.display_name, - parent_name=parent.display_name) - ) - raise AccessError( - _("You are not allowed to make '%(article_name)s' private.", article_name=self.display_name) - ) - - # first detach unwritable children (see ``_detach_unwritable_descendants``) - writable_descendants_sudo = self._detach_unwritable_descendants() - - article_values = { - # reset internal permission if parent is set (will inherit) or force private (aka 'none') - 'internal_permission': False if parent else 'none', - # cannot be desync when made private as we wipe members access - 'is_desynchronized': False, - 'parent_id': parent.id if parent else False, - } - if before_article: - article_values['sequence'] = before_article.sequence - - self_sudo = self.sudo() - # remove members as the article is moved to private - members_to_remove = self_sudo.article_member_ids - self_member_command = [] - if not parent: - # make sure the current user is the only member left with write access to the article - # if we have a parent, this is not necessary as we will inherit members access from them - self_member = self_sudo.article_member_ids.filtered(lambda m: m.partner_id == self.env.user.partner_id) - if self_member: - self_member_command = [(1, self_member.id, {'permission': 'write'})] - # keep current member - members_to_remove = members_to_remove.filtered( - lambda member: member.partner_id != self.env.user.partner_id - ) - else: - self_member_command = [(0, 0, { - 'partner_id': self.env.user.partner_id.id, - 'permission': 'write' - })] - - article_values['article_member_ids'] = [ - (2, member.id) - for member in members_to_remove - ] + self_member_command - - res = self_sudo.with_context(knowledge_member_skip_writable_check=True).write(article_values) - - # remove all specific memberships configurations on children. They now inherit - # the only 'write' member from their parent now, making them private. - writable_descendants_sudo.with_context(knowledge_member_skip_writable_check=True).write({ - 'internal_permission': False, - 'article_member_ids': [(5, 0)], - }) - - return res - - def _has_write_member(self, partners_to_exclude=False, members_to_exclude=False): - """ Method allowing to check if this article still has at least one member - with write access. Typically used during constraints checks. - - Please note that this method is *not* optimized and should be avoided by - using ``_get_article_member_permissions`` instead when possible. - - :param partners_to_exclude: used when checking recursively - through article parents, we only check for the most specific access - for a given partner; - :param members_to_exclude: memberships that - should not be considered when checking for a write access, used when - unlinking members that should not be taken into account; - - :return boolean: whether a write member has been found; - """ - self.ensure_one() - partners_to_exclude = partners_to_exclude if partners_to_exclude else self.env['res.partner'] - - article_members = self.article_member_ids - if members_to_exclude: - article_members -= members_to_exclude - - if any(m.permission == 'write' and m.partner_id not in partners_to_exclude - for m in article_members): - return True - if not self.is_desynchronized and self.parent_id: - return self.parent_id._has_write_member( - partners_to_exclude=article_members.partner_id | partners_to_exclude - ) - return False - - # ------------------------------------------------------------ - # PERMISSIONS BATCH COMPUTATION - # ------------------------------------------------------------ - - @api.model - def _get_internal_permission(self, filter_domain=None): - """ Compute article based permissions. - - Note: we don't use domain because we cannot include properly the where clause - in the custom sql query. The query's output table and fields names does not match - the model we are working on. - """ - self.flush_model() - - args = [] - base_where_domain = '' - if self.ids: - base_where_domain = "WHERE id in %s" - args.append(tuple(self.ids)) - - where_clause = '' - if filter_domain: - query = self.with_context(active_test=False)._where_calc(filter_domain) - table_name, where_clause, where_params = query.get_sql() - where_clause = f'WHERE {where_clause.replace(table_name, "article_perms")}' - args += where_params - - sql = f''' - WITH RECURSIVE article_perms as ( - SELECT id, id as article_id, parent_id, internal_permission, is_desynchronized - FROM knowledge_article - {base_where_domain} - UNION - SELECT parents.id, perms.article_id, parents.parent_id, - COALESCE(perms.internal_permission, parents.internal_permission), - perms.is_desynchronized - FROM knowledge_article parents - INNER JOIN article_perms perms - ON perms.parent_id=parents.id - AND perms.is_desynchronized IS NOT TRUE - AND perms.internal_permission IS NULL - ) - SELECT article_id, max(internal_permission) - FROM article_perms - {where_clause} - GROUP BY article_id''' - self._cr.execute(sql, args) - return dict(self._cr.fetchall()) - - @api.model - def _get_partner_member_permissions(self, partner): - """ Retrieve the permission for the given partner for all articles. - The articles can be filtered using the article_ids param. - - The member model is fully flushed before running the request. """ - self.env['knowledge.article'].flush_model() - self.env['knowledge.article.member'].flush_model() - - args = [partner.id] - base_where_domain = '' - if self.ids: - base_where_domain = "WHERE perms1.id in %s" - args.append(tuple(self.ids)) - - sql = f''' - WITH RECURSIVE article_perms as ( - SELECT a.id, a.parent_id, m.permission, a.is_desynchronized - FROM knowledge_article a - LEFT JOIN knowledge_article_member m - ON a.id=m.article_id and partner_id = %s - ), article_rec as ( - SELECT perms1.id, perms1.id as article_id, perms1.parent_id, - perms1.permission, perms1.is_desynchronized - FROM article_perms as perms1 - {base_where_domain} - UNION - SELECT perms2.id, perms_rec.article_id, perms2.parent_id, - COALESCE(perms_rec.permission, perms2.permission), - perms2.is_desynchronized - FROM article_perms as perms2 - INNER JOIN article_rec perms_rec - ON perms_rec.parent_id=perms2.id - AND perms_rec.is_desynchronized IS NOT TRUE - AND perms_rec.permission IS NULL - ) - SELECT article_id, max(permission) - FROM article_rec - WHERE permission IS NOT NULL - GROUP BY article_id''' - self._cr.execute(sql, args) - return dict(self._cr.fetchall()) - - def _get_article_member_permissions(self, additional_fields=False): - """ Retrieve the permission for all the members that apply to the target article. - Members that apply are not only the ones on the article but can also come from parent articles. - - The caller can pass additional fields to retrieve from the following models: - - res.partner, joined on the partner_id of the membership - - knowledge.article, joined on the 'origin' of the membership - (when the membership is based on one of its parent article) - to retrieve more fields on the origin of the membership - - knowledge.article.member to retrieve more fields on the membership - - The expected format is:: - - {'model': [('field', 'field_alias')]} - e.g:: - { - 'res.partner': [ - ('name', 'partner_name'), - ('email', 'partner_email'), - ] - } - - Please note that these additional fields are not sanitized, the caller - has the responsibility to check that user can access those fields and - that no injection is possible. """ - self.env['res.partner'].flush_model() - self.env['knowledge.article'].flush_model() - self.env['knowledge.article.member'].flush_model() - - add_where_clause = '' - args = [] - if self.ids: - args = [tuple(self.ids)] - add_where_clause += " AND article_id in %s" - - additional_select_fields = '' - join_clause = '' - if additional_fields: - supported_additional_models = [ - 'res.partner', - 'knowledge.article', - 'knowledge.article.member', - ] - - # 1. build the join clause based on the given models (additional_fields keys) - join_clauses = [] - for model in additional_fields.keys(): - if model not in supported_additional_models: - continue - - table_name = self.env[model]._table - join_condition = '' - if model == 'res.partner': - join_condition = f'{table_name}.id = partner_id' - elif model == 'knowledge.article': - join_condition = f'{table_name}.id = origin_id' - elif model == 'knowledge.article.member': - join_condition = f'{table_name}.id = member_id' - - join_clauses.append(f'LEFT OUTER JOIN {table_name} ON {join_condition}') - - join_clause = ' '.join(join_clauses) - - # 2. build the select clause based on the given fields/aliases pairs - # (additional_fields values) - select_fields = [] - for model, fields_list in additional_fields.items(): - if model not in supported_additional_models: - continue - - table_name = self.env[model]._table - for (field, field_alias) in fields_list: - select_fields.append(f'{table_name}.{field} as {field_alias}') - - additional_select_fields = ', %s' % ', '.join(select_fields) - - sql = f''' - WITH article_permission as ( - WITH RECURSIVE article_perms as ( - SELECT a.id, a.parent_id, a.is_desynchronized, m.id as member_id, - m.partner_id, m.permission - FROM knowledge_article a - LEFT JOIN knowledge_article_member m - ON a.id = m.article_id - ), article_rec as ( - SELECT perms1.id, perms1.id as article_id, perms1.parent_id, - perms1.member_id, perms1.partner_id, perms1.permission, - perms1.id as origin_id, 0 as level, - perms1.is_desynchronized - FROM article_perms as perms1 - UNION - SELECT perms2.id, perms_rec.article_id, perms2.parent_id, - perms2.member_id, perms2.partner_id, perms2.permission, - perms2.id as origin_id, perms_rec.level + 1, - perms2.is_desynchronized - FROM article_perms as perms2 - INNER JOIN article_rec perms_rec - ON perms_rec.parent_id=perms2.id - AND perms_rec.is_desynchronized is not true - ) - SELECT article_id, origin_id, member_id, partner_id, - permission, min(level) as min_level - FROM article_rec - WHERE partner_id is not null - {add_where_clause} - GROUP BY article_id, origin_id, member_id, partner_id, permission - ) - SELECT article_id, origin_id, member_id, partner_id, permission, min_level - {additional_select_fields} - FROM article_permission - {join_clause} - ''' - - self._cr.execute(sql, args) - results = self._cr.dictfetchall() - - # Now that we have, for each article, all the members found on themselves and their parents. - # We need to keep only the first partners found (lowest level) for each article - article_members = defaultdict(dict) - min_level_dict = defaultdict(dict) - - _nolevel = -1 - for result in results: - article_id = result['article_id'] - origin_id = result['origin_id'] - partner_id = result['partner_id'] - level = result['min_level'] - min_level = min_level_dict[article_id].get(partner_id, _nolevel) - if min_level == _nolevel or level < min_level: - article_members[article_id][partner_id] = { - 'member_id': result['member_id'], - 'based_on': origin_id if origin_id != article_id else False, - 'permission': result['permission'] - } - min_level_dict[article_id][partner_id] = level - - if additional_fields: - # update our resulting dict based on additional fields - article_members[article_id][partner_id].update({ - field_alias: result[field_alias] if model != 'knowledge.article' or origin_id != article_id else False - for model, fields_list in additional_fields.items() - for (field, field_alias) in fields_list - }) - # add empty member for each article that doesn't have any. - for article in self: - if article.id not in article_members: - article_members[article.id][None] = {'based_on': False, 'member_id': False, 'permission': None} - - if additional_fields: - # update our resulting dict based on additional fields - article_members[article.id][None].update({ - field_alias: False - for model, fields_list in additional_fields.items() - for (field, field_alias) in fields_list - }) - - return article_members - - # ------------------------------------------------------------ - # MAILING - # ------------------------------------------------------------ - - def _mail_track(self, tracked_fields, initial): - changes, tracking_value_ids = super(Article, self)._mail_track(tracked_fields, initial) - if {'parent_id', 'root_article_id'} & changes and not self.user_has_write_access: - partner_name = self.env.user.partner_id.display_name - message_body = _("Logging changes from %(partner_name)s without write access on article %(article_name)s due to hierarchy tree update", - partner_name=partner_name, article_name=self.display_name) - self._track_set_log_message("

%s

" % message_body) - return changes, tracking_value_ids - - def _send_invite_mail(self, partners): - # TDE NOTE: try to cleanup and batchize - self.ensure_one() - for partner in partners: - subject = _("Invitation to access %s", self.name) - partner_lang = get_lang(self.env, lang_code=partner.lang).code - body = self.env['ir.qweb'].with_context(lang=partner_lang)._render( - 'knowledge.knowledge_article_mail_invite', { - 'record': self, - 'user': self.env.user, - 'recipient': partner, - 'link': self._get_invite_url(partner), - }) - - self.with_context(lang=partner_lang).message_notify( - body=body, - email_layout_xmlid='mail.mail_notification_light', - partner_ids=partner.ids, - subject=subject, - ) - - # ------------------------------------------------------------ - # TOOLS - # ------------------------------------------------------------ - - def _get_invite_url(self, partner): - self.ensure_one() - member = self.env['knowledge.article.member'].search([('article_id', '=', self.id), ('partner_id', '=', partner.id)]) - return url_join(self.get_base_url(), "/knowledge/article/invite/%s/%s" % (member.id, member._get_invitation_hash())) - - def _get_first_accessible_article(self): - """ Returns the first accessible article for the current user. - If user has favorites, return first favorite article. """ - article = self.env['knowledge.article'] - if not self.env.user._is_public(): - article = self.env['knowledge.article.favorite'].search([ - ('user_id', '=', self.env.uid), ('article_id.active', '=', True) - ], limit=1).article_id - if not article: - # retrieve workspace articles first, then private/shared ones. - article = self.search([ - ('parent_id', '=', False) - ], limit=1, order='sequence, internal_permission desc') - return article - - def get_valid_parent_options(self, search_term=""): - """ Returns the list of articles that can be set as parent for the - current article (to avoid recursions) """ - return self.search_read( - domain=['&', - ['name', 'ilike', search_term], - ['id', 'not in', (self._get_descendants() + self).ids] - ], - fields=['id', 'display_name', 'root_article_id'], - limit=15, - ) - - def _get_descendants(self): - """ Returns the descendants recordset of the current article. """ - return self.env['knowledge.article'].search([('id', 'not in', self.ids), ('parent_id', 'child_of', self.ids)]) - - def _get_readable_ancetors(self): - """ Returns the parents recordset of the current article. Do the computation - as sudo """ - self.ensure_one() - ancestors = self.env['knowledge.article'].sudo() - current = self.sudo().parent_id - while current: - ancestors += current - current = current.parent_id - return ancestors._filter_access_rules_python('read').with_env(self.env) diff --git a/addons/knowledge/models/knowledge_article_favorite.py b/addons/knowledge/models/knowledge_article_favorite.py deleted file mode 100644 index 8748e6f0ed7..00000000000 --- a/addons/knowledge/models/knowledge_article_favorite.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, exceptions, fields, models, _ - - -class ArticleFavorite(models.Model): - _name = 'knowledge.article.favorite' - _description = 'Favorite Article' - _order = 'sequence ASC, id DESC' - _rec_name = 'article_id' - - article_id = fields.Many2one( - 'knowledge.article', 'Article', - index=True, required=True, ondelete='cascade') - user_id = fields.Many2one( - 'res.users', 'User', - index=True, required=True, ondelete='cascade') - sequence = fields.Integer(default=0) - - _sql_constraints = [ - ('unique_article_user', - 'unique(article_id, user_id)', - 'User already has this article in favorites.') - ] - - @api.model_create_multi - def create(self, vals_list): - """ At creation, we need to set the max sequence, if not given, for each favorite to create, in order to keep - a correct ordering as much as possible. Some sequence could be given in create values, that could lead to - duplicated sequence per user_id. That is not an issue as they will be resequenced the next time the user reorder - their favorites. """ - # TDE TODO: env.uid -> user_id - default_sequence = 1 - if any(not vals.get('sequence') for vals in vals_list): - favorite = self.env['knowledge.article.favorite'].search( - [('user_id', '=', self.env.uid)], - order='sequence DESC', - limit=1 - ) - default_sequence = favorite.sequence + 1 if favorite else default_sequence - for vals in vals_list: - if not vals.get('sequence'): - vals['sequence'] = default_sequence - default_sequence += 1 - return super(ArticleFavorite, self).create(vals_list) - - def write(self, vals): - """ Whatever rights, avoid any attempt at privilege escalation. """ - if ('article_id' in vals or 'user_id' in vals) and not self.env.is_admin(): - raise exceptions.AccessError(_("Can not update the article or user of a favorite.")) - return super().write(vals) diff --git a/addons/knowledge/models/knowledge_article_member.py b/addons/knowledge/models/knowledge_article_member.py deleted file mode 100644 index 8f633bb0716..00000000000 --- a/addons/knowledge/models/knowledge_article_member.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, tools, _ -from odoo.exceptions import AccessError, ValidationError - - -class ArticleMember(models.Model): - _name = 'knowledge.article.member' - _description = 'Article Member' - _rec_name = 'partner_id' - - article_id = fields.Many2one( - 'knowledge.article', 'Article', - index=True, ondelete='cascade', required=True) - partner_id = fields.Many2one( - 'res.partner', 'Partner', - index=True, ondelete='cascade', required=True) - permission = fields.Selection( - [('write', 'Can write'), - ('read', 'Can read'), - ('none', 'No access')], - required=True, default='read') - article_permission = fields.Selection( - related='article_id.inherited_permission', - readonly=True, store=True) - - _sql_constraints = [ - ('unique_article_partner', - 'unique(article_id, partner_id)', - 'You already added this partner on this article.') - ] - - @api.constrains('article_permission', 'permission') - def _check_is_writable(self, on_unlink=False): - """ Articles must always have at least one writer. This constraint is done - on member level, in coordination to the constraint on article model (see - ``_check_is_writable`` on ``knowledge.article``). - - Since this constraint only triggers if we have at least one member another - validation is done on article model. The article_permission related field - has been added and stored to force triggering this constraint when - article.permission is modified. - - Ǹote: computation is done in Py instead of using optimized SQL queries - because value are not yet in DB at this point. - - :param bool on_unlink: when called on unlink we must remove the members - in self (the ones that will be deleted) to check if one of the remaining - members has write access. - """ - if self.env.context.get('knowledge_member_skip_writable_check'): - return - - articles_to_check = self.article_id.filtered(lambda a: a.inherited_permission != 'write') - if not articles_to_check: - return - - if on_unlink: - deleted_members_by_article = dict.fromkeys(articles_to_check.ids, self.env['knowledge.article.member']) - for member in self.filtered(lambda member: member.article_id in articles_to_check): - deleted_members_by_article[member.article_id.id] |= member - - for article in articles_to_check: - # Check on permission on members - members_to_check = article.article_member_ids - if on_unlink: - members_to_check -= deleted_members_by_article[article.id] - if any(m.permission == 'write' for m in members_to_check): - continue - - members_to_exclude = deleted_members_by_article[article.id] if on_unlink else False - if not article._has_write_member(members_to_exclude=members_to_exclude): - raise ValidationError( - _("Article '%s' should always have a writer: inherit write permission, or have a member with write access", - article.display_name) - ) - - @api.constrains('partner_id', 'permission') - def _check_external_member_permission(self): - for member in self.filtered(lambda member: member.permission == 'write'): - if member.partner_id.partner_share: - raise ValidationError( - _("The external user %(user_name)s cannot have a 'write' permission on article %(article_name)s", - user_name=member.partner_id.display_name, - article_name=member.article_id.display_name - )) - - def init(self): - self._cr.execute("CREATE INDEX IF NOT EXISTS knowledge_article_member_article_partner_idx ON knowledge_article_member (article_id, partner_id)") - - def write(self, vals): - """ Whatever rights, avoid any attempt at privilege escalation. """ - if ('article_id' in vals or 'partner_id' in vals) and not self.env.is_admin(): - raise AccessError(_("Can not update the article or partner of a member.")) - return super().write(vals) - - @api.ondelete(at_uninstall=False) - def _unlink_except_no_writer(self): - """ When removing a member, the constraint is not triggered. - We need to check manually on article with no write permission that we do not remove the last write member """ - self._check_is_writable(on_unlink=True) - - def _get_invitation_hash(self): - """ We use a method instead of a field in order to reduce DB space.""" - self.ensure_one() - return tools.hmac(self.env(su=True), - 'knowledge-article-invite', - f'{self.id}-{self.create_date}-{self.partner_id.id}-{self.article_id.id}' - ) diff --git a/addons/knowledge/models/res_partner.py b/addons/knowledge/models/res_partner.py deleted file mode 100644 index a2142e9c413..00000000000 --- a/addons/knowledge/models/res_partner.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - - -class Partner(models.Model): - _name = 'res.partner' - _inherit = 'res.partner' - - def unlink(self): - """ This override will delete all the private articles linked to the deleted partners. """ - self.env['knowledge.article.member'].sudo().search( - [('partner_id', 'in', self.ids), ('article_id.category', '=', 'private')] - ).article_id.unlink() - return super(Partner, self).unlink() diff --git a/addons/knowledge/models/res_users.py b/addons/knowledge/models/res_users.py deleted file mode 100644 index d401b707ad4..00000000000 --- a/addons/knowledge/models/res_users.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, api -from odoo.tools.translate import _lt - - -class Users(models.Model): - _name = 'res.users' - _inherit = 'res.users' - @api.model_create_multi - def create(self, vals_list): - users = super(Users, self).create(vals_list) - if not self.env.context.get('knowledge_skip_onboarding_article'): - users.filtered(lambda user: not user.partner_share)._generate_tutorial_articles() - return users - - def _generate_tutorial_articles(self): - articles_to_create = [] - for user in self: - self = self.with_context(lang=user.lang or self.env.user.lang) - render_ctx = {'object': user} - body = self.env['ir.qweb']._render( - 'knowledge.knowledge_article_user_onboarding', - render_ctx, - minimal_qcontext=True, - raise_if_not_found=False - ) - if not body: - break - - welcome = _lt('Welcome %s', user.name) - articles_to_create.append({ - 'article_member_ids': [(0, 0, { - 'partner_id': user.partner_id.id, - 'permission': 'write', - })], - 'body': body, - 'icon': "👋", - 'internal_permission': 'none', - 'favorite_ids': [(0, 0, { - 'sequence': 0, - 'user_id': user.id, - })], - 'name': welcome, - }) - - if articles_to_create: - self.env['knowledge.article'].sudo().create(articles_to_create) diff --git a/addons/knowledge/security/ir.model.access.csv b/addons/knowledge/security/ir.model.access.csv deleted file mode 100644 index c3d7cc81eea..00000000000 --- a/addons/knowledge/security/ir.model.access.csv +++ /dev/null @@ -1,16 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_knowledge_article_all,access.knowledge.article.all,knowledge.model_knowledge_article,,0,0,0,0 -access_knowledge_article_portal,access.knowledge.article.portal,knowledge.model_knowledge_article,base.group_portal,1,0,0,0 -access_knowledge_article_user,access.knowledge.article.user,knowledge.model_knowledge_article,base.group_user,1,1,1,0 -access_knowledge_article_system,access.knowledge.article.system,knowledge.model_knowledge_article,base.group_system,1,1,1,1 -access_knowledge_article_member_all,access.knowledge.article.member.all,knowledge.model_knowledge_article_member,,0,0,0,0 -access_knowledge_article_member_portal,access.knowledge.article.member.portal,knowledge.model_knowledge_article_member,base.group_portal,1,0,0,0 -access_knowledge_article_member_user,access.knowledge.article.member.user,knowledge.model_knowledge_article_member,base.group_user,1,0,0,0 -access_knowledge_article_member_system,access.knowledge.article.member.system,knowledge.model_knowledge_article_member,base.group_system,1,1,1,1 -access_knowledge_article_favorite_all,access.knowledge.article.favorite.all,knowledge.model_knowledge_article_favorite,,0,0,0,0 -access_knowledge_article_favorite_portal,access.knowledge.article.favorite.portal,knowledge.model_knowledge_article_favorite,base.group_portal,1,1,1,1 -access_knowledge_article_favorite_user,access.knowledge.article.favorite.user,knowledge.model_knowledge_article_favorite,base.group_user,1,1,1,1 -access_knowledge_article_favorite_system,access.knowledge.article.favorite.system,knowledge.model_knowledge_article_favorite,base.group_system,1,1,1,1 -access_knowledge_invite_all,access.knowledge.invite.all,knowledge.model_knowledge_invite,,0,0,0,0 -access_knowledge_invite_user,access.knowledge.invite.user,knowledge.model_knowledge_invite,base.group_user,1,1,1,0 -access_knowledge_invite_system,access.knowledge.invite.system,knowledge.model_knowledge_invite,base.group_system,1,1,1,1 diff --git a/addons/knowledge/security/ir_rule.xml b/addons/knowledge/security/ir_rule.xml deleted file mode 100644 index 59cf66d460b..00000000000 --- a/addons/knowledge/security/ir_rule.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - Articles: System = CRUD on all articles - - [(1, '=', 1)] - - - - - Articles: users/portal: read based on access - - [('user_has_access', '=', True)] - - - - - - - - Articles: users/portal: write based on flag - - [('user_has_write_access', '=', True)] - - - - - - - Article members: users: read article members - - [('article_id.user_has_access', '=', True)] - - - - - - - - Article members: portal: read own article members - - [('partner_id', '=', user.partner_id.id), ('article_id.user_has_access', '=', True)] - - - - - - - - Article members: System CRUD all - - [(1,'=',1)] - - - - - - Article favorite: users/portal: own + readable articles - - [('user_id', '=', user.id), ('article_id.user_has_access', '=', True)] - - - - - Article favorite: System CRUD all - - [(1, '=', 1)] - - - - - - Invite: System invite members - - [('article_id.user_has_write_access', '=', True)] - - - diff --git a/addons/knowledge/static/description/icon.png b/addons/knowledge/static/description/icon.png deleted file mode 100644 index 94d8d546a64a2db3ee27b16ea5dcafc903f5f467..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27374 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 A+5i9m diff --git a/addons/knowledge/static/src/components/emoji_picker/emoji_list.xml b/addons/knowledge/static/src/components/emoji_picker/emoji_list.xml deleted file mode 100644 index 86d1d0f892c..00000000000 --- a/addons/knowledge/static/src/components/emoji_picker/emoji_list.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - -
- -
-
-
- -
diff --git a/addons/knowledge/static/src/components/emoji_picker/emoji_picker.js b/addons/knowledge/static/src/components/emoji_picker/emoji_picker.js deleted file mode 100644 index bdee20d72ea..00000000000 --- a/addons/knowledge/static/src/components/emoji_picker/emoji_picker.js +++ /dev/null @@ -1,60 +0,0 @@ -/** @odoo-module **/ - -import emojis from '@mail/js/emojis'; -const { Component } = owl; - -class LegacyEmoji extends Component { - /** - * @override - */ - setup () { - // Mock the template variables: - this.className = ''; - this.emojiListView = this.props.emojiListView; - } -} - -LegacyEmoji.template = 'mail.LegacyEmoji'; -LegacyEmoji.props = { - emojiView: Object -}; - -class EmojiPicker extends Component { - /** - * @override - */ - setup () { - // Mock the template variables: - this.className = ''; - const viewEventHandlers = { - /** - * @param {Event} event - */ - onClickEmoji: event => { - this.props.onClickEmoji(event.target.dataset.unicode); - }, - }; - this.emojiListView = { - emojiViews: emojis.map((emoji, index) => { - return { - localId: index, - emoji: emoji, - emojiListView: viewEventHandlers - }; - }) - }; - } - - /** - * @param {Event} event - */ - onRemoveEmoji (event) { - this.props.onClickEmoji(false); - } -} - -EmojiPicker.template = 'knowledge.LegacyEmojiList'; -EmojiPicker.props = ['onClickEmoji']; -EmojiPicker.components = { LegacyEmoji }; - -export default EmojiPicker; diff --git a/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js b/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js deleted file mode 100644 index 13be1ac4d6f..00000000000 --- a/addons/knowledge/static/src/components/media_dialog/knowledge_documents_selector.js +++ /dev/null @@ -1,70 +0,0 @@ -/** @odoo-module */ - -import { qweb as QWeb } from 'web.core'; -import { DocumentSelector } from '@web_editor/components/media_dialog/document_selector'; -import { TABS, MediaDialog } from '@web_editor/components/media_dialog/media_dialog'; -import { patch } from '@web/core/utils/patch'; - -/** - * Override the @see DocumentSelector to manage files in a @see MediaDialog used - * by the /file command. The purpose of this override is to redefine the - * rendering of the media (/file block), and to merge images in the documents - * tab of the MediaDialog, since the /file block displays a default mimetype for - * every files. - */ -export class KnowledgeDocumentSelector extends DocumentSelector { - /** - * Filter files for the documents tab of the MediaDialog. Any file with a - * mimetype is valid. (images and documents are displayed together) - * - * @override - */ - get attachmentsDomain() { - const domain = super.attachmentsDomain; - return domain.map(d => { - if (d[0] === 'mimetype') { - return ['mimetype', '!=', false]; - } - return d; - }); - } - - static async createElements(...args) { - const files = await DocumentSelector.createElements(...args); - return files.map(file => { - const extension = (file.title && file.title.split('.').pop()) || file.dataset.mimetype; - const fileEl = $(QWeb.render('knowledge.file_block', { - img: { - name: file.title, - extension: extension, - }, - }))[0]; - file.classList.add(...DocumentSelector.mediaSpecificClasses); - fileEl.querySelector('.o_knowledge_file_image').replaceChildren(file); - return fileEl; - }); - } -} -KnowledgeDocumentSelector.mediaSpecificClasses = []; -KnowledgeDocumentSelector.tagNames = DocumentSelector.tagNames; - -patch(TABS, 'knowledge_media_dialog_tabs', { - KNOWLEDGE_DOCUMENTS: { - ...TABS.DOCUMENTS, - id: 'KNOWLEDGE_DOCUMENTS', - Component: KnowledgeDocumentSelector, - } -}); - -patch(MediaDialog.prototype, 'knowledge_media_dialog', { - /** - * @override - */ - addTabs() { - this._super(...arguments); - - if (this.props.knowledgeDocuments) { - this.addTab(TABS.KNOWLEDGE_DOCUMENTS); - } - } -}); diff --git a/addons/knowledge/static/src/components/permission_panel/permission_panel.js b/addons/knowledge/static/src/components/permission_panel/permission_panel.js deleted file mode 100644 index dfde08f6faa..00000000000 --- a/addons/knowledge/static/src/components/permission_panel/permission_panel.js +++ /dev/null @@ -1,327 +0,0 @@ -/** @odoo-module **/ - -import { session } from "@web/session"; -import Dialog from 'web.Dialog'; -import { _lt } from "@web/core/l10n/translation"; -import { _t } from 'web.core'; -import { useService } from '@web/core/utils/hooks'; - -const { Component, onWillStart, useState } = owl; -const permissionLevel = {'none': 0, 'read': 1, 'write': 2} -const restrictMessage = _lt("Are you sure you want to restrict this role and restrict access ? " -+ "This article will no longer inherit access settings from the parent page."); -const loseWriteMessage = _lt('Are you sure you want to remove you own "Write" access ?'); - -class PermissionPanel extends Component { - /** - * @override - */ - setup () { - this.rpc = useService('rpc'); - this.state = useState({ - loading: true, - partner_id: session.partner_id - }) - onWillStart(this.loadPanel); - } - - async loadPanel () { - const data = await this.loadData(); - this.state = { - ...this.state, - ...data, - loading: false - }; - this.render(); - this._showPanel(); - } - - /** - * @returns {Object} - */ - loadData () { - return this.rpc({ - route: '/knowledge/get_article_permission_panel_data', - params: { - article_id: this.props.article_id - } - }); - } - - /** - * @returns {Array[Array]} - */ - getInternalPermissionOptions () { - return this.state.internal_permission_options; - } - - /** - * @param {Proxy} member - * @returns {Boolean} - */ - isLoggedUser (member) { - return member.partner_id === session.partner_id; - } - - /** - * Opens the article with the given id - * @param {integer} id - */ - openArticle (id) { - this.env.bus.trigger('do-action', { - action: 'knowledge.ir_actions_server_knowledge_home_page', - options: { - additional_context: { - res_id: id - } - } - }); - } - - /** - * Callback function called when the internal permission of the article changes. - * @param {Event} event - */ - _onChangeInternalPermission (event) { - const $select = $(event.target); - const index = this.state.members.findIndex(current => { - return current.partner_id === session.partner_id; - }); - const newPermission = $select.val(); - const oldPermission = this.state.internal_permission; - const willRestrict = this.state.based_on && permissionLevel[newPermission] < permissionLevel[oldPermission] - && permissionLevel[newPermission] < permissionLevel[this.state.parent_permission]; - const willLoseAccess = $select.val() === 'none' && (index >= 0 && this.state.members[index].permission === 'none'); - const confirm = () => { - this.rpc({ - route: '/knowledge/article/set_internal_permission', - params: { - article_id: this.props.article_id, - permission: newPermission, - } - }).then(res => { - if (this._onChangedPermission(res, willLoseAccess)) { - this.loadPanel(); - } - }); - }; - - if (!willLoseAccess && !willRestrict) { - confirm(); - return; - } - - const discard = () => { - $select.val(oldPermission); - this.loadPanel(); - }; - const loseAccessMessage = _t('Are you sure you want to set the internal permission to "none" ? If you do, you will no longer have access to the article.'); - this._showConfirmDialog(willLoseAccess ? loseAccessMessage : restrictMessage, confirm, discard); - } - - /** - * Callback function called when the permission of a user changes. - * @param {Event} event - * @param {Proxy} member - */ - _onChangeMemberPermission (event, member) { - const index = this.state.members.indexOf(member); - if (index < 0) { - return; - } - const $select = $(event.target); - const newPermission = $select.val(); - const oldPermission = member.permission; - const willLoseAccess = this.isLoggedUser(member) && newPermission === 'none'; - const willRestrict = this.state.based_on && permissionLevel[newPermission] < permissionLevel[oldPermission]; - const willLoseWrite = this.isLoggedUser(member) && newPermission !== 'write' && oldPermission === 'write'; - const confirm = () => { - this.rpc({ - route: '/knowledge/article/set_member_permission', - params: { - article_id: this.props.article_id, - permission: newPermission, - member_id: member.based_on ? false : member.id, - inherited_member_id: member.based_on ? member.id: false, - } - }).then(res => { - const reloadArticleId = willLoseWrite && !willLoseAccess ? this.props.article_id : false; - if (this._onChangedPermission(res, willLoseAccess||willLoseWrite, reloadArticleId)) { - this.loadPanel(); - } - }); - }; - - if (!willLoseAccess && !willRestrict && !willLoseWrite) { - confirm(); - return; - } - - const discard = () => { - $select.val(this.state.members[index].permission); - this.loadPanel(); - }; - const loseAccessMessage = _t('Are you sure you want to set your permission to "none"? If you do, you will no longer have access to the article.'); - const message = willLoseAccess ? loseAccessMessage : willLoseWrite ? loseWriteMessage : loseAccessMessage; - this._showConfirmDialog(message, confirm, discard); - } - - /** - * @param {Event} event - * @param {integer} id - article id - */ - _onOpen (event, id) { - event.preventDefault(); - this.openArticle(id); - } - - /** - * Callback function called when a member is removed. - * @param {Event} event - * @param {Proxy} member - */ - _onRemoveMember (event, member) { - if (!this.state.members.includes(member)) { - return; - } - const willRestrict = member.based_on ? true : false; - const willLoseAccess = this.isLoggedUser(member); - const confirm = () => { - this.rpc({ - route: '/knowledge/article/remove_member', - params: { - article_id: this.props.article_id, - member_id: member.based_on ? false : member.id, - inherited_member_id: member.based_on ? member.id: false, - } - }).then(res => { - if (this._onChangedPermission(res, willLoseAccess)) { - this.loadPanel(); - } - }); - }; - - if (!willLoseAccess && !willRestrict) { - confirm(); - return; - } - - const loseAccessMessage = _t('Are you sure you want to leave this article? If you do, you will no longer have access to the article.'); - const message = willLoseAccess ? loseAccessMessage : restrictMessage; - const discard = () => { - this.loadPanel(); - }; - this._showConfirmDialog(message, confirm, discard); - } - - /** - * Callback function called when user clicks on 'Restore' button. - * @param {Event} event - */ - _onRestore (event) { - const articleId = this.props.article_id; - const confirm = () => { - this.rpc({ - model: 'knowledge.article', - method: 'restore_article_access', - args: [[articleId]], - }).then(res => { - if (res) { - if (this._onChangedPermission({success: res})) { - this.loadPanel(); - } - } - }); - }; - - const message = _t('Are you sure you want to restore access?'); - this._showConfirmDialog(message, confirm); - } - - /** - * @param {Event} event - * @param {Proxy} member - */ - async _onMemberAvatarClick (event, member) { - if (!member.partner_share) { - const partnerRead = await this.rpc({ - model: 'res.partner', - method: 'read', - args: [member.partner_id, ['user_ids']], - }); - const userIds = partnerRead && partnerRead.length === 1 ? partnerRead[0]['user_ids'] : false; - const userId = userIds && userIds.length === 1 ? userIds[0] : false; - - if (userId) { - const messaging = await this.env.services.messaging.get(); - messaging.openChat({ - userId: userId - }); - } - } - } - - /** - * This method is called before each permission change rpc when the user needs to confirm the change as them - * would lose them access to the article if them do confirm. - * @param {str} message - * @param {function} confirm - * @param {function} discard - */ - _showConfirmDialog (message, confirm, discard) { - if (discard === undefined) { - discard = this.loadPanel; - } - Dialog.confirm(this, message, { - buttons: [{ - text: _t('confirm'), - classes: 'btn-primary', - close: true, - click: confirm - }, { - text: _t('Discard'), - close: true, - click: discard - }], - }); - } - - /** - * This method is called after each permission change rpc. - * It will check if a reloading of the article tree or a complete reload is needed in function - * of the new article state (if change of category or if user lost their own access to the current article). - * return True if the caller should continue after executing this method, and False, if caller should stop. - * @param {Dict} result - * @param {Boolean} lostAccess - */ - _onChangedPermission (result, reloadAll, reloadArticleId) { - if (result.error) { - Dialog.alert(this, result.error,{ - title: _t("Error"), - }); - } else if (reloadAll && reloadArticleId) { // Lose write access - this.openArticle(reloadArticleId); - return false; - } else if (reloadAll) { // Lose access -> Hard Reload - window.location.replace('/knowledge/home'); - } else if (result.reload_tree) { - this.env.bus.trigger('reload_tree', {}); - } - return true; - } - - _showPanel () { - // TODO DBE: get permission panel with owl brol ?? - const $permissionPanel = $('.o_knowledge_share_panel'); - $permissionPanel.addClass('show'); - $permissionPanel.parent().addClass('show'); - } -} - -PermissionPanel.template = 'knowledge.PermissionPanel'; -PermissionPanel.props = [ - 'article_id', - 'user_permission' -]; - -export default PermissionPanel; diff --git a/addons/knowledge/static/src/components/permission_panel/permission_panel.scss b/addons/knowledge/static/src/components/permission_panel/permission_panel.scss deleted file mode 100644 index f538a990f6c..00000000000 --- a/addons/knowledge/static/src/components/permission_panel/permission_panel.scss +++ /dev/null @@ -1,39 +0,0 @@ -.o_knowledge_permission_panel { - .o_m2m_avatar, - .o_company_icon { - width: 30px; - height: 30px; - } - .active { - background: #cceaf9; - } - .text-muted { - font-size: 0.85rem; - } - - .o_member_email { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - max-width: 150px; - } - - .o_delete { - display: none; - } - div:hover { - .o_delete { - display: inline; - } - } - - .o_permission { - &.readonly { - background: white; - cursor: default; - color: black; - border-color: #787878; - } - width:100px; - } -} diff --git a/addons/knowledge/static/src/components/permission_panel/permission_panel.xml b/addons/knowledge/static/src/components/permission_panel/permission_panel.xml deleted file mode 100644 index a2d85dc1289..00000000000 --- a/addons/knowledge/static/src/components/permission_panel/permission_panel.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - -
- -
-
- -
- Access Restricted. May not be shared with everyone from - - -
- - -
-
- -
-
- - -
- As an administrator, you can always modify this article. -
- - - diff --git a/addons/knowledge/static/src/img/fleet_icon.png b/addons/knowledge/static/src/img/fleet_icon.png deleted file mode 100644 index 20f5a083f0e986cce25038915ee15512663fd78e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5702 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 diff --git a/addons/knowledge/static/src/js/field_html.js b/addons/knowledge/static/src/js/field_html.js deleted file mode 100644 index c91b3fe4531..00000000000 --- a/addons/knowledge/static/src/js/field_html.js +++ /dev/null @@ -1,85 +0,0 @@ -/** @odoo-module */ - -import FieldHtml from 'web_editor.field.html'; -import {FieldHtmlInjector} from './knowledge_field_html_injector'; -import {KnowledgePlugin} from './knowledge_plugin'; - -FieldHtml.include({ - /** - * @override - */ - _renderReadonly: function () { - const prom = this._super.apply(this, arguments); - if (prom) { - return prom.then(function () { - return this._addFieldHtmlInjector(); - }.bind(this)); - } else { - return this._addFieldHtmlInjector(); - } - }, - /** - * Add a @see FieldHtmlInjector to this field_html, which will add temporary - * @see KnowledgeBehavior and/or @see KnowledgeToolbar to @see OdooEditor - * blocks. Delegate control of the editor to the injector, mainly to access - * history methods. i.e.: @see KnowledgeToolbar insertion in the dom should - * not be registered in the user history - * - * @returns {Promise} - */ - _addFieldHtmlInjector: function () { - if (this.$content && this.$content.length) { - const editor = (this.mode === 'edit' && this.wysiwyg) ? this.wysiwyg.odooEditor : null; - const fieldHtmlInjector = new FieldHtmlInjector(this, this.mode, this.$content[0], editor); - return fieldHtmlInjector.appendTo(this.el); - } - }, - /** - * Some events can cause the insertion of new elements which may need a - * @see KnowledgeBehavior and/or @see KnowledgeToolbar , so the - * @see FieldHtmlInjector needs to be notified. - * - * @override - */ - _onLoadWysiwyg: function () { - this._super.apply(this, arguments); - this._addFieldHtmlInjector(); - /** - * setTimeout is called so that the refresh_injector event is dispatched - * at the start of the next event cycle. This is to ensure that other - * handlers for those events have execution priority over the refresh. - * In particular, the content to drop in the editor would be inserted - * after the refreshInjector without setTimeout. - */ - const refreshInjector = () => setTimeout(this.refreshInjector.bind(this)); - this.wysiwyg.odooEditor.addEventListener('historyUndo', refreshInjector); - this.wysiwyg.odooEditor.addEventListener('historyRedo', refreshInjector); - this.$content[0].addEventListener('paste', refreshInjector); - this.$content[0].addEventListener('drop', refreshInjector); - }, - /** - * Add a plugin (@see KnowledgePlugin ) for the @see OdooEditor which - * implements @see cleanForSave in order to remove @see KnowledgeToolbar - * before saving a record. - * - * @override - */ - _getWysiwygOptions: function () { - const options = this._super.apply(this, arguments); - if (Array.isArray(options.editorPlugins)) { - options.editorPlugins.push(KnowledgePlugin); - } else { - options.editorPlugins = [KnowledgePlugin]; - } - return options; - }, - /** - * Notify the @see FieldHtmlInjector to render and apply KnowledgeToolbar - * and KnowledgeBehavior where it is needed. - */ - refreshInjector: function () { - if (this.$content) { - this.$content.trigger('refresh_injector'); - } - }, -}); diff --git a/addons/knowledge/static/src/js/form_controller.js b/addons/knowledge/static/src/js/form_controller.js deleted file mode 100644 index bde245e2e8b..00000000000 --- a/addons/knowledge/static/src/js/form_controller.js +++ /dev/null @@ -1,192 +0,0 @@ -/** @odoo-module **/ - -import FormController from 'web.FormController'; -import { Domain } from '@web/core/domain'; - -FormController.include({ - /** - * Knowledge articles can interact with some records with the help of the - * @see KnowledgeService. Those records need to have a field whose name is - * in @see knowledgeTriggerFieldNames. This list is ordered and the first - * match found in a record will take precedence. Once a match is found, it - * is stored in the KnowledgeService to be accessed later by an article. - * - * @override - */ - init: function (parent, model, renderer, params) { - this._super.apply(this, arguments); - this.knowledgeRecordFieldNames = [ - 'note', 'memo', 'description', 'comment', 'narration', 'additional_note', 'internal_notes', 'notes' - ]; - if (this._isPotentialKnowledgeRecord()) { - this._searchKnowledgeRecord(); - } - if (this.ignoreKnowledgeRecordSearch) { - this._unregisterObsoleteKnowledgeRecords(this.renderer.breadcrumbs); - } - }, - /** - * Since Knowledge toolbars are cleaned before saving the record - * @see KnowledgePlugin , the renderer needs to render them again once it is - * done. - * - * @override - * @param {string} recordID - */ - saveRecord: async function (recordID) { - const changedFields = this._super(...arguments); - recordID = recordID || this.handle; - if (recordID) { - changedFields.then(() => { - this.renderer.refreshFieldHtmlInjectors(recordID); - }); - } - return changedFields; - }, - /** - * Notify the @see KnowledgeService that the record was updated - * - * @override - */ - update: async function (params, options) { - await this._super(...arguments); - if (this._isPotentialKnowledgeRecord()) { - this._searchKnowledgeRecord(); - } - if (this.ignoreKnowledgeRecordSearch) { - this._unregisterObsoleteKnowledgeRecords(this.renderer.breadcrumbs); - } - }, - /** - * Evaluate the provided breadcrumbs sequence against currently recorded - * records in @see KnowledgeService to remove them if they are obsolete. - * Breadcrumb information does not contain the res_id of the record for a - * form view, and the actionService does not allow to access the breadcrumb - * controller externally. This is why the full breadcrumb string sequence is - * used here as an identifier. - * - * @param {Array} breadcrumbs - * @param {boolean} revoke whether the record is deemed obsolete if a match - * is found or not. - */ - _unregisterObsoleteKnowledgeRecords: function (breadcrumbs, revoke = false) { - const records = this.call('knowledgeService', 'getRecords'); - let isObsolete = revoke; - for (let record of records) { - if (record.breadcrumbs.length > breadcrumbs.length) { - isObsolete = !revoke; - } else { - const slicedBreadcrumbs = breadcrumbs.slice(0, record.breadcrumbs.length); - if (_.isEqual(slicedBreadcrumbs, record.breadcrumbs)) { - isObsolete = revoke; - } else { - isObsolete = !revoke; - } - } - if (isObsolete) { - this.call('knowledgeService', 'unregisterRecord', record); - } - } - }, - /** - * Conditions for a record to be usable by a Knowledge article: - * - The current view is not a Knowledge form - * - has a ControlPanel which uses breadcrumbs - * - is the result of an action - * - displays an existing record (already stored in DB) - * (saved new records will be registered upon @see update ) - */ - _isPotentialKnowledgeRecord: function () { - return !this.ignoreKnowledgeRecordSearch && this.withControlPanel && this.controlPanelProps.withBreadcrumbs && - this.controlPanelProps.action && this.controlPanelProps.action.controllers && - this.handle && !this.model.localData[this.handle].isNew(); - }, - /** - * @param {Object} record raw record - * @param {string} modifier modifier as registered in the view (xml) - * @returns {boolean} whether the domain includes this record - */ - _readModifier: function (record, modifier) { - if (!modifier) { - // falsy modifier - return false; - } - let value = false; - try { - // unaware of context - const preDomain = new Domain(modifier); - // aware of context - const domain = new Domain(preDomain.toList(record.context)); - value = domain.contains(record.data); - } catch (_error) { - // truthy modifier - return true; - } - return value; - }, - /** - * Evaluate the current record and notify @see KnowledgeService if it can or - * could be used in a Knowledge article. - * - * @private - */ - _searchKnowledgeRecord: function () { - const record = this.model.get(this.handle, {raw: true}); - const controller = this.controlPanelProps.action.controllers.form; - const breadcrumbs = this.controlPanelProps.breadcrumbs.slice(); - // add the current breadcrumbs information - breadcrumbs.push({ - title: record.data.display_name, - controllerID: controller.jsId, - }); - /** - * If the current potential record has exactly the same breadcrumbs - * sequence as another record registered in the @see KnowledgeService, - * the previous record should be unregistered here because this problem - * will not be caught later, as the Knowledge form view only checks - * whether its breadcrumbs sequence contains a record's breadcrumbs - * sequence, regardless of the fact that the current potential record - * may not have been registered in the service. - * - * This call could be omitted if the breadcrumbs would also store the - * related res_id if any, but currently two records with the same - * display name and model will have exactly the same breadcrumbs - * information (controllerID and title). - */ - this._unregisterObsoleteKnowledgeRecords(breadcrumbs, true); - const hasMessageIds = this.renderer.chatterFields && this.renderer.chatterFields.hasMessageIds; - const view = this.controlPanelProps.view; - const fields = view.viewFields; - const formFields = view.fieldsInfo.form; - const knowledgeRecord = { - res_id: record.res_id, - res_model: view.model, - breadcrumbs: breadcrumbs, - fieldNames: [], - }; - if (hasMessageIds) { - knowledgeRecord.withChatter = true; - this.call('knowledgeService', 'registerRecord', knowledgeRecord); - } - for (let fieldName of this.knowledgeRecordFieldNames) { - if (fieldName in formFields && fields[fieldName].type === 'html' && !fields[fieldName].readonly) { - const readonlyModifier = formFields[fieldName].modifiers.readonly; - const invisibleModifier = formFields[fieldName].modifiers.invisible; - if (this._readModifier(record, readonlyModifier) || this._readModifier(record, invisibleModifier)) { - continue; - } - knowledgeRecord.fieldNames.push({ - name: fieldName, - string: fields[fieldName].string, - }); - } - } - /** - * While a field by itself can be visible and not readonly, its - * group / notebook pane could be invisible. Another check will be done - * after rendering to determine if any of the catched fields are in fact - * usable by the user. @see FormRenderer - */ - this.call('knowledgeService', 'pushToValidateWithHtmlField', knowledgeRecord); - }, -}); diff --git a/addons/knowledge/static/src/js/form_renderer.js b/addons/knowledge/static/src/js/form_renderer.js deleted file mode 100644 index d55200fb5e6..00000000000 --- a/addons/knowledge/static/src/js/form_renderer.js +++ /dev/null @@ -1,77 +0,0 @@ -/** @odoo-module **/ - -import FormRenderer from 'web.FormRenderer'; -import core from 'web.core'; - -FormRenderer.include({ - /** - * Refresh the injector of html fields (to update toolbars and behaviors) - * @see FieldHtmlInjector - * - * @param {string} recordID - */ - refreshFieldHtmlInjectors: function (recordID) { - const recordWidgets = this.allFieldWidgets[recordID]; - if (!recordWidgets) { - return; - } - recordWidgets.filter(widget => widget.field.type === 'html' && typeof widget.refreshInjector === 'function') - .forEach(widget => { - widget.refreshInjector(); - }); - }, - /** - * Once the view is rendered, check whether a record's field registered in - * @see KnowledgeService is available to the user directly or through - * an inactive pane of the notebook. - * - * @override - */ - _render: async function () { - await this._super(...arguments); - core.bus.once('DOM_updated', this, function () { - let record = this.call('knowledgeService', 'popToValidateWithHtmlField'); - while (record) { - for (let fieldName = record.fieldNames.shift(); fieldName; fieldName = record.fieldNames.shift()) { - if (this._checkAvailability(fieldName.name)) { - record.fieldNames = [fieldName]; - record.withHtmlField = true; - break; - } - } - if (record.withHtmlField) { - this.call('knowledgeService', 'registerRecord', record); - break; - } - record = this.call('knowledgeService', 'popToValidateWithHtmlField'); - } - }); - }, - /** - * Inspect the view to check whether the field matching "name" is accessible - * in the view at least once, directly or through a notebook pane switch. - * - * @param {string} name of a field of the record - * @returns {boolean} - */ - _checkAvailability: function (name) { - const selector = `.oe_form_field_html[name="${name}"]`; - const $sel = $(selector); - for (let i = 0; i < $sel.length; i++) { - if ($sel.eq(i).is(':visible:hasVisibility')) { - return true; - } else if ($sel[i].closest('.o_invisible_modifier')) { - continue; - } else { - let pane = $sel[i].closest('.tab-pane:not(.active)'); - if (pane) { - let $paneSwitch = $(`[data-bs-toggle="tab"][href*="${pane.id}"]`); - if ($paneSwitch.is(':visible:hasVisibility')) { - return true; - } - } - } - } - return false; - } -}); diff --git a/addons/knowledge/static/src/js/knowledge_behaviors.js b/addons/knowledge/static/src/js/knowledge_behaviors.js deleted file mode 100644 index 168cd8a5610..00000000000 --- a/addons/knowledge/static/src/js/knowledge_behaviors.js +++ /dev/null @@ -1,166 +0,0 @@ -/** @odoo-module */ - -import Class from 'web.Class'; -import core from 'web.core'; -import Dialog from "web.Dialog"; -const _t = core._t; - -/** - * Behavior to be injected through @see FieldHtmlInjector to @see OdooEditor - * blocks which have specific classes calling for such behaviors. - * - * A typical usage could be the following: - * - An @see OdooEditor block like /template has the generic class: - * @see o_knowledge_behavior_anchor to signify that it needs to have a - * behavior injected. - * - This block also has the specific class: - * @see o_knowledge_behavior_type_[behaviorType] which specifies the type of - * behavior that needs to be injected. @see FieldHtmlInjector has a dictionary - * mapping those classes to the correct behavior class. - * - * The @see KnowledgeBehavior is a basic behavior intended to be overriden for - * more complex implementations - */ -const KnowledgeBehavior = Class.extend({ - /** - * @param {Widget} handler @see FieldHtmlInjector which has access to - * widget specific functions - * @param {Element} anchor dom node to apply the behavior to - * @param {string} mode edit/readonly - */ - init: function (handler, anchor, mode) { - this.handler = handler; - this.anchor = anchor; - this.mode = mode; - if (this.handler.editor) { - this.handler.editor.observerUnactive('knowledge_attributes'); - } - this.applyAttributes(); - if (this.handler.editor) { - this.handler.editor.observerActive('knowledge_attributes'); - } - this.applyListeners(); - }, - /** - * Add specific attributes related to this behavior to this.anchor - */ - applyAttributes: function () {}, - /** - * Add specific listeners related to this behavior to this.anchor - */ - applyListeners: function () {}, - /** - * Disable the listeners added in @see applyListeners - */ - disableListeners: function () {}, - /** - * Used by @see KnowledgePlugin to remove behaviors when the field_html is - * saved. Also used by @see FieldHtmlInjector to manage injected behaviors - */ - removeBehavior: function () { - this.handler.trigger_up('behavior_removed', { - anchor: this.anchor, - }); - this.disableListeners(); - delete this.anchor.oKnowledgeBehavior; - }, -}); - -/** - * A behavior to set a block as uneditable. Such a block can have children - * marked as @see o_knowledge_content which are set as editable - */ -const ContentsContainerBehavior = KnowledgeBehavior.extend({ - /** - * @override - */ - applyAttributes: function () { - this._super.apply(this, arguments); - if (this.mode === 'edit') { - this.anchor.querySelectorAll('.o_knowledge_content').forEach(element => { - element.setAttribute('contenteditable', 'true'); - }); - this.anchor.setAttribute('contenteditable', 'false'); - } - }, -}); -/** - * A behavior for the /article command @see Wysiwyg - */ -const ArticleBehavior = KnowledgeBehavior.extend({ - /** - * @override - */ - init: function () { - this.busy = false; - this.linkClickHandler = async function (ev) { - if (this.busy) { - ev.preventDefault(); - ev.stopPropagation(); - } else { - this.busy = true; - await this._onLinkClick(ev); - this.busy = false; - } - }.bind(this); - this.linkDblClickHandler = function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - }; - this._super.apply(this, arguments); - }, - /** - * @override - */ - applyAttributes: function () { - this._super.apply(this, arguments); - if (this.mode === 'edit') { - this.anchor.setAttribute('contenteditable', 'false'); - } - }, - /** - * @override - */ - applyListeners: function () { - this._super.apply(this, arguments); - this.anchor.addEventListener("click", this.linkClickHandler); - this.anchor.addEventListener("dblclick", this.linkDblClickHandler); - }, - /** - * @override - */ - disableListeners: function () { - this.anchor.removeEventListener("click", this.linkClickHandler); - this.anchor.removeEventListener("dblclick", this.linkDblClickHandler); - }, - /** - * When the user clicks on an article link, we can directly open the - * article in the current view without having to reload the page. - * - * @param {Event} event - */ - _onLinkClick: async function (event) { - const res_id = parseInt(event.currentTarget.dataset.res_id); - if (res_id) { - event.stopPropagation(); - event.preventDefault(); - const actionPromise = this.handler.do_action('knowledge.ir_actions_server_knowledge_home_page', { - additional_context: { - res_id: res_id - } - }); - await actionPromise.catch(() => { - Dialog.alert(this, - _t("This article was deleted or you don't have the rights to access it."), { - title: _t('Error'), - }); - }); - } - }, -}); - -export { - KnowledgeBehavior, - ContentsContainerBehavior, - ArticleBehavior, -}; diff --git a/addons/knowledge/static/src/js/knowledge_clipboard_whitelist.js b/addons/knowledge/static/src/js/knowledge_clipboard_whitelist.js deleted file mode 100644 index 7c05efc18ce..00000000000 --- a/addons/knowledge/static/src/js/knowledge_clipboard_whitelist.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @odoo-module */ - -import { CLIPBOARD_WHITELISTS } from '@web_editor/js/editor/odoo-editor/src/OdooEditor'; - -/** - * Allow @see o_knowledge_ classes to be preserved in the editor on paste. - */ -CLIPBOARD_WHITELISTS.classes.push(/^o_knowledge_/); diff --git a/addons/knowledge/static/src/js/knowledge_controller.js b/addons/knowledge/static/src/js/knowledge_controller.js deleted file mode 100644 index 61ac267665d..00000000000 --- a/addons/knowledge/static/src/js/knowledge_controller.js +++ /dev/null @@ -1,446 +0,0 @@ -/** @odoo-module */ - -import { _t, bus } from 'web.core'; -import Dialog from 'web.Dialog'; -import FormController from 'web.FormController'; -import { MoveArticleToDialog } from './widgets/knowledge_dialogs.js'; -import emojis from '@mail/js/emojis'; - -const disallowedEmojis = ['💩', '👎', '💔', '😭', '😢', '😐', '😕', '😞', '😢', '💀']; -const emojisRandomPickerSource = emojis.filter(emoji => !disallowedEmojis.includes(emoji.unicode)); - -const KnowledgeArticleFormController = FormController.extend({ - events: Object.assign({}, FormController.prototype.events, { - 'click .o_knowledge_add_icon': '_onAddRandomIcon', - 'click .o_knowledge_add_cover': '_onAddCover', - 'click #knowledge_search_bar': '_onSearch', - 'click .o_breadcrumb_article_name': '_onArticleBreadcrumbClick', - 'change .o_breadcrumb_article_name': '_onRename', - 'click i.o_toggle_favorite': '_onToggleFavorite', - 'input .o_breadcrumb_article_name': '_adjustInputSize', - }), - custom_events: Object.assign({}, FormController.prototype.custom_events, { - create: '_onCreate', - duplicate: '_onDuplicate', - move: '_onMove', - open_move_to_modal: '_onOpenMoveToModal', - reload_tree: '_onReloadTree', - emoji_click: '_onEmojiClick', - }), - /** - * Register the fact that the current @see FormController is one from - * Knowledge in order not to try and search for a matching record for - * @see KnowledgeService . - * - * @override - */ - init: function (parent, model, renderer, params) { - /** - * This property is used to specify that the current form view will - * get/use records stored in the @see KnowledgeService instead of - * replacing them (default). TODO ABD: maybe set this as an option of - * the form view (XML) so that other form views can easily specify that - * they will get/use records from the KnowledgeService instead of - * providing them. - */ - this.ignoreKnowledgeRecordSearch = true; - this.renderer = renderer; - this._super.apply(this, arguments); - this.onFieldSavedListeners = new Map(); - }, - /** - * If a reload is called with param 'keepChanges', only the chatter needs - * to be updated, no need for a full reload - * - * @override - */ - reload: function (params) { - if (params && params.keepChanges) { - return this.renderer.updateChatter(); - } else { - return this._super.apply(this, arguments); - } - }, - /** - * @override - */ - saveRecord: async function () { - const modifiedFields = await this._super(...arguments); - const { data } = this.model.get(this.handle); - for (const field of modifiedFields) { - if (this.onFieldSavedListeners.has(field)) { - this.onFieldSavedListeners.get(field).forEach(listener => { - listener.call(this, data[field]); - }); - } - } - return modifiedFields; - }, - /** - * @override - * @returns {Promise} - */ - start: function () { - return this._super.apply(this, arguments).then(() => { - this._onFieldSaved('icon', unicode => { - const { id } = this.getState(); - this.renderer._setEmoji(id, unicode); - }); - }); - }, - - // Listeners: - - /** - * @param {Event} event - */ - _adjustInputSize: function (event) { - event.target.setAttribute('size', event.target.value.length); - }, - _onAddCover: async function() { - if (this.mode === 'readonly') { - await this._setMode('edit'); - } - this.$('.o_input_file').click(); - }, - _onAddRandomIcon: function() { - this.trigger_up('field_changed', { - dataPointID: this.handle, - changes: { - 'icon': emojisRandomPickerSource[Math.floor(Math.random() * emojisRandomPickerSource.length)].unicode, - } - }); - }, - /** - * When the user clicks on the name of the article, checks if the article - * name hasn't been set yet. If it hasn't, it will look for a title in the - * body of the article and set it as the name of the article. - * @param {Event} event - */ - _onArticleBreadcrumbClick: async function (event) { - const name = event.currentTarget.value; - if (name === _t('New Article')) { - const $articleTitle = this.$('.o_knowledge_editor h1'); - if ($articleTitle.text().length !== 0) { - this.$('.o_breadcrumb_article_name').val($articleTitle.text()); - this.trigger_up('field_changed', { - dataPointID: this.handle, - changes: { - 'name': $articleTitle.text(), - } - }); - this._rename(await this._getId(), $articleTitle.text()); - } - } - }, - /** - * @param {OdooEvent} event - */ - _onCreate: async function (event) { - await this._create(event.data); - }, - /** - * @param {OdooEvent} event - */ - _onDuplicate: async function (event) { - const handle = await this.model.duplicateRecord(this.handle); - const { res_id } = this.model.get(handle); - this.do_action('knowledge.ir_actions_server_knowledge_home_page', { - additional_context: { res_id } - }); - }, - /** - * @param {Event} event - */ - _onEmojiClick: async function (event) { - const { id } = this.getState(); - const { articleId, unicode } = event.data; - if (articleId === id) { - this.trigger_up('field_changed', { - dataPointID: this.handle, - changes: { - icon: unicode - } - }); - } else { - const result = await this._rpc({ - model: 'knowledge.article', - method: 'write', - args: [[articleId], { icon: unicode }], - }); - if (result) { - this.renderer._setEmoji(articleId, unicode); - } - } - }, - /** - * @override - * @param {Event} event - */ - _onFieldChanged: async function (event) { - this._super(...arguments); - const { changes } = event.data; - for (const field of this._getFieldsToForceSave()) { - if (changes.hasOwnProperty(field)) { - await this.saveRecord(this.handle, { - reload: false, - stayInEdit: true - }); - return; - } - } - }, - /** - * @param {String} name - field name - * @param {Function} callback - */ - _onFieldSaved: function (name, callback) { - if (this.onFieldSavedListeners.has(name)) { - this.onFieldSavedListeners.get(name).push(callback); - } else { - this.onFieldSavedListeners.set(name, [callback]); - } - }, - /** - * When the user clicks on a field in readonly mode, a new 'quick_edit' event - * will be triggered. To prevent the view from switching to the edit mode when - * the article is locked, we will overwrite the `_onQuickEdit` handler. This - * function will now ignore the event if the article is locked. - * @override - */ - _onQuickEdit: function () { - const { data } = this.model.get(this.handle); - if (data.is_locked) { - return; - } - this._super.apply(this, arguments); - }, - /** - * @param {Event} event - */ - _onMove: function (event) { - this._confirmMove(event.data); - }, - /** - * Opens the "Move To" modal - * @param {OdooEvent} event - */ - _onOpenMoveToModal: async function (event) { - const id = await this._getId(); - const state = this.model.get(this.handle); - const dialog = new MoveArticleToDialog(this, {}, { - state: state, - /** - * @param {String} value - */ - onSave: async value => { - const params = { article_id: id }; - if (typeof value === 'number') { - params.target_parent_id = value; - } else { - params.newCategory = value; - params.oldCategory = state.data.category; - } - this._confirmMove({...params, - onSuccess: () => { - dialog.close(); - this.reload(); - }, - onReject: () => {} - }); - } - }); - dialog.open(); - }, - /** - * @param {Event} event - */ - _onReloadTree: function (event) { - // TODO JBN: Create a widget for the tree and reload it without reloading the whole view. - this.reload(); - }, - /** - * Callback function called when the user renames the active article. - * The function will update the name of the articles in the aside block. - * @param {Event} event - */ - _onRename: async function (event) { - var name = event.currentTarget.value; - if (name.length === 0) { - name = _t('New Article'); - } - const id = await this._getId(); - await this._rename(id, name); - }, - /** - * @param {Event} event - */ - _onSearch: function (event) { - // TODO: change to this.env.services.commandes.openMainPalette when form views are migrated to owl - bus.trigger("openMainPalette", { - searchValue: "?", - }); - }, - /** - * @param {Event} event - */ - _onToggleFavorite: async function (event) { - const id = await this._getId(); - const active = await this._toggleFavorite(id); - const $target = $(event.target); - $target.toggleClass('fa-star-o', !active); - $target.toggleClass('fa-star', active); - $target.attr('title', active ? _t('Remove from favorites') : _t('Add to favorites')); - this._rpc({ - route: '/knowledge/tree_panel/favorites', - params: { - active_article_id: id, - } - }).then(template => { - const $dom = $(template); - this.$(".o_favorite_container").replaceWith($dom); - this.renderer._setTreeFavoriteListener(); - this.renderer._renderEmojiPicker($dom); - }); - }, - - // API calls: - - /** - * @param {Object} data - * @param {integer} data.article_id - * @param {String} data.oldCategory - * @param {String} data.newCategory - * @param {integer} [data.target_parent_id] - * @param {integer} [data.before_article_id] - * @param {Function} data.onSuccess - * @param {Function} data.onReject - */ - _confirmMove: async function (data) { - data['params'] = { - is_private: data.newCategory === 'private' - }; - if (typeof data.target_parent_id !== 'undefined') { - data['params'].parent_id = data.target_parent_id; - } - if (typeof data.before_article_id !== 'undefined') { - data['params'].before_article_id = data.before_article_id; - } - if (data.newCategory === data.oldCategory) { - await this._move(data); - } else { - let message, confirmation_message; - if (data.newCategory === 'workspace') { - message = _t("Are you sure you want to move this article to the Workspace? It will be shared with all internal users."); - confirmation_message = _t("Move to Workspace"); - } else if (data.newCategory === 'private') { - message = _t("Are you sure you want to move this to private? Only you will be able to access it."); - confirmation_message = _t("Set as Private"); - } - Dialog.confirm(this, message, { - cancel_callback: data.onReject, - buttons: [{ - text: confirmation_message, - classes: 'btn-primary', - close: true, - click: async () => { - await this._move(data); - } - }, { - text: _t("Discard"), - close: true, - click: data.onReject, - }], - }); - } - }, - /** - * @param {Object} data - * @param {String} data.category - * @param {integer} data.target_parent_id - */ - _create: async function (data) { - const articleId = await this._rpc({ - model: 'knowledge.article', - method: 'article_create', - args: [[]], - kwargs: { - is_private: data.category === 'private', - parent_id: data.target_parent_id ? data.target_parent_id : false - }, - }); - if (!articleId) { - return; - } - this.do_action('knowledge.ir_actions_server_knowledge_home_page', { - stackPosition: 'replaceCurrentAction', - additional_context: { - res_id: articleId - } - }); - }, - /** - * @returns {Array[String]} - */ - _getFieldsToForceSave: function () { - return ['full_width', 'icon', 'cover']; - }, - /** - * @returns {integer} - */ - _getId: async function () { - let state = this.getState(); - if (typeof state.id === 'undefined') { - await this.saveRecord(this.handle); - state = this.getState(); - } - return state.id; - }, - /** - * @param {Object} data - * @param {integer} data.article_id - * @param {Function} data.onSuccess - * @param {Function} data.onReject - * @param {Object} data.params - * @return {Promise} - */ - _move: function (data) { - return this._rpc({ - model: 'knowledge.article', - method: 'move_to', - args: [data.article_id], - kwargs: data.params - }).then(result => { - if (result) { - data.onSuccess(); - } else { - data.onReject(); - } - }).catch(error => { - data.onReject(); - }) - }, - /** - * @param {integer} id - Target id - * @param {string} name - Target Name - */ - _rename: async function (id, name) { - this.$(`.o_knowledge_tree .o_article[data-article-id="${id}"] > .o_article_handle > .o_article_name`).text(name); - this.$(`.o_breadcrumb_article_name`).val(name); - }, - /** - * @param {integer} id - article id - * @returns {Promise} - */ - _toggleFavorite: function (id) { - return this._rpc({ - model: 'knowledge.article', - method: 'action_toggle_favorite', - args: [id] - }); - }, -}); - -export { - KnowledgeArticleFormController, -}; diff --git a/addons/knowledge/static/src/js/knowledge_field_html_injector.js b/addons/knowledge/static/src/js/knowledge_field_html_injector.js deleted file mode 100644 index 98314108f14..00000000000 --- a/addons/knowledge/static/src/js/knowledge_field_html_injector.js +++ /dev/null @@ -1,317 +0,0 @@ -/** @odoo-module */ -import Widget from 'web.Widget'; -import { ComponentWrapper, WidgetAdapterMixin } from 'web.OwlCompatibility'; -import { useService } from "@web/core/utils/hooks"; -import { TemplateToolbar, FileToolbar } from './knowledge_toolbars'; -import { ArticleBehavior, ContentsContainerBehavior } from './knowledge_behaviors'; -const { Component } = owl; - -/** - * Component used to access the @see uiService - */ -class FieldHtmlInjectorComponent extends Component { - setup() { - this.uiService = useService('ui'); - } -} -FieldHtmlInjectorComponent.template = 'knowledge.injector_component'; -/** - * Widget used along @see FieldHtml to inject specific @see KnowledgeBehavior - * and/or @see KnowledgeToolbar for specific blocks elements. These behaviors - * can be used in both edit and readonly modes, and will contribute to a more - * interactive @see FieldHtml . Toolbars added through this injector will be - * cleaned before save through the @see KnowledgePlugin and won't be part of - * the field final value. - */ -const FieldHtmlInjector = Widget.extend(WidgetAdapterMixin, { - custom_events: { - toolbar_removed: '_onToolbarRemoved', - behavior_removed: '_onBehaviorRemoved', - }, - /** - * Map classes used in @see OdooEditor blocks to their corresponding - * @see KnowledgeToolbar and template. - */ - toolbar_types: { - o_knowledge_toolbar_type_template: { - template: 'knowledge.template_toolbar', - Toolbar: TemplateToolbar, - }, - o_knowledge_toolbar_type_file: { - template: 'knowledge.file_toolbar', - Toolbar: FileToolbar, - }, - }, - /** - * Map classes used in @see OdooEditor blocks to their corresponding - * @see KnowledgeBehavior - */ - behavior_types: { - o_knowledge_behavior_type_contents_container: { - Behavior: ContentsContainerBehavior, - }, - o_knowledge_behavior_type_article: { - Behavior: ArticleBehavior, - }, - }, - /** - * @override - * @param {Widget} parent - * @param {string} mode edit/readonly - * @param {Element} field content of the @see FieldHtml - * @param {Object} editor @see OdooEditor - */ - init: function (parent, mode, field, editor) { - this._super.apply(this, arguments); - // store every toolbar anchor elements - this.toolbar_anchors = new Set(); - // store every behavior anchor elements - this.behavior_anchors = new Set(); - this.mode = mode; - this.field = field; - this.editor = editor; - }, - /** - * Start the injection process and setup injection event listeners. - * The component is used to access uiService. This widget should be - * converted to a component. TODO ABD - * @see owl_compatibility - * - * @override - */ - start: function () { - const prom = this._super.apply(this, arguments); - this.component = new ComponentWrapper(this, FieldHtmlInjectorComponent, {}); - const componentPromise = this.component.mount(this.el); - $(this.field).on('refresh_injector', this._onRefreshInjector.bind(this)); - return Promise.all([prom, componentPromise]).then(function() { - this.manageBehaviors(); - return this.manageToolbars(); - }.bind(this)); - }, - /** - * @see owl_compatibility - */ - update: function () { - return this.component.update({}); - }, - /** - * Setup the behavior injection process - */ - manageBehaviors: function() { - $(this.field).on('refresh_behaviors', this._onRefreshBehaviors.bind(this)); - this.updateBehaviors(); - }, - /** - * Setup the toolbar injection process - * - * @returns {Promise} promise to append the initial batch of - * @see KnowledgeToolbar - */ - manageToolbars: function() { - $(this.field).on('refresh_toolbars', this._onRefreshToolbars.bind(this)); - return this.updateToolbars(); - }, - /** - * Scan this.field for every Toolbar container and anchor. Remove and - * destroy currently registered obsolete toolbars. - * - * @param {Array[Object]} toolbarsData array to be filled with data - * currently present in this.field - */ - _scanFieldForToolbars: function (toolbarsData) { - const anchors = new Set(); - const types = new Set(Object.getOwnPropertyNames(this.toolbar_types)); - this.field.querySelectorAll('.o_knowledge_toolbars_container').forEach(function (types, container) { - container.querySelectorAll('.o_knowledge_toolbar_anchor').forEach(function (types, container, anchor) { - const type = Array.from(anchor.classList).find(className => types.has(className)); - if (type) { - toolbarsData.push({ - container: container, - anchor: anchor, - type: type, - }); - anchors.add(anchor); - } - }.bind(this, types, container)); - }.bind(this, types)); - // difference between the stored set and the computed one - const differenceAnchors = new Set([...this.toolbar_anchors].filter(anchor => !anchors.has(anchor))); - // remove obsolete toolbars - differenceAnchors.forEach(anchor => { - if (anchor.oKnowledgeToolbar) { - anchor.oKnowledgeToolbar.removeToolbar(); - } - }); - }, - /** - * If toolbarsData is set, update only those toolbars, if not, recompute - * every Toolbar of this.field - * - * @param {Array[Object]} toolbarsData - * @param {Element} [container] element which has a toolbar anchor - * @param {Element} [anchor] element in which to inject the toolbar - * @param {string} [type] html class representing the toolbar type - * (i.e.: @see o_knowledge_toolbar_type_... ) - * @returns {Promise} promise to append toolbars - */ - updateToolbars: function (toolbarsData = []) { - if (this.editor) { - this.editor.observerUnactive(); - } - if (!toolbarsData.length) { - // no toolbarsData, recreate the array from the field value - this._scanFieldForToolbars(toolbarsData); - } - - // inject new toolbars - const promises = []; - toolbarsData.forEach(toolbarData => { - const anchor = toolbarData.anchor; - if (!anchor.oKnowledgeToolbar) { - promises.push(this._createToolbar(toolbarData)); - } else { - this.toolbar_anchors.add(anchor); - } - }); - - return Promise.all(promises).then(() => { - if (this.editor) { - this.editor.observerActive(); - } - }); - }, - /** - * Scan this.field for every Behavior anchor. delete currently registered - * obsolete behaviors. - * - * @param {Array[Object]} behaviorsData array to be filled with data - * currently present in this.field - */ - _scanFieldForBehaviors: function (behaviorsData) { - const anchors = new Set(); - const types = new Set(Object.getOwnPropertyNames(this.behavior_types)); - this.field.querySelectorAll('.o_knowledge_behavior_anchor').forEach(function (types, anchor) { - const type = Array.from(anchor.classList).find(className => types.has(className)); - if (type) { - behaviorsData.push({ - anchor: anchor, - type: type, - }); - anchors.add(anchor); - } - }.bind(this, types)); - // difference between the stored set and the computed one - const differenceAnchors = new Set([...this.behavior_anchors].filter(anchor => !anchors.has(anchor))); - // remove obsolete behaviors - differenceAnchors.forEach(anchor => { - if (anchor.oKnowledgeBehavior) { - anchor.oKnowledgeBehavior.removeBehavior(); - } - }); - }, - /** - * If behaviorsData is set, update only those behaviors, if not, recompute - * every behavior of this.field - * - * @param {Array[Object]} behaviorsData - * @param {Element} [anchor] element to which to inject the behavior - * @param {string} [type] html class representing the behavior type - * (i.e.: @see o_knowledge_behavior_type_... ) - */ - updateBehaviors: function (behaviorsData = []) { - if (!behaviorsData.lenth) { - // no behaviorsData, recreate the array from the field value. - this._scanFieldForBehaviors(behaviorsData); - } - - // inject new behaviors - behaviorsData.forEach(behaviorData => { - const anchor = behaviorData.anchor; - const {Behavior} = this.behavior_types[behaviorData.type] || {}; - if (!Behavior) { - return; - } - if (!anchor.oKnowledgeBehavior) { - anchor.oKnowledgeBehavior = new Behavior(this, anchor, this.mode); - } - this.behavior_anchors.add(anchor); - }); - }, - /** - * @param {Object} - * @param {Element} [container] element which has a toolbar anchor - * @param {Element} [anchor] element in which to inject the toolbar - * @param {string} [type] html class representing the toolbar type - * (i.e.: @see o_knowledge_toolbar_type_... ) - * @returns {Promise} promise to append this toolbar - */ - _createToolbar: function ({container, anchor, type}) { - const {Toolbar, template} = this.toolbar_types[type] || {}; - if (!Toolbar) { - return; - } - const toolbar = new Toolbar(this, container, anchor, template, this.editor, this.component.componentRef.comp.uiService); - anchor.oKnowledgeToolbar = toolbar; - this.toolbar_anchors.add(anchor); - const firstElementChild = anchor.firstElementChild; - if (firstElementChild) { - return toolbar.replace(firstElementChild); - } - return toolbar.appendTo(anchor); - }, - /** - * Clean @see behavior_anchors set when a behavior is removed from the dom - * - * @param {Event} event - */ - _onBehaviorRemoved: function (event) { - event.stopPropagation(); - this.behavior_anchors.delete(event.data.anchor); - }, - /** - * Clean @see toolbar_anchors set when a toolbar is removed from the dom - * - * @param {Event} event - */ - _onToolbarRemoved: function (event) { - event.stopPropagation(); - this.toolbar_anchors.delete(event.data.anchor); - }, - /** - * @param {Event} e - * @param {Object} data - * @param {Array} [toolbarsDatas] - */ - _onRefreshToolbars: function (e, data = {}) { - if (this.field) { - this.updateToolbars("toolbarsData" in data ? data.toolbarsData : []); - } - }, - /** - * @param {Event} e - * @param {Object} data - * @param {Array} [behaviorsData] - */ - _onRefreshBehaviors: function (e, data = {}) { - if (this.field) { - this.updateBehaviors("behaviorsData" in data ? data.behaviorsData : []); - } - }, - /** - * @param {Event} e - * @param {Object} data - * @param {Array} [behaviorsData] - * @param {Array} [toolbarsDatas] - */ - _onRefreshInjector: function (e, data = {}) { - if (this.field) { - this._onRefreshBehaviors(e, data); - this._onRefreshToolbars(e, data); - } - } -}); - -export { - FieldHtmlInjector, -}; diff --git a/addons/knowledge/static/src/js/knowledge_frontend.js b/addons/knowledge/static/src/js/knowledge_frontend.js deleted file mode 100644 index ee7704a190d..00000000000 --- a/addons/knowledge/static/src/js/knowledge_frontend.js +++ /dev/null @@ -1,89 +0,0 @@ -/** @odoo-module */ -'use strict'; - -import publicWidget from 'web.public.widget'; -import KnowledgeTreePanelMixin from './tools/tree_panel_mixin.js' - - -publicWidget.registry.KnowledgeWidget = publicWidget.Widget.extend(KnowledgeTreePanelMixin, { - selector: '.o_knowledge_form_view', - events: { - 'keyup .knowledge_search_bar': '_searchArticles', - 'click .o_article_caret': '_onFold', - 'click .o_favorites_toggle_button': '_toggleFavorite', - }, - - /** - * @override - * @returns {Promise} - */ - start: function () { - return this._super.apply(this, arguments).then(() => { - const id = this.$el.data('article-id'); - this._renderTree(id, '/knowledge/tree_panel/portal'); - }); - }, - - /** - * @param {Event} event - */ - _searchArticles: function (event) { - const $input = $(event.currentTarget); - const $tree = $('.o_tree'); - const keyword = $input.val().toLowerCase(); - this._traverse($tree, $li => { - if ($li.text().toLowerCase().includes(keyword)) { - $li.removeClass('d-none'); - return true; - } else { - $li.addClass('d-none'); - return false; - } - }); - }, - - /** - * Helper function to traverse the dom hierarchy of the aside tree menu. - * The function will call the given callback function with the article item - * beeing visited (i.e: a JQuery dom element). The provided callback function - * should return a boolean indicating whether the algorithm should explore - * the children of the current article item. - * @param {jQuery} $tree - * @param {Function} callback - */ - _traverse: function ($tree, callback) { - const stack = $tree.children('li').toArray(); - while (stack.length > 0) { - const $li = $(stack.shift()); - if (callback($li)) { - const $ul = $li.children('ul'); - stack.unshift(...$ul.children('li').toArray()); - } - } - }, - - /** - * @param {Event} event - */ - _toggleFavorite: function (event) { - const $star = $(event.currentTarget); - const id = $star.data('article-id'); - return this._rpc({ - model: 'knowledge.article', - method: 'action_toggle_favorite', - args: [[id]] - }).then(result => { - $star.find('i').toggleClass('fa-star', result).toggleClass('fa-star-o', !result); - // Add/Remove the article to/from the favorite in the sidebar - return this._rpc({ - route: '/knowledge/tree_panel/favorites', - params: { - active_article_id: id, - } - }).then(template => { - this.$('.o_favorite_container').replaceWith(template); - this._setTreeFavoriteListener(); - }); - }); - }, -}); diff --git a/addons/knowledge/static/src/js/knowledge_macros.js b/addons/knowledge/static/src/js/knowledge_macros.js deleted file mode 100644 index 4d5d0dd7f56..00000000000 --- a/addons/knowledge/static/src/js/knowledge_macros.js +++ /dev/null @@ -1,321 +0,0 @@ -/** @odoo-module */ - -import { MacroEngine } from "@web/core/macro"; -import Dialog from "web.Dialog"; -import core from "web.core"; -const _t = core._t; - -/** - * Wrapper for @see Macro destined to be used by @see KnowledgeToolbar buttons. - * These macros will navigate through breadcrumbs to inject information from - * a Knowledge article into a previously browsed record that met the - * requirements specified in @see FormController (Knowledge) - */ -class KnowledgeMacroPageChangeError extends Error {} -export class KnowledgeMacro { - /** - * @param {Object} breadcrumbs - * @param {string} action name of the action to be undertaken - * @param {Object} data data object to be used during the action - * @param {Object} uiService used to cover the screen during the macro - * @param {integer} interval delay between the macro steps - */ - constructor (breadcrumbs, action, data, uiService, interval = 16) { - this.breadcrumbsIndex = breadcrumbs.length - 1; - this.breadcrumbsTitle = breadcrumbs[this.breadcrumbsIndex].title; - this.breadcrumbsSelector = `[role="navigation"] > .breadcrumb-item:contains(${this.breadcrumbsTitle})`; - this.interval = interval; - this.data = data; - this.engine = new MacroEngine(); - this.action = action; - this.uiService = uiService; - this.blockUI = { action: function () { - if (!this.uiService.isBlocked) { - this.uiService.block(); - } - }.bind(this) }; - this.unblockUI = { action: function () { - if (this.uiService.isBlocked) { - this.uiService.unblock(); - } - }.bind(this) }; - } - /** - * Execute the specified macro - */ - start() { - // Build the desired macro action - const macroAction = this._macroActions(this.action); - if (!macroAction) { - return; - } - /** - * Preliminary breadcrumbs macro. It will use the @see breadcrumbsIndex - * to switch back to the view related to the stored record - * (@see KnowledgeService) . Once and if the view of the target record - * is correctly loaded, run the specific macroAction. - */ - const startMacro = { - name: "restore_record", - interval: this.interval, - onError: this.onError.bind(this), - steps: [ - this.blockUI, { - trigger: function () { - const $breadcrumbs = $(`.breadcrumb-item:not(.active)`); - if ($breadcrumbs.length > this.breadcrumbsIndex) { - const breadcrumb = $breadcrumbs[this.breadcrumbsIndex]; - if (breadcrumb.textContent.includes(this.breadcrumbsTitle)) { - return this.getElement(breadcrumb.querySelector('a')); - } - } - return null; - }.bind(this), - action: 'click', - }, { - trigger: this.getElement.bind(this, `${this.breadcrumbsSelector}.active`), - action: this.engine.activate.bind(this.engine, macroAction), - }], - }; - this.engine.activate(startMacro); - } - /** - * @see Macro - * - * @param {Error} error - * @param {Object} step - * @param {integer} index - */ - onError(error, step, index) { - this.unblockUI.action(); - if (error instanceof KnowledgeMacroPageChangeError) { - Dialog.alert(this, - _t('The operation was interrupted because the page or the record changed. Please try again later.'), { - title: _t('Error'), - }); - } else { - console.error(error); - } - } - /** - * Return the first (or the last) matching element from selector only if it - * is currently visible for the user in the document. - * - * @param {string} selector - * @param {boolean} reverse whether to search from the end to the start - * @returns {Element} - */ - getElement(selector, reverse=false) { - const $sel = $(selector); - for (let i = 0; i < $sel.length; i++) { - i = reverse ? $sel.length - 1 - i : i; - if ($sel.eq(i).is(':visible:hasVisibility')) { - return $sel[i]; - } - } - return null; - } - /** - * Validate the page during the macro action. The page is valid if the - * targeted breadcrumb is currently active. Abort a macro in case of failure - * @see Macro - */ - validatePage() { - if (!this.getElement(`${this.breadcrumbsSelector}.active`)) { - throw new KnowledgeMacroPageChangeError(); - } - } - /** - * Build the desired action description from this.properties. Every macro - * will validate the page at every step and abort upon failure to do so. - * - * @param {string} action name of a handled macro action - * @returns {Object} valid descrption @see Macro - */ - _macroActions(action) { - switch (action) { - /** - * @see TemplateToolbar - * Copy the contents of a /template block and paste into the - * targeted field stored in @see data . This macro is able to switch - * form notebook panes if the field is available in one of them. - */ - case "use_as_description": return { - name: action, - interval: this.interval, - onError: this.onError.bind(this), - steps: [{ - trigger: function () { - this.validatePage(); - const selector = `.oe_form_field_html[name="${this.data.fieldName}"]`; - const el = this.getElement(selector); - if (el) { - return el; - } - // Handle the case where the field is hidden in a tab of the form view notebook - const $sel = $(selector); - for (let i = 0; i < $sel.length; i++) { - const pane = $sel[i].closest('.tab-pane:not(.active)'); - if (pane) { - const paneSwitch = this.getElement(`[data-bs-toggle="tab"][href*="${pane.id}"]`); - if (paneSwitch) { - paneSwitch.click(); - break; - } - } - } - return null; - }.bind(this), - action: 'click', - }, { - trigger: function () { - this.validatePage(); - return this.getElement(`.oe_form_field_html[name="${this.data.fieldName}"] > .odoo-editor-editable`); - }.bind(this), - action: this._pasteTemplate.bind(this), - }, this.unblockUI], - }; - /** - * @see TemplateToolbar - * Copy the contents of a /template block and paste into a new - * chatter message in the targeted record context. Open the full - * composer to access the editor. - */ - case "send_as_message": return { - name: action, - interval: this.interval, - onError: this.onError.bind(this), - steps: [{ - trigger: function() { - this.validatePage(); - return this.getElement('.o_ChatterTopbar_buttonSendMessage'); - }.bind(this), - action: (el) => { - if (!el.classList.contains('o-active')) { - el.click(); - } - }, - }, { - trigger: function() { - this.validatePage(); - return this.getElement('.o_Composer_buttonFullComposer'); - }.bind(this), - action: 'click', - }, { - trigger: function () { - this.validatePage(); - const dialog = this.getElement('.o_dialog_container.modal-open'); - if (dialog) { - return this.getElement(dialog.querySelector('.oe_form_field_html[name="body"] > .odoo-editor-editable')); - } else { - return null; - } - }.bind(this), - action: this._pasteTemplate.bind(this), - }, this.unblockUI], - }; - /** - * @see FileToolbar - * Attach a file to a new chatter message in the targeted record - * context. - */ - case "attach_to_message": return { - name: action, - interval: this.interval, - onError: this.onError.bind(this), - steps: [{ - trigger: function() { - this.validatePage(); - return this.getElement('.o_ChatterTopbar_buttonSendMessage'); - }.bind(this), - action: (el) => { - el.scrollIntoView(); - if (!el.classList.contains('o-active')) { - el.click(); - } - }, - }, { - trigger: function() { - this.validatePage(); - return this.getElement('.o_Composer_buttonAttachment'); - }.bind(this), - action: this._dragAndDrop.bind(this, 'dragenter'), - }, { - trigger: function () { - this.validatePage(); - return this.getElement('.o_Composer_dropZone'); - }.bind(this), - action: this._dragAndDrop.bind(this, 'drop'), - }, this.unblockUI], - }; - /** - * @see FileToolbar - * Attach a file to the targeted record. - */ - case "use_as_attachment": return { - name: action, - interval: this.interval, - onError: this.onError.bind(this), - steps: [{ - trigger: function() { - this.validatePage(); - return this.getElement('.o_ChatterTopbar_buttonAttachments'); - }.bind(this), - action: function(el) { - if (!this.getElement('.o_AttachmentBox_content')) { - el.click(); - } - }.bind(this), - }, { - trigger: function() { - this.validatePage(); - return this.getElement('.o_AttachmentBox_content'); - }.bind(this), - action: (el) => el.scrollIntoView(), - }, this.unblockUI], - }; - } - } - /** - * Handle drag&drop events with the dataTransfer object stored in @see data - * - * @param {string} type event type (related to drag&drop) - * @param {Element} el target element - */ - _dragAndDrop(type, el) { - const fakeDragAndDrop = new Event(type, { - bubbles: true, - cancelable: true, - composed: true, - }); - fakeDragAndDrop.dataTransfer = this.data.dataTransfer; - el.dispatchEvent(fakeDragAndDrop); - } - /** - * Handle paste event with the dataTransfer object stored in @see data - * - * @param {Element} el target element - */ - _pasteTemplate(el) { - const fakePaste = new Event('paste', { - bubbles: true, - cancelable: true, - composed: true, - }); - fakePaste.clipboardData = this.data.dataTransfer; - - const sel = document.getSelection(); - sel.removeAllRanges(); - const range = document.createRange(); - const firstChild = el.firstChild; - if (!firstChild) { - range.setStart(el, 0); - range.setEnd(el, 0); - } else { - range.setStartBefore(firstChild); - range.setEndBefore(firstChild); - } - sel.addRange(range); - el.dispatchEvent(fakePaste); - } -} diff --git a/addons/knowledge/static/src/js/knowledge_plugin.js b/addons/knowledge/static/src/js/knowledge_plugin.js deleted file mode 100644 index c3b9e5c0d27..00000000000 --- a/addons/knowledge/static/src/js/knowledge_plugin.js +++ /dev/null @@ -1,26 +0,0 @@ -/** @odoo-module */ - -/** - * Plugin for OdooEditor. Allow to remove temporary toolbars content which are - * not destined to be stored in the field_html - */ -export class KnowledgePlugin { - /** - * @param {Element} editable - */ - cleanForSave(editable) { - for (const node of editable.querySelectorAll('.o_knowledge_toolbar_anchor')) { - if (node.oKnowledgeToolbar) { - node.oKnowledgeToolbar.removeToolbar(); - } - while (node.firstChild) { - node.removeChild(node.lastChild); - } - } - for (const node of editable.querySelectorAll('.o_knowledge_behavior_anchor')) { - if (node.oKnowledgeBehavior) { - node.oKnowledgeBehavior.removeBehavior(); - } - } - } -} diff --git a/addons/knowledge/static/src/js/knowledge_renderers.js b/addons/knowledge/static/src/js/knowledge_renderers.js deleted file mode 100644 index 127e44c59ee..00000000000 --- a/addons/knowledge/static/src/js/knowledge_renderers.js +++ /dev/null @@ -1,444 +0,0 @@ -/** @odoo-module */ - -import PermissionPanelWidget from './widgets/knowledge_permission_panel.js'; -import EmojiPickerWidget from './widgets/knowledge_emoji_picker.js'; -import FormRenderer from 'web.FormRenderer'; -import KnowledgeTreePanelMixin from '@knowledge/js/tools/tree_panel_mixin'; -import { qweb as QWeb } from 'web.core'; - -import { ChatterContainer } from '@mail/components/chatter_container/chatter_container'; -import { ComponentWrapper } from 'web.OwlCompatibility'; - -class ChatterContainerWrapperComponent extends ComponentWrapper {} - -const KnowledgeArticleFormRenderer = FormRenderer.extend(KnowledgeTreePanelMixin, { - className: 'o_knowledge_form_view', - events: _.extend({}, FormRenderer.prototype.events, { - 'click .btn-chatter.active': '_onCloseChatter', - 'click .btn-chatter:not(.active)': '_onOpenChatter', - 'click .btn-create': '_onBtnCreateClick', - 'click .btn-duplicate': '_onBtnDuplicateClick', - 'click .btn-move': '_onBtnMoveClick', - 'click .breadcrumb a[data-controller-id]': '_onBreadcrumbItemClick', - 'click .o_article_caret': '_onFold', - 'click .o_article_name': '_onOpen', - 'click .o_article_create': '_onBtnArticleCreateClick', - 'click .o_section_create': '_onBtnSectionCreateClick', - 'click .o_knowledge_share_panel': '_preventDropdownClose', - 'click .o_knowledge_more_options_panel': '_preventDropdownClose', - }), - /** - * @override - */ - init: function (parent, state, params) { - this._super(...arguments); - this.breadcrumbs = params.breadcrumbs; - /** - * Manually set values for the chatter props as it is custom handled - * and not loaded with the view (oe_chatter is not used in Knowledge, - * the chatter is only rendered when the user clicks on the button). - */ - this.chatterFields = { - // `knowledge.article` has `mail.activity.mixin` - hasActivities: true, - hasFollowers: true, - hasMessageIds: true, - }; - }, - /** - * Called when the chatter triggers a reload on the Form view with the - * param 'keepChanges=true'. In this case, we only need to update the - * chatter. - * - * @returns {Promise} - */ - updateChatter: function () { - if (this._chatterContainerComponent && this.state.res_id) { - const props = this._makeChatterContainerProps(); - return this._chatterContainerComponent.update(props); - } - this._closeChatter(); - return Promise.resolve(); - }, - /** - * @private - */ - _closeChatter: function () { - const chatter = this.el.querySelector('.o_knowledge_chatter_container'); - while (chatter.firstChild) { - chatter.removeChild(chatter.firstChild); - } - if (this._chatterContainerComponent) { - this._chatterContainerComponent.destroy(); - this._chatterContainerComponent = undefined; - } - }, - /** - * @param {Event} event - */ - _onBreadcrumbItemClick: function (event) { - const $target = $(event.target); - this.trigger_up('breadcrumb_clicked', { - controllerID: $target.data('controller-id'), - }); - }, - /** - * Callback function called when the user clicks on the '+' sign of an article - * list item. The callback function will create a new article under the target article. - * @param {Event} event - */ - _onBtnArticleCreateClick: function (event) { - const $target = $(event.currentTarget); - const $li = $target.closest('li'); - this.trigger_up('create', { - target_parent_id: $li.data('article-id') - }); - }, - /** - * @param {Event} event - */ - _onBtnChatterClick: function (event) { - const $chatter = $('.o_knowledge_chatter'); - $chatter.toggleClass('d-none'); - $('.btn-chatter').toggleClass('active'); - }, - /** - * @param {Event} event - */ - _onBtnCreateClick: function (event) { - this.trigger_up('create', { - category: 'private' - }); - }, - /** - * @param {Event} event - */ - _onBtnDuplicateClick: function (event) { - event.preventDefault(); - this.trigger_up('duplicate', {}); - }, - /** - * @param {Event} event - */ - _onBtnMoveClick: function (event) { - event.preventDefault(); - this.trigger_up('open_move_to_modal', {}); - }, - /** - * Callback function called when the user clicks on the '+' sign of a section - * (workspace, private, shared). The callback function will create a new article - * on the root of the target section. - * @param {Event} event - */ - _onBtnSectionCreateClick: function (event) { - const $target = $(event.currentTarget); - const $section = $target.closest('.o_section'); - this.trigger_up('create', { - category: $section.data('section') - }); - }, - /** - * Update the btn-chatter appearance and hide the chatter section in the - * view. Closes the chatter. - * - * @private - */ - _onCloseChatter: function () { - this.el.querySelector('.o_knowledge_chatter').classList.add('d-none'); - this.el.querySelector('.btn-chatter').classList.remove('active'); - this._closeChatter(); - }, - /** - * Opens the selected record. - * @param {Event} event - */ - _onOpen: async function (event) { - event.stopPropagation(); - const $li = $(event.target).closest('li'); - this.do_action('knowledge.ir_actions_server_knowledge_home_page', { - stackPosition: 'replaceCurrentAction', - additional_context: { - res_id: $li.data('article-id') - } - }); - }, - /** - * Update tbe btn-chatter appearance and show the chatter section in the - * view. Open the chatter. - * - * @private - * @param {OdooEvent} ev - */ - _onOpenChatter: async function (ev) { - ev.stopPropagation(); - if (this.state.res_id) { - this.el.querySelector('.o_knowledge_chatter').classList.remove('d-none'); - this.el.querySelector('.btn-chatter').classList.add('active'); - await this._renderChatter(); - } - }, - /** - * By default, Bootstrap closes automatically the dropdown menu when the user - * clicks inside it. To avoid that behavior, we will add a new event listener - * on the dropdown menu that will prevent the click event from bubbling up and - * triggering the listener closing the dropdown menu. - * @param {Event} event - */ - _preventDropdownClose: function (event) { - event.stopPropagation(); - }, - _renderArticleEmoji: function () { - const { data } = this.state; - const $dropdown = this.$('.o_knowledge_icon > .o_article_emoji_dropdown'); - $dropdown.attr('data-article-id', this.state.res_id); - $dropdown.find('.o_article_emoji').text(data.icon || ''); - }, - /** - * Renders the breadcrumb - */ - _renderBreadcrumb: function () { - const items = this.breadcrumbs.map(payload => { - return QWeb.render('knowledge.knowledge_breadcrumb_item', { payload }); - }); - this.$('.breadcrumb').prepend(items); - }, - /** - * Render and mount the chatter of the current record. - * - * @private - * @returns {Promise} - */ - _renderChatter: async function () { - if (!this.state.res_id) { - return; - } - this._closeChatter(); - const props = this._makeChatterContainerProps(); - this._chatterContainerComponent = new ChatterContainerWrapperComponent( - this, - ChatterContainer, - props - ); - const target = this.el.querySelector('.o_knowledge_chatter_container'); - await this._chatterContainerComponent.mount(target); - }, - - /** - * Attaches an emoji picker widget to every emoji dropdown of the container. - * Note: The widget will be attached to the view when the user clicks on - * the dropdown menu for the first time. - * @param {JQuery} [$container] - */ - _renderEmojiPicker: function ($container) { - $container = $container || this.$el; - $container.find('.o_article_emoji_dropdown').one('click', event => { - const $dropdown = $(event.currentTarget); - const picker = new EmojiPickerWidget(this, { - article_id: $dropdown.data('article-id') - }); - picker.attachTo($dropdown); - }); - }, - /** - * Renders the permission panel - */ - _renderPermissionPanel: function () { - this.$('.btn-share').one('click', event => { - const $container = this.$('.o_knowledge_permission_panel'); - const panel = new PermissionPanelWidget(this, { - article_id: this.state.data.id, - user_permission: this.state.data.user_permission - }); - panel.attachTo($container); - }); - }, - /** - * @override - * @returns {Promise} - */ - _renderView: async function () { - const result = await this._super.apply(this, arguments); - this._renderBreadcrumb(); - await this._renderTree(this.state.res_id, '/knowledge/tree_panel'); - this._renderArticleEmoji(); - this._renderPermissionPanel(); - this._setResizeListener(); - return result; - }, - /** - * @param {integer} id - Article id - * @param {String} unicode - */ - _setEmoji: function (id, emoji) { - const emojis = this.$(`.o_article_emoji_dropdown[data-article-id="${id}"] > .o_article_emoji`); - emojis.text(emoji || ''); - }, - /** - * Enables the user to resize the aside block. - * Note: When the user grabs the resizer, a new listener will be attached - * to the document. The listener will be removed as soon as the user releases - * the resizer to free some resources. - */ - _setResizeListener: function () { - /** - * @param {PointerEvent} event - */ - const onPointerMove = _.throttle(event => { - event.preventDefault(); - this.el.style.setProperty('--default-sidebar-size', `${event.pageX}px`); - }, 100); - /** - * @param {PointerEvent} event - */ - const onPointerUp = event => { - $(document).off('pointermove', onPointerMove); - }; - const $resizer = this.$('.o_knowledge_article_form_resizer'); - $resizer.on('pointerdown', event => { - event.preventDefault(); - $(document).on('pointermove', onPointerMove); - $(document).one('pointerup', onPointerUp); - }); - }, - /** - * Initializes the drag-and-drop behavior of the tree listing all articles. - * Once this function is called, the user will be able to move an article - * in the tree hierarchy by dragging an article around. - * When an article is moved, the script will send an rpc call to the server - * and the drag-and-drop behavior will be deactivated while the request is pending. - * - If the rpc call succeeds, the drag-and-drop behavior will be reactivated. - * - If the rpc call fails, the change will be undo and the drag-and-drop - * behavior will be reactivated. - * Unfortunately, `nestedSortable` can only restore one transformation. Disabling - * the drag-and-drop behavior will ensure that the tree structure can be restored - * if something went wrong. - */ - _setTreeListener: function () { - const $sortable = this.$('.o_tree'); - $sortable.nestedSortable({ - axis: 'y', - handle: 'div', - items: 'li', - listType: 'ul', - toleranceElement: '> div', - forcePlaceholderSize: true, - opacity: 0.6, - placeholder: 'bg-info', - tolerance: 'pointer', - helper: 'clone', - cursor: 'grabbing', - cancel: '.readonly', - /** - * @param {Event} event - * @param {Object} ui - */ - stop: (event, ui) => { - $sortable.sortable('disable'); - - const $li = $(ui.item); - const $section = $li.closest('section'); - const $parent = $li.parentsUntil('.o_tree', 'li'); - - const data = { - article_id: $li.data('article-id'), - oldCategory: $li.data('category'), - newCategory: $section.data('section') - }; - - if ($parent.length > 0) { - data.target_parent_id = $parent.data('article-id'); - } - const $next = $li.next(); - if ($next.length > 0) { - data.before_article_id = $next.data('article-id'); - } - $li.siblings('.o_knowledge_empty_info').addClass('d-none'); - this.$('.o_knowledge_empty_info:only-child').removeClass('d-none'); - this.trigger_up('move', {...data, - onSuccess: () => { - const id = $li.data('parent-id'); - if (typeof id !== 'undefined') { - const $parent = this.$(`.o_article[data-article-id="${id}"]`); - if (!$parent.children('ul').is(':parent')) { - const $caret = $parent.find('> .o_article_handle > .o_article_caret'); - $caret.remove(); - } - } - if ($parent.length > 0) { - const $handle = $parent.children('.o_article_handle:first'); - if ($handle.children('.o_article_caret').length === 0) { - const $caret = $(QWeb.render('knowledge.knowledge_article_caret', {})); - $handle.prepend($caret); - } - } - $li.data('parent-id', $parent.data('article-id')); - $li.attr('data-parent-id', $parent.data('article-id')); - $li.data('category', data.newCategory); - $li.attr('data-category', data.newCategory); - let $children = $li.find('.o_article'); - $children.each((_, child) => { - $(child).data('category', data.newCategory); - $(child).attr('data-category', data.newCategory); - }); - $sortable.sortable('enable'); - }, - onReject: () => { - /** - * When a move between two connected nestedSortable - * trees is canceled, more than one operation may be - * undone (library bug). To bypass sortable('cancel'), - * the last moved $item is returned at its original - * location (which may have to be restored too if it was - * cleaned), and a 'change' event is triggered from that - * rectified position for consistency (see the - * nestedSortable library). - */ - const $item = ui.item.data('nestedSortableItem'); - if ($item.domPosition.prev) { - // Restore $item position after its previous sibling - $item.domPosition.prev.after($item.currentItem[0]); - } else { - // Restore $item as the first child of the parent ul - $item.domPosition.parent.prepend($item.currentItem[0]); - if (!$item.domPosition.parent.parentElement) { - // The ul was cleaned from the document since it - // was empty, so it has to be restored too - const offsetParent = $item.offsetParent[0]; - offsetParent.append($item.domPosition.parent); - } - } - // For consistency with the nestedSortable library, - // trigger the 'change' event from the moved $item - $item._trigger('change', null, $item._uiHash()); - $sortable.sortable('enable'); - this.$('.o_knowledge_empty_info').addClass('d-none'); - this.$('.o_knowledge_empty_info:only-child').removeClass('d-none'); - } - }); - }, - }); - - // Allow drag and drop between sections: - - this.$('section[data-section="workspace"] .o_tree').nestedSortable( - 'option', - 'connectWith', - 'section[data-section="private"] .o_tree' - ); - this.$('section[data-section="private"] .o_tree').nestedSortable( - 'option', - 'connectWith', - 'section[data-section="workspace"] .o_tree' - ); - // connectWith both workspace and private sections: - this.$('section[data-section="shared"] .o_tree').nestedSortable( - 'option', - 'connectWith', - 'section[data-section="workspace"] .o_tree, section[data-section="private"] .o_tree' - ); - }, -}); - -export { - KnowledgeArticleFormRenderer, -}; diff --git a/addons/knowledge/static/src/js/knowledge_service.js b/addons/knowledge/static/src/js/knowledge_service.js deleted file mode 100644 index 4cbe2a8008a..00000000000 --- a/addons/knowledge/static/src/js/knowledge_service.js +++ /dev/null @@ -1,151 +0,0 @@ -/** @odoo-module */ -import AbstractService from 'web.AbstractService'; -import core from 'web.core'; - -/** - * This service store data from non-knowledge form view records that can be used - * by a Knowledge form view. - * - * A typical usage could be the following: - * - A form view is loaded and one field of the current record is a match for - * Knowledge @see FormController @see FormRenderer - * - Information about this record and how to access its form view is stored - * in this @see KnowledgeService . - * - A knowledge Article is opened and it contains a @see TemplateToolbar . - * - When the toolbar is injected (@see FieldHtmlInjector ) in the view, it - * asks this @see KnowledgeService if the record can be interacted with. - * - if there is one such record, the related buttons are displayed in the - * toolbar. - * - When one such button is used, the form view of the record is reloaded - * @see KnowledgeMacro and the button action is executed through a @see Macro - * - an exemple of macro action would be copying the template contents as the - * value of a field_html of the record, such as "description" - * - * Scope of the service: - * It is meant to be called on 3 occasions: - * 1) by @see FormController : - * It will only be called if the viewed record can be used within the - * Knowledge module. Such a record should have a chatter in its form - * view, or have at least one field in a whitelist specified in the - * controller. - * 2) by @see FormRenderer : - * It will only be called if the viewed record has a field match in the - * whitelist of the controller. The renderer has to verify that the - * field's container is visible to the user (so that the field can be - * interacted with). - * 3) by @see KnowledgeToolbar : - * It will be called by a toolbar to check whether it has a record that - * can be interacted with in the context of the toolbar (withChatter or - * withHtmlField). - */ -const KnowledgeService = AbstractService.extend({ - /** - * @override - */ - start() { - this._super.apply(...arguments); - this._records = new Set(); - this._lastVisitedRecordWithChatter = null; - this._lastVisitedRecordWithHtmlField = null; - /** - * Some records are added by the controller but need to be validated by - * the renderer. They are stored in this stack until the rendering is - * done. - */ - this._toValidateStackWithHtmlField = []; - }, - /** - * Called by the controller to indicate that a record need to be validated - * by the renderer @see FormController - * - * @param {Object} record - */ - pushToValidateWithHtmlField(record) { - if (record && record.fieldNames.length) { - this._toValidateStackWithHtmlField.push(record); - } - }, - /** - * Called by the renderer to validate a record @see FormRenderer - * - * @returns {Object} - */ - popToValidateWithHtmlField() { - return this._toValidateStackWithHtmlField.pop(); - }, - /** - * Called when a record may be used by a Knowledge form view. - * - * @param {Object} record - */ - registerRecord(record) { - if (!record) { - return; - } - this._records.add(record); - if (record.withChatter) { - /** - * if the record is flagged "withChatter", overwrite the previously - * registered record - */ - this._lastVisitedRecordWithChatter = record; - } - if (record.withHtmlField) { - /** - * if the record is flagged "withHtmlField", reset the stack and - * the previously registered record - */ - this._toValidateStackWithHtmlField = []; - this._lastVisitedRecordWithHtmlField = record; - } - }, - /** - * Remove a record to signify that it can not be used by a Knowledge form - * view anymore. - * - * @param {Object} record - */ - unregisterRecord(record) { - this._records.delete(record); - }, - /** - * Recover a record that is able to interact with the chatter - * - * @returns {Object} - */ - getAvailableRecordWithChatter() { - if (this._lastVisitedRecordWithChatter && !this._lastVisitedRecordWithChatter.withChatter) { - this._records.delete(this._lastVisitedRecordWithChatter); - } - if (!this._records.has(this._lastVisitedRecordWithChatter)) { - this._lastVisitedRecordWithChatter = null; - } - return this._lastVisitedRecordWithChatter; - }, - /** - * Recover a record that has an available field_html - * - * @returns {Object} - */ - getAvailableRecordWithHtmlField() { - if (this._lastVisitedRecordWithHtmlField && !this._lastVisitedRecordWithHtmlField.withHtmlField) { - this._records.delete(this._lastVisitedRecordWithHtmlField); - } - if (!this._records.has(this._lastVisitedRecordWithHtmlField)) { - this._lastVisitedRecordWithHtmlField = null; - } - return this._lastVisitedRecordWithHtmlField; - }, - /** - * Recover a copy of the set of the currently registered records - * - * @returns {Set} - */ - getRecords() { - return new Set(this._records); - }, -}); - -core.serviceRegistry.add('knowledgeService', KnowledgeService); - -export default KnowledgeService; diff --git a/addons/knowledge/static/src/js/knowledge_toolbars.js b/addons/knowledge/static/src/js/knowledge_toolbars.js deleted file mode 100644 index 09c8824aefa..00000000000 --- a/addons/knowledge/static/src/js/knowledge_toolbars.js +++ /dev/null @@ -1,339 +0,0 @@ -/** @odoo-module */ - -import core from 'web.core'; -import Widget from 'web.Widget'; -import Dialog from "web.Dialog"; -import utils from 'web.utils'; -import { KnowledgeMacro } from './knowledge_macros'; -import { sprintf } from '@web/core/utils/strings'; -const _t = core._t; - -/** - * Toolbar to be injected through @see FieldHtmlInjector to @see OdooEditor - * blocks which have specific classes calling for such toolbars. - * - * A typical usage could be the following: - * - An @see OdooEditor block like /template has the generic class: - * @see o_knowledge_toolbars_container to signify that some of its children need - * to have toolbars injected. - * - At least one of the children has the generic class: - * @see o_knowledge_toolbar_anchor that is a container in which the toolbar - * will be appended - * - The same child also as the specific class: - * @see o_knowledge_toolbar_type_[toolbarType] which specifies the type of the - * toolbar that needs to be injected. @see FieldHtmlInjector has a dictionary - * mapping those classes to the correct toolbar class. - * - * The @see KnowledgeToolbar is a basic toolbar intended to be overriden for - * more complex implementations - * - * This widget should be converted to a component. - */ -const KnowledgeToolbar = Widget.extend({ - /** - * @override - * @param {Widget} parent - * @param {Element} container root node of the @see OdooEditor block, i.e.: - * /template or /file root container - * @param {Element} anchor sub-child of @see container container of the - * toolbar - * @param {string} template - * @param {Object} editor @see OdooEditor - * @param {Object} uiService when this widget is converted to a component, - * uiService won't be needed as a parameter - */ - init: function (parent, container, anchor, template, editor, uiService) { - this._super.apply(this, [parent]); - this.container = container; - this.anchor = anchor; - this.template = template; - this.mode = parent.mode; - this.field = parent.field; - this.editor = editor; - this.uiService = uiService; - }, - /** - * Used by @see KnowledgePlugin to remove toolbars when the field_html is - * saved. Also used by @see FieldHtmlInjector to manage injected toolbars - */ - removeToolbar: function () { - this.trigger_up('toolbar_removed', { - anchor: this.anchor, - }); - delete this.anchor.oKnowledgeToolbar; - this.destroy(); - }, - /** - * @override - */ - start: function () { - const prom = this._super.apply(this, arguments); - return prom.then(function () { - this._setupButtons(); - }.bind(this)); - }, - /** - * Setup the toolbar buttons - */ - _setupButtons: function () { - const buttons = this.el.querySelectorAll('button'); - buttons.forEach(this._setupButton.bind(this)); - }, - // FUNCTIONS TO OVERRIDE \\ - /** - * Called for each button of the toolbar. Each button should have a - * data-call attribute which is used as a unique key for differentiation. - * A common implementation would be a switch case on "button.dataset.call". - * Intercept dblclick events to avoid @see OdooEditor interference - * - * @param {Element} button - */ - _setupButton: function (button) { - button.addEventListener("dblclick", function (ev) { - ev.stopPropagation(); - ev.preventDefault(); - }); - return; - }, -}); - -/** - * Toolbar for the /file command - */ -const FileToolbar = KnowledgeToolbar.extend({ - /** - * Recover the eventual related record from @see KnowledgeService - * - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.recordWithChatter = this.call('knowledgeService', 'getAvailableRecordWithChatter'); - }, - /** - * Get data from the file related to this toolbar - * - * @param {Element} fileLink
element to extract file data from - */ - _fetchData: async function (fileLink) { - if (!fileLink || !fileLink.href) { - return null; - } - return await fetch(fileLink.href).then(async function (response) { - if (response.ok) { - return await response.blob(); - } else { - return null; - } - }); - }, - /** - * @override - */ - _setupButton: function (button) { - this._super.apply(this, arguments); - switch (button.dataset.call) { - /** - * Create a @see KnowledgeMacro to add the file to a new message - * in the context of the related record. - */ - case 'attach_to_message': - button.addEventListener("click", async function(ev) { - ev.stopPropagation(); - ev.preventDefault(); - const record = this.recordWithChatter; - if (record) { - const fileLink = this.container.querySelector('.o_knowledge_file_image > a'); - const data = await this._fetchData(fileLink); - if (!data) { - return; - } - const file = new File([data], fileLink.getAttribute('title'), { type: data.type }); - /** - * dataTransfer will be used to mimic a drag and drop of - * the file in the target record chatter. - * @see KnowledgeMacro - */ - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - let macro = new KnowledgeMacro(record.breadcrumbs, button.dataset.call, { - dataTransfer: dataTransfer, - }, this.uiService); - macro.start(); - } - }.bind(this)); - break; - /** - * Create a @see KnowledgeMacro to add the file as an attachment of - * the related record. - */ - case 'use_as_attachment': - button.addEventListener("click", async function(ev) { - ev.stopPropagation(); - ev.preventDefault(); - const record = this.recordWithChatter; - if (record) { - const fileLink = this.container.querySelector('.o_knowledge_file_image > a'); - const data = await this._fetchData(fileLink); - if (!data) { - return; - } - const dataURL = await utils.getDataURLFromFile(data); - const attachment = await this._rpc({ - route: '/web_editor/attachment/add_data', - params: { - 'name': fileLink.getAttribute('title'), - 'data': dataURL.split(',')[1], - 'is_image': false, - 'res_id': this.recordWithChatter.res_id, - 'res_model': this.recordWithChatter.res_model, - } - }); - if (!attachment) { - return; - } - let macro = new KnowledgeMacro(record.breadcrumbs, button.dataset.call, {}, this.uiService); - macro.start(); - } - }.bind(this)); - break; - /** - * Add the file download behavior to the button and the image link - */ - case 'download': - const download = async function (ev) { - ev.stopPropagation(); - ev.preventDefault(); - // Roundabout way to click on the link, to avoid OdooEditor interference with the event - const downloadLink = document.createElement('a'); - const originalLink = this.container.querySelector('.o_knowledge_file_image > a'); - const title = originalLink.getAttribute('title') ? `"${originalLink.getAttribute('title')}" `: ""; - const href = originalLink.getAttribute('href'); - const response = await fetch(href).then((response) => response); - if (response.ok) { - downloadLink.setAttribute('href', href); - downloadLink.setAttribute('download', ''); - downloadLink.setAttribute('target', '_blank'); - downloadLink.click(); - } else { - Dialog.alert(this, - sprintf(_t('Oops, the file %s could not be found. Please replace this file box by a new one to re-upload the file.'), title), { - title: _t('Missing File'), - buttons: [{ - text: _t('Close'), - close: true, - }] - }); - } - }.bind(this); - button.addEventListener("click", download); - const imageElement = this.container.querySelector('.o_knowledge_file_image'); - if (!imageElement.oKnowledgeClickListener) { - imageElement.addEventListener("click", download); - imageElement.oKnowledgeClickListener = true; - } - break; - } - }, -}); - -/** - * Toolbar for the /template command - */ -const TemplateToolbar = KnowledgeToolbar.extend({ - /** - * Recover the eventual related records from @see KnowledgeService - * - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.recordWithChatter = this.call('knowledgeService', 'getAvailableRecordWithChatter'); - this.recordWithHtmlField = this.call('knowledgeService', 'getAvailableRecordWithHtmlField'); - }, - /** - * Create a dataTransfer object with the editable content of the template - * block, to be used for a paste event in the editor - */ - _createHtmlDataTransfer: function () { - const dataTransfer = new DataTransfer(); - const content = this.container.querySelector('.o_knowledge_content'); - dataTransfer.setData('text/html', content.outerHTML); - return dataTransfer; - }, - /** - * @override - */ - _setupButton: function (button) { - this._super.apply(this, arguments); - switch (button.dataset.call) { - /** - * Create a @see KnowledgeMacro to copy the content of the /template - * block and paste it as the content of a new message in the context - * of the related record, in a fullComposer form dialog - */ - case 'send_as_message': - button.addEventListener("click", function (ev) { - ev.stopPropagation(); - ev.preventDefault(); - const record = this.recordWithChatter; - if (record) { - const dataTransfer = this._createHtmlDataTransfer(); - let macro = new KnowledgeMacro(record.breadcrumbs, button.dataset.call, { - dataTransfer: dataTransfer, - }, this.uiService); - macro.start(); - } - }.bind(this)); - break; - /** - * Create a @see KnowledgeMacro to copy the content of the /template - * block and paste it as the content (prepend) of the field_html - * value of the related record - */ - case 'use_as_description': - button.addEventListener("click", function(ev) { - ev.stopPropagation(); - ev.preventDefault(); - const record = this.recordWithHtmlField; - if (record) { - const dataTransfer = this._createHtmlDataTransfer(); - let macro = new KnowledgeMacro(record.breadcrumbs, button.dataset.call, { - fieldName: record.fieldNames[0].name, - dataTransfer: dataTransfer, - }, this.uiService); - macro.start(); - } - }.bind(this)); - break; - /** - * Copy the content of the /template block to the clipboard, and - * prevent @see OdooEditor interference - */ - case 'copy_to_clipboard': - button.addEventListener("click", function (ev) { - ev.stopPropagation(); - ev.preventDefault(); - }); - const content = this.container.querySelector('.o_knowledge_content'); - const clipboard = new ClipboardJS( - button, - {target: () => content} - ); - clipboard.on('success', (e) => { - e.clearSelection(); - this.displayNotification({ - type: 'success', - message: _t("Template copied to clipboard."), - }); - }); - break; - } - }, -}); - -export { - TemplateToolbar, - FileToolbar, - KnowledgeToolbar, -}; diff --git a/addons/knowledge/static/src/js/knowledge_toolbars_edit.js b/addons/knowledge/static/src/js/knowledge_toolbars_edit.js deleted file mode 100644 index 7eedf3648db..00000000000 --- a/addons/knowledge/static/src/js/knowledge_toolbars_edit.js +++ /dev/null @@ -1,36 +0,0 @@ -/** @odoo-module */ - -import { KnowledgeToolbar } from './knowledge_toolbars'; - -/** - * @see KnowledgeToolbar - * This override is loaded as an asset of web_editor to access editor methods - * and utils. - */ -KnowledgeToolbar.include({ - /** - * set the toolbar anchor as non-editable - * - * @override - */ - init: function () { - this._super.apply(this, arguments); - if (this.mode === 'edit') { - this.editor.observerUnactive('knowledge_toolbar_uneditable'); - this.anchor.setAttribute('contenteditable', 'false'); - this.editor.observerActive('knowledge_toolbar_uneditable'); - } - }, - /** - * @override - */ - removeToolbar: function () { - if (this.mode === 'edit') { - this.editor.observerUnactive(); - } - this._super.apply(this, arguments); - if (this.mode === 'edit') { - this.editor.observerActive(); - } - }, -}); diff --git a/addons/knowledge/static/src/js/knowledge_views.js b/addons/knowledge/static/src/js/knowledge_views.js deleted file mode 100644 index 5880070908e..00000000000 --- a/addons/knowledge/static/src/js/knowledge_views.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module */ - -import FormView from 'web.FormView'; -import viewRegistry from 'web.view_registry'; -import { KnowledgeArticleFormController } from './knowledge_controller.js'; -import { KnowledgeArticleFormRenderer } from './knowledge_renderers.js'; - -const KnowledgeArticleFormView = FormView.extend({ - config: _.extend({}, FormView.prototype.config, { - Controller: KnowledgeArticleFormController, - Renderer: KnowledgeArticleFormRenderer, - }), - /** - * For the knowledge module, we want to display a headless layout. - * To get a headless layout, the `init` function will deactivate the - * control panel as well as the search bar and the default breadcrumb. - * @override - * @param {Object} viewInfo - * @param {Object} params - */ - init: function (viewInfo, params) { - params.withBreadcrumbs = false; - params.withControlPanel = false; - params.withSearchBar = false; - this._super.apply(this, arguments); - this.rendererParams.breadcrumbs = params.breadcrumbs; - }, -}); - -viewRegistry.add('knowledge_article_view_form', KnowledgeArticleFormView); - -export { - KnowledgeArticleFormView, -}; diff --git a/addons/knowledge/static/src/js/tools/tree_panel_mixin.js b/addons/knowledge/static/src/js/tools/tree_panel_mixin.js deleted file mode 100644 index 00428fcdfac..00000000000 --- a/addons/knowledge/static/src/js/tools/tree_panel_mixin.js +++ /dev/null @@ -1,130 +0,0 @@ -/** @odoo-module **/ - -export default { - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - /** - * Renders the tree listing all articles. - * To minimize loading time, the function will initially load the root articles. - * The other articles will be loaded lazily: The user will have to click on - * the carret next to an article to load and see their children. - * The id of the unfolded articles will be cached so that they will - * automatically be displayed on page load. - * @param {integer} active_article_id - * @param {String} route - */ - _renderTree: async function (active_article_id, route) { - const $container = this.$('.o_knowledge_tree'); - const portalReadonlyMode = $container.data('portalReadonlyMode'); - let unfoldedArticles = localStorage.getItem('unfoldedArticles'); - unfoldedArticles = unfoldedArticles ? unfoldedArticles.split(";").map(Number) : false; - return this._rpc({ - route: route, - params: { - active_article_id: active_article_id, - unfolded_articles: unfoldedArticles, - } - }).then(htmlTree => { - $container.empty(); - $container.append(htmlTree); - if (!portalReadonlyMode) { - this._setTreeListener(); - this._renderEmojiPicker(); - } - this._setTreeFavoriteListener(); - }).catch(error => { - $container.empty(); - }); - }, - - /** - * Initializes the drag-and-drop behavior of the favorite. - * Once this function is called, the user will be able to reorder their favorites. - * When a favorite is reordered, the script will send an rpc call to the server - * and the drag-and-drop behavior will be deactivated while the request is pending. - * - If the rpc call succeeds, the drag-and-drop behavior will be reactivated. - * - If the rpc call fails, the change will be undo and the drag-and-drop - * behavior will be reactivated. - * Unfortunately, `sortable` can only restore one transformation. Disabling - * the drag-and-drop behavior will ensure that the list structure can be restored - * if something went wrong. - */ - _setTreeFavoriteListener: function () { - const $sortable = this.$('.o_tree_favorite'); - $sortable.sortable({ - axis: 'y', - items: 'li', - cursor: 'grabbing', - forcePlaceholderSize: true, - placeholder: 'o_placeholder', - /** - * @param {Event} event - * @param {Object} ui - */ - stop: (event, ui) => { - const favorite_ids = $sortable.find('.o_article').map(function () { - return $(this).data('favorite-article-id'); - }).get(); - $sortable.sortable('disable'); - this._rpc({ - route: '/web/dataset/resequence', - params: { - model: "knowledge.article.favorite", - ids: favorite_ids, - offset: 1, - } - }).then(() => { - $sortable.sortable('enable'); - }).catch(() => { - $sortable.sortable('cancel'); - $sortable.sortable('enable'); - }); - }, - }); - }, - - /** - * Callback function called when the user clicks on the carret of an article - * The function will load the children of the article and append them to the - * dom. Then, the id of the unfolded article will be added to the cache. - * (see: `_renderTree`). - * @param {Event} event - */ - _onFold: async function (event) { - event.stopPropagation(); - const $button = $(event.currentTarget); - const $icon = $button.find('i'); - const $li = $button.closest('li'); - const $ul = $li.find('ul'); - let unfoldedArticles = localStorage.getItem('unfoldedArticles'); - unfoldedArticles = unfoldedArticles ? unfoldedArticles.split(";") : []; - const articleId = $li.data('articleId').toString(); - if ($ul.is(':visible')) { - if (unfoldedArticles.indexOf(articleId) !== -1) { - unfoldedArticles.splice(unfoldedArticles.indexOf(articleId), 1); - } - $icon.removeClass('fa-caret-down'); - $icon.addClass('fa-caret-right'); - } else { - if ($ul.length === 0) { - // Call the children content - const children = await this._rpc({ - route: '/knowledge/tree_panel/children', - params: { - parent_id: $li.data('articleId') - } - }); - $li.append($('