diff --git a/README.md b/README.md index 6e9f5acc8b..bf21689f3b 100644 --- a/README.md +++ b/README.md @@ -22,18 +22,21 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[website_altcha](website_altcha/) | 18.0.1.0.1 | | Use self hosted privacy friendly captcha for verifying website users are not bots +[website_altcha](website_altcha/) | 18.0.1.1.0 | hbrunn | Use self hosted privacy friendly captcha for verifying website users are not bots [website_analytics_matomo](website_analytics_matomo/) | 18.0.1.0.0 | | Track website users using matomo -[website_blog_scheduled_publication](website_blog_scheduled_publication/) | 18.0.1.0.0 | marcelsavegnago CristianoMafraJunior | Schedule blog posts publication date +[website_blog_scheduled_publication](website_blog_scheduled_publication/) | 18.0.1.1.0 | marcelsavegnago CristianoMafraJunior | Schedule blog posts publication date [website_breadcrumb](website_breadcrumb/) | 18.0.1.0.0 | | Let you have breadcrumbs in website pages [website_cf_turnstile_login](website_cf_turnstile_login/) | 18.0.1.0.0 | adasatorres | Add Cloudflare Turnstile captcha to login form [website_cookiebot](website_cookiebot/) | 18.0.1.0.0 | | Ask for cookies consent connecting with Cookiebot [website_cookiefirst](website_cookiefirst/) | 18.0.2.0.0 | | Cookiefirst integration [website_crm_quick_answer](website_crm_quick_answer/) | 18.0.1.0.0 | | Add an automatic answer for contacts asking for info +[website_event_altcha](website_event_altcha/) | 18.0.1.1.0 | hbrunn | Support event registration with ALTCHA +[website_exclude_sitemap](website_exclude_sitemap/) | 18.0.1.0.0 | | Exclude addresses from the sitemap.xml file according to the values defined in the configuration [website_form_require_legal](website_form_require_legal/) | 18.0.1.0.2 | | Add possibility to require confirm legal terms. [website_forum_subscription](website_forum_subscription/) | 18.0.1.0.2 | | Adds a button to allow subscription from the website [website_google_tag_manager](website_google_tag_manager/) | 18.0.1.0.0 | | Add support for Google Tag Manager [website_legal_page](website_legal_page/) | 18.0.1.0.0 | | Website Legal Page +[website_llms](website_llms/) | 18.0.1.0.0 | marcelsavegnago | This module adds support for serving a /llms.txt file in the website root. The content can be configured per website in the website settings. [website_menu_by_user_status](website_menu_by_user_status/) | 18.0.1.0.0 | | Allow to manage the display of website.menus [website_odoo_debranding](website_odoo_debranding/) | 18.0.1.0.0 | | Remove Odoo Branding from Website [website_partner_form](website_partner_form/) | 18.0.1.0.0 | legalsylvain | Allow to edit partner website fields on partner form view, in the back office @@ -42,6 +45,7 @@ addon | version | maintainers | summary [website_search_header](website_search_header/) | 18.0.1.0.0 | | Website Search in Header [website_select2](website_select2/) | 18.0.1.0.0 | hbrunn | Integrate select2 in Odoo websites [website_snippet_country_dropdown](website_snippet_country_dropdown/) | 18.0.1.0.1 | | Allow to select country in a dropdown +[website_social_media_link](website_social_media_link/) | 18.0.1.0.0 | legalsylvain | Display Social Medial Links on Website back-end. [website_user_login_redirect_custom](website_user_login_redirect_custom/) | 18.0.1.0.0 | | Redirect website/portal user to custom URL after login or signup [website_whatsapp](website_whatsapp/) | 18.0.1.0.1 | ioans73 | Whatsapp integration diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 257b45616f..8b0e583850 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-website" -version = "18.0.20260727.0" +version = "18.0.20260917.0" dependencies = [ "odoo-addon-website_altcha==18.0.*", "odoo-addon-website_analytics_matomo==18.0.*", @@ -10,10 +10,13 @@ dependencies = [ "odoo-addon-website_cookiebot==18.0.*", "odoo-addon-website_cookiefirst==18.0.*", "odoo-addon-website_crm_quick_answer==18.0.*", + "odoo-addon-website_event_altcha==18.0.*", + "odoo-addon-website_exclude_sitemap==18.0.*", "odoo-addon-website_form_require_legal==18.0.*", "odoo-addon-website_forum_subscription==18.0.*", "odoo-addon-website_google_tag_manager==18.0.*", "odoo-addon-website_legal_page==18.0.*", + "odoo-addon-website_llms==18.0.*", "odoo-addon-website_menu_by_user_status==18.0.*", "odoo-addon-website_odoo_debranding==18.0.*", "odoo-addon-website_partner_form==18.0.*", @@ -22,6 +25,7 @@ dependencies = [ "odoo-addon-website_search_header==18.0.*", "odoo-addon-website_select2==18.0.*", "odoo-addon-website_snippet_country_dropdown==18.0.*", + "odoo-addon-website_social_media_link==18.0.*", "odoo-addon-website_user_login_redirect_custom==18.0.*", "odoo-addon-website_whatsapp==18.0.*", ] diff --git a/website_altcha/README.rst b/website_altcha/README.rst index 327a2d7d84..8f4f7a2765 100644 --- a/website_altcha/README.rst +++ b/website_altcha/README.rst @@ -11,7 +11,7 @@ Privacy Friendly Captcha !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:47240fe67bd99327829258a0f654f2a147a5332dca76e6e66d67df44800779c8 + !! source digest: sha256:83e03d4eacacddd6d9b4c9b29eed8eee8f9d1bde28564186bfb553e8f5a29b6f !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -126,6 +126,14 @@ 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. +.. |maintainer-hbrunn| image:: https://github.com/hbrunn.png?size=40px + :target: https://github.com/hbrunn + :alt: hbrunn + +Current `maintainer `__: + +|maintainer-hbrunn| + This module is part of the `OCA/website `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_altcha/__manifest__.py b/website_altcha/__manifest__.py index a1a71c6110..73817e4d8b 100644 --- a/website_altcha/__manifest__.py +++ b/website_altcha/__manifest__.py @@ -5,24 +5,28 @@ "name": "Privacy Friendly Captcha", "summary": """Use self hosted privacy friendly captcha for verifying website users are not bots""", - "version": "18.0.1.0.1", + "version": "18.0.1.1.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", + "maintainers": ["hbrunn"], "website": "https://github.com/OCA/website", "depends": [ "website", ], "assets": { "web.assets_frontend": [ - "website_altcha/static/src/**/*.esm.js", - "website_altcha/static/src/**/*.xml", - "website_altcha/static/src/**/*.scss", + "website_altcha/static/src/*.esm.js", + "website_altcha/static/src/*.xml", + "website_altcha/static/src/*.scss", ], "web.altcha_libs": [ "website_altcha/static/lib/altcha.js", "website_altcha/static/lib/altcha-l10n.js", "website_altcha/static/lib/altcha.css", ], + "web.assets_tests": [ + "website_altcha/static/tests/tours/*.esm.js", + ], }, "external_dependencies": { "python": ["altcha"], @@ -31,4 +35,7 @@ "security/ir.model.access.csv", "views/res_config_settings_views.xml", ], + "demo": [ + "demo/website_page.xml", + ], } diff --git a/website_altcha/demo/website_page.xml b/website_altcha/demo/website_page.xml new file mode 100644 index 0000000000..ad2f5d5e13 --- /dev/null +++ b/website_altcha/demo/website_page.xml @@ -0,0 +1,36 @@ + + + + /website_altcha_demo + + + + diff --git a/website_altcha/i18n/fr.po b/website_altcha/i18n/fr.po new file mode 100644 index 0000000000..f025ce9373 --- /dev/null +++ b/website_altcha/i18n/fr.po @@ -0,0 +1,217 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_altcha +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-08-11 18:46+0000\n" +"Last-Translator: Thibault REY \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +". In that case the website_altcha_form class only takes care that " +"the javascript code is loaded and activated." +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "Add Altcha functionality" +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "Algorithm" +msgstr "Algorithme" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_algorithm +#: model:ir.model.fields,field_description:website_altcha.field_website__altcha_algorithm +msgid "Altcha Algorithm" +msgstr "Algorithme Altcha" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_cost +#: model:ir.model.fields,field_description:website_altcha.field_website__altcha_cost +msgid "Altcha Cost" +msgstr "Coût du challenge Altcha" + +#. module: website_altcha +#: model:ir.model,name:website_altcha.model_altcha_key +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_key +#: model:ir.model.fields,field_description:website_altcha.field_website__altcha_key +msgid "Altcha Key" +msgstr "Clé Altcha" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_private_key +#: model:ir.model.fields,field_description:website_altcha.field_website__altcha_private_key +msgid "Altcha Private Key" +msgstr "Clé privée Altcha" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_timeout +#: model:ir.model.fields,field_description:website_altcha.field_website__altcha_timeout +msgid "Altcha Timeout" +msgstr "Délai d'attente Altcha" + +#. module: website_altcha +#: model:ir.model,name:website_altcha.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de configuration" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "Cost" +msgstr "Coût" + +#. module: website_altcha +#: model:ir.model.fields,help:website_altcha.field_res_config_settings__altcha_cost +#: model:ir.model.fields,help:website_altcha.field_website__altcha_cost +msgid "" +"Cost factor for the hashing algorithm.\n" +" For PBKDF2, this is the number of iterations.\n" +" Default is 5000 for PBKDF2/SHA-512." +msgstr "" +"Facteur de coût pour l'algorithme de hachage.\n" +" Pour PBKDF2, il s'agit du nombre d'itérations.\n" +" La valeur par défaut est 5000 pour PBKDF2/SHA-512." + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__display_name +msgid "Display Name" +msgstr "Nom" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_res_config_settings__altcha_enabled +msgid "Enable Altcha" +msgstr "Activer Altcha" + +#. module: website_altcha +#: model:ir.model.fields,help:website_altcha.field_res_config_settings__altcha_enabled +msgid "Enable Altcha functionality on the website." +msgstr "Activer Altcha sur le site web." + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__expires_at +msgid "Expires At" +msgstr "Expire le" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "HMAC key secret" +msgstr "Clé secrète HMAC" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "HMAC secret" +msgstr "Secret HMAC" + +#. module: website_altcha +#: model:ir.model,name:website_altcha.model_ir_http +msgid "HTTP Routing" +msgstr "Routage HTTP" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__id +msgid "ID" +msgstr "ID" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__key +msgid "Key" +msgstr "Clé" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"Note in both cases, you'll have to verify the token you received in your " +"custom controller." +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "Submit" +msgstr "" + +#. module: website_altcha +#. odoo-python +#: code:addons/website_altcha/models/ir_http.py:0 +msgid "Suspicious activity detected by Altcha" +msgstr "Activité suspecte détectée par Altcha" + +#. module: website_altcha +#: model:ir.model.constraint,message:website_altcha.constraint_altcha_key_key_uniq +msgid "The Altcha key must be unique." +msgstr "La clé Altcha doit être unique." + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"This is a demo form for website_altcha. Note how it automatically inserts " +"the widget in the container containing the submit button" +msgstr "" + +#. module: website_altcha +#: model:ir.model.fields,help:website_altcha.field_res_config_settings__altcha_timeout +#: model:ir.model.fields,help:website_altcha.field_website__altcha_timeout +msgid "Time in minutes before a captcha expires. Default is 5 minutes." +msgstr "" +"Durée en minutes avant l'expiration d'un captcha. La valeur par défaut est " +"de 5 minutes." + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form +msgid "Timeout" +msgstr "Délais d'attente" + +#. module: website_altcha +#: model:ir.model.fields,field_description:website_altcha.field_altcha_key__used +msgid "Used" +msgstr "Utilisé" + +#. module: website_altcha +#: model:ir.model,name:website_altcha.model_website +msgid "Website" +msgstr "Site web" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"You can however also place the widget yourself and configure it according to " +"the" +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "documentation" +msgstr "" diff --git a/website_altcha/i18n/it.po b/website_altcha/i18n/it.po index 4f917f8381..01b17686cf 100644 --- a/website_altcha/i18n/it.po +++ b/website_altcha/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-06-03 09:46+0000\n" +"PO-Revision-Date: 2026-09-21 20:37+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -16,6 +16,15 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.15.2\n" +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +". In that case the website_altcha_form class only takes care that " +"the javascript code is loaded and activated." +msgstr "" +". In tal caso, la classe website_altcha_form si occupa " +"esclusivamente di far sì che il codice JavaScript venga caricato e attivato." + #. module: website_altcha #: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form msgid "Add Altcha functionality" @@ -144,6 +153,20 @@ msgstr "Ultimo aggiornamento di" msgid "Last Updated on" msgstr "Ultimo aggiornamento il" +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"Note in both cases, you'll have to verify the token you received in your " +"custom controller." +msgstr "" +"Nota: in entrambi i casi, dovrai verificare il token ricevuto nel tuo " +"controller personalizzato." + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "Submit" +msgstr "Invia" + #. module: website_altcha #. odoo-python #: code:addons/website_altcha/models/ir_http.py:0 @@ -155,6 +178,15 @@ msgstr "Attività sospetta rilevata da Altcha" msgid "The Altcha key must be unique." msgstr "La chiave Altcha deve essere univoca." +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"This is a demo form for website_altcha. Note how it automatically inserts " +"the widget in the container containing the submit button" +msgstr "" +"Questo è un modulo dimostrativo per website_altcha. Notare come inserisca " +"automaticamente il widget nel contenitore che ospita il pulsante di invio" + #. module: website_altcha #: model:ir.model.fields,help:website_altcha.field_res_config_settings__altcha_timeout #: model:ir.model.fields,help:website_altcha.field_website__altcha_timeout @@ -176,3 +208,17 @@ msgstr "Usata" #: model:ir.model,name:website_altcha.model_website msgid "Website" msgstr "Sito web" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"You can however also place the widget yourself and configure it according to " +"the" +msgstr "" +"Tuttavia, si può anche posizionare il widget autonomamente e configurarlo " +"secondo le proprie esigenze" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "documentation" +msgstr "documentazione" diff --git a/website_altcha/i18n/website_altcha.pot b/website_altcha/i18n/website_altcha.pot index 5ffdfad4f3..5ec58bd77a 100644 --- a/website_altcha/i18n/website_altcha.pot +++ b/website_altcha/i18n/website_altcha.pot @@ -13,6 +13,13 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +". In that case the website_altcha_form class only takes care that " +"the javascript code is loaded and activated." +msgstr "" + #. module: website_altcha #: model_terms:ir.ui.view,arch_db:website_altcha.res_config_settings_view_form msgid "Add Altcha functionality" @@ -138,6 +145,18 @@ msgstr "" msgid "Last Updated on" msgstr "" +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"Note in both cases, you'll have to verify the token you received in your " +"custom controller." +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "Submit" +msgstr "" + #. module: website_altcha #. odoo-python #: code:addons/website_altcha/models/ir_http.py:0 @@ -149,6 +168,13 @@ msgstr "" msgid "The Altcha key must be unique." msgstr "" +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"This is a demo form for website_altcha. Note how it automatically inserts " +"the widget in the container containing the submit button" +msgstr "" + #. module: website_altcha #: model:ir.model.fields,help:website_altcha.field_res_config_settings__altcha_timeout #: model:ir.model.fields,help:website_altcha.field_website__altcha_timeout @@ -169,3 +195,15 @@ msgstr "" #: model:ir.model,name:website_altcha.model_website msgid "Website" msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "" +"You can however also place the widget yourself and configure it according to" +" the" +msgstr "" + +#. module: website_altcha +#: model_terms:ir.ui.view,arch_db:website_altcha.demo_page_view +msgid "documentation" +msgstr "" diff --git a/website_altcha/static/description/index.html b/website_altcha/static/description/index.html index cc3b4dafcc..9ec2c0824a 100644 --- a/website_altcha/static/description/index.html +++ b/website_altcha/static/description/index.html @@ -372,7 +372,7 @@

Privacy Friendly Captcha

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:47240fe67bd99327829258a0f654f2a147a5332dca76e6e66d67df44800779c8 +!! source digest: sha256:83e03d4eacacddd6d9b4c9b29eed8eee8f9d1bde28564186bfb553e8f5a29b6f !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module allows to use a Captcha System completly handled by Odoo.

@@ -470,6 +470,8 @@

Maintainers

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.

+

Current maintainer:

+

hbrunn

This module is part of the OCA/website project on GitHub.

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

diff --git a/website_altcha/static/lib/altcha.js b/website_altcha/static/lib/altcha.js index 1b70b870b7..7dab37aa64 100644 --- a/website_altcha/static/lib/altcha.js +++ b/website_altcha/static/lib/altcha.js @@ -1,4 +1,3 @@ -/* @odoo-module */ const DEV = false; var is_array = Array.isArray; var index_of = Array.prototype.indexOf; diff --git a/website_altcha/static/src/altcha.esm.js b/website_altcha/static/src/altcha.esm.js new file mode 100644 index 0000000000..996bfbb97a --- /dev/null +++ b/website_altcha/static/src/altcha.esm.js @@ -0,0 +1,49 @@ +import {loadBundle} from "@web/core/assets"; +import {renderToString} from "@web/core/utils/render"; +import {session} from "@web/session"; + +export const AltchaBaseFunctionality = { + altcha_prepend_to: "div.oe_login_buttons", + altcha_enabled: false, + + altcha_init() { + this.altcha_enabled = Boolean(session.altcha_public_key); + }, + + /** + * Loads the altcha libraries. + * + * @returns {Promise|Boolean} promise if libs are loading else false if the altcha key is empty. + */ + async altcha_load_libs() { + if (this.altcha_enabled) { + this._altchaReady = loadBundle(`web.altcha_libs`); + return this._altchaReady; + } + return false; + }, + altcha_insert_widget() { + if (this.altcha_enabled && !this.$el.find("altcha-widget").length) { + this.$el + .find(this.altcha_prepend_to) + .prepend(renderToString("website_altcha.AltchaWidget", {})); + } + }, +}; + +export const AltchaLegacyClassFunctionality = { + ...AltchaBaseFunctionality, + init() { + this._super(...arguments); + this.altcha_init(); + }, + + async willStart() { + this.altcha_load_libs(); + return this._super(...arguments); + }, + start() { + this.altcha_insert_widget(); + return this._super(...arguments); + }, +}; diff --git a/website_altcha/static/src/signup/altcha.scss b/website_altcha/static/src/altcha.scss similarity index 100% rename from website_altcha/static/src/signup/altcha.scss rename to website_altcha/static/src/altcha.scss diff --git a/website_altcha/static/src/signup/altcha.xml b/website_altcha/static/src/altcha.xml similarity index 100% rename from website_altcha/static/src/signup/altcha.xml rename to website_altcha/static/src/altcha.xml diff --git a/website_altcha/static/src/altcha/altcha.esm.js b/website_altcha/static/src/altcha/altcha.esm.js deleted file mode 100644 index 4f18e82e33..0000000000 --- a/website_altcha/static/src/altcha/altcha.esm.js +++ /dev/null @@ -1,27 +0,0 @@ -/* global document */ - -import {loadBundle} from "@web/core/assets"; -import {session} from "@web/session"; - -export class Altcha { - /** - * @override - */ - constructor() { - this._publicKey = session.altcha_public_key; - } - /** - * Loads the altcha libraries. - * - * @returns {Promise|Boolean} promise if libs are loading else false if the altcha key is empty. - */ - loadLibs() { - if (this._publicKey) { - this._altchaReady = loadBundle(`web.altcha_libs`); - return this._altchaReady.then(() => - Boolean(document.querySelector(".o_altcha_widget")) - ); - } - return false; - } -} diff --git a/website_altcha/static/src/generic.esm.js b/website_altcha/static/src/generic.esm.js new file mode 100644 index 0000000000..c1130b6845 --- /dev/null +++ b/website_altcha/static/src/generic.esm.js @@ -0,0 +1,9 @@ +import {AltchaLegacyClassFunctionality} from "@website_altcha/altcha.esm"; +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.GenericAltcha = publicWidget.Widget.extend({ + ...AltchaLegacyClassFunctionality, + selector: "form.website_altcha_form, form.js_website_submit_form", + altcha_prepend_to: + "*:has( > input[type='submit']), *:has( > button[type='submit'])", +}); diff --git a/website_altcha/static/src/signup.esm.js b/website_altcha/static/src/signup.esm.js new file mode 100644 index 0000000000..49954f71b2 --- /dev/null +++ b/website_altcha/static/src/signup.esm.js @@ -0,0 +1,12 @@ +import {AltchaLegacyClassFunctionality} from "./altcha.esm"; +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.SignupAltcha = publicWidget.Widget.extend({ + ...AltchaLegacyClassFunctionality, + selector: ".oe_signup_form", +}); + +publicWidget.registry.ResetPasswordAltcha = publicWidget.Widget.extend({ + ...AltchaLegacyClassFunctionality, + selector: ".oe_reset_password_form", +}); diff --git a/website_altcha/static/src/signup/signup.esm.js b/website_altcha/static/src/signup/signup.esm.js deleted file mode 100644 index fb5de1032d..0000000000 --- a/website_altcha/static/src/signup/signup.esm.js +++ /dev/null @@ -1,73 +0,0 @@ -import "@website/snippets/s_website_form/000"; -import {Altcha} from "@website_altcha/altcha/altcha.esm"; -import publicWidget from "@web/legacy/js/public/public_widget"; -import {renderToString} from "@web/core/utils/render"; - -export const AltchaFunctionality = { - init() { - this._super(...arguments); - this._altcha = new Altcha(); - }, - - async willStart() { - this._altcha.loadLibs(); - return this._super(...arguments); - }, - start: function () { - if (this._altcha._publicKey && !this.$el.find(".o_altcha_widget").length) { - this.$el - .find("div.oe_login_buttons") - .prepend(renderToString("website_altcha.AltchaWidget", {})); - } - return this._super(...arguments); - }, -}; - -publicWidget.registry.SignupAltcha = publicWidget.Widget.extend({ - ...AltchaFunctionality, - selector: ".oe_signup_form", -}); - -publicWidget.registry.ResetPasswordAltcha = publicWidget.Widget.extend({ - ...AltchaFunctionality, - selector: ".oe_reset_password_form", -}); - -publicWidget.registry.s_website_form.include({ - init() { - this._super(...arguments); - this._altcha = new Altcha(); - }, - - async willStart() { - this._altcha.loadLibs(); - return this._super(...arguments); - }, - - /** - * @override - */ - start: function () { - const res = this._super(...arguments); - if (this.$target[0].classList.contains("s_website_form_no_recaptcha")) { - return res; - } - if (this._altcha._publicKey) { - this.$el - .find(".s_website_form_submit") - .before(renderToString("website_altcha.AltchaWidget", {})); - } - return res; - }, -}); -publicWidget.registry.EditModeWebsiteForm.include({ - start: function () { - const res = this._super(...arguments); - if (this.editableMode) { - // We should delete the altcha widget in edit mode - this.$el.find(".o_altcha_widget").remove(); - this.$el.find(".o_altcha_widget_container").remove(); - } - return res; - }, -}); diff --git a/website_altcha/static/src/website_form.esm.js b/website_altcha/static/src/website_form.esm.js new file mode 100644 index 0000000000..f97201195d --- /dev/null +++ b/website_altcha/static/src/website_form.esm.js @@ -0,0 +1,36 @@ +import "@website/snippets/s_website_form/000"; +import {AltchaLegacyClassFunctionality} from "./altcha.esm"; +import publicWidget from "@web/legacy/js/public/public_widget"; +import {renderToString} from "@web/core/utils/render"; + +publicWidget.registry.s_website_form.include({ + ...AltchaLegacyClassFunctionality, +}); +publicWidget.registry.s_website_form.include({ + altcha_insert_widget() { + // Intentional no-op + }, + start: function () { + const res = this._super(...arguments); + if (this.$target[0].classList.contains("s_website_form_no_recaptcha")) { + return res; + } + if (this.altcha_enabled) { + this.$el + .find(".s_website_form_submit") + .before(renderToString("website_altcha.AltchaWidget", {})); + } + return res; + }, +}); +publicWidget.registry.EditModeWebsiteForm.include({ + start: function () { + const res = this._super(...arguments); + if (this.editableMode) { + // We should delete the altcha widget in edit mode + this.$el.find(".o_altcha_widget").remove(); + this.$el.find(".o_altcha_widget_container").remove(); + } + return res; + }, +}); diff --git a/website_altcha/static/tests/tours/website_altcha_contact.esm.js b/website_altcha/static/tests/tours/website_altcha_contact.esm.js new file mode 100644 index 0000000000..68321c3d08 --- /dev/null +++ b/website_altcha/static/tests/tours/website_altcha_contact.esm.js @@ -0,0 +1,41 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("website_altcha_contact", { + steps: () => [ + { + content: "Fill in a name", + trigger: "input[name='name']", + run: "fill Testname", + }, + { + content: "Fill in an email", + trigger: "input[name='email_from']", + run: "fill test_website_altcha@test.com", + }, + { + content: "Fill in a subject", + trigger: "input[name='subject']", + run: "fill the subject", + }, + { + content: "Fill in a question", + trigger: "textarea[name='description']", + run: "fill the question", + }, + { + content: "Verify altcha", + trigger: "altcha-widget input[type='checkbox']", + run: "click", + }, + { + content: "Verify altcha value exists", + trigger: "input[name='altcha']:not(:visible):not(:empty)", + }, + { + content: "Click Submit", + trigger: "div.s_website_form_submit a", + run: "click", + }, + ], +}); diff --git a/website_altcha/static/tests/tours/website_altcha_generic.esm.js b/website_altcha/static/tests/tours/website_altcha_generic.esm.js new file mode 100644 index 0000000000..3d4ad89cf0 --- /dev/null +++ b/website_altcha/static/tests/tours/website_altcha_generic.esm.js @@ -0,0 +1,21 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("website_altcha_generic", { + steps: () => [ + { + content: "Verify altcha", + trigger: "altcha-widget input[type='checkbox']", + run: "click", + }, + { + content: "Verify altcha value exists", + trigger: "input[name='altcha']:not(:visible):not(:empty)", + }, + { + content: "Click Submit", + trigger: "button[type='submit']", + run: "click", + }, + ], +}); diff --git a/website_altcha/static/tests/tours/website_altcha_signup.esm.js b/website_altcha/static/tests/tours/website_altcha_signup.esm.js new file mode 100644 index 0000000000..824087f07d --- /dev/null +++ b/website_altcha/static/tests/tours/website_altcha_signup.esm.js @@ -0,0 +1,26 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("website_altcha_signup", { + steps: () => [ + { + content: "Fill in a login", + trigger: "input[name='login']", + run: "fill admin", + }, + { + content: "Verify altcha", + trigger: "altcha-widget input[type='checkbox']", + run: "click", + }, + { + content: "Verify altcha value exists", + trigger: "input[name='altcha']:not(:visible):not(:empty)", + }, + { + content: "Click Submit", + trigger: "button[type='submit']", + run: "click", + }, + ], +}); diff --git a/website_altcha/tests/__init__.py b/website_altcha/tests/__init__.py index 65d22d8cf1..2fb64ca391 100644 --- a/website_altcha/tests/__init__.py +++ b/website_altcha/tests/__init__.py @@ -1 +1,2 @@ from . import test_altcha +from . import test_altcha_tours diff --git a/website_altcha/tests/common.py b/website_altcha/tests/common.py new file mode 100644 index 0000000000..dadd8112b2 --- /dev/null +++ b/website_altcha/tests/common.py @@ -0,0 +1,18 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("-at_install", "post_install") +class Common(HttpCase): + @classmethod + def setUpClass(cls): + result = super().setUpClass() + cls.website = cls.env["website"].get_current_website() + cls.website.altcha_key = "test_key" + cls.website.altcha_private_key = "test_secret_key" + cls.website.altcha_cost = 2 + # Forcing a small power of 2 cost for allowing scrypt to work + # and make everything faster + return result diff --git a/website_altcha/tests/test_altcha.py b/website_altcha/tests/test_altcha.py index 462f16638b..0fea6cb962 100644 --- a/website_altcha/tests/test_altcha.py +++ b/website_altcha/tests/test_altcha.py @@ -8,13 +8,15 @@ from requests.exceptions import HTTPError from odoo.exceptions import UserError -from odoo.tests import Form, HttpCase +from odoo.tests import Form from odoo.tools import mute_logger from odoo.addons.website.tools import MockRequest +from .common import Common -class TestAltchaCreation(HttpCase): + +class TestAltchaCreation(Common): @classmethod def setUpClass(cls): result = super().setUpClass() diff --git a/website_altcha/tests/test_altcha_tours.py b/website_altcha/tests/test_altcha_tours.py new file mode 100644 index 0000000000..4cfeef523e --- /dev/null +++ b/website_altcha/tests/test_altcha_tours.py @@ -0,0 +1,24 @@ +# Copyright 2026 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.website_altcha.tests.common import Common + + +class TestWebsiteAltchaTours(Common): + def test_contact_tour(self): + """ + Run the contact test tour + """ + self.start_tour("/contactus", "website_altcha_contact") + + def test_signup_tour(self): + """ + Run the signup test tour + """ + self.start_tour("/web/reset_password", "website_altcha_signup") + + def test_generic_tour(self): + """ + Run the generic test tour + """ + self.start_tour("/website_altcha_demo", "website_altcha_generic") diff --git a/website_analytics_matomo/i18n/sl.po b/website_analytics_matomo/i18n/sl.po index a09b524f50..150e30c190 100644 --- a/website_analytics_matomo/i18n/sl.po +++ b/website_analytics_matomo/i18n/sl.po @@ -9,70 +9,72 @@ msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-01-16 03:45+0000\n" -"PO-Revision-Date: 2018-01-16 03:45+0000\n" -"Last-Translator: OCA Transbot , 2018\n" +"PO-Revision-Date: 2026-08-11 04:59+0000\n" +"Last-Translator: Matjaz Mozetic \n" "Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" "Language: sl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " -"n%100==4 ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " +"n%100==4 ? 2 : 3;\n" +"X-Generator: Weblate 5.15.2\n" #. module: website_analytics_matomo #: model_terms:ir.ui.view,arch_db:website_analytics_matomo.res_config_settings_view_form msgid "Active Time (seconds)" -msgstr "" +msgstr "Aktivni čas (v sekundah)" #. module: website_analytics_matomo #: model:ir.model,name:website_analytics_matomo.model_res_config_settings msgid "Config Settings" -msgstr "" +msgstr "Nastavitve" #. module: website_analytics_matomo #: model_terms:ir.ui.view,arch_db:website_analytics_matomo.res_config_settings_view_form msgid "Enable User ID" -msgstr "" +msgstr "Omogoči ID uporabnika" #. module: website_analytics_matomo #: model_terms:ir.ui.view,arch_db:website_analytics_matomo.res_config_settings_view_form msgid "Enable heartbeat" -msgstr "" +msgstr "Omogoči srčni utrip" #. module: website_analytics_matomo #: model:ir.model.fields,help:website_analytics_matomo.field_res_config_settings__matomo_heartbeat_timer #: model:ir.model.fields,help:website_analytics_matomo.field_website__matomo_heartbeat_timer msgid "How many seconds a tab needs to be active to be counted as viewed." msgstr "" +"Koliko sekund mora biti zavihek aktiven, da se lahko obravnava kot ogledan." #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__has_matomo_analytics #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__has_matomo_analytics msgid "Matomo Analytics" -msgstr "" +msgstr "Matomo analitika" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__matomo_enable_heartbeat #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__matomo_enable_heartbeat msgid "Matomo Enable Heartbeat" -msgstr "" +msgstr "Matomo omogoči srčni utrip" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__matomo_enable_userid #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__matomo_enable_userid msgid "Matomo Enable Userid" -msgstr "" +msgstr "Matomo omogoči ID uporabnika" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__matomo_get_userid msgid "Matomo Get Userid" -msgstr "" +msgstr "Matomo pridobi ID uporabnika" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__matomo_heartbeat_timer #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__matomo_heartbeat_timer msgid "Matomo Heartbeat Timer" -msgstr "" +msgstr "Matomo – časomer srčnega utripa" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__matomo_analytics_host @@ -83,7 +85,7 @@ msgstr "Matomo gostitelj" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_website__matomo_analytics_host_url msgid "Matomo host URL" -msgstr "" +msgstr "Matomo URL gostitelja" #. module: website_analytics_matomo #: model:ir.model.fields,field_description:website_analytics_matomo.field_res_config_settings__matomo_analytics_id @@ -100,6 +102,11 @@ msgid "" "page or focuses away from the tab)\n" " then a ping request will be sent." msgstr "" +"Nastavite čas aktivnosti v sekundah. Šele če je bila stran ogledana vsaj " +"toliko\n" +" časa (in če uporabnik zapusti stran ali " +"preusmeri pozornost stran od zavihka),\n" +" bo ping-poizvedba poslana." #. module: website_analytics_matomo #: model:ir.model.fields,help:website_analytics_matomo.field_res_config_settings__matomo_analytics_id @@ -113,11 +120,12 @@ msgstr "Matomo ID za identifikacijo spletne strani" msgid "" "The host/path your Matomo installation is accessible by on the internet." msgstr "" +"Gostitelj/pot, prek katere je vaša namestitev Matomo dostopna na internetu." #. module: website_analytics_matomo #: model_terms:ir.ui.view,arch_db:website_analytics_matomo.res_config_settings_view_form msgid "Track visits in Matomo Analytics" -msgstr "" +msgstr "Sledenje obiskom v Matomo Analytics" #. module: website_analytics_matomo #: model:ir.model,name:website_analytics_matomo.model_website @@ -127,7 +135,7 @@ msgstr "Spletna stran" #. module: website_analytics_matomo #: model_terms:ir.ui.view,arch_db:website_analytics_matomo.res_config_settings_view_form msgid "https://matomo.host.name/" -msgstr "" +msgstr "https://matomo.host.name/" #~ msgid "" #~ "The host/path your Matomo installation is accessible by on the internet. " diff --git a/website_blog_scheduled_publication/README.rst b/website_blog_scheduled_publication/README.rst index 05c2e8efe4..6377a80a27 100644 --- a/website_blog_scheduled_publication/README.rst +++ b/website_blog_scheduled_publication/README.rst @@ -11,7 +11,7 @@ Blog Scheduled Publication !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:cef753abd2d2190dffaeb9c0e46f4a7f4668b6f462166a90b86c8a8e16dc88d1 + !! source digest: sha256:8e0d97fc54eec30888d9864ded2931bb756cc3cfb3303dbd434cab7c2aabcc0a !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/website_blog_scheduled_publication/__manifest__.py b/website_blog_scheduled_publication/__manifest__.py index 421c6b6a70..357fcdb6ff 100644 --- a/website_blog_scheduled_publication/__manifest__.py +++ b/website_blog_scheduled_publication/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Blog Scheduled Publication", "summary": "Schedule blog posts publication date", - "version": "18.0.1.0.0", + "version": "18.0.1.1.0", "author": "Escodoo, Odoo Community Association (OCA)", "maintainers": ["marcelsavegnago", "CristianoMafraJunior"], "license": "AGPL-3", diff --git a/website_blog_scheduled_publication/static/description/index.html b/website_blog_scheduled_publication/static/description/index.html index b4104c8801..ba21b69df0 100644 --- a/website_blog_scheduled_publication/static/description/index.html +++ b/website_blog_scheduled_publication/static/description/index.html @@ -372,7 +372,7 @@

Blog Scheduled Publication

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:cef753abd2d2190dffaeb9c0e46f4a7f4668b6f462166a90b86c8a8e16dc88d1 +!! source digest: sha256:8e0d97fc54eec30888d9864ded2931bb756cc3cfb3303dbd434cab7c2aabcc0a !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module allows you to schedule blog posts for automatic publication diff --git a/website_blog_scheduled_publication/views/blog_post_views.xml b/website_blog_scheduled_publication/views/blog_post_views.xml index 05d5ab4e65..00b7d0c6fd 100644 --- a/website_blog_scheduled_publication/views/blog_post_views.xml +++ b/website_blog_scheduled_publication/views/blog_post_views.xml @@ -125,7 +125,7 @@ Scheduled Blog Posts blog.post - calendar,tree,form + calendar,list,form [('scheduled_publication_date', '!=', False)] {} diff --git a/website_event_altcha/README.rst b/website_event_altcha/README.rst new file mode 100644 index 0000000000..a093f1b4ec --- /dev/null +++ b/website_event_altcha/README.rst @@ -0,0 +1,89 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================================= +Privacy Friendly Captcha - Event Registration +============================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f2eb50353d5a95993455af76b139a72c0735ecfa07a9cf5e016198b817a5d06c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fwebsite-lightgray.png?logo=github + :target: https://github.com/OCA/website/tree/18.0/website_event_altcha + :alt: OCA/website +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/website-18-0/website-18-0-website_event_altcha + :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/website&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds ALTCHA support to event registration forms + +**Table of contents** + +.. contents:: + :local: + +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 +------- + +* Hunki Enterprises BV + +Contributors +------------ + +- Holger Brunn + (https://hunki-enterprises.com) + +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. + +.. |maintainer-hbrunn| image:: https://github.com/hbrunn.png?size=40px + :target: https://github.com/hbrunn + :alt: hbrunn + +Current `maintainer `__: + +|maintainer-hbrunn| + +This module is part of the `OCA/website `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_event_altcha/__init__.py b/website_event_altcha/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website_event_altcha/__manifest__.py b/website_event_altcha/__manifest__.py new file mode 100644 index 0000000000..8134b74d0b --- /dev/null +++ b/website_event_altcha/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2026 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Privacy Friendly Captcha - Event Registration", + "summary": "Support event registration with ALTCHA", + "version": "18.0.1.1.0", + "license": "AGPL-3", + "author": "Hunki Enterprises BV,Odoo Community Association (OCA)", + "maintainers": ["hbrunn"], + "website": "https://github.com/OCA/website", + "depends": [ + "website_altcha", + "website_event", + ], + "assets": { + "web.assets_frontend": [ + "website_event_altcha/static/src/*.esm.js", + ], + "web.assets_tests": [ + "website_event_altcha/static/tests/tours/website_event_altcha.esm.js", + ], + }, + "auto_install": True, +} diff --git a/website_event_altcha/i18n/it.po b/website_event_altcha/i18n/it.po new file mode 100644 index 0000000000..c0d5085374 --- /dev/null +++ b/website_event_altcha/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/website_event_altcha/i18n/website_event_altcha.pot b/website_event_altcha/i18n/website_event_altcha.pot new file mode 100644 index 0000000000..aadee09bfe --- /dev/null +++ b/website_event_altcha/i18n/website_event_altcha.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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" diff --git a/website_event_altcha/pyproject.toml b/website_event_altcha/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_event_altcha/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_event_altcha/readme/CONTRIBUTORS.md b/website_event_altcha/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..cf8bf7205a --- /dev/null +++ b/website_event_altcha/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Holger Brunn \ (https://hunki-enterprises.com) diff --git a/website_event_altcha/readme/DESCRIPTION.md b/website_event_altcha/readme/DESCRIPTION.md new file mode 100644 index 0000000000..33a3690849 --- /dev/null +++ b/website_event_altcha/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module adds ALTCHA support to event registration forms diff --git a/website_event_altcha/static/description/icon.png b/website_event_altcha/static/description/icon.png new file mode 100644 index 0000000000..d8e30bafa5 Binary files /dev/null and b/website_event_altcha/static/description/icon.png differ diff --git a/website_event_altcha/static/description/index.html b/website_event_altcha/static/description/index.html new file mode 100644 index 0000000000..b515601620 --- /dev/null +++ b/website_event_altcha/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +README.rst + + + +

+ + + +Odoo Community Association + +
+

Privacy Friendly Captcha - Event Registration

+ +

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

+

This module adds ALTCHA support to event registration forms

+

Table of contents

+ +
+

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

+
    +
  • Hunki Enterprises BV
  • +
+
+ +
+

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.

+

Current maintainer:

+

hbrunn

+

This module is part of the OCA/website project on GitHub.

+

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

+
+
+
+
+ + diff --git a/website_event_altcha/static/src/EventRegistrationForm.esm.js b/website_event_altcha/static/src/EventRegistrationForm.esm.js new file mode 100644 index 0000000000..f375ef6324 --- /dev/null +++ b/website_event_altcha/static/src/EventRegistrationForm.esm.js @@ -0,0 +1,32 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import {AltchaLegacyClassFunctionality} from "@website_altcha/altcha.esm"; +import EventRegistrationForm from "@website_event/js/website_event"; + +EventRegistrationForm.include({ + ...AltchaLegacyClassFunctionality, +}); + +EventRegistrationForm.include({ + altcha_prepend_to: "div.modal-footer", + start() { + const result = this._super(...arguments); + const altcha_widget = this.el.querySelector("altcha-widget"); + if (altcha_widget) { + const button = this.el.querySelector("button.btn-primary"); + altcha_widget.addEventListener("statechange", (ev) => { + button.disabled = ev.detail.state !== "verified"; + }); + altcha_widget.addEventListener("load", () => { + button.disabled = altcha_widget.getState() !== "verified"; + }); + } + return result; + }, + _addTurnstile(form) { + const altcha_widget = this.el.querySelector("altcha-widget"); + if (altcha_widget) { + form.querySelector("div.modal-footer").prepend(altcha_widget); + } + this._super(...arguments); + }, +}); diff --git a/website_event_altcha/static/src/ticketDetailsWidget.esm.js b/website_event_altcha/static/src/ticketDetailsWidget.esm.js new file mode 100644 index 0000000000..0a3c3548e0 --- /dev/null +++ b/website_event_altcha/static/src/ticketDetailsWidget.esm.js @@ -0,0 +1,20 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import ticketDetailsWidget from "@website_event/js/website_event_ticket_details"; + +ticketDetailsWidget.include({ + start() { + this.disableSubmitIfUnverified(); + return this._super(...arguments); + }, + _onTicketQuantityChange() { + this._super(...arguments); + this.disableSubmitIfUnverified(); + }, + disableSubmitIfUnverified() { + const altcha_widget = this.el.querySelector("altcha-widget"); + if (altcha_widget) { + const button = this.el.querySelector("button.btn-primary"); + button.disabled = altcha_widget.getState() !== "verified"; + } + }, +}); diff --git a/website_event_altcha/static/tests/tours/website_event_altcha.esm.js b/website_event_altcha/static/tests/tours/website_event_altcha.esm.js new file mode 100644 index 0000000000..74983b2a3f --- /dev/null +++ b/website_event_altcha/static/tests/tours/website_event_altcha.esm.js @@ -0,0 +1,51 @@ +/* Copyright 2026 Hunki Enterprises BV */ +import {registry} from "@web/core/registry"; + +registry.category("web_tour.tours").add("website_event_altcha", { + steps: () => [ + { + content: "Click register button", + trigger: "button[data-bs-target='#modal_ticket_registration']", + run: "click", + }, + { + content: "Select a ticket", + trigger: "#o_wevent_tickets_collapse select", + run: "selectByIndex 1", + }, + { + content: "Verify altcha", + trigger: "altcha-widget input[type='checkbox']", + run: "click", + }, + { + content: "Click Submit", + trigger: "#registration_form button[type='submit']:enabled", + run: "click", + }, + { + content: "Fill in name", + trigger: "#attendee_registration input[name*='-name-']:enabled", + async run() { + this.anchor.value = "Testname"; + }, + }, + { + content: "Fill in email", + trigger: "#attendee_registration input[name*='-email-']:enabled", + async run() { + this.anchor.value = "test_website_altcha@test.com"; + }, + }, + { + content: "Verify altcha value exists", + trigger: + "#attendee_registration input[name='altcha']:not(:visible):not(:empty)", + }, + { + content: "Click submit", + trigger: "#attendee_registration button[type='submit']", + run: "click", + }, + ], +}); diff --git a/website_event_altcha/tests/__init__.py b/website_event_altcha/tests/__init__.py new file mode 100644 index 0000000000..75a0c88ebe --- /dev/null +++ b/website_event_altcha/tests/__init__.py @@ -0,0 +1 @@ +from . import test_website_event_altcha diff --git a/website_event_altcha/tests/test_website_event_altcha.py b/website_event_altcha/tests/test_website_event_altcha.py new file mode 100644 index 0000000000..f23594165b --- /dev/null +++ b/website_event_altcha/tests/test_website_event_altcha.py @@ -0,0 +1,13 @@ +# Copyright 2026 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.website_altcha.tests.common import Common + + +class TestWebsiteEventAltcha(Common): + def test_event_registration_with_questions(self): + """ + Test an event registration that contains questions + """ + event = self.env.ref("event.event_7") + self.start_tour(f"/event/{event.id}", "website_event_altcha") diff --git a/website_exclude_sitemap/README.rst b/website_exclude_sitemap/README.rst new file mode 100644 index 0000000000..ebf263394e --- /dev/null +++ b/website_exclude_sitemap/README.rst @@ -0,0 +1,250 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +Website Exclude Sitemap +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1ccf5cfe1f1d791baa26c077f275917693c8d97994ffa35c33769fd3ddd03198 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fwebsite-lightgray.png?logo=github + :target: https://github.com/OCA/website/tree/18.0/website_exclude_sitemap + :alt: OCA/website +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/website-18-0/website-18-0-website_exclude_sitemap + :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/website&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module lets website administrators exclude public website URLs from +``/sitemap.xml``. + +The module only affects sitemap generation. It does not unpublish pages, +change access rights, or block direct access to the excluded URLs. + +It also clears the cached sitemap when website pages are created, +deleted, or their URL changes, so sitemap URLs are refreshed in the next +sitemap generation. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Odoo includes public website URLs in the generated sitemap. + +Some public URLs can be useful for direct navigation but add little or +no SEO value when exposed in ``/sitemap.xml``. + +This module was created to keep the sitemap focused on strategic public +pages while leaving normal website access untouched. + +Configuration +============= + +Configure sitemap exclusions: + +1. Go to **Website > Configuration > Settings**. +2. Locate the **Sitemap Exclusions** setting. +3. Enter one or more paths or glob patterns. +4. Save the settings. + +Values can be separated by: + +- line breaks; +- commas; +- semicolons. + +For example, these values are equivalent: + +.. code:: text + + /customers/ + /livechat + /blog/*/feed + /jobs/apply/ + /profile/ + +.. code:: text + + /customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/ + +.. code:: text + + /customers/; /livechat; /blog/*/feed + /jobs/apply/, /profile/ + +Lines starting with ``#`` are comments. + +See the usage section for the full pattern syntax: ``*`` matches inside +one path segment, ``**`` crosses ``/``, and a pattern ending in ``/`` or +in ``/**`` excludes that path and everything below it. + +The default value only covers paths that any Odoo website has. Add the +paths that are specific to your own deployment, for example: + +.. code:: text + + /website/info + /create-container-error + /machine-creation + +``/website/info`` is already left out of the sitemap by the core when +its view is inactive; add it here when that view stays active. + +Use the **Reload Sitemap** button in the same settings block to clear +the cached sitemap manually. + +Usage +===== + +Sitemap exclusions: + +1. Create or publish website pages as usual. + +2. Configure the paths or patterns to exclude in **Website > + Configuration > Settings > Sitemap Exclusions**. + + |Sitemap exclusions| + +3. Open ``/sitemap.xml``. + +4. Confirm that excluded URLs are absent and non-excluded public pages + are still present. + +Pattern syntax: + ++------------------+--------------------------+-------------------------+ +| Pattern | Matches | Does not match | ++==================+==========================+=========================+ +| ``/livechat`` | ``/livechat`` | ``/livechat/room`` | ++------------------+--------------------------+-------------------------+ +| ``/customers/`` | ``/customers``, | ``/customers-list`` | +| | ``/customers/acme``, | | +| | ``/customers/acme/2024`` | | ++------------------+--------------------------+-------------------------+ +| ``/blog/*/feed`` | ``/blog/news/feed`` | ``/blog/a/b/feed``, | +| | | ``/blog/news`` | ++------------------+--------------------------+-------------------------+ +| ``/solutions*`` | ``/solutions``, | ``/solutions/cloud`` | +| | ``/solutions-cloud`` | | ++------------------+--------------------------+-------------------------+ +| ``/jobs/**`` | ``/jobs``, | ``/jobs-list`` | +| | ``/jobs/apply/1``, | | +| | ``/jobs/a/b/c`` | | ++------------------+--------------------------+-------------------------+ + +In short: + +- ``*`` matches inside a single path segment; it never crosses a ``/``. +- ``**`` crosses ``/``, so it matches any number of segments. +- A pattern ending in ``/`` or in ``/**`` is a prefix: it excludes that + path and everything below it. +- Any other pattern matches that exact path. The trailing slash of the + URL is ignored, so ``/livechat`` and ``/livechat/`` are the same path. + +Example: + +- configure ``/customers/``; +- publish a page at ``/customers``; +- publish another page at ``/customers/other``; +- open ``/sitemap.xml``. + +Both URLs are excluded, because ``/customers/`` is a prefix. Use +``/customers`` without the trailing slash to exclude only the first one. + +Ways to update the sitemap.xml: + +1. The sitemap cache is automatically cleared when website pages are + created or deleted, and when their URL, publication state, + indexation, publication date or website changes. + +2. The manual **Reload Sitemap** button can be used when a forced + sitemap cache clear is needed. + + |Reload manual sitemap| + +.. |Sitemap exclusions| image:: https://raw.githubusercontent.com/OCA/website/18.0/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png +.. |Reload manual sitemap| image:: https://raw.githubusercontent.com/OCA/website/18.0/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png + +Known issues / Roadmap +====================== + +The exclusion list is a denylist evaluated after Odoo has collected +every sitemap source, so it only removes URLs, never adds them. When a +record is marked as indexed but its URL matches an exclusion pattern, +the pattern wins and the URL stays out of ``/sitemap.xml``. + +Nothing warns about that in the record itself: a page whose **Indexed** +toggle is on, or a published blog post, gives no hint that a global +pattern is keeping it out of the sitemap. The only place where the +exclusions are visible is \**Website + + Configuration > Settings*\*. + +A future version could show the conflict where the record is edited, for +example a message next to the **Indexed** toggle of ``website.page``. +Covering models from other modules, such as ``blog.post``, belongs in +bridge modules instead, to keep this module depending on ``website`` +alone. + +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 +------- + +* Binhex + +Contributors +------------ + +- `Binhex `__: + + - Edilio Escalona Almira e.escalona@binhex.cloud + +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/website `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_exclude_sitemap/__init__.py b/website_exclude_sitemap/__init__.py new file mode 100644 index 0000000000..ab9413bd19 --- /dev/null +++ b/website_exclude_sitemap/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizards diff --git a/website_exclude_sitemap/__manifest__.py b/website_exclude_sitemap/__manifest__.py new file mode 100644 index 0000000000..3a40d8a4d8 --- /dev/null +++ b/website_exclude_sitemap/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Website Exclude Sitemap", + "summary": """ + Exclude addresses from the sitemap.xml file according to + the values defined in the configuration + """, + "category": "Website", + "version": "18.0.1.0.0", + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/website", + "license": "AGPL-3", + "depends": [ + "website", + ], + "data": [ + "wizards/res_config_settings_views.xml", + ], +} diff --git a/website_exclude_sitemap/i18n/it.po b/website_exclude_sitemap/i18n/it.po new file mode 100644 index 0000000000..9cba8a4ed7 --- /dev/null +++ b/website_exclude_sitemap/i18n/it.po @@ -0,0 +1,77 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_exclude_sitemap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Exclude specific URLs or wildcard patterns from sitemap.xml." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,help:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,help:website_exclude_sitemap.field_website__sitemap_excluded_paths +msgid "" +"One path or pattern per line, comma, or semicolon. * matches inside one path" +" segment, ** across segments, and a trailing / excludes a path and " +"everything below it. Examples: /contactus, /solutions*, /blog/*/feed, " +"/customers/, /jobs/**" +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website_page +msgid "Page" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Reload Sitemap" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "" +"Separate paths with line breaks, commas, or semicolons.\n" +" Use * inside a segment, ** across segments, and a\n" +" trailing / to exclude a path and everything below it." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_website__sitemap_excluded_paths +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Sitemap Exclusions" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "Sitemap reloaded" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "The cached sitemap has been cleared for the current website." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website +msgid "Website" +msgstr "" diff --git a/website_exclude_sitemap/i18n/website_exclude_sitemap.pot b/website_exclude_sitemap/i18n/website_exclude_sitemap.pot new file mode 100644 index 0000000000..c370fc3a3a --- /dev/null +++ b/website_exclude_sitemap/i18n/website_exclude_sitemap.pot @@ -0,0 +1,76 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_exclude_sitemap +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Exclude specific URLs or wildcard patterns from sitemap.xml." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,help:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,help:website_exclude_sitemap.field_website__sitemap_excluded_paths +msgid "" +"One path or pattern per line, comma, or semicolon. * matches inside one path" +" segment, ** across segments, and a trailing / excludes a path and " +"everything below it. Examples: /contactus, /solutions*, /blog/*/feed, " +"/customers/, /jobs/**" +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website_page +msgid "Page" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Reload Sitemap" +msgstr "" + +#. module: website_exclude_sitemap +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "" +"Separate paths with line breaks, commas, or semicolons.\n" +" Use * inside a segment, ** across segments, and a\n" +" trailing / to exclude a path and everything below it." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_res_config_settings__sitemap_excluded_paths +#: model:ir.model.fields,field_description:website_exclude_sitemap.field_website__sitemap_excluded_paths +#: model_terms:ir.ui.view,arch_db:website_exclude_sitemap.res_config_settings_view_form +msgid "Sitemap Exclusions" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "Sitemap reloaded" +msgstr "" + +#. module: website_exclude_sitemap +#. odoo-python +#: code:addons/website_exclude_sitemap/wizards/res_config_settings.py:0 +msgid "The cached sitemap has been cleared for the current website." +msgstr "" + +#. module: website_exclude_sitemap +#: model:ir.model,name:website_exclude_sitemap.model_website +msgid "Website" +msgstr "" diff --git a/website_exclude_sitemap/models/__init__.py b/website_exclude_sitemap/models/__init__.py new file mode 100644 index 0000000000..5898d43284 --- /dev/null +++ b/website_exclude_sitemap/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import website +from . import website_page diff --git a/website_exclude_sitemap/models/website.py b/website_exclude_sitemap/models/website.py new file mode 100644 index 0000000000..9619718ac8 --- /dev/null +++ b/website_exclude_sitemap/models/website.py @@ -0,0 +1,159 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import re +from urllib.parse import urlsplit + +from odoo import api, fields, models, tools + +SITEMAP_DEFAULT_EXCLUDED_PATHS = ( + "/customers/", + "/livechat", + "/blog/*/feed", + "/jobs/apply/", + "/profile/", +) + +SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT = "\n".join(SITEMAP_DEFAULT_EXCLUDED_PATHS) + +SITEMAP_WILDCARDS = {"**": ".*", "*": "[^/]*"} + + +class Website(models.Model): + _inherit = "website" + + sitemap_excluded_paths = fields.Text( + string="Sitemap Exclusions", + default=SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT, + help=( + "One path or pattern per line, comma, or semicolon. " + "* matches inside one path segment, ** across segments, and a " + "trailing / excludes a path and everything below it. " + "Examples: /contactus, /solutions*, /blog/*/feed, /customers/, " + "/jobs/**" + ), + ) + + @staticmethod + def _normalize_sitemap_path(value): + """Return the path to compare, without its trailing slash.""" + value = (value or "").strip() + if not value: + return "/" + if "://" in value: + parsed = urlsplit(value) + value = parsed.path or "/" + if parsed.query: + value = f"{value}?{parsed.query}" + if not value.startswith("/"): + value = f"/{value}" + return "/" if value == "/" else value.rstrip("/") + + @classmethod + def _normalize_sitemap_pattern(cls, value): + """Return the pattern, keeping the trailing slash that marks a prefix.""" + value = (value or "").strip() + normalized = cls._normalize_sitemap_path(value) + if normalized == "/" or not value.endswith("/"): + return normalized + return f"{normalized}/" + + @classmethod + def _split_sitemap_patterns(cls, excluded_paths): + """Split the raw exclusions text into normalized patterns.""" + return [ + cls._normalize_sitemap_pattern(pattern) + for pattern in re.split(r"[\n,;]+", excluded_paths or "") + if pattern.strip() and not pattern.lstrip().startswith("#") + ] + + def _get_sitemap_excluded_patterns(self): + self.ensure_one() + return self._split_sitemap_patterns(self.sitemap_excluded_paths) + + @classmethod + def _compile_sitemap_pattern(cls, pattern): + """Compile an exclusion pattern into a regex matched against a path. + + ``*`` matches inside a single path segment, ``**`` crosses ``/``, and a + pattern ending in ``/`` or in ``/**`` is a prefix: it matches that path + and everything below it. + """ + is_prefix = False + if pattern.endswith("/**"): + is_prefix = True + pattern = pattern[: -len("/**")] + elif pattern != "/" and pattern.endswith("/"): + is_prefix = True + pattern = pattern[:-1] + body = "".join( + SITEMAP_WILDCARDS.get(token, re.escape(token)) + for token in re.split(r"(\*\*|\*)", pattern) + ) + return re.compile(f"^{body}(/.*)?$" if is_prefix else f"^{body}$") + + @tools.ormcache("excluded_paths") + def _get_sitemap_excluded_regexes(self, excluded_paths): + """Return the compiled patterns of the given raw exclusions text. + + The result is cached per database on the text itself, so the patterns + are compiled once instead of once per sitemap entry. + """ + return tuple( + self._compile_sitemap_pattern(pattern) + for pattern in self._split_sitemap_patterns(excluded_paths) + ) + + def _get_own_sitemap_excluded_regexes(self): + self.ensure_one() + return self._get_sitemap_excluded_regexes(self.sitemap_excluded_paths or "") + + def _is_sitemap_path_excluded(self, path): + self.ensure_one() + normalized_path = self._normalize_sitemap_path(path) + return any( + regex.match(normalized_path) + for regex in self._get_own_sitemap_excluded_regexes() + ) + + def _clear_sitemap_cache(self): + IrAttachment = self.env["ir.attachment"].sudo() + for website in self: + IrAttachment.search( + [ + ("type", "=", "binary"), + ("url", "=like", f"/sitemap-{website.id}-%"), + ] + ).unlink() + + @api.model_create_multi + def create(self, vals_list): + websites = super().create(vals_list) + websites.filtered("sitemap_excluded_paths")._clear_sitemap_cache() + return websites + + def write(self, vals): + result = super().write(vals) + if "sitemap_excluded_paths" in vals: + self.env.registry.clear_cache() + self._clear_sitemap_cache() + return result + + def _enumerate_pages(self, query_string=None, force=False): + """Drop the excluded URLs from the sitemap enumeration. + + ``force=True`` means the caller wants every page, not the sitemap + listing: it is used by ``search_pages`` to feed the link autocomplete of + the editor. The exclusions only apply to the sitemap, so that call is + left untouched. + """ + self.ensure_one() + pages = super()._enumerate_pages(query_string=query_string, force=force) + if force: + yield from pages + return + regexes = self._get_own_sitemap_excluded_regexes() + for page in pages: + path = self._normalize_sitemap_path(page.get("loc")) + if not any(regex.match(path) for regex in regexes): + yield page diff --git a/website_exclude_sitemap/models/website_page.py b/website_exclude_sitemap/models/website_page.py new file mode 100644 index 0000000000..6b984cd451 --- /dev/null +++ b/website_exclude_sitemap/models/website_page.py @@ -0,0 +1,41 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class WebsitePage(models.Model): + _inherit = "website.page" + + _SITEMAP_CACHE_FIELDS = { + "url", + "website_published", + "website_indexed", + "date_publish", + "website_id", + } + + def _get_sitemap_cache_websites(self): + website_id = self.website_id + if any(not page.website_id for page in self): + website_id |= self.env["website"].search([]) + return website_id + + @api.model_create_multi + def create(self, vals_list): + pages = super().create(vals_list) + pages._get_sitemap_cache_websites()._clear_sitemap_cache() + return pages + + def write(self, vals): + websites = self._get_sitemap_cache_websites() + result = super().write(vals) + if self._SITEMAP_CACHE_FIELDS.intersection(vals): + (websites | self._get_sitemap_cache_websites())._clear_sitemap_cache() + return result + + def unlink(self): + websites = self._get_sitemap_cache_websites() + result = super().unlink() + websites._clear_sitemap_cache() + return result diff --git a/website_exclude_sitemap/pyproject.toml b/website_exclude_sitemap/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_exclude_sitemap/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_exclude_sitemap/readme/CONFIGURE.md b/website_exclude_sitemap/readme/CONFIGURE.md new file mode 100644 index 0000000000..40e4acd3a8 --- /dev/null +++ b/website_exclude_sitemap/readme/CONFIGURE.md @@ -0,0 +1,52 @@ +Configure sitemap exclusions: + +1. Go to **Website > Configuration > Settings**. +2. Locate the **Sitemap Exclusions** setting. +3. Enter one or more paths or glob patterns. +4. Save the settings. + +Values can be separated by: + +- line breaks; +- commas; +- semicolons. + +For example, these values are equivalent: + +```text +/customers/ +/livechat +/blog/*/feed +/jobs/apply/ +/profile/ +``` + +```text +/customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/ +``` + +```text +/customers/; /livechat; /blog/*/feed +/jobs/apply/, /profile/ +``` + +Lines starting with `#` are comments. + +See the usage section for the full pattern syntax: `*` matches inside one path +segment, `**` crosses `/`, and a pattern ending in `/` or in `/**` excludes that +path and everything below it. + +The default value only covers paths that any Odoo website has. Add the paths +that are specific to your own deployment, for example: + +```text +/website/info +/create-container-error +/machine-creation +``` + +`/website/info` is already left out of the sitemap by the core when its view is +inactive; add it here when that view stays active. + +Use the **Reload Sitemap** button in the same settings block to clear the +cached sitemap manually. diff --git a/website_exclude_sitemap/readme/CONTEXT.md b/website_exclude_sitemap/readme/CONTEXT.md new file mode 100644 index 0000000000..c8bddc83cd --- /dev/null +++ b/website_exclude_sitemap/readme/CONTEXT.md @@ -0,0 +1,7 @@ +Odoo includes public website URLs in the generated sitemap. + +Some public URLs can be useful for direct navigation but add little or no SEO value when exposed +in `/sitemap.xml`. + +This module was created to keep the sitemap focused on strategic public pages +while leaving normal website access untouched. diff --git a/website_exclude_sitemap/readme/CONTRIBUTORS.md b/website_exclude_sitemap/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..4c895949c9 --- /dev/null +++ b/website_exclude_sitemap/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Binhex](https://www.binhex.cloud): + - Edilio Escalona Almira diff --git a/website_exclude_sitemap/readme/DESCRIPTION.md b/website_exclude_sitemap/readme/DESCRIPTION.md new file mode 100644 index 0000000000..9efe28a477 --- /dev/null +++ b/website_exclude_sitemap/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +This module lets website administrators exclude public website URLs from +`/sitemap.xml`. + +The module only affects sitemap generation. It does not unpublish pages, change +access rights, or block direct access to the excluded URLs. + +It also clears the cached sitemap when website pages are created, deleted, or +their URL changes, so sitemap URLs are refreshed in the next sitemap generation. diff --git a/website_exclude_sitemap/readme/ROADMAP.md b/website_exclude_sitemap/readme/ROADMAP.md new file mode 100644 index 0000000000..1b4ea35171 --- /dev/null +++ b/website_exclude_sitemap/readme/ROADMAP.md @@ -0,0 +1,14 @@ +The exclusion list is a denylist evaluated after Odoo has collected every +sitemap source, so it only removes URLs, never adds them. When a record is +marked as indexed but its URL matches an exclusion pattern, the pattern wins and +the URL stays out of `/sitemap.xml`. + +Nothing warns about that in the record itself: a page whose **Indexed** toggle is +on, or a published blog post, gives no hint that a global pattern is keeping it +out of the sitemap. The only place where the exclusions are visible is **Website +> Configuration > Settings**. + +A future version could show the conflict where the record is edited, for example +a message next to the **Indexed** toggle of `website.page`. Covering models from +other modules, such as `blog.post`, belongs in bridge modules instead, to keep +this module depending on `website` alone. diff --git a/website_exclude_sitemap/readme/USAGE.md b/website_exclude_sitemap/readme/USAGE.md new file mode 100644 index 0000000000..d64f6898f3 --- /dev/null +++ b/website_exclude_sitemap/readme/USAGE.md @@ -0,0 +1,51 @@ +Sitemap exclusions: + +1. Create or publish website pages as usual. +2. Configure the paths or patterns to exclude in **Website > Configuration > + Settings > Sitemap Exclusions**. + + ![Sitemap exclusions](../static/img/readme/config_sitemap_exclusions.png) + +3. Open `/sitemap.xml`. +4. Confirm that excluded URLs are absent and non-excluded public pages are still + present. + +Pattern syntax: + +| Pattern | Matches | Does not match | +| -------------- | ------------------------------------------------------- | ---------------------------------- | +| `/livechat` | `/livechat` | `/livechat/room` | +| `/customers/` | `/customers`, `/customers/acme`, `/customers/acme/2024` | `/customers-list` | +| `/blog/*/feed` | `/blog/news/feed` | `/blog/a/b/feed`, `/blog/news` | +| `/solutions*` | `/solutions`, `/solutions-cloud` | `/solutions/cloud` | +| `/jobs/**` | `/jobs`, `/jobs/apply/1`, `/jobs/a/b/c` | `/jobs-list` | + +In short: + +- `*` matches inside a single path segment; it never crosses a `/`. +- `**` crosses `/`, so it matches any number of segments. +- A pattern ending in `/` or in `/**` is a prefix: it excludes that path and + everything below it. +- Any other pattern matches that exact path. The trailing slash of the URL is + ignored, so `/livechat` and `/livechat/` are the same path. + +Example: + +- configure `/customers/`; +- publish a page at `/customers`; +- publish another page at `/customers/other`; +- open `/sitemap.xml`. + +Both URLs are excluded, because `/customers/` is a prefix. Use `/customers` +without the trailing slash to exclude only the first one. + +Ways to update the sitemap.xml: + +1. The sitemap cache is automatically cleared when website pages are created or + deleted, and when their URL, publication state, indexation, publication date + or website changes. + +2. The manual **Reload Sitemap** button can be used when a forced sitemap cache clear is + needed. + + ![Reload manual sitemap](../static/img/readme/reload_manual_sitemap.png) diff --git a/website_exclude_sitemap/static/description/icon.png b/website_exclude_sitemap/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/website_exclude_sitemap/static/description/icon.png differ diff --git a/website_exclude_sitemap/static/description/index.html b/website_exclude_sitemap/static/description/index.html new file mode 100644 index 0000000000..29abdf15f5 --- /dev/null +++ b/website_exclude_sitemap/static/description/index.html @@ -0,0 +1,602 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Website Exclude Sitemap

+ +

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

+

This module lets website administrators exclude public website URLs from +/sitemap.xml.

+

The module only affects sitemap generation. It does not unpublish pages, +change access rights, or block direct access to the excluded URLs.

+

It also clears the cached sitemap when website pages are created, +deleted, or their URL changes, so sitemap URLs are refreshed in the next +sitemap generation.

+

Table of contents

+ +
+

Use Cases / Context

+

Odoo includes public website URLs in the generated sitemap.

+

Some public URLs can be useful for direct navigation but add little or +no SEO value when exposed in /sitemap.xml.

+

This module was created to keep the sitemap focused on strategic public +pages while leaving normal website access untouched.

+
+
+

Configuration

+

Configure sitemap exclusions:

+
    +
  1. Go to Website > Configuration > Settings.
  2. +
  3. Locate the Sitemap Exclusions setting.
  4. +
  5. Enter one or more paths or glob patterns.
  6. +
  7. Save the settings.
  8. +
+

Values can be separated by:

+
    +
  • line breaks;
  • +
  • commas;
  • +
  • semicolons.
  • +
+

For example, these values are equivalent:

+
+/customers/
+/livechat
+/blog/*/feed
+/jobs/apply/
+/profile/
+
+
+/customers/, /livechat, /blog/*/feed, /jobs/apply/, /profile/
+
+
+/customers/; /livechat; /blog/*/feed
+/jobs/apply/, /profile/
+
+

