From f8119d986afa50de43b157341b5ef03de550165f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Mon, 22 Apr 2024 10:50:29 +0200 Subject: [PATCH] Create tokens for groups (#15) * add controller * add back button * Update config/locales/en.yml * Update app/views/decidim/anonymous_codes/admin/codes/index.html.erb * add new method to codes_controller * add command createtokens and destroy method * fix merge * table headers * fix index page * add warning when inactive gorup * fix index empty status * add system token_codes_spec * relocate exporters * resolve review comments * use custom find_exporter * add create_tokens_spec * add tokens_form_spec * add codes_controller_spec * add checks to controller spec * fix codes_controller_spec get index check * allow to create tokens from the group creation * fix controller spec * add test case for GET #new controller method * add destroy method spec check * add create controller method spec * add create_tokens_job spec * resolve PR specs review comments --------- Co-authored-by: elviabth --- .../admin/create_code_group.rb | 1 + .../anonymous_codes/admin/create_tokens.rb | 26 ++++ .../surveys_controller_override.rb | 10 +- .../anonymous_codes/admin/codes_controller.rb | 22 +++- .../exporters/anonymous_tokens_pdf.rb | 33 ++++++ .../anonymous_tokens_pdf_controller_helper.rb | 14 +++ .../anonymous_codes/admin/code_group_form.rb | 2 + .../anonymous_codes/admin/tokens_form.rb | 13 ++ .../anonymous_codes/create_tokens_job.rb | 31 +++++ .../export_group_tokens_job.rb | 10 +- app/models/decidim/anonymous_codes/group.rb | 3 + .../admin/code_groups/_form.html.erb | 36 +++--- .../admin/code_groups/edit.html.erb | 12 +- .../admin/code_groups/index.html.erb | 12 +- .../admin/code_groups/new.html.erb | 16 ++- .../admin/codes/_form.html.erb | 14 +++ .../admin/codes/index.html.erb | 32 ++++- .../anonymous_codes/admin/codes/new.html.erb | 7 ++ .../_callout.html.erb | 5 +- config/locales/en.yml | 29 ++++- lib/decidim/anonymous_codes.rb | 7 +- lib/decidim/exporters/anonymous_tokens_pdf.rb | 31 ----- .../anonymous_tokens_pdf_controller_helper.rb | 12 -- spec/commands/admin/create_code_group_spec.rb | 26 +++- spec/commands/admin/create_tokens_spec.rb | 45 +++++++ .../controller/admin/codes_controller_spec.rb | 112 ++++++++++++++++++ spec/forms/admin/code_group_form_spec.rb | 67 ++++++----- spec/forms/admin/tokens_form_spec.rb | 36 ++++++ spec/jobs/create_tokens_job_spec.rb | 36 ++++++ spec/system/admin/access_code_groups_spec.rb | 18 +-- spec/system/admin/token_codes_spec.rb | 72 +++++++++++ spec/system/surveys_answering_spec.rb | 18 +++ 32 files changed, 668 insertions(+), 140 deletions(-) create mode 100644 app/commands/decidim/anonymous_codes/admin/create_tokens.rb create mode 100644 app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf.rb create mode 100644 app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf_controller_helper.rb create mode 100644 app/forms/decidim/anonymous_codes/admin/tokens_form.rb create mode 100644 app/jobs/decidim/anonymous_codes/create_tokens_job.rb create mode 100644 app/views/decidim/anonymous_codes/admin/codes/_form.html.erb create mode 100644 app/views/decidim/anonymous_codes/admin/codes/new.html.erb delete mode 100644 lib/decidim/exporters/anonymous_tokens_pdf.rb delete mode 100644 lib/decidim/exporters/anonymous_tokens_pdf_controller_helper.rb create mode 100644 spec/commands/admin/create_tokens_spec.rb create mode 100644 spec/controller/admin/codes_controller_spec.rb create mode 100644 spec/forms/admin/tokens_form_spec.rb create mode 100644 spec/jobs/create_tokens_job_spec.rb create mode 100644 spec/system/admin/token_codes_spec.rb diff --git a/app/commands/decidim/anonymous_codes/admin/create_code_group.rb b/app/commands/decidim/anonymous_codes/admin/create_code_group.rb index 8920229..a51372f 100644 --- a/app/commands/decidim/anonymous_codes/admin/create_code_group.rb +++ b/app/commands/decidim/anonymous_codes/admin/create_code_group.rb @@ -13,6 +13,7 @@ def call transaction do create_code_group! + CreateTokensJob.perform_later(code_group, form.num_tokens) if form.num_tokens.present? end broadcast(:ok) diff --git a/app/commands/decidim/anonymous_codes/admin/create_tokens.rb b/app/commands/decidim/anonymous_codes/admin/create_tokens.rb new file mode 100644 index 0000000..377a85f --- /dev/null +++ b/app/commands/decidim/anonymous_codes/admin/create_tokens.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + module Admin + class CreateTokens < Decidim::Command + def initialize(form, code_group) + @form = form + @code_group = code_group + end + + def call + return broadcast(:invalid) if form.invalid? + + CreateTokensJob.perform_later(code_group, form.num_tokens) + + broadcast(:ok) + end + + private + + attr_reader :form, :code_group + end + end + 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 index c2e67a4..033da39 100644 --- a/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb +++ b/app/controllers/concerns/decidim/anonymous_codes/surveys_controller_override.rb @@ -10,7 +10,7 @@ module SurveysControllerOverride next unless current_settings.allow_answers? && survey.open? next if visitor_already_answered? - if token_groups.any? + if token_groups.active.any? next if current_token&.available? if current_token.blank? @@ -22,6 +22,10 @@ module SurveysControllerOverride end render "decidim/anonymous_codes/surveys/code_required" end + + if token_groups.inactive.any? && current_user&.admin? + flash.now[:alert] = I18n.t("decidim.anonymous_codes.inactive_group", path: decidim_admin_anonymous_codes.edit_code_group_path(token_groups.inactive.first)).html_safe + end end after_action only: :answer do @@ -35,11 +39,11 @@ module SurveysControllerOverride private def token_groups - @token_groups ||= Decidim::AnonymousCodes::Group.where(resource: survey, active: true) + @token_groups ||= Decidim::AnonymousCodes::Group.where(resource: survey) end def current_token - @current_token ||= Decidim::AnonymousCodes::Token.where(group: token_groups).find_by(token: token_param) + @current_token ||= Decidim::AnonymousCodes::Token.where(group: token_groups.active).find_by(token: token_param) end def token_param diff --git a/app/controllers/decidim/anonymous_codes/admin/codes_controller.rb b/app/controllers/decidim/anonymous_codes/admin/codes_controller.rb index a46a9b0..b3a6bad 100644 --- a/app/controllers/decidim/anonymous_codes/admin/codes_controller.rb +++ b/app/controllers/decidim/anonymous_codes/admin/codes_controller.rb @@ -18,18 +18,34 @@ def index def new enforce_permission_to :create, :anonymous_code_token - # todo + @form = form(TokensForm).instance end def create enforce_permission_to :create, :anonymous_code_token - # todo + @form = form(TokensForm).from_params(params) + + CreateTokens.call(@form, code_group) do + on(:ok) do + flash[:notice] = I18n.t("codes.create.success", scope: "decidim.anonymous_codes.admin") + redirect_to code_group_codes_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("codes.create.invalid", scope: "decidim.anonymous_codes.admin") + render action: "new" + end + end end def destroy enforce_permission_to :destroy, :anonymous_code_token, token: token - # todo + token.destroy! + + flash[:notice] = I18n.t("codes.destroy.success", scope: "decidim.anonymous_codes.admin") + + redirect_to code_group_codes_path end def export diff --git a/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf.rb b/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf.rb new file mode 100644 index 0000000..aa3b9d0 --- /dev/null +++ b/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "wicked_pdf" + +module Decidim + module AnonymousCodes + module Exporters + # Inherits from abstract PDF exporter. This class is used to set + # the parameters used to create a PDF when exporting Survey Answers. + # + class AnonymousTokensPdf < Decidim::Exporters::PDF + def controller + @controller ||= AnonymousTokensPdfControllerHelper.new + end + + def template + "decidim/anonymous_codes/admin/export/tokens_pdf" + end + + def layout + "decidim/anonymous_codes/admin/export/pdf" + end + + def locals + { + code_group: collection&.first&.group, + collection: collection.map { |token| Decidim::AnonymousCodes::TokenSerializer.new(token).serialize } + } + end + end + end + end +end diff --git a/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf_controller_helper.rb b/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf_controller_helper.rb new file mode 100644 index 0000000..cb3947f --- /dev/null +++ b/app/exporters/decidim/anonymous_codes/exporters/anonymous_tokens_pdf_controller_helper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + module Exporters + # rubocop: disable Rails/ApplicationController + # A dummy controller to render views while exporting questionnaires + class AnonymousTokensPdfControllerHelper < ActionController::Base + # rubocop: enable Rails/ApplicationController + helper Decidim::TranslationsHelper + end + end + end +end diff --git a/app/forms/decidim/anonymous_codes/admin/code_group_form.rb b/app/forms/decidim/anonymous_codes/admin/code_group_form.rb index 2ab0feb..48ac1a3 100644 --- a/app/forms/decidim/anonymous_codes/admin/code_group_form.rb +++ b/app/forms/decidim/anonymous_codes/admin/code_group_form.rb @@ -11,10 +11,12 @@ class CodeGroupForm < Decidim::Form attribute :active, Boolean, default: true attribute :max_reuses, Integer, default: 1 attribute :resource_id, Integer + attribute :num_tokens, Integer validates :title, translatable_presence: true validates :expires_at, date: { after: Time.current }, if: ->(form) { form.expires_at.present? && form.active } validates :max_reuses, presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :num_tokens, numericality: { only_integer: true, greater_than: 0 }, if: ->(form) { form.num_tokens.present? } def resource @resource ||= Decidim::Surveys::Survey.find_by(id: resource_id) diff --git a/app/forms/decidim/anonymous_codes/admin/tokens_form.rb b/app/forms/decidim/anonymous_codes/admin/tokens_form.rb new file mode 100644 index 0000000..892fdf4 --- /dev/null +++ b/app/forms/decidim/anonymous_codes/admin/tokens_form.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + module Admin + class TokensForm < Decidim::Form + attribute :num_tokens, Integer, default: 1 + + validates :num_tokens, presence: true, numericality: { only_integer: true, greater_than: 0 } + end + end + end +end diff --git a/app/jobs/decidim/anonymous_codes/create_tokens_job.rb b/app/jobs/decidim/anonymous_codes/create_tokens_job.rb new file mode 100644 index 0000000..988d83c --- /dev/null +++ b/app/jobs/decidim/anonymous_codes/create_tokens_job.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Decidim + module AnonymousCodes + class CreateTokensJob < ApplicationJob + def perform(code_group, num_tokens) + @code_group = code_group + num_tokens.times do + create_token! + end + end + + private + + attr_reader :code_group + + def create_token! + token = new_token while token.blank? + token.save! + end + + def new_token + token = Decidim::AnonymousCodes::Token.new( + token: Decidim::AnonymousCodes.token_generator, + group: code_group + ) + token if token.valid? + end + end + end +end diff --git a/app/jobs/decidim/anonymous_codes/export_group_tokens_job.rb b/app/jobs/decidim/anonymous_codes/export_group_tokens_job.rb index 994bc72..c0e3e10 100644 --- a/app/jobs/decidim/anonymous_codes/export_group_tokens_job.rb +++ b/app/jobs/decidim/anonymous_codes/export_group_tokens_job.rb @@ -6,7 +6,15 @@ class ExportGroupTokensJob < ApplicationJob queue_as :exports def perform(user, group, format) - export_data = Decidim::Exporters.find_exporter(format).new(group.tokens, TokenSerializer).export + exporter = format == "AnonymousTokensPdf" ? Decidim::AnonymousCodes::Exporters::AnonymousTokensPdf : Decidim::Exporters.find_exporter(format) + + if exporter + Rails.logger.info "Exporting tokens for group #{group.id} in #{format} format" + else + Rails.logger.error "Cannot export tokens for group #{group.id}: Unknown format: #{format}" + end + + export_data = exporter.new(group.tokens, TokenSerializer).export ExportMailer.export(user, "tokens_for_group_#{group.id}", export_data).deliver_now end diff --git a/app/models/decidim/anonymous_codes/group.rb b/app/models/decidim/anonymous_codes/group.rb index 097746e..a75919d 100644 --- a/app/models/decidim/anonymous_codes/group.rb +++ b/app/models/decidim/anonymous_codes/group.rb @@ -14,6 +14,9 @@ class Group < ApplicationRecord has_many :tokens, class_name: "Decidim::AnonymousCodes::Token", dependent: :destroy belongs_to :resource, polymorphic: true, optional: true + scope :active, -> { where(active: true) } + scope :inactive, -> { where(active: false) } + def self.for(organization) where(organization: organization) end diff --git a/app/views/decidim/anonymous_codes/admin/code_groups/_form.html.erb b/app/views/decidim/anonymous_codes/admin/code_groups/_form.html.erb index 633b5f3..407cc7b 100644 --- a/app/views/decidim/anonymous_codes/admin/code_groups/_form.html.erb +++ b/app/views/decidim/anonymous_codes/admin/code_groups/_form.html.erb @@ -1,27 +1,19 @@ -
-
-

<%= title %>

-
- -
-
- <%= form.translated :text_field, :title, label: t(".title") %> -
+
+ <%= form.translated :text_field, :title, label: t(".title") %> +
-
- <%= form.datetime_field :expires_at, label: t(".expires_at") %> -
+
+ <%= form.datetime_field :expires_at, label: t(".expires_at") %> +
-
- <%= form.check_box :active, label: t(".active") %> -
+
+ <%= form.check_box :active, label: t(".active") %> +
-
- <%= form.number_field :max_reuses, label: t(".max_reuses") %> -
+
+ <%= form.number_field :max_reuses, label: t(".max_reuses") %> +
-
- <%= form.select :resource_id, surveys, include_blank: true, label: t(".resource") %> -
-
+
+ <%= form.select :resource_id, surveys, include_blank: true, label: t(".resource") %>
diff --git a/app/views/decidim/anonymous_codes/admin/code_groups/edit.html.erb b/app/views/decidim/anonymous_codes/admin/code_groups/edit.html.erb index ffbd79c..8326a6d 100644 --- a/app/views/decidim/anonymous_codes/admin/code_groups/edit.html.erb +++ b/app/views/decidim/anonymous_codes/admin/code_groups/edit.html.erb @@ -1,5 +1,15 @@ <%= decidim_form_for(@form, html: { class: "form edit_code_group" }) do |f| %> - <%= render partial: "form", object: f, locals: { title: t(".title") } %> +
+ + +
+ <%= render partial: "form", object: f %> +
+
<%= f.submit "update" %> diff --git a/app/views/decidim/anonymous_codes/admin/code_groups/index.html.erb b/app/views/decidim/anonymous_codes/admin/code_groups/index.html.erb index 95b6b73..c69d9b6 100644 --- a/app/views/decidim/anonymous_codes/admin/code_groups/index.html.erb +++ b/app/views/decidim/anonymous_codes/admin/code_groups/index.html.erb @@ -25,15 +25,15 @@ <% groups.each do |group| %> <%= translated_attribute(group.title) %> - <%= t("booleans.#{group.active}") %> - <%= group.expires_at.present? ? ("#{l(group.expires_at, format: :decidim_short)}".html_safe) : t(".never") %> + "><%= t("booleans.#{group.active}") %> + <%= group.expires_at.present? ? ("#{l(group.expires_at, format: :decidim_short)}".html_safe) : content_tag(:em, t(".never")) %> <%= "#{group.tokens.used.count} / #{group.tokens_count}" %> <%= group.max_reuses %> <%= icon_link_to "eye", resource_path(group), t("actions.preview", scope: "decidim.anonymous_codes.admin"), class: "action-icon--preview#{group&.resource ? '':' invisible'}", target: :_blank %> - <%= icon_link_to "list", code_group_codes_path(group), t("actions.list_tokens", scope: "decidim.anonymous_codes.admin"), class: "action-icon--preview#{group&.resource ? '':' invisible'}" %> - <%= icon_link_to "pencil", edit_code_group_path(group.id), t("actions.edit", scope: "decidim.anonymous_codes.admin"), class: "action-icon--edit#{allowed_to?(:update, :anonymous_code_group, code_group: group) ? '' : ' invisible'}" %> - <%= icon_link_to "circle-x", code_group_path(group.id), t("actions.destroy", scope: "decidim.anonymous_codes.admin"), method: :delete, class: "action-icon--remove#{allowed_to?(:destroy, :anonymous_code_group, code_group: group) ? '' : ' invisible'}", data: { confirm: t("actions.confirm_destroy", scope: "decidim.anonymous_codes.admin") } %> + <%= icon_link_to "list", code_group_codes_path(group), t("actions.list_tokens", scope: "decidim.anonymous_codes.admin"), class: "action-icon--preview#{allowed_to?(:view, :anonymous_code_token) ? '':' invisible'}" %> + <%= icon_link_to "pencil", edit_code_group_path(group), t("actions.edit", scope: "decidim.anonymous_codes.admin"), class: "action-icon--edit#{allowed_to?(:update, :anonymous_code_group, code_group: group) ? '' : ' invisible'}" %> + <%= icon_link_to "circle-x", code_group_path(group), t("actions.destroy", scope: "decidim.anonymous_codes.admin"), method: :delete, class: "action-icon--remove#{allowed_to?(:destroy, :anonymous_code_group, code_group: group) ? '' : ' invisible'}", data: { confirm: t("actions.confirm_destroy", scope: "decidim.anonymous_codes.admin") } %> <% end %> @@ -44,7 +44,7 @@ <% else %>
<%= t("code_groups.index.no_access_code_groups_records", scope: "decidim.anonymous_codes.admin") %> -

<%= t("code_groups.index.start_by", scope: "decidim.anonymous_codes.admin", button: link_to(t("code_groups.index.new_access_code_group_button", scope: "decidim.anonymous_codes.admin"), new_code_group_path)) %>

+

<%= link_to t("code_groups.index.start_by", scope: "decidim.anonymous_codes.admin"), new_code_group_path %>

<% end %>
diff --git a/app/views/decidim/anonymous_codes/admin/code_groups/new.html.erb b/app/views/decidim/anonymous_codes/admin/code_groups/new.html.erb index 44b1d8c..3994cf3 100644 --- a/app/views/decidim/anonymous_codes/admin/code_groups/new.html.erb +++ b/app/views/decidim/anonymous_codes/admin/code_groups/new.html.erb @@ -1,5 +1,19 @@ <%= decidim_form_for(@form, url: code_groups_path, html: { class: "form new_code_group" }) do |f| %> - <%= render partial: "form", object: f, locals: { title: t(".title") } %> +
+ + +
+ <%= render partial: "form", object: f %> + +
+ <%= f.number_field :num_tokens, label: t(".num_tokens") %> +
+
+
<%= f.submit "create" %> diff --git a/app/views/decidim/anonymous_codes/admin/codes/_form.html.erb b/app/views/decidim/anonymous_codes/admin/codes/_form.html.erb new file mode 100644 index 0000000..1414989 --- /dev/null +++ b/app/views/decidim/anonymous_codes/admin/codes/_form.html.erb @@ -0,0 +1,14 @@ +
+ + +
+
+ <%= form.number_field :num_tokens, label: t(".number_of_tokens_to_generate") %> +
+
+
diff --git a/app/views/decidim/anonymous_codes/admin/codes/index.html.erb b/app/views/decidim/anonymous_codes/admin/codes/index.html.erb index c2a3d06..87db4ad 100644 --- a/app/views/decidim/anonymous_codes/admin/codes/index.html.erb +++ b/app/views/decidim/anonymous_codes/admin/codes/index.html.erb @@ -6,16 +6,44 @@
- <%= tokens.all %> +
+ + + + + + + + + + + + <% @tokens.each do |token| %> + <% serialized = Decidim::AnonymousCodes::TokenSerializer.new(token).serialize %> + + + + + + + + <% end %> + +
<%= t(".token") %><%= t(".available") %><%= t(".used") %><%= t(".usage_count") %>
<%= token.token %><%= t("booleans.#{token.available?}") %><%= t("booleans.#{token.used?}") %><%= token.usage_count %> + <%= icon_link_to "eye", serialized[:resource_url], t("actions.preview", scope: "decidim.anonymous_codes.admin"), class: "#{serialized[:resource_url].present? ? '' : ' invisible'}", target: "_blank" %> + <%= icon_link_to "circle-x", code_group_code_path(code_group, token), t("actions.destroy", scope: "decidim.anonymous_codes.admin"), method: :delete, class: "action-icon--remove#{allowed_to?(:destroy, :anonymous_code_token, token: token) ? '' : ' invisible'}", data: { confirm: t("actions.confirm_destroy_code", scope: "decidim.anonymous_codes.admin") } %> +
+ <%= paginate @tokens, theme: "decidim" %> +
diff --git a/app/views/decidim/anonymous_codes/admin/codes/new.html.erb b/app/views/decidim/anonymous_codes/admin/codes/new.html.erb new file mode 100644 index 0000000..c485d59 --- /dev/null +++ b/app/views/decidim/anonymous_codes/admin/codes/new.html.erb @@ -0,0 +1,7 @@ +<%= decidim_form_for(@form, url: code_group_codes_path, html: { class: "form new_code_group_code" }) do |f| %> + <%= render partial: "form", object: f, locals: { title: t(".title", group: translated_attribute(code_group.title)) } %> + +
+ <%= f.submit t(".button_generate_tokens") %> +
+<% end %> diff --git a/app/views/decidim/anonymous_codes/admin/surveys_component_settings/_callout.html.erb b/app/views/decidim/anonymous_codes/admin/surveys_component_settings/_callout.html.erb index 3b2cf42..356ef05 100644 --- a/app/views/decidim/anonymous_codes/admin/surveys_component_settings/_callout.html.erb +++ b/app/views/decidim/anonymous_codes/admin/surveys_component_settings/_callout.html.erb @@ -4,7 +4,10 @@

<%= t(".group_locked") %>

<%= t(".group_locked_desc") %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index 302ebaa..179960b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -11,11 +11,12 @@ en: admin: exports: formats: - AnonymousTokensPDF: PDF + AnonymousTokensPdf: PDF anonymous_codes: admin: actions: confirm_destroy: Are you sure you want to delete this access code group? + confirm_destroy_code: Are you sure you want to delete this code? destroy: Delete edit: Edit list_tokens: List tokens @@ -42,19 +43,36 @@ en: never: Never new_access_code_group_button: New access code group no_access_code_groups_records: No access code groups available. - num_of_tokens: Num. of Tokens - start_by: Start by adding an access code group from the "%{button}" button. + num_of_tokens: Num. of tokens + start_by: Start by adding an access code group. title: Access code groups new: + num_tokens: Num. of tokens to generate (you can do it later too) title: New access code group update: invalid: There was a problem updating this access code group success: Access code group successfully updated codes: + create: + invalid: There was a problem generating the codes + success: Codes are being generated in the background. Please wait a few + seconds and refresh the page. + destroy: + success: Access code token successfully destroyed + form: + number_of_tokens_to_generate: Number of tokens to generate index: + available: Available? back: Back to groups + back_to_codes: Back to codes new_codes_button: Generate new codes title: Codes for group %{group} + token: Token + usage_count: Num. of uses + used: Used? + new: + button_generate_tokens: Generate Tokens + title: Generate tokens for "%{group}" export: tokens_pdf: expires: Expires? %{expires} @@ -75,8 +93,13 @@ en: group_unlocked_desc: Note that "Allow unregistered users to answer" will still apply if you choose to use this option. groups: 'Edit groups:' + inactive: "- ⚠️ This group is inactive! You need to activate it in order + to restrict access to the survey." new_group: "\U0001F449 Create answer codes here" expired_code: The introduced code has expired. Please try again. + inactive_group: Warning: This survey is restricted with codes, + but the group is inactive. You can activate it in the access + code groups section. invalid_code: The introduced code is invalid. Please try again. surveys: code_required: diff --git a/lib/decidim/anonymous_codes.rb b/lib/decidim/anonymous_codes.rb index bbc0c51..aff4b9a 100644 --- a/lib/decidim/anonymous_codes.rb +++ b/lib/decidim/anonymous_codes.rb @@ -18,7 +18,7 @@ module AnonymousCodes end config_accessor :export_formats do - ENV.fetch("ANONYMOUS_CODES_EXPORT_FORMATS", "CSV JSON Excel AnonymousTokensPDF").split + ENV.fetch("ANONYMOUS_CODES_EXPORT_FORMATS", "CSV JSON Excel AnonymousTokensPdf").split end def self.token_generator(length = nil) @@ -31,9 +31,4 @@ def self.token_generator(length = nil) end end end - - module Exporters - autoload :AnonymousTokensPDF, "decidim/exporters/anonymous_tokens_pdf" - autoload :AnonymousTokensPDFControllerHelper, "decidim/exporters/anonymous_tokens_pdf_controller_helper" - end end diff --git a/lib/decidim/exporters/anonymous_tokens_pdf.rb b/lib/decidim/exporters/anonymous_tokens_pdf.rb deleted file mode 100644 index 298585e..0000000 --- a/lib/decidim/exporters/anonymous_tokens_pdf.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require "wicked_pdf" - -module Decidim - module Exporters - # Inherits from abstract PDF exporter. This class is used to set - # the parameters used to create a PDF when exporting Survey Answers. - # - class AnonymousTokensPDF < PDF - def controller - @controller ||= AnonymousTokensPDFControllerHelper.new - end - - def template - "decidim/anonymous_codes/admin/export/tokens_pdf" - end - - def layout - "decidim/anonymous_codes/admin/export/pdf" - end - - def locals - { - code_group: collection&.first&.group, - collection: collection.map { |token| Decidim::AnonymousCodes::TokenSerializer.new(token).serialize } - } - end - end - end -end diff --git a/lib/decidim/exporters/anonymous_tokens_pdf_controller_helper.rb b/lib/decidim/exporters/anonymous_tokens_pdf_controller_helper.rb deleted file mode 100644 index 1ef37a9..0000000 --- a/lib/decidim/exporters/anonymous_tokens_pdf_controller_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Exporters - # rubocop: disable Rails/ApplicationController - # A dummy controller to render views while exporting questionnaires - class AnonymousTokensPDFControllerHelper < ActionController::Base - # rubocop: enable Rails/ApplicationController - helper Decidim::TranslationsHelper - end - end -end diff --git a/spec/commands/admin/create_code_group_spec.rb b/spec/commands/admin/create_code_group_spec.rb index f1be537..8506b33 100644 --- a/spec/commands/admin/create_code_group_spec.rb +++ b/spec/commands/admin/create_code_group_spec.rb @@ -6,8 +6,8 @@ module Decidim module AnonymousCodes module Admin describe CreateCodeGroup do - let(:current_organization) { create(:organization) } - let(:current_user) { create(:user, :confirmed, :admin, organization: current_organization) } + let(:organization) { create(:organization) } + let(:current_user) { create(:user, :confirmed, :admin, organization: organization) } let(:form_params) do { title_en: "Sample Title", @@ -15,14 +15,16 @@ module Admin title_es: "Titulo de ejemplo", expires_at: 1.day.from_now, active: true, - max_reuses: 10 + max_reuses: 10, + num_tokens: num_tokens } end + let(:num_tokens) { nil } let(:form) do CodeGroupForm.from_params( form_params ).with_context( - current_organization: current_organization + current_organization: organization ) end let(:command) { described_class.new(form) } @@ -45,7 +47,21 @@ module Admin describe "when the form is valid" do it "broadcasts ok" do - expect { command.call }.to change(Group, :count).by(1).and broadcast(:ok) + perform_enqueued_jobs do + expect { command.call }.to change(Group, :count).by(1).and broadcast(:ok) + expect(Token.count).to eq(0) + end + end + + context "and num_tokens is specified" do + let(:num_tokens) { 10 } + + it "broadcasts ok" do + perform_enqueued_jobs do + expect { command.call }.to change(Token, :count).by(10).and broadcast(:ok) + expect(Group.count).to eq(1) + end + end end end end diff --git a/spec/commands/admin/create_tokens_spec.rb b/spec/commands/admin/create_tokens_spec.rb new file mode 100644 index 0000000..f5c7a14 --- /dev/null +++ b/spec/commands/admin/create_tokens_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module AnonymousCodes + module Admin + describe CreateTokens, type: :command do + let(:form) { double("FormObject", invalid?: false, num_tokens: 10) } + let(:code_group) { double("CodeGroup") } + let(:command) { described_class.new(form, code_group) } + + describe "#call" do + before do + allow(CreateTokensJob).to receive(:perform_later) + command.call + end + + context "when the form is valid" do + it "queues a background job to create tokens" do + expect(CreateTokensJob).to have_received(:perform_later).with(code_group, 10) + end + + it "broadcasts :ok" do + expect(command).to broadcast(:ok) + end + end + + context "when the form is invalid" do + let(:form) { double("FormObject", invalid?: true) } + + it "broadcasts :invalid" do + expect(command).to broadcast(:invalid) + end + + it "does not queue a background job" do + expect(CreateTokensJob).not_to have_received(:perform_later) + expect(CreateTokensJob).not_to have_received(:perform_later).with(code_group, 10) + end + end + end + end + end + end +end diff --git a/spec/controller/admin/codes_controller_spec.rb b/spec/controller/admin/codes_controller_spec.rb new file mode 100644 index 0000000..16bee68 --- /dev/null +++ b/spec/controller/admin/codes_controller_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module AnonymousCodes + module Admin + describe CodesController, type: :controller do + routes { Decidim::AnonymousCodes::AdminEngine.routes } + + let(:current_user) { create(:user, :confirmed, :admin, organization: current_organization) } + let(:current_organization) { create(:organization) } + let!(:group) { create(:anonymous_codes_group, expires_at: 1.day.from_now, active: true, max_reuses: 10, organization: current_organization) } + let(:code_group) { create(:anonymous_codes_group, organization: current_organization) } + + before do + request.env["decidim.current_organization"] = current_organization + sign_in current_user, scope: :user + end + + describe "GET #index" do + let!(:token1) { create(:anonymous_codes_token, group: code_group, created_at: 2.days.ago) } + let!(:token2) { create(:anonymous_codes_token, group: code_group, created_at: 1.day.ago) } + + it "enforces permission to view anonymous code tokens" do + expect(controller).to receive(:enforce_permission_to).with(:view, :anonymous_code_token) + get :index, params: { code_group_id: code_group.id } + end + + it "assigns @tokens with paginated tokens ordered by creation date" do + get :index, params: { code_group_id: code_group.id } + + expect(assigns(:tokens)).to eq([token2, token1]) + end + + it "renders the index template" do + get :index, params: { code_group_id: code_group.id } + expect(response).to render_template(:index) + end + end + + describe "GET #new" do + it "enforces permission to create anonymous code tokens" do + expect(controller).to receive(:enforce_permission_to).with(:create, :anonymous_code_token) + get :new, params: { code_group_id: code_group.id } + end + + it "assigns a new instance of TokensForm to @form" do + get :new, params: { code_group_id: code_group.id } + expect(assigns(:form)).to be_an_instance_of(TokensForm) + end + + it "renders the new template" do + get :new, params: { code_group_id: code_group.id } + expect(response).to render_template(:new) + end + end + + describe "POST #create" do + context "with valid parameters" do + let(:valid_params) do + { + code_group_id: code_group.id, + num_tokens: 5 + } + end + + it "enqueues a job to create tokens and redirects to code group codes path" do + expect(CreateTokensJob).to receive(:perform_later).with(code_group, valid_params[:num_tokens]) + + post :create, params: valid_params + + expect(response).to redirect_to(code_group_codes_path(code_group)) + expect(flash[:notice]).to be_present + end + end + + context "with invalid parameters" do + let(:invalid_params) do + { + code_group_id: code_group.id, + num_tokens: 0 + } + end + + it "does not enqueue a job and renders the new template with an alert message" do + expect(CreateTokensJob).not_to receive(:perform_later) + + post :create, params: invalid_params + + expect(response).to render_template("new") + expect(flash[:alert]).to be_present + end + end + end + + describe "DELETE #destroy" do + let(:token) { create(:anonymous_codes_token, group: code_group, created_at: 2.days.ago) } + let!(:group) { create(:anonymous_codes_group, expires_at: 1.day.from_now, active: true, max_reuses: 10, organization: current_organization) } + let(:code_group) { create(:anonymous_codes_group, organization: current_organization) } + + it "destroys the token" do + delete :destroy, params: { code_group_id: code_group.id, id: token.id } + expect(response).to redirect_to(code_group_codes_path) + expect(flash[:notice]).to be_present + expect { token.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + end +end diff --git a/spec/forms/admin/code_group_form_spec.rb b/spec/forms/admin/code_group_form_spec.rb index 2496ccb..a166ef8 100644 --- a/spec/forms/admin/code_group_form_spec.rb +++ b/spec/forms/admin/code_group_form_spec.rb @@ -6,6 +6,7 @@ module Decidim module AnonymousCodes module Admin describe CodeGroupForm do + subject { form } let(:current_organization) { create(:organization) } let(:current_user) { create :user, organization: current_organization } let(:form_params) do @@ -13,11 +14,16 @@ module Admin title_en: "Sample Title", title_ca: "Títol de la mostra", title_es: "Titulo de ejemplo", - expires_at: 1.day.from_now, - active: true, - max_reuses: 10 + expires_at: expires, + active: active, + max_reuses: max_reuses, + num_tokens: num_tokens } end + let(:active) { true } + let(:expires) { 1.day.from_now } + let(:max_reuses) { 10 } + let(:num_tokens) { nil } let(:form) do CodeGroupForm.from_params( form_params @@ -27,51 +33,28 @@ module Admin end describe "validations" do - context "when all attributes are valid" do - it "is valid" do - expect(form).to be_valid - end - end + it { is_expected.to be_valid } context "when max reuses is less than 1" do - let(:form_params) do - { - title_en: "Sample Title", - title_ca: "Títol de la mostra", - title_es: "Titulo de ejemplo", - expires_at: 1.day.from_now, - active: true, - max_reuses: 0 - } - end + let(:max_reuses) { 0 } it "is invalid" do - expect(form).not_to be_valid + expect(subject).to be_invalid expect(form.errors[:max_reuses]).to include("must be greater than 0") end end context "when expires_at is in the past" do - let(:form_params) do - { - title_en: "Sample Title", - expires_at: expires, - active: active, - max_reuses: 10 - } - end let(:active) { false } let(:expires) { 1.day.ago } - it "is valid" do - expect(form).to be_valid - end + it { is_expected.to be_valid } context "when active" do let(:active) { true } it "is invalid" do - expect(form).to be_invalid + expect(subject).to be_invalid expect(form.errors[:expires_at].first).to include("must be after") end end @@ -79,9 +62,25 @@ module Admin context "and date is not present" do let(:expires) { "" } - it "is valid" do - expect(form).to be_valid - end + it { is_expected.to be_valid } + end + end + + context "when num_tokens" do + let(:num_tokens) { 10 } + + it { is_expected.to be_valid } + + context "and num_tokens is zero" do + let(:num_tokens) { 0 } + + it { is_expected.to be_invalid } + end + + context "and num_tokens is less than zero" do + let(:num_tokens) { -1 } + + it { is_expected.to be_invalid } end end end diff --git a/spec/forms/admin/tokens_form_spec.rb b/spec/forms/admin/tokens_form_spec.rb new file mode 100644 index 0000000..a9f4814 --- /dev/null +++ b/spec/forms/admin/tokens_form_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module AnonymousCodes + module Admin + describe TokensForm, type: :form do + let(:form) { described_class.new } + + it "is valid with valid attributes" do + form.num_tokens = 5 + expect(form).to be_valid + end + + it "is invalid without num_tokens" do + form.num_tokens = nil + expect(form).to be_invalid + expect(form.errors[:num_tokens]).to include("can't be blank") + end + + it "is invalid with num_tokens not being an integer" do + form.num_tokens = "abc" + expect(form).to be_invalid + expect(form.errors[:num_tokens]).to include("must be greater than 0") + end + + it "is invalid with num_tokens less than or equal to 0" do + form.num_tokens = 0 + expect(form).to be_invalid + expect(form.errors[:num_tokens]).to include("must be greater than 0") + end + end + end + end +end diff --git a/spec/jobs/create_tokens_job_spec.rb b/spec/jobs/create_tokens_job_spec.rb new file mode 100644 index 0000000..ffe38be --- /dev/null +++ b/spec/jobs/create_tokens_job_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module AnonymousCodes + describe CreateTokensJob do + subject { described_class.perform_now(code_group, num_tokens) } + + let(:organization) { create(:organization) } + let(:code_group) { create :anonymous_codes_group } + let(:num_tokens) { 2 } + let(:token1) { "TOKEN1" } + let(:token2) { "TOKEN2" } + let(:token3) { "TOKEN3" } + + before do + allow(Decidim::AnonymousCodes).to receive(:token_generator).and_return(token1, token2, token3) + end + + it "generates 2 tokens" do + expect { subject }.to change(Token, :count).by(2) + expect(Token.all.pluck(:token)).to match_array([token1, token2]) + end + + context "when token is repeated" do + let!(:existing_token) { create(:anonymous_codes_token, token: token2, group: code_group) } + + it "skips the repeated" do + expect { subject }.to change(Token, :count).by(2) + expect(Token.all.pluck(:token)).to match_array([token1, token2, token3]) + end + end + end + end +end diff --git a/spec/system/admin/access_code_groups_spec.rb b/spec/system/admin/access_code_groups_spec.rb index 6adc6ae..24f082f 100644 --- a/spec/system/admin/access_code_groups_spec.rb +++ b/spec/system/admin/access_code_groups_spec.rb @@ -77,21 +77,21 @@ fill_in_i18n(:code_group_title, "#code_group-title-tabs", en: "New Group", es: "Nuevo Grupo", ca: "Nou Grup") check "Active" fill_in "Re-use max", with: 10 + fill_in "Num. of tokens to generate (you can do it later too)", with: 10 select "#{component.participatory_space.title["en"]} :: #{component.name["en"]}", from: "code_group_resource_id" - click_on "create" + perform_enqueued_jobs do + click_on "create" + end expect(page).to have_content("Access code group successfully created") expect(page).to have_content("New access code group") - last_group = Decidim::AnonymousCodes::Group.last - expect(last_group.title["en"]).to eq("New Group") - expect(last_group.max_reuses).to eq(10) - expect(last_group.active).to be(true) - - visit decidim_admin_anonymous_codes.code_groups_path - expect(page).to have_content("New Group") - expect(page).to have_content("Never") + within find("tr", text: "New Group") do + expect(page).to have_content("0 / 10") + expect(page).to have_content("Yes") + expect(page).to have_content("Never") + end end it "destroys existing access code group" do diff --git a/spec/system/admin/token_codes_spec.rb b/spec/system/admin/token_codes_spec.rb new file mode 100644 index 0000000..f22fa9f --- /dev/null +++ b/spec/system/admin/token_codes_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Token codes", type: :system do + let(:organization) { component.organization } + let(:user) { create :user, :admin, :confirmed, organization: organization } + let(:component) { survey.component } + let!(:survey) { create(:survey) } + let!(:existing_group) { create(:anonymous_codes_group, title: { en: "Existing group" }, organization: organization, resource: survey) } + let!(:existing_empty_group) { create(:anonymous_codes_group, title: { en: "Existing empty group" }, organization: organization, resource: survey) } + let!(:anonymous_codes_token1) { create(:anonymous_codes_token, group: existing_group) } + let!(:anonymous_codes_token2) { create(:anonymous_codes_token, :used, group: existing_group) } + let(:last_token) { Decidim::AnonymousCodes::Token.last } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_anonymous_codes.code_groups_path + + within find("tr", text: existing_empty_group.title["en"]) do + expect(page).to have_link("List") + click_link "List" + end + end + + it "generates tokens for access code groups" do + expect(page).to have_content("Codes for group") + expect(page).to have_content("Token") + expect(page).to have_content("Available?") + expect(page).to have_content("Used?") + expect(page).to have_content("Num. of uses") + + click_link "Generate new codes" + + fill_in "Number of tokens to generate", with: 10 + perform_enqueued_jobs do + click_on "Generate Tokens" + end + + expect(page).to have_content("Codes are being generated in the background. Please wait a few seconds and refresh the page.") + + click_link "Back to groups" + within find("tr", text: "Existing empty group") do + expect(page).to have_content("0 / 10") + end + within find("tr", text: "Existing group") do + expect(page).to have_content("1 / 2") + end + end + + it "destroys an existing token code" do + click_link "Generate new codes" + + fill_in "Number of tokens to generate", with: 5 + + perform_enqueued_jobs do + click_on "Generate Tokens" + end + + expect(page).to have_content(last_token.reload.token) + + within find("tr", text: last_token.reload.token) do + expect(page).to have_link("Delete") + accept_confirm do + click_link "Delete", match: :first + end + end + + expect(page).to have_content("Access code token successfully destroyed") + end +end diff --git a/spec/system/surveys_answering_spec.rb b/spec/system/surveys_answering_spec.rb index 21bb433..1e77ae1 100644 --- a/spec/system/surveys_answering_spec.rb +++ b/spec/system/surveys_answering_spec.rb @@ -91,8 +91,26 @@ def visit_component it_behaves_like "form requires codes" context "and code group is inactive" do + before do + login_as user, scope: :user + visit_component + end + let(:active) { false } + it "has no alert callout" do + expect(page).not_to have_css(".callout.alert") + end + + context "when user is an admin" do + let(:user) { create :user, :confirmed, :admin, organization: organization } + + it "has a alert callout" do + expect(page).to have_css(".callout.alert") + expect(page).to have_content("This survey is restricted with codes, but the group is inactive") + end + end + it_behaves_like "can be answered without codes" end