From e6fccc28a7a51666e41dd91ac03171542eddf9e5 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Fri, 27 Sep 2024 10:16:59 +0100 Subject: [PATCH] Add project dataset select format Allow project owners to add questions with a pre set list of options which can be selected when extracting information from requests. There are also options to allow blank value or multiple values, these effect how the form select input is rendered in the extracting sidebar. Fixes #8117 --- .../alaveteli_pro/projects_controller.rb | 6 ++- .../projects/extracts_controller.rb | 2 +- .../projects/key_set/select_controller.js | 24 +++++++++++ app/models/dataset/key.rb | 16 +++++++- app/models/dataset/value.rb | 9 ++++ .../alaveteli_pro/projects/_key_set.html.erb | 5 +++ .../projects/_select_key.html.erb | 41 +++++++++++++++++++ .../dataset/keys/_select_key.html.erb | 4 ++ ...240926164308_add_options_to_dataset_key.rb | 5 +++ doc/CHANGES.md | 1 + spec/factories/dataset_keys.rb | 7 +++- spec/models/dataset/key_spec.rb | 13 +++++- spec/models/dataset/value_spec.rb | 41 +++++++++++++++++++ 13 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 app/javascript/controllers/projects/key_set/select_controller.js create mode 100644 app/views/alaveteli_pro/projects/_select_key.html.erb create mode 100644 app/views/projects/dataset/keys/_select_key.html.erb create mode 100644 db/migrate/20240926164308_add_options_to_dataset_key.rb diff --git a/app/controllers/alaveteli_pro/projects_controller.rb b/app/controllers/alaveteli_pro/projects_controller.rb index d4c589aede..93cdbbfbe7 100644 --- a/app/controllers/alaveteli_pro/projects_controller.rb +++ b/app/controllers/alaveteli_pro/projects_controller.rb @@ -141,7 +141,11 @@ def project_params when 'edit_key_set', 'update_key_set' params.fetch(:project, {}).permit( key_set_attributes: [ - :id, keys_attributes: %i[id title format order _destroy] + :id, keys_attributes: [ + :id, :title, :format, :order, :_destroy, options: [ + :select_allow_blank, :select_allow_muliple, { select_options: [] } + ] + ] ] ) when 'edit_contributors', 'update_contributors' diff --git a/app/controllers/projects/extracts_controller.rb b/app/controllers/projects/extracts_controller.rb index 9cc041ab24..1014ebb90e 100644 --- a/app/controllers/projects/extracts_controller.rb +++ b/app/controllers/projects/extracts_controller.rb @@ -71,7 +71,7 @@ def find_info_request def extract_params params.require(:extract).permit( - :dataset_key_set_id, values_attributes: [:dataset_key_id, :value] + :dataset_key_set_id, values_attributes: [:dataset_key_id, :value, value: []] ) end diff --git a/app/javascript/controllers/projects/key_set/select_controller.js b/app/javascript/controllers/projects/key_set/select_controller.js new file mode 100644 index 0000000000..11b02b356a --- /dev/null +++ b/app/javascript/controllers/projects/key_set/select_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "@hotwired/stimulus"; +import { streamUpdate } from "helpers/stream_update"; + +export default class extends Controller { + static targets = ["newInput"]; + static values = { name: String }; + + addOption(event) { + event.preventDefault(); + + const newValue = this.newInputTarget.value; + if (!newValue) return; + + const form = this.element.closest("form"); + streamUpdate(form, { [this.nameValue]: newValue }); + } + + removeOption(event) { + event.preventDefault(); + + const li = event.target.closest("li"); + if (li) li.remove(); + } +} diff --git a/app/models/dataset/key.rb b/app/models/dataset/key.rb index f48db23ce0..2d4aef091f 100644 --- a/app/models/dataset/key.rb +++ b/app/models/dataset/key.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20240926164308 # # Table name: dataset_keys # @@ -10,6 +10,7 @@ # order :integer # created_at :datetime not null # updated_at :datetime not null +# options :jsonb # ## @@ -24,6 +25,7 @@ class Dataset::Key < ApplicationRecord FORMATS = { text: { title: _('Text'), regexp: /\A.*\z/m }, + select: { title: _('Select'), regexp: /\A.*\z/m }, numeric: { title: _('Numeric'), regexp: /\A[0-9,%\+\-\s]*\z/ }, boolean: { title: _('Yes/No'), regexp: /\A(0|1)\z/ } }.freeze @@ -40,4 +42,16 @@ def self.format_options def format_regexp FORMATS[format.to_sym][:regexp] end + + def select_options + options.fetch('select_options', []) + end + + def select_allow_blank? + options.fetch('select_allow_blank', '0').to_i == 1 + end + + def select_allow_muliple? + options.fetch('select_allow_muliple', '0').to_i == 1 + end end diff --git a/app/models/dataset/value.rb b/app/models/dataset/value.rb index 735b99a3b0..c1488009cb 100644 --- a/app/models/dataset/value.rb +++ b/app/models/dataset/value.rb @@ -29,6 +29,15 @@ def title key.title end + def value=(new_value) + case key.format + when 'select' + super(Array(new_value).reject(&:blank?).join(', ')) + else + super + end + end + def mapped_value case key.format when 'boolean' diff --git a/app/views/alaveteli_pro/projects/_key_set.html.erb b/app/views/alaveteli_pro/projects/_key_set.html.erb index 632a1433e8..aa8184285f 100644 --- a/app/views/alaveteli_pro/projects/_key_set.html.erb +++ b/app/views/alaveteli_pro/projects/_key_set.html.erb @@ -24,6 +24,11 @@ data: { action: 'change->projects--key-set#updateKey' }, autocomplete: 'off' %> + + <% if lookup_context.exists?("alaveteli_pro/projects/_#{key.format}_key") %> + <%= render partial: "alaveteli_pro/projects/#{key.format}_key", + locals: { form: f, key: key } %> + <% end %>
diff --git a/app/views/alaveteli_pro/projects/_select_key.html.erb b/app/views/alaveteli_pro/projects/_select_key.html.erb new file mode 100644 index 0000000000..a60b9947fe --- /dev/null +++ b/app/views/alaveteli_pro/projects/_select_key.html.erb @@ -0,0 +1,41 @@ +<%= form.fields_for :options, key.options do |f| %> +
+ <%= f.label :select_allow_blank do %> + <%= f.check_box :select_allow_blank, checked: key.select_allow_blank?, autocomplete: 'off' %> + <%= _('Allow blank') %> + <% end %> +
+ +
+ <%= f.label :select_allow_muliple do %> + <%= f.check_box :select_allow_muliple, checked: key.select_allow_muliple?, autocomplete: 'off' %> + <%= _('Allow muliple') %> + <% end %> +
+ + <%= content_tag(:div, class: "project-key-set__key__value", data: { + controller: "projects--key-set--select", + projects__key_set__select_name_value: "#{f.object_name}[select_options][]" + }) do %> + <%= f.label :select_options, _('Options') %> + +
+
+ <%= text_field_tag "select_option", nil, data: { projects__key_set__select_target: 'newInput' } %> +
+
+ <%= button_tag _('Add'), class: 'button-tertiary postfix', data: { action: 'click->projects--key-set--select#addOption' } %> +
+
+ <% end %> +<% end %> diff --git a/app/views/projects/dataset/keys/_select_key.html.erb b/app/views/projects/dataset/keys/_select_key.html.erb new file mode 100644 index 0000000000..cfebb4a910 --- /dev/null +++ b/app/views/projects/dataset/keys/_select_key.html.erb @@ -0,0 +1,4 @@ +
+ <%= f.label :value, key.title %> + <%= f.select :value, key.select_options, { include_blank: key.select_allow_blank? }, multiple: key.select_allow_muliple? %> +
diff --git a/db/migrate/20240926164308_add_options_to_dataset_key.rb b/db/migrate/20240926164308_add_options_to_dataset_key.rb new file mode 100644 index 0000000000..2ef98b5b0d --- /dev/null +++ b/db/migrate/20240926164308_add_options_to_dataset_key.rb @@ -0,0 +1,5 @@ +class AddOptionsToDatasetKey < ActiveRecord::Migration[7.0] + def change + add_column :dataset_keys, :options, :jsonb, default: {} + end +end diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 116e1ab02d..d23e713bb7 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -2,6 +2,7 @@ ## Highlighted Features +* Add project dataset question "select" format (Graeme Porteous) * Fix script/mailin when multiple EXCEPTION_NOTIFICATIONS_TO addresses are specified (Graeme Porteous) * Add example logrotate configuration (Graeme Porteous) diff --git a/spec/factories/dataset_keys.rb b/spec/factories/dataset_keys.rb index 8af52b242f..3a39856d12 100644 --- a/spec/factories/dataset_keys.rb +++ b/spec/factories/dataset_keys.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20240926164308 # # Table name: dataset_keys # @@ -10,6 +10,7 @@ # order :integer # created_at :datetime not null # updated_at :datetime not null +# options :jsonb # FactoryBot.define do @@ -24,6 +25,10 @@ format { 'text' } end + trait :select do + format { 'select' } + end + trait :numeric do format { 'numeric' } end diff --git a/spec/models/dataset/key_spec.rb b/spec/models/dataset/key_spec.rb index e266abadb4..8352caf8b4 100644 --- a/spec/models/dataset/key_spec.rb +++ b/spec/models/dataset/key_spec.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20240926164308 # # Table name: dataset_keys # @@ -10,6 +10,7 @@ # order :integer # created_at :datetime not null # updated_at :datetime not null +# options :jsonb # require 'spec_helper' @@ -63,6 +64,8 @@ is_expected.not_to be_valid key.format = 'text' is_expected.to be_valid + key.format = 'select' + is_expected.to be_valid key.format = 'numeric' is_expected.to be_valid key.format = 'boolean' @@ -75,7 +78,8 @@ it 'returns title/format key hash' do is_expected.to eq( - { 'Text' => :text, 'Numeric' => :numeric, 'Yes/No' => :boolean } + { 'Text' => :text, 'Select' => :select, 'Numeric' => :numeric, + 'Yes/No' => :boolean } ) end end @@ -88,6 +92,11 @@ it { is_expected.to eq described_class::FORMATS[:text][:regexp] } end + context 'select format' do + let(:key) { FactoryBot.build(:dataset_key, :select) } + it { is_expected.to eq described_class::FORMATS[:select][:regexp] } + end + context 'numeric format' do let(:key) { FactoryBot.build(:dataset_key, :numeric) } it { is_expected.to eq described_class::FORMATS[:numeric][:regexp] } diff --git a/spec/models/dataset/value_spec.rb b/spec/models/dataset/value_spec.rb index 938a056309..a33c42758f 100644 --- a/spec/models/dataset/value_spec.rb +++ b/spec/models/dataset/value_spec.rb @@ -81,4 +81,45 @@ def invalid(value_to_test) end end end + + describe 'value=' do + context 'when format is a select' do + before do + value.key = FactoryBot.build(:dataset_key, :select) + end + + it 'accept an array' do + value.value = %w[a b] + expect(value.value).to eq('a, b') + end + + it 'ignores empty array values' do + value.value = %w[a] + [nil] + %w[c] + expect(value.value).to eq('a, c') + end + + it 'accepts as string' do + value.value = 'a' + expect(value.value).to eq('a') + end + end + end + + describe 'mapped_value' do + context 'when format is a boolean' do + before do + value.key = FactoryBot.build(:dataset_key, :boolean) + end + + it 'returns No for value 0' do + value.value = 0 + expect(value.mapped_value).to eq('No') + end + + it 'returns Yes for value 1' do + value.value = 1 + expect(value.mapped_value).to eq('Yes') + end + end + end end