Lines starting with # are comments.

+

See the usage section for the full pattern syntax: * matches inside +one path segment, ** crosses /, and a pattern ending in / or +in /** excludes that path and everything below it.

+

The default value only covers paths that any Odoo website has. Add the +paths that are specific to your own deployment, for example:

+
+/website/info
+/create-container-error
+/machine-creation
+
+

/website/info is already left out of the sitemap by the core when +its view is inactive; add it here when that view stays active.

+

Use the Reload Sitemap button in the same settings block to clear +the cached sitemap manually.

+
+
+

Usage

+

Sitemap exclusions:

+
    +
  1. Create or publish website pages as usual.

    +
  2. +
  3. Configure the paths or patterns to exclude in Website > +Configuration > Settings > Sitemap Exclusions.

    +

    Sitemap exclusions

    +
  4. +
  5. Open /sitemap.xml.

    +
  6. +
  7. Confirm that excluded URLs are absent and non-excluded public pages +are still present.

    +
  8. +
+

Pattern syntax:

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PatternMatchesDoes not match
/livechat/livechat/livechat/room
/customers//customers, +/customers/acme, +/customers/acme/2024/customers-list
/blog/*/feed/blog/news/feed/blog/a/b/feed, +/blog/news
/solutions*/solutions, +/solutions-cloud/solutions/cloud
/jobs/**/jobs, +/jobs/apply/1, +/jobs/a/b/c/jobs-list
+

