diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index 2304a8b3a7e..d24e2683fde 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -107,6 +107,132 @@ + + True diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py index 85449fd23c2..054ec2d019c 100644 --- a/addons/website/models/res_config_settings.py +++ b/addons/website/models/res_config_settings.py @@ -28,6 +28,7 @@ class ResConfigSettings(models.TransientModel): website_default_lang_code = fields.Char('Default language code', related='website_id.default_lang_id.code', readonly=False) specific_user_account = fields.Boolean(related='website_id.specific_user_account', readonly=False, help='Are newly created user accounts website specific') + website_cookies_bar = fields.Boolean(related='website_id.cookies_bar', readonly=False) google_analytics_key = fields.Char('Google Analytics Key', related='website_id.google_analytics_key', readonly=False) google_management_client_id = fields.Char('Google Client ID', related='website_id.google_management_client_id', readonly=False) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index f9a6a9f4c77..6de22c8f241 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -59,6 +59,7 @@ class Website(models.Model): language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages) default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True) auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language") + cookies_bar = fields.Boolean('Cookies Bar', help="Display a customizable cookies bar on your website.") def _default_social_facebook(self): return self.env.ref('base.main_company').social_facebook @@ -168,6 +169,26 @@ class Website(models.Model): if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values: # invalidate the caches from static node at compile time self.env['ir.qweb'].clear_caches() + + if 'cookies_bar' in values: + if values['cookies_bar']: + cookies_view = self.env.ref('website.cookie_policy', raise_if_not_found=False) + if cookies_view: + cookies_view.with_context(website_id=self.id).write({'website_id': self.id}) + specific_cook_view = self.with_context(website_id=self.id).viewref('website.cookie_policy') + self.env['website.page'].create({ + 'is_published': True, + 'website_indexed': False, + 'url': '/cookie-policy', + 'website_id': self.id, + 'view_id': specific_cook_view.id, + }) + else: + self.env['website.page'].search([ + ('website_id', '=', self.id), + ('url', '=', '/cookie-policy'), + ]).unlink() + return result @api.model diff --git a/addons/website/static/src/js/backend/res_config_settings.js b/addons/website/static/src/js/backend/res_config_settings.js index 47a52579447..ad9373d26f1 100644 --- a/addons/website/static/src/js/backend/res_config_settings.js +++ b/addons/website/static/src/js/backend/res_config_settings.js @@ -1,7 +1,14 @@ odoo.define('website.settings', function (require) { -var BaseSettingController = require('base.settings').Controller; -var FormController = require('web.FormController'); +const BaseSettingController = require('base.settings').Controller; +const core = require('web.core'); +const Dialog = require('web.Dialog'); +const FieldBoolean = require('web.basic_fields').FieldBoolean; +const fieldRegistry = require('web.field_registry'); +const FormController = require('web.FormController'); + +const QWeb = core.qweb; +const _t = core._t; BaseSettingController.include({ @@ -33,4 +40,34 @@ BaseSettingController.include({ }, }); +const WebsiteCookiesbarField = FieldBoolean.extend({ + xmlDependencies: ['/website/static/src/xml/website.res_config_settings.xml'], + + _onChange: function () { + const checked = this.$input[0].checked; + if (!checked) { + return this._setValue(checked); + } + + const cancelCallback = () => this.$input[0].checked = !checked; + Dialog.confirm(this, null, { + title: _t("Please confirm"), + $content: QWeb.render('website.res_config_settings.cookies_modal_main'), + buttons: [{ + text: 'Do not activate', + classes: 'btn-primary', + close: true, + click: cancelCallback, + }, + { + text: 'Activate anyway', + close: true, + click: () => this._setValue(checked), + }], + cancel_callback: cancelCallback, + }); + }, +}); + +fieldRegistry.add('website_cookiesbar_field', WebsiteCookiesbarField); }); diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 9765bc57473..0ef07ccc12e 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -1,6 +1,7 @@ odoo.define('website.editor.snippets.options', function (require) { 'use strict'; +require('website.s_popup_options'); var core = require('web.core'); var Dialog = require('web.Dialog'); const wUtils = require('website.utils'); @@ -913,6 +914,63 @@ options.registry.anchor = options.Class.extend({ }, }); +options.registry.CookiesBar = options.registry.SnippetPopup.extend({ + xmlDependencies: (options.registry.SnippetPopup.prototype.xmlDependencies || []).concat( + ['/website/static/src/xml/website.cookies_bar.xml'] + ), + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change the cookies bar layout. + * + * @see this.selectClass for parameters + */ + selectLayout: function (previewMode, widgetValue, params) { + let websiteId; + this.trigger_up('context_get', { + callback: function (ctx) { + websiteId = ctx['website_id']; + }, + }); + + const $template = $(qweb.render(`website.cookies_bar.${widgetValue}`, { + websiteId: websiteId, + })); + + const $content = this.$target.find('.s_popup_content'); + const selectorsToKeep = [ + '.o_cookies_bar_text_button', + '.o_cookies_bar_text_policy', + '.o_cookies_bar_text_title', + '.o_cookies_bar_text_primary', + '.o_cookies_bar_text_secondary', + ]; + + if (this.$savedSelectors === undefined) { + this.$savedSelectors = []; + } + + for (const selector of selectorsToKeep) { + const $currentLayoutEls = $content.find(selector).contents(); + const $newLayoutEl = $template.find(selector); + if ($currentLayoutEls.length) { + // save value before change, eg 'title' is not inside 'discrete' template + // but we want to preserve it in case of select another layout later + this.$savedSelectors[selector] = $currentLayoutEls; + } + const $savedSelector = this.$savedSelectors[selector]; + if ($newLayoutEl.length && $savedSelector && $savedSelector.length) { + $newLayoutEl.empty().append($savedSelector); + } + } + + $content.empty().append($template); + }, +}); + /** * Allows edition of 'cover_properties' in website models which have such * fields (blogs, posts, events, ...). diff --git a/addons/website/static/src/snippets/s_popup/000.js b/addons/website/static/src/snippets/s_popup/000.js index 0e7983002f0..16820dcc988 100644 --- a/addons/website/static/src/snippets/s_popup/000.js +++ b/addons/website/static/src/snippets/s_popup/000.js @@ -8,14 +8,14 @@ const utils = require('web.utils'); const PopupWidget = publicWidget.Widget.extend({ selector: '.s_popup', events: { - 'click .s_popup_close': '_onCloseClick', + 'click .js_close_popup': '_onCloseClick', }, /** * @override */ start: function () { - if (!utils.get_cookie(this.$target.attr('id'))) { + if (!utils.get_cookie(this.$el.attr('id'))) { this._bindPopup(); } return this._super(...arguments); @@ -77,8 +77,8 @@ const PopupWidget = publicWidget.Widget.extend({ * @private */ _onCloseClick: function () { - const nbDays = this.$target.find('.s_popup_main').data('consentsDelay'); - utils.set_cookie(this.$target.attr('id'), true, nbDays * 24 * 60 * 60); + const nbDays = this.$el.find('.s_popup_main').data('consentsDuration'); + utils.set_cookie(this.$el.attr('id'), true, nbDays * 24 * 60 * 60); this._hidePopup(); }, }); diff --git a/addons/website/static/src/snippets/s_popup/000.scss b/addons/website/static/src/snippets/s_popup/000.scss index bb14c100322..71566512868 100644 --- a/addons/website/static/src/snippets/s_popup/000.scss +++ b/addons/website/static/src/snippets/s_popup/000.scss @@ -1,7 +1,8 @@ // s_popup .s_popup_main { .s_popup_content { - min-height: var(--o-we-toolbar-height); + // keep lower than

