diff --git a/app/components/admin/cookies/vendors/edit_component.html.erb b/app/components/admin/cookies/vendors/edit_component.html.erb new file mode 100644 index 000000000..7f55fac75 --- /dev/null +++ b/app/components/admin/cookies/vendors/edit_component.html.erb @@ -0,0 +1,5 @@ +<%= back_link_to admin_settings_path(anchor: "tab-cookies-consent") %> + +<%= header %> + +<%= render Admin::Cookies::Vendors::FormComponent.new(vendor) %> diff --git a/app/components/admin/cookies/vendors/edit_component.rb b/app/components/admin/cookies/vendors/edit_component.rb new file mode 100644 index 000000000..a791c6505 --- /dev/null +++ b/app/components/admin/cookies/vendors/edit_component.rb @@ -0,0 +1,15 @@ +class Admin::Cookies::Vendors::EditComponent < ApplicationComponent + include Header + + attr_reader :vendor + + def initialize(vendor) + @vendor = vendor + end + + private + + def title + t("admin.cookies.vendors.edit.title") + end +end diff --git a/app/components/admin/cookies/vendors/form_component.html.erb b/app/components/admin/cookies/vendors/form_component.html.erb new file mode 100644 index 000000000..1d51fbbbe --- /dev/null +++ b/app/components/admin/cookies/vendors/form_component.html.erb @@ -0,0 +1,10 @@ +<%= form_for [:admin, vendor], html: { class: "vendor-form" } do |f| %> + <%= render "shared/errors", resource: vendor %> + + <%= f.text_field :name, hint: t("admin.cookies.vendors.form.name.help_text") %> + <%= f.text_area :description, hint: t("admin.cookies.vendors.form.description.help_text"), rows: 4 %> + <%= f.text_field :cookie, hint: t("admin.cookies.vendors.form.cookie.help_text") %> + <%= f.text_area :script, hint: t("admin.cookies.vendors.form.script.help_text"), rows: 10 %> + + <%= f.submit %> +<% end %> diff --git a/app/components/admin/cookies/vendors/form_component.rb b/app/components/admin/cookies/vendors/form_component.rb new file mode 100644 index 000000000..44dc4dc04 --- /dev/null +++ b/app/components/admin/cookies/vendors/form_component.rb @@ -0,0 +1,7 @@ +class Admin::Cookies::Vendors::FormComponent < ApplicationComponent + attr_reader :vendor + + def initialize(vendor) + @vendor = vendor + end +end diff --git a/app/components/admin/cookies/vendors/new_component.html.erb b/app/components/admin/cookies/vendors/new_component.html.erb new file mode 100644 index 000000000..7f55fac75 --- /dev/null +++ b/app/components/admin/cookies/vendors/new_component.html.erb @@ -0,0 +1,5 @@ +<%= back_link_to admin_settings_path(anchor: "tab-cookies-consent") %> + +<%= header %> + +<%= render Admin::Cookies::Vendors::FormComponent.new(vendor) %> diff --git a/app/components/admin/cookies/vendors/new_component.rb b/app/components/admin/cookies/vendors/new_component.rb new file mode 100644 index 000000000..f96fcec0a --- /dev/null +++ b/app/components/admin/cookies/vendors/new_component.rb @@ -0,0 +1,15 @@ +class Admin::Cookies::Vendors::NewComponent < ApplicationComponent + include Header + + attr_reader :vendor + + def initialize(vendor) + @vendor = vendor + end + + private + + def title + t("admin.cookies.vendors.new.title") + end +end diff --git a/app/components/admin/cookies/vendors/row_component.html.erb b/app/components/admin/cookies/vendors/row_component.html.erb new file mode 100644 index 000000000..cd08c5b45 --- /dev/null +++ b/app/components/admin/cookies/vendors/row_component.html.erb @@ -0,0 +1,5 @@ + + <%= vendor.name %> + <%= vendor.cookie %> + <%= render Admin::TableActionsComponent.new(vendor) %> + diff --git a/app/components/admin/cookies/vendors/row_component.rb b/app/components/admin/cookies/vendors/row_component.rb new file mode 100644 index 000000000..10e7ac2e2 --- /dev/null +++ b/app/components/admin/cookies/vendors/row_component.rb @@ -0,0 +1,9 @@ +class Admin::Cookies::Vendors::RowComponent < ApplicationComponent + with_collection_parameter :vendor + + attr_reader :vendor + + def initialize(vendor:) + @vendor = vendor + end +end diff --git a/app/components/admin/cookies/vendors/table_component.html.erb b/app/components/admin/cookies/vendors/table_component.html.erb new file mode 100644 index 000000000..741dd207d --- /dev/null +++ b/app/components/admin/cookies/vendors/table_component.html.erb @@ -0,0 +1,22 @@ +

