diff --git a/Gemfile.lock b/Gemfile.lock
index 2571c53..86e2efd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,6 +4,7 @@ PATH
decidim-anonymous_codes (1.0)
decidim-admin (>= 0.27.0, < 0.28)
decidim-core (>= 0.27.0, < 0.28)
+ decidim-forms (>= 0.27.0, < 0.28)
decidim-surveys (>= 0.27.0, < 0.28)
deface (>= 1.9.0)
diff --git a/Rakefile b/Rakefile
index 98db11d..92564cd 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-require "decidim/dev/common_rake"
require "fileutils"
+require "decidim/dev/common_rake"
+require "decidim/anonymous_codes/engine"
def install_module(path)
Dir.chdir(path) do
@@ -13,6 +14,7 @@ end
def seed_db(path)
Dir.chdir(path) do
system("bundle exec rake db:seed")
+ system("bundle exec rake decidim_anonymous_codes:db:seed")
end
end
diff --git a/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb b/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb
new file mode 100644
index 0000000..c2e67a4
--- /dev/null
+++ b/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Decidim
+ module AnonymousCodes
+ module SurveysControllerOverride
+ extend ActiveSupport::Concern
+
+ included do
+ before_action do
+ next unless current_settings.allow_answers? && survey.open?
+ next if visitor_already_answered?
+
+ if token_groups.any?
+ next if current_token&.available?
+
+ if current_token.blank?
+ flash.now[:alert] = I18n.t("decidim.anonymous_codes.invalid_code") if params.has_key?(:token)
+ elsif current_token.used?
+ flash.now[:alert] = I18n.t("decidim.anonymous_codes.used_code")
+ elsif current_token.expired?
+ flash.now[:alert] = I18n.t("decidim.anonymous_codes.expired_code")
+ end
+ render "decidim/anonymous_codes/surveys/code_required"
+ end
+ end
+
+ after_action only: :answer do
+ next unless current_token&.available?
+
+ # find any answer for the current user and questionnaire that would be used as a resource to link the usage counter
+ answer = Decidim::Forms::Answer.find_by(questionnaire: questionnaire, user: current_user, session_token: @form.context.session_token)
+ current_token.answers << answer if answer.present?
+ end
+
+ private
+
+ def token_groups
+ @token_groups ||= Decidim::AnonymousCodes::Group.where(resource: survey, active: true)
+ end
+
+ def current_token
+ @current_token ||= Decidim::AnonymousCodes::Token.where(group: token_groups).find_by(token: token_param)
+ end
+
+ def token_param
+ @token_param ||= begin
+ session[:anonymous_codes_token] = params[:token] if params.has_key?(:token)
+ session[:anonymous_codes_token]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/decidim/anonymous_codes/token.rb b/app/models/decidim/anonymous_codes/token.rb
index 8972ebd..122124a 100644
--- a/app/models/decidim/anonymous_codes/token.rb
+++ b/app/models/decidim/anonymous_codes/token.rb
@@ -10,8 +10,25 @@ class Token < ApplicationRecord
throw(:abort) if usage_count.positive?
end
- belongs_to :group, class_name: "Decidim::AnonymousCodes::Group"
- belongs_to :resource, polymorphic: true, optional: true
+ belongs_to :group, class_name: "Decidim::AnonymousCodes::Group", counter_cache: true
+ has_many :token_resources, class_name: "Decidim::AnonymousCodes::TokenResource", dependent: :destroy
+ has_many :answers, through: :token_resources, source_type: "Decidim::Forms::Answer", source: :resource
+
+ delegate :active?, to: :group
+ validates :token, presence: true
+ validates :token, uniqueness: { scope: [:group] }
+
+ def available?
+ !used? && !expired? && active?
+ end
+
+ def used?
+ usage_count.to_i >= group.max_reuses.to_i
+ end
+
+ def expired?
+ group.expires_at.present? && group.expires_at < Time.current
+ end
end
end
end
diff --git a/app/models/decidim/anonymous_codes/token_resource.rb b/app/models/decidim/anonymous_codes/token_resource.rb
new file mode 100644
index 0000000..6d5bf37
--- /dev/null
+++ b/app/models/decidim/anonymous_codes/token_resource.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Decidim
+ module AnonymousCodes
+ class TokenResource < ApplicationRecord
+ self.table_name = :decidim_anonymous_codes_token_resources
+
+ belongs_to :token, class_name: "Decidim::AnonymousCodes::Token", counter_cache: :usage_count
+ belongs_to :resource, polymorphic: true
+
+ validate :max_uses_not_exceeded
+ validate :valid_parent_questionnaire
+
+ private
+
+ def max_uses_not_exceeded
+ return unless token.usage_count >= token.group.max_reuses
+
+ errors.add(:base, :max_uses_exceeded)
+ end
+
+ def valid_parent_questionnaire
+ return unless resource.try(:questionnaire) != token.group.resource.try(:questionnaire)
+
+ errors.add(:base, :invalid_questionnaire)
+ end
+ end
+ end
+end
diff --git a/app/views/decidim/anonymous_codes/surveys/code_required.html.erb b/app/views/decidim/anonymous_codes/surveys/code_required.html.erb
new file mode 100644
index 0000000..b6afd7d
--- /dev/null
+++ b/app/views/decidim/anonymous_codes/surveys/code_required.html.erb
@@ -0,0 +1,50 @@
+<% add_decidim_meta_tags({
+ title: translated_attribute(questionnaire.title),
+ description: translated_attribute(questionnaire.description),
+}) %>
+
+<%= render partial: "decidim/shared/component_announcement" if current_component.manifest_name == "surveys" %>
+
+
+
<%= translated_attribute questionnaire.title %>
+
+
+ <%= decidim_sanitize_editor_admin translated_attribute questionnaire.description %>
+
+
+
+
+
+
+
+
+ <% unless questionnaire_for.try(:component)&.try(:published?) %>
+
+
+
<%= t("questionnaire_not_published.body", scope: "decidim.forms.questionnaires.show") %>
+
+
+ <% end %>
+
+
+
+
<%= t(".title") %>
+
<%= t(".body") %>
+
+
+
+
+
+
+
+
+
+
+
+
+<% content_for :js_content do %>
+ <%= javascript_pack_tag "decidim_forms" %>
+<% end %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 133736d..6dbcf99 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -6,6 +6,7 @@ locales: [en]
data:
external:
- "<%= %x[bundle info decidim-core --path].chomp %>/config/locales/%{locale}.yml"
+ - "<%= %x[bundle info decidim-forms --path].chomp %>/config/locales/%{locale}.yml"
ignore_unused:
ignore_missing:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ecc9f75..7fb2eb1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,5 +1,12 @@
---
en:
+ activerecord:
+ errors:
+ models:
+ decidim/anonymous_codes/token_resource:
+ invalid_questionnaire: The attached answer belongs to a different questionnaire.
+ max_uses_exceeded: The code has already been used the maximum number of
+ times.
decidim:
anonymous_codes:
admin:
@@ -15,3 +22,13 @@ en:
still apply if you choose to use this option.
groups: 'Edit groups:'
new_group: "\U0001F449 Create answer codes here"
+ expired_code: The introduced code has expired. Please try again.
+ invalid_code: The introduced code is invalid. Please try again.
+ surveys:
+ code_required:
+ body: This form can only be accessed using a valid code. If you have a valid
+ code, please enter it below.
+ label: Code
+ submit: Continue
+ title: Form restricted
+ used_code: The introduced code has already been used. Please try again.
diff --git a/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb b/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb
index e4c4ef0..15f7674 100644
--- a/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb
+++ b/db/migrate/20240403091336_create_decidim_anonymous_codes_groups.rb
@@ -8,6 +8,7 @@ def change
t.datetime :expires_at
t.boolean :active, default: true, null: false
t.integer :max_reuses, default: 1, null: false
+ t.integer :tokens_count, default: 0, null: false
t.references :resource, polymorphic: true, null: true, index: { name: "decidim_anonymous_codes_groups_on_resource" }
t.references :decidim_organization, null: false, foreign_key: true, index: { name: "decidim_anonymous_codes_groups_on_organization" }
diff --git a/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb b/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb
index 1c6b7ed..38d3c1f 100644
--- a/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb
+++ b/db/migrate/20240403091356_create_decidim_anonymous_codes_tokens.rb
@@ -7,7 +7,7 @@ def change
t.integer :usage_count, default: 0, null: false
t.references :group, null: false, index: { name: "decidim_anonymous_codes_tokens_on_group" }
- t.references :resource, polymorphic: true, null: true, index: { name: "decidim_anonymous_codes_tokens_on_resource" }
+ t.index [:token, :group_id], name: "index_anonymous_codes_token_group_uniqueness", unique: true
t.timestamps
end
end
diff --git a/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb b/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb
new file mode 100644
index 0000000..01b1c6f
--- /dev/null
+++ b/db/migrate/20240403091376_create_decidim_anonymous_codes_token_resources.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class CreateDecidimAnonymousCodesTokenResources < ActiveRecord::Migration[6.1]
+ def change
+ create_table :decidim_anonymous_codes_token_resources do |t|
+ t.references :token, null: false, index: { name: "decidim_anonymous_codes_token_resources_on_token" }
+ t.references :resource, polymorphic: true, null: false, index: { name: "decidim_anonymous_codes_token_resources_on_resource" }
+ t.timestamps
+ end
+ end
+end
diff --git a/db/seeds.rb b/db/seeds.rb
index 0bc7ce5..b7e5015 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,33 +1,54 @@
# frozen_string_literal: true
if !Rails.env.production? || ENV.fetch("SEED", nil)
- print "Creating seeds for decidim_anonymous_codes...\n" unless Rails.env.test?
-
organization = Decidim::Organization.first
+ participatory_processes = Decidim::ParticipatoryProcess.where(organization: organization)
+
+ unless participatory_processes.count.positive?
+ puts "No participatory processes found. Skipping seeds for decidim_anonymous_codes..."
+ return
+ end
+ print "Creating seeds for decidim_anonymous_codes...\n" unless Rails.env.test?
- group1 = Decidim::AnonymousCodes::Group.create!(
- title: { en: "First Group" },
- organization: organization,
- expires_at: 1.week.from_now,
- active: true,
- max_reuses: 1,
- resource: Decidim::Surveys::Survey.first
- )
+ admin = Decidim::User.where(admin: true).first
+ survey_components = Decidim::Component.where(manifest_name: "surveys", participatory_space: participatory_processes)
+ survey_components.each do |component|
+ form = OpenStruct.new(name: component.name, weight: component.weight, settings: component.settings, default_step_settings: component.default_step_settings,
+ step_settings: component.step_settings)
+ form.step_settings.each do |key, _value|
+ form.step_settings[key]["allow_answers"] = true
+ form.step_settings[key]["allow_unregistered"] = true
+ end
+ Decidim::Admin::UpdateComponent.call(form, component, admin) do
+ on(:ok) do
+ puts "Component #{component.id} updated for allowing answers and unregistered users"
+ end
- group2 = Decidim::AnonymousCodes::Group.create!(
- title: { en: "Second Group" },
- organization: organization,
- expires_at: 1.week.from_now,
- active: true,
- max_reuses: 1,
- resource: Decidim::Surveys::Survey.second
- )
+ on(:invalid) do
+ puts "ERROR: Component #{component.id} not updated for allowing answers and unregistered users"
+ end
+ end
- 5.times do
- Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, usage_count: 0, group: group1)
- end
+ survey = Decidim::Surveys::Survey.find_by(component: component)
+ next unless survey
- 25.times do
- Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, usage_count: 0, group: group2)
+ group = Decidim.traceability.create!(
+ Decidim::AnonymousCodes::Group,
+ admin,
+ {
+ title: { en: "Group for Survey #{survey.id}" },
+ organization: organization,
+ expires_at: 1.week.from_now,
+ active: rand(2).zero?,
+ max_reuses: rand(3),
+ resource: survey
+ },
+ visibility: "admin-only"
+ )
+ total = rand(1..25)
+ total.times do
+ Decidim::AnonymousCodes::Token.create!(token: Decidim::AnonymousCodes.token_generator, group: group)
+ end
+ puts "Created #{total} tokens for survey #{survey.id}"
end
end
diff --git a/decidim-anonymous_codes.gemspec b/decidim-anonymous_codes.gemspec
index 78a68ae..1ef6fe9 100644
--- a/decidim-anonymous_codes.gemspec
+++ b/decidim-anonymous_codes.gemspec
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "decidim-admin", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION
spec.add_dependency "decidim-core", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION
+ spec.add_dependency "decidim-forms", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION
spec.add_dependency "decidim-surveys", Decidim::AnonymousCodes::COMPAT_DECIDIM_VERSION
spec.add_dependency "deface", ">= 1.9.0"
diff --git a/lib/decidim/anonymous_codes/engine.rb b/lib/decidim/anonymous_codes/engine.rb
index c37b7d2..1c1f01b 100644
--- a/lib/decidim/anonymous_codes/engine.rb
+++ b/lib/decidim/anonymous_codes/engine.rb
@@ -9,9 +9,9 @@ class Engine < ::Rails::Engine
isolate_namespace Decidim::AnonymousCodes
initializer "decidim_anonymous_codes.overrides", after: "decidim.action_controller" do
- # config.to_prepare do
-
- # end
+ config.to_prepare do
+ Decidim::Surveys::SurveysController.include(Decidim::AnonymousCodes::SurveysControllerOverride)
+ end
end
initializer "decidim-anonymous_codes.webpacker.assets_path" do
diff --git a/lib/decidim/anonymous_codes/test/factories.rb b/lib/decidim/anonymous_codes/test/factories.rb
index 8cc9a03..5d864b9 100644
--- a/lib/decidim/anonymous_codes/test/factories.rb
+++ b/lib/decidim/anonymous_codes/test/factories.rb
@@ -12,7 +12,17 @@
factory :anonymous_codes_token, class: "Decidim::AnonymousCodes::Token" do
token { Decidim::AnonymousCodes.token_generator }
- usage_count { 0 }
group { create(:anonymous_codes_group) }
+
+ trait :used do
+ after(:create) do |object|
+ object.token_resources << create(:anonymous_codes_token_resource, token: object)
+ end
+ end
+ end
+
+ factory :anonymous_codes_token_resource, class: "Decidim::AnonymousCodes::TokenResource" do
+ token { create(:anonymous_codes_token) }
+ resource { create(:answer, questionnaire: token.group.resource.questionnaire) }
end
end
diff --git a/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake b/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake
index 86db4b4..15da310 100644
--- a/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake
+++ b/lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake
@@ -3,3 +3,12 @@
Rake::Task["decidim:choose_target_plugins"].enhance do
ENV["FROM"] = "#{ENV.fetch("FROM", nil)},decidim_anonymous_codes"
end
+
+namespace :decidim_anonymous_codes do
+ namespace :db do
+ desc "loads all seeds in db/seeds.rb"
+ task seed: :environment do
+ Decidim::AnonymousCodes::Engine.load_seed
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index ebe4f82..563b0c0 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -2,5 +2,6 @@
require "decidim/core/test/factories"
require "decidim/surveys/test/factories"
+require "decidim/forms/test/factories"
require "decidim/proposals/test/factories"
require "decidim/anonymous_codes/test/factories"
diff --git a/spec/lib/overrides_spec.rb b/spec/lib/overrides_spec.rb
index 2a805c4..d1da067 100644
--- a/spec/lib/overrides_spec.rb
+++ b/spec/lib/overrides_spec.rb
@@ -7,8 +7,16 @@
# file should be updated to match any change/bug fix introduced in the core
checksums = [
{
- package: "decidim-core",
+ package: "decidim-surveys",
files: {
+ "/app/controllers/decidim/surveys/surveys_controller.rb" => "f443ed19838d251fd9d9b960e5908418"
+ }
+ },
+ {
+ package: "decidim-forms",
+ files: {
+ "/app/controllers/decidim/forms/concerns/has_questionnaire.rb" => "3e68234b1e489158b3377426236db093",
+ "/app/views/decidim/forms/questionnaires/show.html.erb" => "b54864ffbdaab74bfc82f7b047cbf170"
}
}
]
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 136ef92..a1c4a72 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -14,6 +14,10 @@ module AnonymousCodes
it { expect(described_class.reflect_on_association(:organization).macro).to eq(:belongs_to) }
end
+ it "has an empty counter cache" do
+ expect(group.tokens_count).to eq(0)
+ end
+
context "when tokens exist" do
let!(:token) { create(:anonymous_codes_token, group: group) }
@@ -21,6 +25,14 @@ module AnonymousCodes
expect(group.organization).to be_a(Decidim::Organization)
end
+ it "has a counter cache" do
+ expect(group.tokens_count).to eq(1)
+ end
+
+ it "lowers the counter on destroying tokens" do
+ expect { token.destroy }.to change(group, :tokens_count).by(-1)
+ end
+
it "destroys the tokens when destroyed" do
expect { group.destroy }.to change(Token, :count).by(-1)
end
diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb
index bcc4f9b..1b819e9 100644
--- a/spec/models/token_spec.rb
+++ b/spec/models/token_spec.rb
@@ -13,6 +13,130 @@ module AnonymousCodes
it { expect(described_class.reflect_on_association(:group).macro).to eq(:belongs_to) }
end
+ it { is_expected.not_to be_used }
+
+ it "has an empty counter cache" do
+ expect(token.usage_count).to eq(0)
+ end
+
+ context "when adding an answer" do
+ let(:resource) { create(:answer, questionnaire: token.group.resource.questionnaire) }
+
+ it "can be associated" do
+ expect { token.answers << resource }.to change(token.token_resources, :count).by(1)
+ end
+
+ context "and the answer associated does not belong to the parent questionnaire" do
+ let(:resource) { create(:answer) }
+
+ it "cannot be associated" do
+ expect { token.answers << resource }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ context "when different resource type" do
+ let(:resource) { create(:questionnaire) }
+
+ it "cannot be associated" do
+ expect { create(:anonymous_codes_token_resource, token: token, resource: resource) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+
+ context "and max uses have been reached" do
+ before do
+ token.answers << resource
+ end
+
+ it "cannot be associated" do
+ expect { token.answers << resource }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+
+ context "when resource exists" do
+ let!(:token) { create(:anonymous_codes_token, :used) }
+
+ it { is_expected.to be_used }
+
+ it "has a counter cache" do
+ expect(token.usage_count).to eq(1)
+ end
+
+ it "lowers the counter on removing the resource" do
+ expect(TokenResource.count).to eq(1)
+ expect { token.update!(token_resources: []) }.to change(token, :usage_count).by(-1)
+ expect(TokenResource.count).to eq(0)
+ end
+
+ context "when max_reuses is 2" do
+ before do
+ token.group.update!(max_reuses: 2)
+ end
+
+ it { is_expected.not_to be_used }
+ end
+ end
+
+ describe "expired?" do
+ context "when expires_at is nil" do
+ it { is_expected.not_to be_expired }
+ end
+
+ context "when expires_at is in the past" do
+ before do
+ token.group.update!(expires_at: 1.minute.ago)
+ end
+
+ it { is_expected.to be_expired }
+ end
+
+ context "when expires_at is in the future" do
+ before do
+ token.group.update!(expires_at: 1.minute.from_now)
+ end
+
+ it { is_expected.not_to be_expired }
+ end
+ end
+
+ describe "active?" do
+ it { is_expected.to be_active }
+
+ context "when inactive" do
+ before do
+ token.group.update!(active: false)
+ end
+
+ it { is_expected.not_to be_active }
+ end
+ end
+
+ describe "available?" do
+ it { is_expected.to be_available }
+
+ context "when used" do
+ let(:token) { create(:anonymous_codes_token, usage_count: 1) }
+
+ it { is_expected.not_to be_available }
+ end
+
+ context "when expired" do
+ before do
+ token.group.update!(expires_at: 1.minute.ago)
+ end
+
+ it { is_expected.not_to be_available }
+ end
+
+ context "when inactive" do
+ before do
+ token.group.update!(active: false)
+ end
+
+ it { is_expected.not_to be_available }
+ end
+ end
+
context "when created" do
let!(:token) { create(:anonymous_codes_token) }
@@ -27,6 +151,22 @@ module AnonymousCodes
expect { token.destroy }.not_to change(Token, :count)
end
end
+
+ context "and another one is created with the same token" do
+ let(:another_token) { build(:anonymous_codes_token, token: token.token, group: token.group) }
+
+ it "cannot be created" do
+ expect { another_token.save! }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ context "and a different group" do
+ let(:another_token) { build(:anonymous_codes_token, token: token.token) }
+
+ it "can be created" do
+ expect { another_token.save! }.to change(Token, :count).by(1)
+ end
+ end
+ end
end
end
end
diff --git a/spec/system/surveys_answering_spec.rb b/spec/system/surveys_answering_spec.rb
new file mode 100644
index 0000000..21bb433
--- /dev/null
+++ b/spec/system/surveys_answering_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "Surveys Component Settings", type: :system do
+ let(:organization) { component.organization }
+ let(:component) { survey.component }
+ let(:user) { create :user, :confirmed, organization: organization }
+ let(:another_user) { create :user, :confirmed, organization: organization }
+ let(:group) { create :anonymous_codes_group, organization: organization, expires_at: expires, resource: resource, active: active }
+ let!(:token) { create :anonymous_codes_token, group: group }
+ let!(:another_token) { create :anonymous_codes_token }
+ let(:active) { true }
+ let(:expires) { nil }
+ let(:survey) { create(:survey, questionnaire: questionnaire) }
+ let(:questionnaire) { create(:questionnaire) }
+ let!(:question) { create(:questionnaire_question, body: { en: "What's the meaning of life?" }, mandatory: true, question_type: :short_answer, questionnaire: questionnaire) }
+ let(:resource) { survey }
+ let(:code) { token.token }
+ let(:settings) do
+ {
+ allow_answers: true,
+ allow_unregistered: allow_unregistered
+ }
+ end
+ let(:allow_unregistered) { false }
+
+ def visit_component
+ visit Decidim::EngineRouter.main_proxy(component).survey_path(component.id)
+ end
+
+ before do
+ component.update!(
+ step_settings: {
+ component.participatory_space.active_step.id => settings
+ }
+ )
+
+ switch_to_host(organization.host)
+ end
+
+ shared_examples "form is restricted" do
+ it "shows the restricted message" do
+ expect(page).to have_content("Form restricted")
+ expect(page).to have_field("token")
+ end
+ end
+
+ shared_examples "form is readonly" do
+ it "sends the code" do
+ fill_in :token, with: code
+ click_button("Continue")
+ expect(page).to have_content(question.body["en"])
+ expect(page).to have_link("Sign in with your account")
+ expect(page).not_to have_button("Submit")
+ end
+ end
+
+ shared_examples "form requires codes" do
+ it "sends the code and the form" do
+ fill_in :token, with: code
+ click_button("Continue")
+
+ expect(token).not_to be_used
+
+ fill_in question.body["en"], with: "42"
+ check "questionnaire_tos_agreement"
+ accept_confirm { click_button "Submit" }
+ expect(page).to have_css(".callout.success")
+ expect(page).to have_content("Already answered")
+
+ expect(token.reload).to be_used
+ end
+ end
+
+ shared_examples "can be answered without codes" do
+ it "sends the form" do
+ expect(token).not_to be_used
+
+ fill_in question.body["en"], with: "42"
+ check "questionnaire_tos_agreement"
+ accept_confirm { click_button "Submit" }
+ expect(page).to have_css(".callout.success")
+ expect(page).to have_content("Already answered")
+
+ expect(token.reload).not_to be_used
+ end
+ end
+
+ shared_examples "can be answered with codes" do
+ it_behaves_like "form requires codes"
+
+ context "and code group is inactive" do
+ let(:active) { false }
+
+ it_behaves_like "can be answered without codes"
+ end
+
+ context "and code re-uses have exceeded" do
+ let!(:token) { create :anonymous_codes_token, group: group, answers: [create(:answer, questionnaire: questionnaire, user: another_user)] }
+
+ it "sends the code and fails" do
+ fill_in :token, with: code
+ click_button("Continue")
+ expect(page).to have_css(".callout.alert")
+ expect(page).to have_content("The introduced code has already been used.")
+ expect(page).to have_content("Form restricted")
+ expect(page).to have_field("token")
+ expect(page).not_to have_content(question.body["en"])
+ end
+ end
+
+ context "and group has an expiration" do
+ let(:expires) { 1.hour.from_now }
+
+ it_behaves_like "form requires codes"
+ end
+
+ context "and group has expired" do
+ let(:expires) { 1.minute.ago }
+
+ it "sends the code and fails" do
+ fill_in :token, with: code
+ click_button("Continue")
+ expect(page).to have_css(".callout.alert")
+ expect(page).to have_content("The introduced code has expired.")
+ expect(page).to have_content("Form restricted")
+ expect(page).to have_field("token")
+ expect(page).not_to have_content(question.body["en"])
+ end
+ end
+ end
+
+ shared_examples "cannot be answered with bad codes" do
+ it "sends the code and fails" do
+ fill_in :token, with: "dirty-things"
+ click_button("Continue")
+ expect(page).to have_css(".callout.alert")
+ expect(page).to have_content("The introduced code is invalid.")
+ expect(page).to have_content("Form restricted")
+ expect(page).to have_field("token")
+ expect(page).not_to have_content(question.body["en"])
+ end
+
+ it "sends another code and fails" do
+ fill_in :token, with: another_token.token
+ click_button("Continue")
+ expect(page).to have_css(".callout.alert")
+ expect(page).to have_content("The introduced code is invalid.")
+ expect(page).to have_content("Form restricted")
+ expect(page).to have_field("token")
+ expect(page).not_to have_content(question.body["en"])
+ end
+ end
+
+ context "when user is not logged" do
+ before do
+ visit_component
+ end
+
+ it_behaves_like "form is restricted"
+ it_behaves_like "form is readonly"
+
+ context "and login is not required" do
+ let(:allow_unregistered) { true }
+
+ it_behaves_like "can be answered with codes"
+ it_behaves_like "cannot be answered with bad codes"
+
+ context "and there are no codes required" do
+ let(:resource) { nil }
+
+ it_behaves_like "can be answered without codes"
+ end
+ end
+ end
+
+ context "when user is logged" do
+ before do
+ login_as user, scope: :user
+ visit_component
+ end
+
+ it_behaves_like "form is restricted"
+ it_behaves_like "can be answered with codes"
+ it_behaves_like "cannot be answered with bad codes"
+
+ context "and there are no codes required" do
+ let(:resource) { nil }
+
+ it_behaves_like "can be answered without codes"
+ end
+ end
+end