diff --git a/website_sale_dynamic_review_snippet/README.rst b/website_sale_dynamic_review_snippet/README.rst new file mode 100644 index 0000000000..5ddcdc10d6 --- /dev/null +++ b/website_sale_dynamic_review_snippet/README.rst @@ -0,0 +1,164 @@ +=================================== +Website Sale Dynamic Review Snippet +=================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d43413aa9ae52f19e0b4b115657a500479b34562b7628473e4cdc7c3007976b2 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github + :target: https://github.com/OCA/e-commerce/tree/18.0/website_sale_dynamic_review_snippet + :alt: OCA/e-commerce +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/e-commerce-18-0/e-commerce-18-0-website_sale_dynamic_review_snippet + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +| This module provides a new website snippet that aggregates customer + reviews from product templates. +| It allows website administrators to display up to 100 reviews from one + or more products in a single dynamic block, directly within the Odoo + Website Builder. + +| The snippet offers configuration options (maximum number of reviews) + and renders reviews with rating stars, reviewer names, comments, and + associated product names (if multiple products are chosen). +| It is designed to be responsive and mobile-friendly, making it easy to + showcase customer feedback on landing pages or promotional sections. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Business Need +------------- + +E-commerce websites often want to highlight customer feedback not only +on individual product pages but also in centralized locations such as +landing pages, category pages, or custom marketing sections. + +By default, Odoo reviews are tied to specific products and cannot be +easily aggregated or reused elsewhere on the website. + +This module addresses this limitation by introducing a snippet that +collects and displays multiple reviews in one place. + +For example: + +- A landing page showcasing testimonials from the most popular products. + +- A category introduction page summarizing customer experiences. + +- A homepage section displaying reviews from newly launched products. + +Approach +-------- + +The module extends the existing ``website_sale`` and ``website_rating`` +functionality to provide a configurable snippet. + +The snippet fetches published reviews across selected products and +displays them with a configurable limit (default 20, maximum 100). + +Useful Information +------------------ + +- **Dependencies:** Relies on ``website_sale`` and ``website_rating``. + +- **Compatible modules:** Works well with marketing and promotional + website features such as sliders, banners, or call-to-action snippets. + +- **Suggested setups:** Useful in multi-website or multi-company + scenarios where aggregated feedback should be highlighted across + different contexts. + +Usage +===== + +To use this module: + +1. Go to the Website application. + +2. Open the Website Builder (Edit → Add Blocks). + +3. In the "Products Snippets" section, drag and drop **Customer + Reviews** into your page. + + |Insert Snippet| + +4. Configure the snippet by adjusting the maximum number of reviews + (default: 20, max: 100) and set two columns option. + + |Configure Snippet| + +5. Save your changes and publish the page. + + The snippet will now display the aggregated reviews with rating + stars, reviewer details, and comments. + + |Rendered Snippet| + +.. |Insert Snippet| image:: https://raw.githubusercontent.com/OCA/e-commerce/18.0/website_sale_dynamic_review_snippet/static/description/snippet_insert.png +.. |Configure Snippet| image:: https://raw.githubusercontent.com/OCA/e-commerce/18.0/website_sale_dynamic_review_snippet/static/description/snippet_config.png +.. |Rendered Snippet| image:: https://raw.githubusercontent.com/OCA/e-commerce/18.0/website_sale_dynamic_review_snippet/static/description/snippet_render.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* XXP + +Contributors +------------ + +- `XXP `__: + + - Maksim Shurupov + - Mike Lapin + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/e-commerce `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_sale_dynamic_review_snippet/__init__.py b/website_sale_dynamic_review_snippet/__init__.py new file mode 100644 index 0000000000..e046e49fbe --- /dev/null +++ b/website_sale_dynamic_review_snippet/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/website_sale_dynamic_review_snippet/__manifest__.py b/website_sale_dynamic_review_snippet/__manifest__.py new file mode 100644 index 0000000000..8ef8a844e8 --- /dev/null +++ b/website_sale_dynamic_review_snippet/__manifest__.py @@ -0,0 +1,31 @@ +{ + "name": "Website Sale Dynamic Review Snippet", + "version": "18.0.1.0.0", + "category": "Website", + "depends": ["website_sale"], + "summary": ( + "Aggregate website users’ reviews from product templates into a single " + "website snippet (configurable, up to 100 records) " + "accessible in the Odoo Website Builder." + ), + "author": "XXP, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/e-commerce", + "data": [ + "views/snippets/options.xml", + "views/snippets/s_dynamic_review.xml", + ], + "assets": { + "portal.assets_chatter": [ + "website_sale_dynamic_review_snippet/static/src/core/*", + "website_sale_dynamic_review_snippet/static/src/components/*", + "website_sale_dynamic_review_snippet/static/src/services/*", + ], + "web.assets_frontend": [ + "website_sale_dynamic_review_snippet/static/src/boot/boot_service.esm.js", + ], + "web.assets_tests": [ + "website_sale_dynamic_review_snippet/static/src/tests/*.esm.js" + ], + }, +} diff --git a/website_sale_dynamic_review_snippet/controllers/__init__.py b/website_sale_dynamic_review_snippet/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/website_sale_dynamic_review_snippet/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/website_sale_dynamic_review_snippet/controllers/main.py b/website_sale_dynamic_review_snippet/controllers/main.py new file mode 100644 index 0000000000..533b464de6 --- /dev/null +++ b/website_sale_dynamic_review_snippet/controllers/main.py @@ -0,0 +1,65 @@ +from odoo.http import Controller, request, route + +from odoo.addons.mail.tools.discuss import Store + + +class CustomerReview(Controller): + @route( + ["/mail/review/messages"], + methods=["POST"], + type="json", + auth="public", + website=True, + ) + def mail_review_messages(self, before=None, after=None, limit=30): + domain = [ + ("model", "=", "product.template"), + ("message_type", "=", "comment"), + ("rating_value", ">=", 1), + ] + result = ( + request.env["mail.message"] + .sudo() + ._message_fetch(domain, None, before, after, None, limit) + ) + messages = result.pop("messages") + messages_vals_list = messages.portal_message_format( + options={"rating_include": True} + ) + for vals in messages_vals_list: + record = request.env[vals["model"]].sudo().browse(vals["res_id"]) + vals["thread"]["name"] = record.name + vals["website_url"] = record.website_url + return { + **result, + "data": { + "mail.message": messages_vals_list, + }, + "messages": Store.many_ids(messages), + } + + @route("/portal/review_init", type="json", auth="public", website=True) + def portal_review_init(self, **kwargs): + store = Store() + request.env["res.users"]._init_store_data(store) + if request.env.user.has_group("website.group_website_restricted_editor"): + store.add(request.env.user.partner_id, {"is_user_publisher": True}) + products = request.env["product.template"].search([("is_published", "=", True)]) + product_obj = request.env["product.template"] + for product in products: + thread = product_obj._get_thread_with_access(product.id, **kwargs) + if thread: + mode = product_obj._get_mail_message_access([product.id], "create") + has_react_access = product_obj._get_thread_with_access( + product.id, mode, **kwargs + ) + can_react = has_react_access + store.add( + thread, + { + "can_react": bool(can_react), + "hasReadAccess": thread.sudo(False).has_access("read"), + }, + as_thread=True, + ) + return store.get_result() diff --git a/website_sale_dynamic_review_snippet/i18n/ru.po b/website_sale_dynamic_review_snippet/i18n/ru.po new file mode 100644 index 0000000000..5839190c1e --- /dev/null +++ b/website_sale_dynamic_review_snippet/i18n/ru.po @@ -0,0 +1,79 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_dynamic_review_snippet +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-11 23:17+0000\n" +"PO-Revision-Date: 2025-09-11 23:17+0000\n" +"Last-Translator: AI Assistant\n" +"Language-Team: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "100" +msgstr "100" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "20" +msgstr "20" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "40" +msgstr "40" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "60" +msgstr "60" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "80" +msgstr "80" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_snippet +msgid "Customer Reviews" +msgstr "Отзывы клиентов" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml:0 +msgid "Load More" +msgstr "Загрузить еще" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "Max Number of Reviews" +msgstr "Максимальное количество отзывов" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.esm.js:0 +msgid "No messages found" +msgstr "Сообщения не найдены" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.snippets +msgid "Product Reviews Snippet" +msgstr "Сниппет отзывов о товарах" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml:0 +msgid "There are no reviews" +msgstr "Отзывов нет" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "Two Columns" +msgstr "Две колонки" diff --git a/website_sale_dynamic_review_snippet/i18n/website_sale_dynamic_review_snippet.pot b/website_sale_dynamic_review_snippet/i18n/website_sale_dynamic_review_snippet.pot new file mode 100644 index 0000000000..2eecb5cffa --- /dev/null +++ b/website_sale_dynamic_review_snippet/i18n/website_sale_dynamic_review_snippet.pot @@ -0,0 +1,79 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_dynamic_review_snippet +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-11 23:17+0000\n" +"PO-Revision-Date: 2025-09-11 23:17+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "100" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "20" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "40" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "60" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "80" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_snippet +msgid "Customer Reviews" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml:0 +msgid "Load More" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "Max Number of Reviews" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.esm.js:0 +msgid "No messages found" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.snippets +msgid "Product Reviews Snippet" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#. odoo-javascript +#: code:addons/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml:0 +msgid "There are no reviews" +msgstr "" + +#. module: website_sale_dynamic_review_snippet +#: model_terms:ir.ui.view,arch_db:website_sale_dynamic_review_snippet.s_dynamic_review_options +msgid "Two Columns" +msgstr "" diff --git a/website_sale_dynamic_review_snippet/pyproject.toml b/website_sale_dynamic_review_snippet/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_sale_dynamic_review_snippet/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_sale_dynamic_review_snippet/readme/CONTEXT.md b/website_sale_dynamic_review_snippet/readme/CONTEXT.md new file mode 100644 index 0000000000..250d6fb1ca --- /dev/null +++ b/website_sale_dynamic_review_snippet/readme/CONTEXT.md @@ -0,0 +1,33 @@ +## Business Need + +E-commerce websites often want to highlight customer feedback not only on individual product pages but also in centralized locations such as landing pages, category pages, or custom marketing sections. + +By default, Odoo reviews are tied to specific products and cannot be easily aggregated or reused elsewhere on the website. + + +This module addresses this limitation by introducing a snippet that collects and displays multiple reviews in one place. + +For example: + +- A landing page showcasing testimonials from the most popular products. + +- A category introduction page summarizing customer experiences. + +- A homepage section displaying reviews from newly launched products. + + +## Approach + +The module extends the existing `website_sale` and `website_rating` functionality to provide a configurable snippet. + +The snippet fetches published reviews across selected products and displays them with a configurable limit (default 20, maximum 100). + + +## Useful Information + +- **Dependencies:** Relies on `website_sale` and `website_rating`. + +- **Compatible modules:** Works well with marketing and promotional website features such as sliders, banners, or call-to-action snippets. + +- **Suggested setups:** Useful in multi-website or multi-company scenarios where aggregated feedback should be highlighted across different contexts. + diff --git a/website_sale_dynamic_review_snippet/readme/CONTRIBUTORS.md b/website_sale_dynamic_review_snippet/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..f27e3061f0 --- /dev/null +++ b/website_sale_dynamic_review_snippet/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [XXP](https://xxp-odoo.com): + - Maksim Shurupov + - Mike Lapin \ No newline at end of file diff --git a/website_sale_dynamic_review_snippet/readme/DESCRIPTION.md b/website_sale_dynamic_review_snippet/readme/DESCRIPTION.md new file mode 100644 index 0000000000..4d08951750 --- /dev/null +++ b/website_sale_dynamic_review_snippet/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module provides a new website snippet that aggregates customer reviews from product templates. +It allows website administrators to display up to 100 reviews from one or more products in a single dynamic block, directly within the Odoo Website Builder. + + +The snippet offers configuration options (maximum number of reviews) and renders reviews with rating stars, reviewer names, comments, and associated product names (if multiple products are chosen). +It is designed to be responsive and mobile-friendly, making it easy to showcase customer feedback on landing pages or promotional sections. \ No newline at end of file diff --git a/website_sale_dynamic_review_snippet/readme/USAGE.md b/website_sale_dynamic_review_snippet/readme/USAGE.md new file mode 100644 index 0000000000..ada0c85ef8 --- /dev/null +++ b/website_sale_dynamic_review_snippet/readme/USAGE.md @@ -0,0 +1,25 @@ +To use this module: + + +1. Go to the Website application. + +2. Open the Website Builder (Edit → Add Blocks). + +3. In the "Products Snippets" section, drag and drop **Customer Reviews** into your page. + + + ![Insert Snippet](../static/description/snippet_insert.png) + + +4. Configure the snippet by adjusting the maximum number of reviews (default: 20, max: 100) and set two columns option. + + + ![Configure Snippet](../static/description/snippet_config.png) + + +5. Save your changes and publish the page. + + The snippet will now display the aggregated reviews with rating stars, reviewer details, and comments. + + + ![Rendered Snippet](../static/description/snippet_render.png) diff --git a/website_sale_dynamic_review_snippet/static/description/icon.png b/website_sale_dynamic_review_snippet/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/website_sale_dynamic_review_snippet/static/description/icon.png differ diff --git a/website_sale_dynamic_review_snippet/static/description/index.html b/website_sale_dynamic_review_snippet/static/description/index.html new file mode 100644 index 0000000000..1b981f38f5 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/description/index.html @@ -0,0 +1,507 @@ + + + + + +Website Sale Dynamic Review Snippet + + + +
+

