From 31ee62c3deec2f9264df64996c99c7ca6a6a40f6 Mon Sep 17 00:00:00 2001 From: Songb <827033417@qq.com> Date: Wed, 8 May 2024 09:30:44 +0800 Subject: [PATCH] init --- knowledge/__init__.py | 26 + knowledge/__manifest__.py | 115 + knowledge/controllers/__init__.py | 7 + knowledge/controllers/article_thread.py | 47 + knowledge/controllers/knowledge_unsplash.py | 67 + knowledge/controllers/main.py | 291 + knowledge/controllers/portal.py | 15 + knowledge/data/article_templates.xml | 354 + knowledge/data/ir_actions_data.xml | 11 + knowledge/data/ir_attachment_data.xml | 57 + knowledge/data/ir_config_parameter_data.xml | 6 + .../data/knowledge_article_stage_data.xml | 42 + ...owledge_article_template_category_data.xml | 28 + .../data/knowledge_article_template_data.xml | 2223 +++++ knowledge/data/knowledge_cover_data.xml | 41 + knowledge/data/mail_templates.xml | 49 + .../data/mail_templates_email_layouts.xml | 27 + knowledge/i18n/ar.po | 7315 ++++++++++++++++ knowledge/i18n/cs.po | 6595 +++++++++++++++ knowledge/i18n/de.po | 7430 +++++++++++++++++ knowledge/i18n/es.po | 7070 ++++++++++++++++ knowledge/i18n/es_419.po | 7391 ++++++++++++++++ knowledge/i18n/et.po | 6676 +++++++++++++++ knowledge/i18n/fa.po | 6602 +++++++++++++++ knowledge/i18n/fi.po | 6827 +++++++++++++++ knowledge/i18n/fr.po | 7407 ++++++++++++++++ knowledge/i18n/he.po | 6613 +++++++++++++++ knowledge/i18n/hu.po | 6605 +++++++++++++++ knowledge/i18n/id.po | 7385 ++++++++++++++++ knowledge/i18n/it.po | 7383 ++++++++++++++++ knowledge/i18n/ja.po | 7189 ++++++++++++++++ knowledge/i18n/knowledge.pot | 6580 +++++++++++++++ knowledge/i18n/ko.po | 7207 ++++++++++++++++ knowledge/i18n/lt.po | 6605 +++++++++++++++ knowledge/i18n/lv.po | 6595 +++++++++++++++ knowledge/i18n/nl.po | 7263 ++++++++++++++++ knowledge/i18n/pl.po | 6899 +++++++++++++++ knowledge/i18n/pt_BR.po | 7380 ++++++++++++++++ knowledge/i18n/sr.po | 6591 +++++++++++++++ knowledge/i18n/th.po | 6623 +++++++++++++++ knowledge/i18n/uk.po | 6972 ++++++++++++++++ knowledge/i18n/vi.po | 7383 ++++++++++++++++ knowledge/i18n/zh_CN.po | 7163 ++++++++++++++++ knowledge/i18n/zh_TW.po | 6639 +++++++++++++++ knowledge/models/__init__.py | 12 + knowledge/models/knowledge_article.py | 2795 +++++++ .../models/knowledge_article_favorite.py | 64 + knowledge/models/knowledge_article_member.py | 97 + knowledge/models/knowledge_article_stage.py | 18 + .../knowledge_article_template_category.py | 15 + knowledge/models/knowledge_article_thread.py | 123 + knowledge/models/knowledge_cover.py | 43 + knowledge/models/res_partner.py | 16 + knowledge/models/res_users.py | 50 + knowledge/populate/__init__.py | 5 + knowledge/populate/knowledge_article.py | 265 + knowledge/populate/tools.py | 54 + knowledge/security/ir.model.access.csv | 30 + knowledge/security/ir_rule.xml | 121 + knowledge/static/description/icon.png | Bin 0 -> 1346 bytes knowledge/static/description/icon.svg | 1 + .../article_template_picker_dialog.js | 118 + .../article_template_picker_dialog.scss | 58 + .../article_template_picker_dialog.xml | 73 + .../abstract_behavior/abstract_behavior.js | 169 + .../article_behavior/article_behavior.js | 77 + .../article_behavior/article_behavior.xml | 7 + .../article_behavior_dialog.js | 104 + .../article_behavior_dialog.xml | 42 + .../articles_structure_behavior.js | 240 + .../articles_structure_behavior.xml | 54 + .../behavior_toolbar/behavior_toolbar.js | 31 + .../behavior_toolbar/behavior_toolbar.xml | 17 + .../embedded_view_behavior.js | 242 + .../embedded_view_behavior.xml | 30 + .../embedded_view_manager.js | 337 + .../embedded_view_manager.scss | 107 + .../embedded_view_manager.xml | 34 + .../behaviors/file_behavior/file_behavior.js | 385 + .../behaviors/file_behavior/file_behavior.xml | 64 + .../table_of_content_behavior.js | 225 + .../table_of_content_behavior.xml | 17 + .../template_behavior/template_behavior.js | 153 + .../template_behavior/template_behavior.xml | 40 + .../video_behavior/video_behavior.js | 39 + .../video_behavior/video_behavior.xml | 13 + .../view_link_behavior/view_link_behavior.js | 71 + .../view_link_behavior.scss | 7 + .../view_link_behavior/view_link_behavior.xml | 6 + .../components/chatter_panel/chatter_panel.js | 56 + .../chatter_panel/chatter_panel.xml | 17 + .../static/src/components/comment/comment.js | 559 ++ .../src/components/comment/comment.scss | 56 + .../static/src/components/comment/comment.xml | 55 + .../comments_handler/comments_handler.js | 473 ++ .../comments_handler/comments_handler.xml | 15 + .../comments_panel/comments_panel.js | 200 + .../comments_panel/comments_panel.xml | 49 + .../comments_popover/comments_popover.js | 87 + .../comments_popover/comments_popover.xml | 22 + .../src/components/composer/composer.js | 27 + .../custom_favorite_filter.js | 23 + .../custom_favorite_item.xml | 8 + .../embedded_view_favorite_menu.js | 37 + .../insert_embedded_view.xml | 17 + .../views_renderers_patches.js | 249 + .../src/components/html_field/html_field.js | 779 ++ .../src/components/html_field/html_field.xml | 8 + .../item_calendar_props_dialog.js | 279 + .../item_calendar_props_dialog.scss | 5 + .../item_calendar_props_dialog.xml | 109 + .../knowledge_article_html_field.js | 208 + .../knowledge_article_html_field.scss | 10 + .../knowledge_article_html_field.xml | 27 + .../knowledge_cover/knowledge_cover.js | 116 + .../knowledge_cover/knowledge_cover.xml | 39 + .../knowledge_cover/knowledge_cover_dialog.js | 149 + .../knowledge_cover_dialog.xml | 35 + .../knowledge_icon/knowledge_icon.js | 61 + .../knowledge_icon/knowledge_icon.xml | 13 + .../knowledge_documents_selector.js | 50 + .../media_dialog/knowledge_media_dialog.js | 61 + .../src/components/message/message_patch.js | 33 + .../move_article_dialog.js | 125 + .../move_article_dialog.xml | 51 + .../permission_panel/permission_panel.js | 343 + .../permission_panel/permission_panel.scss | 27 + .../permission_panel/permission_panel.xml | 167 + .../prompt_embedded_view_name_dialog.js | 68 + .../prompt_embedded_view_name_dialog.xml | 24 + .../properties_panel/properties_panel.js | 54 + .../properties_panel/properties_panel.scss | 24 + .../properties_panel/properties_panel.xml | 28 + .../static/src/components/sidebar/sidebar.js | 875 ++ .../static/src/components/sidebar/sidebar.xml | 62 + .../src/components/sidebar/sidebar_row.js | 128 + .../src/components/sidebar/sidebar_row.xml | 32 + .../src/components/sidebar/sidebar_section.js | 74 + .../components/sidebar/sidebar_section.xml | 70 + .../static/src/components/topbar/topbar.js | 337 + .../static/src/components/topbar/topbar.xml | 227 + .../video_selector_dialog.js | 65 + .../video_selector_dialog.scss | 5 + .../video_selector_dialog.xml | 23 + knowledge/static/src/demo/Onboarding.pdf | Bin 0 -> 76043 bytes knowledge/static/src/demo/fleet_icon.png | Bin 0 -> 5702 bytes .../static/src/discuss/message_actions.js | 20 + .../src/emoji_picker/emoji_picker_patch.js | 16 + .../src/emoji_picker/emoji_picker_patch.xml | 8 + knowledge/static/src/img/Books.svg | 18 + .../src/img/computer_monitor_with_code.jpg | Bin 0 -> 95473 bytes .../static/src/img/entrepreneurship_fair.jpg | Bin 0 -> 87878 bytes knowledge/static/src/img/house.jpg | Bin 0 -> 98489 bytes knowledge/static/src/img/odoo_experience.jpg | Bin 0 -> 91647 bytes knowledge/static/src/img/odoo_run.jpg | Bin 0 -> 93486 bytes knowledge/static/src/img/persona_1.jpg | Bin 0 -> 97392 bytes knowledge/static/src/img/persona_2.jpg | Bin 0 -> 91680 bytes knowledge/static/src/img/persona_3.jpg | Bin 0 -> 99257 bytes knowledge/static/src/img/persona_4.jpg | Bin 0 -> 99522 bytes knowledge/static/src/img/podcast.jpg | Bin 0 -> 88419 bytes knowledge/static/src/img/todo.jpg | Bin 0 -> 97432 bytes .../src/js/knowledge_clipboard_whitelist.js | 8 + .../static/src/js/knowledge_controller.js | 233 + knowledge/static/src/js/knowledge_plugin.js | 65 + .../static/src/js/knowledge_renderers.js | 106 + knowledge/static/src/js/knowledge_utils.js | 290 + knowledge/static/src/js/knowledge_views.js | 15 + knowledge/static/src/js/knowledge_wysiwyg.js | 73 + .../static/src/js/tools/knowledge_tools.js | 31 + knowledge/static/src/js/wysiwyg.js | 433 + knowledge/static/src/macros/abstract_macro.js | 193 + knowledge/static/src/macros/file_macros.js | 99 + .../static/src/macros/template_macros.js | 114 + knowledge/static/src/macros/utils.js | 60 + knowledge/static/src/portal_webclient/main.js | 5 + .../src/portal_webclient/portal_webclient.js | 51 + .../src/portal_webclient/portal_webclient.xml | 9 + .../static/src/scss/knowledge_blocks.scss | 141 + .../static/src/scss/knowledge_common.scss | 125 + .../static/src/scss/knowledge_editor.scss | 41 + .../static/src/scss/knowledge_print.scss | 55 + .../static/src/scss/knowledge_views.dark.scss | 12 + .../static/src/scss/knowledge_views.scss | 442 + .../static/src/search_model/search_model.js | 45 + .../services/knowledge_commands_service.js | 189 + .../knowledge_embedded_filters_service.js | 61 + .../src/views/embedded_controllers_patch.js | 36 + .../static/src/views/embedded_kanban_view.js | 24 + .../static/src/views/embedded_list_view.js | 24 + knowledge/static/src/views/embedded_view.js | 35 + .../item_calendar/item_calendar_model.js | 194 + .../views/item_calendar/item_calendar_view.js | 144 + .../item_calendar/item_calendar_views.xml | 39 + knowledge/static/src/views/list_view.js | 46 + knowledge/static/src/web/chatter_patch.js | 109 + .../static/src/web/form_controller_patch.js | 195 + .../webclient/commands/command_category.js | 9 + .../webclient/commands/knowledge_providers.js | 147 + knowledge/static/src/xml/form_controller.xml | 25 + .../src/xml/knowledge_article_templates.xml | 9 + .../src/xml/knowledge_command_palette.xml | 57 + .../static/src/xml/knowledge_controller.xml | 22 + knowledge/static/src/xml/knowledge_editor.xml | 33 + .../static/src/xml/knowledge_wysiwyg.xml | 10 + .../tests/form_controller_patch_tests.js | 92 + .../knowledge_behaviors_upgrade_tests.js | 202 + .../knowledge_external_embeds_hotkeys.js | 204 + .../static/tests/knowledge_form_view_tests.js | 925 ++ .../knowledge_wysiwyg_collaboration_tests.js | 95 + knowledge/static/tests/mock_services.js | 43 + .../knowledge_article_command_tour.js | 54 + .../knowledge_calendar_command_tour.js | 384 + .../commands/knowledge_file_command_tour.js | 49 + .../commands/knowledge_index_command_tour.js | 31 + .../commands/knowledge_kanban_command_tour.js | 242 + .../commands/knowledge_list_command_tour.js | 79 + .../knowledge_outline_command_tour.js | 28 + .../knowledge_search_favorites_tour.js | 192 + ...knowledge_table_of_content_command_tour.js | 67 + .../knowledge_template_command_tour.js | 104 + .../commands/knowledge_video_command_tour.js | 69 + .../tours/knowledge_article_thread_tours.js | 102 + .../tests/tours/knowledge_cover_picker.js | 193 + .../tours/knowledge_cover_random_unplash.js | 72 + .../tours/knowledge_embedded_views_tour.js | 55 + .../tests/tours/knowledge_history_tour.js | 105 + .../tests/tours/knowledge_main_flow_tour.js | 233 + .../tours/knowledge_main_flow_tour_portal.js | 118 + .../tests/tours/knowledge_pick_emoji_tour.js | 51 + .../tests/tours/knowledge_properties_tour.js | 55 + .../tours/knowledge_readonly_favorite_tour.js | 42 + .../knowledge_readonly_item_kanban_tour.js | 27 + .../knowledge_readonly_item_list_tour.js | 28 + ...quence_children_of_readonly_parent_tour.js | 35 + .../tours/knowledge_sidebar_readonly_tour.js | 48 + .../tests/tours/knowledge_sidebar_tour.js | 939 +++ .../tests/tours/knowledge_template_tour.js | 34 + .../tests/tours/knowledge_tour_utils.js | 235 + knowledge/tests/__init__.py | 17 + knowledge/tests/common.py | 428 + .../tests/test_knowledge_article_business.py | 1516 ++++ .../test_knowledge_article_constraints.py | 616 ++ .../tests/test_knowledge_article_internals.py | 295 + .../test_knowledge_article_permissions.py | 776 ++ .../tests/test_knowledge_article_sequence.py | 192 + .../tests/test_knowledge_article_stage.py | 37 + .../tests/test_knowledge_article_template.py | 279 + .../tests/test_knowledge_article_thread.py | 55 + ...st_knowledge_article_thread_permissions.py | 122 + .../tests/test_knowledge_editor_commands.py | 131 + knowledge/tests/test_knowledge_form_ui.py | 395 + knowledge/tests/test_knowledge_performance.py | 189 + knowledge/tests/test_knowledge_security.py | 284 + knowledge/tests/test_res_users.py | 53 + .../knowledge_article_favorite_views.xml | 61 + .../views/knowledge_article_member_views.xml | 64 + .../views/knowledge_article_stage_views.xml | 113 + ...wledge_article_template_category_views.xml | 33 + knowledge/views/knowledge_article_views.xml | 611 ++ knowledge/views/knowledge_menus.xml | 82 + .../views/knowledge_templates_portal.xml | 19 + knowledge/views/portal_templates.xml | 14 + knowledge/wizard/__init__.py | 4 + knowledge/wizard/knowledge_invite.py | 27 + knowledge/wizard/knowledge_invite_views.xml | 42 + 265 files changed, 220807 insertions(+) create mode 100644 knowledge/__init__.py create mode 100644 knowledge/__manifest__.py create mode 100644 knowledge/controllers/__init__.py create mode 100644 knowledge/controllers/article_thread.py create mode 100644 knowledge/controllers/knowledge_unsplash.py create mode 100644 knowledge/controllers/main.py create mode 100644 knowledge/controllers/portal.py create mode 100644 knowledge/data/article_templates.xml create mode 100644 knowledge/data/ir_actions_data.xml create mode 100644 knowledge/data/ir_attachment_data.xml create mode 100644 knowledge/data/ir_config_parameter_data.xml create mode 100644 knowledge/data/knowledge_article_stage_data.xml create mode 100644 knowledge/data/knowledge_article_template_category_data.xml create mode 100644 knowledge/data/knowledge_article_template_data.xml create mode 100644 knowledge/data/knowledge_cover_data.xml create mode 100644 knowledge/data/mail_templates.xml create mode 100644 knowledge/data/mail_templates_email_layouts.xml create mode 100644 knowledge/i18n/ar.po create mode 100644 knowledge/i18n/cs.po create mode 100644 knowledge/i18n/de.po create mode 100644 knowledge/i18n/es.po create mode 100644 knowledge/i18n/es_419.po create mode 100644 knowledge/i18n/et.po create mode 100644 knowledge/i18n/fa.po create mode 100644 knowledge/i18n/fi.po create mode 100644 knowledge/i18n/fr.po create mode 100644 knowledge/i18n/he.po create mode 100644 knowledge/i18n/hu.po create mode 100644 knowledge/i18n/id.po create mode 100644 knowledge/i18n/it.po create mode 100644 knowledge/i18n/ja.po create mode 100644 knowledge/i18n/knowledge.pot create mode 100644 knowledge/i18n/ko.po create mode 100644 knowledge/i18n/lt.po create mode 100644 knowledge/i18n/lv.po create mode 100644 knowledge/i18n/nl.po create mode 100644 knowledge/i18n/pl.po create mode 100644 knowledge/i18n/pt_BR.po create mode 100644 knowledge/i18n/sr.po create mode 100644 knowledge/i18n/th.po create mode 100644 knowledge/i18n/uk.po create mode 100644 knowledge/i18n/vi.po create mode 100644 knowledge/i18n/zh_CN.po create mode 100644 knowledge/i18n/zh_TW.po create mode 100644 knowledge/models/__init__.py create mode 100644 knowledge/models/knowledge_article.py create mode 100644 knowledge/models/knowledge_article_favorite.py create mode 100644 knowledge/models/knowledge_article_member.py create mode 100644 knowledge/models/knowledge_article_stage.py create mode 100644 knowledge/models/knowledge_article_template_category.py create mode 100644 knowledge/models/knowledge_article_thread.py create mode 100644 knowledge/models/knowledge_cover.py create mode 100644 knowledge/models/res_partner.py create mode 100644 knowledge/models/res_users.py create mode 100644 knowledge/populate/__init__.py create mode 100644 knowledge/populate/knowledge_article.py create mode 100644 knowledge/populate/tools.py create mode 100644 knowledge/security/ir.model.access.csv create mode 100644 knowledge/security/ir_rule.xml create mode 100644 knowledge/static/description/icon.png create mode 100644 knowledge/static/description/icon.svg create mode 100644 knowledge/static/src/components/article_template_picker_dialog/article_template_picker_dialog.js create mode 100644 knowledge/static/src/components/article_template_picker_dialog/article_template_picker_dialog.scss create mode 100644 knowledge/static/src/components/article_template_picker_dialog/article_template_picker_dialog.xml create mode 100644 knowledge/static/src/components/behaviors/abstract_behavior/abstract_behavior.js create mode 100644 knowledge/static/src/components/behaviors/article_behavior/article_behavior.js create mode 100644 knowledge/static/src/components/behaviors/article_behavior/article_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/article_behavior_dialog/article_behavior_dialog.js create mode 100644 knowledge/static/src/components/behaviors/article_behavior_dialog/article_behavior_dialog.xml create mode 100644 knowledge/static/src/components/behaviors/articles_structure_behavior/articles_structure_behavior.js create mode 100644 knowledge/static/src/components/behaviors/articles_structure_behavior/articles_structure_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/behavior_toolbar/behavior_toolbar.js create mode 100644 knowledge/static/src/components/behaviors/behavior_toolbar/behavior_toolbar.xml create mode 100644 knowledge/static/src/components/behaviors/embedded_view_behavior/embedded_view_behavior.js create mode 100644 knowledge/static/src/components/behaviors/embedded_view_behavior/embedded_view_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/embedded_view_behavior/embedded_view_manager.js create mode 100644 knowledge/static/src/components/behaviors/embedded_view_behavior/embedded_view_manager.scss create mode 100644 knowledge/static/src/components/behaviors/embedded_view_behavior/embedded_view_manager.xml create mode 100644 knowledge/static/src/components/behaviors/file_behavior/file_behavior.js create mode 100644 knowledge/static/src/components/behaviors/file_behavior/file_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/table_of_content_behavior/table_of_content_behavior.js create mode 100644 knowledge/static/src/components/behaviors/table_of_content_behavior/table_of_content_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/template_behavior/template_behavior.js create mode 100644 knowledge/static/src/components/behaviors/template_behavior/template_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/video_behavior/video_behavior.js create mode 100644 knowledge/static/src/components/behaviors/video_behavior/video_behavior.xml create mode 100644 knowledge/static/src/components/behaviors/view_link_behavior/view_link_behavior.js create mode 100644 knowledge/static/src/components/behaviors/view_link_behavior/view_link_behavior.scss create mode 100644 knowledge/static/src/components/behaviors/view_link_behavior/view_link_behavior.xml create mode 100644 knowledge/static/src/components/chatter_panel/chatter_panel.js create mode 100644 knowledge/static/src/components/chatter_panel/chatter_panel.xml create mode 100644 knowledge/static/src/components/comment/comment.js create mode 100644 knowledge/static/src/components/comment/comment.scss create mode 100644 knowledge/static/src/components/comment/comment.xml create mode 100644 knowledge/static/src/components/comments_handler/comments_handler.js create mode 100644 knowledge/static/src/components/comments_handler/comments_handler.xml create mode 100644 knowledge/static/src/components/comments_panel/comments_panel.js create mode 100644 knowledge/static/src/components/comments_panel/comments_panel.xml create mode 100644 knowledge/static/src/components/comments_popover/comments_popover.js create mode 100644 knowledge/static/src/components/comments_popover/comments_popover.xml create mode 100644 knowledge/static/src/components/composer/composer.js create mode 100644 knowledge/static/src/components/custom_favorite_item/custom_favorite_filter.js create mode 100644 knowledge/static/src/components/custom_favorite_item/custom_favorite_item.xml create mode 100644 knowledge/static/src/components/external_embedded_view_favorite_menu/embedded_view_favorite_menu.js create mode 100644 knowledge/static/src/components/external_embedded_view_favorite_menu/insert_embedded_view.xml create mode 100644 knowledge/static/src/components/external_embedded_view_insertion/views_renderers_patches.js create mode 100644 knowledge/static/src/components/html_field/html_field.js create mode 100644 knowledge/static/src/components/html_field/html_field.xml create mode 100644 knowledge/static/src/components/item_calendar_props_dialog/item_calendar_props_dialog.js create mode 100644 knowledge/static/src/components/item_calendar_props_dialog/item_calendar_props_dialog.scss create mode 100644 knowledge/static/src/components/item_calendar_props_dialog/item_calendar_props_dialog.xml create mode 100644 knowledge/static/src/components/knowledge_article_html_field/knowledge_article_html_field.js create mode 100644 knowledge/static/src/components/knowledge_article_html_field/knowledge_article_html_field.scss create mode 100644 knowledge/static/src/components/knowledge_article_html_field/knowledge_article_html_field.xml create mode 100644 knowledge/static/src/components/knowledge_cover/knowledge_cover.js create mode 100644 knowledge/static/src/components/knowledge_cover/knowledge_cover.xml create mode 100644 knowledge/static/src/components/knowledge_cover/knowledge_cover_dialog.js create mode 100644 knowledge/static/src/components/knowledge_cover/knowledge_cover_dialog.xml create mode 100644 knowledge/static/src/components/knowledge_icon/knowledge_icon.js create mode 100644 knowledge/static/src/components/knowledge_icon/knowledge_icon.xml create mode 100644 knowledge/static/src/components/media_dialog/knowledge_documents_selector.js create mode 100644 knowledge/static/src/components/media_dialog/knowledge_media_dialog.js create mode 100644 knowledge/static/src/components/message/message_patch.js create mode 100644 knowledge/static/src/components/move_article_dialog/move_article_dialog.js create mode 100644 knowledge/static/src/components/move_article_dialog/move_article_dialog.xml create mode 100644 knowledge/static/src/components/permission_panel/permission_panel.js create mode 100644 knowledge/static/src/components/permission_panel/permission_panel.scss create mode 100644 knowledge/static/src/components/permission_panel/permission_panel.xml create mode 100644 knowledge/static/src/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog.js create mode 100644 knowledge/static/src/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog.xml create mode 100644 knowledge/static/src/components/properties_panel/properties_panel.js create mode 100644 knowledge/static/src/components/properties_panel/properties_panel.scss create mode 100644 knowledge/static/src/components/properties_panel/properties_panel.xml create mode 100644 knowledge/static/src/components/sidebar/sidebar.js create mode 100644 knowledge/static/src/components/sidebar/sidebar.xml create mode 100644 knowledge/static/src/components/sidebar/sidebar_row.js create mode 100644 knowledge/static/src/components/sidebar/sidebar_row.xml create mode 100644 knowledge/static/src/components/sidebar/sidebar_section.js create mode 100644 knowledge/static/src/components/sidebar/sidebar_section.xml create mode 100644 knowledge/static/src/components/topbar/topbar.js create mode 100644 knowledge/static/src/components/topbar/topbar.xml create mode 100644 knowledge/static/src/components/video_selector_dialog/video_selector_dialog.js create mode 100644 knowledge/static/src/components/video_selector_dialog/video_selector_dialog.scss create mode 100644 knowledge/static/src/components/video_selector_dialog/video_selector_dialog.xml create mode 100644 knowledge/static/src/demo/Onboarding.pdf create mode 100644 knowledge/static/src/demo/fleet_icon.png create mode 100644 knowledge/static/src/discuss/message_actions.js create mode 100644 knowledge/static/src/emoji_picker/emoji_picker_patch.js create mode 100644 knowledge/static/src/emoji_picker/emoji_picker_patch.xml create mode 100644 knowledge/static/src/img/Books.svg create mode 100644 knowledge/static/src/img/computer_monitor_with_code.jpg create mode 100644 knowledge/static/src/img/entrepreneurship_fair.jpg create mode 100644 knowledge/static/src/img/house.jpg create mode 100644 knowledge/static/src/img/odoo_experience.jpg create mode 100644 knowledge/static/src/img/odoo_run.jpg create mode 100644 knowledge/static/src/img/persona_1.jpg create mode 100644 knowledge/static/src/img/persona_2.jpg create mode 100644 knowledge/static/src/img/persona_3.jpg create mode 100644 knowledge/static/src/img/persona_4.jpg create mode 100644 knowledge/static/src/img/podcast.jpg create mode 100644 knowledge/static/src/img/todo.jpg create mode 100644 knowledge/static/src/js/knowledge_clipboard_whitelist.js create mode 100644 knowledge/static/src/js/knowledge_controller.js create mode 100644 knowledge/static/src/js/knowledge_plugin.js create mode 100644 knowledge/static/src/js/knowledge_renderers.js create mode 100644 knowledge/static/src/js/knowledge_utils.js create mode 100644 knowledge/static/src/js/knowledge_views.js create mode 100644 knowledge/static/src/js/knowledge_wysiwyg.js create mode 100644 knowledge/static/src/js/tools/knowledge_tools.js create mode 100644 knowledge/static/src/js/wysiwyg.js create mode 100644 knowledge/static/src/macros/abstract_macro.js create mode 100644 knowledge/static/src/macros/file_macros.js create mode 100644 knowledge/static/src/macros/template_macros.js create mode 100644 knowledge/static/src/macros/utils.js create mode 100644 knowledge/static/src/portal_webclient/main.js create mode 100644 knowledge/static/src/portal_webclient/portal_webclient.js create mode 100644 knowledge/static/src/portal_webclient/portal_webclient.xml create mode 100644 knowledge/static/src/scss/knowledge_blocks.scss create mode 100644 knowledge/static/src/scss/knowledge_common.scss create mode 100644 knowledge/static/src/scss/knowledge_editor.scss create mode 100644 knowledge/static/src/scss/knowledge_print.scss create mode 100644 knowledge/static/src/scss/knowledge_views.dark.scss create mode 100644 knowledge/static/src/scss/knowledge_views.scss create mode 100644 knowledge/static/src/search_model/search_model.js create mode 100644 knowledge/static/src/services/knowledge_commands_service.js create mode 100644 knowledge/static/src/services/knowledge_embedded_filters_service.js create mode 100644 knowledge/static/src/views/embedded_controllers_patch.js create mode 100644 knowledge/static/src/views/embedded_kanban_view.js create mode 100644 knowledge/static/src/views/embedded_list_view.js create mode 100644 knowledge/static/src/views/embedded_view.js create mode 100644 knowledge/static/src/views/item_calendar/item_calendar_model.js create mode 100644 knowledge/static/src/views/item_calendar/item_calendar_view.js create mode 100644 knowledge/static/src/views/item_calendar/item_calendar_views.xml create mode 100644 knowledge/static/src/views/list_view.js create mode 100644 knowledge/static/src/web/chatter_patch.js create mode 100644 knowledge/static/src/web/form_controller_patch.js create mode 100644 knowledge/static/src/webclient/commands/command_category.js create mode 100644 knowledge/static/src/webclient/commands/knowledge_providers.js create mode 100644 knowledge/static/src/xml/form_controller.xml create mode 100644 knowledge/static/src/xml/knowledge_article_templates.xml create mode 100644 knowledge/static/src/xml/knowledge_command_palette.xml create mode 100644 knowledge/static/src/xml/knowledge_controller.xml create mode 100644 knowledge/static/src/xml/knowledge_editor.xml create mode 100644 knowledge/static/src/xml/knowledge_wysiwyg.xml create mode 100644 knowledge/static/tests/form_controller_patch_tests.js create mode 100644 knowledge/static/tests/knowledge_behaviors_upgrade_tests.js create mode 100644 knowledge/static/tests/knowledge_external_embeds_hotkeys.js create mode 100644 knowledge/static/tests/knowledge_form_view_tests.js create mode 100644 knowledge/static/tests/knowledge_wysiwyg_collaboration_tests.js create mode 100644 knowledge/static/tests/mock_services.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_article_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_calendar_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_file_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_index_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_kanban_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_list_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_outline_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_search_favorites_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_table_of_content_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_template_command_tour.js create mode 100644 knowledge/static/tests/tours/commands/knowledge_video_command_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_article_thread_tours.js create mode 100644 knowledge/static/tests/tours/knowledge_cover_picker.js create mode 100644 knowledge/static/tests/tours/knowledge_cover_random_unplash.js create mode 100644 knowledge/static/tests/tours/knowledge_embedded_views_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_history_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_main_flow_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_main_flow_tour_portal.js create mode 100644 knowledge/static/tests/tours/knowledge_pick_emoji_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_properties_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_readonly_favorite_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_readonly_item_kanban_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_readonly_item_list_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_resequence_children_of_readonly_parent_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_sidebar_readonly_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_sidebar_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_template_tour.js create mode 100644 knowledge/static/tests/tours/knowledge_tour_utils.js create mode 100644 knowledge/tests/__init__.py create mode 100644 knowledge/tests/common.py create mode 100644 knowledge/tests/test_knowledge_article_business.py create mode 100644 knowledge/tests/test_knowledge_article_constraints.py create mode 100644 knowledge/tests/test_knowledge_article_internals.py create mode 100644 knowledge/tests/test_knowledge_article_permissions.py create mode 100644 knowledge/tests/test_knowledge_article_sequence.py create mode 100644 knowledge/tests/test_knowledge_article_stage.py create mode 100644 knowledge/tests/test_knowledge_article_template.py create mode 100644 knowledge/tests/test_knowledge_article_thread.py create mode 100644 knowledge/tests/test_knowledge_article_thread_permissions.py create mode 100644 knowledge/tests/test_knowledge_editor_commands.py create mode 100644 knowledge/tests/test_knowledge_form_ui.py create mode 100644 knowledge/tests/test_knowledge_performance.py create mode 100644 knowledge/tests/test_knowledge_security.py create mode 100644 knowledge/tests/test_res_users.py create mode 100644 knowledge/views/knowledge_article_favorite_views.xml create mode 100644 knowledge/views/knowledge_article_member_views.xml create mode 100644 knowledge/views/knowledge_article_stage_views.xml create mode 100644 knowledge/views/knowledge_article_template_category_views.xml create mode 100644 knowledge/views/knowledge_article_views.xml create mode 100644 knowledge/views/knowledge_menus.xml create mode 100644 knowledge/views/knowledge_templates_portal.xml create mode 100644 knowledge/views/portal_templates.xml create mode 100644 knowledge/wizard/__init__.py create mode 100644 knowledge/wizard/knowledge_invite.py create mode 100644 knowledge/wizard/knowledge_invite_views.xml diff --git a/knowledge/__init__.py b/knowledge/__init__.py new file mode 100644 index 0000000..e702d39 --- /dev/null +++ b/knowledge/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from lxml import etree + +from . import controllers +from . import models +from . import populate +from . import wizard + +from odoo.exceptions import UserError + +def pre_init_knowledge(env): + """ Some lxml arm64 versions cannot decode icons and cause the installation to crash. + This will test to decode an emoji before the installation of the app, and show + a helper message if it crashed. + """ + try: + etree.fromstring("
😀
") + except etree.XMLSyntaxError: + raise UserError( + "The version of the lxml package used is not supported. " + "Consider reinstalling lxml package using 'pip install --nobinary :all: lxml'") + +def _init_private_article_per_user(env): + env['res.users'].search([('partner_share', '=', False)])._generate_tutorial_articles() diff --git a/knowledge/__manifest__.py b/knowledge/__manifest__.py new file mode 100644 index 0000000..8a57f0b --- /dev/null +++ b/knowledge/__manifest__.py @@ -0,0 +1,115 @@ +# -*- 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': '1.0', + 'depends': [ + 'web', + 'web_editor', + 'mail', + 'portal', + 'web_unsplash', + 'web_hierarchy', + 'web_map', + ], + 'data': [ + 'data/article_templates.xml', + 'data/ir_config_parameter_data.xml', + 'data/ir_attachment_data.xml', + 'data/knowledge_cover_data.xml', + 'data/knowledge_article_template_category_data.xml', + 'data/knowledge_article_template_data.xml', + 'data/knowledge_article_stage_data.xml', + 'data/ir_actions_data.xml', + 'data/mail_templates.xml', + 'data/mail_templates_email_layouts.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_article_stage_views.xml', + 'views/knowledge_article_template_category_views.xml', + 'views/knowledge_templates_portal.xml', + 'views/knowledge_menus.xml', + 'views/portal_templates.xml', + 'security/ir.model.access.csv', + 'security/ir_rule.xml', + ], + 'installable': True, + 'application': True, + 'license': 'OEEL-1', + 'pre_init_hook': 'pre_init_knowledge', + 'post_init_hook': '_init_private_article_per_user', + 'assets': { + 'web.assets_backend': [ + 'knowledge/static/src/scss/knowledge_common.scss', + 'knowledge/static/src/scss/knowledge_views.scss', + 'knowledge/static/src/scss/knowledge_editor.scss', + 'knowledge/static/src/scss/knowledge_blocks.scss', + 'knowledge/static/src/xml/**/*', + ('remove', 'knowledge/static/src/xml/knowledge_wysiwyg.xml'), + 'knowledge/static/src/components/**/*', + 'knowledge/static/src/discuss/**/*', + 'knowledge/static/src/emoji_picker/**/*', + 'knowledge/static/src/search_model/**/*', + 'knowledge/static/src/web/**/*', + 'knowledge/static/src/js/knowledge_controller.js', + 'knowledge/static/src/js/knowledge_plugin.js', + 'knowledge/static/src/js/knowledge_utils.js', + 'knowledge/static/src/js/knowledge_renderers.js', + 'knowledge/static/src/js/knowledge_views.js', + 'knowledge/static/src/js/tools/**/*', + 'knowledge/static/src/webclient/**/*', + 'knowledge/static/src/views/**/*', + 'knowledge/static/src/services/**/*', + 'knowledge/static/src/macros/**/*', + ], + "web.assets_web_dark": [ + 'knowledge/static/src/scss/knowledge_views.dark.scss', + ], + 'web.assets_frontend': [ + 'knowledge/static/src/scss/knowledge_common.scss', + 'knowledge/static/src/scss/knowledge_blocks.scss', + 'knowledge/static/src/js/tools/**/*', + 'knowledge/static/src/js/knowledge_utils.js', + ], + 'web.assets_tests': [ + 'knowledge/static/tests/tours/**/*', + ], + 'web.qunit_suite_tests': [ + 'knowledge/static/tests/knowledge_form_view_tests.js', + 'knowledge/static/tests/form_controller_patch_tests.js', + 'knowledge/static/tests/knowledge_external_embeds_hotkeys.js', + 'knowledge/static/tests/knowledge_form_view_tests.js', + 'knowledge/static/tests/knowledge_behaviors_upgrade_tests.js', + 'knowledge/static/tests/knowledge_wysiwyg_collaboration_tests.js', + ('include', 'knowledge.assets_wysiwyg'), + ], + 'web.tests_assets': [ + 'knowledge/static/tests/mock_services.js', + ], + 'web_editor.backend_assets_wysiwyg': [ + 'knowledge/static/src/xml/knowledge_wysiwyg.xml', + ], + 'knowledge.assets_wysiwyg': [ + 'knowledge/static/src/js/wysiwyg.js', + 'knowledge/static/src/js/knowledge_wysiwyg.js', + 'knowledge/static/src/xml/knowledge_editor.xml', + 'knowledge/static/src/xml/knowledge_article_templates.xml', + 'knowledge/static/src/js/knowledge_clipboard_whitelist.js', + ], + 'knowledge.webclient': [ + ('include', 'web.assets_backend'), + # knowledge webclient overrides + 'knowledge/static/src/portal_webclient/**/*', + 'web/static/src/start.js', + ], + 'knowledge.assets_knowledge_print': [ + 'knowledge/static/src/scss/knowledge_print.scss', + ] + }, +} diff --git a/knowledge/controllers/__init__.py b/knowledge/controllers/__init__.py new file mode 100644 index 0000000..978d386 --- /dev/null +++ b/knowledge/controllers/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import article_thread +from . import main +from . import knowledge_unsplash +from . import portal diff --git a/knowledge/controllers/article_thread.py b/knowledge/controllers/article_thread.py new file mode 100644 index 0000000..a36ca70 --- /dev/null +++ b/knowledge/controllers/article_thread.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.addons.portal.controllers.mail import MailController +from odoo.addons.knowledge.controllers.main import KnowledgeController +from odoo.http import request +from odoo.addons.mail.controllers.thread import ThreadController +from werkzeug.exceptions import Forbidden + + +class ArticleThreadController(KnowledgeController): + + @http.route('/knowledge/thread/resolve', type='http', auth='user') + def resolve_thread(self, res_id, token): + _, thread, redirect = MailController._check_token_and_record_or_redirect('knowledge.article.thread', int(res_id), token) + if not thread or not thread.article_id.user_can_write: + return redirect + if not thread.is_resolved: + thread.is_resolved = True + return self.redirect_to_article(thread.article_id.id, show_resolved_threads=True) + + +class KnowledgeThreadController(ThreadController): + + @http.route() + def mail_thread_messages(self, thread_model, thread_id, **kwargs): + """Portal users doesn't have access to the mail.message model but we want them to be able to + see the messages from a `knowledge.article.thread` on which they can access, if access rules + applies to them. + So for them, we check if they indeed have access to the article linked to the thread and if + that's the case we sudo the search to return the messages. + """ + if request.env.user._is_portal() and thread_model == 'knowledge.article.thread': + thread = request.env['knowledge.article.thread'].browse(thread_id).exists() + if not thread or not thread.article_id.user_has_access: + raise Forbidden() + domain = [ + ("res_id", "=", int(thread_id)), + ("model", "=", thread_model), + ("message_type", "=", "comment"), # only user input + ("subtype_id", "=", request.env.ref('mail.mt_comment').id), # comments in threads are sent as notes + ("is_internal", "=", False) # respect internal users only flag + ] + res = request.env["mail.message"].sudo()._message_fetch(domain, **kwargs) + return {**res, "messages": res["messages"].message_format()} + return super().mail_thread_messages(thread_model, thread_id, **kwargs) diff --git a/knowledge/controllers/knowledge_unsplash.py b/knowledge/controllers/knowledge_unsplash.py new file mode 100644 index 0000000..a0c6584 --- /dev/null +++ b/knowledge/controllers/knowledge_unsplash.py @@ -0,0 +1,67 @@ +import requests +import werkzeug + +from odoo.addons.web_unsplash.controllers import main +from odoo import http +from odoo.exceptions import AccessError +from odoo.http import request +from werkzeug.urls import url_encode + +# ID of the unsplash collection, used as a fallback for knowledge covers when we can't find a suitable image +UNSPLASH_COLLECTION_ID = 317099 + +class KnowledgeUnsplash(main.Web_Unsplash): + + @http.route('/knowledge/article/+ + This is where you and your team can centralize your + Knowledge + and best practices! 🚀 + +
+
+ This private page is for you to play around with.
+
+ Ready to give it a spin?
+
+ Try the following 👇 +
++ Got it? Now let's try advanced features 🧠 +
+
+ Hello there, I am a template 👋
+
+ Use the buttons at the top-right of this box to re-use my content.
+
+ No more time wasted! 🔥
+
+
+ Want to go
+ even
+ faster? ⚡️
+
+
+
+ Access
+
+ Articles
+
+ by opening the
+
+ Command Palette
+
+ (Ctrl + k/⌘ + k) then search through articles by starting your query with
+ "?".
+
+
+ + 👈 See the + + Menu + + there, on the left? + +
++ + Those are your + Articles. + +
++ Each of them can be used both as: +
++ + To change the way + + Articles + + are organized, you can simply + + Drag & Drop + + them + +
++ + + Articles + + are stored into different + Sections: + +
++ + And again, to move an + + Article + + from a + + Section + + to another, just + + Drag & Drop + + it. + +
++ + A good workflow is to write your drafts in + + Private + + and, once done, move it from + + Private + + to + + Workspace + + to share it with everyone. + +
++ And voilà, it is that simple. +
++ 🚀 +
++ To be sure to stay updated, follow us on + Twitter, + + + YouTube + + + or + Facebook. +
+ + +Please pick the following tasks first:
+ +Otherwise, feel free to handle others listed below:
+ ++ + Productivity is never an accident. It is always the result of a commitment to excellence, intelligent planning, and focused effort. + + - Paul J. Meyer ++
Things to handle this week*
+
+
|
+
+ + *💡 tick the box when the task is scheduled in the agenda + +
++ + + Reminders + + +
++ + + MONDAY 🏢 @office + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + TUESDAY 🏢 @office + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + WEDNESDAY 🏠 @home + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + THURSDAY 🏢 @office + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + FRIDAY 🏠 @home + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + SATURDAY 🏠 @home + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + + SUNDAY 🏠 @home + + +
+⚡ TOP 3 PRIORITIES
+Optional Tasks
+Extra Notes:
++ + List here all the text you could not do this week. These shall be postponed in the next weekly schedule. + +
+Compiled below are tips & tricks collected among our veterans to help newcomers get started. We hope it will help you sign deals.
++ PRO TIP: From a lead, use the book 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 book button in the chatter to find this article and autocomplete your description with this qualification template. +
+
+ 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. + |
+
☝🏼 Please prepare the following before the meeting:
+
+ 🗣 Meeting Agenda+ |
+
+ 🙌🏼 Decisions Taken+ |
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
☝🏼 Please prepare the following before the meeting:
+
+ 🗣 Meeting Agenda+ |
+
+ 🙌🏼 Decisions Taken+ |
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
+
|
+
+ + Planned Next Step: + ++
+
+
+ Fast Facts+ |
+ |
| Company Name: | +|
| Industry: | +|
| Company Location: | +|
| # Employees: | +|
| Company Structure: | +|
| Estimated Revenues: | +|
+ Sales Details+ |
+ |
| Current Contract: | +|
| Contract Due Date: | +|
| Current Satisfaction: | +|
+ Objectives with our Collaboration+ |
+ |
| Issues they are trying to solve: | +|
| How they measure success: | +|
| Potential Risks: | +
+ + + Blue: Expects accurate and rigorous results + + +++ + Green: Values trust above all
+
+ Red: Values results above all
+ + + Yellow: Values creativity and enthusiasm above results + + + +
Main Point of Contact:
+| + + Name + + | ++ + Role + + | ++ + Color + + | +
| Name 1 | +Job Title 1 | +Blue/Green/Red/Yellow | +
| Name 2 | +Job Title 2 | +Blue/Green/Red/Yellow | +
| + + Name + + | ++ + Strengths + + | ++ + Weaknesses + + | +
| Competitor 1 | +||
| Competitor 2 | +||
| Competitor 3 | +
+ What is your action plan to move forward with this account?
+ Which KPI will be used to measure this progress?
+
Please submit a request for assistance to the Marketing Team if you fall into one of the following:
+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. + |
+
+ Here are logos that you can use at your own convenience.
+
+ They can also be shared with customers, journalists and resellers.
+
+ 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 + ++
|
+
+ + Do Not ++
|
+
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.
+
+ + + Describe your campaign in just a few words. + + +
++ + + What are you trying to accomplish with this campaign? + + +
++ + + Who are you trying to reach? + + +
++ + + What are you trying to convince them of? + + +
++ + + How will your measure progress? + + +
+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.
+
+ 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.
+
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 FleetOfficer
+ ++ 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. +
+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. | +
+ + Explain what went wrong in just a few words. + +
++ + Explain the consequences of this issue. + +
++ + How did it get to happen? + +
++ + How was it solved? + +
+Lessons Learnt:
+Actions Taken:
++ Summary: What an exciting release! This time the focus was on... +
++ + Explain in a single sentence what has been changed. + +
++ + List here all the material and documentation related to the task. + +
+ + + ++ + Explain in a few words the problem that this change is going to solve. + +
++ + List here all the changes to implement. + +
++ + Ask a Senior Engineer to fill this part before launching the task. + +
+| + Change + | ++ Complexity + | +
| Change 1 | ++ + | +
| Change 2 | ++ + | +
| Change 3 | ++ + | +
Extra Technical Instructions:+
+ + Lay here any remaining question or doubt. + +
+Q1: Would it be possible to...+
Q2: Would there be an issue if...+
+ + Ask the developer to go through this checklist before asking for a review. + +
+Here is a short guide that will help you pick the right tiny house for you.
++ + 1. How many people will live in this house? + ++
While tiny houses are inherently small, it's important to prioritize comfort when sharing such limited space. To ensure everyone's happiness, we recommend allowing at least 9m² per person. If you plan to live with multiple roommates, our Serenità model is highly recommended, providing ample space for each individual to enjoy
+ + 2. What is your budget? + ++
While tiny houses do offer a more cost-effective alternative to traditional houses, at MyCompany, we prioritize durability, which comes with a price. However, if you're on a tight budget, we have the perfect solution: our Cielo model.
It provides everything you need while allowing you to save.
+ + 3. Which style do you prefer: Natural or Industrial? + ++
Whether your heart leans towards the rustic charm of a wooden hut or the intricate beauty of Victorian steelworks, we have you covered. Desiring a specific option? Rest assured, we are here to fulfill your wishes! Just get in touch with our architects and we will make sure to provide any specific choice you desire.
+ + ++ + 4. What can I do if I haven't found exactly what I wanted?" + ++
If none of those offers convinced you, just get in touch with our team.
At MyCompany, your happiness is our utmost priority, and we'll go the extra mile to make sure you find what you're looking for!
+ + To add more, use the clipboard below 👇🏼 + +
++ + How to best reach this customer type. + +
+
+
+ How to best reach this customer type.
+
+ As an avid music listener, the best way to reach Sonya is through the radio since hers is always on.
+
+
+ How to best reach this customer type.
+
+ Julius follows politics very tightly, and can best be reached with TV ads.
+
+
+ How to best reach this customer type.
+
+ Abigail works a lot and never watches TV. Better reach her on her phone or on her to work.
+
+
+ How to best reach this customer type.
+
+ As a classic Gen Z member, Vittorio never watches TV and never listens to the radio. For him to see our message, we need to get to his Instagram feed.
+
+ Welcome to the last edition of the MyCompany Newsletter!
+ We are very excited to share with you the hottest updates and news! 🤩
+
The podcast launches its new technical talks, for all tech-savvy listeners out there! This + new episode of the series features Géry, the mastermind behind OWL, the world fastest JS + framework. 🚀
+A must listen for all developers out there and anyone interested in Javascript!
+ 👨💻
+
+ Subscribe here to make sure you will not miss an episode!
+
PLANET ODOO
+
+


This year again, Odoo was present at the Tech and Innovation festival for students in
+ Antwerp, ready to promote our company!
Fabien was also there and gave an interview on the main
+ stage.📢
More than 150 Odooers participated in this years' edition of the run of 5 or 11
+ kilometers.
Starting from the office, they enjoyed a great tour in the countryside before
+ coming back to Grand-Rosière, where they were welcomed with a drink and a burger.🍔
Thank you to everyone involved in the organization of this edition of the + + Odoo Run + !
+We already cannot wait for the next one🏃
+
+ And that's all for this month, folks!
+ Thanks for reading, see you soon.👋
+
+
|
+
+
+
+
+
+
+ |
| + + |
| + + | +
| + + | +
%s
") % message_body) + return changes, tracking_value_ids + + def _send_invite_mail(self, partners, permission, message=None): + self.ensure_one() + + partner_to_bodies = {} + for partner in partners: + partner_to_bodies[partner] = self.env['ir.qweb'].with_context(lang=partner.lang)._render( + 'knowledge.knowledge_article_mail_invite', + { + 'record': self, + 'user': self.env.user, + 'permission': permission, + 'message': message, + } + ) + + if self.display_name: + subject = _('Article shared with you: %s', self.display_name) + else: + subject = _('Invitation to access an article') + + if permission == 'read': + permission_label = _('Read') + else: + permission_label = _('Write') + + for partner, body in partner_to_bodies.items(): + self.with_context(lang=partner.lang).message_notify( + body=body, + email_layout_xmlid='mail.mail_notification_layout', + partner_ids=partner.ids, + subject=subject, + subtitles=[self.display_name, _('Your Access: %s', permission_label)], + ) + + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): + groups = super()._notify_get_recipients_groups(message, model_description, msg_vals=msg_vals) + if not self or not msg_vals.get('partner_ids'): + return groups + new_group = [] + for member in self.article_member_ids.filtered( + lambda member: member.partner_id.id in msg_vals['partner_ids'] and member.partner_id.partner_share + ): + url = url_join( + self.get_base_url(), + f"/knowledge/article/invite/{member.id}/{member._get_invitation_hash()}" + ) + new_group.append( + (f'group_knowledge_member_{member.id}', lambda pdata: pdata['id'] == member.partner_id.id, { + 'has_button_access': True, + 'button_access': { + 'url': url, + }, + }) + ) + return new_group + groups + + def _send_trash_notifications(self): + """ This method searches all the partners that should be notified about + articles have been trashed. As each partner to notify may have different + accessible articles depending on their rights, for each partner, we need + to retrieve the first accessible article that will be considered for + them as the root trashed article. A notification is sent to each partner + to notify with the list of their own accessible articles.""" + partners_to_notify = self.article_member_ids.filtered( + lambda member: member.permission in ['read', 'write'] + ).partner_id + + KnowledgeArticle = self.env["knowledge.article"].with_context(active_test=False, allowed_company_ids=[]) + sent_messages = self.env['mail.message'] + for partner in partners_to_notify.filtered(lambda p: not p.partner_share): + # if only one article, all the partner_to_notify have access to the article. + if len(self) == 1: + main_articles, children = self, KnowledgeArticle + else: + # Current partner may have no access to some of the articles_to_notify. + # Get all accessible articles for the current partner + partner_user = partner.user_ids.filtered(lambda u: not u.share)[0] + accessible_articles = KnowledgeArticle.with_user(partner_user).search( + [('id', 'in', self.ids)] + ) + + # "Main articles" are articles that: + # - has no parent + # - OR the current partner as no access to their parent + # - OR the parent article can be accessed but is not archived. + main_articles = accessible_articles.sudo().filtered( + lambda a: a.parent_id not in accessible_articles + ) + children = accessible_articles - main_articles + + # Set the partner lang in context to send mail in partner lang. + partner_lang = get_lang(self.env, lang_code=partner.lang).code + self = self.with_context(lang=partner_lang) # force translation of subject + + if len(main_articles) == 1: + subject = _("%s has been sent to Trash", main_articles.name or _("Untitled")) + else: + subject = _("Some articles have been sent to Trash") + + body = self.env['ir.qweb'].with_context(lang=partner_lang)._render( + 'knowledge.knowledge_article_trash_notification', { + 'articles': main_articles, + 'recipient': partner, + 'child_articles': children, + }) + + # If multiple "main articles", to keep sending only one mail, + # don't link the notification to any document. + document_to_notify = main_articles if len(main_articles) == 1 else self.env['mail.thread'] + sent_messages += document_to_notify.with_context(lang=partner_lang).message_notify( + body=body, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=partner.ids, + subject=subject, + ) + + return sent_messages + + # ------------------------------------------------------------ + # BUSINESS METHODS + # ------------------------------------------------------------ + + def create_article_from_template(self): + self.ensure_one() + article = self.env["knowledge.article"].article_create(is_private=True) + article.apply_template(self.id, skip_body_update=False) + return article.id + + def apply_template(self, template_id, skip_body_update=False): + """Applies the given template on the current article + :param int template_id: Template id + :param boolean skip_body_update: Whether the method should skip writing + the body and return it for further management by the caller. Note that + this does to apply to child articles as they are not managed the same + way and are side records. Typically + - False: when creating a template based article from scratch; + - True: in other cases to avoid collaborative issues (write on + body should be done at client side); + :return str: body of the article, used notably client side for + collaborative mode + """ + self.ensure_one() + template = self.env['knowledge.article'].browse(template_id) + template.ensure_one() + + # The following algorithm will proceed in 3 steps: + # 1. In the first step, we will recursively create the articles and the + # stages following the same structure as the templates. This will + # ensure that the records exist in the database for the following steps. + # 2. In the second step, we will build a dict mapping the template + # xml ids with the article ids created from it. The dict will be + # used to convert the template xml ids mentionned in the templates + # with the ids of the articles generated from them. + # 3. In the third step, we will populate the articles using the values + # set on the associated templates. + + # Step 1: Create the articles and the stages + + template_to_article_pairs = [] + stack = [(template, self)] + + while stack: + (parent_template, parent_article) = stack.pop() + template_to_article_pairs.append((parent_template, parent_article)) + + # Create the stages: + parent_template_stages = self.env['knowledge.article.stage'].search([ + ('parent_id', '=', parent_template.id) + ]) + parent_article_stages = self.env['knowledge.article.stage'].create([{ + 'name': stage.name, + 'sequence': stage.sequence, + 'fold': stage.fold, + 'parent_id': parent_article.id + } for stage in parent_template_stages]) + + # Create the child articles: + child_templates = parent_template.child_ids.sorted( + lambda template: (template.write_date, template.id)) + if not child_templates: + continue + + child_articles_values = [] + for template in child_templates: + article_values = { + 'is_article_item': template.is_article_item, + 'parent_id': parent_article.id, + } + article_stage = next((article_stage for (article_stage, template_stage) in \ + zip(parent_article_stages, parent_template_stages) \ + if template_stage == template.stage_id), False) + if article_stage: + article_values['stage_id'] = article_stage.id + child_articles_values.append(article_values) + + child_articles = self.env['knowledge.article'].create(child_articles_values) + stack.extend(zip(child_templates, child_articles)) + + # Step 2: Build the dict mapping the template xml ids with the article ids + + template_xml_id_to_article_id_mapping = {} + all_ir_model_data = self.env['ir.model.data'].sudo().search([ + ('model', '=', 'knowledge.article'), + ('res_id', 'in', [template.id for (template, _) in template_to_article_pairs]) + ]) + + for (template, article) in template_to_article_pairs: + ir_model_data = all_ir_model_data.filtered( + lambda ir_model_data: ir_model_data.res_id == template.id) + + if ir_model_data: + template_xml_id = 'knowledge.' + ir_model_data.name + template_xml_id_to_article_id_mapping[template_xml_id] = article.id + + # When rendering the template, the `ref` function should return the id + # of the article created from the template having the given xml id. + # This will ensure that the ids stored in the body of the newly created + # article will refer to the right article and not to the original template. + + def ref(xml_id): + return template_xml_id_to_article_id_mapping[xml_id] \ + if xml_id in template_xml_id_to_article_id_mapping \ + else self.env.ref(xml_id).id + + # Step 3: Copy the template values to the new articles + + (root_template, root_article) = template_to_article_pairs.pop(0) + for (template, article) in reversed(template_to_article_pairs): + article.write({ + 'article_properties': template.article_properties or {}, + 'article_properties_definition': template.article_properties_definition, + 'body': template._render_template(ref), + 'cover_image_id': template.cover_image_id.id, + 'full_width': template.full_width, + 'icon': template.icon, + 'name': template.template_name, + }) + + values = { + 'article_properties': root_template.article_properties or {}, + 'article_properties_definition': root_template.article_properties_definition, + 'cover_image_id': root_template.cover_image_id.id, + 'full_width': root_template.full_width, + 'icon': root_template.icon, + 'name': root_article.name or root_template.template_name, + } + body = root_template._render_template(ref) + if not skip_body_update: + values['body'] = body + root_article.write(values) + + return body + + def _render_template(self, ref=False): + """ + Generates the HTML body based on the template content. + :param callable ref: The `ref` function will be used to refer to an + external record and integrate advanced elements such as embedded views + of article items and article links. + """ + self.ensure_one() + if not self.is_template or not self.template_body: + return False + + if not ref: + def ref(xml_id): + return self.env.ref(xml_id).id + + def transform_xmlid_to_res_id(match): + return str(ref(match.group('xml_id'))) + + fragment = html.fragment_fromstring(self.template_body, create_parent='div') + for element in fragment.xpath('//*[@data-behavior-props]'): + # When encoding the "behavior props", we find and replace the function + # calls of `ref` with the ids returned by the given `ref` function for + # the given xml ids. The generated HTML will then only contain ids. + # Example: + # When the "behavior props" contains `ref('knowledge.article_template_1')`, + # we replace that string occurence with the id returned by the given + # `ref` function evaluated with the xml_id 'knowledge.article_template_1'. + behavior_props = ast.literal_eval(re.sub( + r'(?\w+\.\w+)\'\)', + transform_xmlid_to_res_id, + element.get('data-behavior-props'))) + element.set('data-behavior-props', + parse.quote(json.dumps(behavior_props), safe="()*!'")) + if 'o_knowledge_behavior_type_article' in element.get('class'): + element.set('href', '/knowledge/article/%s' % (behavior_props.get('article_id'))) + + return ''.join(html.tostring(child, encoding='unicode', method='html') \ + for child in fragment.getchildren()) # unwrap the elements from the parent node + + def create_default_item_stages(self): + """ Need to create stages if this article has no stage yet. """ + stage_count = self.env['knowledge.article.stage'].search_count( + [('parent_id', '=', self.id)]) + if not stage_count: + self.env['knowledge.article.stage'].create([{ + "name": stage_name, + "sequence": sequence, + "parent_id": self.id, + "fold": fold + } for stage_name, sequence, fold in [ + (_("New"), 0, False), (_("Ongoing"), 1, False), (_("Done"), 2, True)] + ]) + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + @api.model + def _extract_icon_from_name(self, name): + """ See name_create / _name_search overrides for details. """ + if not isinstance(name, str) or len(name) < 3: + return name, None + + # we consider that a non-alphabetical and non-special character is an emoji + emoji_match = re.match(r'([^\w.,;:_%+!\\/@$€#&()*=~-]) (.*)', name) + if not emoji_match or len(emoji_match.groups()) != 2: + return name, None + + emoji = emoji_match.groups(1)[0] + article_name = emoji_match.groups(1)[1] + return article_name, emoji + + def _get_ancestor_ids(self): + """ Return the union of sets including the ids for the ancestors of + records in recordset. E.g., + * if self = Article `8` which has for parent `4` that has itself + parent `2`, return `{2, 4}`; + * if article `11` is a child of `6` and is also in `self`, return + `{2, 4, 6}`; + + :rtype: set + """ + ancestor_ids = set() + for article in self: + if article.id in ancestor_ids: + continue + for ancestor_id in map(int, article.parent_path.split('/')[-3::-1]): + if ancestor_id in ancestor_ids: + break + ancestor_ids.add(ancestor_id) + return ancestor_ids + + 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), ('is_article_active', '=', True) + ], limit=1).article_id + if not article: + # retrieve workspace articles first, then private/shared ones. + article = self.search( + expression.AND([ + [('parent_id', '=', False), ('is_template', '=', False)], + self._get_read_domain(), + ]), + 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=[ + '&', '&', '&', '&', + ('is_template', '=', False), + ('name', 'ilike', search_term), + ('id', 'not in', self.ids), + '!', ('parent_id', 'child_of', self.ids), + ('user_has_access', '=', True), + ], + 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)]) + + @api.model + def get_empty_list_help(self, help_message): + # Meant to target knowledge_article_action_trashed action only. + # -> Use the specific context key of that action to target it. + if not "search_default_filter_trashed" in self.env.context: + return super().get_empty_list_help(help_message) + get_param = self.env['ir.config_parameter'].sudo().get_param + limit_days = get_param('knowledge.knowledge_article_trash_limit_days') + try: + limit_days = int(limit_days) + except ValueError: + limit_days = self.DEFAULT_ARTICLE_TRASH_LIMIT_DAYS + title = _("No Article in Trash") + description = Markup( + _("""Deleted articles are stored in Trash an extra %(threshold)s days + before being permanently removed for your database""")) % {"threshold": limit_days} + + return super().get_empty_list_help( + f'{title}
{description}
' + ) + + def get_visible_articles(self, root_articles_ids, unfolded_ids): + """ Get the articles that are visible in the sidebar with the given + root articles and unfolded ids. + + An article is visible if it is a root article, or if it is a child + article (not item) of an unfolded visible article. + """ + if root_articles_ids: + visible_articles_domain = [ + '|', + ('id', 'in', root_articles_ids), + '&', + '&', + ('parent_id', 'in', unfolded_ids), + ('id', 'child_of', root_articles_ids), # Don't fetch hidden unfolded + ('is_article_item', '=', False) + ] + + return self.env['knowledge.article'].search( + visible_articles_domain, + order='sequence, id', + ) + return self.env['knowledge.article'] + + def get_sidebar_articles(self, unfolded_ids=False): + """ Get the data used by the sidebar on load in the form view. + It returns some information from every article that is accessible by + the user and that is either: + - a visible root article + - a favorite article or a favorite item (for the current user) + - the current article (except if it is a descendant of a hidden + root article or of an non accessible article - but even if it is + a hidden root article) + - an ancestor of the current article, if the current article is + shown + - a child article of any unfolded article that is shown + """ + + root_articles_domain = [ + ("parent_id", "=", False), + ("is_template", "=", False) + ] + if self.env.user._is_internal(): + # Do not fetch articles that the user did not join (articles with + # internal permissions may be set as visible to members only) + root_articles_domain.append(("is_article_visible", "=", True)) + + # Fetch root article_ids as sudo, ACLs will be checked on next global call fetching 'all_visible_articles' + # this helps avoiding 2 queries done for ACLs (and redundant with the global fetch) + root_articles_ids = self.env['knowledge.article'].sudo().search(root_articles_domain).ids + + favorite_articles_ids = self.env['knowledge.article.favorite'].sudo().search( + [("user_id", "=", self.env.user.id), ('is_article_active', '=', True)] + ).article_id.ids + + # Add favorite articles and items (they are root articles in the + # favorite tree) + root_articles_ids += favorite_articles_ids + + if unfolded_ids is False: + unfolded_ids = [] + + # Add active article and its parents in list of unfolded articles + if self.is_article_visible: + if self.parent_id: + unfolded_ids += self._get_ancestor_ids() + # If the current article is a hidden root article, show the article + elif not self.parent_id and self.id: + root_articles_ids += [self.id] + + all_visible_articles = self.get_visible_articles(root_articles_ids, unfolded_ids) + + return { + "articles": all_visible_articles.read( + ['name', 'icon', 'parent_id', 'category', 'is_locked', 'user_can_write', 'is_user_favorite', 'is_article_item', 'has_article_children'], + None, # To not fetch the name of parent_id + ), + "favorite_ids": favorite_articles_ids, + } diff --git a/knowledge/models/knowledge_article_favorite.py b/knowledge/models/knowledge_article_favorite.py new file mode 100644 index 0000000..071ad6f --- /dev/null +++ b/knowledge/models/knowledge_article_favorite.py @@ -0,0 +1,64 @@ +# -*- 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') + is_article_active = fields.Boolean('Is Article Active', related='article_id.active', + store=True, readonly=True) + 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) + + def resequence_favorites(self, article_ids): + # Some article may not be accessible by the user anymore. Therefore, + # to prevent an access error, one will only resequence the favorites + # related to the articles accessible by the user + sequence = 0 + # Keep the same order as in article_ids + for article_id in article_ids: + self.search([('article_id', '=', article_id), ('user_id', '=', self.env.uid)]).write({"sequence": sequence}) + sequence += 1 diff --git a/knowledge/models/knowledge_article_member.py b/knowledge/models/knowledge_article_member.py new file mode 100644 index 0000000..9e0f2d2 --- /dev/null +++ b/knowledge/models/knowledge_article_member.py @@ -0,0 +1,97 @@ +# -*- 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', + ondelete='cascade', required=True) + partner_id = fields.Many2one( + 'res.partner', 'Partner', + index=True, ondelete='cascade', required=True) + permission = fields.Selection( + [('write', 'Can edit'), + ('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) + ) + + 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/knowledge/models/knowledge_article_stage.py b/knowledge/models/knowledge_article_stage.py new file mode 100644 index 0000000..affbdae --- /dev/null +++ b/knowledge/models/knowledge_article_stage.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class KnowledgeStage(models.Model): + _name = "knowledge.article.stage" + _description = "Knowledge Stage" + _order = 'parent_id, sequence, id' + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(default=1) + fold = fields.Boolean("Folded in kanban view") + parent_id = fields.Many2one("knowledge.article", string="Owner Article", + required=True, ondelete="cascade", help="Stages are shared among a" + "common parent and its children articles." + ) diff --git a/knowledge/models/knowledge_article_template_category.py b/knowledge/models/knowledge_article_template_category.py new file mode 100644 index 0000000..7311338 --- /dev/null +++ b/knowledge/models/knowledge_article_template_category.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ArticleTemplateCategory(models.Model): + """This model represents the categories of the article templates.""" + _name = "knowledge.article.template.category" + _description = "Article Template Category" + _order = "sequence ASC, id ASC" + + name = fields.Char(string="Title", translate=True, required=True) + sequence = fields.Integer("Category Sequence", default=0, required=True, + help="It determines the display order of the category") diff --git a/knowledge/models/knowledge_article_thread.py b/knowledge/models/knowledge_article_thread.py new file mode 100644 index 0000000..e5ea408 --- /dev/null +++ b/knowledge/models/knowledge_article_thread.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class KnowledgeArticleThread(models.Model): + """ + This is the model for a comment thread linked to a `knowledge.article`. Each thread inherits + the `mail.thread` mixin. + + These threads allow end-users to discuss specific parts of the body of a knowledge article. + Which enables reviews, taking notes, pinging a colleague to get more information on a topic, ... + + Each initial comment starts its own thread, which will then accumulate replies, reactions, etc. + It is also possible to mark a thread as closed so that it no longer appears inside the editor + of the article if the conversation does not need to be continued. + """ + _name = "knowledge.article.thread" + _description = "Article Discussion Thread" + _inherit = ['mail.thread'] + _mail_post_access = 'read' # if you can read, you can post a message on an article thread + _order = 'write_date desc, id desc' + _rec_name = 'display_name' + + article_id = fields.Many2one('knowledge.article', ondelete="cascade", readonly=True, required=True) + is_resolved = fields.Boolean("Thread Closed", tracking=True) + + @api.depends('article_id') + def _compute_display_name(self): + for record in self: + record.display_name = record.article_id.display_name + + def toggle_thread(self): + """Toggles the resolution state of the article""" + self.ensure_one() + self.is_resolved = not self.is_resolved + + + +# =========================================================================== +# CRUD METHODS +# =========================================================================== + + @api.model_create_multi + def create(self, vals_list): + return super(KnowledgeArticleThread, self.with_context(mail_create_nolog=True)).create(vals_list) + +# ========================================================================== +# THREAD OVERRIDES +# ========================================================================== + + def message_post(self, **kwargs): + """This function overrides the 'mail.thread' message_post in order to let portal users that + have access to an article to post a message in the thread. + We need to apply this method with sudo for portal users because they do not have access to the + `mail.message` model, which is needed to post the message. + This idea is based on the method `portal_chatter_post` which needs to check access rights in + order to let the portal post in the chatter. + + Before posting as a portal we filter what's being sent to lessen security risks. Notably + partner_ids should be a list of ids (not the records themselves) so that we don't allow command + executions, even with the sudo call. + """ + self.ensure_one() + if self.env.user._is_portal() and self.article_id.user_has_access: + authorized_keys = {'body', 'partner_ids', 'author_id'} + return super(KnowledgeArticleThread, self.sudo()).message_post( + **{key: kwargs.get(key) for key in authorized_keys}, + message_type='comment', subtype_xmlid='mail.mt_comment' + ) + return super().message_post(**kwargs) + + def _get_access_action(self, access_uid=None, force_website=False): + self.ensure_one() + user = self.env['res.users'].sudo().browse(access_uid) if access_uid else self.env.user + action = { + 'type': 'ir.actions.act_url', + 'url': f'/knowledge/article/{self.article_id.id}', + } + if access_uid is None: + action['target_type'] = 'public' + if self.article_id.with_user(user).user_has_access or access_uid is None: + return action + return super()._get_access_action(access_uid=access_uid, force_website=force_website) + + def _notify_thread_by_email(self, message, recipients_data, **kwargs): + """We need to override this method to set our own mail template to be sent to users that + have been tagged inside a comment. We are using the template 'knowledge.knowledge_mail_notification_layout' + which is a simple template comprised of the comment sent and the person that tagged the notified user. + """ + + kwargs['msg_vals'] = {**kwargs['msg_vals'], 'email_layout_xmlid': 'knowledge.knowledge_mail_notification_layout'} + + return super()._notify_thread_by_email(message, recipients_data, **kwargs) + + def _message_compute_subject(self): + self.ensure_one() + return _('New Mention in %s') % self.display_name + + def _notify_get_recipients_groups(self, message, model_description, msg_vals=None): + groups = super()._notify_get_recipients_groups( + message, model_description, msg_vals=msg_vals + ) + if message.model != 'knowledge.article.thread': + return groups + + self.ensure_one() + action = self._notify_get_action_link('controller', controller='/knowledge/thread/resolve', **msg_vals) + user_actions = [{'url': action, 'title': _('Mark Comment as Closed')}] + + new_groups = [( + 'group_knowledge_article_thread_portal_and_users', + lambda pdata: + pdata['uid'] and self.article_id.with_user(pdata['uid']).user_has_access, + { + 'actions': user_actions, + 'active': True, + 'has_button_access': True, + } + )] + + return new_groups + groups diff --git a/knowledge/models/knowledge_cover.py b/knowledge/models/knowledge_cover.py new file mode 100644 index 0000000..4944c25 --- /dev/null +++ b/knowledge/models/knowledge_cover.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class Cover(models.Model): + _name = "knowledge.cover" + _description = "Knowledge Cover" + + attachment_id = fields.Many2one("ir.attachment", string="Cover attachment", required=True, ondelete="cascade") + article_ids = fields.One2many("knowledge.article", "cover_image_id", string="Articles using cover") + attachment_url = fields.Char("Cover URL", compute="_compute_attachment_url", store=True) + + @api.depends('attachment_id') + def _compute_attachment_url(self): + # Add an url for frontend access. + for cover in self: + if cover.attachment_id.url: + cover.attachment_url = cover.attachment_id.url + else: + access_token = cover.attachment_id.generate_access_token()[0] + cover.attachment_url = "/web/image/%s?access_token=%s" % (cover.attachment_id.id, access_token) + + @api.model_create_multi + def create(self, vals_list): + """ Create the covers, then link the attachments used to the created + records, because when uploading a new cover, the attachment is uploaded + with res_id=0, then the cover is created using the uploaded attachment. + """ + if any(len(vals) == 1 and 'name' in vals for vals in vals_list): + raise UserError(_('You cannot create a new Knowledge Cover from here.')) + covers = super().create(vals_list) + + for cover in covers.filtered(lambda cover: not cover.attachment_id.res_id): + cover.attachment_id.write({'res_model': 'knowledge.cover', 'res_id': cover.id, }) + + return covers + + @api.autovacuum + def _gc_unused_covers(self): + return self.with_context(active_test=False).search([('article_ids', '=', False)]).unlink() diff --git a/knowledge/models/res_partner.py b/knowledge/models/res_partner.py new file mode 100644 index 0000000..a2142e9 --- /dev/null +++ b/knowledge/models/res_partner.py @@ -0,0 +1,16 @@ +# -*- 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/knowledge/models/res_users.py b/knowledge/models/res_users.py new file mode 100644 index 0000000..a75d1d0 --- /dev/null +++ b/knowledge/models/res_users.py @@ -0,0 +1,50 @@ +# -*- 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', + 'is_article_visible_by_everyone': False, + '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/knowledge/populate/__init__.py b/knowledge/populate/__init__.py new file mode 100644 index 0000000..c81b658 --- /dev/null +++ b/knowledge/populate/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import knowledge_article +from . import tools diff --git a/knowledge/populate/knowledge_article.py b/knowledge/populate/knowledge_article.py new file mode 100644 index 0000000..18e6426 --- /dev/null +++ b/knowledge/populate/knowledge_article.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.tools import populate +from odoo.addons.base.models.ir_qweb_fields import nl2br +from odoo.addons.knowledge.populate import tools + + +class KnowledgeArticle(models.Model): + _inherit = 'knowledge.article' + _populate_dependencies = [ + 'res.users', # membership management + ] + # be careful that size only defines the amount of ROOT articles + # there will be a lot more articles in the end, counting children + # see underlying docstring in '_prepare_children_articles' + # manual testing gives around 3000-4000 generated articles for "medium" size + # (giving a bigger size also increases the amount of res.users we have for members config) + # /!\ using "large" can take a very long time as we can't take advantage of the batching + # because of parent / children relationship, everything is committed in one singular batch + _populate_sizes = { + 'small': 5, # will result in ~1.500 + 'medium': 20, # will result in ~5.000 records + 'large': 100, # will result in ~27.000 records + } + + def _populate(self, size): + # create some dummy portal users as res.users populate only creates internal users + self.env['res.users'].create([{ + 'login': 'portal_user_knowledge_%i' % i, + 'email': 'portaluserknowledge%i@example.com' % i, + 'name': 'Portal User %i' % i, + 'groups_id': [ + (6, 0, [self.env.ref('base.group_portal').id]), + ] + } for i in range(self.env['res.users']._populate_sizes[size])]) + + return super()._populate(size) + + def _populate_factories(self, depth=0): + internal_partner_ids = self.env['res.users'].search([ + ('share', '=', False), + ]).partner_id.ids + shared_partner_ids = self.env['res.users'].search([ + ('share', '=', True), + ]).partner_id.ids + + return self._populate_article_factories(depth, internal_partner_ids, shared_partner_ids) + + def _populate_article_factories(self, depth, internal_partner_ids, shared_partner_ids): + random = populate.Random('articles') + if depth == 0: + names = populate.iterate(tools._a_title_root) + elif depth == 1: + names = populate.iterate(tools._a_title_top_level) + elif depth == 2: + names = populate.iterate(tools._a_title_leaf) + else: + names = populate.randomize(tools._a_title_low_level, seed=random.randint(1, 100)) + + return { + ('name', names), + ('body', populate.randomize([ + nl2br('%s' % tuple(tools.a_body_content_lorem_ipsum[:1])), + nl2br('%s %s' % tuple(tools.a_body_content_lorem_ipsum[:2])), + nl2br('%s %s %s' % tuple(tools.a_body_content_lorem_ipsum[:3])), + ])), + ('child_ids', populate.compute(lambda *args, **kwargs: self._prepare_children_articles(depth, internal_partner_ids, shared_partner_ids))), + ('icon', populate.randomize(['🗒️', '🤖', '⭐', '🚀', '🎉', '☕', '🏆', '🛫', '💰', '📫'])), + ('internal_permission', populate.compute(lambda *args, **kwargs: self._generate_internal_permission(depth))), + ('is_locked', populate.randomize([True, False], [0.02, 0.98], seed=random.randint(1, 100))), + ('full_width', populate.randomize([True, False], [0.2, 0.8], seed=random.randint(1, 100))), + ('article_member_ids', populate.compute(lambda random, values, **kwargs: self._prepare_member_ids(depth, values, internal_partner_ids, shared_partner_ids))), + ('is_locked', populate.randomize([True, False], [0.02, 0.98])), + ('full_width', populate.randomize([True, False], [0.2, 0.8])), + # TODO add article items / fill some properties fields + ('favorite_ids', populate.compute(lambda values, *args, **kwargs: self._prepare_favorites(values, internal_partner_ids, shared_partner_ids))), + } + + def _prepare_favorites(self, values, internal_partner_ids, shared_partner_ids): + """In Knowledge we offer the possibility for a user to set articles as favorites. + To handle this in the model we have a table `knowledge.article.favorite` that stores every + favorited article for each user. + + Every article has a chance to be the favorite of a user but it is more likely that user will add + a root as a favorite than a child. + Thus to prepare the favorites we will need some information: + * Will the article be favorited by a user ? + * Which user wants to add the article to its favorite ? + + Of course we do not want a lot of articles being favorited so the initial check will reject many of the attempts, + but if we want to add some favorite_ids to an article then we will check how many users will add the article as + favorite. + This is how we will populate the table.""" + random = populate.Random('favorites') + + article_member_ids = values.get('article_member_ids', False) + internal_permission = values.get('internal_permission', False) + # 2% of the articles can be set as favorites + can_be_favorite = random.choices([True, False], weights=[0.02, 0.98], k=1)[0] and ( + internal_permission or article_member_ids) + if not can_be_favorite: + return [] + + admin_partner_id = self.env.ref('base.user_admin').partner_id.id + + # sampling some partners + internals = random.sample(internal_partner_ids, k=min(random.randint(2, 10), len(internal_partner_ids))) + externals = random.sample(shared_partner_ids, k=min(random.randint(2, 10), len(shared_partner_ids))) + # 50% chance to force admin into the mix (easier for testing) + if admin_partner_id not in internals and random.choices([True, False], weights=[0.5, 0.5], k=1)[0]: + internals.append(admin_partner_id) + + # If there are article members, check their permissions, otherwise, all are favorites + all_partners = internals + externals + favorite_partner_ids, partners_to_check_access = ([], all_partners) if article_member_ids else (all_partners, []) + + for partner_id in partners_to_check_access: + member_access = next( + ( + member[2]['permission'] + for member in article_member_ids + if member[2]['partner_id'] == partner_id + ), + False + ) + + if member_access == 'none': + continue # specified no access for this partner -> pass + + if internal_permission == 'none' and not member_access: + continue # internal permission denies access -> pass + + favorite_partner_ids.append(partner_id) + + # sadly the knowledge.article.favorite model needs user_id, so we have to fetch them + linked_users = self.env['res.users'].search([('partner_id', 'in', favorite_partner_ids)]) + user_per_partner = { + user.partner_id.id: user + for user in linked_users + } + + return [(0, 0, { + 'user_id': user_per_partner[partner_id].id + }) for partner_id in favorite_partner_ids] + + def _prepare_children_articles(self, depth, internal_partner_ids, shared_partner_ids): + """ As knowledge.article is a bit meaningless without a parent / children configuration, + this methods aims to fill-up child_ids recursively. + + As the regular populate only allows to specify the 'total amount of records' we want to + create, and it does not handle parent / children relationship, we apply a specific logic for + children articles. + + The idea is to always have children for root articles and then lower the chances of generating + children as you go into higher 'depth', with a maximum of 5 levels in total. + The amount of children is chosen randomly between 2 and 10. + + The code that generates values re-uses the '_populate_factories' method and increases the depth + every time we loop. """ + random = populate.Random('childrenarticles') + + if depth > 4 or random.randint(1, depth + 1) != depth + 1: + # higher depth means lower chance of having children articles + # depth of 0 -> 100% (randint(1,1) needs to equal 1) + # depth of 1 -> 50% (randint(1,2) needs to equal 2) + # depth of 2 -> 33% (randint(1,2,3) needs to equal 3) + # ... + return [] + + record_count = 0 + create_values = [] + field_generators = self._populate_article_factories(depth + 1, internal_partner_ids, shared_partner_ids) + + generator = populate.chain_factories(field_generators, self._name) + for _i in range(random.randint(2, 10)): + values = next(generator) + values.pop('__complete') + create_values.append((0, 0, values)) + record_count += 1 + + return create_values + + def _generate_internal_permission(self, depth): + random = populate.Random('internalpermission') + if depth != 0: + # we keep it simple and only set custom internal permission to root articles + return False + + category = random.choices(['workspace', 'private'], weights=[0.9, 0.1], k=1)[0] + if category == 'private': + # private articles should be 'none' and handled at members level + return 'none' + + # 80% will be write, 20% read + return random.choices(['write', 'read'], weights=[0.8, 0.2], k=1)[0] + + def _prepare_member_ids(self, depth, values, internal_partner_ids, shared_partner_ids): + random = populate.Random('members') + private_member_values = [] + + admin_partner_id = self.env.ref('base.user_admin').partner_id.id + if values.get('internal_permission') == 'none': + # private article -> keep it simple and assume that people will test with admin user + # there will be one auto-generated private article for every user anyway (so size is already big) + private_member_values = [(0, 0, { + 'partner_id': admin_partner_id, + 'permission': 'write' + })] + + # we tweak the end range, so that the deeper you go, the less chance you have of having + # members configuration + randint = random.randint(1, (depth + 1) * 10) + if randint > 4 and values.get('internal_permission') == 'write': + # 60% base chance of not having member configuration + # unless we don't have a 'write' internal permission, in which case we need members + return private_member_values + + internal_partner_ids = random.sample(internal_partner_ids, k=min(random.randint(2, 10), len(internal_partner_ids))) + members_values = [] + for partner_id in internal_partner_ids: + if partner_id == admin_partner_id and private_member_values: + continue # avoid duplicate member + + # force one write members, otherwise 70% of write, 25% read, 5% none + permission = 'write' if partner_id == internal_partner_ids[0] else random.choices( + ['write', 'read', 'none'], weights=[70, 25, 5], k=1 + )[0] + + members_values.append({ + 'partner_id': partner_id, + 'permission': permission + }) + + internal_member_ids = [ + (0, 0, member_values) + for member_values in members_values + ] + + if randint > 2 or values.get('internal_permission') == 'read': + # 20% base chance of having only internal members + return private_member_values + internal_member_ids + + # 10% base chance of having only external members + shared_partner_ids = random.sample(shared_partner_ids, k=min(random.randint(2, 10), len(shared_partner_ids))) + members_values = [] + for partner_id in shared_partner_ids: + # 90% of write + members_values.append({ + 'partner_id': partner_id, + 'permission': random.choices(['read', 'none'], weights=[0.9, 0.1], k=1)[0] + }) + + shared_member_ids = [ + (0, 0, member_values) + for member_values in members_values + ] + + if randint == 2 and values.get('internal_permission') == 'write': + # 10% base chance of having only external members (only for write internal permission) + return shared_member_ids + + # 10% chance to have a mix of both + return private_member_values + internal_member_ids + shared_member_ids diff --git a/knowledge/populate/tools.py b/knowledge/populate/tools.py new file mode 100644 index 0000000..e9ca0ac --- /dev/null +++ b/knowledge/populate/tools.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# article +_a_title_root = [ + 'HR', 'Accounting', 'Company', 'Day 2 Day', + 'Branding', 'Business', 'Marketing' +] + +_a_title_top_level = [ + 'FAQ', 'Meetings', 'TODOs', 'Documentation', + 'Ideas', '(To Sort / Remove)' +] + +_a_title_leaf = [ + 'Global explanation', 'In Details', 'I have more Questions?', + 'Going further', 'Closing Words' +] + +_a_title_low_level = [ + 'Friday Meeting Notes', 'Contacts Information', '[TO CLEAN]', + 'Untitled', 'Work In Progress' +] + +# 3 paragraphs to combine into an article body +a_body_content_lorem_ipsum = [""" ++
+
+
+
+
+ Highlight content and use the button to add comments +
+Start typing to continue with an empty page or pick an option below to get started.
+ +
+ Organize your database with custom fields
+ (Text, Selection, ...).
+ Those fields will be available on all articles that share the same parent.
+