consentful 0.1.0.pre1
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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/consentful/application.css +15 -0
- data/app/assets/stylesheets/consentful/consent.scss +518 -0
- data/app/components/consentful/banner.html.erb +53 -0
- data/app/components/consentful/banner.rb +27 -0
- data/app/components/consentful/modal.html.erb +19 -0
- data/app/components/consentful/modal.rb +21 -0
- data/app/components/consentful/preferences.html.erb +167 -0
- data/app/components/consentful/preferences.rb +48 -0
- data/app/components/consentful/service_detail.html.erb +99 -0
- data/app/components/consentful/service_detail.rb +18 -0
- data/app/components/consentful/toggle.html.erb +30 -0
- data/app/components/consentful/toggle.rb +30 -0
- data/app/controllers/concerns/consentful/visitor_cookie.rb +52 -0
- data/app/controllers/consentful/admin/base_controller.rb +27 -0
- data/app/controllers/consentful/admin/categories_controller.rb +46 -0
- data/app/controllers/consentful/admin/copy_versions_controller.rb +113 -0
- data/app/controllers/consentful/admin/dashboard_controller.rb +11 -0
- data/app/controllers/consentful/admin/records_controller.rb +11 -0
- data/app/controllers/consentful/admin/services_controller.rb +80 -0
- data/app/controllers/consentful/application_controller.rb +9 -0
- data/app/controllers/consentful/consent_controller.rb +132 -0
- data/app/helpers/consentful/application_helper.rb +4 -0
- data/app/helpers/consentful/consent_helper.rb +23 -0
- data/app/javascript/consentful/controllers/banner_controller.js +92 -0
- data/app/javascript/consentful/controllers/clipboard_copy_controller.js +30 -0
- data/app/javascript/consentful/controllers/modal_controller.js +50 -0
- data/app/javascript/consentful/controllers/modal_trigger_controller.js +9 -0
- data/app/javascript/consentful/controllers/preferences_controller.js +182 -0
- data/app/javascript/consentful/index.js +42 -0
- data/app/javascript/consentful/state.js +91 -0
- data/app/jobs/consentful/application_job.rb +4 -0
- data/app/mailers/consentful/application_mailer.rb +6 -0
- data/app/models/consentful/application_record.rb +8 -0
- data/app/models/consentful/consent_category.rb +21 -0
- data/app/models/consentful/consent_copy_version.rb +49 -0
- data/app/models/consentful/consent_record.rb +33 -0
- data/app/models/consentful/consent_service.rb +38 -0
- data/app/models/consentful/consent_state.rb +80 -0
- data/app/models/consentful/consent_visitor_link.rb +26 -0
- data/app/presenters/consentful/consent_record_presenter.rb +73 -0
- data/app/services/consentful/dashboard_report.rb +45 -0
- data/app/services/consentful/sar_lookup.rb +47 -0
- data/app/views/consentful/admin/categories/edit.html.erb +22 -0
- data/app/views/consentful/admin/categories/index.html.erb +17 -0
- data/app/views/consentful/admin/copy_versions/_form.html.erb +34 -0
- data/app/views/consentful/admin/copy_versions/edit.html.erb +2 -0
- data/app/views/consentful/admin/copy_versions/index.html.erb +34 -0
- data/app/views/consentful/admin/copy_versions/new.html.erb +2 -0
- data/app/views/consentful/admin/copy_versions/show.html.erb +24 -0
- data/app/views/consentful/admin/dashboard/show.html.erb +35 -0
- data/app/views/consentful/admin/records/index.html.erb +35 -0
- data/app/views/consentful/admin/services/_form.html.erb +42 -0
- data/app/views/consentful/admin/services/edit.html.erb +2 -0
- data/app/views/consentful/admin/services/index.html.erb +27 -0
- data/app/views/consentful/admin/services/new.html.erb +2 -0
- data/app/views/consentful/consent/preferences.html.erb +14 -0
- data/app/views/layouts/consentful/admin.html.erb +66 -0
- data/app/views/layouts/consentful/application.html.erb +17 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20260522100000_create_consent_categories.rb +19 -0
- data/db/migrate/20260522100100_create_consent_services.rb +38 -0
- data/db/migrate/20260522100200_create_consent_copy_versions.rb +30 -0
- data/db/migrate/20260522100300_create_consent_records.rb +22 -0
- data/db/migrate/20260522100400_create_consent_visitor_links.rb +21 -0
- data/lib/consentful/configuration.rb +100 -0
- data/lib/consentful/engine.rb +5 -0
- data/lib/consentful/seed_starter.rb +57 -0
- data/lib/consentful/version.rb +3 -0
- data/lib/consentful.rb +27 -0
- data/lib/generators/consentful/install_generator.rb +29 -0
- data/lib/generators/consentful/templates/consentful.rb +35 -0
- data/lib/tasks/consentful_tasks.rake +9 -0
- metadata +149 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<div
|
|
2
|
+
data-controller="consentful--preferences"
|
|
3
|
+
data-consentful--preferences-expected-version-id-value="<%= version.id %>"
|
|
4
|
+
data-consentful--preferences-submit-path-value="<%= submit_path %>"
|
|
5
|
+
data-consentful--preferences-categories-value="<%= categories_payload.to_json %>"
|
|
6
|
+
class="c-consent-preferences <%= modal? ? "c-consent-preferences--modal" : "c-consent-preferences--page" %>"
|
|
7
|
+
>
|
|
8
|
+
<% if modal? %>
|
|
9
|
+
<div class="c-consent-preferences__header">
|
|
10
|
+
<h2 class="c-consent-preferences__header-title"><%= version.banner_title %></h2>
|
|
11
|
+
<button type="button" data-action="consentful--preferences#close" aria-label="Close" class="c-consent-preferences__close">
|
|
12
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
|
13
|
+
<path d="M5 5l10 10M15 5L5 15" stroke-linecap="round"/>
|
|
14
|
+
</svg>
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="c-consent-preferences__body">
|
|
20
|
+
<div class="c-consent-preferences__intro">
|
|
21
|
+
<% unless modal? %>
|
|
22
|
+
<h1 class="c-consent-preferences__page-title"><%= version.banner_title %></h1>
|
|
23
|
+
<% end %>
|
|
24
|
+
<p class="c-consent-preferences__intro-body"><%= version.modal_intro.presence || version.banner_body %></p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="c-consent-preferences__tabs" role="tablist">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
role="tab"
|
|
31
|
+
data-action="consentful--preferences#switchTab"
|
|
32
|
+
data-consentful--preferences-target="tab"
|
|
33
|
+
data-tab="categories"
|
|
34
|
+
aria-selected="true"
|
|
35
|
+
class="c-consent-preferences__tab c-consent-preferences__tab--active"
|
|
36
|
+
>Categories</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
role="tab"
|
|
40
|
+
data-action="consentful--preferences#switchTab"
|
|
41
|
+
data-consentful--preferences-target="tab"
|
|
42
|
+
data-tab="services"
|
|
43
|
+
aria-selected="false"
|
|
44
|
+
class="c-consent-preferences__tab"
|
|
45
|
+
>Services</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div data-consentful--preferences-target="tabPanel" data-tab="categories" class="c-consent-preferences__panel">
|
|
49
|
+
<% categories.each do |category| %>
|
|
50
|
+
<section class="c-consent-category" data-category-key="<%= category.key %>">
|
|
51
|
+
<header class="c-consent-category__header">
|
|
52
|
+
<div class="c-consent-category__heading">
|
|
53
|
+
<h3 class="c-consent-category__title"><%= category.name %></h3>
|
|
54
|
+
<% if category_blurb(category).present? %>
|
|
55
|
+
<p class="c-consent-category__blurb"><%= category_blurb(category) %></p>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
<%= render Consentful::Toggle.new(
|
|
59
|
+
id_scope: variant,
|
|
60
|
+
name: "categories[#{category.key}]",
|
|
61
|
+
value: "true",
|
|
62
|
+
checked: category.required?,
|
|
63
|
+
disabled: category.required?,
|
|
64
|
+
data: {
|
|
65
|
+
"consentful--preferences-target": "categoryMaster",
|
|
66
|
+
"action": "change->consentful--preferences#cascadeCategory",
|
|
67
|
+
"category-key": category.key
|
|
68
|
+
}
|
|
69
|
+
) %>
|
|
70
|
+
<button type="button" data-action="consentful--preferences#toggleSection" data-section-target="category-<%= category.key %>" aria-expanded="false" class="c-consent-category__chevron">
|
|
71
|
+
<svg width="20" height="20" data-section-chevron="category-<%= category.key %>" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
|
72
|
+
<path d="M6 8l4 4 4-4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
</header>
|
|
76
|
+
<div data-section-body="category-<%= category.key %>" class="c-consent-category__body c-consent__hidden">
|
|
77
|
+
<% services.select { |service| service.consent_category_id == category.id }.each do |service| %>
|
|
78
|
+
<div class="c-consent-service__row">
|
|
79
|
+
<span class="c-consent-service__name"><%= service.name %></span>
|
|
80
|
+
<button type="button" class="c-consent-service__info" data-action="consentful--preferences#toggleSection" data-section-target="service-<%= service.slug %>" aria-label="Service details">
|
|
81
|
+
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
82
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm0-9.5a1 1 0 100-2 1 1 0 000 2zM9 10a1 1 0 011-1h.01a1 1 0 110 2H10v3a1 1 0 11-2 0v-4z" clip-rule="evenodd"/>
|
|
83
|
+
</svg>
|
|
84
|
+
</button>
|
|
85
|
+
<%= render Consentful::Toggle.new(
|
|
86
|
+
id_scope: "#{variant}-cat",
|
|
87
|
+
name: "services[#{service.slug}]",
|
|
88
|
+
value: "true",
|
|
89
|
+
checked: category.required?,
|
|
90
|
+
disabled: category.required?,
|
|
91
|
+
data: {
|
|
92
|
+
"consentful--preferences-target": "serviceToggle",
|
|
93
|
+
"action": "change->consentful--preferences#syncServiceToggle",
|
|
94
|
+
"category-key": category.key,
|
|
95
|
+
"service-slug": service.slug
|
|
96
|
+
}
|
|
97
|
+
) %>
|
|
98
|
+
</div>
|
|
99
|
+
<div data-section-body="service-<%= service.slug %>" class="c-consent-service__detail c-consent__hidden">
|
|
100
|
+
<%= render Consentful::ServiceDetail.new(service: service) %>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div data-consentful--preferences-target="tabPanel" data-tab="services" class="c-consent-preferences__panel c-consent__hidden">
|
|
109
|
+
<% services.each do |service| %>
|
|
110
|
+
<div class="c-consent-services-card">
|
|
111
|
+
<header class="c-consent-services-card__header">
|
|
112
|
+
<div class="c-consent-services-card__heading">
|
|
113
|
+
<p class="c-consent-services-card__name"><%= service.name %></p>
|
|
114
|
+
<p class="c-consent-services-card__category"><%= service.consent_category.name %></p>
|
|
115
|
+
</div>
|
|
116
|
+
<%= render Consentful::Toggle.new(
|
|
117
|
+
id_scope: "#{variant}-flat",
|
|
118
|
+
name: "services[#{service.slug}]",
|
|
119
|
+
value: "true",
|
|
120
|
+
checked: service.consent_category.required?,
|
|
121
|
+
disabled: service.consent_category.required?,
|
|
122
|
+
data: {
|
|
123
|
+
"consentful--preferences-target": "serviceToggle",
|
|
124
|
+
"action": "change->consentful--preferences#syncServiceToggle",
|
|
125
|
+
"category-key": service.consent_category.key,
|
|
126
|
+
"service-slug": service.slug
|
|
127
|
+
}
|
|
128
|
+
) %>
|
|
129
|
+
<button type="button" data-action="consentful--preferences#toggleSection" data-section-target="service-flat-<%= service.slug %>" aria-expanded="false" class="c-consent-category__chevron">
|
|
130
|
+
<svg width="20" height="20" data-section-chevron="service-flat-<%= service.slug %>" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
|
131
|
+
<path d="M6 8l4 4 4-4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
132
|
+
</svg>
|
|
133
|
+
</button>
|
|
134
|
+
</header>
|
|
135
|
+
<div data-section-body="service-flat-<%= service.slug %>" class="c-consent-service__detail c-consent__hidden">
|
|
136
|
+
<%= render Consentful::ServiceDetail.new(service: service) %>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<% end %>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<% if visitor_uuid.present? %>
|
|
144
|
+
<div class="c-consent-preferences__reference">
|
|
145
|
+
<% if sar_contact_email.present? %>
|
|
146
|
+
If you'd like to request your consent history, quote this reference and send it to
|
|
147
|
+
<a href="mailto:<%= sar_contact_email %>"><%= sar_contact_email %></a>:
|
|
148
|
+
<% else %>
|
|
149
|
+
If you'd like to request your consent history, quote this reference:
|
|
150
|
+
<% end %>
|
|
151
|
+
<code class="c-consent-preferences__reference-code" data-consentful--preferences-target="reference"><%= visitor_uuid %></code>
|
|
152
|
+
<button type="button" data-action="consentful--preferences#copyReference" class="c-consent-preferences__copy-button">Copy</button>
|
|
153
|
+
</div>
|
|
154
|
+
<% end %>
|
|
155
|
+
|
|
156
|
+
<div class="c-consent-preferences__footer">
|
|
157
|
+
<button type="button" data-action="consentful--preferences#save" class="c-consent-action">
|
|
158
|
+
<%= version.save_label.presence || "Save Settings" %>
|
|
159
|
+
</button>
|
|
160
|
+
<button type="button" data-action="consentful--preferences#reject" class="c-consent-action">
|
|
161
|
+
<%= version.reject_label.presence || "Deny" %>
|
|
162
|
+
</button>
|
|
163
|
+
<button type="button" data-action="consentful--preferences#accept" class="c-consent-action">
|
|
164
|
+
<%= version.accept_label.presence || "Accept All" %>
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
class Preferences < ViewComponent::Base
|
|
5
|
+
VARIANTS = %i[page modal].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(version:, categories:, visitor_uuid:, submit_path:, variant: :page)
|
|
8
|
+
super()
|
|
9
|
+
@version = version
|
|
10
|
+
@categories = categories
|
|
11
|
+
@visitor_uuid = visitor_uuid
|
|
12
|
+
@submit_path = submit_path
|
|
13
|
+
@variant = VARIANTS.include?(variant) ? variant : :page
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render?
|
|
17
|
+
@version.present?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def services
|
|
21
|
+
@services ||= ConsentService.active.includes(:consent_category).order(:position, :name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :version, :categories, :visitor_uuid, :submit_path, :variant
|
|
27
|
+
|
|
28
|
+
def modal?
|
|
29
|
+
variant == :modal
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sar_contact_email
|
|
33
|
+
Consentful.config.sar_contact_email
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def category_blurb(category)
|
|
37
|
+
raw = version.category_blurbs[category.key]
|
|
38
|
+
blurb = raw.is_a?(Hash) ? raw["description"] || raw[:description] : raw
|
|
39
|
+
blurb.presence || category.description.presence || ""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def categories_payload
|
|
43
|
+
categories.map do |category|
|
|
44
|
+
{ key: category.key, name: category.name, required: category.required? }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<% if service.description.present? %>
|
|
3
|
+
<div class="c-consent-service__detail-section">
|
|
4
|
+
<p class="c-consent-service__detail-label">Description of Service</p>
|
|
5
|
+
<p class="c-consent-service__detail-value"><%= service.description %></p>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<% if service.processing_company_name.present? || service.processing_company_address.present? %>
|
|
10
|
+
<div class="c-consent-service__detail-section">
|
|
11
|
+
<p class="c-consent-service__detail-label">Processing Company</p>
|
|
12
|
+
<p class="c-consent-service__detail-value"><%= service.processing_company_name %></p>
|
|
13
|
+
<% if service.processing_company_address.present? %>
|
|
14
|
+
<p class="c-consent-service__detail-value" style="color: #494949;"><%= service.processing_company_address %></p>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if service.dpo_email.present? %>
|
|
20
|
+
<div class="c-consent-service__detail-section">
|
|
21
|
+
<p class="c-consent-service__detail-label">Data Protection Officer of Processing Company</p>
|
|
22
|
+
<p class="c-consent-service__detail-value"><%= mail_to service.dpo_email, class: "c-consent-service__detail-link" %></p>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if chips_for(service.data_purposes).any? %>
|
|
27
|
+
<div class="c-consent-service__detail-section">
|
|
28
|
+
<p class="c-consent-service__detail-label">Data Purposes</p>
|
|
29
|
+
<div class="c-consent-service__chips">
|
|
30
|
+
<% chips_for(service.data_purposes).each do |purpose| %>
|
|
31
|
+
<span class="c-consent-service__chip"><%= purpose %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<% if chips_for(service.technologies_used).any? %>
|
|
38
|
+
<div class="c-consent-service__detail-section">
|
|
39
|
+
<p class="c-consent-service__detail-label">Technologies Used</p>
|
|
40
|
+
<div class="c-consent-service__chips">
|
|
41
|
+
<% chips_for(service.technologies_used).each do |tech| %>
|
|
42
|
+
<span class="c-consent-service__chip"><%= tech %></span>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<% if chips_for(service.cookie_names).any? %>
|
|
49
|
+
<div class="c-consent-service__detail-section">
|
|
50
|
+
<p class="c-consent-service__detail-label">Cookies</p>
|
|
51
|
+
<div class="c-consent-service__chips">
|
|
52
|
+
<% chips_for(service.cookie_names).each do |cookie| %>
|
|
53
|
+
<span class="c-consent-service__chip c-consent-service__chip--mono"><%= cookie %></span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<% if chips_for(service.data_collected).any? %>
|
|
60
|
+
<div class="c-consent-service__detail-section">
|
|
61
|
+
<p class="c-consent-service__detail-label">Data Collected</p>
|
|
62
|
+
<div class="c-consent-service__chips">
|
|
63
|
+
<% chips_for(service.data_collected).each do |data| %>
|
|
64
|
+
<span class="c-consent-service__chip"><%= data %></span>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<% if service.legal_basis.present? %>
|
|
71
|
+
<div class="c-consent-service__detail-section">
|
|
72
|
+
<p class="c-consent-service__detail-label">Legal Basis</p>
|
|
73
|
+
<p class="c-consent-service__detail-value"><%= service.legal_basis %></p>
|
|
74
|
+
</div>
|
|
75
|
+
<% end %>
|
|
76
|
+
|
|
77
|
+
<% if service.processing_location.present? %>
|
|
78
|
+
<div class="c-consent-service__detail-section">
|
|
79
|
+
<p class="c-consent-service__detail-label">Location of Processing</p>
|
|
80
|
+
<ul>
|
|
81
|
+
<li><%= service.processing_location %></li>
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if service.retention_period.present? %>
|
|
87
|
+
<div class="c-consent-service__detail-section">
|
|
88
|
+
<p class="c-consent-service__detail-label">Retention Period</p>
|
|
89
|
+
<p class="c-consent-service__detail-value"><%= service.retention_period %></p>
|
|
90
|
+
</div>
|
|
91
|
+
<% end %>
|
|
92
|
+
|
|
93
|
+
<% if service.third_country_transfer.present? %>
|
|
94
|
+
<div class="c-consent-service__detail-section">
|
|
95
|
+
<p class="c-consent-service__detail-label">Transfer to Third Countries</p>
|
|
96
|
+
<p class="c-consent-service__detail-value"><%= service.third_country_transfer %></p>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
class ServiceDetail < ViewComponent::Base
|
|
5
|
+
def initialize(service:)
|
|
6
|
+
super()
|
|
7
|
+
@service = service
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_reader :service
|
|
13
|
+
|
|
14
|
+
def chips_for(values)
|
|
15
|
+
Array(values).compact_blank
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<label for="<%= id %>" class="c-consent-toggle <%= 'c-consent-toggle--disabled' if disabled %>">
|
|
2
|
+
<% if label && label_before? %>
|
|
3
|
+
<span class="c-consent-toggle__label"><%= label %></span>
|
|
4
|
+
<% end %>
|
|
5
|
+
|
|
6
|
+
<span class="c-consent-toggle__pill-wrap">
|
|
7
|
+
<input
|
|
8
|
+
type="checkbox"
|
|
9
|
+
id="<%= id %>"
|
|
10
|
+
name="<%= name %>"
|
|
11
|
+
value="<%= value %>"
|
|
12
|
+
<%= "checked" if checked %>
|
|
13
|
+
<%= "disabled" if disabled %>
|
|
14
|
+
class="c-consent-toggle__input"
|
|
15
|
+
<% data.each do |key, val| %>data-<%= key.to_s.dasherize %>="<%= val %>" <% end %>
|
|
16
|
+
/>
|
|
17
|
+
<span class="c-consent-toggle__pill"></span>
|
|
18
|
+
<span class="c-consent-toggle__knob"></span>
|
|
19
|
+
<svg class="c-consent-toggle__icon c-consent-toggle__icon--off" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
20
|
+
<path d="M5 5l10 10M15 5L5 15" stroke-linecap="round"/>
|
|
21
|
+
</svg>
|
|
22
|
+
<svg class="c-consent-toggle__icon c-consent-toggle__icon--on" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
23
|
+
<path d="M4 10l4 4 8-8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
24
|
+
</svg>
|
|
25
|
+
</span>
|
|
26
|
+
|
|
27
|
+
<% if label && !label_before? %>
|
|
28
|
+
<span class="c-consent-toggle__label"><%= label %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</label>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
class Toggle < ViewComponent::Base
|
|
5
|
+
def initialize(name:, id: nil, id_scope: nil, value: "1", checked: false, disabled: false, label: nil, label_position: :right, data: {}) # rubocop:disable Metrics/ParameterLists
|
|
6
|
+
super()
|
|
7
|
+
@name = name
|
|
8
|
+
@id = id || dom_id_for(name, value, id_scope)
|
|
9
|
+
@value = value
|
|
10
|
+
@checked = checked
|
|
11
|
+
@disabled = disabled
|
|
12
|
+
@label = label
|
|
13
|
+
@label_position = label_position
|
|
14
|
+
@data = data
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :name, :id, :value, :checked, :disabled, :label, :label_position, :data
|
|
20
|
+
|
|
21
|
+
def dom_id_for(name, value, scope)
|
|
22
|
+
parts = [ scope, name, value ].compact.map { |part| part.to_s.parameterize(separator: "_") }
|
|
23
|
+
"toggle-#{parts.join('-')}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def label_before?
|
|
27
|
+
label_position.to_sym == :left
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
# Mix into the host's ApplicationController to give every request an anonymous
|
|
5
|
+
# visitor UUID cookie and the consent-linking helper:
|
|
6
|
+
#
|
|
7
|
+
# class ApplicationController < ActionController::Base
|
|
8
|
+
# include Consentful::VisitorCookie
|
|
9
|
+
# before_action :ensure_visitor_id
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# The engine's own controllers include it too.
|
|
13
|
+
module VisitorCookie
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
helper_method :current_consent, :consent_visitor_id
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def ensure_visitor_id
|
|
23
|
+
return if consent_visitor_id.present?
|
|
24
|
+
|
|
25
|
+
cookies[Consentful.config.visitor_cookie_name] = consent_cookie_options.merge(
|
|
26
|
+
value: SecureRandom.uuid,
|
|
27
|
+
expires: Consentful.config.cookie_expiry_months.months.from_now
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def consent_visitor_id
|
|
32
|
+
cookies[Consentful.config.visitor_cookie_name]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def current_consent
|
|
36
|
+
@current_consent ||= Consentful::ConsentState.from_cookie(cookies[Consentful.config.cookie_name])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def consent_cookie_options
|
|
40
|
+
Consentful.config.cookie_options(request)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Bridge the anonymous visitor cookie to a known identifier (e.g. an email a
|
|
44
|
+
# user submitted) so a later SAR can resolve every consent record for them.
|
|
45
|
+
# Safe to call repeatedly — the unique index de-dupes. No-ops on blanks.
|
|
46
|
+
def link_consent_identifier!(type:, value:)
|
|
47
|
+
Consentful.link!(consent_visitor_id, type:, value:)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Consentful.config.error_handler(e)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
module Admin
|
|
5
|
+
# Base for the self-contained admin panel. Auth is delegated to the host via
|
|
6
|
+
# Consentful.config.authorize (defaults to allow-all — hosts MUST set it).
|
|
7
|
+
class BaseController < ActionController::Base
|
|
8
|
+
protect_from_forgery with: :exception
|
|
9
|
+
layout "consentful/admin"
|
|
10
|
+
|
|
11
|
+
before_action :authorize_consentful!
|
|
12
|
+
|
|
13
|
+
helper_method :consentful_identity
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def authorize_consentful!
|
|
18
|
+
head :forbidden unless Consentful.config.authorize(self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Who is acting, per the host. Stamped onto published copy versions.
|
|
22
|
+
def consentful_identity
|
|
23
|
+
Consentful.config.identify(self)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
module Admin
|
|
5
|
+
class CategoriesController < BaseController
|
|
6
|
+
before_action :set_category, only: [ :edit, :update ]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@categories = ConsentCategory.all
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def edit; end
|
|
13
|
+
|
|
14
|
+
def update
|
|
15
|
+
if @category.update(category_params)
|
|
16
|
+
redirect_to admin_categories_path, notice: "Category updated."
|
|
17
|
+
else
|
|
18
|
+
render :edit, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reorder
|
|
23
|
+
reorder_entries.each { |entry| ConsentCategory.update(entry[:id], position: entry[:position]) }
|
|
24
|
+
head :no_content
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def set_category
|
|
30
|
+
@category = ConsentCategory.find(params[:id])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def category_params
|
|
34
|
+
attrs = params.require(:consent_category).permit(:name, :description, :gtag_keys)
|
|
35
|
+
attrs[:gtag_keys] = attrs[:gtag_keys].to_s.split(",").map(&:strip).compact_blank if attrs.key?(:gtag_keys)
|
|
36
|
+
attrs
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reorder_entries
|
|
40
|
+
raw = params.require(:consent_categories)
|
|
41
|
+
list = raw.respond_to?(:values) ? raw.values : raw
|
|
42
|
+
list.map { |entry| entry.permit(:id, :position) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consentful
|
|
4
|
+
module Admin
|
|
5
|
+
class CopyVersionsController < BaseController
|
|
6
|
+
before_action :set_version, only: [ :show, :edit, :update, :destroy, :publish, :archive, :restore ]
|
|
7
|
+
before_action :require_draft, only: [ :edit, :update, :destroy ]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@versions = ConsentCopyVersion.order(created_at: :desc)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show; end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@version = ConsentCopyVersion.new(category_blurbs: blank_category_blurbs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def edit; end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
@version = ConsentCopyVersion.new(version_params)
|
|
23
|
+
if @version.save
|
|
24
|
+
redirect_to edit_admin_copy_version_path(@version), notice: "Draft created."
|
|
25
|
+
else
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
if @version.update(version_params)
|
|
32
|
+
redirect_to edit_admin_copy_version_path(@version), notice: "Draft updated."
|
|
33
|
+
else
|
|
34
|
+
render :edit, status: :unprocessable_entity
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def destroy
|
|
39
|
+
@version.destroy
|
|
40
|
+
redirect_to admin_copy_versions_path, notice: "Draft deleted."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def publish
|
|
44
|
+
unless @version.draft?
|
|
45
|
+
redirect_to admin_copy_versions_path, alert: "Only draft versions can be published."
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@version.publish!(consentful_identity)
|
|
50
|
+
redirect_to admin_copy_versions_path, notice: "Published. Any previously published version has been archived."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def archive
|
|
54
|
+
unless @version.published?
|
|
55
|
+
redirect_to admin_copy_versions_path, alert: "Only published versions can be archived."
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@version.archive!
|
|
60
|
+
redirect_to admin_copy_versions_path, notice: "Archived."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def restore
|
|
64
|
+
if @version.draft?
|
|
65
|
+
redirect_to admin_copy_versions_path, alert: "Draft versions are already editable."
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
draft = ConsentCopyVersion.create!(
|
|
70
|
+
banner_title: @version.banner_title,
|
|
71
|
+
banner_body: @version.banner_body,
|
|
72
|
+
modal_intro: @version.modal_intro,
|
|
73
|
+
accept_label: @version.accept_label,
|
|
74
|
+
reject_label: @version.reject_label,
|
|
75
|
+
manage_label: @version.manage_label,
|
|
76
|
+
save_label: @version.save_label,
|
|
77
|
+
category_blurbs: @version.category_blurbs.deep_dup,
|
|
78
|
+
status: :draft
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
redirect_to edit_admin_copy_version_path(draft),
|
|
82
|
+
notice: "Cloned to a new draft. Edit and publish when you're ready."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def set_version
|
|
88
|
+
@version = ConsentCopyVersion.find(params[:id])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def require_draft
|
|
92
|
+
return if @version.draft?
|
|
93
|
+
|
|
94
|
+
redirect_to admin_copy_versions_path,
|
|
95
|
+
alert: "Published and archived versions are read-only. Create a new draft to make changes."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def version_params
|
|
99
|
+
params.require(:consent_copy_version).permit(
|
|
100
|
+
:banner_title, :banner_body, :modal_intro,
|
|
101
|
+
:accept_label, :reject_label, :manage_label, :save_label,
|
|
102
|
+
category_blurbs: {}
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def blank_category_blurbs
|
|
107
|
+
ConsentCategory.order(:position).each_with_object({}) do |category, blurbs|
|
|
108
|
+
blurbs[category.key] = ""
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|