Website Sale Dynamic Review Snippet

+ + +

Beta License: AGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runboat

+
+
This module provides a new website snippet that aggregates customer +reviews from product templates.
+
It allows website administrators to display up to 100 reviews from one +or more products in a single dynamic block, directly within the Odoo +Website Builder.
+
+
+
The snippet offers configuration options (maximum number of reviews) +and renders reviews with rating stars, reviewer names, comments, and +associated product names (if multiple products are chosen).
+
It is designed to be responsive and mobile-friendly, making it easy to +showcase customer feedback on landing pages or promotional sections.
+
+

Table of contents

+ +
+

Use Cases / Context

+
+

Business Need

+

E-commerce websites often want to highlight customer feedback not only +on individual product pages but also in centralized locations such as +landing pages, category pages, or custom marketing sections.

+

By default, Odoo reviews are tied to specific products and cannot be +easily aggregated or reused elsewhere on the website.

+

This module addresses this limitation by introducing a snippet that +collects and displays multiple reviews in one place.

+

For example:

+
    +
  • A landing page showcasing testimonials from the most popular products.
  • +
  • A category introduction page summarizing customer experiences.
  • +
  • A homepage section displaying reviews from newly launched products.
  • +
+
+
+

Approach

+

The module extends the existing website_sale and website_rating +functionality to provide a configurable snippet.

