Skip to content

Commit

Permalink
Cookie popup improvements (HyphaApp#3976)
Browse files Browse the repository at this point in the history
Fixes HyphaApp#3925.
- Cookie consent preference is now stored in local storage rather than
using cookies
- Improves on the existing cookie banner by adding a `Learn More` option
where users can read more about the essential & analytic cookies, and
toggle analytics specifically.
- If analytics are not used, the user is no longer given the option to
opt out, as Hypha only uses essential cookies by default.
- Added a link/button to the footer default that allows the user to
re-open the cookie prompt without having to clear it from their local
storage.
  • Loading branch information
wes-otf authored Jul 4, 2024
1 parent a892b7d commit 972396a
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 49 deletions.
44 changes: 44 additions & 0 deletions docs/setup/administrators/cookie-consent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Hypha comes stock with a cookie banner indicating that only essential cookies are used. You can configure this banner to display information about analytics cookies in Wagtail Admin under `Settings` -> `Cookie banner settings`.

It's possible to configure settings such as:

* Edit General cookie consent message
* Edit Essential cookies informational statement
* Enable & edit the analytics cookies informational statement

### Retrieving preferences

Cookie preferences are stored in the browser's local storage using the key `cookieconsent`, and can be globally retrieved via:

```js
localStorage.get('cookieconsent')
```

There are three valid values `cookieconsent` in local storage:

* `decline` - analytics cookies have been declined, only essential cookies should be used
* `accept` - all cookies are consented to
* `ack` - there are no analytics cookies and user accepts that only essential cookies are in use
* **null** - no selection has been made by the user in the cookie banner

On page load the cookie banner JavaScript snippet will check if cookie policies have changed (ie. the site originally only used essential cookies, user ack'd, then analytics cookies were enabled) and automatically reprompt the user with the new cookie options.

### Allowing the user to change cookie preferences

The functions to open and close the cookie consent prompts are globally exposed in the JavaScript and can be utilized via:

```js
// Open consent prompt
window.openConsentPrompt()

// Close consent prompt
window.closeConsentPrompt()
```

By default, there is a button in the footer that allows the user to re-open the cookie consent prompt:

```html
<a href="#" onclick="openConsentPrompt()">Cookie Settings</a>
```

This can be further configured in Wagtail Admin under `Settings` -> `System settings` -> `Footer content`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 4.2.11 on 2024-06-04 19:41

from django.db import migrations, models
import wagtail.fields


class Migration(migrations.Migration):
dependencies = [
("cookieconsent", "0002_remove_cookieconsentsettings_site_and_more"),
]

operations = [
migrations.AlterModelOptions(
name="cookieconsentsettings",
options={"verbose_name": "Cookie banner settings"},
),
migrations.AddField(
model_name="cookieconsentsettings",
name="cookieconsent_analytics",
field=models.BooleanField(
default=False,
verbose_name="Include consent option for analytics cookies",
),
),
migrations.AddField(
model_name="cookieconsentsettings",
name="cookieconsent_analytics_about",
field=wagtail.fields.RichTextField(
default="<p>With these cookies we count visits and traffic sources to help improve the performance of our services through metrics. These cookies show us which pages on our services are the most and the least popular, and how users navigate our services. The information collected is aggregated and contains no personally identifiable information. If you block these cookies, then we will not know when you have used our services.</p>",
verbose_name='Analytics cookies information to be displayed under "Learn More"',
),
),
migrations.AddField(
model_name="cookieconsentsettings",
name="cookieconsent_essential_about",
field=wagtail.fields.RichTextField(
default="<p>Strictly necessary for the operation of a website because they enable you to navigate around the site and use features. These cookies cannot be switched off in our systems and do not store any personally identifiable information.</p>",
verbose_name='Essential cookies information to be displayed under "Learn More"',
),
),
migrations.AlterField(
model_name="cookieconsentsettings",
name="cookieconsent_active",
field=models.BooleanField(
default=False, verbose_name="Activate cookie pop-up banner"
),
),
migrations.AlterField(
model_name="cookieconsentsettings",
name="cookieconsent_message",
field=wagtail.fields.RichTextField(
default='<p>This website deploys cookies for basic functionality and to keep it secure. These cookies are strictly necessary. Optional analysis cookies which provide us with statistical information about the use of the website may also be deployed, but only with your consent. Please review our <a href="/data-privacy-policy/">Privacy &amp; Data Policy</a> for more information.</p>',
verbose_name="Cookie consent message",
),
),
migrations.AlterField(
model_name="cookieconsentsettings",
name="cookieconsent_title",
field=models.CharField(
default="Your cookie settings",
max_length=255,
verbose_name="Cookie banner title",
),
),
]
28 changes: 23 additions & 5 deletions hypha/cookieconsent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,49 @@
@register_setting
class CookieConsentSettings(BaseGenericSetting):
class Meta:
verbose_name = "Cookie consent settings"
verbose_name = "Cookie banner settings"

cookieconsent_active = models.BooleanField(
"Activate cookie consent feature",
"Activate cookie pop-up banner",
default=False,
)

cookieconsent_title = models.CharField(
"cookie consent title",
"Cookie banner title",
max_length=255,
default="Your cookie settings",
)

cookieconsent_message = RichTextField(
"cookie consent message",
"Cookie consent message",
default='<p>This website deploys cookies for basic functionality and to keep it secure. These cookies are strictly necessary. Optional analysis cookies which provide us with statistical information about the use of the website may also be deployed, but only with your consent. Please review our <a href="/data-privacy-policy/">Privacy &amp; Data Policy</a> for more information.</p>',
)

cookieconsent_essential_about = RichTextField(
'Essential cookies information to be displayed under "Learn More"',
default="<p>Strictly necessary for the operation of a website because they enable you to navigate around the site and use features. These cookies cannot be switched off in our systems and do not store any personally identifiable information.</p>",
)

cookieconsent_analytics = models.BooleanField(
"Include consent option for analytics cookies",
default=False,
)

cookieconsent_analytics_about = RichTextField(
'Analytics cookies information to be displayed under "Learn More"',
default="<p>With these cookies we count visits and traffic sources to help improve the performance of our services through metrics. These cookies show us which pages on our services are the most and the least popular, and how users navigate our services. The information collected is aggregated and contains no personally identifiable information. If you block these cookies, then we will not know when you have used our services.</p>",
)

panels = [
MultiFieldPanel(
[
FieldPanel("cookieconsent_active"),
FieldPanel("cookieconsent_title"),
FieldPanel("cookieconsent_message"),
FieldPanel("cookieconsent_essential_about"),
FieldPanel("cookieconsent_analytics"),
FieldPanel("cookieconsent_analytics_about"),
],
"cookie banner",
"Cookie banner",
),
]
118 changes: 89 additions & 29 deletions hypha/cookieconsent/static/js/cookieconsent.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,98 @@
(function () {
"use strict";

if (typeof Cookies !== "undefined") {
const cookieconsent = document.querySelector(".cookieconsent");

if (
typeof Cookies.get("cookieconsent") === "undefined" &&
cookieconsent
) {
cookieconsent.classList.add("js-cookieconsent-open");
// Used when an analytics cookie notice is enabled
const ACCEPT = "accept";
const DECLINE = "decline";
const ACK = "ack"; // Only for essential cookies

// Constant key used for localstorage
const COOKIECONSENT_KEY = "cookieconsent";

// Class constants
const CLASS_COOKIECONSENT = "cookieconsent";
const CLASS_LEARNMORE = "cookieconsent__learnmore";
const CLASS_COOKIEBRIEF = "cookieconsent__brief";
const CLASS_COOKIECONTENT = "cookieconsent__content";
const CLASS_JS_CONSENT_OPEN = "js-cookieconsent-open";
const CLASS_JS_LEARNMORE = "js-cookieconsent-show-learnmore";
const CLASS_JS_LEARNMORE_EXPAND = `${CLASS_JS_LEARNMORE}-expand`;

const cookieconsent = document.querySelector(`.${CLASS_COOKIECONSENT}`);
if (!cookieconsent) return;

const cookieButtons = cookieconsent.querySelectorAll(
"button[data-consent]"
);
const learnMoreToggles = cookieconsent.querySelectorAll(
".button--learn-more"
);

function getConsentValue() {
return localStorage.getItem(COOKIECONSENT_KEY);
}

function setConsentValue(value) {
if ([ACCEPT, DECLINE, ACK].includes(value)) {
localStorage.setItem(COOKIECONSENT_KEY, value);
} else {
// If for whatever reason the value is not in the predefined values, assume decline
localStorage.setItem(COOKIECONSENT_KEY, DECLINE);
}
}

const cookie_buttons = Array.prototype.slice.call(
document.querySelectorAll("button[data-consent]")
);
const sitedomain = window.location.hostname.split(".").slice(-2);
const cookiedomain = sitedomain.join(".");
let cookie_options = [];
cookie_options["domain"] = cookiedomain;
cookie_options["sameSite"] = "strict";
cookie_options["expires"] = 365;
if (window.location.protocol === "https:") {
cookie_options["secure"] = true;
function openConsentPrompt() {
cookieconsent.classList.add(CLASS_JS_CONSENT_OPEN);
}

function closeConsentPrompt() {
cookieconsent.classList.remove(CLASS_JS_CONSENT_OPEN);
}

// Expose consent prompt opening/closing globally (ie. to use in a footer)
window.openConsentPrompt = openConsentPrompt;
window.closeConsentPrompt = closeConsentPrompt;

function toggleLearnMore(open) {
const content = cookieconsent.querySelector(`.${CLASS_COOKIECONTENT}`);
if (open) {
content.classList.add(CLASS_JS_LEARNMORE);
cookieconsent.classList.add(CLASS_JS_LEARNMORE_EXPAND);
} else {
content.classList.remove(CLASS_JS_LEARNMORE);
cookieconsent.classList.remove(CLASS_JS_LEARNMORE_EXPAND);
}
setInputTabIndex(`.${CLASS_LEARNMORE}`, open ? 0 : -1);
setInputTabIndex(`.${CLASS_COOKIEBRIEF}`, open ? -1 : 0);
}

cookie_buttons.forEach(function (button) {
button.addEventListener("click", function () {
if (button.getAttribute("data-consent") == "true") {
Cookies.set("cookieconsent", "accept", cookie_options);
} else {
Cookies.set("cookieconsent", "decline", cookie_options);
}
cookieconsent.classList.remove("js-cookieconsent-open");
});
});
// Adds "tabability" to menu buttons/toggles
function setInputTabIndex(wrapperClassSelector, tabValue) {
const wrapper = cookieconsent.querySelector(wrapperClassSelector);
const tabables = wrapper.querySelectorAll("button, input");
tabables.forEach((element) => (element.tabIndex = tabValue));
}

// Open the prompt if consent value is undefined OR if analytics has been added since the user ack'd essential cookies
if (
getConsentValue() == undefined ||
(getConsentValue() === ACK && cookieButtons.length > 1)
) {
openConsentPrompt();
}

cookieButtons.forEach(function (button) {
button.addEventListener("click", function () {
const buttonValue = button.getAttribute("data-consent");
setConsentValue(buttonValue);
closeConsentPrompt();
});
});

learnMoreToggles.forEach(function (button) {
button.addEventListener("click", function () {
const buttonValue = button.getAttribute("show-learn-more");
toggleLearnMore(buttonValue === "true");
});
});
})();
52 changes: 45 additions & 7 deletions hypha/cookieconsent/templates/includes/banner.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,52 @@

{% if show_banner %}
<div class="wrapper wrapper--cookieconsent cookieconsent">
<div class="wrapper wrapper--small">
<h2 class="cookieconsent__title">{% trans title %}</h2>
<div class="cookieconsent__message rich-text">
{{ message|richtext }}
<div class="cookieconsent__content">
<div class="cookieconsent__brief">
<div>
<h2 class="cookieconsent__title">{% trans settings.cookieconsent_title %}</h2>
<div class="cookieconsent__message rich-text">
{{ settings.cookieconsent_message|richtext }}
</div>
<div class="cookieconsent__actions">
{% if settings.cookieconsent_analytics %}
<button class="button button--cookieconsent button--decline" title="{% trans 'Decline tracking cookies.' %}" type="button" data-consent="decline">{% trans 'Essential only' %}</button>
<button class="button button--cookieconsent button--accept lg:ms-[20px]" title="{% trans 'Accept tracking cookies.' %}" type="button" data-consent="accept">{% trans 'Accept all' %}</button>
{% else %}
<button class="button button--cookieconsent button--accept" title="{% trans 'Acknowledge use of essential cookies.' %}" data-consent="ack">{% trans 'Ok' %}</button>
{% endif %}
<button class="button button--cookieconsent button--learn-more lg:ms-[20px]" title="{% trans 'Learn more about specific cookies used.' %}" show-learn-more="true">{% trans 'Learn More' %}</button>
</div>
</div>
</div>
<div class="cookieconsent__actions">
<button class="button button--cookieconsent button--decline" title="{% trans 'Decline tracking cookies.' %}" type="button" data-consent="false">{% trans 'Decline' %}</button>
<button class="button button--cookieconsent button--accept button--left-space" title="{% trans 'Accept tracking cookies.' %}" type="button" data-consent="true">{% trans 'Accept' %}</button>
<div class="cookieconsent__learnmore">
<div>
<div class="cookieconsent__statement">
<p>{% trans "This Website uses cookies. Below you can select the categories of cookies to allow when you use our website and services." %}</p>
</div>
<div class="cookieconsent__info-wrapper">
<div class="cookieconsent__info">
<p class="font-semibold">{% trans "Essential Cookies" %}</p>
{{ settings.cookieconsent_essential_about|richtext }}
<span class="font-bold">{% trans "Always enabled." %}</span>
</div>
{% if settings.cookieconsent_analytics %}
<div class="cookieconsent__info lg:pl-10">
<p class="font-semibold">{% trans "Analytics Cookies" %}</p>
{{ settings.cookieconsent_analytics_about|richtext }}
</div>
{% endif %}
</div>
<div class="cookieconsent__actions">
{% if settings.cookieconsent_analytics %}
<button class="button button--cookieconsent button--decline" title="{% trans 'Decline tracking cookies.' %}" type="button" data-consent="decline">{% trans 'Essential only' %}</button>
<button class="button button--cookieconsent button--accept lg:ms-[20px]" title="{% trans 'Accept tracking cookies.' %}" type="button" data-consent="accept">{% trans 'Accept all' %}</button>
{% else %}
<button class="button button--cookieconsent button--accept" title="{% trans 'Acknowledge use of essential cookies.' %}" data-consent="ack">{% trans 'Ok' %}</button>
{% endif %}
<button class="button button--cookieconsent button--learn-more lg:ms-[20px]" title="{% trans 'Return to main cookie menu.' %}" show-learn-more="false" tabindex="-1">{% trans 'Back' %}</button>
</div>
</div>
</div>
</div>
</div>
Expand Down
6 changes: 1 addition & 5 deletions hypha/cookieconsent/templatetags/cookieconsent_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,4 @@ def cookie_banner(context):
"cookieconsent"
)

return {
"show_banner": show_banner,
"title": settings.cookieconsent_title,
"message": settings.cookieconsent_message,
}
return {"show_banner": show_banner, "settings": settings}
22 changes: 22 additions & 0 deletions hypha/core/migrations/0005_alter_systemsettings_footer_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.11 on 2024-06-06 21:08

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0004_move_system_settings_data"),
]

operations = [
migrations.AlterField(
model_name="systemsettings",
name="footer_content",
field=models.TextField(
blank=True,
default='<p>Configure this text in Wagtail admin -> Settings -> System settings.</p>\n<br>\n<a href="#" onclick="openConsentPrompt()">Cookie Settings</a>',
help_text="This will be added to the footer, html tags is allowed.",
verbose_name="Footer content",
),
),
]
2 changes: 1 addition & 1 deletion hypha/core/models/system_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Meta:

footer_content = models.TextField(
"Footer content",
default="<p>Configure this text in Wagtail admin -> Settings -> System settings.</p>",
default='<p>Configure this text in Wagtail admin -> Settings -> System settings.</p>\n<br>\n<a href="#" onclick="openConsentPrompt()">Cookie Settings</a>',
help_text=_("This will be added to the footer, html tags is allowed."),
blank=True,
)
Expand Down
Loading

0 comments on commit 972396a

Please sign in to comment.