consently 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +279 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/consently.css +252 -0
- data/app/controllers/consently/application_controller.rb +4 -0
- data/app/controllers/consently/consents_controller.rb +44 -0
- data/app/helpers/consently/application_helper.rb +4 -0
- data/app/helpers/consently/tags_helper.rb +176 -0
- data/app/javascript/consently/banner_controller.js +190 -0
- data/app/models/consently/application_record.rb +5 -0
- data/app/models/consently/consent_record.rb +18 -0
- data/app/views/consently/_banner.html.erb +93 -0
- data/app/views/consently/_policy.html.erb +85 -0
- data/config/importmap.rb +3 -0
- data/config/locales/cs.yml +33 -0
- data/config/locales/de.yml +33 -0
- data/config/locales/en.yml +33 -0
- data/config/locales/es.yml +33 -0
- data/config/locales/fr.yml +33 -0
- data/config/locales/hu.yml +33 -0
- data/config/locales/it.yml +33 -0
- data/config/locales/nl.yml +33 -0
- data/config/locales/pl.yml +33 -0
- data/config/locales/sk.yml +33 -0
- data/config/routes.rb +5 -0
- data/lib/consently/configuration.rb +150 -0
- data/lib/consently/consent.rb +62 -0
- data/lib/consently/cookie.rb +12 -0
- data/lib/consently/engine.rb +33 -0
- data/lib/consently/providers/base.rb +95 -0
- data/lib/consently/providers/clarity.rb +34 -0
- data/lib/consently/providers/custom.rb +34 -0
- data/lib/consently/providers/google_ads.rb +32 -0
- data/lib/consently/providers/google_analytics.rb +45 -0
- data/lib/consently/providers/google_tag_manager.rb +34 -0
- data/lib/consently/providers/hotjar.rb +39 -0
- data/lib/consently/providers/meta_pixel.rb +37 -0
- data/lib/consently/providers/plausible.rb +30 -0
- data/lib/consently/providers.rb +9 -0
- data/lib/consently/script.rb +17 -0
- data/lib/consently/version.rb +3 -0
- data/lib/consently.rb +74 -0
- data/lib/generators/consently/consent_log/consent_log_generator.rb +28 -0
- data/lib/generators/consently/consent_log/templates/create_consently_consent_records.rb +19 -0
- data/lib/generators/consently/install/install_generator.rb +44 -0
- data/lib/generators/consently/install/templates/consently.rb +63 -0
- data/lib/generators/consently/views/views_generator.rb +15 -0
- data/lib/tasks/consently_tasks.rake +4 -0
- metadata +121 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# The three helpers a host application calls: the tags in <head>, the
|
|
3
|
+
# noscript fallbacks right after <body>, and the banner anywhere on the page.
|
|
4
|
+
module TagsHelper
|
|
5
|
+
# Every tag configured for this request. Tags whose category the visitor
|
|
6
|
+
# has not agreed to are rendered inert (type="text/plain") and the banner
|
|
7
|
+
# turns them into real scripts the moment consent is given - so a visitor
|
|
8
|
+
# who accepts does not have to reload to be counted.
|
|
9
|
+
def consently_tags
|
|
10
|
+
return "".html_safe unless consently_enabled?
|
|
11
|
+
|
|
12
|
+
parts = []
|
|
13
|
+
parts << consently_stylesheet_tag if Consently.config.stylesheet
|
|
14
|
+
parts << consently_consent_mode_tag if Consently.config.google_consent_mode
|
|
15
|
+
Consently.tags_for(request).each do |provider|
|
|
16
|
+
granted = consently_consent.granted?(provider.category)
|
|
17
|
+
provider.scripts.each { |script| parts << consently_script_tag(script, provider, granted) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
safe_join(parts, "\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Goes directly after <body> - Google Tag Manager and Meta both still ship
|
|
24
|
+
# a noscript fallback. Only rendered for categories already granted: there
|
|
25
|
+
# is no way to hold an iframe back and release it later.
|
|
26
|
+
def consently_noscript_tags
|
|
27
|
+
return "".html_safe unless consently_enabled?
|
|
28
|
+
|
|
29
|
+
fallbacks = Consently.tags_for(request).filter_map do |provider|
|
|
30
|
+
next unless consently_consent.granted?(provider.category)
|
|
31
|
+
|
|
32
|
+
provider.noscript&.html_safe
|
|
33
|
+
end
|
|
34
|
+
return "".html_safe if fallbacks.empty?
|
|
35
|
+
|
|
36
|
+
content_tag(:noscript, safe_join(fallbacks, "\n"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The banner, the preferences panel, and the JavaScript that releases the
|
|
40
|
+
# blocked tags. Render it once per page, ideally at the end of the body.
|
|
41
|
+
#
|
|
42
|
+
# The container stays in the DOM after a choice is made so that
|
|
43
|
+
# `consently_preferences_link` has something to reopen.
|
|
44
|
+
def consently_banner(policy_url: nil)
|
|
45
|
+
return "".html_safe unless consently_enabled? && Consently.consent_required?(request)
|
|
46
|
+
|
|
47
|
+
render "consently/banner",
|
|
48
|
+
consent: consently_consent,
|
|
49
|
+
policy_url: policy_url || consently_policy_url,
|
|
50
|
+
categories: Consently.config.optional_categories
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A "Cookie settings" link for the footer. Reopens the panel.
|
|
54
|
+
#
|
|
55
|
+
# Marked rather than wired: the link lives outside the banner element, so
|
|
56
|
+
# a data-action on it would never bind. The controller watches the whole
|
|
57
|
+
# document for a click on anything carrying this attribute, which also
|
|
58
|
+
# means your own markup can reopen the panel just by wearing it.
|
|
59
|
+
def consently_preferences_link(name = nil, **options, &block)
|
|
60
|
+
name ||= t("consently.preferences_link")
|
|
61
|
+
options[:data] = { consently_open: true }.merge(options[:data] || {})
|
|
62
|
+
|
|
63
|
+
link_to(name, "#consently", options, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def consently_consent
|
|
67
|
+
@consently_consent ||= if Consently.consent_required?(request)
|
|
68
|
+
Consent.from_cookie(cookies[Consently.config.cookie_name], version: Consently.config.consent_version)
|
|
69
|
+
else
|
|
70
|
+
# Nobody to ask, so nothing is held back.
|
|
71
|
+
Consent.new(categories: Consently.config.categories, version: Consently.config.consent_version)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Push an event onto the dataLayer from a view, respecting consent: with
|
|
76
|
+
# no analytics consent the event is simply not emitted.
|
|
77
|
+
#
|
|
78
|
+
# <%= consently_data_layer_push("purchase", value: 120, currency: "EUR") %>
|
|
79
|
+
def consently_data_layer_push(event, category: :analytics, **payload)
|
|
80
|
+
return "".html_safe unless consently_enabled? && consently_consent.granted?(category)
|
|
81
|
+
|
|
82
|
+
payload = payload.merge(event: event)
|
|
83
|
+
consently_inline_script "window.dataLayer = window.dataLayer || []; window.dataLayer.push(#{payload.to_json});"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A complete cookie policy for the tags this request would load: every
|
|
87
|
+
# category, every vendor, every cookie it sets and for how long, plus
|
|
88
|
+
# whether the visitor has agreed to it right now.
|
|
89
|
+
#
|
|
90
|
+
# Drop it into your own policy page under your own heading and legal text.
|
|
91
|
+
def consently_policy
|
|
92
|
+
render "consently/policy", tags: Consently.tags_for(request), consent: consently_consent
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The banner brings its own plain CSS - no framework, no build step. The
|
|
96
|
+
# look is driven by custom properties, so overriding a few variables is
|
|
97
|
+
# usually enough; `rails g consently:views` is there for the rest.
|
|
98
|
+
def consently_stylesheet_tag
|
|
99
|
+
stylesheet_link_tag "consently", media: "all"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Where the banner POSTs the decision, when consent logging is on and the
|
|
103
|
+
# engine is mounted. Nil otherwise, and the banner skips the request.
|
|
104
|
+
def consently_log_url
|
|
105
|
+
return nil unless Consently.config.log_consents
|
|
106
|
+
|
|
107
|
+
consently.consents_path
|
|
108
|
+
rescue NoMethodError, NameError
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def consently_enabled?
|
|
115
|
+
Consently.enabled?(request)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Google's consent mode v2 defaults. This one is never blocked: its whole
|
|
119
|
+
# job is to tell Google's tags that they may not store anything yet, and
|
|
120
|
+
# it has to be on the page before them.
|
|
121
|
+
def consently_consent_mode_tag
|
|
122
|
+
analytics = consently_consent.granted?(:analytics) ? "granted" : "denied"
|
|
123
|
+
marketing = consently_consent.granted?(:marketing) ? "granted" : "denied"
|
|
124
|
+
|
|
125
|
+
consently_inline_script <<~JS
|
|
126
|
+
window.dataLayer = window.dataLayer || [];
|
|
127
|
+
function gtag(){dataLayer.push(arguments);}
|
|
128
|
+
gtag('consent', 'default', {
|
|
129
|
+
'ad_storage': 'denied',
|
|
130
|
+
'ad_user_data': 'denied',
|
|
131
|
+
'ad_personalization': 'denied',
|
|
132
|
+
'analytics_storage': 'denied',
|
|
133
|
+
'functionality_storage': 'granted',
|
|
134
|
+
'security_storage': 'granted',
|
|
135
|
+
'wait_for_update': 500
|
|
136
|
+
});
|
|
137
|
+
#{"gtag('consent', 'update', { 'analytics_storage': '#{analytics}', 'ad_storage': '#{marketing}', 'ad_user_data': '#{marketing}', 'ad_personalization': '#{marketing}' });" if consently_consent.given?}
|
|
138
|
+
JS
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A plain <script> with the JS as written. javascript_tag would wrap it in
|
|
142
|
+
# a CDATA comment nobody has needed since XHTML.
|
|
143
|
+
def consently_inline_script(javascript)
|
|
144
|
+
attributes = {}
|
|
145
|
+
attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
|
|
146
|
+
|
|
147
|
+
content_tag(:script, javascript.html_safe, attributes)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def consently_policy_url
|
|
151
|
+
url = Consently.config.policy_url
|
|
152
|
+
url.respond_to?(:call) ? url.call(self) : url
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def consently_script_tag(script, provider, granted)
|
|
156
|
+
attributes = { data: { "consently-category" => provider.category }.merge(script.data_attributes) }
|
|
157
|
+
attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
|
|
158
|
+
|
|
159
|
+
if granted
|
|
160
|
+
attributes[:src] = script.src if script.external?
|
|
161
|
+
attributes[:async] = true if script.async
|
|
162
|
+
attributes[:defer] = true if script.defer
|
|
163
|
+
content_tag(:script, script.inline&.html_safe, attributes)
|
|
164
|
+
else
|
|
165
|
+
# Inert until the visitor agrees: browsers neither execute nor fetch a
|
|
166
|
+
# script of an unknown type, and the src lives in a data attribute so
|
|
167
|
+
# it is not even requested.
|
|
168
|
+
attributes[:type] = "text/plain"
|
|
169
|
+
attributes[:data]["consently-src"] = script.src if script.external?
|
|
170
|
+
attributes[:data]["consently-async"] = "true" if script.async
|
|
171
|
+
attributes[:data]["consently-defer"] = "true" if script.defer
|
|
172
|
+
content_tag(:script, script.inline&.html_safe, attributes)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// The banner, the preferences panel, and the part that actually matters:
|
|
4
|
+
// turning the blocked <script type="text/plain"> tags into live ones the
|
|
5
|
+
// moment the visitor agrees, so nobody has to reload to be counted.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["card", "preferences", "category", "settingsButton", "saveButton", "cancelButton"]
|
|
8
|
+
|
|
9
|
+
// The panel slides open on a grid row, and things disappear by class rather
|
|
10
|
+
// than by inline style. Both class names come from the markup, so restyling
|
|
11
|
+
// the banner never means touching this file.
|
|
12
|
+
static classes = ["open", "hidden"]
|
|
13
|
+
|
|
14
|
+
static values = {
|
|
15
|
+
cookie: { type: String, default: "consently" },
|
|
16
|
+
version: { type: String, default: "1" },
|
|
17
|
+
maxAge: { type: Number, default: 60 * 60 * 24 * 180 },
|
|
18
|
+
path: { type: String, default: "/" },
|
|
19
|
+
categories: Array,
|
|
20
|
+
googleConsentMode: { type: Boolean, default: true },
|
|
21
|
+
respectDoNotTrack: { type: Boolean, default: false },
|
|
22
|
+
respectGpc: { type: Boolean, default: true },
|
|
23
|
+
reload: { type: Boolean, default: false },
|
|
24
|
+
// Whether the visitor had already chosen when the page was rendered, so
|
|
25
|
+
// closing the panel knows whether to leave the banner behind or not.
|
|
26
|
+
decided: { type: Boolean, default: false },
|
|
27
|
+
logUrl: String,
|
|
28
|
+
nonce: String
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connect() {
|
|
32
|
+
// An opt-out signal is an answer, so the banner never appears for it.
|
|
33
|
+
if (this.#optedOut()) this.rejectAll()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
acceptAll() {
|
|
37
|
+
this.#store(this.categoriesValue)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
rejectAll() {
|
|
41
|
+
this.#store([])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Anything anywhere on the page carrying data-consently-open reopens the
|
|
45
|
+
// panel - a footer link is outside this controller's element, so it cannot
|
|
46
|
+
// carry an action of its own.
|
|
47
|
+
openFromLink(event) {
|
|
48
|
+
if (!event.target.closest("[data-consently-open]")) return
|
|
49
|
+
|
|
50
|
+
this.openPreferences(event)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
openPreferences(event) {
|
|
54
|
+
if (event) event.preventDefault()
|
|
55
|
+
|
|
56
|
+
this.cardTarget.classList.remove(...this.hiddenClasses)
|
|
57
|
+
this.preferencesTarget.classList.add(...this.openClasses)
|
|
58
|
+
this.settingsButtonTarget.classList.add(...this.hiddenClasses)
|
|
59
|
+
this.settingsButtonTarget.setAttribute("aria-expanded", "true")
|
|
60
|
+
this.saveButtonTarget.classList.remove(...this.hiddenClasses)
|
|
61
|
+
this.cancelButtonTarget.classList.remove(...this.hiddenClasses)
|
|
62
|
+
|
|
63
|
+
// Someone who reopened this from a link in the footer is now looking at a
|
|
64
|
+
// panel their screen reader has not been told about.
|
|
65
|
+
this.cardTarget.focus({ preventScroll: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Out of the panel without deciding anything: back to the short banner, or
|
|
69
|
+
// out of the way entirely if the visitor had already chosen before.
|
|
70
|
+
closePreferences() {
|
|
71
|
+
this.#collapse()
|
|
72
|
+
if (this.decidedValue) this.cardTarget.classList.add(...this.hiddenClasses)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
savePreferences() {
|
|
76
|
+
this.#store(this.categoryTargets.filter((box) => box.checked).map((box) => box.value))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Consent is written, the page catches up, and the choice is logged - in
|
|
80
|
+
// that order, so a failing log request cannot cost the visitor their click.
|
|
81
|
+
#store(categories) {
|
|
82
|
+
this.#writeCookie(categories)
|
|
83
|
+
this.#activateScripts(categories)
|
|
84
|
+
this.#updateGoogleConsent(categories)
|
|
85
|
+
this.#logConsent(categories)
|
|
86
|
+
|
|
87
|
+
this.#collapse()
|
|
88
|
+
this.cardTarget.classList.add(...this.hiddenClasses)
|
|
89
|
+
this.decidedValue = true
|
|
90
|
+
|
|
91
|
+
this.dispatch("change", { detail: { categories }, target: document, prefix: "consently" })
|
|
92
|
+
|
|
93
|
+
// Last, so anything listening for the event has already run.
|
|
94
|
+
if (this.reloadValue) window.location.reload()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#collapse() {
|
|
98
|
+
this.preferencesTarget.classList.remove(...this.openClasses)
|
|
99
|
+
this.settingsButtonTarget.classList.remove(...this.hiddenClasses)
|
|
100
|
+
this.settingsButtonTarget.setAttribute("aria-expanded", "false")
|
|
101
|
+
this.saveButtonTarget.classList.add(...this.hiddenClasses)
|
|
102
|
+
this.cancelButtonTarget.classList.add(...this.hiddenClasses)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#writeCookie(categories) {
|
|
106
|
+
const value = JSON.stringify({ v: this.versionValue, c: categories, t: new Date().toISOString() })
|
|
107
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : ""
|
|
108
|
+
|
|
109
|
+
document.cookie = `${this.cookieValue}=${encodeURIComponent(value)}; path=${this.pathValue}; max-age=${this.maxAgeValue}; SameSite=Lax${secure}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#activateScripts(categories) {
|
|
113
|
+
const granted = new Set([...categories, "necessary"])
|
|
114
|
+
|
|
115
|
+
document.querySelectorAll('script[type="text/plain"][data-consently-category]').forEach((blocked) => {
|
|
116
|
+
if (!granted.has(blocked.dataset.consentlyCategory)) return
|
|
117
|
+
|
|
118
|
+
const script = document.createElement("script")
|
|
119
|
+
// Copy everything the server put on the tag except our own bookkeeping,
|
|
120
|
+
// so vendor attributes like data-domain survive.
|
|
121
|
+
for (const { name, value } of blocked.attributes) {
|
|
122
|
+
if (name === "type" || name.startsWith("data-consently-")) continue
|
|
123
|
+
script.setAttribute(name, value)
|
|
124
|
+
}
|
|
125
|
+
script.dataset.consentlyCategory = blocked.dataset.consentlyCategory
|
|
126
|
+
|
|
127
|
+
if (this.nonceValue) script.setAttribute("nonce", this.nonceValue)
|
|
128
|
+
if (blocked.dataset.consentlySrc) {
|
|
129
|
+
script.src = blocked.dataset.consentlySrc
|
|
130
|
+
} else {
|
|
131
|
+
script.textContent = blocked.textContent
|
|
132
|
+
}
|
|
133
|
+
if (blocked.dataset.consentlyAsync) script.async = true
|
|
134
|
+
if (blocked.dataset.consentlyDefer) script.defer = true
|
|
135
|
+
|
|
136
|
+
blocked.replaceWith(script)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Google's consent mode v2: the page loaded with everything denied, this
|
|
141
|
+
// lifts exactly what was granted.
|
|
142
|
+
#updateGoogleConsent(categories) {
|
|
143
|
+
if (!this.googleConsentModeValue) return
|
|
144
|
+
|
|
145
|
+
const analytics = categories.includes("analytics") ? "granted" : "denied"
|
|
146
|
+
const marketing = categories.includes("marketing") ? "granted" : "denied"
|
|
147
|
+
|
|
148
|
+
window.dataLayer = window.dataLayer || []
|
|
149
|
+
// gtag pushes its `arguments` object, not an array - Google's tags read it
|
|
150
|
+
// by position and an array is not the same thing to them.
|
|
151
|
+
const gtag = function () { window.dataLayer.push(arguments) }
|
|
152
|
+
gtag("consent", "update", {
|
|
153
|
+
analytics_storage: analytics,
|
|
154
|
+
ad_storage: marketing,
|
|
155
|
+
ad_user_data: marketing,
|
|
156
|
+
ad_personalization: marketing
|
|
157
|
+
})
|
|
158
|
+
window.dataLayer.push({ event: "consently_consent_update", consently_categories: categories.join(",") })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async #logConsent(categories) {
|
|
162
|
+
if (!this.logUrlValue) return
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await fetch(this.logUrlValue, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
// The page may be reloading a moment later; keepalive means the
|
|
168
|
+
// request still reaches us instead of being cancelled.
|
|
169
|
+
keepalive: true,
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content ?? ""
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({ categories, version: this.versionValue })
|
|
175
|
+
})
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// A lost log entry is not worth breaking the page over.
|
|
178
|
+
console.warn("[consently] could not record consent", error)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Global Privacy Control is a binding opt-out signal in California and
|
|
183
|
+
// Colorado; Do Not Track is advisory, which is why that one is opt-in.
|
|
184
|
+
#optedOut() {
|
|
185
|
+
if (this.respectGpcValue && navigator.globalPrivacyControl === true) return true
|
|
186
|
+
|
|
187
|
+
return this.respectDoNotTrackValue &&
|
|
188
|
+
[navigator.doNotTrack, window.doNotTrack, navigator.msDoNotTrack].includes("1")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# Proof that a visitor agreed to something, and to what. Optional: create it
|
|
3
|
+
# with `rails g consently:consent_log` and switch config.log_consents on.
|
|
4
|
+
class ConsentRecord < ApplicationRecord
|
|
5
|
+
self.table_name = "consently_consent_records"
|
|
6
|
+
|
|
7
|
+
scope :for_version, ->(version) { where(consent_version: version.to_s) }
|
|
8
|
+
scope :in_scope, ->(scope) { where(scope: scope.to_s) }
|
|
9
|
+
|
|
10
|
+
def categories
|
|
11
|
+
self[:categories].to_s.split(",").map(&:to_sym)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def granted?(category)
|
|
15
|
+
category.to_sym == Consent::NECESSARY || categories.include?(category.to_sym)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<%# Plain CSS classes, no framework: see app/assets/stylesheets/consently.css.
|
|
2
|
+
The container stays in the DOM after a choice is made so that
|
|
3
|
+
`consently_preferences_link` has something to reopen. Run
|
|
4
|
+
`rails g consently:views` to take this file over into your own app. %>
|
|
5
|
+
<div id="consently"
|
|
6
|
+
class="consently"
|
|
7
|
+
data-turbo-permanent
|
|
8
|
+
data-controller="consently-banner"
|
|
9
|
+
data-action="click@document->consently-banner#openFromLink"
|
|
10
|
+
data-consently-banner-open-class="consently__panel--open"
|
|
11
|
+
data-consently-banner-hidden-class="consently-hidden"
|
|
12
|
+
data-consently-banner-cookie-value="<%= Consently.config.cookie_name %>"
|
|
13
|
+
data-consently-banner-version-value="<%= Consently.config.consent_version %>"
|
|
14
|
+
data-consently-banner-max-age-value="<%= Consently.config.cookie_max_age %>"
|
|
15
|
+
data-consently-banner-path-value="<%= Consently.config.cookie_path %>"
|
|
16
|
+
data-consently-banner-categories-value="<%= categories.map(&:to_s).to_json %>"
|
|
17
|
+
data-consently-banner-google-consent-mode-value="<%= Consently.config.google_consent_mode %>"
|
|
18
|
+
data-consently-banner-respect-do-not-track-value="<%= Consently.config.respect_do_not_track %>"
|
|
19
|
+
data-consently-banner-respect-gpc-value="<%= Consently.config.respect_global_privacy_control %>"
|
|
20
|
+
data-consently-banner-reload-value="<%= Consently.config.reload_after_choice %>"
|
|
21
|
+
data-consently-banner-decided-value="<%= consent.given? %>"
|
|
22
|
+
data-consently-banner-log-url-value="<%= consently_log_url %>"
|
|
23
|
+
data-consently-banner-nonce-value="<%= content_security_policy_nonce %>">
|
|
24
|
+
<%# A non-modal dialog: it must be announced and reachable, but it must not
|
|
25
|
+
trap anyone who came to read the page. tabindex lets the reopen link put
|
|
26
|
+
focus here without adding the card to the tab order. %>
|
|
27
|
+
<div class="consently__card <%= "consently-hidden" if consent.given? %>"
|
|
28
|
+
data-consently-banner-target="card"
|
|
29
|
+
role="dialog"
|
|
30
|
+
aria-modal="false"
|
|
31
|
+
aria-labelledby="consently-message"
|
|
32
|
+
tabindex="-1">
|
|
33
|
+
<p class="consently__text" id="consently-message">
|
|
34
|
+
<%= t("consently.banner.message") %>
|
|
35
|
+
<% if policy_url.present? %>
|
|
36
|
+
<%= link_to t("consently.banner.policy_link"), policy_url, class: "consently__link" %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<div class="consently__panel" id="consently-preferences" data-consently-banner-target="preferences">
|
|
41
|
+
<div class="consently__panel-inner">
|
|
42
|
+
<div class="consently__options">
|
|
43
|
+
<label class="consently__option">
|
|
44
|
+
<%# Always on, and shown that way rather than hidden: people look for it. %>
|
|
45
|
+
<%= tag.input type: "checkbox", checked: true, disabled: true %>
|
|
46
|
+
<span>
|
|
47
|
+
<span class="consently__option-name"><%= t("consently.categories.necessary.name") %></span>
|
|
48
|
+
<span class="consently__option-description"><%= t("consently.categories.necessary.description") %></span>
|
|
49
|
+
</span>
|
|
50
|
+
</label>
|
|
51
|
+
|
|
52
|
+
<% categories.each do |category| %>
|
|
53
|
+
<label class="consently__option">
|
|
54
|
+
<%= tag.input type: "checkbox", value: category, checked: consent.granted?(category),
|
|
55
|
+
id: "consently-#{category}", data: { consently_banner_target: "category" } %>
|
|
56
|
+
<span>
|
|
57
|
+
<span class="consently__option-name"><%= t("consently.categories.#{category}.name", default: category.to_s.humanize) %></span>
|
|
58
|
+
<span class="consently__option-description"><%= t("consently.categories.#{category}.description", default: "") %></span>
|
|
59
|
+
</span>
|
|
60
|
+
</label>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="consently__actions">
|
|
67
|
+
<button type="button" data-action="consently-banner#acceptAll" class="consently__button consently__button--primary">
|
|
68
|
+
<%= t("consently.banner.accept_all") %>
|
|
69
|
+
</button>
|
|
70
|
+
<button type="button" data-action="consently-banner#rejectAll" class="consently__button">
|
|
71
|
+
<%= t("consently.banner.reject_all") %>
|
|
72
|
+
</button>
|
|
73
|
+
<button type="button" data-action="consently-banner#openPreferences"
|
|
74
|
+
data-consently-banner-target="settingsButton"
|
|
75
|
+
aria-expanded="false"
|
|
76
|
+
aria-controls="consently-preferences"
|
|
77
|
+
class="consently__button consently__button--quiet">
|
|
78
|
+
<%= t("consently.banner.preferences") %>
|
|
79
|
+
</button>
|
|
80
|
+
<button type="button" data-action="consently-banner#savePreferences"
|
|
81
|
+
data-consently-banner-target="saveButton"
|
|
82
|
+
class="consently__button consently-hidden">
|
|
83
|
+
<%= t("consently.banner.save") %>
|
|
84
|
+
</button>
|
|
85
|
+
<%# Only offered while the panel is open - a way back out of it. %>
|
|
86
|
+
<button type="button" data-action="consently-banner#closePreferences"
|
|
87
|
+
data-consently-banner-target="cancelButton"
|
|
88
|
+
class="consently__button consently__button--quiet consently-hidden">
|
|
89
|
+
<%= t("consently.banner.cancel") %>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<%# A cookie policy that cannot go stale: it is rendered from the same
|
|
2
|
+
configuration the tags are, so a tag added to the initializer shows up
|
|
3
|
+
here, in the right category, with the cookies it sets.
|
|
4
|
+
|
|
5
|
+
Plain CSS, scoped under .consently-policy - see the gem's stylesheet.
|
|
6
|
+
`rails g consently:views` to take it over. %>
|
|
7
|
+
<div class="consently-policy">
|
|
8
|
+
<% Consently.config.categories.each do |category| %>
|
|
9
|
+
<% providers = tags.select { |provider| provider.category == category } %>
|
|
10
|
+
|
|
11
|
+
<section class="consently-policy__category">
|
|
12
|
+
<div class="consently-policy__heading">
|
|
13
|
+
<h2 class="consently-policy__title"><%= t("consently.categories.#{category}.name", default: category.to_s.humanize) %></h2>
|
|
14
|
+
<% if consent.granted?(category) %>
|
|
15
|
+
<span class="consently-policy__status consently-policy__status--granted"><%= t("consently.policy.granted") %></span>
|
|
16
|
+
<% else %>
|
|
17
|
+
<span class="consently-policy__status"><%= t("consently.policy.denied") %></span>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
<p class="consently-policy__description"><%= t("consently.categories.#{category}.description", default: "") %></p>
|
|
21
|
+
|
|
22
|
+
<% if providers.empty? %>
|
|
23
|
+
<p class="consently-policy__empty"><%= t("consently.policy.no_tags") %></p>
|
|
24
|
+
<% else %>
|
|
25
|
+
<% providers.each do |provider| %>
|
|
26
|
+
<div class="consently-policy__vendor">
|
|
27
|
+
<p class="consently-policy__vendor-name">
|
|
28
|
+
<%= provider.key.to_s.titleize %>
|
|
29
|
+
<span class="consently-policy__vendor-id"><%= provider.options[:id] || provider.options[:domain] %></span>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<% if provider.cookies.empty? %>
|
|
33
|
+
<p class="consently-policy__empty"><%= t("consently.policy.no_cookies") %></p>
|
|
34
|
+
<% else %>
|
|
35
|
+
<table class="consently-policy__table">
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th><%= t("consently.policy.cookie_name") %></th>
|
|
39
|
+
<th><%= t("consently.policy.cookie_duration") %></th>
|
|
40
|
+
</tr>
|
|
41
|
+
</thead>
|
|
42
|
+
<tbody>
|
|
43
|
+
<% provider.cookies.each do |cookie| %>
|
|
44
|
+
<tr>
|
|
45
|
+
<td><code><%= cookie.name %></code></td>
|
|
46
|
+
<td>
|
|
47
|
+
<%= cookie.session? ? t("consently.policy.duration_session") : t("consently.policy.duration_days", count: cookie.days) %>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</section>
|
|
58
|
+
<% end %>
|
|
59
|
+
|
|
60
|
+
<section class="consently-policy__category">
|
|
61
|
+
<h2 class="consently-policy__title"><%= t("consently.policy.own_heading") %></h2>
|
|
62
|
+
<p class="consently-policy__description"><%= t("consently.policy.own_body") %></p>
|
|
63
|
+
|
|
64
|
+
<table class="consently-policy__table">
|
|
65
|
+
<thead>
|
|
66
|
+
<tr>
|
|
67
|
+
<th><%= t("consently.policy.cookie_name") %></th>
|
|
68
|
+
<th><%= t("consently.policy.cookie_duration") %></th>
|
|
69
|
+
<th><%= t("consently.policy.own_version") %></th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
<tr>
|
|
74
|
+
<td><code><%= Consently.config.cookie_name %></code></td>
|
|
75
|
+
<td><%= t("consently.policy.duration_days", count: Consently.config.cookie_max_age / 86_400) %></td>
|
|
76
|
+
<td><%= Consently.config.consent_version %></td>
|
|
77
|
+
</tr>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<p class="consently-policy__footer">
|
|
83
|
+
<%= consently_preferences_link %>
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
cs:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Používáme soubory cookie, aby tento web fungoval, a s vaším souhlasem také k tomu, abychom rozuměli, jak jej používáte, a mohli vám ukazovat relevantní nabídky."
|
|
5
|
+
policy_link: "Přečtěte si naše zásady cookies."
|
|
6
|
+
accept_all: "Přijmout vše"
|
|
7
|
+
reject_all: "Odmítnout vše"
|
|
8
|
+
preferences: "Nastavení"
|
|
9
|
+
save: "Uložit volbu"
|
|
10
|
+
cancel: "Zrušit"
|
|
11
|
+
preferences_link: "Nastavení cookies"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "souhlas udělen"
|
|
14
|
+
denied: "bez souhlasu"
|
|
15
|
+
no_tags: "V této kategorii nic není."
|
|
16
|
+
no_cookies: "Nenastavuje žádné cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Doba uložení"
|
|
19
|
+
duration_days: "%{count} dní"
|
|
20
|
+
duration_session: "do zavření prohlížeče"
|
|
21
|
+
own_heading: "Samotná cookie se souhlasem"
|
|
22
|
+
own_body: "Vaše volba je uložená v jedné cookie ve vašem prohlížeči: kategorie, se kterými jste souhlasili, verze těchto zásad a čas. Nic o vás."
|
|
23
|
+
own_version: "Verze zásad"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Nezbytné"
|
|
27
|
+
description: "Nutné pro fungování webu. Nelze je vypnout."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Analytické"
|
|
30
|
+
description: "Ukazují nám, jak web používáte, abychom jej mohli zlepšovat."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketingové"
|
|
33
|
+
description: "Umožňují nám ukazovat vám nabídky, které odpovídají vašim zájmům."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
de:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Wir verwenden Cookies, damit diese Website funktioniert - und mit Ihrer Einwilligung, um zu verstehen, wie sie genutzt wird, und Ihnen passende Angebote zu zeigen."
|
|
5
|
+
policy_link: "Zur Cookie-Richtlinie."
|
|
6
|
+
accept_all: "Alle akzeptieren"
|
|
7
|
+
reject_all: "Alle ablehnen"
|
|
8
|
+
preferences: "Einstellungen"
|
|
9
|
+
save: "Auswahl speichern"
|
|
10
|
+
cancel: "Abbrechen"
|
|
11
|
+
preferences_link: "Cookie-Einstellungen"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "eingewilligt"
|
|
14
|
+
denied: "nicht eingewilligt"
|
|
15
|
+
no_tags: "Nichts in dieser Kategorie."
|
|
16
|
+
no_cookies: "Setzt keine Cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Speicherdauer"
|
|
19
|
+
duration_days: "%{count} Tage"
|
|
20
|
+
duration_session: "bis zum Schließen des Browsers"
|
|
21
|
+
own_heading: "Das Einwilligungs-Cookie selbst"
|
|
22
|
+
own_body: "Ihre Wahl liegt in einem Cookie in Ihrem Browser: die Kategorien, denen Sie zugestimmt haben, die Version dieser Richtlinie und die Uhrzeit. Nichts über Sie."
|
|
23
|
+
own_version: "Version der Richtlinie"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Notwendig"
|
|
27
|
+
description: "Für den Betrieb der Website erforderlich. Sie lassen sich nicht abschalten."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Statistik"
|
|
30
|
+
description: "Zeigen uns, wie die Website genutzt wird, damit wir sie verbessern können."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Erlauben uns, Ihnen Angebote zu zeigen, die zu Ihren Interessen passen."
|