+

The snippet fetches published reviews across selected products and +displays them with a configurable limit (default 20, maximum 100).

+
+
+

Useful Information

+
    +
  • Dependencies: Relies on website_sale and website_rating.
  • +
  • Compatible modules: Works well with marketing and promotional +website features such as sliders, banners, or call-to-action snippets.
  • +
  • Suggested setups: Useful in multi-website or multi-company +scenarios where aggregated feedback should be highlighted across +different contexts.
  • +
+
+
+
+

Usage

+

To use this module:

+
    +
  1. Go to the Website application.

    +
  2. +
  3. Open the Website Builder (Edit → Add Blocks).

    +
  4. +
  5. In the “Products Snippets” section, drag and drop Customer +Reviews into your page.

    +

    Insert Snippet

    +
  6. +
  7. Configure the snippet by adjusting the maximum number of reviews +(default: 20, max: 100) and set two columns option.

    +

    Configure Snippet

    +
  8. +
  9. Save your changes and publish the page.

    +

    The snippet will now display the aggregated reviews with rating +stars, reviewer details, and comments.

    +

    Rendered Snippet

    +
  10. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • XXP
  • +
+
+
+

Contributors

+
    +
  • XXP:
      +
    • Maksim Shurupov
    • +
    • Mike Lapin
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/e-commerce project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/website_sale_dynamic_review_snippet/static/description/snippet_config.png b/website_sale_dynamic_review_snippet/static/description/snippet_config.png new file mode 100644 index 0000000000..e29162d6c9 Binary files /dev/null and b/website_sale_dynamic_review_snippet/static/description/snippet_config.png differ diff --git a/website_sale_dynamic_review_snippet/static/description/snippet_insert.png b/website_sale_dynamic_review_snippet/static/description/snippet_insert.png new file mode 100644 index 0000000000..8c8cf6fede Binary files /dev/null and b/website_sale_dynamic_review_snippet/static/description/snippet_insert.png differ diff --git a/website_sale_dynamic_review_snippet/static/description/snippet_render.png b/website_sale_dynamic_review_snippet/static/description/snippet_render.png new file mode 100644 index 0000000000..81d8b65063 Binary files /dev/null and b/website_sale_dynamic_review_snippet/static/description/snippet_render.png differ diff --git a/website_sale_dynamic_review_snippet/static/src/boot/boot_service.esm.js b/website_sale_dynamic_review_snippet/static/src/boot/boot_service.esm.js new file mode 100644 index 0000000000..a6a157e835 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/boot/boot_service.esm.js @@ -0,0 +1,20 @@ +/* eslint-disable no-undef */ +import {Deferred} from "@web/core/utils/concurrency"; +import {loadBundle} from "@web/core/assets"; +import {memoize} from "@web/core/utils/functions"; +import {registry} from "@web/core/registry"; + +odoo.portalChatterReady = new Deferred(); + +const loader = { + loadChatter: memoize(() => loadBundle("portal.assets_chatter")), +}; +export const portalReviewBootService = { + start() { + const reviewEl = document.querySelector(".o_portal_reviews"); + if (reviewEl) { + loader.loadChatter(); + } + }, +}; +registry.category("services").add("portal.review.boot", portalReviewBootService); diff --git a/website_sale_dynamic_review_snippet/static/src/components/message_review.esm.js b/website_sale_dynamic_review_snippet/static/src/components/message_review.esm.js new file mode 100644 index 0000000000..82b2037030 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/message_review.esm.js @@ -0,0 +1,5 @@ +import {Message} from "@mail/core/common/message"; + +export class MessageReview extends Message {} + +MessageReview.template = "website_sale_dynamic_review_snippet.MessageReview"; diff --git a/website_sale_dynamic_review_snippet/static/src/components/message_review.xml b/website_sale_dynamic_review_snippet/static/src/components/message_review.xml new file mode 100644 index 0000000000..12e3290b14 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/message_review.xml @@ -0,0 +1,17 @@ + + + + + + + + + _blank + + + + diff --git a/website_sale_dynamic_review_snippet/static/src/components/message_review_list.esm.js b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.esm.js new file mode 100644 index 0000000000..5edf443add --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.esm.js @@ -0,0 +1,30 @@ +import {Component, useSubEnv} from "@odoo/owl"; +import {MessageReview} from "@website_sale_dynamic_review_snippet/components/message_review.esm"; +import {_t} from "@web/core/l10n/translation"; + +export class MessageReviewList extends Component { + static template = "website_sale_dynamic_review_snippet.MessageReviewList"; + static components = {MessageReview}; + + static props = ["messages", "loadMore?", "onLoadMoreVisible?"]; + + setup() { + super.setup(); + useSubEnv({ + displayRating: false, + inFrontendPortalChatter: true, + }); + } + + get messages() { + return this.props.messages; + } + + get emptyText() { + return _t("No messages found"); + } + + async loadMore() { + this.props.onLoadMoreVisible?.(); + } +} diff --git a/website_sale_dynamic_review_snippet/static/src/components/message_review_list.scss b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.scss new file mode 100644 index 0000000000..6d0a8cf952 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.scss @@ -0,0 +1,12 @@ +.o-mail-Message-sidebar { + padding-right: 10px; +} + +.o-mail-Message-body { + padding: 0px !important; + + p { + margin: 0px; + padding: 5px; + } +} diff --git a/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml new file mode 100644 index 0000000000..26d8fdc307 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/message_review_list.xml @@ -0,0 +1,47 @@ + + + +
+
+
+ +
+
+ + + +