height (cookies bar) + min-height: $o-font-size-base * $o-line-height-base; box-shadow: $modal-content-box-shadow-sm-up; .container { // keep margin when fixed bottom diff --git a/addons/website/static/src/snippets/s_popup/options.js b/addons/website/static/src/snippets/s_popup/options.js index 4c43041c614..842cc284c83 100644 --- a/addons/website/static/src/snippets/s_popup/options.js +++ b/addons/website/static/src/snippets/s_popup/options.js @@ -8,7 +8,10 @@ options.registry.SnippetPopup = options.Class.extend({ * @override */ start: function () { - this.$target.find('.s_popup_close').on('click', () => { + // Note: the link are excluded here so that internal modal buttons do + // not close the popup as we want to allow edition of those buttons. + this.$target.find('.js_close_popup:not(a, .btn)').on('click', ev => { + ev.stopPropagation(); this.onTargetHide(); this.trigger_up('snippet_option_visibility_update', {show: false}); }); diff --git a/addons/website/static/src/xml/website.cookies_bar.xml b/addons/website/static/src/xml/website.cookies_bar.xml new file mode 100644 index 00000000000..8f43ccb3698 --- /dev/null +++ b/addons/website/static/src/xml/website.cookies_bar.xml @@ -0,0 +1,75 @@ + + + +

+

Respecting your privacy is our priority.

+
+ + +
+ We use cookies to provide you a better user experience. +
+
+ +
+ We use them to store info about your habits on our website. It will helps us to provide you the very best experience and customize what you see. +
+ By clicking on this banner, you give us permission to collect data. +
+
+ + +
+
+
+ +
+ Cookie Policy + I agree +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ Website Logo +

+
+ +
+
+
+ +
+
+ +
+
+ Cookie Policy +
+ I agree +
+
+
+ diff --git a/addons/website/static/src/xml/website.res_config_settings.xml b/addons/website/static/src/xml/website.res_config_settings.xml new file mode 100644 index 00000000000..6b6d48172bd --- /dev/null +++ b/addons/website/static/src/xml/website.res_config_settings.xml @@ -0,0 +1,22 @@ + + + +
+

+ Cookie bars may significantly impair the experience of + your visitors. We recommend you avoid them unless you have + verified with a legal advisor that you absolutely need cookie + consent in your country. +

+

+ For session cookies, authentification and analytics*, + you do not need to ask for the consent (see e.g. Opinion + 04/2012 on Cookie Consent Exemption by the EU Art.29 WP). +

+

+ * provided that your analytics is anonymized, which is not the + case by default with Google Analytics. +

+
+
+
diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml index 6a32d2b55a7..870eb8cd59e 100644 --- a/addons/website/views/res_config_settings_views.xml +++ b/addons/website/views/res_config_settings_views.xml @@ -127,7 +127,18 @@ - +
+
+ +
+
+
+
diff --git a/addons/website/views/snippets/s_popup.xml b/addons/website/views/snippets/s_popup.xml index 3ca662e8ff6..ea0b13a1c14 100644 --- a/addons/website/views/snippets/s_popup.xml +++ b/addons/website/views/snippets/s_popup.xml @@ -4,10 +4,10 @@ diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index b40c459e081..dd6092435dc 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -271,6 +271,31 @@ + +