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 94d8d546a64..00000000000 Binary files a/addons/knowledge/static/description/icon.png and /dev/null differ 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 20f5a083f0e..00000000000 Binary files a/addons/knowledge/static/src/img/fleet_icon.png and /dev/null differ 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($('