+

+
+
+ 😶 + There are no reviews +
+
+
+
diff --git a/website_sale_dynamic_review_snippet/static/src/components/portal_review.esm.js b/website_sale_dynamic_review_snippet/static/src/components/portal_review.esm.js new file mode 100644 index 0000000000..b6854f0363 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/portal_review.esm.js @@ -0,0 +1,73 @@ +import {Component, onWillStart, useState} from "@odoo/owl"; +import {MessageReviewList} from "@website_sale_dynamic_review_snippet/components/message_review_list.esm"; +import {OverlayContainer} from "@web/core/overlay/overlay_container"; +import {RatingReview} from "@website_sale_dynamic_review_snippet/components/rating_review.esm"; +import {rpc} from "@web/core/network/rpc"; +import {useService} from "@web/core/utils/hooks"; + +export class PortalReview extends Component { + static template = "website_sale_dynamic_review_snippet.PortalReview"; + static components = {RatingReview, MessageReviewList, OverlayContainer}; + static props = ["limit", "twoColumns"]; + setup() { + this.store = useState(useService("mail.store")); + this.overlayService = useService("overlay"); + this.state = useState({ + selectedRating: false, + messages: [], + searchedMessage: [], + loadMore: false, + }); + onWillStart(async () => { + this.state.messages = await this.fetchMessages(); + await this.updateThread(); + this.state.searchedMessage = this.state.messages; + this.state.loadMore = true; + $(".o_portal_reviews_loader").addClass("d-none"); + }); + } + + async updateThread() { + for (const thread of Object.values(this.store.Thread.records)) { + await thread.fetchMessages(); + } + } + + async fetchMessages({after, around, before} = {}) { + const {data, messages} = await this.fetchMessagesData({after, around, before}); + this.store.insert(data, {html: true}); + odoo.portalChatterReady.resolve(true); + return this.store.Message.insert(messages); + } + + async fetchMessagesData({after, around, before} = {}) { + return await rpc("/mail/review/messages", { + limit: this.props.limit || 20, + after, + around, + before, + }); + } + + async setStar(star) { + this.state.selectedRating = star; + this.state.searchedMessage = this.state.messages.filter( + (item) => item.rating_value === parseInt(star, 10) + ); + } + + async resetStars() { + this.state.selectedRating = false; + this.state.searchedMessage = this.state.messages; + } + + async onLoadMoreVisible() { + const res = await this.fetchMessages({before: this.state.messages.at(-1).id}); + if (res.length === 0) { + this.state.loadMore = false; + } else { + this.state.messages = this.state.messages.concat(res); + this.state.searchedMessage = this.state.searchedMessage.concat(res); + } + } +} diff --git a/website_sale_dynamic_review_snippet/static/src/components/portal_review.xml b/website_sale_dynamic_review_snippet/static/src/components/portal_review.xml new file mode 100644 index 0000000000..fd9b7f1bed --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/portal_review.xml @@ -0,0 +1,22 @@ + + + +
+ + +
+ +
+
+
+
diff --git a/website_sale_dynamic_review_snippet/static/src/components/rating_review.esm.js b/website_sale_dynamic_review_snippet/static/src/components/rating_review.esm.js new file mode 100644 index 0000000000..dc7e6b5094 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/rating_review.esm.js @@ -0,0 +1,52 @@ +import {Component, onWillUpdateProps} from "@odoo/owl"; + +export class RatingReview extends Component { + static template = "website_sale_dynamic_review_snippet.RatingReview"; + static props = [ + "messages", + "selectedRating", + "setStar", + "resetStars", + "twoColumns", + ]; + + setup() { + this.updateRating(this.props.messages); + onWillUpdateProps((nextProps) => { + this.updateRating(nextProps.messages); + }); + } + + updateRating(messages) { + this.rating = messages + .map((rate) => rate.rating_value) + .filter((rateValue) => rateValue >= 1); + this.ratingTotal = this.rating.reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0 + ); + const ratingPercent = Object.groupBy(this.rating, (r) => r); + const ratings = [1, 2, 3, 4, 5]; + ratings.forEach((key) => { + const value = ratingPercent[key] || []; + ratingPercent[key] = Math.round((value.length / this.rating.length) * 100); + }); + this.ratingPercent = ratingPercent; + } + + get ratingStats() { + return { + total: this.rating.length, + avg: Math.round(this.ratingTotal / this.rating.length), + percent: this.ratingPercent, + }; + } + + async onClickStarDomain(star) { + this.props.setStar(star); + } + + async onClickStarDomainReset() { + this.props.resetStars(); + } +} diff --git a/website_sale_dynamic_review_snippet/static/src/components/rating_review.xml b/website_sale_dynamic_review_snippet/static/src/components/rating_review.xml new file mode 100644 index 0000000000..fc1ef544bf --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/components/rating_review.xml @@ -0,0 +1,12 @@ + + + +
+ + + + + +
+
+
diff --git a/website_sale_dynamic_review_snippet/static/src/core/message_model_patch.esm.js b/website_sale_dynamic_review_snippet/static/src/core/message_model_patch.esm.js new file mode 100644 index 0000000000..f4675568b5 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/core/message_model_patch.esm.js @@ -0,0 +1,16 @@ +import {Message} from "@mail/core/common/message_model"; + +import {patch} from "@web/core/utils/patch"; +import {url} from "@web/core/utils/urls"; + +patch(Message.prototype, { + setup() { + super.setup(...arguments); + this.website_url = ""; + }, + + get resUrl() { + if (this.website_url) return url(this.website_url); + return super.resUrl; + }, +}); diff --git a/website_sale_dynamic_review_snippet/static/src/services/portal_chatter_service_patch.esm.js b/website_sale_dynamic_review_snippet/static/src/services/portal_chatter_service_patch.esm.js new file mode 100644 index 0000000000..d0a5fe6045 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/services/portal_chatter_service_patch.esm.js @@ -0,0 +1,37 @@ +/* eslint-disable no-undef */ +import {App} from "@odoo/owl"; +import {PortalChatterService} from "@portal/chatter/frontend/portal_chatter_service"; +import {PortalReview} from "@website_sale_dynamic_review_snippet/components/portal_review.esm"; +import {_t} from "@web/core/l10n/translation"; +import {getTemplate} from "@web/core/templates"; +import {patch} from "@web/core/utils/patch"; +import {rpc} from "@web/core/network/rpc"; + +patch(PortalChatterService.prototype, { + async initialize(env) { + const reviewEl = document.querySelector(".o_portal_reviews"); + if (!reviewEl) { + return super.initialize(env); + } + const reviewSnippet = document.querySelector(".s_customer_review"); + const props = { + limit: + parseInt(reviewSnippet.getAttribute("data-max-number-reviews"), 10) || + 20, + twoColumns: reviewSnippet.getAttribute("data-two-columns") !== "true", + }; + this.createShadow(reviewEl).then((shadow) => { + new App(PortalReview, { + env, + getTemplate, + props, + translatableAttributes: ["data-tooltip"], + translateFn: _t, + dev: env.debug, + }).mount(shadow); + }); + const data = await rpc("/portal/review_init", {}, {silent: true}); + this.store.insert(data); + odoo.portalChatterReady.resolve(true); + }, +}); diff --git a/website_sale_dynamic_review_snippet/static/src/tests/website_snippet_dynamic_review.esm.js b/website_sale_dynamic_review_snippet/static/src/tests/website_snippet_dynamic_review.esm.js new file mode 100644 index 0000000000..33b9ea5354 --- /dev/null +++ b/website_sale_dynamic_review_snippet/static/src/tests/website_snippet_dynamic_review.esm.js @@ -0,0 +1,17 @@ +import { + clickOnSave, + insertSnippet, + registerWebsitePreviewTour, +} from "@website/js/tours/tour_utils"; + +registerWebsitePreviewTour( + "dynamic_review", + { + url: "/", + edition: true, + }, + () => [ + ...insertSnippet({id: "s_dynamic_review_snippet", groupName: "Products"}), + ...clickOnSave(), + ] +); diff --git a/website_sale_dynamic_review_snippet/tests/__init__.py b/website_sale_dynamic_review_snippet/tests/__init__.py new file mode 100644 index 0000000000..7fb00af307 --- /dev/null +++ b/website_sale_dynamic_review_snippet/tests/__init__.py @@ -0,0 +1 @@ +from . import test_website_sale_dynamic_review diff --git a/website_sale_dynamic_review_snippet/tests/test_website_sale_dynamic_review.py b/website_sale_dynamic_review_snippet/tests/test_website_sale_dynamic_review.py new file mode 100644 index 0000000000..8fab81604c --- /dev/null +++ b/website_sale_dynamic_review_snippet/tests/test_website_sale_dynamic_review.py @@ -0,0 +1,46 @@ +from odoo.tests import HttpCase, tagged + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.website_sale_dynamic_review_snippet.controllers.main import ( + CustomerReview, +) + + +@tagged("post_install", "-at_install") +class TestUi(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.controller = CustomerReview() + cls.user_admin = cls.env.ref("base.user_admin") + cls.company_admin = cls.user_admin.company_id + cls.user_portal = mail_new_test_user( + cls.env, + login="portal_test", + groups="base.group_portal", + company_id=cls.company_admin.id, + name="Chell Gladys", + notification_type="email", + ) + cls.partner_portal = cls.user_portal.partner_id + cls.product = cls.env["product.template"].create( + { + "name": "Test Product", + "is_published": True, + } + ) + cls.message = cls.product.with_user(cls.user_portal).message_post( + body="Not bad", + message_type="comment", + rating_value=3, + subtype_xmlid="mail.mt_comment", + ) + cls.website = cls.env["website"].browse(1) + + def test_admin_tour(self): + self.start_tour( + self.env["website"].get_client_action_url("/"), + "dynamic_review", + login="admin", + step_delay=2000, + ) diff --git a/website_sale_dynamic_review_snippet/views/snippets/options.xml b/website_sale_dynamic_review_snippet/views/snippets/options.xml new file mode 100644 index 0000000000..f3f3727a29 --- /dev/null +++ b/website_sale_dynamic_review_snippet/views/snippets/options.xml @@ -0,0 +1,44 @@ + + + + + + diff --git a/website_sale_dynamic_review_snippet/views/snippets/s_dynamic_review.xml b/website_sale_dynamic_review_snippet/views/snippets/s_dynamic_review.xml new file mode 100644 index 0000000000..2cee75975c --- /dev/null +++ b/website_sale_dynamic_review_snippet/views/snippets/s_dynamic_review.xml @@ -0,0 +1,19 @@ + + + +