In short:

+
    +
  • * matches inside a single path segment; it never crosses a /.
  • +
  • ** crosses /, so it matches any number of segments.
  • +
  • A pattern ending in / or in /** is a prefix: it excludes that +path and everything below it.
  • +
  • Any other pattern matches that exact path. The trailing slash of the +URL is ignored, so /livechat and /livechat/ are the same path.
  • +
+

Example:

+
    +
  • configure /customers/;
  • +
  • publish a page at /customers;
  • +
  • publish another page at /customers/other;
  • +
  • open /sitemap.xml.
  • +
+

Both URLs are excluded, because /customers/ is a prefix. Use +/customers without the trailing slash to exclude only the first one.

+

Ways to update the sitemap.xml:

+
    +
  1. The sitemap cache is automatically cleared when website pages are +created or deleted, and when their URL, publication state, +indexation, publication date or website changes.

    +
  2. +
  3. The manual Reload Sitemap button can be used when a forced +sitemap cache clear is needed.

    +

    Reload manual sitemap

    +
  4. +
+
+
+

Known issues / Roadmap

+

The exclusion list is a denylist evaluated after Odoo has collected +every sitemap source, so it only removes URLs, never adds them. When a +record is marked as indexed but its URL matches an exclusion pattern, +the pattern wins and the URL stays out of /sitemap.xml.

+

Nothing warns about that in the record itself: a page whose Indexed +toggle is on, or a published blog post, gives no hint that a global +pattern is keeping it out of the sitemap. The only place where the +exclusions are visible is **Website

+
+Configuration > Settings**.
+

A future version could show the conflict where the record is edited, for +example a message next to the Indexed toggle of website.page. +Covering models from other modules, such as blog.post, belongs in +bridge modules instead, to keep this module depending on website +alone.

+
+
+

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

+
    +
  • Binhex
  • +
+
+
+

Contributors

+ +
+
+

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/website project on GitHub.

+

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

+
+
+
+
+ + diff --git a/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png b/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png new file mode 100644 index 0000000000..422fae99c7 Binary files /dev/null and b/website_exclude_sitemap/static/img/readme/config_sitemap_exclusions.png differ diff --git a/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png b/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png new file mode 100644 index 0000000000..632790b98b Binary files /dev/null and b/website_exclude_sitemap/static/img/readme/reload_manual_sitemap.png differ diff --git a/website_exclude_sitemap/tests/__init__.py b/website_exclude_sitemap/tests/__init__.py new file mode 100644 index 0000000000..8bfe40b8ca --- /dev/null +++ b/website_exclude_sitemap/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_website_exclude_sitemap diff --git a/website_exclude_sitemap/tests/test_website_exclude_sitemap.py b/website_exclude_sitemap/tests/test_website_exclude_sitemap.py new file mode 100644 index 0000000000..edfe4f7f36 --- /dev/null +++ b/website_exclude_sitemap/tests/test_website_exclude_sitemap.py @@ -0,0 +1,313 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.addons.base.tests.common import BaseCommon + +from ..models.website import ( + SITEMAP_DEFAULT_EXCLUDED_PATHS, + SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT, +) + + +class TestWebsiteExcludeSitemap(BaseCommon): + def setUp(self): + super().setUp() + self.website = self.env["website"].create({"name": "Sitemap Test Website"}) + + def _create_cached_sitemap(self): + sitemap_xml = f"/sitemap-{self.website.id}-test.xml" + return self.env["ir.attachment"].create( + { + "name": sitemap_xml, + "type": "binary", + "url": sitemap_xml, + "raw": b"", + "mimetype": "application/xml;charset=utf-8", + } + ) + + def _create_website_page(self, url="/test-sitemap-page"): + key_suffix = (url.strip("/") or "home").replace("/", "_").replace("-", "_") + key = f"website_exclude_sitemap.test_sitemap_page_{key_suffix}" + view = self.env["ir.ui.view"].create( + { + "name": f"Test Sitemap Page {url}", + "type": "qweb", + "key": key, + "arch": ( + f"" + "
" + "" + ), + "website_id": self.website.id, + } + ) + return self.env["website.page"].create( + { + "url": url, + "view_id": view.id, + "website_published": True, + "website_indexed": True, + } + ) + + def test_config_settings_updates_website_exclusions(self): + settings = self.env["res.config.settings"].create( + { + "website_id": self.website.id, + "sitemap_excluded_paths": "/contactus\n/shop*", + } + ) + + self.assertEqual( + self.website.sitemap_excluded_paths, settings.sitemap_excluded_paths + ) + self.assertTrue(self.website._is_sitemap_path_excluded("/contactus")) + self.assertTrue(self.website._is_sitemap_path_excluded("/shop-category")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog")) + + def test_default_exclusions_on_new_website(self): + website = self.env["website"].create({"name": "Default Sitemap Exclusions"}) + + self.assertEqual( + website.sitemap_excluded_paths, SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT + ) + + def test_normalize_sitemap_path(self): + normalize = self.website._normalize_sitemap_path + + self.assertEqual(normalize(None), "/") + self.assertEqual(normalize(" "), "/") + self.assertEqual(normalize("/"), "/") + self.assertEqual(normalize("customers/"), "/customers") + self.assertEqual( + normalize("https://www.example.com/blog/example?feed=1"), + "/blog/example?feed=1", + ) + self.assertEqual(normalize("https://www.example.com"), "/") + self.assertEqual(normalize("/customers/acme/"), "/customers/acme") + + def test_normalize_sitemap_pattern_keeps_the_prefix_mark(self): + normalize = self.website._normalize_sitemap_pattern + + self.assertEqual(normalize("/livechat"), "/livechat") + self.assertEqual(normalize("/customers/"), "/customers/") + self.assertEqual(normalize("customers/"), "/customers/") + self.assertEqual(normalize(" /jobs/apply/ "), "/jobs/apply/") + self.assertEqual(normalize("/"), "/") + + def test_exact_patterns_match_that_path_only(self): + self.website.sitemap_excluded_paths = "/livechat" + + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat/")) + self.assertFalse(self.website._is_sitemap_path_excluded("/livechat/room")) + self.assertFalse(self.website._is_sitemap_path_excluded("/livechat-room")) + + def test_trailing_slash_patterns_match_as_a_prefix(self): + self.website.sitemap_excluded_paths = "/customers/" + + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/acme")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/acme/2024")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers-list")) + + def test_single_star_matches_inside_one_segment(self): + self.website.sitemap_excluded_paths = "/blog/*/feed\n/solutions*" + + self.assertTrue(self.website._is_sitemap_path_excluded("/blog/noticias/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/a/b/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/noticias")) + self.assertTrue(self.website._is_sitemap_path_excluded("/solutions")) + self.assertTrue(self.website._is_sitemap_path_excluded("/solutions-cloud")) + self.assertFalse(self.website._is_sitemap_path_excluded("/solutions/cloud")) + + def test_double_star_crosses_segments(self): + self.website.sitemap_excluded_paths = "/jobs/**" + + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply/1")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/a/b/c")) + self.assertFalse(self.website._is_sitemap_path_excluded("/jobs-list")) + + def test_patterns_can_be_separated_by_commas_semicolons_or_newlines(self): + self.website.sitemap_excluded_paths = ( + "/customers/, /livechat; /blog/*/feed\n/jobs/apply/; /profile/" + ) + + self.assertEqual( + self.website._get_sitemap_excluded_patterns(), + [ + "/customers/", + "/livechat", + "/blog/*/feed", + "/jobs/apply/", + "/profile/", + ], + ) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/other")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + self.assertTrue(self.website._is_sitemap_path_excluded("/blog/other/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/feed/other")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile")) + + def test_commented_lines_are_ignored(self): + self.website.sitemap_excluded_paths = "# /customers/\n/livechat" + + self.assertEqual(self.website._get_sitemap_excluded_patterns(), ["/livechat"]) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + + def test_only_star_is_treated_as_wildcard(self): + self.website.sitemap_excluded_paths = ( + "/docs?/page\n/category/[abc]\n/files/*/download" + ) + + self.assertTrue(self.website._is_sitemap_path_excluded("/docs?/page")) + self.assertFalse(self.website._is_sitemap_path_excluded("/docsx/page")) + self.assertTrue(self.website._is_sitemap_path_excluded("/category/[abc]")) + self.assertFalse(self.website._is_sitemap_path_excluded("/category/a")) + self.assertTrue(self.website._is_sitemap_path_excluded("/files/a/download")) + self.assertFalse( + self.website._is_sitemap_path_excluded("/files/a/download/extra") + ) + + def test_default_patterns_are_generic(self): + self.assertEqual(len(SITEMAP_DEFAULT_EXCLUDED_PATHS), 5) + for path in ("/website/info", "/create-container-error", "/machine-creation"): + self.assertNotIn(path, SITEMAP_DEFAULT_EXCLUDED_PATHS) + + def test_requirement_default_patterns(self): + self.website.sitemap_excluded_paths = SITEMAP_DEFAULT_EXCLUDED_PATHS_TEXT + + self.assertTrue(self.website._is_sitemap_path_excluded("/customers")) + self.assertTrue(self.website._is_sitemap_path_excluded("/customers/acme")) + self.assertFalse(self.website._is_sitemap_path_excluded("/customers-list")) + self.assertTrue(self.website._is_sitemap_path_excluded("/livechat")) + self.assertFalse(self.website._is_sitemap_path_excluded("/livechat/room")) + self.assertTrue(self.website._is_sitemap_path_excluded("/blog/example/feed")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/feed/example")) + self.assertFalse(self.website._is_sitemap_path_excluded("/blog/example-post")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply")) + self.assertTrue(self.website._is_sitemap_path_excluded("/jobs/apply/engineer")) + self.assertFalse(self.website._is_sitemap_path_excluded("/jobs/application")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile")) + self.assertTrue(self.website._is_sitemap_path_excluded("/profile/demo")) + self.assertFalse(self.website._is_sitemap_path_excluded("/website/info")) + self.assertFalse(self.website._is_sitemap_path_excluded("/product/example")) + + def test_write_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + + self.website.write({"sitemap_excluded_paths": "/contactus"}) + + self.assertFalse(attachment.exists()) + + def test_reload_sitemap_button_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + settings = self.env["res.config.settings"].create( + { + "website_id": self.website.id, + } + ) + + result = settings.action_reload_sitemap() + + self.assertFalse(attachment.exists()) + self.assertEqual(result["tag"], "display_notification") + + def test_website_page_create_clears_cached_sitemaps(self): + attachment = self._create_cached_sitemap() + + self._create_website_page() + + self.assertFalse(attachment.exists()) + + def test_website_page_write_clears_cached_sitemaps(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.write({"url": "/renamed-sitemap-page"}) + + self.assertFalse(attachment.exists()) + + def test_website_page_unpublish_clears_cached_sitemaps(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.write({"website_published": False}) + + self.assertFalse(attachment.exists()) + + def test_website_page_write_keeps_cached_sitemaps_on_other_fields(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.write({"name": "Renamed Sitemap Page"}) + + self.assertTrue(attachment.exists()) + + def test_website_page_unlink_clears_cached_sitemaps(self): + page = self._create_website_page() + attachment = self._create_cached_sitemap() + + page.unlink() + + self.assertFalse(attachment.exists()) + + def _enumerated_locs(self, force=False): + pages = self.website.with_context(website_id=self.website.id)._enumerate_pages( + force=force + ) + return {page["loc"] for page in pages} + + def test_enumerate_pages_skips_excluded_urls(self): + self._create_website_page("/qa-excluded") + self._create_website_page("/qa-visible") + self.website.sitemap_excluded_paths = "/qa-excluded" + + locs = self._enumerated_locs() + + self.assertNotIn("/qa-excluded", locs) + self.assertIn("/qa-visible", locs) + + def test_enumerate_pages_skips_excluded_controller_urls(self): + """A URL yielded by a controller is excluded like a website page. + + Such URLs have no ``website.page`` record, so they cannot be taken out + of the sitemap with the ``website_indexed`` flag of the page. + """ + controller_locs = self._enumerated_locs() + page_urls = set(self.env["website.page"].search([]).mapped("url")) + controller_locs -= page_urls + self.assertTrue( + controller_locs, "no controller URL is enumerated in this database" + ) + excluded = sorted(controller_locs)[0] + + self.website.sitemap_excluded_paths = excluded + + self.assertNotIn(excluded, self._enumerated_locs()) + + def test_enumerate_pages_keeps_excluded_urls_when_forced(self): + """``force=True`` feeds the link autocomplete, not the sitemap.""" + self._create_website_page("/qa-forced") + self.website.sitemap_excluded_paths = "/qa-forced" + + self.assertNotIn("/qa-forced", self._enumerated_locs()) + self.assertIn("/qa-forced", self._enumerated_locs(force=True)) + + def test_changing_exclusions_applies_the_new_patterns(self): + self._create_website_page("/qa-first") + self._create_website_page("/qa-second") + self.website.sitemap_excluded_paths = "/qa-first" + self.assertNotIn("/qa-first", self._enumerated_locs()) + + self.website.write({"sitemap_excluded_paths": "/qa-second"}) + + locs = self._enumerated_locs() + self.assertIn("/qa-first", locs) + self.assertNotIn("/qa-second", locs) diff --git a/website_exclude_sitemap/wizards/__init__.py b/website_exclude_sitemap/wizards/__init__.py new file mode 100644 index 0000000000..c54f5395ec --- /dev/null +++ b/website_exclude_sitemap/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import res_config_settings diff --git a/website_exclude_sitemap/wizards/res_config_settings.py b/website_exclude_sitemap/wizards/res_config_settings.py new file mode 100644 index 0000000000..7aeba618f5 --- /dev/null +++ b/website_exclude_sitemap/wizards/res_config_settings.py @@ -0,0 +1,30 @@ +# Copyright 2026 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sitemap_excluded_paths = fields.Text( + related="website_id.sitemap_excluded_paths", + readonly=False, + ) + + def action_reload_sitemap(self): + self.ensure_one() + website = self.website_id or self.env["website"].get_current_website() + website._clear_sitemap_cache() + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": self.env._("Sitemap reloaded"), + "message": self.env._( + "The cached sitemap has been cleared for the current website." + ), + "type": "success", + "sticky": False, + }, + } diff --git a/website_exclude_sitemap/wizards/res_config_settings_views.xml b/website_exclude_sitemap/wizards/res_config_settings_views.xml new file mode 100644 index 0000000000..04021128a5 --- /dev/null +++ b/website_exclude_sitemap/wizards/res_config_settings_views.xml @@ -0,0 +1,30 @@ + + + + website.exclude.sitemap.res.config.settings + res.config.settings + + + + +
+ Separate paths with line breaks, commas, or semicolons. + Use * inside a segment, ** across segments, and a + trailing / to exclude a path and everything below it. +
+ +