Skip to content

Commit

Permalink
Restrict surveys access if code required (#12)
Browse files Browse the repository at this point in the history
* restrict surveys unless code

* add tests

* fix tests

* add overrides spec

* add intermediate table for resouce links and usage_count

* fix tests

* ensure no duplicated tokens on the same group

* fix seeds

* rename examples
  • Loading branch information
microstudi authored Apr 9, 2024
1 parent 4dd10fc commit e60439c
Show file tree
Hide file tree
Showing 21 changed files with 611 additions and 32 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
21 changes: 19 additions & 2 deletions app/models/decidim/anonymous_codes/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 29 additions & 0 deletions app/models/decidim/anonymous_codes/token_resource.rb
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions app/views/decidim/anonymous_codes/surveys/code_required.html.erb
Original file line number Diff line number Diff line change
@@ -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" %>

<div class="row columns">
<h2 class="section-heading"><%= translated_attribute questionnaire.title %></h2>
<div class="row">
<div class="columns large-6 medium-centered lead">
<%= decidim_sanitize_editor_admin translated_attribute questionnaire.description %>
</div>
</div>
</div>

<div class="row">
<div class="columns large-6 medium-centered">
<div class="card">
<div class="card__content">
<% unless questionnaire_for.try(:component)&.try(:published?) %>
<div class="section">
<div class="callout warning">
<p><%= t("questionnaire_not_published.body", scope: "decidim.forms.questionnaires.show") %></p>
</div>
</div>
<% end %>

<div class="section">
<div class="callout warning">
<h3 class="heading4"><%= t(".title") %></h3>
<p><%= t(".body") %></p>
</div>
<fi3eldset class="field">
<label class="field__label" for="code"><%= t(".label") %></label>
<div class="field__input">
<form method="get">
<%= text_field_tag :token, nil, class: "input", required: true %>
<button type="submit" class="button"><%= t(".submit") %></button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>

<% content_for :js_content do %>
<%= javascript_pack_tag "decidim_forms" %>
<% end %>
1 change: 1 addition & 0 deletions config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
67 changes: 44 additions & 23 deletions db/seeds.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions decidim-anonymous_codes.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
6 changes: 3 additions & 3 deletions lib/decidim/anonymous_codes/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion lib/decidim/anonymous_codes/test/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions lib/tasks/decidim_anonymous_codes_upgrade_tasks.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions spec/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading

0 comments on commit e60439c

Please sign in to comment.