<%= t("admin.cookies.vendors.third_party_cookies") %>

+ +<% if vendors.any? %> + + + + + + + + + + <%= render Admin::Cookies::Vendors::RowComponent.with_collection(vendors) %> + +
<%= attribute_name(:name) %><%= attribute_name(:cookie) %><%= t("admin.shared.actions") %>
+<% else %> +
+ <%= t("admin.cookies.vendors.empty") %> +
+<% end %> + +<%= link_to t("admin.cookies.vendors.create_button"), new_admin_cookies_vendor_path, class: "button" %> diff --git a/app/components/admin/cookies/vendors/table_component.rb b/app/components/admin/cookies/vendors/table_component.rb new file mode 100644 index 000000000..bd897ce58 --- /dev/null +++ b/app/components/admin/cookies/vendors/table_component.rb @@ -0,0 +1,11 @@ +class Admin::Cookies::Vendors::TableComponent < ApplicationComponent + private + + def vendors + ::Cookies::Vendor.all + end + + def attribute_name(attribute) + ::Cookies::Vendor.human_attribute_name(attribute) + end +end diff --git a/app/components/admin/settings/cookies_consent_tab_component.html.erb b/app/components/admin/settings/cookies_consent_tab_component.html.erb index 7bb2e9bfe..88fbedbdb 100644 --- a/app/components/admin/settings/cookies_consent_tab_component.html.erb +++ b/app/components/admin/settings/cookies_consent_tab_component.html.erb @@ -4,3 +4,5 @@ <%= render Admin::Settings::RowComponent.new("feature.cookies_consent", type: :feature, tab: tab) %> <%= render Admin::Settings::RowComponent.new("cookies_consent.more_info_link", type: :text, tab: tab) %> <% end %> + +<%= render Admin::Cookies::Vendors::TableComponent.new %> diff --git a/app/controllers/admin/cookies/vendors_controller.rb b/app/controllers/admin/cookies/vendors_controller.rb new file mode 100644 index 000000000..a011936d2 --- /dev/null +++ b/app/controllers/admin/cookies/vendors_controller.rb @@ -0,0 +1,38 @@ +class Admin::Cookies::VendorsController < Admin::BaseController + load_and_authorize_resource :vendor, class: "::Cookies::Vendor" + + def create + if @vendor.save + redirect_to admin_settings_path(anchor: "tab-cookies-consent"), + notice: t("admin.cookies.vendors.create.notice") + else + render :new + end + end + + def update + if @vendor.update(vendor_params) + redirect_to admin_settings_path(anchor: "tab-cookies-consent"), + notice: t("admin.cookies.vendors.update.notice") + else + render :edit + end + end + + def destroy + @vendor.destroy! + + redirect_to admin_settings_path(anchor: "tab-cookies-consent"), + notice: t("admin.cookies.vendors.destroy.notice") + end + + private + + def vendor_params + params.require(:cookies_vendor).permit(allowed_params) + end + + def allowed_params + [:name, :description, :cookie, :script] + end +end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 5c9fb6dee..9e9c123ce 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -142,6 +142,8 @@ module Abilities can :manage, LocalCensusRecord can [:create, :read], LocalCensusRecords::Import + can :manage, Cookies::Vendor + if Rails.application.config.multitenancy && Tenant.default? can [:create, :read, :update, :hide, :restore], Tenant end diff --git a/app/models/cookies.rb b/app/models/cookies.rb new file mode 100644 index 000000000..6661769bd --- /dev/null +++ b/app/models/cookies.rb @@ -0,0 +1,5 @@ +module Cookies + def self.table_name_prefix + "cookies_" + end +end diff --git a/app/models/cookies/vendor.rb b/app/models/cookies/vendor.rb new file mode 100644 index 000000000..198f9ce6a --- /dev/null +++ b/app/models/cookies/vendor.rb @@ -0,0 +1,4 @@ +class Cookies::Vendor < ApplicationRecord + validates :name, presence: true + validates :cookie, presence: true, uniqueness: true, format: { with: /\A[a-zA-Z0-9\_]+\Z/ } +end diff --git a/app/views/admin/cookies/vendors/edit.html.erb b/app/views/admin/cookies/vendors/edit.html.erb new file mode 100644 index 000000000..09e16f2cb --- /dev/null +++ b/app/views/admin/cookies/vendors/edit.html.erb @@ -0,0 +1 @@ +<%= render Admin::Cookies::Vendors::EditComponent.new(@vendor) %> diff --git a/app/views/admin/cookies/vendors/new.html.erb b/app/views/admin/cookies/vendors/new.html.erb new file mode 100644 index 000000000..7e92b901e --- /dev/null +++ b/app/views/admin/cookies/vendors/new.html.erb @@ -0,0 +1 @@ +<%= render Admin::Cookies::Vendors::NewComponent.new(@vendor) %> diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index 068a4dde0..17f39b6dc 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -151,6 +151,9 @@ en: votation_type: one: Votation type other: Votation types + cookies/vendor: + one: cookie vendor + other: cookie vendors attributes: budget: name: "Name" @@ -534,6 +537,10 @@ en: votation_type/vote_type: unique: Unique answer multiple: Multiple answers + cookies/vendor: + name: Vendor name + cookie: Cookie name + script: Javascript code errors: models: user: diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index fc6d35e6d..f51a3daf6 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -1808,3 +1808,27 @@ en: tags: "Tags" tags_description: "Generates automatic tags on all items that can be tagged on." title: "AI / Machine learning" + cookies: + vendors: + empty: No vendors found + third_party_cookies: Third party cookies + create_button: Create cookie vendor + create: + notice: Cookie vendor created successfully + new: + title: New cookie vendor + edit: + title: Edit cookie vendor + update: + notice: Cookie vendor updated successfully + destroy: + notice: Cookie vendor deleted successfully + form: + name: + help_text: This information will be publicly visible. + cookie: + help_text: This information will be used internally. The cookie name must be unique, and it can only contain letters, digits and underscores. + description: + help_text: This information will be publicly visible. + script: + help_text: This is the script to run when this cookie is accepted by the user. You can enter vanilla Javascript code here and introduce references to other application javascript. diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index 9bab05b5a..a2ba75682 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -151,6 +151,9 @@ es: votation_type: one: Tipo de votación other: Tipos de votación + cookies/vendor: + one: proveedor de cookies + other: proveedores de cookies attributes: budget: name: "Nombre" @@ -534,6 +537,10 @@ es: votation_type/vote_type: unique: Respuesta única multiple: Respuesta múltiple + cookies/vendor: + name: Nombre del proveedor + cookie: Nombre de la cookie + script: Código javascript errors: models: user: diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 74f91a71f..3e2c0067b 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -1808,3 +1808,27 @@ es: tags: "Etiquetas" tags_description: "Genera etiquetas automáticas para todos los elementos que pueden ser etiquetados." title: "IA / Machine learning" + cookies: + vendors: + empty: No se han encontrado cookies de terceros + third_party_cookies: Cookies de terceros + create_button: Nueva cookie + create: + notice: Cookie creada correctamente + new: + title: Nueva cookie + edit: + title: Editar cookie + update: + notice: Cookie actualizada correctamente + destroy: + notice: Cookie eliminada correctamente + form: + name: + help_text: Esta información será visible públicamente. + cookie: + help_text: Esta información se utilizará internamente. El nombre de la cookie debe ser único, solo puede contener letras, dígitos y guiones bajos. + description: + help_text: Esta información será visible públicamente. + script: + help_text: Este es el script que se ejecutará cuando esta cookie sea aceptada por el usuario. Puede introducir código Javascript aquí e introducir referencias a otro javascript de la aplicación. diff --git a/config/routes/admin.rb b/config/routes/admin.rb index ed300820a..e0eec7867 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -304,6 +304,10 @@ namespace :admin do post :execute, on: :collection delete :cancel, on: :collection end + + namespace :cookies do + resources :vendors, except: [:index, :show] + end end end diff --git a/db/migrate/20240108110154_create_cookies_vendors.rb b/db/migrate/20240108110154_create_cookies_vendors.rb new file mode 100644 index 000000000..e4bc42cd2 --- /dev/null +++ b/db/migrate/20240108110154_create_cookies_vendors.rb @@ -0,0 +1,14 @@ +class CreateCookiesVendors < ActiveRecord::Migration[6.1] + def change + create_table :cookies_vendors do |t| + t.string :name + t.text :description + t.string :cookie + t.text :script + + t.timestamps + + t.index :cookie, unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c30aad673..e3b541dc0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -454,6 +454,16 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_26_112901) do t.datetime "updated_at", precision: nil, null: false end + create_table "cookies_vendors", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "cookie" + t.text "script" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["cookie"], name: "index_cookies_vendors_on_cookie", unique: true + end + create_table "dashboard_actions", id: :serial, force: :cascade do |t| t.string "title", limit: 80 t.text "description" diff --git a/spec/factories/cookies.rb b/spec/factories/cookies.rb new file mode 100644 index 000000000..b3ac24e62 --- /dev/null +++ b/spec/factories/cookies.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :cookies_vendor, class: "Cookies::Vendor" do + name { "Vendor name" } + sequence(:cookie) { |n| "vendor_cookie_#{n}" } + description { "Vendor description" } + end +end diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index f75aac370..24b10a9cc 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -172,6 +172,10 @@ describe Abilities::Administrator do it { should be_able_to(:manage, Widget::Card) } + it { should be_able_to(:create, Cookies::Vendor) } + it { should be_able_to(:update, Cookies::Vendor) } + it { should be_able_to(:destroy, Cookies::Vendor) } + describe "tenants" do context "with multitenancy disabled" do before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) } diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index 9f80ee67c..f79295d13 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -372,4 +372,8 @@ describe Abilities::Common do it { should_not be_able_to(:read, SDG::Manager) } it { should_not be_able_to(:create, SDG::Manager) } it { should_not be_able_to(:delete, SDG::Manager) } + + it { should_not be_able_to(:create, Cookies::Vendor) } + it { should_not be_able_to(:update, Cookies::Vendor) } + it { should_not be_able_to(:destroy, Cookies::Vendor) } end diff --git a/spec/models/abilities/moderator_spec.rb b/spec/models/abilities/moderator_spec.rb index 9dd4f4c0c..cddaa8c8f 100644 --- a/spec/models/abilities/moderator_spec.rb +++ b/spec/models/abilities/moderator_spec.rb @@ -113,4 +113,8 @@ describe Abilities::Moderator do it { should_not be_able_to(:read, SDG::Manager) } it { should_not be_able_to(:create, SDG::Manager) } it { should_not be_able_to(:delete, SDG::Manager) } + + it { should_not be_able_to(:create, Cookies::Vendor) } + it { should_not be_able_to(:update, Cookies::Vendor) } + it { should_not be_able_to(:destroy, Cookies::Vendor) } end diff --git a/spec/models/abilities/valuator_spec.rb b/spec/models/abilities/valuator_spec.rb index 87b52f472..8e64833fc 100644 --- a/spec/models/abilities/valuator_spec.rb +++ b/spec/models/abilities/valuator_spec.rb @@ -51,4 +51,8 @@ describe Abilities::Valuator do it { should_not be_able_to(:read, SDG::Manager) } it { should_not be_able_to(:create, SDG::Manager) } it { should_not be_able_to(:delete, SDG::Manager) } + + it { should_not be_able_to(:create, Cookies::Vendor) } + it { should_not be_able_to(:update, Cookies::Vendor) } + it { should_not be_able_to(:destroy, Cookies::Vendor) } end diff --git a/spec/models/cookies/vendor_spec.rb b/spec/models/cookies/vendor_spec.rb new file mode 100644 index 000000000..3a56f098d --- /dev/null +++ b/spec/models/cookies/vendor_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" + +describe Cookies::Vendor do + let(:cookies_vendor) { build(:cookies_vendor) } + + it "is valid" do + expect(cookies_vendor).to be_valid + end + + it "is not valid without a name" do + cookies_vendor.name = nil + + expect(cookies_vendor).not_to be_valid + end + + it "is not valid without the cookie name" do + cookies_vendor.cookie = nil + + expect(cookies_vendor).not_to be_valid + end + + it "is not valid when cookie_name contains whitespaces, special characters" do + cookies_vendor.cookie = "cookie vendor name" + + expect(cookies_vendor).not_to be_valid + + cookies_vendor.cookie = "cookie_vendor/name" + + expect(cookies_vendor).not_to be_valid + + cookies_vendor.cookie = "cookie_vendor_name" + + expect(cookies_vendor).to be_valid + end + + it "is not valid when the cookie name already exists" do + create(:cookies_vendor, cookie: "existing_name") + + cookies_vendor.cookie = "existing_name" + + expect(cookies_vendor).not_to be_valid + end +end diff --git a/spec/system/admin/cookies/vendors_spec.rb b/spec/system/admin/cookies/vendors_spec.rb new file mode 100644 index 000000000..fda94914b --- /dev/null +++ b/spec/system/admin/cookies/vendors_spec.rb @@ -0,0 +1,62 @@ +require "rails_helper" + +describe "Admin cookies vendors", :admin do + describe "Index" do + scenario "Shows existing cookies and links to actions" do + create(:cookies_vendor, name: "Third party", cookie: "third_party") + visit admin_settings_path(anchor: "tab-cookies-consent") + + expect(page).to have_content "Third party" + expect(page).to have_content "third_party" + expect(page).to have_link "Edit" + expect(page).to have_button "Delete" + expect(page).to have_link "Create cookie vendor" + end + end + + describe "Create" do + scenario "Shows a notice and the new cookie after creation" do + visit admin_settings_path(anchor: "tab-cookies-consent") + + click_link "Create cookie vendor" + fill_in "Vendor name", with: "Vendor name" + fill_in "Cookie name", with: "vendor_cookie" + fill_in "Description", with: "Cookie details" + click_button "Create cookie vendor" + + expect(page).to have_content "Cookie vendor created successfully" + expect(page).to have_content "Vendor name" + expect(page).to have_content "vendor_cookie" + end + end + + describe "Update" do + scenario "Shows a notice and the cookie changes after update" do + create(:cookies_vendor, name: "Third party", cookie: "third_party") + visit admin_settings_path(anchor: "tab-cookies-consent") + + click_link "Edit" + fill_in "Vendor name", with: "Cool Company Name" + click_button "Update cookie vendor" + + expect(page).to have_content "Cookie vendor updated successfully" + expect(page).to have_content "Cool Company Name" + end + end + + describe "Destroy" do + scenario "Shows a notice and removes cookie" do + create(:cookies_vendor, name: "Analitics cookie", cookie: "analitics_cookie") + visit admin_settings_path(anchor: "tab-cookies-consent") + + expect(page).to have_content "Analitics cookie" + expect(page).to have_content "analitics_cookie" + + accept_confirm { click_button "Delete Analitics cookie" } + + expect(page).to have_content "Cookie vendor deleted successfully" + expect(page).not_to have_content "Analitics cookie" + expect(page).not_to have_content "analitics_cookie" + end + end +end