From 0052ab8f6abdea116e7ec973958aff711418d015 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 26 Aug 2024 00:49:00 -0700 Subject: [PATCH 01/80] Pattern Search controllers for every searchable model --- .../comments/pattern_search_controller.rb | 15 ++++++ .../pattern_search_controller.rb | 15 ++++++ .../herbaria/pattern_search_controller.rb | 15 ++++++ .../pattern_search_controller.rb | 15 ++++++ .../locations/pattern_search_controller.rb | 15 ++++++ .../names/pattern_search_controller.rb | 46 +++++++++++++++++++ .../observations/pattern_search_controller.rb | 15 ++++++ .../projects/pattern_search_controller.rb | 15 ++++++ .../pattern_search_controller.rb | 15 ++++++ .../users/pattern_search_controller.rb | 15 ++++++ 10 files changed, 181 insertions(+) create mode 100644 app/controllers/comments/pattern_search_controller.rb create mode 100644 app/controllers/glossary_terms/pattern_search_controller.rb create mode 100644 app/controllers/herbaria/pattern_search_controller.rb create mode 100644 app/controllers/herbarium_records/pattern_search_controller.rb create mode 100644 app/controllers/locations/pattern_search_controller.rb create mode 100644 app/controllers/names/pattern_search_controller.rb create mode 100644 app/controllers/observations/pattern_search_controller.rb create mode 100644 app/controllers/projects/pattern_search_controller.rb create mode 100644 app/controllers/species_lists/pattern_search_controller.rb create mode 100644 app/controllers/users/pattern_search_controller.rb diff --git a/app/controllers/comments/pattern_search_controller.rb b/app/controllers/comments/pattern_search_controller.rb new file mode 100644 index 0000000000..0e9631ce9c --- /dev/null +++ b/app/controllers/comments/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Comments pattern search form. +# +# Route: `new_comment_pattern_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/comments/pattern_search", action: :create }` +module Comments + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/glossary_terms/pattern_search_controller.rb b/app/controllers/glossary_terms/pattern_search_controller.rb new file mode 100644 index 0000000000..f3d8bf4fbc --- /dev/null +++ b/app/controllers/glossary_terms/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# GlossaryTerms pattern search form. +# +# Route: `new_glossary_term_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/glossary_terms/pattern_search", action: :create }` +module GlossaryTerms + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/herbaria/pattern_search_controller.rb b/app/controllers/herbaria/pattern_search_controller.rb new file mode 100644 index 0000000000..2e6e585777 --- /dev/null +++ b/app/controllers/herbaria/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Herbaria pattern search form. +# +# Route: `new_herbarium_pattern_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/herbaria/pattern_search", action: :create }` +module Herbaria + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/herbarium_records/pattern_search_controller.rb b/app/controllers/herbarium_records/pattern_search_controller.rb new file mode 100644 index 0000000000..13943efcd4 --- /dev/null +++ b/app/controllers/herbarium_records/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Herbarium record pattern search form. +# +# Route: `new_herbarium_record_pattern_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/herbarium_records/pattern_search", action: :create }` +module HerbariumRecords + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/locations/pattern_search_controller.rb b/app/controllers/locations/pattern_search_controller.rb new file mode 100644 index 0000000000..f9937c2915 --- /dev/null +++ b/app/controllers/locations/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Locations pattern search form. +# +# Route: `new_location_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/locations/pattern_search", action: :create }` +module Locations + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb new file mode 100644 index 0000000000..47d62993a6 --- /dev/null +++ b/app/controllers/names/pattern_search_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Names pattern search form. +# +# Route: `new_name_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/names/pattern_search", action: :create }` +module Names + class PatternSearchController < ApplicationController + before_action :login_required + + def new + @fields = name_search_params + end + + def create + @pattern = human_formatted_pattern_search_string + redirect_to(controller: "/names", action: :index, pattern: @pattern) + end + + private + + def permitted_search_params + params.permit(name_search_params) + end + + def name_search_params + PatternSearch::Name.params.keys + end + + # Roundabout: We're converting the params hash back into a normal query + # string to start with, and then we're translating the query string into the + # format that the user would have typed into the search box if they knew how + # to do that, because that's what the PatternSearch class expects to parse. + # The PatternSearch class then unpacks, validates and re-translates all + # these params into the actual params used by the Query class. This may seem + # odd: of course we do know the Query param names in advance, so we could + # theoretically just pass the values directly to the receiving controller. + # But we'd still have to be able to validate the input, and give messages + # for all the possible errors there. PatternSearch class handles all that. + def human_formatted_pattern_search_string + query_string = permitted_search_params.compact_blank.to_query + query_string.tr("=", ":").tr("&", " ").tr("%2C", "\\\\,") + end + end +end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb new file mode 100644 index 0000000000..45cd801ace --- /dev/null +++ b/app/controllers/observations/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Observations pattern search form. +# +# Route: `new_observation_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/observations/pattern_search", action: :create }` +module Observations + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/projects/pattern_search_controller.rb b/app/controllers/projects/pattern_search_controller.rb new file mode 100644 index 0000000000..d932f0695a --- /dev/null +++ b/app/controllers/projects/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Projects pattern search form. +# +# Route: `new_project_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/projects/pattern_search", action: :create }` +module Projects + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/species_lists/pattern_search_controller.rb b/app/controllers/species_lists/pattern_search_controller.rb new file mode 100644 index 0000000000..405f52155f --- /dev/null +++ b/app/controllers/species_lists/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# SpeciesLists pattern search form. +# +# Route: `new_species_list_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/species_lists/pattern_search", action: :create }` +module SpeciesLists + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end diff --git a/app/controllers/users/pattern_search_controller.rb b/app/controllers/users/pattern_search_controller.rb new file mode 100644 index 0000000000..3dafffa362 --- /dev/null +++ b/app/controllers/users/pattern_search_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Users pattern search form. +# +# Route: `new_user_search_path` +# Only one action here. Call namespaced controller actions with a hash like +# `{ controller: "/users/pattern_search", action: :create }` +module Users + class PatternSearchController < ApplicationController + before_action :login_required + + def new + end + end +end From 57a4acab6013e6a0640a202dbd6f30e76aaaaf2d Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 26 Aug 2024 00:49:22 -0700 Subject: [PATCH 02/80] names search routes and form --- app/views/controllers/names/pattern_search/new.erb | 13 +++++++++++++ config/routes.rb | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 app/views/controllers/names/pattern_search/new.erb diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb new file mode 100644 index 0000000000..c6ad81a317 --- /dev/null +++ b/app/views/controllers/names/pattern_search/new.erb @@ -0,0 +1,13 @@ +<% +fields = PatternSearch::Name.params.keys +%> + +<%= form_with(url: { action: :create }) do |f| %> + + <% fields.each do |field| %> + <%= text_field_with_label(form: f, field:) %> + <% end %> + + <%= submit_button(form: f, button: :SEARCH.l, center: true) %> + +<% end %> diff --git a/config/routes.rb b/config/routes.rb index b139a0139f..527eb5896a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -567,6 +567,9 @@ def route_actions_hash as: "names_eol_preview") get("names/eol_expanded_review", to: "names/eol_data/expanded_review#show", as: "names_eol_expanded_review") + get("names/search/new", to: "names/pattern_search#new", as: "new_name_search") + post("names/search", to: "names/pattern_search#create", as: "name_search") + # ----- Observations: standard actions ---------------------------- namespace :observations do From 6517d2fb83c4b19f74b56c2bddd9af6c9b59d2d9 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 26 Aug 2024 00:52:50 -0700 Subject: [PATCH 03/80] Update pattern_search_controller.rb --- app/controllers/names/pattern_search_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index 47d62993a6..791e103237 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -35,9 +35,10 @@ def name_search_params # The PatternSearch class then unpacks, validates and re-translates all # these params into the actual params used by the Query class. This may seem # odd: of course we do know the Query param names in advance, so we could - # theoretically just pass the values directly to the receiving controller. - # But we'd still have to be able to validate the input, and give messages - # for all the possible errors there. PatternSearch class handles all that. + # theoretically just pass the values directly into Query and render the + # index. But we'd still have to be able to validate the input, and give + # messages for all the possible errors there. PatternSearch class handles + # all that. def human_formatted_pattern_search_string query_string = permitted_search_params.compact_blank.to_query query_string.tr("=", ":").tr("&", " ").tr("%2C", "\\\\,") From 752034400efd64394d73de824f29fe69113fdcc4 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 26 Aug 2024 14:38:11 -0700 Subject: [PATCH 04/80] Add some pattern search field helpers --- app/helpers/pattern_search_helper.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/helpers/pattern_search_helper.rb diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb new file mode 100644 index 0000000000..094216c40c --- /dev/null +++ b/app/helpers/pattern_search_helper.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# helpers for pattern search forms. These call field helpers in forms_helper. +# args should provide form, field, label at a minimum. +module PatternSearchHelper + def pattern_search_boolean_field(**args) + options = [ + ["", null], + ["yes", "yes"], + ["no", "no"] + ] + select_with_label(options:, **args) + end + + def pattern_search_yes_field(**args) + check_box_with_label(value: "yes", **args) + end + + def pattern_search_yes_no_both_field(**args) + options = [ + ["", null], + ["yes", "yes"], + ["no", "no"], + ["both", "either"] + ] + select_with_label(options:, **args) + end +end From c8cb86564fd43660de5d3132866a8fec7b74cea2 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 30 Aug 2024 00:15:33 -0700 Subject: [PATCH 05/80] more helpers --- app/helpers/pattern_search_helper.rb | 46 +++++++++++++++++-- .../controllers/names/pattern_search/new.erb | 30 +++++++++++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 094216c40c..e28695410f 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -3,6 +3,10 @@ # helpers for pattern search forms. These call field helpers in forms_helper. # args should provide form, field, label at a minimum. module PatternSearchHelper + def pattern_search_yes_field(**args) + check_box_with_label(value: "yes", **args) + end + def pattern_search_boolean_field(**args) options = [ ["", null], @@ -12,10 +16,6 @@ def pattern_search_boolean_field(**args) select_with_label(options:, **args) end - def pattern_search_yes_field(**args) - check_box_with_label(value: "yes", **args) - end - def pattern_search_yes_no_both_field(**args) options = [ ["", null], @@ -25,4 +25,42 @@ def pattern_search_yes_no_both_field(**args) ] select_with_label(options:, **args) end + + def pattern_search_date_range_field(**args) + concat(tag.div do + tag.strong(args[:label]) + end) + concat(tag.div(class: "row") do + concat(tag.div(class: "col-12 col-sm-6") do + text_field_with_label( + **args.merge(label: :START.l, field: "#{args[:field]}_start") + ) + end) + concat(tag.div(class: "col-12 col-sm-6") do + text_field_with_label( + **args.merge(label: :END.l, field: "#{args[:field]}_end") + ) + end) + end) + end + + def pattern_search_rank_range_field(**args) + concat(tag.div do + tag.strong(args[:label]) + end) + concat(tag.div(class: "row") do + concat(tag.div do + select_with_label( + options: Rank.all_ranks, + **args.merge(label: :LOW.l, field: "#{args[:field]}_low") + ) + end) + concat(tag.div do + select_with_label( + options: Rank.all_ranks, + **args.merge(label: :HIGH.l, field: "#{args[:field]}_high") + ) + end) + end) + end end diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index c6ad81a317..0188a527b7 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -1,10 +1,36 @@ <% -fields = PatternSearch::Name.params.keys +fields = { + created: :date_range, + modified: :date_range, + + # quality + deprecated: :boolean, + has_author: :boolean, + author: :text, + has_citation: :boolean, + citation: :text, + has_observations: :yes, + # scope + has_synonyms: :boolean, + include_synonyms: :boolean, + include_subtaxa: :boolean, + include_misspellings: :yes_no_both, + rank: :rank_range, + lichen: :boolean, + # detail + has_classification: :boolean, + classification: :text, + has_notes: :boolean, + notes: :text, + has_comments: :yes, + comments: :text, + has_description: :boolean +} %> <%= form_with(url: { action: :create }) do |f| %> - <% fields.each do |field| %> + <% fields.each do |field, type| %> <%= text_field_with_label(form: f, field:) %> <% end %> From efe8ae33ef09fb05b5344e62c0649ec9f24e3bdb Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 30 Aug 2024 20:31:38 -0700 Subject: [PATCH 06/80] simplify range fields --- app/helpers/pattern_search_helper.rb | 46 +++++++++++----------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index e28695410f..62ecee5a19 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -26,41 +26,31 @@ def pattern_search_yes_no_both_field(**args) select_with_label(options:, **args) end + # The first field gets the label, the range field is optional def pattern_search_date_range_field(**args) - concat(tag.div do - tag.strong(args[:label]) - end) - concat(tag.div(class: "row") do - concat(tag.div(class: "col-12 col-sm-6") do - text_field_with_label( - **args.merge(label: :START.l, field: "#{args[:field]}_start") - ) + tag.div(class: "row") do + concat(tag.div(class: "col-xs-12 col-sm-6") do + text_field_with_label(**args) end) - concat(tag.div(class: "col-12 col-sm-6") do - text_field_with_label( - **args.merge(label: :END.l, field: "#{args[:field]}_end") - ) + concat(tag.div(class: "col-xs-12 col-sm-6") do + text_field_with_label(**args.merge( + label: :TO.l, optional: true, field: "#{args[:field]}_range" + )) end) - end) + end end + # The first field gets the label, the range field is optional def pattern_search_rank_range_field(**args) - concat(tag.div do - tag.strong(args[:label]) - end) - concat(tag.div(class: "row") do - concat(tag.div do - select_with_label( - options: Rank.all_ranks, - **args.merge(label: :LOW.l, field: "#{args[:field]}_low") - ) + tag.div(class: "row") do + concat(tag.div(class: "col-xs-12 col-sm-6") do + select_with_label(options: Rank.all_ranks, **args) end) - concat(tag.div do - select_with_label( - options: Rank.all_ranks, - **args.merge(label: :HIGH.l, field: "#{args[:field]}_high") - ) + concat(tag.div(class: "col-xs-12 col-sm-6") do + select_with_label(options: Rank.all_ranks, **args.merge( + label: :TO.l, optional: true, field: "#{args[:field]}_range" + )) end) - end) + end end end From 16a2156204e987e91f290e93e0a42ec82863216c Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 31 Aug 2024 01:10:36 -0700 Subject: [PATCH 07/80] pattern_search form builder --- app/helpers/pattern_search_helper.rb | 24 ++++++++++++ .../names/pattern_search/_form.erb | 32 +++++++++++++++ .../controllers/names/pattern_search/new.erb | 39 +------------------ config/locales/en.txt | 2 + 4 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 app/views/controllers/names/pattern_search/_form.erb diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 62ecee5a19..9275784b50 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -3,6 +3,30 @@ # helpers for pattern search forms. These call field helpers in forms_helper. # args should provide form, field, label at a minimum. module PatternSearchHelper + def pattern_search_field(**args) + field = args[:field] + klass = args[:klass] + args[:label] ||= :"search_#{field}".l + helper = pattern_search_helper_for_field(field, klass) + send(helper, **args) + end + + # The subclasses say how they're going to parse their fields, so we can use + # that to determine which helper to use. + def pattern_search_helper_for_field(field, klass) + type = klass.params[field][1] + PATTERN_SEARCH_FIELD_HELPERS[type] + end + + # Convenience for subclasses to access helper methods via PARAMS + PATTERN_SEARCH_FIELD_HELPERS = { + parse_boolean: :pattern_search_boolean_field, + parse_yes_no_both: :pattern_search_yes_no_both_field, + parse_date_range: :pattern_search_date_range_field, + parse_rank_range: :pattern_search_rank_range_field, + parse_string: :text_field_with_label + }.freeze + def pattern_search_yes_field(**args) check_box_with_label(value: "yes", **args) end diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb new file mode 100644 index 0000000000..44326e9c28 --- /dev/null +++ b/app/views/controllers/names/pattern_search/_form.erb @@ -0,0 +1,32 @@ +<% +fields_1 = { + date: [:created, :modified], + quality: [:deprecated, :has_author, :author, :has_citation, :citation, + :has_observations] +} +fields_2 = { + scope: [:has_synonyms, :include_synonyms, :include_subtaxa, + :include_misspellings, :rank, :lichen], + detail: [:has_classification, :classification, :has_notes, :notes, + :has_comments, :comments, :has_description] +} +%> + +<%= form_with(url: { action: :create }) do |f| %> + + <%= tag.div(class: "row") do %> + <% [fields_1, fields_2].each do |field_groups| %> + <%= tag.div(class: "col-xs-12 col-sm-6") do %> + <% field_groups.each do |header, fields| %> + <%= tag.h5(:"search_term_group_#{header}".l) %> + <% fields.each do |field| %> + <%= pattern_search_field(form: f, field: field) %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + + <%= submit_button(form: f, button: :SEARCH.l, center: true) %> + +<% end %> diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index 0188a527b7..1276080c9b 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -1,39 +1,2 @@ -<% -fields = { - created: :date_range, - modified: :date_range, - # quality - deprecated: :boolean, - has_author: :boolean, - author: :text, - has_citation: :boolean, - citation: :text, - has_observations: :yes, - # scope - has_synonyms: :boolean, - include_synonyms: :boolean, - include_subtaxa: :boolean, - include_misspellings: :yes_no_both, - rank: :rank_range, - lichen: :boolean, - # detail - has_classification: :boolean, - classification: :text, - has_notes: :boolean, - notes: :text, - has_comments: :yes, - comments: :text, - has_description: :boolean -} -%> - -<%= form_with(url: { action: :create }) do |f| %> - - <% fields.each do |field, type| %> - <%= text_field_with_label(form: f, field:) %> - <% end %> - - <%= submit_button(form: f, button: :SEARCH.l, center: true) %> - -<% end %> +<%= render(partial: "names/pattern_search/form", locals: { local: true } ) %> diff --git a/config/locales/en.txt b/config/locales/en.txt index d71243de76..a681ee6bb9 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -876,6 +876,8 @@ "NO": "No" "yes": "yes" "no": "no" + "TO": "To" + "to": "to" ############################################################################## From 4556505d924a46034c5ea6ecda84fb9b2215e3c8 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 31 Aug 2024 01:40:39 -0700 Subject: [PATCH 08/80] Pared down --- app/helpers/pattern_search_helper.rb | 24 +++++++++---------- .../names/pattern_search/_form.erb | 2 +- config/locales/en.txt | 4 ++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 9275784b50..d540613ea6 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -4,17 +4,15 @@ # args should provide form, field, label at a minimum. module PatternSearchHelper def pattern_search_field(**args) - field = args[:field] - klass = args[:klass] - args[:label] ||= :"search_#{field}".l - helper = pattern_search_helper_for_field(field, klass) - send(helper, **args) + args[:label] ||= :"search_term_#{args[:field]}".l.humanize + helper = pattern_search_helper_for_field(args[:field], args[:type]) + send(helper, **args.except(:type)) if helper end # The subclasses say how they're going to parse their fields, so we can use # that to determine which helper to use. - def pattern_search_helper_for_field(field, klass) - type = klass.params[field][1] + def pattern_search_helper_for_field(field, type) + type = PatternSearch.const_get(type.capitalize).params[field][1] PATTERN_SEARCH_FIELD_HELPERS[type] end @@ -33,21 +31,21 @@ def pattern_search_yes_field(**args) def pattern_search_boolean_field(**args) options = [ - ["", null], + ["", nil], ["yes", "yes"], ["no", "no"] ] - select_with_label(options:, **args) + select_with_label(options:, inline: true, **args) end def pattern_search_yes_no_both_field(**args) options = [ - ["", null], + ["", nil], ["yes", "yes"], ["no", "no"], ["both", "either"] ] - select_with_label(options:, **args) + select_with_label(options:, inline: true, **args) end # The first field gets the label, the range field is optional @@ -68,10 +66,10 @@ def pattern_search_date_range_field(**args) def pattern_search_rank_range_field(**args) tag.div(class: "row") do concat(tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Rank.all_ranks, **args) + select_with_label(options: Name.all_ranks, **args) end) concat(tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Rank.all_ranks, **args.merge( + select_with_label(options: Name.all_ranks, **args.merge( label: :TO.l, optional: true, field: "#{args[:field]}_range" )) end) diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb index 44326e9c28..7dfe274f28 100644 --- a/app/views/controllers/names/pattern_search/_form.erb +++ b/app/views/controllers/names/pattern_search/_form.erb @@ -20,7 +20,7 @@ fields_2 = { <% field_groups.each do |header, fields| %> <%= tag.h5(:"search_term_group_#{header}".l) %> <% fields.each do |field| %> - <%= pattern_search_field(form: f, field: field) %> + <%= pattern_search_field(form: f, field: field, type: :name) %> <% end %> <% end %> <% end %> diff --git a/config/locales/en.txt b/config/locales/en.txt index a681ee6bb9..058b35b824 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -1167,6 +1167,10 @@ search_term_south: south search_term_user: user search_term_west: west + search_term_group_date: [DATE] + search_term_group_quality: [QUALITY] + search_term_group_scope: Scope + search_term_group_detail: Detail # Words recognized in search bar. # e.g. "user:me" From d8693857c5417a5851e3c5f5a832290895b4a8f6 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 31 Aug 2024 18:24:49 -0700 Subject: [PATCH 09/80] Panels for search groups --- app/helpers/pattern_search_helper.rb | 51 ++++++++++++------- .../names/pattern_search/_form.erb | 9 ++-- .../controllers/names/pattern_search/new.erb | 5 +- config/locales/en.txt | 14 +++-- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index d540613ea6..4a631d4edf 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -6,7 +6,8 @@ module PatternSearchHelper def pattern_search_field(**args) args[:label] ||= :"search_term_#{args[:field]}".l.humanize helper = pattern_search_helper_for_field(args[:field], args[:type]) - send(helper, **args.except(:type)) if helper + args = prepare_args_for_pattern_search_field(args, helper) + send(helper, **args) if helper end # The subclasses say how they're going to parse their fields, so we can use @@ -25,6 +26,13 @@ def pattern_search_helper_for_field(field, type) parse_string: :text_field_with_label }.freeze + # Bootstrap 3 can't do full-width inline label/field. + def prepare_args_for_pattern_search_field(args, helper) + # args[:inline] = true if helper == :text_field_with_label + + args.except(:type) + end + def pattern_search_yes_field(**args) check_box_with_label(value: "yes", **args) end @@ -48,31 +56,36 @@ def pattern_search_yes_no_both_field(**args) select_with_label(options:, inline: true, **args) end - # The first field gets the label, the range field is optional + # RANGE FIELDS The first field gets the label, name and ID of the actual + # param; the end `_range` field is optional. The controller needs to check for + # the second & join them with a hyphen if it exists (in both cases here). def pattern_search_date_range_field(**args) tag.div(class: "row") do - concat(tag.div(class: "col-xs-12 col-sm-6") do - text_field_with_label(**args) - end) - concat(tag.div(class: "col-xs-12 col-sm-6") do - text_field_with_label(**args.merge( - label: :TO.l, optional: true, field: "#{args[:field]}_range" - )) - end) + [ + tag.div(class: "col-xs-12 col-sm-6") do + text_field_with_label(**args) + end, + tag.div(class: "col-xs-12 col-sm-6") do + text_field_with_label(**args.merge( + { label: :TO.l, optional: true, field: "#{args[:field]}_range" } + )) + end + ].safe_join end end - # The first field gets the label, the range field is optional def pattern_search_rank_range_field(**args) tag.div(class: "row") do - concat(tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Name.all_ranks, **args) - end) - concat(tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Name.all_ranks, **args.merge( - label: :TO.l, optional: true, field: "#{args[:field]}_range" - )) - end) + [ + tag.div(class: "col-xs-12 col-sm-6") do + select_with_label(options: Name.all_ranks, **args) + end, + tag.div(class: "col-xs-12 col-sm-6") do + select_with_label(options: Name.all_ranks, **args.merge( + { label: :TO.l, optional: true, field: "#{args[:field]}_range" } + )) + end + ].safe_join end end end diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb index 7dfe274f28..c0f95acfce 100644 --- a/app/views/controllers/names/pattern_search/_form.erb +++ b/app/views/controllers/names/pattern_search/_form.erb @@ -17,10 +17,11 @@ fields_2 = { <%= tag.div(class: "row") do %> <% [fields_1, fields_2].each do |field_groups| %> <%= tag.div(class: "col-xs-12 col-sm-6") do %> - <% field_groups.each do |header, fields| %> - <%= tag.h5(:"search_term_group_#{header}".l) %> - <% fields.each do |field| %> - <%= pattern_search_field(form: f, field: field, type: :name) %> + <% field_groups.each do |heading, fields| %> + <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> + <% fields.each do |field| %> + <%= pattern_search_field(form: f, field: field, type: :name) %> + <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index 1276080c9b..3718c1bfb9 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -1,2 +1,5 @@ - +<% +# add_page_title(:pattern_search) +@container = :full +%> <%= render(partial: "names/pattern_search/form", locals: { local: true } ) %> diff --git a/config/locales/en.txt b/config/locales/en.txt index 058b35b824..5fc8972c43 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -497,6 +497,8 @@ # deposit of Sequence Bases in an Archive DEPOSIT: Deposit deposit: deposit + DETAIL: Detail + detail: detail EDITOR: Editor editor: editor EDITORS: Editors @@ -593,6 +595,8 @@ reviewer: reviewer REVIEWERS: Reviewers reviewers: reviewers + SCOPE: Scope + scope: scope SECONDS: Seconds SIZE: Size size: size @@ -1167,10 +1171,10 @@ search_term_south: south search_term_user: user search_term_west: west - search_term_group_date: [DATE] - search_term_group_quality: [QUALITY] - search_term_group_scope: Scope - search_term_group_detail: Detail + search_term_group_date: "[:DATE]" + search_term_group_quality: "[:QUALITY]" + search_term_group_scope: "[:SCOPE]" + search_term_group_detail: "[:DETAIL]" # Words recognized in search bar. # e.g. "user:me" @@ -2259,7 +2263,7 @@ show_observation_site_id: Site ID show_observation_owner_id: Observer Preference show_observation_no_clear_preference: no clear preference - show_observation_details: Details + show_observation_details: "[:DETAIL]" show_observation_edit_observation: "[:edit_object(type=:observation)]" show_observation_propose_new_name: "[:show_namings_propose_new_name]" show_observation_debug_consensus: Debug Consensus From ef5c74a791d8defa414329fcc9bfccff3a1d5259 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 2 Sep 2024 12:32:42 -0700 Subject: [PATCH 10/80] Start to fix inline text fields --- app/helpers/pattern_search_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 4a631d4edf..5a0ff6136c 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -28,7 +28,7 @@ def pattern_search_helper_for_field(field, type) # Bootstrap 3 can't do full-width inline label/field. def prepare_args_for_pattern_search_field(args, helper) - # args[:inline] = true if helper == :text_field_with_label + args[:inline] = true if helper == :text_field_with_label args.except(:type) end From ca9c16fcaa745b36303c8128fd25bd9dec24488a Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 2 Sep 2024 15:08:53 -0700 Subject: [PATCH 11/80] Regroup and polish names form --- app/assets/stylesheets/mo/_utilities.scss | 4 +++ app/helpers/forms_helper.rb | 7 ++++- app/helpers/pattern_search_helper.rb | 23 +++++++++++---- .../names/pattern_search/_form.erb | 28 +++++++++++++------ config/locales/en.txt | 7 +++-- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/mo/_utilities.scss b/app/assets/stylesheets/mo/_utilities.scss index adada3d7fb..0a29b0a978 100644 --- a/app/assets/stylesheets/mo/_utilities.scss +++ b/app/assets/stylesheets/mo/_utilities.scss @@ -122,6 +122,10 @@ object-fit: contain; } +.flex-grow-1 { + flex-grow: 1 !important; +} + .text-larger { font-size: larger !important; } diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 4cc398a318..a85b71dc98 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -210,7 +210,12 @@ def text_area_with_label(**args) # Content for `between` and `label_after` come right after the label on left, # content for `label_end` is at the end of the same line, right justified. def text_label_row(args, label_opts) - tag.div(class: "d-flex justify-content-between") do + row_class = if args[:inline] == true + "d-inline-block" + else + "d-flex justify-content-between" + end + tag.div(class: row_class) do concat(tag.div do concat(args[:form].label(args[:field], args[:label], label_opts)) concat(args[:between]) if args[:between].present? diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 5a0ff6136c..da92bcab88 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -4,21 +4,32 @@ # args should provide form, field, label at a minimum. module PatternSearchHelper def pattern_search_field(**args) - args[:label] ||= :"search_term_#{args[:field]}".l.humanize + args[:label] ||= pattern_search_helper_for_label(args[:field]) helper = pattern_search_helper_for_field(args[:field], args[:type]) args = prepare_args_for_pattern_search_field(args, helper) send(helper, **args) if helper end + def pattern_search_helper_for_label(field) + if field == :pattern + :PATTERN.l + else + :"search_term_#{field}".l.humanize + end + end + # The subclasses say how they're going to parse their fields, so we can use # that to determine which helper to use. def pattern_search_helper_for_field(field, type) + return :text_field_with_label if field == :pattern + type = PatternSearch.const_get(type.capitalize).params[field][1] PATTERN_SEARCH_FIELD_HELPERS[type] end # Convenience for subclasses to access helper methods via PARAMS PATTERN_SEARCH_FIELD_HELPERS = { + parse_yes: :pattern_search_yes_field, parse_boolean: :pattern_search_boolean_field, parse_yes_no_both: :pattern_search_yes_no_both_field, parse_date_range: :pattern_search_date_range_field, @@ -28,7 +39,9 @@ def pattern_search_helper_for_field(field, type) # Bootstrap 3 can't do full-width inline label/field. def prepare_args_for_pattern_search_field(args, helper) - args[:inline] = true if helper == :text_field_with_label + if helper == :text_field_with_label && args[:field] != :pattern + args[:inline] = true + end args.except(:type) end @@ -63,11 +76,11 @@ def pattern_search_date_range_field(**args) tag.div(class: "row") do [ tag.div(class: "col-xs-12 col-sm-6") do - text_field_with_label(**args) + text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) end, tag.div(class: "col-xs-12 col-sm-6") do text_field_with_label(**args.merge( - { label: :TO.l, optional: true, field: "#{args[:field]}_range" } + { label: :to.l, between: :optional, field: "#{args[:field]}_range" } )) end ].safe_join @@ -82,7 +95,7 @@ def pattern_search_rank_range_field(**args) end, tag.div(class: "col-xs-12 col-sm-6") do select_with_label(options: Name.all_ranks, **args.merge( - { label: :TO.l, optional: true, field: "#{args[:field]}_range" } + { label: :to.l, between: :optional, field: "#{args[:field]}_range" } )) end ].safe_join diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb index c0f95acfce..91fe6df0ca 100644 --- a/app/views/controllers/names/pattern_search/_form.erb +++ b/app/views/controllers/names/pattern_search/_form.erb @@ -1,14 +1,15 @@ <% fields_1 = { + search: [:pattern], date: [:created, :modified], - quality: [:deprecated, :has_author, :author, :has_citation, :citation, - :has_observations] + quality: [[:has_observations, :deprecated], [:has_author, :author], + [:has_citation, :citation]] } fields_2 = { - scope: [:has_synonyms, :include_synonyms, :include_subtaxa, - :include_misspellings, :rank, :lichen], - detail: [:has_classification, :classification, :has_notes, :notes, - :has_comments, :comments, :has_description] + scope: [[:has_synonyms, :include_synonyms], [:include_subtaxa, + :include_misspellings], :rank, :lichen], + detail: [[:has_classification, :classification], [:has_notes, :notes], + [:has_comments, :comments], :has_description] } %> @@ -16,11 +17,22 @@ fields_2 = { <%= tag.div(class: "row") do %> <% [fields_1, fields_2].each do |field_groups| %> - <%= tag.div(class: "col-xs-12 col-sm-6") do %> + <%= tag.div(class: "col-xs-12 col-md-6") do %> <% field_groups.each do |heading, fields| %> <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> <% fields.each do |field| %> - <%= pattern_search_field(form: f, field: field, type: :name) %> + <% if field.is_a?(Array) %> + <%= tag.div(class: "row") do %> + <% field.each do |subfield| %> + <%= tag.div(class: "col-xs-12 col-lg-6") do %> + <%= pattern_search_field(form: f, field: subfield, + type: :name) %> + <% end %> + <% end %> + <% end %> + <% else %> + <%= pattern_search_field(form: f, field: field, type: :name) %> + <% end %> <% end %> <% end %> <% end %> diff --git a/config/locales/en.txt b/config/locales/en.txt index 5fc8972c43..d1be79f8a2 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -727,6 +727,8 @@ OKAY: Okay okay: okay OPEN: Open + pattern: pattern + PATTERN: Pattern # reload a form RELOAD: Reload reload: reload @@ -880,8 +882,8 @@ "NO": "No" "yes": "yes" "no": "no" - "TO": "To" - "to": "to" + TO: To + to: to ############################################################################## @@ -1171,6 +1173,7 @@ search_term_south: south search_term_user: user search_term_west: west + search_term_group_search: "[:SEARCH]" search_term_group_date: "[:DATE]" search_term_group_quality: "[:QUALITY]" search_term_group_scope: "[:SCOPE]" From 3c76fe6c909d06a165f8280d45b39a24a80c2f59 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 2 Sep 2024 15:20:10 -0700 Subject: [PATCH 12/80] Update _form.erb --- app/views/controllers/names/pattern_search/_form.erb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb index 91fe6df0ca..6220ef1e0f 100644 --- a/app/views/controllers/names/pattern_search/_form.erb +++ b/app/views/controllers/names/pattern_search/_form.erb @@ -17,10 +17,13 @@ fields_2 = { <%= tag.div(class: "row") do %> <% [fields_1, fields_2].each do |field_groups| %> + <%= tag.div(class: "col-xs-12 col-md-6") do %> <% field_groups.each do |heading, fields| %> + <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> <% fields.each do |field| %> + <% if field.is_a?(Array) %> <%= tag.div(class: "row") do %> <% field.each do |subfield| %> @@ -33,10 +36,13 @@ fields_2 = { <% else %> <%= pattern_search_field(form: f, field: field, type: :name) %> <% end %> + <% end %> <% end %> + <% end %> <% end %> + <% end %> <% end %> From 3546116cdb8717483cef7120ce90a3d695d549f1 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 2 Sep 2024 15:22:58 -0700 Subject: [PATCH 13/80] Start obs form --- .../controllers/names/pattern_search/new.erb | 1 + .../observations/pattern_search/_form.erb | 51 +++++++++++++++++++ .../observations/pattern_search/new.erb | 7 +++ 3 files changed, 59 insertions(+) create mode 100644 app/views/controllers/observations/pattern_search/_form.erb create mode 100644 app/views/controllers/observations/pattern_search/new.erb diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index 3718c1bfb9..2cdfa3f5ae 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -2,4 +2,5 @@ # add_page_title(:pattern_search) @container = :full %> + <%= render(partial: "names/pattern_search/form", locals: { local: true } ) %> diff --git a/app/views/controllers/observations/pattern_search/_form.erb b/app/views/controllers/observations/pattern_search/_form.erb new file mode 100644 index 0000000000..66e6cef490 --- /dev/null +++ b/app/views/controllers/observations/pattern_search/_form.erb @@ -0,0 +1,51 @@ +<% +fields_1 = { + search: [:pattern], + date: [:date, :created, :modified], + quality: [[:has_observations, :deprecated], [:has_author, :author], + [:has_citation, :citation]] +} +fields_2 = { + scope: [[:has_synonyms, :include_synonyms], [:include_subtaxa, + :include_misspellings], :rank, :lichen], + detail: [[:has_classification, :classification], [:has_notes, :notes], + [:has_comments, :comments], :has_description] +} +%> + +<%= form_with(url: { action: :create }) do |f| %> + + <%= tag.div(class: "row") do %> + <% [fields_1, fields_2].each do |field_groups| %> + + <%= tag.div(class: "col-xs-12 col-md-6") do %> + <% field_groups.each do |heading, fields| %> + + <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> + <% fields.each do |field| %> + + <% if field.is_a?(Array) %> + <%= tag.div(class: "row") do %> + <% field.each do |subfield| %> + <%= tag.div(class: "col-xs-12 col-lg-6") do %> + <%= pattern_search_field(form: f, field: subfield, + type: :name) %> + <% end %> + <% end %> + <% end %> + <% else %> + <%= pattern_search_field(form: f, field: field, type: :name) %> + <% end %> + + <% end %> + <% end %> + + <% end %> + <% end %> + + <% end %> + <% end %> + + <%= submit_button(form: f, button: :SEARCH.l, center: true) %> + +<% end %> diff --git a/app/views/controllers/observations/pattern_search/new.erb b/app/views/controllers/observations/pattern_search/new.erb new file mode 100644 index 0000000000..5f21408fde --- /dev/null +++ b/app/views/controllers/observations/pattern_search/new.erb @@ -0,0 +1,7 @@ +<% +# add_page_title(:pattern_search) +@container = :full +%> + +<%= render(partial: "observations/pattern_search/form", + locals: { local: true } ) %> From a7682235140a741bdaa66809e2abdce1f6d3e6dc Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 3 Sep 2024 23:30:02 -0700 Subject: [PATCH 14/80] organize obs search --- .../observations/pattern_search/_form.erb | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/app/views/controllers/observations/pattern_search/_form.erb b/app/views/controllers/observations/pattern_search/_form.erb index 66e6cef490..c516719a0b 100644 --- a/app/views/controllers/observations/pattern_search/_form.erb +++ b/app/views/controllers/observations/pattern_search/_form.erb @@ -1,16 +1,48 @@ <% fields_1 = { - search: [:pattern], - date: [:date, :created, :modified], - quality: [[:has_observations, :deprecated], [:has_author, :author], - [:has_citation, :citation]] -} -fields_2 = { - scope: [[:has_synonyms, :include_synonyms], [:include_subtaxa, - :include_misspellings], :rank, :lichen], - detail: [[:has_classification, :classification], [:has_notes, :notes], - [:has_comments, :comments], :has_description] -} + # date + date: [:date, :parse_date_range], + created: [:created_at, :parse_date_range], + modified: [:updated_at, :parse_date_range], + + # names + has_name: [:with_name, :parse_boolean], + name: [:names, :parse_list_of_names], + confidence: [:confidence, :parse_confidence], + exclude_consensus: [:exclude_consensus, :parse_boolean], # of_look_alikes + include_subtaxa: [:include_subtaxa, :parse_boolean], + include_synonyms: [:include_synonyms, :parse_boolean], + include_all_name_proposals: [:include_all_name_proposals, :parse_boolean], + lichen: [:lichen, :parse_boolean], + + # location + location: [:locations, :parse_list_of_locations], + region: [:region, :parse_list_of_strings], + has_public_lat_lng: [:with_public_lat_lng, :parse_boolean], + is_collection_location: [:is_collection_location, :parse_boolean], + east: [:east, :parse_longitude], + north: [:north, :parse_latitude], + south: [:south, :parse_latitude], + west: [:west, :parse_longitude], + + # detail + has_images: [:with_images, :parse_boolean], + has_specimen: [:with_specimen, :parse_boolean], + has_sequence: [:with_sequences, :parse_yes], + has_notes: [:with_notes, :parse_boolean], + has_field: [:with_notes_fields, :parse_string], + notes: [:notes_has, :parse_string], + has_comments: [:with_comments, :parse_yes], + comments: [:comments_has, :parse_string], + + # associated with + user: [:users, :parse_list_of_users], + herbarium: [:herbaria, :parse_list_of_herbaria], + list: [:species_lists, :parse_list_of_species_lists], + project: [:projects, :parse_list_of_projects], + project_lists: [:project_lists, :parse_list_of_projects], + field_slip: [:field_slips, :parse_list_of_strings], +}.freeze %> <%= form_with(url: { action: :create }) do |f| %> From c5253fc4d0925d9ef237538e46b8cfa1da699cac Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 4 Sep 2024 12:17:49 -0700 Subject: [PATCH 15/80] Refactor pattern search field helpers --- app/helpers/pattern_search_helper.rb | 73 ++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index da92bcab88..b87c2215a9 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -5,9 +5,10 @@ module PatternSearchHelper def pattern_search_field(**args) args[:label] ||= pattern_search_helper_for_label(args[:field]) - helper = pattern_search_helper_for_field(args[:field], args[:type]) - args = prepare_args_for_pattern_search_field(args, helper) - send(helper, **args) if helper + field_type = pattern_search_field_type_from_parser(**args) + component = PATTERN_SEARCH_FIELD_HELPERS[field_type][:component] + args = prepare_args_for_pattern_search_field(args, field_type, component) + send(component, **args) if component end def pattern_search_helper_for_label(field) @@ -18,34 +19,56 @@ def pattern_search_helper_for_label(field) end end - # The subclasses say how they're going to parse their fields, so we can use - # that to determine which helper to use. - def pattern_search_helper_for_field(field, type) - return :text_field_with_label if field == :pattern + # The PatternSearch subclasses define how they're going to parse their + # fields, so we can use that to assign a field helper. + # example: :parse_yes -> :pattern_search_yes_field + # If the field is :pattern, there's no assigned parser. + def pattern_search_field_type_from_parser(**args) + return :pattern if args[:field] == :pattern - type = PatternSearch.const_get(type.capitalize).params[field][1] - PATTERN_SEARCH_FIELD_HELPERS[type] + subclass = PatternSearch.const_get(args[:type].capitalize) + parser = subclass.params[args[:field]][1] + parser.to_s.gsub(/^parse_/, "").to_sym end # Convenience for subclasses to access helper methods via PARAMS PATTERN_SEARCH_FIELD_HELPERS = { - parse_yes: :pattern_search_yes_field, - parse_boolean: :pattern_search_boolean_field, - parse_yes_no_both: :pattern_search_yes_no_both_field, - parse_date_range: :pattern_search_date_range_field, - parse_rank_range: :pattern_search_rank_range_field, - parse_string: :text_field_with_label + pattern: { component: :text_field_with_label, args: {} }, + yes: { component: :pattern_search_yes_field, args: {} }, + boolean: { component: :pattern_search_boolean_field, args: {} }, + yes_no_both: { component: :pattern_search_yes_no_both_field, args: {} }, + date_range: { component: :pattern_search_date_range_field, args: {} }, + rank_range: { component: :pattern_search_rank_range_field, args: {} }, + string: { component: :text_field_with_label, args: {} }, + list_of_strings: { component: :text_field_with_label, args: {} }, + list_of_herbaria: { component: :autocompleter_field, + args: { type: :herbarium } }, + list_of_locations: { component: :autocompleter_field, + args: { type: :location } }, + list_of_names: { component: :autocompleter_field, args: { type: :name } }, + list_of_projects: { component: :autocompleter_field, + args: { type: :project } }, + list_of_species_lists: { component: :autocompleter_field, + args: { type: :species_list } }, + list_of_users: { component: :autocompleter_field, args: { type: :user } }, + confidence: { component: :pattern_search_confidence_field, args: {} }, + longitude: { component: :pattern_search_longitude_field, args: {} }, + latitude: { component: :pattern_search_latitude_field, args: {} } }.freeze - # Bootstrap 3 can't do full-width inline label/field. - def prepare_args_for_pattern_search_field(args, helper) - if helper == :text_field_with_label && args[:field] != :pattern + # Prepares HTML args for the field helper. This is where we can make + # adjustments to the args hash before passing it to the field helper. + # NOTE: Bootstrap 3 can't do full-width inline label/field. + def prepare_args_for_pattern_search_field(args, field_type, component) + if component == :text_field_with_label && args[:field] != :pattern args[:inline] = true end - args.except(:type) + PATTERN_SEARCH_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) end + # FIELD HELPERS + # def pattern_search_yes_field(**args) check_box_with_label(value: "yes", **args) end @@ -101,4 +124,16 @@ def pattern_search_rank_range_field(**args) ].safe_join end end + + def pattern_search_confidence_field(**args) + select_with_label(options: Vote.opinion_menu, **args) + end + + def pattern_search_longitude_field(**args) + text_field_with_label(**args.merge(between: "(-180.0 to 180.0)")) + end + + def pattern_search_latitude_field(**args) + text_field_with_label(**args.merge(between: "(-90.0 to 90.0)")) + end end From b2e7b98f1bc802bcd437a9d61d0df8d0d7048e05 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 4 Sep 2024 15:27:03 -0700 Subject: [PATCH 16/80] Observation form field groupings --- .../observations/pattern_search/_form.erb | 54 +++++-------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/app/views/controllers/observations/pattern_search/_form.erb b/app/views/controllers/observations/pattern_search/_form.erb index c516719a0b..64a8d25086 100644 --- a/app/views/controllers/observations/pattern_search/_form.erb +++ b/app/views/controllers/observations/pattern_search/_form.erb @@ -1,47 +1,17 @@ <% fields_1 = { - # date - date: [:date, :parse_date_range], - created: [:created_at, :parse_date_range], - modified: [:updated_at, :parse_date_range], - - # names - has_name: [:with_name, :parse_boolean], - name: [:names, :parse_list_of_names], - confidence: [:confidence, :parse_confidence], - exclude_consensus: [:exclude_consensus, :parse_boolean], # of_look_alikes - include_subtaxa: [:include_subtaxa, :parse_boolean], - include_synonyms: [:include_synonyms, :parse_boolean], - include_all_name_proposals: [:include_all_name_proposals, :parse_boolean], - lichen: [:lichen, :parse_boolean], - - # location - location: [:locations, :parse_list_of_locations], - region: [:region, :parse_list_of_strings], - has_public_lat_lng: [:with_public_lat_lng, :parse_boolean], - is_collection_location: [:is_collection_location, :parse_boolean], - east: [:east, :parse_longitude], - north: [:north, :parse_latitude], - south: [:south, :parse_latitude], - west: [:west, :parse_longitude], - - # detail - has_images: [:with_images, :parse_boolean], - has_specimen: [:with_specimen, :parse_boolean], - has_sequence: [:with_sequences, :parse_yes], - has_notes: [:with_notes, :parse_boolean], - has_field: [:with_notes_fields, :parse_string], - notes: [:notes_has, :parse_string], - has_comments: [:with_comments, :parse_yes], - comments: [:comments_has, :parse_string], - - # associated with - user: [:users, :parse_list_of_users], - herbarium: [:herbaria, :parse_list_of_herbaria], - list: [:species_lists, :parse_list_of_species_lists], - project: [:projects, :parse_list_of_projects], - project_lists: [:project_lists, :parse_list_of_projects], - field_slip: [:field_slips, :parse_list_of_strings], + search: [:pattern], + date: [:date, :created, :modified], + taxon: [[:has_name, :name], [:lichen, :confidence], + [:include_subtaxa, :include_synonyms], + [:include_all_name_proposals, :exclude_consensus]], + location: [:location, :region, :has_public_lat_lng, :is_collection_location, + :east, :north, :south, :west], +}.freeze +fields_2 = { + detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], + [:has_field, :notes], [:has_comments, :comments]], + associated: [:user, :herbarium, :list, :project, :project_lists, :field_slip], }.freeze %> From 483ffec4feedeaa5676bc5e3b81f3ae3f7b159e4 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 4 Sep 2024 19:50:29 -0700 Subject: [PATCH 17/80] Add concern and routes --- .../concerns/pattern_searchable.rb | 31 +++++++++++++++++++ .../names/pattern_search_controller.rb | 20 ++---------- .../observations/pattern_search_controller.rb | 24 ++++++++++++-- config/routes.rb | 11 +++++-- 4 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 app/controllers/concerns/pattern_searchable.rb diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb new file mode 100644 index 0000000000..e9721c201b --- /dev/null +++ b/app/controllers/concerns/pattern_searchable.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# +# = PatternSearchable Concern +# +# This is a module of reusable methods that can be included by controllers that +# +################################################################################ + +module PatternSearchable + extend ActiveSupport::Concern + + included do + + # Roundabout: We're converting the params hash back into a normal query + # string to start with, and then we're translating the query string into the + # format that the user would have typed into the search box if they knew how + # to do that, because that's what the PatternSearch class expects to parse. + # The PatternSearch class then unpacks, validates and re-translates all + # these params into the actual params used by the Query class. This may seem + # odd: of course we do know the Query param names in advance, so we could + # theoretically just pass the values directly into Query and render the + # index. But we'd still have to be able to validate the input, and give + # messages for all the possible errors there. PatternSearch class handles + # all that. + def human_formatted_pattern_search_string + query_string = permitted_search_params.compact_blank.to_query + query_string.tr("=", ":").tr("&", " ").tr("%2C", "\\\\,") + end + end +end diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index 791e103237..9ffb651833 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -7,6 +7,8 @@ # `{ controller: "/names/pattern_search", action: :create }` module Names class PatternSearchController < ApplicationController + include ::PatternSearchable + before_action :login_required def new @@ -25,23 +27,7 @@ def permitted_search_params end def name_search_params - PatternSearch::Name.params.keys - end - - # Roundabout: We're converting the params hash back into a normal query - # string to start with, and then we're translating the query string into the - # format that the user would have typed into the search box if they knew how - # to do that, because that's what the PatternSearch class expects to parse. - # The PatternSearch class then unpacks, validates and re-translates all - # these params into the actual params used by the Query class. This may seem - # odd: of course we do know the Query param names in advance, so we could - # theoretically just pass the values directly into Query and render the - # index. But we'd still have to be able to validate the input, and give - # messages for all the possible errors there. PatternSearch class handles - # all that. - def human_formatted_pattern_search_string - query_string = permitted_search_params.compact_blank.to_query - query_string.tr("=", ":").tr("&", " ").tr("%2C", "\\\\,") + PatternSearch::Name.params.keys + [:pattern] end end end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 45cd801ace..425f5c4e6a 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -1,15 +1,33 @@ # frozen_string_literal: true -# Observations pattern search form. +# Names pattern search form. # -# Route: `new_observation_search_path` +# Route: `new_name_search_path` # Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/observations/pattern_search", action: :create }` +# `{ controller: "/names/pattern_search", action: :create }` module Observations class PatternSearchController < ApplicationController + include ::PatternSearchable + before_action :login_required def new + @fields = observation_search_params + end + + def create + @pattern = human_formatted_pattern_search_string + redirect_to(controller: "/names", action: :index, pattern: @pattern) + end + + private + + def permitted_search_params + params.permit(observation_search_params) + end + + def observation_search_params + PatternSearch::Observation.params.keys + [:pattern] end end end diff --git a/config/routes.rb b/config/routes.rb index 527eb5896a..29867ea880 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -483,6 +483,11 @@ def route_actions_hash get("locations/map", to: "locations/maps#show", as: "map_locations") # ----- Names: a lot of actions ---------------------------- + namespace :names do + get("search/new", to: "pattern_search#new", as: "new_search") + post("search", to: "pattern_search#create", as: "search") + end + resources :names, id: /\d+/, shallow: true do # These routes are for dealing with name attributes. # They're not `resources` because they don't have their own IDs. @@ -567,15 +572,15 @@ def route_actions_hash as: "names_eol_preview") get("names/eol_expanded_review", to: "names/eol_data/expanded_review#show", as: "names_eol_expanded_review") - get("names/search/new", to: "names/pattern_search#new", as: "new_name_search") - post("names/search", to: "names/pattern_search#create", as: "name_search") # ----- Observations: standard actions ---------------------------- namespace :observations do resources :downloads, only: [:new, :create] - # Not under resources :observations because the obs doesn't have an id yet + get("search/new", to: "pattern_search#new", as: "new_search") + post("search", to: "pattern_search#create", as: "search") + # uploads are not under resources because the obs doesn't have an id yet get("images/uploads/new", to: "images/uploads#new", as: "new_image_upload_for") post("images/uploads", to: "images/uploads#create", From 8a21c441abfe7d7301070c7d982e8ea2f30335a3 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Thu, 5 Sep 2024 17:36:13 -0700 Subject: [PATCH 18/80] Smooth out observations --- .../observations/pattern_search_controller.rb | 3 ++- app/helpers/pattern_search_helper.rb | 4 ++++ .../observations/pattern_search/_form.erb | 19 ++++++++++--------- config/locales/en.txt | 6 +++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 425f5c4e6a..0374f2dc6f 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -17,7 +17,8 @@ def new def create @pattern = human_formatted_pattern_search_string - redirect_to(controller: "/names", action: :index, pattern: @pattern) + redirect_to(controller: "/observations", action: :index, + pattern: @pattern) end private diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index b87c2215a9..948512816f 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -27,6 +27,10 @@ def pattern_search_field_type_from_parser(**args) return :pattern if args[:field] == :pattern subclass = PatternSearch.const_get(args[:type].capitalize) + unless subclass.params[args[:field]] + raise "No parser defined for #{args[:field]} in #{subclass}" + end + parser = subclass.params[args[:field]][1] parser.to_s.gsub(/^parse_/, "").to_sym end diff --git a/app/views/controllers/observations/pattern_search/_form.erb b/app/views/controllers/observations/pattern_search/_form.erb index 64a8d25086..08fd226c74 100644 --- a/app/views/controllers/observations/pattern_search/_form.erb +++ b/app/views/controllers/observations/pattern_search/_form.erb @@ -1,17 +1,17 @@ <% fields_1 = { search: [:pattern], - date: [:date, :created, :modified], - taxon: [[:has_name, :name], [:lichen, :confidence], - [:include_subtaxa, :include_synonyms], - [:include_all_name_proposals, :exclude_consensus]], - location: [:location, :region, :has_public_lat_lng, :is_collection_location, - :east, :north, :south, :west], + name: [[:has_name, :lichen], :name, :confidence, + [:include_subtaxa, :include_synonyms], + [:include_all_name_proposals, :exclude_consensus]], + location: [:location, :region, [:has_public_lat_lng, :is_collection_location], + [:east, :west], [:north, :south]], }.freeze fields_2 = { + date: [:date, :created, :modified], detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], [:has_field, :notes], [:has_comments, :comments]], - associated: [:user, :herbarium, :list, :project, :project_lists, :field_slip], + connected: [:user, :herbarium, :list, :project, :project_lists, :field_slip], }.freeze %> @@ -31,12 +31,13 @@ fields_2 = { <% field.each do |subfield| %> <%= tag.div(class: "col-xs-12 col-lg-6") do %> <%= pattern_search_field(form: f, field: subfield, - type: :name) %> + type: :observation) %> <% end %> <% end %> <% end %> <% else %> - <%= pattern_search_field(form: f, field: field, type: :name) %> + <%= pattern_search_field(form: f, field: field, + type: :observation) %> <% end %> <% end %> diff --git a/config/locales/en.txt b/config/locales/en.txt index d8b4969c7a..e058b40d52 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -477,6 +477,7 @@ confidence_level: confidence level CONFIDENCE_LEVELS: Confidence Levels confidence_levels: confidence levels + CONNECTED_TO: Connected to CONSTRAINT_VIOLATIONS: Constraint Violations constraint_violations: constraint violations COORDINATES: Coordinates @@ -1180,6 +1181,9 @@ search_term_group_quality: "[:QUALITY]" search_term_group_scope: "[:SCOPE]" search_term_group_detail: "[:DETAIL]" + search_term_group_name: "[:NAME]" + search_term_group_location: "[:LOCATION]" + search_term_group_connected: "[:CONNECTED_TO]" # Words recognized in search bar. # e.g. "user:me" @@ -2849,7 +2853,7 @@ field_slip_other_codes: Other Codes field_slip_other_example: e.g., iNat ID field_slip_project_success: Field Slip associated with the [TITLE] Project - field_slip_quick_no_location: Quick create requires an existing location + field_slip_quick_no_location: Quick create requires an existing location field_slip_quick_no_name: Quick create requires an existing name field_slip_remove_observation: Removed [OBSERVATION] from [TITLE] Project From e43c5e80e122c4a33a2bd8c75641312d20b5a4d9 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 6 Sep 2024 00:30:37 -0700 Subject: [PATCH 19/80] permit hidden_id fields --- app/controllers/observations/pattern_search_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 0374f2dc6f..2c7d456647 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -27,8 +27,13 @@ def permitted_search_params params.permit(observation_search_params) end + # need to add :pattern to the list of params, plus the hidden_id fields + # of the autocompleters. def observation_search_params - PatternSearch::Observation.params.keys + [:pattern] + PatternSearch::Observation.params.keys + [ + :pattern, :name_id, :user_id, :location_id, :species_list_id, + :project_id, :herbarium_id + ] end end end From a2ae78415dfae38dd4a0237f9f1527835cde64e0 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 6 Sep 2024 17:09:01 -0700 Subject: [PATCH 20/80] Autocompleters - change hidden field id to match field + "_id" --- app/helpers/autocompleter_helper.rb | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/helpers/autocompleter_helper.rb b/app/helpers/autocompleter_helper.rb index 6e803a6271..46b4f263bd 100644 --- a/app/helpers/autocompleter_helper.rb +++ b/app/helpers/autocompleter_helper.rb @@ -141,29 +141,31 @@ def autocompleter_edit_box_button(args) ) end - # minimum args :form, :type. + # minimum args :form, :field. # Send :hidden to fill the id, :hidden_data to merge with hidden field data def autocompleter_hidden_field(**args) - return unless args[:form].present? && args[:type].present? + return unless args[:form].present? && args[:field].present? - model = autocompleter_type_to_model(args[:type]) + # model = autocompleter_type_to_model(args[:type]) data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {}) args[:form].hidden_field( - :"#{model}_id", + :"#{args[:field]}_id", value: args[:hidden_value], data:, class: "form-control", readonly: true ) end - def autocompleter_type_to_model(type) - case type - when :region - :location - when :clade - :name - else - type - end - end + # Hmm. On forms with both region and location autocompleters, + # these id fields would need to have different ids. + # def autocompleter_type_to_model(type) + # case type + # when :region + # :location + # when :clade + # :name + # else + # type + # end + # end def autocompleter_append(args) [autocompleter_dropdown, From ed6288a9687ef0f5e43f9bc10f60812f41d77988 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 6 Sep 2024 17:09:24 -0700 Subject: [PATCH 21/80] Update check_box_with_label to accept checked_value --- app/helpers/forms_helper.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 341d46f529..a87d9972f4 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -93,7 +93,9 @@ def check_box_with_label(**args) tag.div(class: wrap_class) do concat(args[:form].label(args[:field]) do - concat(args[:form].check_box(args[:field], opts)) + concat(args[:form].check_box(args[:field], opts, + args[:checked_value] || "1", + args[:unchecked_value] || "0")) concat(args[:label]) if args[:between].present? concat(tag.div(class: "d-inline-block ml-3") { args[:between] }) @@ -537,7 +539,8 @@ def separate_field_options_from_args(args, extras = []) exceptions = [ :form, :field, :label, :class, :width, :inline, :between, :label_after, :label_end, :append, :help, :addon, :optional, :required, :monospace, - :type, :wrap_data, :wrap_id, :button, :button_data + :type, :wrap_data, :wrap_id, :button, :button_data, :checked_value, + :unchecked_value ] + extras args.clone.except(*exceptions) From 4c1a4f0bc9b8d991d34abdafc6411a0888a99bce Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 6 Sep 2024 17:09:38 -0700 Subject: [PATCH 22/80] Field concatenation! --- .../concerns/pattern_searchable.rb | 100 +++++++++++++++--- .../names/pattern_search_controller.rb | 23 +++- .../observations/pattern_search_controller.rb | 38 ++++++- app/helpers/pattern_search_helper.rb | 19 ++-- .../names/pattern_search/_form.erb | 51 --------- .../controllers/names/pattern_search/new.erb | 3 +- .../observations/pattern_search/new.erb | 4 +- .../_pattern_search_form.erb} | 21 +--- 8 files changed, 157 insertions(+), 102 deletions(-) delete mode 100644 app/views/controllers/names/pattern_search/_form.erb rename app/views/controllers/{observations/pattern_search/_form.erb => shared/_pattern_search_form.erb} (59%) diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index e9721c201b..e2baaa1ea0 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -3,7 +3,18 @@ # # = PatternSearchable Concern # -# This is a module of reusable methods that can be included by controllers that +# This is a module of reusable methods included by controllers that handle +# "faceted" pattern searches per model, with separate inputs for each keyword. +# +# We're translating the params hash into the format that the user would have +# typed into the search box if they knew how to do that, because that's what +# the PatternSearch class expects to parse. The PatternSearch class then +# unpacks, validates and re-translates all these params into the actual params +# used by the Query class. This may seem roundabout: of course we do know the +# Query param names in advance, so we could theoretically just pass the values +# directly into Query and render the index. But we'd still have to be able to +# validate the input, and give messages for all the possible errors there. +# PatternSearch class handles all that. # ################################################################################ @@ -12,20 +23,79 @@ module PatternSearchable included do - # Roundabout: We're converting the params hash back into a normal query - # string to start with, and then we're translating the query string into the - # format that the user would have typed into the search box if they knew how - # to do that, because that's what the PatternSearch class expects to parse. - # The PatternSearch class then unpacks, validates and re-translates all - # these params into the actual params used by the Query class. This may seem - # odd: of course we do know the Query param names in advance, so we could - # theoretically just pass the values directly into Query and render the - # index. But we'd still have to be able to validate the input, and give - # messages for all the possible errors there. PatternSearch class handles - # all that. - def human_formatted_pattern_search_string - query_string = permitted_search_params.compact_blank.to_query - query_string.tr("=", ":").tr("&", " ").tr("%2C", "\\\\,") + def formatted_pattern_search_string + sift_and_restructure_pattern_params + keyword_strings = @sendable_params.map do |key, value| + "#{key}:#{value}" + end + keyword_strings.join(" ") + end + + def sift_and_restructure_pattern_params + @keywords = permitted_search_params.to_h.compact_blank.reject do |_, v| + v == "0" + end + concatenate_range_fields + @sendable_params = substitute_ids_for_names(@keywords) + # @storable_params = storable_params(@keywords) + end + + # Check for `fields_with_range`, and concatenate them if range val present, + # removing the range field. + def concatenate_range_fields + @keywords.each_key do |key| + next unless fields_with_range.include?(key.to_sym) && + @keywords[:"#{key}_range"].present? + + @keywords[key] = [@keywords[key].strip, + @keywords[:"#{key}_range"].strip].join("-") + @keywords.delete(:"#{key}_range") + end + end + + # Controller declares `fields_with_ids` which autocompleter send ids. + # This method substitutes the ids for the names. + def substitute_ids_for_names(keywords) + keywords.each_key do |key| + next unless fields_with_ids.include?(key.to_sym) && + keywords[:"#{key}_id"].present? + + keywords[key] = keywords[:"#{key}_id"] + keywords.delete(:"#{key}_id") + end + keywords + end + + def storable_params(keywords) + keywords = escape_names_and_remove_ids(keywords) + escape_locations_and_remove_ids(keywords) + end + + # Waiting to hear how this should be built. + def escape_names_and_remove_ids(keywords) + keywords.each_key do |key| + next unless fields_with_ids.include?(key.to_sym) && + keywords[:"#{key}_id"].present? + + list = keywords[key].split(",").map(&:strip) + list = list.map { |name| "\"#{name}\"" } + keywords[key] = list.join(",") + keywords.delete(:"#{key}_id") + end + keywords + end + + def escape_locations_and_remove_ids(keywords) + keywords.each_key do |key| + next unless [:location, :region].include?(key.to_sym) && + keywords[:"#{key}_id"].present? + + list = keywords[key].split(",").map(&:strip) + list = list.map { |location| "\"#{location.tr(",", "\\,")}\"" } + keywords[key] = list.join(",") + keywords.delete(:"#{key}_id") + end + keywords end end end diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index 9ffb651833..4d1aad7c9a 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -12,16 +12,35 @@ class PatternSearchController < ApplicationController before_action :login_required def new - @fields = name_search_params + @field_columns = name_field_groups end def create - @pattern = human_formatted_pattern_search_string + @pattern = formatted_pattern_search_string redirect_to(controller: "/names", action: :index, pattern: @pattern) end private + # This is the list of fields that are displayed in the search form. In the + # template, each hash is interpreted as a column, and each key is a panel + # with an array of fields or field pairings. + def name_field_groups + [ + { date: [:created, :modified], + quality: [[:has_observations, :deprecated], + [:has_author, :author], + [:has_citation, :citation]] }, + { scope: [[:has_synonyms, :include_synonyms], + [:include_subtaxa, :include_misspellings], + :rank, :lichen], + detail: [[:has_classification, :classification], + [:has_notes, :notes], + [:has_comments, :comments], + :has_description] } + ].freeze + end + def permitted_search_params params.permit(name_search_params) end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 2c7d456647..e54fbac343 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -12,17 +12,46 @@ class PatternSearchController < ApplicationController before_action :login_required def new - @fields = observation_search_params + @field_columns = observation_field_groups end def create - @pattern = human_formatted_pattern_search_string + @field_columns = observation_field_groups + @pattern = formatted_pattern_search_string + redirect_to(controller: "/observations", action: :index, pattern: @pattern) end private + # This is the list of fields that are displayed in the search form. In the + # template, each hash is interpreted as a column, and each key is a panel + # with an array of fields or field pairings. + def observation_field_groups + [ + { date: [:date, :created, :modified], + detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], + [:has_field, :notes], [:has_comments, :comments]], + connected: [:user, :herbarium, :list, :project, :project_lists, + :field_slip] }, + { name: [[:has_name, :lichen], :name, :confidence, + [:include_subtaxa, :include_synonyms], + [:include_all_name_proposals, :exclude_consensus]], + location: [:location, :region, + [:has_public_lat_lng, :is_collection_location], + [:east, :west], [:north, :south]] } + ].freeze + end + + def fields_with_range + [:date, :created, :modified, :rank] + end + + def fields_with_ids + [:name, :location, :user, :herbarium, :list, :project] + end + def permitted_search_params params.permit(observation_search_params) end @@ -31,8 +60,9 @@ def permitted_search_params # of the autocompleters. def observation_search_params PatternSearch::Observation.params.keys + [ - :pattern, :name_id, :user_id, :location_id, :species_list_id, - :project_id, :herbarium_id + :name_id, :user_id, :location_id, :list_id, + :project_id, :project_lists_id, :herbarium_id, + :date_range, :created_range, :modified_range, :rank_range ] end end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 948512816f..17d3465bc4 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -28,7 +28,7 @@ def pattern_search_field_type_from_parser(**args) subclass = PatternSearch.const_get(args[:type].capitalize) unless subclass.params[args[:field]] - raise "No parser defined for #{args[:field]} in #{subclass}" + raise("No parser defined for #{args[:field]} in #{subclass}") end parser = subclass.params[args[:field]][1] @@ -46,15 +46,17 @@ def pattern_search_field_type_from_parser(**args) string: { component: :text_field_with_label, args: {} }, list_of_strings: { component: :text_field_with_label, args: {} }, list_of_herbaria: { component: :autocompleter_field, - args: { type: :herbarium } }, + args: { type: :herbarium, separator: ", " } }, list_of_locations: { component: :autocompleter_field, - args: { type: :location } }, - list_of_names: { component: :autocompleter_field, args: { type: :name } }, + args: { type: :location, separator: ", " } }, + list_of_names: { component: :autocompleter_field, + args: { type: :name, separator: ", " } }, list_of_projects: { component: :autocompleter_field, - args: { type: :project } }, + args: { type: :project, separator: ", " } }, list_of_species_lists: { component: :autocompleter_field, - args: { type: :species_list } }, - list_of_users: { component: :autocompleter_field, args: { type: :user } }, + args: { type: :species_list, separator: ", " } }, + list_of_users: { component: :autocompleter_field, + args: { type: :user, separator: ", " } }, confidence: { component: :pattern_search_confidence_field, args: {} }, longitude: { component: :pattern_search_longitude_field, args: {} }, latitude: { component: :pattern_search_latitude_field, args: {} } @@ -74,7 +76,8 @@ def prepare_args_for_pattern_search_field(args, field_type, component) # FIELD HELPERS # def pattern_search_yes_field(**args) - check_box_with_label(value: "yes", **args) + check_box_with_label(checked_value: "yes", unchecked_value: "", + include_hidden: false, **args) end def pattern_search_boolean_field(**args) diff --git a/app/views/controllers/names/pattern_search/_form.erb b/app/views/controllers/names/pattern_search/_form.erb deleted file mode 100644 index 6220ef1e0f..0000000000 --- a/app/views/controllers/names/pattern_search/_form.erb +++ /dev/null @@ -1,51 +0,0 @@ -<% -fields_1 = { - search: [:pattern], - date: [:created, :modified], - quality: [[:has_observations, :deprecated], [:has_author, :author], - [:has_citation, :citation]] -} -fields_2 = { - scope: [[:has_synonyms, :include_synonyms], [:include_subtaxa, - :include_misspellings], :rank, :lichen], - detail: [[:has_classification, :classification], [:has_notes, :notes], - [:has_comments, :comments], :has_description] -} -%> - -<%= form_with(url: { action: :create }) do |f| %> - - <%= tag.div(class: "row") do %> - <% [fields_1, fields_2].each do |field_groups| %> - - <%= tag.div(class: "col-xs-12 col-md-6") do %> - <% field_groups.each do |heading, fields| %> - - <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> - <% fields.each do |field| %> - - <% if field.is_a?(Array) %> - <%= tag.div(class: "row") do %> - <% field.each do |subfield| %> - <%= tag.div(class: "col-xs-12 col-lg-6") do %> - <%= pattern_search_field(form: f, field: subfield, - type: :name) %> - <% end %> - <% end %> - <% end %> - <% else %> - <%= pattern_search_field(form: f, field: field, type: :name) %> - <% end %> - - <% end %> - <% end %> - - <% end %> - <% end %> - - <% end %> - <% end %> - - <%= submit_button(form: f, button: :SEARCH.l, center: true) %> - -<% end %> diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index 2cdfa3f5ae..3972f59a26 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -3,4 +3,5 @@ @container = :full %> -<%= render(partial: "names/pattern_search/form", locals: { local: true } ) %> +<%= render(partial: "shared/pattern_search_form", + locals: { local: true, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/pattern_search/new.erb b/app/views/controllers/observations/pattern_search/new.erb index 5f21408fde..3972f59a26 100644 --- a/app/views/controllers/observations/pattern_search/new.erb +++ b/app/views/controllers/observations/pattern_search/new.erb @@ -3,5 +3,5 @@ @container = :full %> -<%= render(partial: "observations/pattern_search/form", - locals: { local: true } ) %> +<%= render(partial: "shared/pattern_search_form", + locals: { local: true, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/pattern_search/_form.erb b/app/views/controllers/shared/_pattern_search_form.erb similarity index 59% rename from app/views/controllers/observations/pattern_search/_form.erb rename to app/views/controllers/shared/_pattern_search_form.erb index 08fd226c74..b4ac68812b 100644 --- a/app/views/controllers/observations/pattern_search/_form.erb +++ b/app/views/controllers/shared/_pattern_search_form.erb @@ -1,27 +1,10 @@ -<% -fields_1 = { - search: [:pattern], - name: [[:has_name, :lichen], :name, :confidence, - [:include_subtaxa, :include_synonyms], - [:include_all_name_proposals, :exclude_consensus]], - location: [:location, :region, [:has_public_lat_lng, :is_collection_location], - [:east, :west], [:north, :south]], -}.freeze -fields_2 = { - date: [:date, :created, :modified], - detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], - [:has_field, :notes], [:has_comments, :comments]], - connected: [:user, :herbarium, :list, :project, :project_lists, :field_slip], -}.freeze -%> - <%= form_with(url: { action: :create }) do |f| %> <%= tag.div(class: "row") do %> - <% [fields_1, fields_2].each do |field_groups| %> + <% field_columns.each do |panels| %> <%= tag.div(class: "col-xs-12 col-md-6") do %> - <% field_groups.each do |heading, fields| %> + <% panels.each do |heading, fields| %> <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> <% fields.each do |field| %> From 58c68cd6c12e55bd7058b2a3b7b907fcb0e95a91 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 7 Sep 2024 10:42:57 -0700 Subject: [PATCH 23/80] Update autocompleter_helper.rb --- app/helpers/autocompleter_helper.rb | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/helpers/autocompleter_helper.rb b/app/helpers/autocompleter_helper.rb index 46b4f263bd..74d57954c8 100644 --- a/app/helpers/autocompleter_helper.rb +++ b/app/helpers/autocompleter_helper.rb @@ -141,32 +141,20 @@ def autocompleter_edit_box_button(args) ) end - # minimum args :form, :field. - # Send :hidden to fill the id, :hidden_data to merge with hidden field data + # minimum args :form, :type. Send :hidden_name to override default field name, + # :hidden_value to fill id, :hidden_data to merge with hidden field data def autocompleter_hidden_field(**args) return unless args[:form].present? && args[:field].present? - # model = autocompleter_type_to_model(args[:type]) + # Default field name is "#{type}_id", so obs.place_name gets obs.location_id + id = args[:hidden_name] || :"#{args[:type]}_id" data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {}) args[:form].hidden_field( - :"#{args[:field]}_id", + id, value: args[:hidden_value], data:, class: "form-control", readonly: true ) end - # Hmm. On forms with both region and location autocompleters, - # these id fields would need to have different ids. - # def autocompleter_type_to_model(type) - # case type - # when :region - # :location - # when :clade - # :name - # else - # type - # end - # end - def autocompleter_append(args) [autocompleter_dropdown, autocompleter_hidden_field(**args)].safe_join From c3bb665988669736fe7e64e61fe6cd755b2dd593 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 7 Sep 2024 12:33:55 -0700 Subject: [PATCH 24/80] Update pattern_search_controller.rb --- app/controllers/observations/pattern_search_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index e54fbac343..bb17defeea 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -49,7 +49,7 @@ def fields_with_range end def fields_with_ids - [:name, :location, :user, :herbarium, :list, :project] + [:name, :location, :user, :herbarium, :list, :project, :project_lists] end def permitted_search_params From b16abdaf04cae9e3eaafe79cf33ff0aa1517e10c Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 7 Sep 2024 12:41:55 -0700 Subject: [PATCH 25/80] Update pattern_search_helper.rb --- app/helpers/pattern_search_helper.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 17d3465bc4..05f725ed99 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -35,7 +35,9 @@ def pattern_search_field_type_from_parser(**args) parser.to_s.gsub(/^parse_/, "").to_sym end - # Convenience for subclasses to access helper methods via PARAMS + PATTERN_SEARCH_SEPARATOR = ", " + + # Convenience for subclasses to access helper methods via subclass.params PATTERN_SEARCH_FIELD_HELPERS = { pattern: { component: :text_field_with_label, args: {} }, yes: { component: :pattern_search_yes_field, args: {} }, @@ -46,15 +48,20 @@ def pattern_search_field_type_from_parser(**args) string: { component: :text_field_with_label, args: {} }, list_of_strings: { component: :text_field_with_label, args: {} }, list_of_herbaria: { component: :autocompleter_field, - args: { type: :herbarium, separator: ", " } }, + args: { type: :herbarium, + separator: PATTERN_SEARCH_SEPARATOR } }, list_of_locations: { component: :autocompleter_field, - args: { type: :location, separator: ", " } }, + args: { type: :location, + separator: PATTERN_SEARCH_SEPARATOR } }, list_of_names: { component: :autocompleter_field, - args: { type: :name, separator: ", " } }, + args: { type: :name, + separator: PATTERN_SEARCH_SEPARATOR } }, list_of_projects: { component: :autocompleter_field, - args: { type: :project, separator: ", " } }, + args: { type: :project, + separator: PATTERN_SEARCH_SEPARATOR } }, list_of_species_lists: { component: :autocompleter_field, - args: { type: :species_list, separator: ", " } }, + args: { type: :species_list, + separator: PATTERN_SEARCH_SEPARATOR } }, list_of_users: { component: :autocompleter_field, args: { type: :user, separator: ", " } }, confidence: { component: :pattern_search_confidence_field, args: {} }, From 56fdeaa3324709ec9aa1da08e7b9cead560a51d6 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Thu, 12 Sep 2024 02:00:08 -0700 Subject: [PATCH 26/80] Add help text to pattern search fields --- app/helpers/forms_helper.rb | 5 +++-- app/helpers/pattern_search_helper.rb | 10 ++++++++-- app/views/controllers/names/pattern_search/new.erb | 3 ++- .../controllers/observations/pattern_search/new.erb | 3 ++- app/views/controllers/shared/_pattern_search_form.erb | 6 ++++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 31e5154ddd..d1977921e1 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -488,7 +488,8 @@ def form_group_wrap_class(args, base = "form-group") end def field_label_opts(args) - label_opts = { class: "mr-3" } + label_opts = {} + label_opts[:class] = args[:help].present? ? "" : "mr-3" label_opts[:index] = args[:index] if args[:index].present? label_opts end @@ -520,7 +521,7 @@ def check_for_help_block(args) ].join("_") args[:between] = capture do concat(args[:between]) - concat(collapse_info_trigger(id)) + concat(collapse_info_trigger(id, class: "mx-3")) end args[:append] = capture do concat(args[:append]) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 05f725ed99..2a7c97e7e7 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -76,10 +76,14 @@ def prepare_args_for_pattern_search_field(args, field_type, component) if component == :text_field_with_label && args[:field] != :pattern args[:inline] = true end + args[:help] = pattern_search_help_text(args) PATTERN_SEARCH_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) end + def pattern_search_help_text(args) + :"#{args[:type]}_term_#{args[:field]}".l + end # FIELD HELPERS # def pattern_search_yes_field(**args) @@ -117,7 +121,8 @@ def pattern_search_date_range_field(**args) end, tag.div(class: "col-xs-12 col-sm-6") do text_field_with_label(**args.merge( - { label: :to.l, between: :optional, field: "#{args[:field]}_range" } + { label: :to.l, between: :optional, help: nil, + field: "#{args[:field]}_range" } )) end ].safe_join @@ -132,7 +137,8 @@ def pattern_search_rank_range_field(**args) end, tag.div(class: "col-xs-12 col-sm-6") do select_with_label(options: Name.all_ranks, **args.merge( - { label: :to.l, between: :optional, field: "#{args[:field]}_range" } + { label: :to.l, between: :optional, help: nil, + field: "#{args[:field]}_range" } )) end ].safe_join diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/pattern_search/new.erb index 3972f59a26..b2f0403141 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/pattern_search/new.erb @@ -4,4 +4,5 @@ %> <%= render(partial: "shared/pattern_search_form", - locals: { local: true, field_columns: @field_columns } ) %> + locals: { local: true, search_type: :name, + field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/pattern_search/new.erb b/app/views/controllers/observations/pattern_search/new.erb index 3972f59a26..e4a76b5c93 100644 --- a/app/views/controllers/observations/pattern_search/new.erb +++ b/app/views/controllers/observations/pattern_search/new.erb @@ -4,4 +4,5 @@ %> <%= render(partial: "shared/pattern_search_form", - locals: { local: true, field_columns: @field_columns } ) %> + locals: { local: true, search_type: :observation, + field_columns: @field_columns } ) %> diff --git a/app/views/controllers/shared/_pattern_search_form.erb b/app/views/controllers/shared/_pattern_search_form.erb index b4ac68812b..dcd7da4e8c 100644 --- a/app/views/controllers/shared/_pattern_search_form.erb +++ b/app/views/controllers/shared/_pattern_search_form.erb @@ -1,3 +1,5 @@ +<%# locals: (field_columns: [], search_type: nil, local: true) -%> + <%= form_with(url: { action: :create }) do |f| %> <%= tag.div(class: "row") do %> @@ -14,13 +16,13 @@ <% field.each do |subfield| %> <%= tag.div(class: "col-xs-12 col-lg-6") do %> <%= pattern_search_field(form: f, field: subfield, - type: :observation) %> + type: search_type) %> <% end %> <% end %> <% end %> <% else %> <%= pattern_search_field(form: f, field: field, - type: :observation) %> + type: search_type) %> <% end %> <% end %> From b0ae5887de87ca684015c51687816df66ad9a027 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 13 Sep 2024 10:25:06 -0700 Subject: [PATCH 27/80] Maybe switch to date selects --- app/controllers/concerns/pattern_searchable.rb | 1 + .../names/pattern_search_controller.rb | 16 +++++++++++++++- .../observations/pattern_search_controller.rb | 10 ++++++++-- app/helpers/pattern_search_helper.rb | 14 ++++++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index e2baaa1ea0..f5c69aadcf 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -43,6 +43,7 @@ def sift_and_restructure_pattern_params # Check for `fields_with_range`, and concatenate them if range val present, # removing the range field. def concatenate_range_fields + debugger @keywords.each_key do |key| next unless fields_with_range.include?(key.to_sym) && @keywords[:"#{key}_range"].present? diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index 4d1aad7c9a..dce6647b79 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -41,12 +41,26 @@ def name_field_groups ].freeze end + def fields_with_range + [:created, :modified, :rank] + end + + def fields_with_ids + [] + end + def permitted_search_params params.permit(name_search_params) end def name_search_params - PatternSearch::Name.params.keys + [:pattern] + PatternSearch::Name.params.keys + [ + "created(1i)", "created(2i)", "created(3i)", + "created_range(1i)", "created_range(2i)", "created_range(3i)", + "modified(1i)", "modified(2i)", "modified(3i)", + "modified_range(1i)", "modified_range(2i)", "modified_range(3i)", + :rank_range + ] end end end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index bb17defeea..a82634e1e0 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -49,7 +49,7 @@ def fields_with_range end def fields_with_ids - [:name, :location, :user, :herbarium, :list, :project, :project_lists] + [:name, :location, :user, :herbarium, :list, :project, :species_list] end def permitted_search_params @@ -62,7 +62,13 @@ def observation_search_params PatternSearch::Observation.params.keys + [ :name_id, :user_id, :location_id, :list_id, :project_id, :project_lists_id, :herbarium_id, - :date_range, :created_range, :modified_range, :rank_range + :date_range, :created_range, :modified_range, :rank_range, + "[date(1i)]", "[date(2i)]", "[date(3i)]", + "[date_range(1i)]", "[date_range(2i)]", "[date_range(3i)]", + "[created(1i)]", "[created(2i)]", "[created(3i)]", + "[created_range(1i)]", "[created_range(2i)]", "[created_range(3i)]", + "[modified(1i)]", "[modified(2i)]", "[modified(3i)]", + "[modified_range(1i)]", "[modified_range(2i)]", "[modified_range(3i)]" ] end end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 2a7c97e7e7..b6ea1bf327 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -84,6 +84,7 @@ def prepare_args_for_pattern_search_field(args, field_type, component) def pattern_search_help_text(args) :"#{args[:type]}_term_#{args[:field]}".l end + # FIELD HELPERS # def pattern_search_yes_field(**args) @@ -117,12 +118,17 @@ def pattern_search_date_range_field(**args) tag.div(class: "row") do [ tag.div(class: "col-xs-12 col-sm-6") do - text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) + # text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) + date_select_with_label(**args.merge( + { between: "(YYYY-MM-DD)", include_blank: true, + selected: 0, order: [:year, :month, :day] } + )) end, tag.div(class: "col-xs-12 col-sm-6") do - text_field_with_label(**args.merge( - { label: :to.l, between: :optional, help: nil, - field: "#{args[:field]}_range" } + date_select_with_label(**args.merge( + { field: "#{args[:field]}_range", label: :to.l, + between: :optional, help: nil, include_blank: true, + selected: 0, order: [:year, :month, :day] } )) end ].safe_join From 54bbfe1248f4f69bec9af1fca619677bfc1ff561 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 13 Sep 2024 11:23:51 -0700 Subject: [PATCH 28/80] Change "yes field" to select --- app/helpers/pattern_search_helper.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 2a7c97e7e7..63c10515e3 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -87,8 +87,11 @@ def pattern_search_help_text(args) # FIELD HELPERS # def pattern_search_yes_field(**args) - check_box_with_label(checked_value: "yes", unchecked_value: "", - include_hidden: false, **args) + options = [ + ["", nil], + ["yes", "yes"] + ] + select_with_label(options:, inline: true, **args) end def pattern_search_boolean_field(**args) From 806c0c388d43e043fc12d488314177e45b0716cf Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 14 Sep 2024 14:24:02 -0700 Subject: [PATCH 29/80] Okay, dates are working --- .../concerns/pattern_searchable.rb | 28 +++++++++++++++++-- .../names/pattern_search_controller.rb | 14 ++++++---- .../observations/pattern_search_controller.rb | 25 ++++++++++------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index f5c69aadcf..4b85b463a7 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -31,19 +31,43 @@ def formatted_pattern_search_string keyword_strings.join(" ") end + # One oddball is `confidence` - the string "0" should not count as a value. def sift_and_restructure_pattern_params @keywords = permitted_search_params.to_h.compact_blank.reject do |_, v| - v == "0" + v == "0" || incomplete_date?(v) end + format_date_params_into_strings concatenate_range_fields @sendable_params = substitute_ids_for_names(@keywords) # @storable_params = storable_params(@keywords) end + def incomplete_date?(value) + value.is_a?(Hash) && value.values.any?(&:blank?) + end + + # Deal with date fields, which are stored as hashes with year, month, day. + # Convert them to a single string. Can use `web_date` method on date fields. + def format_date_params_into_strings + @keywords.each_key do |key| + next unless fields_with_dates.include?(key.to_sym) + next if @keywords[key][:year].blank? + + @keywords[key] = date_into_string(key) + if @keywords[:"#{key}_range"].present? + @keywords[:"#{key}_range"] = date_into_string(:"#{key}_range") + end + end + end + + # date is a hash with year, month, day. Convert to string. + def date_into_string(key) + Date.new(*permitted_search_params.to_h[key].values.map(&:to_i)).web_date + end + # Check for `fields_with_range`, and concatenate them if range val present, # removing the range field. def concatenate_range_fields - debugger @keywords.each_key do |key| next unless fields_with_range.include?(key.to_sym) && @keywords[:"#{key}_range"].present? diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index dce6647b79..f769cda3b0 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -41,6 +41,10 @@ def name_field_groups ].freeze end + def fields_with_dates + [:created, :modified] + end + def fields_with_range [:created, :modified, :rank] end @@ -50,16 +54,14 @@ def fields_with_ids end def permitted_search_params - params.permit(name_search_params) + params.permit(name_search_params + [ + { created: {} }, { modified: {} } + ]) end def name_search_params PatternSearch::Name.params.keys + [ - "created(1i)", "created(2i)", "created(3i)", - "created_range(1i)", "created_range(2i)", "created_range(3i)", - "modified(1i)", "modified(2i)", "modified(3i)", - "modified_range(1i)", "modified_range(2i)", "modified_range(3i)", - :rank_range + :created_range, :modified_range, :rank_range ] end end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index a82634e1e0..479d759d9b 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -44,6 +44,10 @@ def observation_field_groups ].freeze end + def fields_with_dates + [:date, :created, :modified] + end + def fields_with_range [:date, :created, :modified, :rank] end @@ -53,22 +57,23 @@ def fields_with_ids end def permitted_search_params - params.permit(observation_search_params) + params.permit(observation_search_params + [ + { date: [:year, :month, :day] }, + { date_range: [:year, :month, :day] }, + { created: [:year, :month, :day] }, + { created_range: [:year, :month, :day] }, + { modified: [:year, :month, :day] }, + { modified_range: [:year, :month, :day] } + ]) end # need to add :pattern to the list of params, plus the hidden_id fields # of the autocompleters. def observation_search_params PatternSearch::Observation.params.keys + [ - :name_id, :user_id, :location_id, :list_id, - :project_id, :project_lists_id, :herbarium_id, - :date_range, :created_range, :modified_range, :rank_range, - "[date(1i)]", "[date(2i)]", "[date(3i)]", - "[date_range(1i)]", "[date_range(2i)]", "[date_range(3i)]", - "[created(1i)]", "[created(2i)]", "[created(3i)]", - "[created_range(1i)]", "[created_range(2i)]", "[created_range(3i)]", - "[modified(1i)]", "[modified(2i)]", "[modified(3i)]", - "[modified_range(1i)]", "[modified_range(2i)]", "[modified_range(3i)]" + :name_id, :user_id, :location_id, :list_id, :project_id, + :project_lists_id, :herbarium_id, :species_list_id, + :rank_range ] end end From ecdf1c380eac82fbc82a68a4840ede092ccef4e5 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 14 Sep 2024 16:04:40 -0700 Subject: [PATCH 30/80] Adjust the hidden name of the autocompleters on the form --- app/controllers/concerns/pattern_searchable.rb | 15 ++++++++++++--- .../observations/pattern_search_controller.rb | 5 ++--- app/helpers/pattern_search_helper.rb | 13 +++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index 4b85b463a7..337c572062 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -24,7 +24,7 @@ module PatternSearchable included do def formatted_pattern_search_string - sift_and_restructure_pattern_params + sift_and_restructure_form_params keyword_strings = @sendable_params.map do |key, value| "#{key}:#{value}" end @@ -32,7 +32,7 @@ def formatted_pattern_search_string end # One oddball is `confidence` - the string "0" should not count as a value. - def sift_and_restructure_pattern_params + def sift_and_restructure_form_params @keywords = permitted_search_params.to_h.compact_blank.reject do |_, v| v == "0" || incomplete_date?(v) end @@ -78,6 +78,9 @@ def concatenate_range_fields end end + # SENDABLE_PARAMS + # These methods don't modify the original @keywords hash. + # # Controller declares `fields_with_ids` which autocompleter send ids. # This method substitutes the ids for the names. def substitute_ids_for_names(keywords) @@ -91,12 +94,17 @@ def substitute_ids_for_names(keywords) keywords end + # STORABLE_PARAMS + # These methods don't modify the original @keywords hash. + # + # Store full strings for all values, including names and locations, + # so we can repopulate the form with the same values. def storable_params(keywords) keywords = escape_names_and_remove_ids(keywords) escape_locations_and_remove_ids(keywords) end - # Waiting to hear how this should be built. + # Escape-quote the names, the way the short form requires. def escape_names_and_remove_ids(keywords) keywords.each_key do |key| next unless fields_with_ids.include?(key.to_sym) && @@ -110,6 +118,7 @@ def escape_names_and_remove_ids(keywords) keywords end + # Escape-quote the locations and their commas. def escape_locations_and_remove_ids(keywords) keywords.each_key do |key| next unless [:location, :region].include?(key.to_sym) && diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 479d759d9b..bdf6d30a90 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -71,9 +71,8 @@ def permitted_search_params # of the autocompleters. def observation_search_params PatternSearch::Observation.params.keys + [ - :name_id, :user_id, :location_id, :list_id, :project_id, - :project_lists_id, :herbarium_id, :species_list_id, - :rank_range + :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, + :project_lists_id, :rank_range ] end end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 61ee73743b..f63e126888 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -77,6 +77,7 @@ def prepare_args_for_pattern_search_field(args, field_type, component) args[:inline] = true end args[:help] = pattern_search_help_text(args) + args[:hidden_name] = pattern_search_check_for_hidden_name(args) PATTERN_SEARCH_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) end @@ -85,6 +86,18 @@ def pattern_search_help_text(args) :"#{args[:type]}_term_#{args[:field]}".l end + # Overrides for the assumed name of the id field for autocompleter. + def pattern_search_check_for_hidden_name(args) + case args[:field] + when :list + "list_id" + when :project_lists + "project_lists_id" + else + nil + end + end + # FIELD HELPERS # def pattern_search_yes_field(**args) From 564bd6bcf7e21d24d1929f0e48cfbfc448b8e439 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 15 Sep 2024 21:20:43 -0700 Subject: [PATCH 31/80] Use form `SearchFilter` models --- app/classes/pattern_search/base.rb | 86 ++++++++++++++++++- app/classes/pattern_search/observation.rb | 2 +- .../observations/pattern_search_controller.rb | 30 ++++--- app/helpers/pattern_search_helper.rb | 36 +++++--- app/models/name_filter.rb | 19 ++++ app/models/observation_filter.rb | 19 ++++ app/models/search_filter.rb | 7 ++ app/models/translation_string.rb | 12 +++ .../shared/_pattern_search_form.erb | 4 +- config/locales/en.txt | 5 +- 10 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 app/models/name_filter.rb create mode 100644 app/models/observation_filter.rb create mode 100644 app/models/search_filter.rb diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index bee1a82a59..6f8229a57d 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -3,17 +3,19 @@ module PatternSearch # Base class for PatternSearch; handles everything but build_query class Base - attr_accessor :errors, :parser, :flavor, :args, :query + attr_accessor :errors, :parser, :flavor, :args, :query, :form_params def initialize(string) self.errors = [] self.parser = PatternSearch::Parser.new(string) + self.form_params = make_terms_available_to_faceted_form build_query self.query = Query.lookup(model.name.to_sym, flavor, args) rescue Error => e errors << e end + # rubocop:disable Metrics/AbcSize def build_query self.flavor = :all self.args = {} @@ -33,6 +35,7 @@ def build_query end end end + # rubocop:enable Metrics/AbcSize def help_message "#{:pattern_search_terms_help.l}\n#{self.class.terms_help}" @@ -54,5 +57,86 @@ def lookup_param(var) end nil end + + # Build a hash so we can populate the form fields with from the values from + # the saved search string. Turn ranges into ranges, and dates into dates. + # NOTE: The terms may be translated! We have to look up the param names that + # the translations map to. + # rubocop:disable Metrics/AbcSize + def make_terms_available_to_faceted_form + parser.terms.each_with_object({}) do |term, hash| + param = lookup_param_name(term.var) + if fields_with_dates.include?(param) + # term is what the user typed in, not the parsed value. + start, range = check_for_date_range(term) + hash[param] = start + hash[:"#{param}_range"] = range if range + elsif fields_with_numeric_range.include?(param) + start, range = check_for_numeric_range(term) + hash[param] = start + hash[:"#{param}_range"] = range if range + else + hash[param] = term.vals.join(", ") + end + end + end + # rubocop:enable Metrics/AbcSize + + def lookup_param_name(var) + # See if this var matches an English parameter name first. + return var if params[var].present? + + # Then check if any of the translated parameter names match. + params.each_key do |key| + return key if var.to_s == :"search_term_#{key}".l.tr(" ", "_") + end + nil + end + + # The string could be a date string like "2010-01-01", or a range string + # like "2010-01-01-2010-01-31", or "2023-2024", or "08-10". + # If it is a range, return the two dates. + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + def check_for_date_range(term) + bits = term.vals[0].split("-") + + case bits.size + when 2 + start = Date.new(*bits[0].map(&:to_i)) + range = Date.new(*bits[1].map(&:to_i)) + [start, range] + when 6 + start = Date.new(*bits[0..2].map(&:to_i)) + range = Date.new(*bits[3..5].map(&:to_i)) + [start, range] + when 3 + start = Date.new(*bits.map(&:to_i)) + [start, nil] + else + [nil, nil] + end + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + + def check_for_numeric_range(term) + bits = term.vals[0].split("-") + + if bits.size == 2 + [bits[0].to_i, bits[1].to_i] + else + [nil, nil] + end + end + + # These are set in the subclasses, but seem stable enough to be here. + def fields_with_dates + [:when, :created, :modified].freeze + end + + def fields_with_numeric_range + [:rank].freeze + end end end diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index 18529f86eb..0338dfa71d 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -5,7 +5,7 @@ module PatternSearch class Observation < Base PARAMS = { # dates / times - date: [:date, :parse_date_range], + when: [:date, :parse_date_range], created: [:created_at, :parse_date_range], modified: [:updated_at, :parse_date_range], diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index bdf6d30a90..f6aa5fe275 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -13,12 +13,20 @@ class PatternSearchController < ApplicationController def new @field_columns = observation_field_groups + terms = PatternSearch::Observation.new(session[:pattern]).form_params + + @filter = if session[:pattern] + ObservationFilter.new(terms) + else + ObservationFilter.new + end end def create @field_columns = observation_field_groups @pattern = formatted_pattern_search_string + # This will save the pattern in the session. redirect_to(controller: "/observations", action: :index, pattern: @pattern) end @@ -30,26 +38,26 @@ def create # with an array of fields or field pairings. def observation_field_groups [ - { date: [:date, :created, :modified], - detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], - [:has_field, :notes], [:has_comments, :comments]], - connected: [:user, :herbarium, :list, :project, :project_lists, - :field_slip] }, - { name: [[:has_name, :lichen], :name, :confidence, + { date: [:when, :created, :modified], + name: [:name, :confidence, [:has_name, :lichen], [:include_subtaxa, :include_synonyms], [:include_all_name_proposals, :exclude_consensus]], location: [:location, :region, [:has_public_lat_lng, :is_collection_location], - [:east, :west], [:north, :south]] } + [:east, :west], [:north, :south]] }, + { detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], + [:has_field, :notes], [:has_comments, :comments]], + connected: [:user, :herbarium, :list, :project, :project_lists, + :field_slip] } ].freeze end def fields_with_dates - [:date, :created, :modified] + [:when, :created, :modified] end def fields_with_range - [:date, :created, :modified, :rank] + [:when, :created, :modified, :rank] end def fields_with_ids @@ -58,8 +66,8 @@ def fields_with_ids def permitted_search_params params.permit(observation_search_params + [ - { date: [:year, :month, :day] }, - { date_range: [:year, :month, :day] }, + { when: [:year, :month, :day] }, + { when_range: [:year, :month, :day] }, { created: [:year, :month, :day] }, { created_range: [:year, :month, :day] }, { modified: [:year, :month, :day] }, diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index f63e126888..2a8fcd94a6 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -76,26 +76,29 @@ def prepare_args_for_pattern_search_field(args, field_type, component) if component == :text_field_with_label && args[:field] != :pattern args[:inline] = true end - args[:help] = pattern_search_help_text(args) + args[:help] = pattern_search_help_text(args, field_type) args[:hidden_name] = pattern_search_check_for_hidden_name(args) PATTERN_SEARCH_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) end - def pattern_search_help_text(args) - :"#{args[:type]}_term_#{args[:field]}".l + def pattern_search_help_text(args, field_type) + component = PATTERN_SEARCH_FIELD_HELPERS[field_type][:component] + multiple_note = if component == :autocompleter_field + :pattern_search_terms_multiple.l + end + [:"#{args[:type]}_term_#{args[:field]}".l, multiple_note].compact.join(" ") end # Overrides for the assumed name of the id field for autocompleter. def pattern_search_check_for_hidden_name(args) case args[:field] when :list - "list_id" + return "list_id" when :project_lists - "project_lists_id" - else - nil + return "project_lists_id" end + nil end # FIELD HELPERS @@ -133,14 +136,14 @@ def pattern_search_yes_no_both_field(**args) def pattern_search_date_range_field(**args) tag.div(class: "row") do [ - tag.div(class: "col-xs-12 col-sm-6") do + tag.div(class: pattern_search_columns) do # text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) date_select_with_label(**args.merge( { between: "(YYYY-MM-DD)", include_blank: true, selected: 0, order: [:year, :month, :day] } )) end, - tag.div(class: "col-xs-12 col-sm-6") do + tag.div(class: pattern_search_columns) do date_select_with_label(**args.merge( { field: "#{args[:field]}_range", label: :to.l, between: :optional, help: nil, include_blank: true, @@ -154,12 +157,15 @@ def pattern_search_date_range_field(**args) def pattern_search_rank_range_field(**args) tag.div(class: "row") do [ - tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Name.all_ranks, **args) + tag.div(class: pattern_search_columns) do + select_with_label(**args.merge( + { options: Name.all_ranks, include_blank: true, selected: 0 } + )) end, - tag.div(class: "col-xs-12 col-sm-6") do - select_with_label(options: Name.all_ranks, **args.merge( + tag.div(class: pattern_search_columns) do + select_with_label(**args.merge( { label: :to.l, between: :optional, help: nil, + options: Name.all_ranks, include_blank: true, selected: 0, field: "#{args[:field]}_range" } )) end @@ -178,4 +184,8 @@ def pattern_search_longitude_field(**args) def pattern_search_latitude_field(**args) text_field_with_label(**args.merge(between: "(-90.0 to 90.0)")) end + + def pattern_search_columns + "col-xs-12 col-sm-6 col-md-12 col-lg-6" + end end diff --git a/app/models/name_filter.rb b/app/models/name_filter.rb new file mode 100644 index 0000000000..b5832e22de --- /dev/null +++ b/app/models/name_filter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Non-AR model for the faceted PatternSearch form. +class NameFilter < SearchFilter + PatternSearch::Name.params.map do |keyword, values| + case values[1] + when :parse_date_range + attribute(keyword, :date) + attribute(:"#{keyword}_range", :date) + when :parse_rank_range + attribute(keyword, :integer) + attribute(:"#{keyword}_range", :integer) + when :parse_confidence + attribute(keyword, :integer) + else + attribute(keyword, :string) + end + end +end diff --git a/app/models/observation_filter.rb b/app/models/observation_filter.rb new file mode 100644 index 0000000000..c367dfa7d3 --- /dev/null +++ b/app/models/observation_filter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Non-AR model for the faceted PatternSearch form. +class ObservationFilter < SearchFilter + # Assign attributes from the PatternSearch::Observation.params hash + PatternSearch::Observation.params.map do |keyword, values| + case values[1] + when :parse_date_range + attribute(keyword, :date) + attribute(:"#{keyword}_range", :date) + when :parse_confidence + attribute(keyword, :integer) + when :parse_longitude, :parse_latitude + attribute(keyword, :float) + else + attribute(keyword, :string) + end + end +end diff --git a/app/models/search_filter.rb b/app/models/search_filter.rb new file mode 100644 index 0000000000..d7e904f339 --- /dev/null +++ b/app/models/search_filter.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Non-AR model for the faceted PatternSearch form. +class SearchFilter + include ActiveModel::Model + include ActiveModel::Attributes +end diff --git a/app/models/translation_string.rb b/app/models/translation_string.rb index 9708319f76..db28bbb1a5 100644 --- a/app/models/translation_string.rb +++ b/app/models/translation_string.rb @@ -14,6 +14,18 @@ # updated_at:: DateTime it was last updated. # user:: User who last updated it. # +# == Methods +# +# translations(locale):: Get all translations for a given locale. +# update_localization:: Update the translation in the I18n backend. +# store_localization:: Store the translation in the I18n backend. +# +# == Class Methods +# +# banner_time:: Get age of official language's banner. +# store_localizations(locale, hash_of_tags_and_texts):: Batch update. +# rename_tags(tags):: Rename a hash of tags in the database. +# # == Versions # # ActsAsVersioned tracks changes in +text+, +updated_at+, and +user+. diff --git a/app/views/controllers/shared/_pattern_search_form.erb b/app/views/controllers/shared/_pattern_search_form.erb index dcd7da4e8c..ea69c78452 100644 --- a/app/views/controllers/shared/_pattern_search_form.erb +++ b/app/views/controllers/shared/_pattern_search_form.erb @@ -1,6 +1,6 @@ <%# locals: (field_columns: [], search_type: nil, local: true) -%> -<%= form_with(url: { action: :create }) do |f| %> +<%= form_with(model: @filter, url: { action: :create }) do |f| %> <%= tag.div(class: "row") do %> <% field_columns.each do |panels| %> @@ -14,7 +14,7 @@ <% if field.is_a?(Array) %> <%= tag.div(class: "row") do %> <% field.each do |subfield| %> - <%= tag.div(class: "col-xs-12 col-lg-6") do %> + <%= tag.div(class: pattern_search_columns) do %> <%= pattern_search_field(form: f, field: subfield, type: search_type) %> <% end %> diff --git a/config/locales/en.txt b/config/locales/en.txt index f49d3912d8..8033dfa11e 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -1137,7 +1137,7 @@ search_term_comments: comments search_term_confidence: confidence search_term_created: created - search_term_date: date + search_term_when: date search_term_deprecated: deprecated search_term_east: east search_term_exclude_consensus: exclude_consensus @@ -3622,8 +3622,9 @@ # Search bar help pattern_search_terms_help: "Your search string may contain terms of the form \"variable:value\", where value can be quoted, and in some cases can contain more than one value separated by commas. Recognized variables include:" + pattern_search_terms_multiple: Separate multiple values with commas. - observation_term_date: Date mushroom was observed; YYYY-MM-DD, YYYY or MM; ranges okay. + observation_term_when: Date mushroom was observed; YYYY-MM-DD, YYYY or MM; ranges okay. observation_term_created: Date observation was first posted. observation_term_modified: Date observation was last modified. observation_term_name: Consensus name is one of these names. From 7c0061ffdc119c77e5fcf7a1b8670af856f01d34 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 15 Sep 2024 21:20:59 -0700 Subject: [PATCH 32/80] Allow blanks in selects --- app/helpers/forms_helper.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 4d0b4babf2..86aa8c9690 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -234,6 +234,7 @@ def text_label_row(args, label_opts) def select_with_label(**args) args = auto_label_if_form_is_account_prefs(args) args = select_generate_default_options(args) + select_opts = select_opts(args) args = check_for_optional_or_required_note(args) args = check_for_help_block(args) @@ -250,21 +251,24 @@ def select_with_label(**args) concat(args[:form].label(args[:field], args[:label], label_opts)) concat(args[:between]) if args[:between].present? concat(args[:form].select(args[:field], args[:options], - args[:select_opts], opts)) + select_opts, opts)) concat(args[:append]) if args[:append].present? end end # default select_opts - also generate year options if start_year given def select_generate_default_options(args) - args[:select_opts] ||= (args[:value] ? { selected: args[:value] } : {}) - return args unless args[:start_year].present? && args[:end_year].present? args[:options] = args[:end_year].downto(args[:start_year]) args end + def select_opts(args) + selected = args[:value] || args[:default] || "" + { selected:, include_blank: args[:include_blank] } + end + # MO mostly uses year-input_controller to switch the year selects to # text inputs, but you can pass data: { controller: "" } to get a year select. # The three "selects" will always be inline, but pass inline: true to make @@ -273,6 +277,7 @@ def select_generate_default_options(args) # it identifies the wrapping div. (That's also valid HTML.) # https://stackoverflow.com/a/16426122/3357635 def date_select_with_label(**args) + args = check_for_optional_or_required_note(args) args = check_for_help_block(args) opts = separate_field_options_from_args(args, [:object, :data]) opts[:class] = "form-control" @@ -300,8 +305,10 @@ def date_select_opts(args = {}) selected = args[:selected] || Time.zone.today # The field may not be an attribute of the object if obj.present? && obj.respond_to?(field) - init_year = obj.try(&field).try(&:year) - selected = obj.try(&field) || Time.zone.today + init_year = obj.try(&field.to_sym).try(&:year) + selected = obj.try(&field.to_sym) + # Keep blank fields blank on search filters + selected ||= Time.zone.today unless obj.is_a?(SearchFilter) end if init_year && init_year < start_year && init_year > 1900 start_year = init_year From cbf9a4b8888f71bd4e68d9dbde1bbc47cb36630a Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 15 Sep 2024 21:49:28 -0700 Subject: [PATCH 33/80] Use parse_date_range --- app/classes/pattern_search/base.rb | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index 6f8229a57d..d0f1a81c11 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -96,29 +96,21 @@ def lookup_param_name(var) # The string could be a date string like "2010-01-01", or a range string # like "2010-01-01-2010-01-31", or "2023-2024", or "08-10". # If it is a range, return the two dates. - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/CyclomaticComplexity def check_for_date_range(term) - bits = term.vals[0].split("-") + dates = term.parse_date_range + dates.map do |date| + next if date.blank? - case bits.size - when 2 - start = Date.new(*bits[0].map(&:to_i)) - range = Date.new(*bits[1].map(&:to_i)) - [start, range] - when 6 - start = Date.new(*bits[0..2].map(&:to_i)) - range = Date.new(*bits[3..5].map(&:to_i)) - [start, range] - when 3 - start = Date.new(*bits.map(&:to_i)) - [start, nil] - else - [nil, nil] + date.split("-").map(&:to_i) end + + start = Date.new(*dates[0]) if dates[0] + range = Date.new(*dates[1]) if dates[1] + + range = nil if start == range + + [start, range] end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/CyclomaticComplexity def check_for_numeric_range(term) bits = term.vals[0].split("-") From 30140980da6554c0e59c60f78888692f3a733e28 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 15 Sep 2024 21:52:05 -0700 Subject: [PATCH 34/80] Whoops map! --- app/classes/pattern_search/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index d0f1a81c11..9e1a925ed6 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -98,7 +98,7 @@ def lookup_param_name(var) # If it is a range, return the two dates. def check_for_date_range(term) dates = term.parse_date_range - dates.map do |date| + dates.map! do |date| next if date.blank? date.split("-").map(&:to_i) From df372aecf0a78a4281fb02a462949e2b581ed33a Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 16 Sep 2024 15:06:03 -0700 Subject: [PATCH 35/80] Update schema.rb --- db/schema.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 531e68e2c2..fe6930edbd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_09_06_052017) do +ActiveRecord::Schema[7.1].define(version: 2024_09_16_211404) do create_table "api_keys", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "last_used", precision: nil @@ -191,6 +191,17 @@ t.boolean "diagnostic", default: true, null: false end + create_table "inat_imports", charset: "utf8mb3", force: :cascade do |t| + t.integer "user_id" + t.integer "state", default: 0 + t.string "inat_ids" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "token" + t.string "inat_username" + t.boolean "import_all" + end + create_table "interests", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.string "target_type", limit: 30 t.integer "target_id" @@ -512,6 +523,7 @@ t.integer "source" t.datetime "log_updated_at", precision: nil t.boolean "needs_naming", default: false, null: false + t.integer "inat_id" t.index ["needs_naming"], name: "needs_naming_index" end From 24c855ce2d5e64abafbf1cb349a051afe20b219e Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 16 Sep 2024 16:32:39 -0700 Subject: [PATCH 36/80] Add migration, fix numeric range values --- app/classes/pattern_search/base.rb | 2 +- ...214_change_search_term_date_translation_tags.rb | 14 ++++++++++++++ db/schema.rb | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20240916224214_change_search_term_date_translation_tags.rb diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index 9e1a925ed6..049c14344e 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -118,7 +118,7 @@ def check_for_numeric_range(term) if bits.size == 2 [bits[0].to_i, bits[1].to_i] else - [nil, nil] + [term.vals[0], nil] end end diff --git a/db/migrate/20240916224214_change_search_term_date_translation_tags.rb b/db/migrate/20240916224214_change_search_term_date_translation_tags.rb new file mode 100644 index 0000000000..bc763185b6 --- /dev/null +++ b/db/migrate/20240916224214_change_search_term_date_translation_tags.rb @@ -0,0 +1,14 @@ +class ChangeSearchTermDateTranslationTags < ActiveRecord::Migration[7.1] + def up + TranslationString.rename_tags( + { search_term_date: :search_term_when, + observation_term_date: :observation_term_when } + ) + end + def down + TranslationString.rename_tags( + { search_term_when: :search_term_date, + observation_term_when: :observation_term_date } + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index fe6930edbd..e5aeb6fe0b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_09_16_211404) do +ActiveRecord::Schema[7.1].define(version: 2024_09_16_224214) do create_table "api_keys", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "last_used", precision: nil From 10f769d599cdd8eec277d95d6b64a70a432a3ba8 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 16 Sep 2024 16:47:24 -0700 Subject: [PATCH 37/80] Revert to text fields for dates --- app/classes/pattern_search/base.rb | 14 ++++++------- .../names/pattern_search_controller.rb | 4 +--- .../observations/pattern_search_controller.rb | 19 +++++++++-------- app/helpers/pattern_search_helper.rb | 21 +++++++++++-------- app/models/observation_filter.rb | 4 ++-- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index 049c14344e..1ce722bdc3 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -97,15 +97,15 @@ def lookup_param_name(var) # like "2010-01-01-2010-01-31", or "2023-2024", or "08-10". # If it is a range, return the two dates. def check_for_date_range(term) - dates = term.parse_date_range - dates.map! do |date| - next if date.blank? + start, range = term.parse_date_range + # dates.map! do |date| + # next if date.blank? - date.split("-").map(&:to_i) - end + # date.split("-").map(&:to_i) + # end - start = Date.new(*dates[0]) if dates[0] - range = Date.new(*dates[1]) if dates[1] + # start = dates[0] if dates[0] + # range = dates[1] if dates[1] range = nil if start == range diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index f769cda3b0..3cac6fbb39 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -54,9 +54,7 @@ def fields_with_ids end def permitted_search_params - params.permit(name_search_params + [ - { created: {} }, { modified: {} } - ]) + params.permit(name_search_params) end def name_search_params diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index f6aa5fe275..fc447895eb 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -65,14 +65,14 @@ def fields_with_ids end def permitted_search_params - params.permit(observation_search_params + [ - { when: [:year, :month, :day] }, - { when_range: [:year, :month, :day] }, - { created: [:year, :month, :day] }, - { created_range: [:year, :month, :day] }, - { modified: [:year, :month, :day] }, - { modified_range: [:year, :month, :day] } - ]) + params.permit(observation_search_params) # + [ + # { when: [:year, :month, :day] }, + # { when_range: [:year, :month, :day] }, + # { created: [:year, :month, :day] }, + # { created_range: [:year, :month, :day] }, + # { modified: [:year, :month, :day] }, + # { modified_range: [:year, :month, :day] } + # ]) end # need to add :pattern to the list of params, plus the hidden_id fields @@ -80,7 +80,8 @@ def permitted_search_params def observation_search_params PatternSearch::Observation.params.keys + [ :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, - :project_lists_id, :rank_range + :project_lists_id, :when_range, :created_range, :modified_range, + :rank_range ] end end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 2a8fcd94a6..7c8a467ea3 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -137,18 +137,21 @@ def pattern_search_date_range_field(**args) tag.div(class: "row") do [ tag.div(class: pattern_search_columns) do - # text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) - date_select_with_label(**args.merge( - { between: "(YYYY-MM-DD)", include_blank: true, - selected: 0, order: [:year, :month, :day] } - )) + text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) + # date_select_with_label(**args.merge( + # { between: "(YYYY-MM-DD)", include_blank: true, + # selected: 0, order: [:year, :month, :day] } + # )) end, tag.div(class: pattern_search_columns) do - date_select_with_label(**args.merge( - { field: "#{args[:field]}_range", label: :to.l, - between: :optional, help: nil, include_blank: true, - selected: 0, order: [:year, :month, :day] } + text_field_with_label(**args.merge( + { label: :to.l, help: nil, between: :optional } )) + # date_select_with_label(**args.merge( + # { field: "#{args[:field]}_range", label: :to.l, + # between: :optional, help: nil, include_blank: true, + # selected: 0, order: [:year, :month, :day] } + # )) end ].safe_join end diff --git a/app/models/observation_filter.rb b/app/models/observation_filter.rb index c367dfa7d3..2523a2b471 100644 --- a/app/models/observation_filter.rb +++ b/app/models/observation_filter.rb @@ -6,8 +6,8 @@ class ObservationFilter < SearchFilter PatternSearch::Observation.params.map do |keyword, values| case values[1] when :parse_date_range - attribute(keyword, :date) - attribute(:"#{keyword}_range", :date) + attribute(keyword, :string) + attribute(:"#{keyword}_range", :string) when :parse_confidence attribute(keyword, :integer) when :parse_longitude, :parse_latitude From eb4a51dfa87c3a3bdea593c60f2cd20df6f96372 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 16 Sep 2024 18:33:38 -0700 Subject: [PATCH 38/80] Fix ranges, clean --- app/classes/pattern_search/base.rb | 19 ++++++++++--------- app/helpers/pattern_search_helper.rb | 16 +++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index 1ce722bdc3..d0e4e6e520 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -96,16 +96,17 @@ def lookup_param_name(var) # The string could be a date string like "2010-01-01", or a range string # like "2010-01-01-2010-01-31", or "2023-2024", or "08-10". # If it is a range, return the two dates. + # Try for fidelity to the stored string, eg only years. def check_for_date_range(term) - start, range = term.parse_date_range - # dates.map! do |date| - # next if date.blank? - - # date.split("-").map(&:to_i) - # end - - # start = dates[0] if dates[0] - # range = dates[1] if dates[1] + bits = term.vals[0].split("-") + if bits.size == 2 + start, range = bits + elsif bits.size == 4 + start = "#{bits[0]}-#{bits[1]}" + range = "#{bits[2]}-#{bits[3]}" + else + start, range = term.parse_date_range + end range = nil if start == range diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index 7c8a467ea3..e4d433ad23 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -137,21 +137,15 @@ def pattern_search_date_range_field(**args) tag.div(class: "row") do [ tag.div(class: pattern_search_columns) do - text_field_with_label(**args.merge(between: "(YYYY-MM-DD)")) - # date_select_with_label(**args.merge( - # { between: "(YYYY-MM-DD)", include_blank: true, - # selected: 0, order: [:year, :month, :day] } - # )) + text_field_with_label(**args.merge( + { between: "(YYYY-MM-DD)", label_class: "mr-2" } + )) end, tag.div(class: pattern_search_columns) do text_field_with_label(**args.merge( - { label: :to.l, help: nil, between: :optional } + { field: "#{args[:field]}_range", label: :to.l, + help: nil, between: :optional } )) - # date_select_with_label(**args.merge( - # { field: "#{args[:field]}_range", label: :to.l, - # between: :optional, help: nil, include_blank: true, - # selected: 0, order: [:year, :month, :day] } - # )) end ].safe_join end From 43a1700232b25ad3c3b9ad7fbe057819bb5554fa Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 00:08:11 -0700 Subject: [PATCH 39/80] guard incoming_string nil --- app/classes/pattern_search/parser.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/classes/pattern_search/parser.rb b/app/classes/pattern_search/parser.rb index 46ab010681..22f039da26 100644 --- a/app/classes/pattern_search/parser.rb +++ b/app/classes/pattern_search/parser.rb @@ -17,6 +17,8 @@ def initialize(string) end def clean_incoming_string + return "" unless incoming_string + incoming_string.strip.gsub(/\s+/, " ") end From d785c90cd4e9c90a5ab0bdf32bd1ad93d0f5613d Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 00:08:24 -0700 Subject: [PATCH 40/80] spacing for between --- app/helpers/forms_helper.rb | 3 ++- app/helpers/pattern_search_helper.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 04a76cbb73..b7817e347e 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -511,7 +511,8 @@ def form_group_wrap_class(args, base = "form-group") def field_label_opts(args) label_opts = {} - label_opts[:class] = args[:help].present? ? "" : "mr-3" + need_margin = args[:between].present? + label_opts[:class] = need_margin ? "mr-2" : "" label_opts[:index] = args[:index] if args[:index].present? label_opts end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index e4d433ad23..d1da900852 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -138,7 +138,7 @@ def pattern_search_date_range_field(**args) [ tag.div(class: pattern_search_columns) do text_field_with_label(**args.merge( - { between: "(YYYY-MM-DD)", label_class: "mr-2" } + { between: "(YYYY-MM-DD)" } )) end, tag.div(class: pattern_search_columns) do From 030867691ad927b44823873deb34c0f087a31a14 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 00:19:53 -0700 Subject: [PATCH 41/80] truly improved spacing --- app/helpers/forms_helper.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index b7817e347e..b6595f8a3c 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -543,8 +543,10 @@ def check_for_help_block(args) "help" ].compact_blank.join("_") args[:between] = capture do - concat(args[:between]) - concat(collapse_info_trigger(id, class: "mx-3")) + if args[:between].present? + concat(tag.span(class: "mr-2") { args[:between] }) + end + concat(collapse_info_trigger(id, class: "ml-1 mr-3")) end args[:append] = capture do concat(args[:append]) From 6d6975007f443246c886ca4858a7121c8042e7e4 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 00:22:51 -0700 Subject: [PATCH 42/80] Set default "" incoming string upstream --- app/classes/pattern_search/parser.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/classes/pattern_search/parser.rb b/app/classes/pattern_search/parser.rb index 22f039da26..4592635acf 100644 --- a/app/classes/pattern_search/parser.rb +++ b/app/classes/pattern_search/parser.rb @@ -12,13 +12,11 @@ class Parser /x def initialize(string) - self.incoming_string = string + self.incoming_string = string || "" self.terms = parse_incoming_string end def clean_incoming_string - return "" unless incoming_string - incoming_string.strip.gsub(/\s+/, " ") end From 10259c5dc80ee0c6d0a4c13a3121f616e7664912 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 01:35:09 -0700 Subject: [PATCH 43/80] rank and confidence as ranges, reformat options --- .../concerns/pattern_searchable.rb | 42 +++++++-------- .../observations/pattern_search_controller.rb | 4 +- app/helpers/pattern_search_helper.rb | 52 ++++++++++++------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index 337c572062..d2f87be2cd 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -33,37 +33,37 @@ def formatted_pattern_search_string # One oddball is `confidence` - the string "0" should not count as a value. def sift_and_restructure_form_params - @keywords = permitted_search_params.to_h.compact_blank.reject do |_, v| - v == "0" || incomplete_date?(v) - end - format_date_params_into_strings + @keywords = permitted_search_params.to_h.compact_blank #.reject do |_, v| + # v == "0" || incomplete_date?(v) + # end + # format_date_params_into_strings concatenate_range_fields @sendable_params = substitute_ids_for_names(@keywords) # @storable_params = storable_params(@keywords) end - def incomplete_date?(value) - value.is_a?(Hash) && value.values.any?(&:blank?) - end + # def incomplete_date?(value) + # value.is_a?(Hash) && value.values.any?(&:blank?) + # end # Deal with date fields, which are stored as hashes with year, month, day. # Convert them to a single string. Can use `web_date` method on date fields. - def format_date_params_into_strings - @keywords.each_key do |key| - next unless fields_with_dates.include?(key.to_sym) - next if @keywords[key][:year].blank? - - @keywords[key] = date_into_string(key) - if @keywords[:"#{key}_range"].present? - @keywords[:"#{key}_range"] = date_into_string(:"#{key}_range") - end - end - end + # def format_date_params_into_strings + # @keywords.each_key do |key| + # next unless fields_with_dates.include?(key.to_sym) + # next if @keywords[key][:year].blank? + + # @keywords[key] = date_into_string(key) + # if @keywords[:"#{key}_range"].present? + # @keywords[:"#{key}_range"] = date_into_string(:"#{key}_range") + # end + # end + # end # date is a hash with year, month, day. Convert to string. - def date_into_string(key) - Date.new(*permitted_search_params.to_h[key].values.map(&:to_i)).web_date - end + # def date_into_string(key) + # Date.new(*permitted_search_params.to_h[key].values.map(&:to_i)).web_date + # end # Check for `fields_with_range`, and concatenate them if range val present, # removing the range field. diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index fc447895eb..55e4156f85 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -57,7 +57,7 @@ def fields_with_dates end def fields_with_range - [:when, :created, :modified, :rank] + [:when, :created, :modified, :rank, :confidence] end def fields_with_ids @@ -81,7 +81,7 @@ def observation_search_params PatternSearch::Observation.params.keys + [ :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, :project_lists_id, :when_range, :created_range, :modified_range, - :rank_range + :rank_range, :confidence_range ] end end diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/pattern_search_helper.rb index d1da900852..a8be90b9c5 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/pattern_search_helper.rb @@ -64,7 +64,7 @@ def pattern_search_field_type_from_parser(**args) separator: PATTERN_SEARCH_SEPARATOR } }, list_of_users: { component: :autocompleter_field, args: { type: :user, separator: ", " } }, - confidence: { component: :pattern_search_confidence_field, args: {} }, + confidence: { component: :pattern_search_confidence_range_field, args: {} }, longitude: { component: :pattern_search_longitude_field, args: {} }, latitude: { component: :pattern_search_latitude_field, args: {} } }.freeze @@ -152,26 +152,40 @@ def pattern_search_date_range_field(**args) end def pattern_search_rank_range_field(**args) - tag.div(class: "row") do - [ - tag.div(class: pattern_search_columns) do - select_with_label(**args.merge( - { options: Name.all_ranks, include_blank: true, selected: 0 } - )) - end, - tag.div(class: pattern_search_columns) do - select_with_label(**args.merge( - { label: :to.l, between: :optional, help: nil, - options: Name.all_ranks, include_blank: true, selected: 0, - field: "#{args[:field]}_range" } - )) - end - ].safe_join - end + [ + tag.div(class: "d-inline-block mr-4") do + select_with_label(**args.merge( + { inline: true, options: Name.all_ranks, + include_blank: true, selected: nil } + )) + end, + tag.div(class: "d-inline-block") do + select_with_label(**args.merge( + { label: :to.l, between: :optional, help: nil, inline: true, + options: Name.all_ranks, include_blank: true, selected: nil, + field: "#{args[:field]}_range" } + )) + end + ].safe_join end - def pattern_search_confidence_field(**args) - select_with_label(options: Vote.opinion_menu, **args) + def pattern_search_confidence_range_field(**args) + confidences = Vote.opinion_menu.map { |k, v| [k, Vote.percent(v)] } + [ + tag.div(class: "d-inline-block mr-4") do + select_with_label(**args.merge( + { inline: true, options: confidences, + include_blank: true, selected: nil } + )) + end, + tag.div(class: "d-inline-block") do + select_with_label(**args.merge( + { label: :to.l, between: :optional, help: nil, inline: true, + options: confidences, include_blank: true, selected: nil, + field: "#{args[:field]}_range" } + )) + end + ].safe_join end def pattern_search_longitude_field(**args) From db6da28a96a1b8a9c4afc704df51548d08ab01f3 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 17:04:03 -0700 Subject: [PATCH 44/80] Handle params nested under filter object, add `fields` methods --- app/classes/pattern_search/name.rb | 8 ++++++++ app/classes/pattern_search/observation.rb | 10 ++++++++++ app/controllers/concerns/pattern_searchable.rb | 2 +- app/controllers/names/pattern_search_controller.rb | 7 ++++--- .../observations/pattern_search_controller.rb | 9 ++++----- app/models/name_filter.rb | 5 +++-- app/models/observation_filter.rb | 4 ++++ db/schema.rb | 1 - 8 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/classes/pattern_search/name.rb b/app/classes/pattern_search/name.rb index 00513b620a..db9a7c4a66 100644 --- a/app/classes/pattern_search/name.rb +++ b/app/classes/pattern_search/name.rb @@ -34,6 +34,14 @@ def self.params PARAMS end + # List of fields that are displayed in the search form. + # Autocompleters have id fields, and range fields are concatenated. + def self.fields + params.keys + [ + :created_range, :modified_range, :rank_range + ] + end + def params self.class.params end diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index 0338dfa71d..21a5c54980 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -53,6 +53,16 @@ def self.params PARAMS end + # List of fields that are displayed in the search form. + # Autocompleters have id fields, and range fields are concatenated. + def self.fields + params.keys + [ + :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, + :project_lists_id, :when_range, :created_range, :modified_range, + :rank_range, :confidence_range + ] + end + def params self.class.params end diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index d2f87be2cd..054592bd6f 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -33,7 +33,7 @@ def formatted_pattern_search_string # One oddball is `confidence` - the string "0" should not count as a value. def sift_and_restructure_form_params - @keywords = permitted_search_params.to_h.compact_blank #.reject do |_, v| + @keywords = @filter.attributes.to_h.compact_blank #.reject do |_, v| # v == "0" || incomplete_date?(v) # end # format_date_params_into_strings diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index 3cac6fbb39..ea0cd3bf2b 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -17,6 +17,9 @@ def new def create @pattern = formatted_pattern_search_string + @filter = ObservationFilter.new( + permitted_search_params[:name_filter] + ) redirect_to(controller: "/names", action: :index, pattern: @pattern) end @@ -58,9 +61,7 @@ def permitted_search_params end def name_search_params - PatternSearch::Name.params.keys + [ - :created_range, :modified_range, :rank_range - ] + [{ name_filter: PatternSearch::Name.fields }] end end end diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index 55e4156f85..fc2717d73b 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -24,6 +24,9 @@ def new def create @field_columns = observation_field_groups + @filter = ObservationFilter.new( + permitted_search_params[:observation_filter] + ) @pattern = formatted_pattern_search_string # This will save the pattern in the session. @@ -78,11 +81,7 @@ def permitted_search_params # need to add :pattern to the list of params, plus the hidden_id fields # of the autocompleters. def observation_search_params - PatternSearch::Observation.params.keys + [ - :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, - :project_lists_id, :when_range, :created_range, :modified_range, - :rank_range, :confidence_range - ] + [{ observation_filter: PatternSearch::Observation.fields }] end end end diff --git a/app/models/name_filter.rb b/app/models/name_filter.rb index b5832e22de..a605423f31 100644 --- a/app/models/name_filter.rb +++ b/app/models/name_filter.rb @@ -8,10 +8,11 @@ class NameFilter < SearchFilter attribute(keyword, :date) attribute(:"#{keyword}_range", :date) when :parse_rank_range - attribute(keyword, :integer) - attribute(:"#{keyword}_range", :integer) + attribute(keyword, :string) + attribute(:"#{keyword}_range", :string) when :parse_confidence attribute(keyword, :integer) + attribute(:"#{keyword}_range", :integer) else attribute(keyword, :string) end diff --git a/app/models/observation_filter.rb b/app/models/observation_filter.rb index 2523a2b471..90b499d60d 100644 --- a/app/models/observation_filter.rb +++ b/app/models/observation_filter.rb @@ -10,8 +10,12 @@ class ObservationFilter < SearchFilter attribute(:"#{keyword}_range", :string) when :parse_confidence attribute(keyword, :integer) + attribute(:"#{keyword}_range", :integer) when :parse_longitude, :parse_latitude attribute(keyword, :float) + when /parse_list_of_/ + attribute(keyword, :string) + attribute(:"#{keyword}_id", :string) else attribute(keyword, :string) end diff --git a/db/schema.rb b/db/schema.rb index e5aeb6fe0b..a497283d31 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -523,7 +523,6 @@ t.integer "source" t.datetime "log_updated_at", precision: nil t.boolean "needs_naming", default: false, null: false - t.integer "inat_id" t.index ["needs_naming"], name: "needs_naming_index" end From ef87b8a9c74756b7924b2e9283f1ce68f6ae5845 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 17:23:15 -0700 Subject: [PATCH 45/80] Move fields_with id upstream, symbolize keys --- app/classes/pattern_search/name.rb | 12 ++++ app/classes/pattern_search/observation.rb | 12 ++++ .../concerns/pattern_searchable.rb | 55 +++++++++---------- .../names/pattern_search_controller.rb | 6 +- .../observations/pattern_search_controller.rb | 15 ++--- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/app/classes/pattern_search/name.rb b/app/classes/pattern_search/name.rb index db9a7c4a66..843e4e7ebc 100644 --- a/app/classes/pattern_search/name.rb +++ b/app/classes/pattern_search/name.rb @@ -42,6 +42,18 @@ def self.fields ] end + def self.fields_with_dates + [:created, :modified] + end + + def self.fields_with_range + [:created, :modified, :rank] + end + + def self.fields_with_ids + [] + end + def params self.class.params end diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index 21a5c54980..59980c2a9f 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -63,6 +63,18 @@ def self.fields ] end + def self.fields_with_dates + [:when, :created, :modified] + end + + def self.fields_with_range + [:when, :created, :modified, :rank, :confidence] + end + + def self.fields_with_ids + [:name, :location, :user, :herbarium, :list, :project, :species_list] + end + def params self.class.params end diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/pattern_searchable.rb index 054592bd6f..9930686c03 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/pattern_searchable.rb @@ -33,13 +33,13 @@ def formatted_pattern_search_string # One oddball is `confidence` - the string "0" should not count as a value. def sift_and_restructure_form_params - @keywords = @filter.attributes.to_h.compact_blank #.reject do |_, v| - # v == "0" || incomplete_date?(v) - # end - # format_date_params_into_strings + @keywords = @filter.attributes.to_h.compact_blank.symbolize_keys + concatenate_range_fields - @sendable_params = substitute_ids_for_names(@keywords) - # @storable_params = storable_params(@keywords) + @sendable_params = @keywords + substitute_ids_for_names + # @storable_params = @keywords + # set_storable_params end # def incomplete_date?(value) @@ -83,15 +83,14 @@ def concatenate_range_fields # # Controller declares `fields_with_ids` which autocompleter send ids. # This method substitutes the ids for the names. - def substitute_ids_for_names(keywords) - keywords.each_key do |key| + def substitute_ids_for_names + @sendable_params.each_key do |key| next unless fields_with_ids.include?(key.to_sym) && - keywords[:"#{key}_id"].present? + @sendable_params[:"#{key}_id"].present? - keywords[key] = keywords[:"#{key}_id"] - keywords.delete(:"#{key}_id") + @sendable_params[key] = @sendable_params[:"#{key}_id"] + @sendable_params.delete(:"#{key}_id") end - keywords end # STORABLE_PARAMS @@ -99,37 +98,35 @@ def substitute_ids_for_names(keywords) # # Store full strings for all values, including names and locations, # so we can repopulate the form with the same values. - def storable_params(keywords) - keywords = escape_names_and_remove_ids(keywords) - escape_locations_and_remove_ids(keywords) + def set_storable_params + escape_names_and_remove_ids + escape_locations_and_remove_ids end # Escape-quote the names, the way the short form requires. - def escape_names_and_remove_ids(keywords) - keywords.each_key do |key| + def escape_names_and_remove_ids + @storable_params.each_key do |key| next unless fields_with_ids.include?(key.to_sym) && - keywords[:"#{key}_id"].present? + @storable_params[:"#{key}_id"].present? - list = keywords[key].split(",").map(&:strip) + list = @storable_params[key].split(",").map(&:strip) list = list.map { |name| "\"#{name}\"" } - keywords[key] = list.join(",") - keywords.delete(:"#{key}_id") + @storable_params[key] = list.join(",") + @storable_params.delete(:"#{key}_id") end - keywords end # Escape-quote the locations and their commas. - def escape_locations_and_remove_ids(keywords) - keywords.each_key do |key| + def escape_locations_and_remove_ids + @storable_params.each_key do |key| next unless [:location, :region].include?(key.to_sym) && - keywords[:"#{key}_id"].present? + @storable_params[:"#{key}_id"].present? - list = keywords[key].split(",").map(&:strip) + list = @storable_params[key].split(",").map(&:strip) list = list.map { |location| "\"#{location.tr(",", "\\,")}\"" } - keywords[key] = list.join(",") - keywords.delete(:"#{key}_id") + @storable_params[key] = list.join(",") + @storable_params.delete(:"#{key}_id") end - keywords end end end diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/pattern_search_controller.rb index ea0cd3bf2b..7c8a089703 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/pattern_search_controller.rb @@ -45,15 +45,15 @@ def name_field_groups end def fields_with_dates - [:created, :modified] + PatternSearch::Name.fields_with_dates end def fields_with_range - [:created, :modified, :rank] + PatternSearch::Name.fields_with_range end def fields_with_ids - [] + PatternSearch::Name.fields_with_ids end def permitted_search_params diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/pattern_search_controller.rb index fc2717d73b..02b50afbe1 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/pattern_search_controller.rb @@ -56,26 +56,19 @@ def observation_field_groups end def fields_with_dates - [:when, :created, :modified] + PatternSearch::Observation.fields_with_dates end def fields_with_range - [:when, :created, :modified, :rank, :confidence] + PatternSearch::Observation.fields_with_range end def fields_with_ids - [:name, :location, :user, :herbarium, :list, :project, :species_list] + PatternSearch::Observation.fields_with_ids end def permitted_search_params - params.permit(observation_search_params) # + [ - # { when: [:year, :month, :day] }, - # { when_range: [:year, :month, :day] }, - # { created: [:year, :month, :day] }, - # { created_range: [:year, :month, :day] }, - # { modified: [:year, :month, :day] }, - # { modified_range: [:year, :month, :day] } - # ]) + params.permit(observation_search_params) end # need to add :pattern to the list of params, plus the hidden_id fields From 8d884215b2f49d14bd60256d7dbf8ccc7251e2a5 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 21:48:12 -0700 Subject: [PATCH 46/80] Switch to FiltersController --- .../concerns/{pattern_searchable.rb => filterable.rb} | 4 ++-- ...tern_search_controller.rb => filters_controller.rb} | 6 +++--- ...tern_search_controller.rb => filters_controller.rb} | 10 +++++----- config/routes.rb | 8 ++++---- test/controllers/name_filters_controller_test.rb | 9 +++++++++ .../controllers/observation_filters_controller_test.rb | 9 +++++++++ 6 files changed, 32 insertions(+), 14 deletions(-) rename app/controllers/concerns/{pattern_searchable.rb => filterable.rb} (98%) rename app/controllers/names/{pattern_search_controller.rb => filters_controller.rb} (92%) rename app/controllers/observations/{pattern_search_controller.rb => filters_controller.rb} (91%) create mode 100644 test/controllers/name_filters_controller_test.rb create mode 100644 test/controllers/observation_filters_controller_test.rb diff --git a/app/controllers/concerns/pattern_searchable.rb b/app/controllers/concerns/filterable.rb similarity index 98% rename from app/controllers/concerns/pattern_searchable.rb rename to app/controllers/concerns/filterable.rb index 9930686c03..4b345b9619 100644 --- a/app/controllers/concerns/pattern_searchable.rb +++ b/app/controllers/concerns/filterable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# = PatternSearchable Concern +# = Filterable Concern # # This is a module of reusable methods included by controllers that handle # "faceted" pattern searches per model, with separate inputs for each keyword. @@ -18,7 +18,7 @@ # ################################################################################ -module PatternSearchable +module Filterable extend ActiveSupport::Concern included do diff --git a/app/controllers/names/pattern_search_controller.rb b/app/controllers/names/filters_controller.rb similarity index 92% rename from app/controllers/names/pattern_search_controller.rb rename to app/controllers/names/filters_controller.rb index 7c8a089703..dfcfd2f71f 100644 --- a/app/controllers/names/pattern_search_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -4,10 +4,10 @@ # # Route: `new_name_search_path` # Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/names/pattern_search", action: :create }` +# `{ controller: "/names/filter", action: :create }` module Names - class PatternSearchController < ApplicationController - include ::PatternSearchable + class FiltersController < ApplicationController + include ::Filterable before_action :login_required diff --git a/app/controllers/observations/pattern_search_controller.rb b/app/controllers/observations/filters_controller.rb similarity index 91% rename from app/controllers/observations/pattern_search_controller.rb rename to app/controllers/observations/filters_controller.rb index 02b50afbe1..0580f988f2 100644 --- a/app/controllers/observations/pattern_search_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# Names pattern search form. +# Observations pattern search form. # -# Route: `new_name_search_path` +# Route: `new_observation_search_path` # Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/names/pattern_search", action: :create }` +# `{ controller: "/observations/filter", action: :create }` module Observations - class PatternSearchController < ApplicationController - include ::PatternSearchable + class FiltersController < ApplicationController + include ::Filterable before_action :login_required diff --git a/config/routes.rb b/config/routes.rb index 97a64d35d7..a90595bf2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -488,8 +488,8 @@ def route_actions_hash # ----- Names: a lot of actions ---------------------------- namespace :names do - get("search/new", to: "pattern_search#new", as: "new_search") - post("search", to: "pattern_search#create", as: "search") + get("search/new", to: "filters#new", as: "new_search") + post("search", to: "filters#create", as: "search") end resources :names, id: /\d+/, shallow: true do @@ -582,8 +582,8 @@ def route_actions_hash namespace :observations do resources :downloads, only: [:new, :create] - get("search/new", to: "pattern_search#new", as: "new_search") - post("search", to: "pattern_search#create", as: "search") + get("search/new", to: "filters#new", as: "new_search") + post("search", to: "filters#create", as: "search") # uploads are not under resources because the obs doesn't have an id yet get("images/uploads/new", to: "images/uploads#new", as: "new_image_upload_for") diff --git a/test/controllers/name_filters_controller_test.rb b/test/controllers/name_filters_controller_test.rb new file mode 100644 index 0000000000..41c67bb106 --- /dev/null +++ b/test/controllers/name_filters_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require("test_helper") + +# ------------------------------------------------------------ +# Name filters - test pattern search +# ------------------------------------------------------------ +class NameFiltersControllerTest < FunctionalTestCase +end diff --git a/test/controllers/observation_filters_controller_test.rb b/test/controllers/observation_filters_controller_test.rb new file mode 100644 index 0000000000..34630034f3 --- /dev/null +++ b/test/controllers/observation_filters_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require("test_helper") + +# ------------------------------------------------------------ +# Observation filters - test pattern search +# ------------------------------------------------------------ +class ObservationFiltersControllerTest < FunctionalTestCase +end From 63cee8a9906bec789524e9121445b45bbc333b7f Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Tue, 17 Sep 2024 22:19:15 -0700 Subject: [PATCH 47/80] Continue renaming templates, add clear button --- app/controllers/names/filters_controller.rb | 5 ++ .../observations/filters_controller.rb | 5 ++ ...ern_search_helper.rb => filters_helper.rb} | 86 +++++++++---------- .../names/{pattern_search => filters}/new.erb | 2 +- .../{pattern_search => filters}/new.erb | 2 +- ...tern_search_form.erb => _filters_form.erb} | 17 ++-- 6 files changed, 66 insertions(+), 51 deletions(-) rename app/helpers/{pattern_search_helper.rb => filters_helper.rb} (66%) rename app/views/controllers/names/{pattern_search => filters}/new.erb (77%) rename app/views/controllers/observations/{pattern_search => filters}/new.erb (78%) rename app/views/controllers/shared/{_pattern_search_form.erb => _filters_form.erb} (60%) diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index dfcfd2f71f..b6806ac501 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -16,6 +16,11 @@ def new end def create + if params[:commit] == :CLEAR.l + session[:pattern] = "" + redirect_to(:new) and return + end + @pattern = formatted_pattern_search_string @filter = ObservationFilter.new( permitted_search_params[:name_filter] diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index 0580f988f2..96e7d480f7 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -23,6 +23,11 @@ def new end def create + if params[:commit] == :CLEAR.l + session[:pattern] = "" + redirect_to(observations_new_search_path) and return + end + @field_columns = observation_field_groups @filter = ObservationFilter.new( permitted_search_params[:observation_filter] diff --git a/app/helpers/pattern_search_helper.rb b/app/helpers/filters_helper.rb similarity index 66% rename from app/helpers/pattern_search_helper.rb rename to app/helpers/filters_helper.rb index a8be90b9c5..c1fd048c3d 100644 --- a/app/helpers/pattern_search_helper.rb +++ b/app/helpers/filters_helper.rb @@ -2,16 +2,16 @@ # helpers for pattern search forms. These call field helpers in forms_helper. # args should provide form, field, label at a minimum. -module PatternSearchHelper - def pattern_search_field(**args) - args[:label] ||= pattern_search_helper_for_label(args[:field]) - field_type = pattern_search_field_type_from_parser(**args) - component = PATTERN_SEARCH_FIELD_HELPERS[field_type][:component] - args = prepare_args_for_pattern_search_field(args, field_type, component) +module FiltersHelper + def filter_field(**args) + args[:label] ||= filter_helper_for_label(args[:field]) + field_type = filter_field_type_from_parser(**args) + component = FILTER_FIELD_HELPERS[field_type][:component] + args = prepare_args_for_filter_field(args, field_type, component) send(component, **args) if component end - def pattern_search_helper_for_label(field) + def filter_helper_for_label(field) if field == :pattern :PATTERN.l else @@ -21,9 +21,9 @@ def pattern_search_helper_for_label(field) # The PatternSearch subclasses define how they're going to parse their # fields, so we can use that to assign a field helper. - # example: :parse_yes -> :pattern_search_yes_field + # example: :parse_yes -> :filter_yes_field # If the field is :pattern, there's no assigned parser. - def pattern_search_field_type_from_parser(**args) + def filter_field_type_from_parser(**args) return :pattern if args[:field] == :pattern subclass = PatternSearch.const_get(args[:type].capitalize) @@ -35,63 +35,63 @@ def pattern_search_field_type_from_parser(**args) parser.to_s.gsub(/^parse_/, "").to_sym end - PATTERN_SEARCH_SEPARATOR = ", " + FILTER_SEPARATOR = ", " # Convenience for subclasses to access helper methods via subclass.params - PATTERN_SEARCH_FIELD_HELPERS = { + FILTER_FIELD_HELPERS = { pattern: { component: :text_field_with_label, args: {} }, - yes: { component: :pattern_search_yes_field, args: {} }, - boolean: { component: :pattern_search_boolean_field, args: {} }, - yes_no_both: { component: :pattern_search_yes_no_both_field, args: {} }, - date_range: { component: :pattern_search_date_range_field, args: {} }, - rank_range: { component: :pattern_search_rank_range_field, args: {} }, + yes: { component: :filter_yes_field, args: {} }, + boolean: { component: :filter_boolean_field, args: {} }, + yes_no_both: { component: :filter_yes_no_both_field, args: {} }, + date_range: { component: :filter_date_range_field, args: {} }, + rank_range: { component: :filter_rank_range_field, args: {} }, string: { component: :text_field_with_label, args: {} }, list_of_strings: { component: :text_field_with_label, args: {} }, list_of_herbaria: { component: :autocompleter_field, args: { type: :herbarium, - separator: PATTERN_SEARCH_SEPARATOR } }, + separator: FILTER_SEPARATOR } }, list_of_locations: { component: :autocompleter_field, args: { type: :location, - separator: PATTERN_SEARCH_SEPARATOR } }, + separator: FILTER_SEPARATOR } }, list_of_names: { component: :autocompleter_field, args: { type: :name, - separator: PATTERN_SEARCH_SEPARATOR } }, + separator: FILTER_SEPARATOR } }, list_of_projects: { component: :autocompleter_field, args: { type: :project, - separator: PATTERN_SEARCH_SEPARATOR } }, + separator: FILTER_SEPARATOR } }, list_of_species_lists: { component: :autocompleter_field, args: { type: :species_list, - separator: PATTERN_SEARCH_SEPARATOR } }, + separator: FILTER_SEPARATOR } }, list_of_users: { component: :autocompleter_field, args: { type: :user, separator: ", " } }, - confidence: { component: :pattern_search_confidence_range_field, args: {} }, - longitude: { component: :pattern_search_longitude_field, args: {} }, - latitude: { component: :pattern_search_latitude_field, args: {} } + confidence: { component: :filter_confidence_range_field, args: {} }, + longitude: { component: :filter_longitude_field, args: {} }, + latitude: { component: :filter_latitude_field, args: {} } }.freeze # Prepares HTML args for the field helper. This is where we can make # adjustments to the args hash before passing it to the field helper. # NOTE: Bootstrap 3 can't do full-width inline label/field. - def prepare_args_for_pattern_search_field(args, field_type, component) + def prepare_args_for_filter_field(args, field_type, component) if component == :text_field_with_label && args[:field] != :pattern args[:inline] = true end - args[:help] = pattern_search_help_text(args, field_type) - args[:hidden_name] = pattern_search_check_for_hidden_name(args) + args[:help] = filter_help_text(args, field_type) + args[:hidden_name] = filter_check_for_hidden_name(args) - PATTERN_SEARCH_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) + FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) end - def pattern_search_help_text(args, field_type) - component = PATTERN_SEARCH_FIELD_HELPERS[field_type][:component] + def filter_help_text(args, field_type) + component = FILTER_FIELD_HELPERS[field_type][:component] multiple_note = if component == :autocompleter_field - :pattern_search_terms_multiple.l + :filter_terms_multiple.l end [:"#{args[:type]}_term_#{args[:field]}".l, multiple_note].compact.join(" ") end # Overrides for the assumed name of the id field for autocompleter. - def pattern_search_check_for_hidden_name(args) + def filter_check_for_hidden_name(args) case args[:field] when :list return "list_id" @@ -103,7 +103,7 @@ def pattern_search_check_for_hidden_name(args) # FIELD HELPERS # - def pattern_search_yes_field(**args) + def filter_yes_field(**args) options = [ ["", nil], ["yes", "yes"] @@ -111,7 +111,7 @@ def pattern_search_yes_field(**args) select_with_label(options:, inline: true, **args) end - def pattern_search_boolean_field(**args) + def filter_boolean_field(**args) options = [ ["", nil], ["yes", "yes"], @@ -120,7 +120,7 @@ def pattern_search_boolean_field(**args) select_with_label(options:, inline: true, **args) end - def pattern_search_yes_no_both_field(**args) + def filter_yes_no_both_field(**args) options = [ ["", nil], ["yes", "yes"], @@ -133,15 +133,15 @@ def pattern_search_yes_no_both_field(**args) # RANGE FIELDS The first field gets the label, name and ID of the actual # param; the end `_range` field is optional. The controller needs to check for # the second & join them with a hyphen if it exists (in both cases here). - def pattern_search_date_range_field(**args) + def filter_date_range_field(**args) tag.div(class: "row") do [ - tag.div(class: pattern_search_columns) do + tag.div(class: filter_columns) do text_field_with_label(**args.merge( { between: "(YYYY-MM-DD)" } )) end, - tag.div(class: pattern_search_columns) do + tag.div(class: filter_columns) do text_field_with_label(**args.merge( { field: "#{args[:field]}_range", label: :to.l, help: nil, between: :optional } @@ -151,7 +151,7 @@ def pattern_search_date_range_field(**args) end end - def pattern_search_rank_range_field(**args) + def filter_rank_range_field(**args) [ tag.div(class: "d-inline-block mr-4") do select_with_label(**args.merge( @@ -169,7 +169,7 @@ def pattern_search_rank_range_field(**args) ].safe_join end - def pattern_search_confidence_range_field(**args) + def filter_confidence_range_field(**args) confidences = Vote.opinion_menu.map { |k, v| [k, Vote.percent(v)] } [ tag.div(class: "d-inline-block mr-4") do @@ -188,15 +188,15 @@ def pattern_search_confidence_range_field(**args) ].safe_join end - def pattern_search_longitude_field(**args) + def filter_longitude_field(**args) text_field_with_label(**args.merge(between: "(-180.0 to 180.0)")) end - def pattern_search_latitude_field(**args) + def filter_latitude_field(**args) text_field_with_label(**args.merge(between: "(-90.0 to 90.0)")) end - def pattern_search_columns + def filter_columns "col-xs-12 col-sm-6 col-md-12 col-lg-6" end end diff --git a/app/views/controllers/names/pattern_search/new.erb b/app/views/controllers/names/filters/new.erb similarity index 77% rename from app/views/controllers/names/pattern_search/new.erb rename to app/views/controllers/names/filters/new.erb index b2f0403141..364c31ddef 100644 --- a/app/views/controllers/names/pattern_search/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -3,6 +3,6 @@ @container = :full %> -<%= render(partial: "shared/pattern_search_form", +<%= render(partial: "shared/filters_form", locals: { local: true, search_type: :name, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/pattern_search/new.erb b/app/views/controllers/observations/filters/new.erb similarity index 78% rename from app/views/controllers/observations/pattern_search/new.erb rename to app/views/controllers/observations/filters/new.erb index e4a76b5c93..c42338b22f 100644 --- a/app/views/controllers/observations/pattern_search/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -3,6 +3,6 @@ @container = :full %> -<%= render(partial: "shared/pattern_search_form", +<%= render(partial: "shared/filters_form", locals: { local: true, search_type: :observation, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/shared/_pattern_search_form.erb b/app/views/controllers/shared/_filters_form.erb similarity index 60% rename from app/views/controllers/shared/_pattern_search_form.erb rename to app/views/controllers/shared/_filters_form.erb index ea69c78452..cdae4df40a 100644 --- a/app/views/controllers/shared/_pattern_search_form.erb +++ b/app/views/controllers/shared/_filters_form.erb @@ -14,15 +14,14 @@ <% if field.is_a?(Array) %> <%= tag.div(class: "row") do %> <% field.each do |subfield| %> - <%= tag.div(class: pattern_search_columns) do %> - <%= pattern_search_field(form: f, field: subfield, - type: search_type) %> + <%= tag.div(class: filter_columns) do %> + <%= filter_field(form: f, field: subfield, + type: search_type) %> <% end %> <% end %> <% end %> <% else %> - <%= pattern_search_field(form: f, field: field, - type: search_type) %> + <%= filter_field(form: f, field: field, type: search_type) %> <% end %> <% end %> @@ -34,6 +33,12 @@ <% end %> <% end %> - <%= submit_button(form: f, button: :SEARCH.l, center: true) %> + <%= tag.div(class: "text-center") do %> + <%= submit_button(form: f, button: :SEARCH.l, + class: "d-inline-block mx-3") %> + + <%= submit_button(form: f, button: :CLEAR.l, + class: "d-inline-block mx-3") %> + <% end %> <% end %> From ede270f3861372be39bd803411bd01313b25a945 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 16:58:19 -0700 Subject: [PATCH 48/80] Collapsed/shown fields, refactor controllers TODO: Conditional fields in name section --- app/controllers/names/filters_controller.rb | 76 ++++++++++----- .../observations/filters_controller.rb | 93 ++++++++++++------- app/helpers/filters_helper.rb | 47 +++++++++- app/helpers/panel_helper.rb | 10 +- app/views/controllers/names/filters/new.erb | 2 +- .../controllers/observations/filters/new.erb | 2 +- .../controllers/shared/_filters_form.erb | 34 ++----- 7 files changed, 177 insertions(+), 87 deletions(-) diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index b6806ac501..184a14dbf8 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -12,40 +12,74 @@ class FiltersController < ApplicationController before_action :login_required def new - @field_columns = name_field_groups + set_up_form_field_groupings + new_filter_instance_from_session end def create + return if check_for_clear_form + + set_up_form_field_groupings # in case we need to re-render the form + set_filter_instance_from_form + set_pattern_string + + redirect_to(controller: "/names", action: :index, pattern: @pattern) + end + + private + + def check_for_clear_form if params[:commit] == :CLEAR.l session[:pattern] = "" - redirect_to(:new) and return + session[:search_type] = nil + redirect_to(names_new_search_path) and return true + end + false + end + + def new_filter_instance_from_session + if session[:pattern] && session[:search_type] == :names + terms = PatternSearch::Name.new(session[:pattern]).form_params + @filter = NameFilter.new(terms) + else + @filter = NameFilter.new end + end - @pattern = formatted_pattern_search_string - @filter = ObservationFilter.new( - permitted_search_params[:name_filter] - ) - redirect_to(controller: "/names", action: :index, pattern: @pattern) + def set_filter_instance_from_form + @filter = NameFilter.new(permitted_search_params[:name_filter]) + redirect_to(names_new_search_path) && return if @filter.invalid? end - private + def set_pattern_string + @pattern = formatted_pattern_search_string + # Save it so that we can keep it in the search bar in subsequent pages. + session[:pattern] = @pattern + session[:search_type] = :names + end # This is the list of fields that are displayed in the search form. In the # template, each hash is interpreted as a column, and each key is a panel # with an array of fields or field pairings. - def name_field_groups - [ - { date: [:created, :modified], - quality: [[:has_observations, :deprecated], - [:has_author, :author], - [:has_citation, :citation]] }, - { scope: [[:has_synonyms, :include_synonyms], - [:include_subtaxa, :include_misspellings], - :rank, :lichen], - detail: [[:has_classification, :classification], - [:has_notes, :notes], - [:has_comments, :comments], - :has_description] } + def set_up_form_field_groupings + @field_columns = [ + { date: { shown: [:created, :modified], collapsed: [] }, + quality: { + shown: [[:has_observations, :deprecated]], + collapsed: [[:has_author, :author], + [:has_citation, :citation]] + } }, + { scope: { + shown: [[:has_synonyms, :include_synonyms], + [:include_subtaxa, :include_misspellings]], + collapsed: [:rank, :lichen] + }, + detail: { + shown: [[:has_classification, :classification]], + collapsed: [[:has_notes, :notes], + [:has_comments, :comments], + :has_description] + } } ].freeze end diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index 96e7d480f7..b6ca39f7d2 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -12,51 +12,82 @@ class FiltersController < ApplicationController before_action :login_required def new - @field_columns = observation_field_groups - terms = PatternSearch::Observation.new(session[:pattern]).form_params - - @filter = if session[:pattern] - ObservationFilter.new(terms) - else - ObservationFilter.new - end + set_up_form_field_groupings + new_filter_instance_from_session end def create + return if check_for_clear_form + + set_up_form_field_groupings # in case we need to re-render the form + set_filter_instance_from_form + set_pattern_string + + redirect_to(controller: "/observations", action: :index, + pattern: @pattern) + end + + private + + def check_for_clear_form if params[:commit] == :CLEAR.l session[:pattern] = "" - redirect_to(observations_new_search_path) and return + session[:search_type] = nil + redirect_to(observations_new_search_path) and return true end + false + end - @field_columns = observation_field_groups + def new_filter_instance_from_session + if session[:pattern] && session[:search_type] == :observations + terms = PatternSearch::Observation.new(session[:pattern]).form_params + @filter = ObservationFilter.new(terms) + else + @filter = ObservationFilter.new + end + end + + def set_filter_instance_from_form @filter = ObservationFilter.new( permitted_search_params[:observation_filter] ) - @pattern = formatted_pattern_search_string - - # This will save the pattern in the session. - redirect_to(controller: "/observations", action: :index, - pattern: @pattern) + redirect_to(observations_new_search_path) && return if @filter.invalid? end - private + def set_pattern_string + @pattern = formatted_pattern_search_string + # Save it so that we can keep it in the search bar in subsequent pages. + session[:pattern] = @pattern + session[:search_type] = :observations + end # This is the list of fields that are displayed in the search form. In the - # template, each hash is interpreted as a column, and each key is a panel - # with an array of fields or field pairings. - def observation_field_groups - [ - { date: [:when, :created, :modified], - name: [:name, :confidence, [:has_name, :lichen], - [:include_subtaxa, :include_synonyms], - [:include_all_name_proposals, :exclude_consensus]], - location: [:location, :region, - [:has_public_lat_lng, :is_collection_location], - [:east, :west], [:north, :south]] }, - { detail: [[:has_specimen, :has_sequence], [:has_images, :has_notes], - [:has_field, :notes], [:has_comments, :comments]], - connected: [:user, :herbarium, :list, :project, :project_lists, - :field_slip] } + # template, each hash is interpreted as a column, and each key is a + # panel_body (either shown or hidden) with an array of fields or field + # pairings. + def set_up_form_field_groupings + @field_columns = [ + { date: { shown: [:when], collapsed: [:created, :modified] }, + name: { + shown: [:name], + collapsed: [:confidence, [:has_name, :lichen], + [:include_subtaxa, :include_synonyms], + [:include_all_name_proposals, :exclude_consensus]] + }, + location: { + shown: [:location, :region], + collapsed: [[:has_public_lat_lng, :is_collection_location], + [:east, :west], [:north, :south]] + } }, + { detail: { + shown: [[:has_specimen, :has_sequence]], + collapsed: [[:has_images, :has_notes], + [:has_field, :notes], [:has_comments, :comments]] + }, + connected: { + shown: [:user, :project], + collapsed: [:herbarium, :list, :project_lists, :field_slip] + } } ].freeze end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index c1fd048c3d..1ef1402947 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -3,15 +3,58 @@ # helpers for pattern search forms. These call field helpers in forms_helper. # args should provide form, field, label at a minimum. module FiltersHelper + # Filter panel for a search form. Sections are shown and collapsed. + # If sections[:collapsed] is present, part of the panel will be collapsed. + def filter_panel(form:, filter:, heading:, sections:, type:) + shown = filter_panel_body(form:, sections:, type:, section: :shown) + collapsed = filter_panel_body(form:, sections:, type:, section: :collapsed) + open = collapse = false + if sections[:collapsed].present? + collapse = heading + open = filter.attributes.keys.intersect?(sections[:collapsed]) + end + panel_block(heading: :"search_term_group_#{heading}".l, + collapse:, open:, panel_bodies: [shown, collapsed]) + end + + # Content of each shown/collapsed section, composed of field rows. + def filter_panel_body(form:, sections:, type:, section:) + return unless sections[section] + + capture do + sections[section].each do |field| + concat(filter_row(form:, field:, type:)) + end + end + end + + # Fields might be paired, so we need to check for that. + def filter_row(form:, field:, type:) + if field.is_a?(Array) + tag.div(class: "row") do + field.each do |subfield| + concat(tag.div(class: filter_columns) do + filter_field(form:, field: subfield, type:) + end) + end + end + else + filter_field(form:, field:, type:) + end + end + + # Figure out what kind of field helper to call, based on definitions below. + # Some field types need args, so there is both the component and args hash. def filter_field(**args) - args[:label] ||= filter_helper_for_label(args[:field]) + args[:label] ||= filter_label(args[:field]) field_type = filter_field_type_from_parser(**args) component = FILTER_FIELD_HELPERS[field_type][:component] args = prepare_args_for_filter_field(args, field_type, component) send(component, **args) if component end - def filter_helper_for_label(field) + # The field's label. + def filter_label(field) if field == :pattern :PATTERN.l else diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb index c6ca7e4cc4..cf0d4f6b35 100644 --- a/app/helpers/panel_helper.rb +++ b/app/helpers/panel_helper.rb @@ -78,10 +78,14 @@ def panel_collapse_icons link_icon(:chevron_up, title: :CLOSE.l)].safe_join end - # Some panels need multiple panel bodies. + # Some panels need multiple panel bodies. Potentially collapse the last one. def panel_bodies(args) - args[:panel_bodies].map do |body| - panel_body(args, body) + args[:panel_bodies].map.with_index do |body, idx| + if args[:collapse].present? && idx == args[:panel_bodies].length - 1 + panel_collapse_body(args, body) + else + panel_body(args, body) + end end.safe_join end diff --git a/app/views/controllers/names/filters/new.erb b/app/views/controllers/names/filters/new.erb index 364c31ddef..c1ae276ba5 100644 --- a/app/views/controllers/names/filters/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -4,5 +4,5 @@ %> <%= render(partial: "shared/filters_form", - locals: { local: true, search_type: :name, + locals: { local: true, filter: @filter, type: :name, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/filters/new.erb b/app/views/controllers/observations/filters/new.erb index c42338b22f..137985cf63 100644 --- a/app/views/controllers/observations/filters/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -4,5 +4,5 @@ %> <%= render(partial: "shared/filters_form", - locals: { local: true, search_type: :observation, + locals: { local: true, filter: @filter, type: :observation, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/shared/_filters_form.erb b/app/views/controllers/shared/_filters_form.erb index cdae4df40a..97fb0c28e0 100644 --- a/app/views/controllers/shared/_filters_form.erb +++ b/app/views/controllers/shared/_filters_form.erb @@ -1,32 +1,13 @@ -<%# locals: (field_columns: [], search_type: nil, local: true) -%> +<%# locals: (filter: {}, field_columns: [], type: nil, local: true) -%> -<%= form_with(model: @filter, url: { action: :create }) do |f| %> +<%= form_with(model: filter, url: { action: :create }) do |form| %> <%= tag.div(class: "row") do %> <% field_columns.each do |panels| %> <%= tag.div(class: "col-xs-12 col-md-6") do %> - <% panels.each do |heading, fields| %> - - <%= panel_block(heading: :"search_term_group_#{heading}".l) do %> - <% fields.each do |field| %> - - <% if field.is_a?(Array) %> - <%= tag.div(class: "row") do %> - <% field.each do |subfield| %> - <%= tag.div(class: filter_columns) do %> - <%= filter_field(form: f, field: subfield, - type: search_type) %> - <% end %> - <% end %> - <% end %> - <% else %> - <%= filter_field(form: f, field: field, type: search_type) %> - <% end %> - - <% end %> - <% end %> - + <% panels.each do |heading, sections| %> + <%= filter_panel(form:, filter:, heading:, sections:, type:) %> <% end %> <% end %> @@ -34,11 +15,8 @@ <% end %> <%= tag.div(class: "text-center") do %> - <%= submit_button(form: f, button: :SEARCH.l, - class: "d-inline-block mx-3") %> - - <%= submit_button(form: f, button: :CLEAR.l, - class: "d-inline-block mx-3") %> + <%= submit_button(form:, button: :SEARCH.l, class: "d-inline-block mx-3") %> + <%= submit_button(form:, button: :CLEAR.l, class: "d-inline-block mx-3") %> <% end %> <% end %> From ea00aa5e31ecbcea251bc64f6153c8a6ceae5083 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 19:45:02 -0700 Subject: [PATCH 49/80] Separate type from model in filter args (type is for autocompleter) --- .../observations/filters_controller.rb | 6 +- app/helpers/filters_helper.rb | 90 +++++++++++++------ app/views/controllers/names/filters/new.erb | 2 +- .../controllers/observations/filters/new.erb | 2 +- .../controllers/shared/_filters_form.erb | 4 +- 5 files changed, 72 insertions(+), 32 deletions(-) diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index b6ca39f7d2..9a6185ce94 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -70,9 +70,9 @@ def set_up_form_field_groupings { date: { shown: [:when], collapsed: [:created, :modified] }, name: { shown: [:name], - collapsed: [:confidence, [:has_name, :lichen], - [:include_subtaxa, :include_synonyms], - [:include_all_name_proposals, :exclude_consensus]] + conditional: [[:include_subtaxa, :include_synonyms], + [:include_all_name_proposals, :exclude_consensus]], + collapsed: [:confidence, [:has_name, :lichen]] }, location: { shown: [:location, :region], diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 1ef1402947..33fff5c619 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -5,9 +5,9 @@ module FiltersHelper # Filter panel for a search form. Sections are shown and collapsed. # If sections[:collapsed] is present, part of the panel will be collapsed. - def filter_panel(form:, filter:, heading:, sections:, type:) - shown = filter_panel_body(form:, sections:, type:, section: :shown) - collapsed = filter_panel_body(form:, sections:, type:, section: :collapsed) + def filter_panel(form:, filter:, heading:, sections:, model:) + shown = filter_panel_shown(form:, sections:, model:) + collapsed = filter_panel_collapsed(form:, sections:, model:) open = collapse = false if sections[:collapsed].present? collapse = heading @@ -17,40 +17,56 @@ def filter_panel(form:, filter:, heading:, sections:, type:) collapse:, open:, panel_bodies: [shown, collapsed]) end - # Content of each shown/collapsed section, composed of field rows. - def filter_panel_body(form:, sections:, type:, section:) - return unless sections[section] + def filter_panel_shown(form:, sections:, model:) + return unless sections.is_a?(Hash) && sections[:shown].present? capture do - sections[section].each do |field| - concat(filter_row(form:, field:, type:)) + sections[:shown].each do |field| + concat(filter_row(form:, field:, model:, sections:)) + end + end + end + + # Content of collapsed section, composed of field rows. + def filter_panel_collapsed(form:, sections:, model:) + return unless sections.is_a?(Hash) && sections[:collapsed].present? + + capture do + sections[:collapsed].each do |field| + concat(filter_row(form:, field:, model:, sections:)) end end end # Fields might be paired, so we need to check for that. - def filter_row(form:, field:, type:) + def filter_row(form:, field:, model:, sections:) if field.is_a?(Array) tag.div(class: "row") do field.each do |subfield| concat(tag.div(class: filter_columns) do - filter_field(form:, field: subfield, type:) + filter_field(form:, field: subfield, model:, sections:) end) end end else - filter_field(form:, field:, type:) + filter_field(form:, field:, model:, sections:) end end # Figure out what kind of field helper to call, based on definitions below. # Some field types need args, so there is both the component and args hash. - def filter_field(**args) - args[:label] ||= filter_label(args[:field]) - field_type = filter_field_type_from_parser(**args) + def filter_field(form:, field:, model:, sections:) + args = { form: form, field: field } + args[:label] ||= filter_label(field) + field_type = filter_field_type_from_parser(field:, model:) component = FILTER_FIELD_HELPERS[field_type][:component] + return unless component + args = prepare_args_for_filter_field(args, field_type, component) - send(component, **args) if component + if component == :filter_autocompleter_with_conditional_fields + args = args.merge(sections:, model:) + end + send(component, **args) end # The field's label. @@ -66,15 +82,15 @@ def filter_label(field) # fields, so we can use that to assign a field helper. # example: :parse_yes -> :filter_yes_field # If the field is :pattern, there's no assigned parser. - def filter_field_type_from_parser(**args) - return :pattern if args[:field] == :pattern + def filter_field_type_from_parser(field:, model:) + return :pattern if field == :pattern - subclass = PatternSearch.const_get(args[:type].capitalize) - unless subclass.params[args[:field]] - raise("No parser defined for #{args[:field]} in #{subclass}") + subclass = PatternSearch.const_get(model.capitalize) + unless subclass.params[field] + raise("No parser defined for #{field} in #{subclass}") end - parser = subclass.params[args[:field]][1] + parser = subclass.params[field][1] parser.to_s.gsub(/^parse_/, "").to_sym end @@ -96,9 +112,8 @@ def filter_field_type_from_parser(**args) list_of_locations: { component: :autocompleter_field, args: { type: :location, separator: FILTER_SEPARATOR } }, - list_of_names: { component: :autocompleter_field, - args: { type: :name, - separator: FILTER_SEPARATOR } }, + list_of_names: { component: :filter_autocompleter_with_conditional_fields, + args: { type: :name, separator: FILTER_SEPARATOR } }, list_of_projects: { component: :autocompleter_field, args: { type: :project, separator: FILTER_SEPARATOR } }, @@ -122,7 +137,7 @@ def prepare_args_for_filter_field(args, field_type, component) args[:help] = filter_help_text(args, field_type) args[:hidden_name] = filter_check_for_hidden_name(args) - FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:type)) + FILTER_FIELD_HELPERS[field_type][:args].merge(args) end def filter_help_text(args, field_type) @@ -144,8 +159,33 @@ def filter_check_for_hidden_name(args) nil end + ############################################################### + # # FIELD HELPERS # + # Complex mechanism: append collapsed fields to autocompleter that only appear + # when autocompleter has a value. Only on the name field + def filter_autocompleter_with_conditional_fields(**args) + return if args[:sections].blank? + + append = filter_conditional_rows(sections: args[:sections], + form: args[:form], model: args[:model]) + autocompleter_field(**args.except(:sections, :model).merge(append:)) + end + + # Rows that only uncollapse if an autocompleter field has a value. + # Note the data-autocompleter-target attribute. + def filter_conditional_rows(sections:, form:, model:) + capture do + tag.div(data: { autocompleter_target: "collapseFields" }, + class: "collapse") do + sections[:conditional].each do |field| + concat(filter_row(form:, field:, model:, sections:)) + end + end + end + end + def filter_yes_field(**args) options = [ ["", nil], diff --git a/app/views/controllers/names/filters/new.erb b/app/views/controllers/names/filters/new.erb index c1ae276ba5..3e1ae65830 100644 --- a/app/views/controllers/names/filters/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -4,5 +4,5 @@ %> <%= render(partial: "shared/filters_form", - locals: { local: true, filter: @filter, type: :name, + locals: { local: true, filter: @filter, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/observations/filters/new.erb b/app/views/controllers/observations/filters/new.erb index 137985cf63..3e1ae65830 100644 --- a/app/views/controllers/observations/filters/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -4,5 +4,5 @@ %> <%= render(partial: "shared/filters_form", - locals: { local: true, filter: @filter, type: :observation, + locals: { local: true, filter: @filter, field_columns: @field_columns } ) %> diff --git a/app/views/controllers/shared/_filters_form.erb b/app/views/controllers/shared/_filters_form.erb index 97fb0c28e0..4388f3e52e 100644 --- a/app/views/controllers/shared/_filters_form.erb +++ b/app/views/controllers/shared/_filters_form.erb @@ -1,4 +1,4 @@ -<%# locals: (filter: {}, field_columns: [], type: nil, local: true) -%> +<%# locals: (filter: {}, field_columns: [], model: filter.class.to_s.underscore.split("_")[0], local: true) -%> <%= form_with(model: filter, url: { action: :create }) do |form| %> @@ -7,7 +7,7 @@ <%= tag.div(class: "col-xs-12 col-md-6") do %> <% panels.each do |heading, sections| %> - <%= filter_panel(form:, filter:, heading:, sections:, type:) %> + <%= filter_panel(form:, filter:, heading:, sections:, model:) %> <% end %> <% end %> From e9366f3adcfbc997b5010f37d432944ce5872485 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 20:54:29 -0700 Subject: [PATCH 50/80] Clean up --- app/helpers/filters_helper.rb | 10 ++++++---- app/helpers/panel_helper.rb | 2 +- config/locales/en.txt | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 33fff5c619..11900154d6 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -56,13 +56,15 @@ def filter_row(form:, field:, model:, sections:) # Figure out what kind of field helper to call, based on definitions below. # Some field types need args, so there is both the component and args hash. def filter_field(form:, field:, model:, sections:) - args = { form: form, field: field } + args = { form:, field:, model: } args[:label] ||= filter_label(field) field_type = filter_field_type_from_parser(field:, model:) component = FILTER_FIELD_HELPERS[field_type][:component] return unless component + # Prepare args for the field helper. Requires but removes args[:model]. args = prepare_args_for_filter_field(args, field_type, component) + # Re-add sections and model for conditional fields. if component == :filter_autocompleter_with_conditional_fields args = args.merge(sections:, model:) end @@ -137,15 +139,15 @@ def prepare_args_for_filter_field(args, field_type, component) args[:help] = filter_help_text(args, field_type) args[:hidden_name] = filter_check_for_hidden_name(args) - FILTER_FIELD_HELPERS[field_type][:args].merge(args) + FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:model)) end def filter_help_text(args, field_type) component = FILTER_FIELD_HELPERS[field_type][:component] multiple_note = if component == :autocompleter_field - :filter_terms_multiple.l + :pattern_search_terms_multiple.l end - [:"#{args[:type]}_term_#{args[:field]}".l, multiple_note].compact.join(" ") + [:"#{args[:model]}_term_#{args[:field]}".l, multiple_note].compact.join(" ") end # Overrides for the assumed name of the id field for autocompleter. diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb index cf0d4f6b35..ea34a9f284 100644 --- a/app/helpers/panel_helper.rb +++ b/app/helpers/panel_helper.rb @@ -82,7 +82,7 @@ def panel_collapse_icons def panel_bodies(args) args[:panel_bodies].map.with_index do |body, idx| if args[:collapse].present? && idx == args[:panel_bodies].length - 1 - panel_collapse_body(args, body) + panel_collapse_body(args.merge(inner_class: "pt-0"), body) else panel_body(args, body) end diff --git a/config/locales/en.txt b/config/locales/en.txt index ecf8b3d27b..1ebfa013b3 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -3636,7 +3636,7 @@ observation_term_location: "Location (\"[:WHERE]\") mushroom was observed. Must exactly match the entire [:WHERE] field. Note that commas must be protected with a back-slash: \"Albion\\, California\\, USA\"." observation_term_region: Location mushroom was observed. Partial match anchored at end, including country at least, e.g., "California\, USA". Note that commas must be protected with a back-slash as shown. observation_term_project: Observation belongs to one of these projects. - observation_term_project_lists: Observation belongs to list in one of these projects. + observation_term_project_lists: Observation belongs to a species list in one of these projects. observation_term_list: Observation belongs to one of these species lists. observation_term_user: Observation created by one of these users. observation_term_notes: Notes contains the given string. From ebe72a51f8abfb1082b43525c04b33b8f6036fd3 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 21:29:09 -0700 Subject: [PATCH 51/80] More tweaks. Try to avoid margin-right --- app/helpers/forms_helper.rb | 21 ++++++++++++--------- app/helpers/panel_helper.rb | 5 +++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index b6595f8a3c..acb2b99319 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -97,9 +97,7 @@ def check_box_with_label(**args) args[:checked_value] || "1", args[:unchecked_value] || "0")) concat(args[:label]) - if args[:between].present? - concat(tag.div(class: "d-inline-block ml-3") { args[:between] }) - end + concat(args[:between]) if args[:between].present? end) concat(args[:append]) if args[:append].present? end @@ -511,8 +509,6 @@ def form_group_wrap_class(args, base = "form-group") def field_label_opts(args) label_opts = {} - need_margin = args[:between].present? - label_opts[:class] = need_margin ? "mr-2" : "" label_opts[:index] = args[:index] if args[:index].present? label_opts end @@ -526,7 +522,9 @@ def check_for_optional_or_required_note(args) keys = [:optional, :required].freeze positions.each do |pos| keys.each do |key| - args[pos] = help_note(:span, "(#{key.l})") if args[pos] == key + if args[pos] == key + args[pos] = help_note(:span, "(#{key.l})", class: "ml-3") + end end end args @@ -538,15 +536,20 @@ def check_for_help_block(args) return args end + need_margin = args[:inline].present? + between_class = need_margin ? "mr-3" : "" + id = [ nested_field_id(args), "help" ].compact_blank.join("_") args[:between] = capture do - if args[:between].present? - concat(tag.span(class: "mr-2") { args[:between] }) + tag.span(class: between_class) do + if args[:between].present? + concat(tag.span(class: "ml-3") { args[:between] }) + end + concat(collapse_info_trigger(id, class: "ml-3")) end - concat(collapse_info_trigger(id, class: "ml-1 mr-3")) end args[:append] = capture do concat(args[:append]) diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb index ea34a9f284..d443f0cd9a 100644 --- a/app/helpers/panel_helper.rb +++ b/app/helpers/panel_helper.rb @@ -156,8 +156,9 @@ def help_tooltip(label, **args) end # make a help-note styled element, like a div, p, or span - def help_note(element = :span, string = "") - content_tag(element, string, class: "help-note mr-3") + def help_note(element = :span, string = "", **args) + args[:class] = class_names("help-note mr-3", args[:class]) + content_tag(element, string, args) end # make a help-block styled element, like a div, p From 9eb6651167127a2838338a813f850b3bc44a97c0 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 21:37:05 -0700 Subject: [PATCH 52/80] forms in panels - less margin at bottom --- app/assets/stylesheets/mo/_form_elements.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/mo/_form_elements.scss b/app/assets/stylesheets/mo/_form_elements.scss index 20ff6d9a2f..7d6f02cb0c 100644 --- a/app/assets/stylesheets/mo/_form_elements.scss +++ b/app/assets/stylesheets/mo/_form_elements.scss @@ -180,3 +180,9 @@ form { max-height: 30rem; overflow-y: auto; } + +.panel-body { + .form-group:last-child { + margin-bottom: 0; + } +} From 9f17c2ec2d8e0771681f75496023a346abae3378 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 22:31:48 -0700 Subject: [PATCH 53/80] Fix range fields, search_type --- app/classes/pattern_search/base.rb | 6 +++--- app/controllers/concerns/filterable.rb | 4 ++-- app/controllers/names/filters_controller.rb | 4 ++-- app/controllers/observations/filters_controller.rb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index d0e4e6e520..a014d0e4da 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -65,9 +65,9 @@ def lookup_param(var) # rubocop:disable Metrics/AbcSize def make_terms_available_to_faceted_form parser.terms.each_with_object({}) do |term, hash| + # term is what the user typed in, not the parsed value. param = lookup_param_name(term.var) if fields_with_dates.include?(param) - # term is what the user typed in, not the parsed value. start, range = check_for_date_range(term) hash[param] = start hash[:"#{param}_range"] = range if range @@ -117,7 +117,7 @@ def check_for_numeric_range(term) bits = term.vals[0].split("-") if bits.size == 2 - [bits[0].to_i, bits[1].to_i] + bits.map(&:to_i) else [term.vals[0], nil] end @@ -129,7 +129,7 @@ def fields_with_dates end def fields_with_numeric_range - [:rank].freeze + [:confidence].freeze end end end diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 4b345b9619..21f3496274 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -72,8 +72,8 @@ def concatenate_range_fields next unless fields_with_range.include?(key.to_sym) && @keywords[:"#{key}_range"].present? - @keywords[key] = [@keywords[key].strip, - @keywords[:"#{key}_range"].strip].join("-") + @keywords[key] = [@keywords[key].to_s.strip, + @keywords[:"#{key}_range"].to_s.strip].join("-") @keywords.delete(:"#{key}_range") end end diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index 184a14dbf8..72f03fadc9 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -38,7 +38,7 @@ def check_for_clear_form end def new_filter_instance_from_session - if session[:pattern] && session[:search_type] == :names + if session[:pattern] && session[:search_type] == :name terms = PatternSearch::Name.new(session[:pattern]).form_params @filter = NameFilter.new(terms) else @@ -55,7 +55,7 @@ def set_pattern_string @pattern = formatted_pattern_search_string # Save it so that we can keep it in the search bar in subsequent pages. session[:pattern] = @pattern - session[:search_type] = :names + session[:search_type] = :name end # This is the list of fields that are displayed in the search form. In the diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index 9a6185ce94..a5cec3f7ff 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -39,7 +39,7 @@ def check_for_clear_form end def new_filter_instance_from_session - if session[:pattern] && session[:search_type] == :observations + if session[:pattern] && session[:search_type] == :observation terms = PatternSearch::Observation.new(session[:pattern]).form_params @filter = ObservationFilter.new(terms) else @@ -58,7 +58,7 @@ def set_pattern_string @pattern = formatted_pattern_search_string # Save it so that we can keep it in the search bar in subsequent pages. session[:pattern] = @pattern - session[:search_type] = :observations + session[:search_type] = :observation end # This is the list of fields that are displayed in the search form. In the From 8156917810166830a7148beba2285535ef28ae88 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 18 Sep 2024 22:52:52 -0700 Subject: [PATCH 54/80] Clean up and rubocop --- .../comments/pattern_search_controller.rb | 15 ----------- app/controllers/concerns/filterable.rb | 27 +++---------------- .../pattern_search_controller.rb | 15 ----------- .../herbaria/pattern_search_controller.rb | 15 ----------- .../pattern_search_controller.rb | 15 ----------- .../locations/pattern_search_controller.rb | 15 ----------- .../projects/pattern_search_controller.rb | 15 ----------- .../pattern_search_controller.rb | 15 ----------- .../users/pattern_search_controller.rb | 15 ----------- app/helpers/filters_helper.rb | 22 +++++++-------- config/routes.rb | 1 - 11 files changed, 14 insertions(+), 156 deletions(-) delete mode 100644 app/controllers/comments/pattern_search_controller.rb delete mode 100644 app/controllers/glossary_terms/pattern_search_controller.rb delete mode 100644 app/controllers/herbaria/pattern_search_controller.rb delete mode 100644 app/controllers/herbarium_records/pattern_search_controller.rb delete mode 100644 app/controllers/locations/pattern_search_controller.rb delete mode 100644 app/controllers/projects/pattern_search_controller.rb delete mode 100644 app/controllers/species_lists/pattern_search_controller.rb delete mode 100644 app/controllers/users/pattern_search_controller.rb diff --git a/app/controllers/comments/pattern_search_controller.rb b/app/controllers/comments/pattern_search_controller.rb deleted file mode 100644 index 0e9631ce9c..0000000000 --- a/app/controllers/comments/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Comments pattern search form. -# -# Route: `new_comment_pattern_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/comments/pattern_search", action: :create }` -module Comments - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 21f3496274..74da6d092b 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -21,8 +21,9 @@ module Filterable extend ActiveSupport::Concern + # Rubocop is incorrect here. This is a concern, not a class. + # rubocop:disable Metrics/BlockLength included do - def formatted_pattern_search_string sift_and_restructure_form_params keyword_strings = @sendable_params.map do |key, value| @@ -42,29 +43,6 @@ def sift_and_restructure_form_params # set_storable_params end - # def incomplete_date?(value) - # value.is_a?(Hash) && value.values.any?(&:blank?) - # end - - # Deal with date fields, which are stored as hashes with year, month, day. - # Convert them to a single string. Can use `web_date` method on date fields. - # def format_date_params_into_strings - # @keywords.each_key do |key| - # next unless fields_with_dates.include?(key.to_sym) - # next if @keywords[key][:year].blank? - - # @keywords[key] = date_into_string(key) - # if @keywords[:"#{key}_range"].present? - # @keywords[:"#{key}_range"] = date_into_string(:"#{key}_range") - # end - # end - # end - - # date is a hash with year, month, day. Convert to string. - # def date_into_string(key) - # Date.new(*permitted_search_params.to_h[key].values.map(&:to_i)).web_date - # end - # Check for `fields_with_range`, and concatenate them if range val present, # removing the range field. def concatenate_range_fields @@ -129,4 +107,5 @@ def escape_locations_and_remove_ids end end end + # rubocop:enable Metrics/BlockLength end diff --git a/app/controllers/glossary_terms/pattern_search_controller.rb b/app/controllers/glossary_terms/pattern_search_controller.rb deleted file mode 100644 index f3d8bf4fbc..0000000000 --- a/app/controllers/glossary_terms/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# GlossaryTerms pattern search form. -# -# Route: `new_glossary_term_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/glossary_terms/pattern_search", action: :create }` -module GlossaryTerms - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/herbaria/pattern_search_controller.rb b/app/controllers/herbaria/pattern_search_controller.rb deleted file mode 100644 index 2e6e585777..0000000000 --- a/app/controllers/herbaria/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Herbaria pattern search form. -# -# Route: `new_herbarium_pattern_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/herbaria/pattern_search", action: :create }` -module Herbaria - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/herbarium_records/pattern_search_controller.rb b/app/controllers/herbarium_records/pattern_search_controller.rb deleted file mode 100644 index 13943efcd4..0000000000 --- a/app/controllers/herbarium_records/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Herbarium record pattern search form. -# -# Route: `new_herbarium_record_pattern_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/herbarium_records/pattern_search", action: :create }` -module HerbariumRecords - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/locations/pattern_search_controller.rb b/app/controllers/locations/pattern_search_controller.rb deleted file mode 100644 index f9937c2915..0000000000 --- a/app/controllers/locations/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Locations pattern search form. -# -# Route: `new_location_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/locations/pattern_search", action: :create }` -module Locations - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/projects/pattern_search_controller.rb b/app/controllers/projects/pattern_search_controller.rb deleted file mode 100644 index d932f0695a..0000000000 --- a/app/controllers/projects/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Projects pattern search form. -# -# Route: `new_project_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/projects/pattern_search", action: :create }` -module Projects - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/species_lists/pattern_search_controller.rb b/app/controllers/species_lists/pattern_search_controller.rb deleted file mode 100644 index 405f52155f..0000000000 --- a/app/controllers/species_lists/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# SpeciesLists pattern search form. -# -# Route: `new_species_list_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/species_lists/pattern_search", action: :create }` -module SpeciesLists - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/controllers/users/pattern_search_controller.rb b/app/controllers/users/pattern_search_controller.rb deleted file mode 100644 index 3dafffa362..0000000000 --- a/app/controllers/users/pattern_search_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# Users pattern search form. -# -# Route: `new_user_search_path` -# Only one action here. Call namespaced controller actions with a hash like -# `{ controller: "/users/pattern_search", action: :create }` -module Users - class PatternSearchController < ApplicationController - before_action :login_required - - def new - end - end -end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 11900154d6..471a3986f5 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -43,7 +43,7 @@ def filter_row(form:, field:, model:, sections:) if field.is_a?(Array) tag.div(class: "row") do field.each do |subfield| - concat(tag.div(class: filter_columns) do + concat(tag.div(class: filter_column_classes) do filter_field(form:, field: subfield, model:, sections:) end) end @@ -166,7 +166,7 @@ def filter_check_for_hidden_name(args) # FIELD HELPERS # # Complex mechanism: append collapsed fields to autocompleter that only appear - # when autocompleter has a value. Only on the name field + # when autocompleter has a value. Only on the name field. def filter_autocompleter_with_conditional_fields(**args) return if args[:sections].blank? @@ -188,31 +188,31 @@ def filter_conditional_rows(sections:, form:, model:) end end - def filter_yes_field(**args) + def filter_yes_field(**) options = [ ["", nil], ["yes", "yes"] ] - select_with_label(options:, inline: true, **args) + select_with_label(options:, inline: true, **) end - def filter_boolean_field(**args) + def filter_boolean_field(**) options = [ ["", nil], ["yes", "yes"], ["no", "no"] ] - select_with_label(options:, inline: true, **args) + select_with_label(options:, inline: true, **) end - def filter_yes_no_both_field(**args) + def filter_yes_no_both_field(**) options = [ ["", nil], ["yes", "yes"], ["no", "no"], ["both", "either"] ] - select_with_label(options:, inline: true, **args) + select_with_label(options:, inline: true, **) end # RANGE FIELDS The first field gets the label, name and ID of the actual @@ -221,12 +221,12 @@ def filter_yes_no_both_field(**args) def filter_date_range_field(**args) tag.div(class: "row") do [ - tag.div(class: filter_columns) do + tag.div(class: filter_column_classes) do text_field_with_label(**args.merge( { between: "(YYYY-MM-DD)" } )) end, - tag.div(class: filter_columns) do + tag.div(class: filter_column_classes) do text_field_with_label(**args.merge( { field: "#{args[:field]}_range", label: :to.l, help: nil, between: :optional } @@ -281,7 +281,7 @@ def filter_latitude_field(**args) text_field_with_label(**args.merge(between: "(-90.0 to 90.0)")) end - def filter_columns + def filter_column_classes "col-xs-12 col-sm-6 col-md-12 col-lg-6" end end diff --git a/config/routes.rb b/config/routes.rb index a90595bf2a..38d30499c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -577,7 +577,6 @@ def route_actions_hash get("names/eol_expanded_review", to: "names/eol_data/expanded_review#show", as: "names_eol_expanded_review") - # ----- Observations: standard actions ---------------------------- namespace :observations do resources :downloads, only: [:new, :create] From a105685d982abfc9e09049eac49d1e8dcca1767e Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Thu, 19 Sep 2024 01:07:41 -0700 Subject: [PATCH 55/80] fields_with_requirements - search_type - search_subclass --- app/classes/pattern_search/name.rb | 4 + app/classes/pattern_search/observation.rb | 7 +- app/controllers/concerns/filterable.rb | 75 ++++++++++++------- app/controllers/names/filters_controller.rb | 12 --- .../observations/filters_controller.rb | 12 --- app/models/name_filter.rb | 5 +- app/models/observation_filter.rb | 3 +- app/models/search_filter.rb | 8 +- .../controllers/shared/_filters_form.erb | 2 +- 9 files changed, 69 insertions(+), 59 deletions(-) diff --git a/app/classes/pattern_search/name.rb b/app/classes/pattern_search/name.rb index 843e4e7ebc..46a05add8a 100644 --- a/app/classes/pattern_search/name.rb +++ b/app/classes/pattern_search/name.rb @@ -54,6 +54,10 @@ def self.fields_with_ids [] end + def self.fields_with_requirements + [] + end + def params self.class.params end diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index 59980c2a9f..6f6297f1d0 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -9,7 +9,7 @@ class Observation < Base created: [:created_at, :parse_date_range], modified: [:updated_at, :parse_date_range], - # names + # names. note that the last four require the first one to be present name: [:names, :parse_list_of_names], exclude_consensus: [:exclude_consensus, :parse_boolean], # of_look_alikes include_subtaxa: [:include_subtaxa, :parse_boolean], @@ -75,6 +75,11 @@ def self.fields_with_ids [:name, :location, :user, :herbarium, :list, :project, :species_list] end + def self.fields_with_requirements + [{ name: [:exclude_consensus, :include_subtaxa, :include_synonyms, + :include_all_name_proposals] }] + end + def params self.class.params end diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 74da6d092b..e34e6e813c 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -24,6 +24,10 @@ module Filterable # Rubocop is incorrect here. This is a concern, not a class. # rubocop:disable Metrics/BlockLength included do + def search_subclass + PatternSearch.const_get(@filter.class.search_type.capitalize) + end + def formatted_pattern_search_string sift_and_restructure_form_params keyword_strings = @sendable_params.map do |key, value| @@ -36,19 +40,33 @@ def formatted_pattern_search_string def sift_and_restructure_form_params @keywords = @filter.attributes.to_h.compact_blank.symbolize_keys + remove_invalid_field_combinations concatenate_range_fields @sendable_params = @keywords - substitute_ids_for_names + substitute_ids_for_strings # @storable_params = @keywords # set_storable_params end + # Passing some fields will raise an error if the required field is missing, + # so just toss them. + def remove_invalid_field_combinations + return unless search_subclass.respond_to?(:fields_with_requirements) + + search_subclass.fields_with_requirements.each do |req, fields| + next if @keywords[req].present? + + fields.each { |field| @keywords.delete(field) } + end + end + # Check for `fields_with_range`, and concatenate them if range val present, - # removing the range field. + # removing the `_range` field. def concatenate_range_fields - @keywords.each_key do |key| - next unless fields_with_range.include?(key.to_sym) && - @keywords[:"#{key}_range"].present? + return unless search_subclass.respond_to?(:fields_with_range) + + search_subclass.fields_with_range.each do |key| + next if @keywords[:"#{key}_range"].blank? @keywords[key] = [@keywords[key].to_s.strip, @keywords[:"#{key}_range"].to_s.strip].join("-") @@ -56,36 +74,35 @@ def concatenate_range_fields end end - # SENDABLE_PARAMS - # These methods don't modify the original @keywords hash. + # SENDABLE_PARAMS - params sent to Query. + # These methods don't modify the original @keywords. # - # Controller declares `fields_with_ids` which autocompleter send ids. - # This method substitutes the ids for the names. - def substitute_ids_for_names - @sendable_params.each_key do |key| - next unless fields_with_ids.include?(key.to_sym) && - @sendable_params[:"#{key}_id"].present? + # This method substitutes the ids for the strings typed in the form. + def substitute_ids_for_strings + search_subclass.fields_with_ids.each do |key| + next if @sendable_params[:"#{key}_id"].blank? @sendable_params[key] = @sendable_params[:"#{key}_id"] @sendable_params.delete(:"#{key}_id") end end - # STORABLE_PARAMS - # These methods don't modify the original @keywords hash. + # STORABLE_PARAMS - params string in session. + # These methods don't modify the original @keywords. # - # Store full strings for all values, including names and locations, - # so we can repopulate the form with the same values. + # Ideally we'd store full strings for all values, including names and + # locations, so we can repopulate the form with the same values. def set_storable_params - escape_names_and_remove_ids + escape_strings_and_remove_ids escape_locations_and_remove_ids end - # Escape-quote the names, the way the short form requires. - def escape_names_and_remove_ids - @storable_params.each_key do |key| - next unless fields_with_ids.include?(key.to_sym) && - @storable_params[:"#{key}_id"].present? + # Escape-quote the strings, the way the short form requires. + def escape_strings_and_remove_ids + search_subclass.fields_with_ids.each do |key| + # location handled separately + next if key.to_sym == :location + next if @storable_params[:"#{key}_id"].blank? list = @storable_params[key].split(",").map(&:strip) list = list.map { |name| "\"#{name}\"" } @@ -94,13 +111,15 @@ def escape_names_and_remove_ids end end - # Escape-quote the locations and their commas. + # Escape-quote the locations and their commas. We'd prefer to have legible + # strings in the url, but we're not storing long location strings yet, + # because the comma handling is difficult. Maybe switch to textarea with + # `\n` separator. def escape_locations_and_remove_ids - @storable_params.each_key do |key| - next unless [:location, :region].include?(key.to_sym) && - @storable_params[:"#{key}_id"].present? + [:location, :region].each do |key| + next if @storable_params[:"#{key}_id"].blank? - list = @storable_params[key].split(",").map(&:strip) + list = @storable_params[key].split("\n").map(&:strip) list = list.map { |location| "\"#{location.tr(",", "\\,")}\"" } @storable_params[key] = list.join(",") @storable_params.delete(:"#{key}_id") diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index 72f03fadc9..9c5e38e5d9 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -83,18 +83,6 @@ def set_up_form_field_groupings ].freeze end - def fields_with_dates - PatternSearch::Name.fields_with_dates - end - - def fields_with_range - PatternSearch::Name.fields_with_range - end - - def fields_with_ids - PatternSearch::Name.fields_with_ids - end - def permitted_search_params params.permit(name_search_params) end diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index a5cec3f7ff..f244f5e07c 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -91,18 +91,6 @@ def set_up_form_field_groupings ].freeze end - def fields_with_dates - PatternSearch::Observation.fields_with_dates - end - - def fields_with_range - PatternSearch::Observation.fields_with_range - end - - def fields_with_ids - PatternSearch::Observation.fields_with_ids - end - def permitted_search_params params.permit(observation_search_params) end diff --git a/app/models/name_filter.rb b/app/models/name_filter.rb index a605423f31..e6e841292f 100644 --- a/app/models/name_filter.rb +++ b/app/models/name_filter.rb @@ -2,6 +2,8 @@ # Non-AR model for the faceted PatternSearch form. class NameFilter < SearchFilter + # Assign attributes from the PatternSearch::Observation.params hash, + # adjusting for range fields and autocompleters with hidden id fields. PatternSearch::Name.params.map do |keyword, values| case values[1] when :parse_date_range @@ -10,9 +12,6 @@ class NameFilter < SearchFilter when :parse_rank_range attribute(keyword, :string) attribute(:"#{keyword}_range", :string) - when :parse_confidence - attribute(keyword, :integer) - attribute(:"#{keyword}_range", :integer) else attribute(keyword, :string) end diff --git a/app/models/observation_filter.rb b/app/models/observation_filter.rb index 90b499d60d..280500e5fa 100644 --- a/app/models/observation_filter.rb +++ b/app/models/observation_filter.rb @@ -2,7 +2,8 @@ # Non-AR model for the faceted PatternSearch form. class ObservationFilter < SearchFilter - # Assign attributes from the PatternSearch::Observation.params hash + # Assign attributes from the PatternSearch::Observation.params hash, + # adjusting for range fields and autocompleters with hidden id fields. PatternSearch::Observation.params.map do |keyword, values| case values[1] when :parse_date_range diff --git a/app/models/search_filter.rb b/app/models/search_filter.rb index d7e904f339..3c31eaaa4b 100644 --- a/app/models/search_filter.rb +++ b/app/models/search_filter.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true -# Non-AR model for the faceted PatternSearch form. +# Non-AR model for the faceted PatternSearch form. Subclass this for each model +# you want to search, named after the model it's for, eg "ObservationFilter" class SearchFilter include ActiveModel::Model include ActiveModel::Attributes + + # Returns the type of search (table_name) the subclass filter is for. + def self.search_type + to_s.underscore.gsub("_filter", "").to_sym + end end diff --git a/app/views/controllers/shared/_filters_form.erb b/app/views/controllers/shared/_filters_form.erb index 4388f3e52e..ff7517acb4 100644 --- a/app/views/controllers/shared/_filters_form.erb +++ b/app/views/controllers/shared/_filters_form.erb @@ -1,4 +1,4 @@ -<%# locals: (filter: {}, field_columns: [], model: filter.class.to_s.underscore.split("_")[0], local: true) -%> +<%# locals: (filter: {}, field_columns: [], model: filter.class.search_type, local: true) -%> <%= form_with(model: filter, url: { action: :create }) do |form| %> From d3aa1b813477440fa5f4c679c9b85bf8c5361847 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 21 Sep 2024 21:32:06 -0700 Subject: [PATCH 56/80] Filters controller tests --- .../filters_controller_test.rb} | 8 +++++++- .../filters_controller_test.rb} | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) rename test/controllers/{name_filters_controller_test.rb => names/filters_controller_test.rb} (52%) rename test/controllers/{observation_filters_controller_test.rb => observations/filters_controller_test.rb} (51%) diff --git a/test/controllers/name_filters_controller_test.rb b/test/controllers/names/filters_controller_test.rb similarity index 52% rename from test/controllers/name_filters_controller_test.rb rename to test/controllers/names/filters_controller_test.rb index 41c67bb106..88f53fa4df 100644 --- a/test/controllers/name_filters_controller_test.rb +++ b/test/controllers/names/filters_controller_test.rb @@ -5,5 +5,11 @@ # ------------------------------------------------------------ # Name filters - test pattern search # ------------------------------------------------------------ -class NameFiltersControllerTest < FunctionalTestCase +module Names + class FiltersControllerTest < FunctionalTestCase + def test_existing_pattern + @request.session["pattern"] = "something" + @request.session["search_type"] = "name" + end + end end diff --git a/test/controllers/observation_filters_controller_test.rb b/test/controllers/observations/filters_controller_test.rb similarity index 51% rename from test/controllers/observation_filters_controller_test.rb rename to test/controllers/observations/filters_controller_test.rb index 34630034f3..e0dd9904f4 100644 --- a/test/controllers/observation_filters_controller_test.rb +++ b/test/controllers/observations/filters_controller_test.rb @@ -5,5 +5,11 @@ # ------------------------------------------------------------ # Observation filters - test pattern search # ------------------------------------------------------------ -class ObservationFiltersControllerTest < FunctionalTestCase +module Observations + class FiltersControllerTest < FunctionalTestCase + def test_existing_pattern + @request.session["pattern"] = "something" + @request.session["search_type"] = "observation" + end + end end From 1a72425eb2dce18edf4c394c55a2361189234270 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 21 Sep 2024 21:42:27 -0700 Subject: [PATCH 57/80] Create pattern_search_integration_test.rb --- .../capybara/pattern_search_integration_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/integration/capybara/pattern_search_integration_test.rb diff --git a/test/integration/capybara/pattern_search_integration_test.rb b/test/integration/capybara/pattern_search_integration_test.rb new file mode 100644 index 0000000000..589035e53c --- /dev/null +++ b/test/integration/capybara/pattern_search_integration_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require("test_helper") + +# Integration tests of the Observations controller and views. +class ObservationsIntegrationTest < CapybaraIntegrationTestCase +end From ad5389598bcc8bd0d2a0869e37571e82c03cf6ea Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 22 Sep 2024 13:44:40 -0700 Subject: [PATCH 58/80] fields_with_requirements is hash --- app/classes/pattern_search/name.rb | 3 ++- app/classes/pattern_search/observation.rb | 5 +++-- app/controllers/concerns/filterable.rb | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/classes/pattern_search/name.rb b/app/classes/pattern_search/name.rb index 46a05add8a..0321d9f2fe 100644 --- a/app/classes/pattern_search/name.rb +++ b/app/classes/pattern_search/name.rb @@ -54,8 +54,9 @@ def self.fields_with_ids [] end + # hash of required: fields def self.fields_with_requirements - [] + {} end def params diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index 6f6297f1d0..b898856953 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -75,9 +75,10 @@ def self.fields_with_ids [:name, :location, :user, :herbarium, :list, :project, :species_list] end + # hash of required: fields def self.fields_with_requirements - [{ name: [:exclude_consensus, :include_subtaxa, :include_synonyms, - :include_all_name_proposals] }] + { name: [:exclude_consensus, :include_subtaxa, :include_synonyms, + :include_all_name_proposals] } end def params diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index e34e6e813c..8e8479fa43 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -54,6 +54,7 @@ def remove_invalid_field_combinations return unless search_subclass.respond_to?(:fields_with_requirements) search_subclass.fields_with_requirements.each do |req, fields| + debugger next if @keywords[req].present? fields.each { |field| @keywords.delete(field) } From f149a405a3dcef8bdd943500201adbcaf2f08927 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 22 Sep 2024 14:07:15 -0700 Subject: [PATCH 59/80] Add "more" to panel uncollapse --- app/controllers/concerns/filterable.rb | 1 - app/helpers/filters_helper.rb | 3 ++- app/helpers/panel_helper.rb | 32 ++++++++++++++++---------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 8e8479fa43..e34e6e813c 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -54,7 +54,6 @@ def remove_invalid_field_combinations return unless search_subclass.respond_to?(:fields_with_requirements) search_subclass.fields_with_requirements.each do |req, fields| - debugger next if @keywords[req].present? fields.each { |field| @keywords.delete(field) } diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 471a3986f5..9820bfe791 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -14,7 +14,8 @@ def filter_panel(form:, filter:, heading:, sections:, model:) open = filter.attributes.keys.intersect?(sections[:collapsed]) end panel_block(heading: :"search_term_group_#{heading}".l, - collapse:, open:, panel_bodies: [shown, collapsed]) + collapse:, open:, collapse_message: :MORE.l, + panel_bodies: [shown, collapsed]) end def filter_panel_shown(form:, sections:, model:) diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb index d443f0cd9a..71ae2efc9a 100644 --- a/app/helpers/panel_helper.rb +++ b/app/helpers/panel_helper.rb @@ -16,21 +16,25 @@ def panel_block(**args, &block) **args.except(*panel_inner_args) ) do concat(heading) - if args[:panel_bodies].present? - concat(panel_bodies(args)) - elsif args[:collapse].present? - concat(panel_collapse_body(args, content)) - else - concat(panel_body(args, content)) - end + concat(panel_body_or_bodies(args, content)) concat(footer) end end + def panel_body_or_bodies(args, content) + if args[:panel_bodies].present? + panel_bodies(args) + elsif args[:collapse].present? + panel_collapse_body(args, content) + else + panel_body(args, content) + end + end + # Args passed to panel components that are not applied to the outer div. def panel_inner_args [:class, :inner_class, :inner_id, :heading, :heading_links, :panel_bodies, - :collapse, :open, :footer].freeze + :collapse, :collapse_message, :open, :footer].freeze end def panel_heading(args) @@ -67,15 +71,19 @@ def panel_heading_collapse_elements(args) aria: { expanded: args[:open], controls: args[:collapse] } ) do [args[:heading], - tag.span(panel_collapse_icons, class: "float-right")].safe_join + tag.span(panel_collapse_icons(args), class: "float-right")].safe_join end end end # The caret icon that indicates toggling the panel open/collapsed. - def panel_collapse_icons - [link_icon(:chevron_down, title: :OPEN.l, class: "active-icon"), - link_icon(:chevron_up, title: :CLOSE.l)].safe_join + def panel_collapse_icons(args) + if (message = args[:collapse_message]).present? + message = tag.span(message, class: "font-weight-normal mr-2") + end + [message, + link_icon(:chevron_down, title: :OPEN.l, class: "active-icon"), + link_icon(:chevron_up, title: :CLOSE.l)].compact_blank.safe_join end # Some panels need multiple panel bodies. Potentially collapse the last one. From 0275935981cbd5c76d6a23588157a4957566f78a Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 22 Sep 2024 14:31:05 -0700 Subject: [PATCH 60/80] Go back to storing strings. Ids needs more work --- app/controllers/concerns/filterable.rb | 56 ++++++++++++++------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index e34e6e813c..4b9202528c 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -42,10 +42,9 @@ def sift_and_restructure_form_params remove_invalid_field_combinations concatenate_range_fields - @sendable_params = @keywords - substitute_ids_for_strings - # @storable_params = @keywords - # set_storable_params + + @sendable_params = substitute_ids_with_strings(@keywords) + # @storable_params = configure_storable_params(@keywords) end # Passing some fields will raise an error if the required field is missing, @@ -74,56 +73,63 @@ def concatenate_range_fields end end - # SENDABLE_PARAMS - params sent to Query. - # These methods don't modify the original @keywords. + # SENDABLE_PARAMS - params with ids can be sent to index and query. + # + # This method deletes the strings typed in the form and sends ids, saving a + # lookup at the receiver. However, we still want a legible string saved in + # the session, so we can repopulate the form with legible values - plus + # maybe in the url. Could send the id versions as separate `filter` param? # - # This method substitutes the ids for the strings typed in the form. - def substitute_ids_for_strings + # Need to modify autocompleters to check for record id on load if prefilled. + def substitute_strings_with_ids(keywords) search_subclass.fields_with_ids.each do |key| - next if @sendable_params[:"#{key}_id"].blank? + next if keywords[:"#{key}_id"].blank? - @sendable_params[key] = @sendable_params[:"#{key}_id"] - @sendable_params.delete(:"#{key}_id") + keywords[key] = keywords[:"#{key}_id"] + keywords.delete(:"#{key}_id") end + keywords end - # STORABLE_PARAMS - params string in session. + # STORABLE_PARAMS - params for the pattern string in session. # These methods don't modify the original @keywords. # # Ideally we'd store full strings for all values, including names and # locations, so we can repopulate the form with the same values. - def set_storable_params - escape_strings_and_remove_ids - escape_locations_and_remove_ids + def substitute_ids_with_strings(keywords) + escape_strings_and_remove_ids(keywords) + escape_locations_and_remove_ids(keywords) end # Escape-quote the strings, the way the short form requires. - def escape_strings_and_remove_ids + def escape_strings_and_remove_ids(keywords) search_subclass.fields_with_ids.each do |key| # location handled separately next if key.to_sym == :location - next if @storable_params[:"#{key}_id"].blank? + next if keywords[:"#{key}_id"].blank? - list = @storable_params[key].split(",").map(&:strip) + list = keywords[key].split(",").map(&:strip) list = list.map { |name| "\"#{name}\"" } - @storable_params[key] = list.join(",") - @storable_params.delete(:"#{key}_id") + keywords[key] = list.join(",") + keywords.delete(:"#{key}_id") end + keywords end # Escape-quote the locations and their commas. We'd prefer to have legible # strings in the url, but we're not storing long location strings yet, # because the comma handling is difficult. Maybe switch to textarea with # `\n` separator. - def escape_locations_and_remove_ids + def escape_locations_and_remove_ids(keywords) [:location, :region].each do |key| - next if @storable_params[:"#{key}_id"].blank? + next if keywords[:"#{key}_id"].blank? - list = @storable_params[key].split("\n").map(&:strip) + list = keywords[key].split("\n").map(&:strip) list = list.map { |location| "\"#{location.tr(",", "\\,")}\"" } - @storable_params[key] = list.join(",") - @storable_params.delete(:"#{key}_id") + keywords[key] = list.join(",") + keywords.delete(:"#{key}_id") end + keywords end end # rubocop:enable Metrics/BlockLength From 1536c5602d336368fc54ecbb5c33dc42c3609482 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sun, 22 Sep 2024 17:43:41 -0700 Subject: [PATCH 61/80] Check that session[:pattern].present? --- app/controllers/names/filters_controller.rb | 2 +- app/controllers/observations/filters_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index 9c5e38e5d9..9d7a77184c 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -38,7 +38,7 @@ def check_for_clear_form end def new_filter_instance_from_session - if session[:pattern] && session[:search_type] == :name + if session[:pattern].present? && session[:search_type] == :name terms = PatternSearch::Name.new(session[:pattern]).form_params @filter = NameFilter.new(terms) else diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index f244f5e07c..0feb98b60f 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -39,7 +39,7 @@ def check_for_clear_form end def new_filter_instance_from_session - if session[:pattern] && session[:search_type] == :observation + if session[:pattern].present? && session[:search_type] == :observation terms = PatternSearch::Observation.new(session[:pattern]).form_params @filter = ObservationFilter.new(terms) else From 7a89e5b2bbd63cd0456bad64cd85278169d6eae0 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 23 Sep 2024 15:56:07 -0700 Subject: [PATCH 62/80] Fix prefilling values on panels, and pre-opening if filled --- app/helpers/filters_helper.rb | 62 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 9820bfe791..f52d5704be 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -6,58 +6,71 @@ module FiltersHelper # Filter panel for a search form. Sections are shown and collapsed. # If sections[:collapsed] is present, part of the panel will be collapsed. def filter_panel(form:, filter:, heading:, sections:, model:) - shown = filter_panel_shown(form:, sections:, model:) - collapsed = filter_panel_collapsed(form:, sections:, model:) + shown = filter_panel_shown(form:, filter:, sections:, model:) + collapsed = filter_panel_collapsed(form:, filter:, sections:, model:) open = collapse = false if sections[:collapsed].present? collapse = heading - open = filter.attributes.keys.intersect?(sections[:collapsed]) + open = filter_panel_open?(filter:, sections:) end panel_block(heading: :"search_term_group_#{heading}".l, collapse:, open:, collapse_message: :MORE.l, panel_bodies: [shown, collapsed]) end - def filter_panel_shown(form:, sections:, model:) + # This returns the current filter terms in the form of a hash. + def filter_params(filter:) + filter.attributes.compact_blank.transform_keys(&:to_sym) + end + + def filter_panel_open?(filter:, sections:) + current = filter_params(filter:)&.keys || [] + this_section = sections[:collapsed].flatten # could be pairs of fields + return true if current.intersect?(this_section) + + false + end + + def filter_panel_shown(form:, filter:, sections:, model:) return unless sections.is_a?(Hash) && sections[:shown].present? capture do sections[:shown].each do |field| - concat(filter_row(form:, field:, model:, sections:)) + concat(filter_row(form:, filter:, field:, model:, sections:)) end end end # Content of collapsed section, composed of field rows. - def filter_panel_collapsed(form:, sections:, model:) + def filter_panel_collapsed(form:, filter:, sections:, model:) return unless sections.is_a?(Hash) && sections[:collapsed].present? capture do sections[:collapsed].each do |field| - concat(filter_row(form:, field:, model:, sections:)) + concat(filter_row(form:, filter:, field:, model:, sections:)) end end end # Fields might be paired, so we need to check for that. - def filter_row(form:, field:, model:, sections:) + def filter_row(form:, filter:, field:, model:, sections:) if field.is_a?(Array) tag.div(class: "row") do field.each do |subfield| concat(tag.div(class: filter_column_classes) do - filter_field(form:, field: subfield, model:, sections:) + filter_field(form:, filter:, field: subfield, model:, sections:) end) end end else - filter_field(form:, field:, model:, sections:) + filter_field(form:, filter:, field:, model:, sections:) end end # Figure out what kind of field helper to call, based on definitions below. # Some field types need args, so there is both the component and args hash. - def filter_field(form:, field:, model:, sections:) - args = { form:, field:, model: } + def filter_field(form:, filter:, field:, model:, sections:) + args = { form:, filter:, field:, model: } args[:label] ||= filter_label(field) field_type = filter_field_type_from_parser(field:, model:) component = FILTER_FIELD_HELPERS[field_type][:component] @@ -83,7 +96,7 @@ def filter_label(field) # The PatternSearch subclasses define how they're going to parse their # fields, so we can use that to assign a field helper. - # example: :parse_yes -> :filter_yes_field + # example: :parse_yes -> :yes, from which we deduce :filter_yes_field # If the field is :pattern, there's no assigned parser. def filter_field_type_from_parser(field:, model:) return :pattern if field == :pattern @@ -130,6 +143,10 @@ def filter_field_type_from_parser(field:, model:) latitude: { component: :filter_latitude_field, args: {} } }.freeze + FILTER_SELECT_TYPES = [ + :yes, :boolean, :yes_no_both, :rank_range, :confidence + ].freeze + # Prepares HTML args for the field helper. This is where we can make # adjustments to the args hash before passing it to the field helper. # NOTE: Bootstrap 3 can't do full-width inline label/field. @@ -139,6 +156,7 @@ def prepare_args_for_filter_field(args, field_type, component) end args[:help] = filter_help_text(args, field_type) args[:hidden_name] = filter_check_for_hidden_name(args) + args = filter_prefill_or_select_values(args, field_type) FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:model)) end @@ -162,6 +180,15 @@ def filter_check_for_hidden_name(args) nil end + def filter_prefill_or_select_values(args, field_type) + if FILTER_SELECT_TYPES.include?(field_type) + args[:selected] = args[:filter].send(args[:field]) || nil + else + args[:value] = args[:filter].send(args[:field]) || nil + end + args + end + ############################################################### # # FIELD HELPERS @@ -242,13 +269,13 @@ def filter_rank_range_field(**args) tag.div(class: "d-inline-block mr-4") do select_with_label(**args.merge( { inline: true, options: Name.all_ranks, - include_blank: true, selected: nil } + include_blank: true } )) end, tag.div(class: "d-inline-block") do select_with_label(**args.merge( { label: :to.l, between: :optional, help: nil, inline: true, - options: Name.all_ranks, include_blank: true, selected: nil, + options: Name.all_ranks, include_blank: true, field: "#{args[:field]}_range" } )) end @@ -260,14 +287,13 @@ def filter_confidence_range_field(**args) [ tag.div(class: "d-inline-block mr-4") do select_with_label(**args.merge( - { inline: true, options: confidences, - include_blank: true, selected: nil } + { inline: true, options: confidences, include_blank: true } )) end, tag.div(class: "d-inline-block") do select_with_label(**args.merge( { label: :to.l, between: :optional, help: nil, inline: true, - options: confidences, include_blank: true, selected: nil, + options: confidences, include_blank: true, field: "#{args[:field]}_range" } )) end From 247167a430a0274521e0e0751f6b0579b5509e0f Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Mon, 23 Sep 2024 16:31:30 -0700 Subject: [PATCH 63/80] Re-add pattern fields --- app/controllers/names/filters_controller.rb | 5 +++-- app/controllers/observations/filters_controller.rb | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/names/filters_controller.rb b/app/controllers/names/filters_controller.rb index 9d7a77184c..8bdb1c99d4 100644 --- a/app/controllers/names/filters_controller.rb +++ b/app/controllers/names/filters_controller.rb @@ -63,12 +63,13 @@ def set_pattern_string # with an array of fields or field pairings. def set_up_form_field_groupings @field_columns = [ - { date: { shown: [:created, :modified], collapsed: [] }, + { pattern: { shown: [:pattern], collapsed: [] }, quality: { shown: [[:has_observations, :deprecated]], collapsed: [[:has_author, :author], [:has_citation, :citation]] - } }, + }, + date: { shown: [:created, :modified], collapsed: [] } }, { scope: { shown: [[:has_synonyms, :include_synonyms], [:include_subtaxa, :include_misspellings]], diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index 0feb98b60f..3ddff1bd37 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -79,7 +79,8 @@ def set_up_form_field_groupings collapsed: [[:has_public_lat_lng, :is_collection_location], [:east, :west], [:north, :south]] } }, - { detail: { + { pattern: { shown: [:pattern], collapsed: [] }, + detail: { shown: [[:has_specimen, :has_sequence]], collapsed: [[:has_images, :has_notes], [:has_field, :notes], [:has_comments, :comments]] From f00564a54e2478f00f944a50093abacc92b29e61 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 15:04:12 -0700 Subject: [PATCH 64/80] Containers for these pages --- app/views/controllers/names/filters/new.erb | 2 +- app/views/controllers/observations/filters/new.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/controllers/names/filters/new.erb b/app/views/controllers/names/filters/new.erb index 3e1ae65830..dec74e1a4c 100644 --- a/app/views/controllers/names/filters/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -1,6 +1,6 @@ <% # add_page_title(:pattern_search) -@container = :full +@container = :double %> <%= render(partial: "shared/filters_form", diff --git a/app/views/controllers/observations/filters/new.erb b/app/views/controllers/observations/filters/new.erb index 3e1ae65830..dec74e1a4c 100644 --- a/app/views/controllers/observations/filters/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -1,6 +1,6 @@ <% # add_page_title(:pattern_search) -@container = :full +@container = :double %> <%= render(partial: "shared/filters_form", From fc31ac9addcdd6647694e6499cbcd5406c801ab5 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 15:41:03 -0700 Subject: [PATCH 65/80] Pass obj to location helpers to prefill value --- app/helpers/form_locations_helper.rb | 30 ++++++++++--------- .../controllers/locations/form/_fields.erb | 8 +++-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/helpers/form_locations_helper.rb b/app/helpers/form_locations_helper.rb index 5752a144db..c0d4f44cb3 100644 --- a/app/helpers/form_locations_helper.rb +++ b/app/helpers/form_locations_helper.rb @@ -13,35 +13,36 @@ def form_location_input_find_on_map(form:, field:, value: nil, label: nil) # This will generate a compass rose of inputs for given form object. # The inputs are for compass directions. - def form_compass_input_group(form:) + def form_compass_input_group(form:, obj:) compass_groups.each do |dir| if compass_north_south.include?(dir) - concat(compass_north_south_row(form, dir)) + concat(compass_north_south_row(form, obj, dir)) else - concat(compass_east_west_row(form, dir)) + concat(compass_east_west_row(form, obj, dir)) end end end - def compass_north_south_row(form, dir) + def compass_north_south_row(form, obj, dir) tag.div(class: compass_row_classes(dir)) do - compass_input(form, dir, compass_col_classes(dir)) + compass_input(form, obj, dir, compass_col_classes(dir)) end end - def compass_east_west_row(form, dir) + def compass_east_west_row(form, obj, dir) tag.div(class: compass_row_classes(dir)) do - [compass_input(form, dir[0], compass_col_classes(dir[0])), + [compass_input(form, obj, dir[0], compass_col_classes(dir[0])), compass_help, - compass_input(form, dir[1], compass_col_classes(dir[1]))].safe_join + compass_input(form, obj, dir[1], compass_col_classes(dir[1]))].safe_join end end # Note these inputs are Stimulus map controller targets - def compass_input(form, dir, col_classes) + def compass_input(form, obj, dir, col_classes) tag.div(class: col_classes) do text_field_with_label( - form:, field: dir, label: "#{dir.upcase.to_sym.t}:", addon: "º", + form:, field: dir, value: send(obj, dir), + label: "#{dir.upcase.to_sym.t}:", addon: "º", data: { map_target: "#{dir}Input", action: "map#bufferInputs" } ) end @@ -82,18 +83,19 @@ def compass_north_south ############################################################################## # Elevation # - def form_elevation_input_group(form:) + def form_elevation_input_group(form:, obj:) tag.div(class: "text-center") do elevation_directions.each do |dir| - concat(elevation_input(form, dir)) + concat(elevation_input(form, obj, dir)) end concat(elevation_request_button) end end - def elevation_input(form, dir) + def elevation_input(form, obj, dir) text_field_with_label( - form: form, field: dir, label: :"show_location_#{dir}est".t, addon: "m", + form: form, field: dir, value: send(obj, dir), + label: :"show_location_#{dir}est".t, addon: "m", data: { map_target: "#{dir}Input", action: "map#bufferInputs" } ) end diff --git a/app/views/controllers/locations/form/_fields.erb b/app/views/controllers/locations/form/_fields.erb index 094143fb58..5b485a46f5 100644 --- a/app/views/controllers/locations/form/_fields.erb +++ b/app/views/controllers/locations/form/_fields.erb @@ -6,8 +6,12 @@ <%= tag.div(class: "row mt-5") do [ - tag.div(class: "col-sm-8") { form_compass_input_group(form: f) }, - tag.div(class: "col-sm-4") { form_elevation_input_group(form: f) } + tag.div(class: "col-sm-8") do + form_compass_input_group(form: f, obj: location) + end, + tag.div(class: "col-sm-4") do + form_elevation_input_group(form: f, obj: location) + end ].safe_join end %> From 8a2ad5bca158be4e85cc9ebf66f3dc86675d1636 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 15:41:14 -0700 Subject: [PATCH 66/80] Send obj to helpers --- app/helpers/filters_helper.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index f52d5704be..116e8b3542 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -82,6 +82,8 @@ def filter_field(form:, filter:, field:, model:, sections:) if component == :filter_autocompleter_with_conditional_fields args = args.merge(sections:, model:) end + return filter_region_with_compass_fields(**args) if field == :region + send(component, **args) end @@ -155,7 +157,7 @@ def prepare_args_for_filter_field(args, field_type, component) args[:inline] = true end args[:help] = filter_help_text(args, field_type) - args[:hidden_name] = filter_check_for_hidden_name(args) + args[:hidden_name] = filter_check_for_hidden_field_name(args) args = filter_prefill_or_select_values(args, field_type) FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:model)) @@ -170,7 +172,7 @@ def filter_help_text(args, field_type) end # Overrides for the assumed name of the id field for autocompleter. - def filter_check_for_hidden_name(args) + def filter_check_for_hidden_field_name(args) case args[:field] when :list return "list_id" @@ -300,6 +302,15 @@ def filter_confidence_range_field(**args) ].safe_join end + def filter_region_with_compass_fields(**args) + [ + form_location_input_find_on_map(form: f, field: :region, + value: args[:filter].region, + label: "#{:WHERE.t}:"), + form_compass_input_group(form: f) + ].safe_join + end + def filter_longitude_field(**args) text_field_with_label(**args.merge(between: "(-180.0 to 180.0)")) end From bafb9fbce852ed9a9436621179e2c4e79de0ece6 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 18:28:45 -0700 Subject: [PATCH 67/80] add pattern param and filter attribute --- app/classes/pattern_search/name.rb | 2 +- app/classes/pattern_search/observation.rb | 2 +- app/models/search_filter.rb | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/classes/pattern_search/name.rb b/app/classes/pattern_search/name.rb index 0321d9f2fe..264d72ccf2 100644 --- a/app/classes/pattern_search/name.rb +++ b/app/classes/pattern_search/name.rb @@ -38,7 +38,7 @@ def self.params # Autocompleters have id fields, and range fields are concatenated. def self.fields params.keys + [ - :created_range, :modified_range, :rank_range + :created_range, :modified_range, :rank_range, :pattern ] end diff --git a/app/classes/pattern_search/observation.rb b/app/classes/pattern_search/observation.rb index b898856953..e633890277 100644 --- a/app/classes/pattern_search/observation.rb +++ b/app/classes/pattern_search/observation.rb @@ -59,7 +59,7 @@ def self.fields params.keys + [ :name_id, :location_id, :user_id, :herbarium_id, :list_id, :project_id, :project_lists_id, :when_range, :created_range, :modified_range, - :rank_range, :confidence_range + :rank_range, :confidence_range, :pattern ] end diff --git a/app/models/search_filter.rb b/app/models/search_filter.rb index 3c31eaaa4b..1a7093fbe7 100644 --- a/app/models/search_filter.rb +++ b/app/models/search_filter.rb @@ -6,6 +6,8 @@ class SearchFilter include ActiveModel::Model include ActiveModel::Attributes + attribute :pattern, :string + # Returns the type of search (table_name) the subclass filter is for. def self.search_type to_s.underscore.gsub("_filter", "").to_sym From 63e1615ddc1ffdc71cb6e5741d405e3567df2c3f Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 18:29:27 -0700 Subject: [PATCH 68/80] region/compass sort of working --- app/helpers/filters_helper.rb | 104 ++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 116e8b3542..20ad673aa2 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -2,6 +2,7 @@ # helpers for pattern search forms. These call field helpers in forms_helper. # args should provide form, field, label at a minimum. +# rubocop:disable Metrics/ModuleLength module FiltersHelper # Filter panel for a search form. Sections are shown and collapsed. # If sections[:collapsed] is present, part of the panel will be collapsed. @@ -82,7 +83,10 @@ def filter_field(form:, filter:, field:, model:, sections:) if component == :filter_autocompleter_with_conditional_fields args = args.merge(sections:, model:) end - return filter_region_with_compass_fields(**args) if field == :region + if field == :region + # debugger + return filter_region_with_compass_fields(**args) + end send(component, **args) end @@ -112,43 +116,6 @@ def filter_field_type_from_parser(field:, model:) parser.to_s.gsub(/^parse_/, "").to_sym end - FILTER_SEPARATOR = ", " - - # Convenience for subclasses to access helper methods via subclass.params - FILTER_FIELD_HELPERS = { - pattern: { component: :text_field_with_label, args: {} }, - yes: { component: :filter_yes_field, args: {} }, - boolean: { component: :filter_boolean_field, args: {} }, - yes_no_both: { component: :filter_yes_no_both_field, args: {} }, - date_range: { component: :filter_date_range_field, args: {} }, - rank_range: { component: :filter_rank_range_field, args: {} }, - string: { component: :text_field_with_label, args: {} }, - list_of_strings: { component: :text_field_with_label, args: {} }, - list_of_herbaria: { component: :autocompleter_field, - args: { type: :herbarium, - separator: FILTER_SEPARATOR } }, - list_of_locations: { component: :autocompleter_field, - args: { type: :location, - separator: FILTER_SEPARATOR } }, - list_of_names: { component: :filter_autocompleter_with_conditional_fields, - args: { type: :name, separator: FILTER_SEPARATOR } }, - list_of_projects: { component: :autocompleter_field, - args: { type: :project, - separator: FILTER_SEPARATOR } }, - list_of_species_lists: { component: :autocompleter_field, - args: { type: :species_list, - separator: FILTER_SEPARATOR } }, - list_of_users: { component: :autocompleter_field, - args: { type: :user, separator: ", " } }, - confidence: { component: :filter_confidence_range_field, args: {} }, - longitude: { component: :filter_longitude_field, args: {} }, - latitude: { component: :filter_latitude_field, args: {} } - }.freeze - - FILTER_SELECT_TYPES = [ - :yes, :boolean, :yes_no_both, :rank_range, :confidence - ].freeze - # Prepares HTML args for the field helper. This is where we can make # adjustments to the args hash before passing it to the field helper. # NOTE: Bootstrap 3 can't do full-width inline label/field. @@ -200,19 +167,20 @@ def filter_prefill_or_select_values(args, field_type) def filter_autocompleter_with_conditional_fields(**args) return if args[:sections].blank? - append = filter_conditional_rows(sections: args[:sections], - form: args[:form], model: args[:model]) + # rightward destructuring assignment ruby 3 feature + args => { form:, model:, filter:, sections: } + append = filter_conditional_rows(form:, model:, filter:, sections:) autocompleter_field(**args.except(:sections, :model).merge(append:)) end # Rows that only uncollapse if an autocompleter field has a value. # Note the data-autocompleter-target attribute. - def filter_conditional_rows(sections:, form:, model:) + def filter_conditional_rows(form:, model:, filter:, sections:) capture do tag.div(data: { autocompleter_target: "collapseFields" }, class: "collapse") do sections[:conditional].each do |field| - concat(filter_row(form:, field:, model:, sections:)) + concat(filter_row(form:, field:, model:, filter:, sections:)) end end end @@ -303,12 +271,13 @@ def filter_confidence_range_field(**args) end def filter_region_with_compass_fields(**args) - [ - form_location_input_find_on_map(form: f, field: :region, - value: args[:filter].region, - label: "#{:WHERE.t}:"), - form_compass_input_group(form: f) - ].safe_join + capture do + concat(form_location_input_find_on_map(form: args[:form], field: :region, + value: args[:filter].region, + label: "#{:REGION.t}:")) + concat(form_compass_input_group(form: args[:form], + obj: args[:filter])) + end end def filter_longitude_field(**args) @@ -322,4 +291,43 @@ def filter_latitude_field(**args) def filter_column_classes "col-xs-12 col-sm-6 col-md-12 col-lg-6" end + + # Separator for autocompleter fields. + FILTER_SEPARATOR = ", " + + # Convenience for subclasses to access helper methods via subclass.params + FILTER_FIELD_HELPERS = { + pattern: { component: :text_field_with_label, args: {} }, + yes: { component: :filter_yes_field, args: {} }, + boolean: { component: :filter_boolean_field, args: {} }, + yes_no_both: { component: :filter_yes_no_both_field, args: {} }, + date_range: { component: :filter_date_range_field, args: {} }, + rank_range: { component: :filter_rank_range_field, args: {} }, + string: { component: :text_field_with_label, args: {} }, + list_of_strings: { component: :text_field_with_label, args: {} }, + list_of_herbaria: { component: :autocompleter_field, + args: { type: :herbarium, + separator: FILTER_SEPARATOR } }, + list_of_locations: { component: :autocompleter_field, + args: { type: :location, separator: "\n" } }, + list_of_names: { component: :filter_autocompleter_with_conditional_fields, + args: { type: :name, separator: FILTER_SEPARATOR } }, + list_of_projects: { component: :autocompleter_field, + args: { type: :project, + separator: FILTER_SEPARATOR } }, + list_of_species_lists: { component: :autocompleter_field, + args: { type: :species_list, + separator: FILTER_SEPARATOR } }, + list_of_users: { component: :autocompleter_field, + args: { type: :user, separator: FILTER_SEPARATOR } }, + confidence: { component: :filter_confidence_range_field, args: {} }, + # handled in filter_region_with_compass_fields + longitude: { component: nil, args: {} }, + latitude: { component: nil, args: {} } + }.freeze + + FILTER_SELECT_TYPES = [ + :yes, :boolean, :yes_no_both, :rank_range, :confidence + ].freeze end +# rubocop:enable Metrics/ModuleLength From d4e796f5bb809f30e9eef19abbf43115502b16a2 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 18:29:43 -0700 Subject: [PATCH 69/80] fix accessor for value in compass/elevation inputs --- app/helpers/form_locations_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/form_locations_helper.rb b/app/helpers/form_locations_helper.rb index 6d6603eed3..af70cb4d8a 100644 --- a/app/helpers/form_locations_helper.rb +++ b/app/helpers/form_locations_helper.rb @@ -42,7 +42,7 @@ def compass_east_west_row(form, obj, dir) def compass_input(form, obj, dir, col_classes) tag.div(class: col_classes) do text_field_with_label( - form:, field: dir, value: obj[dir], + form:, field: dir, value: obj.send(dir), label: "#{dir.upcase.to_sym.t}:", addon: "º", data: { map_target: "#{dir}Input", action: "map#bufferInputs" } ) @@ -95,7 +95,7 @@ def form_elevation_input_group(form:, obj:) def elevation_input(form, obj, dir) text_field_with_label( - form: form, field: dir, value: obj[dir], + form: form, field: dir, value: obj.send(dir), label: :"show_location_#{dir}est".t, addon: "m", data: { map_target: "#{dir}Input", action: "map#bufferInputs" } ) From b35aba034c1ad549da86bded8a1033a4896a7e0e Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 18:36:16 -0700 Subject: [PATCH 70/80] fix concat --- app/helpers/filters_helper.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 20ad673aa2..3633cfddfa 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -83,10 +83,7 @@ def filter_field(form:, filter:, field:, model:, sections:) if component == :filter_autocompleter_with_conditional_fields args = args.merge(sections:, model:) end - if field == :region - # debugger - return filter_region_with_compass_fields(**args) - end + return filter_region_with_compass_fields(**args) if field == :region send(component, **args) end @@ -275,8 +272,7 @@ def filter_region_with_compass_fields(**args) concat(form_location_input_find_on_map(form: args[:form], field: :region, value: args[:filter].region, label: "#{:REGION.t}:")) - concat(form_compass_input_group(form: args[:form], - obj: args[:filter])) + concat(form_compass_input_group(form: args[:form], obj: args[:filter])) end end From 202ecadaa3abc6e99b0abc4094107ee01c1308a8 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Wed, 25 Sep 2024 18:36:27 -0700 Subject: [PATCH 71/80] fix capture/concat --- app/helpers/form_locations_helper.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/helpers/form_locations_helper.rb b/app/helpers/form_locations_helper.rb index af70cb4d8a..54db3a7e3f 100644 --- a/app/helpers/form_locations_helper.rb +++ b/app/helpers/form_locations_helper.rb @@ -15,11 +15,13 @@ def form_location_input_find_on_map(form:, field:, value: nil, label: nil) # inputs are for compass directions. The object can be a location or a filter, # that's what will prefill the values on load or reload. def form_compass_input_group(form:, obj:) - compass_groups.each do |dir| - if compass_north_south.include?(dir) - concat(compass_north_south_row(form, obj, dir)) - else - concat(compass_east_west_row(form, obj, dir)) + capture do + compass_groups.each do |dir| + if compass_north_south.include?(dir) + concat(compass_north_south_row(form, obj, dir)) + else + concat(compass_east_west_row(form, obj, dir)) + end end end end From 66817c9c7a5b8d21b9e8cbea2a51c75f265a02fe Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Thu, 26 Sep 2024 16:01:14 -0700 Subject: [PATCH 72/80] Add map ui Prolly need to make the map centering text field diff from region. --- app/helpers/filters_helper.rb | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 3633cfddfa..d2902efb82 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -268,11 +268,39 @@ def filter_confidence_range_field(**args) end def filter_region_with_compass_fields(**args) - capture do - concat(form_location_input_find_on_map(form: args[:form], field: :region, - value: args[:filter].region, - label: "#{:REGION.t}:")) - concat(form_compass_input_group(form: args[:form], obj: args[:filter])) + tag.div(data: { controller: "map", map_open: true }) do + [ + form_location_input_find_on_map(form: args[:form], field: :region, + value: args[:filter].region, + label: "#{:REGION.t}:"), + filter_compass_and_map_row(form: args[:form], filter: args[:filter]) + ].safe_join + end + end + + def filter_compass_and_map_row(form:, filter:) + minimal_loc = filter_minimal_location(filter) + tag.div(class: "row") do + [ + tag.div(class: filter_column_classes) do + form_compass_input_group(form:, obj: filter) + end, + tag.div(class: filter_column_classes) do + make_map(objects: [minimal_loc], editable: true, map_type: "location", + controller: nil) + end + ].safe_join + end + end + + def filter_minimal_location(filter) + if filter.north.present? && filter.south.present? && + filter.east.present? && filter.west.present? + Mappable::MinimalLocation.new( + nil, nil, filter.north, filter.south, filter.east, filter.west + ) + else + Mappable::MinimalLocation.new(nil, nil, 0, 0, 0, 0) end end From 4481316a0eebaff3db81c2104f077b503f1952b0 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 27 Sep 2024 10:23:52 -0700 Subject: [PATCH 73/80] Fix range values, plus string escaping for region --- app/classes/pattern_search/base.rb | 8 +- app/controllers/concerns/filterable.rb | 44 ++++++--- .../observations/filters_controller.rb | 4 +- app/helpers/filters_helper.rb | 98 +++++++++++-------- app/views/controllers/names/filters/new.erb | 4 +- .../controllers/observations/filters/new.erb | 4 +- 6 files changed, 100 insertions(+), 62 deletions(-) diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index a014d0e4da..1b88baa82e 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -99,9 +99,13 @@ def lookup_param_name(var) # Try for fidelity to the stored string, eg only years. def check_for_date_range(term) bits = term.vals[0].split("-") - if bits.size == 2 + case bits.size + when 1 + start = bits[0] + range = nil + when 2 start, range = bits - elsif bits.size == 4 + when 4 start = "#{bits[0]}-#{bits[1]}" range = "#{bits[2]}-#{bits[3]}" else diff --git a/app/controllers/concerns/filterable.rb b/app/controllers/concerns/filterable.rb index 4b9202528c..cc8bc9d5a8 100644 --- a/app/controllers/concerns/filterable.rb +++ b/app/controllers/concerns/filterable.rb @@ -43,7 +43,7 @@ def sift_and_restructure_form_params remove_invalid_field_combinations concatenate_range_fields - @sendable_params = substitute_ids_with_strings(@keywords) + @sendable_params = remove_ids_and_format_strings(@keywords) # @storable_params = configure_storable_params(@keywords) end @@ -96,41 +96,55 @@ def substitute_strings_with_ids(keywords) # # Ideally we'd store full strings for all values, including names and # locations, so we can repopulate the form with the same values. - def substitute_ids_with_strings(keywords) + def remove_ids_and_format_strings(keywords) escape_strings_and_remove_ids(keywords) escape_locations_and_remove_ids(keywords) end # Escape-quote the strings, the way the short form requires. + # rubocop:disable Metrics/AbcSize def escape_strings_and_remove_ids(keywords) search_subclass.fields_with_ids.each do |key| - # location handled separately - next if key.to_sym == :location - next if keywords[:"#{key}_id"].blank? + # location, region handled separately + next if keywords[key].blank? || strings_with_commas.include?(key.to_sym) list = keywords[key].split(",").map(&:strip) list = list.map { |name| "\"#{name}\"" } keywords[key] = list.join(",") + next if keywords[:"#{key}_id"].blank? + keywords.delete(:"#{key}_id") end keywords end + # rubocop:enable Metrics/AbcSize # Escape-quote the locations and their commas. We'd prefer to have legible - # strings in the url, but we're not storing long location strings yet, - # because the comma handling is difficult. Maybe switch to textarea with - # `\n` separator. + # strings in the url, but the comma handling is difficult. def escape_locations_and_remove_ids(keywords) - [:location, :region].each do |key| - next if keywords[:"#{key}_id"].blank? - - list = keywords[key].split("\n").map(&:strip) - list = list.map { |location| "\"#{location.tr(",", "\\,")}\"" } - keywords[key] = list.join(",") - keywords.delete(:"#{key}_id") + if keywords[:location].present? + list = keywords[:location].split("\n").map(&:strip) + list = list.map { |location| escape_location_string(location) } + keywords[:location] = list.join(",") end + keywords.delete(:location_id) if keywords[:location_id].present? + escape_region_string(keywords) + end + + def escape_region_string(keywords) + return keywords if keywords[:region].blank? + + keywords[:region] = escape_location_string(keywords[:region].strip) keywords end + + def escape_location_string(location) + "\"#{location.tr(",", "\\,")}\"" + end + + def strings_with_commas + [:location, :region].freeze + end end # rubocop:enable Metrics/BlockLength end diff --git a/app/controllers/observations/filters_controller.rb b/app/controllers/observations/filters_controller.rb index 3ddff1bd37..c0054c5556 100644 --- a/app/controllers/observations/filters_controller.rb +++ b/app/controllers/observations/filters_controller.rb @@ -75,9 +75,9 @@ def set_up_form_field_groupings collapsed: [:confidence, [:has_name, :lichen]] }, location: { - shown: [:location, :region], + shown: [:location], collapsed: [[:has_public_lat_lng, :is_collection_location], - [:east, :west], [:north, :south]] + :region, [:east, :west], [:north, :south]] } }, { pattern: { shown: [:pattern], collapsed: [] }, detail: { diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index d2902efb82..530297adf7 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -149,8 +149,6 @@ def filter_check_for_hidden_field_name(args) def filter_prefill_or_select_values(args, field_type) if FILTER_SELECT_TYPES.include?(field_type) args[:selected] = args[:filter].send(args[:field]) || nil - else - args[:value] = args[:filter].send(args[:field]) || nil end args end @@ -167,7 +165,9 @@ def filter_autocompleter_with_conditional_fields(**args) # rightward destructuring assignment ruby 3 feature args => { form:, model:, filter:, sections: } append = filter_conditional_rows(form:, model:, filter:, sections:) - autocompleter_field(**args.except(:sections, :model).merge(append:)) + autocompleter_field( + **args.except(:sections, :model, :filter).merge(append:) + ) end # Rows that only uncollapse if an autocompleter field has a value. @@ -217,82 +217,98 @@ def filter_date_range_field(**args) tag.div(class: "row") do [ tag.div(class: filter_column_classes) do - text_field_with_label(**args.merge( - { between: "(YYYY-MM-DD)" } - )) + text_field_with_label(**filter_date_args(args)) end, tag.div(class: filter_column_classes) do - text_field_with_label(**args.merge( - { field: "#{args[:field]}_range", label: :to.l, - help: nil, between: :optional } - )) + text_field_with_label(**filter_date_range_args(args)) end ].safe_join end end + def filter_date_args(args) + args.except(:filter).merge({ between: "(YYYY-MM-DD)" }) + end + + def filter_date_range_args(args) + args.except(:filter).merge( + { field: "#{args[:field]}_range", label: :to.l, + between: :optional, help: nil } + ) + end + def filter_rank_range_field(**args) [ tag.div(class: "d-inline-block mr-4") do - select_with_label(**args.merge( - { inline: true, options: Name.all_ranks, - include_blank: true } - )) + select_with_label(**filter_rank_args(args)) end, tag.div(class: "d-inline-block") do - select_with_label(**args.merge( - { label: :to.l, between: :optional, help: nil, inline: true, - options: Name.all_ranks, include_blank: true, - field: "#{args[:field]}_range" } - )) + select_with_label(**filter_rank_range_args(args)) end ].safe_join end + def filter_rank_args(args) + args.except(:filter).merge( + { options: Name.all_ranks, include_blank: true, inline: true } + ) + end + + def filter_rank_range_args(args) + args.except(:filter).merge( + { field: "#{args[:field]}_range", label: :to.l, options: Name.all_ranks, + include_blank: true, between: :optional, help: nil, inline: true } + ) + end + def filter_confidence_range_field(**args) confidences = Vote.opinion_menu.map { |k, v| [k, Vote.percent(v)] } [ tag.div(class: "d-inline-block mr-4") do - select_with_label(**args.merge( - { inline: true, options: confidences, include_blank: true } - )) + select_with_label(**filter_confidence_args(confidences, args)) end, tag.div(class: "d-inline-block") do - select_with_label(**args.merge( - { label: :to.l, between: :optional, help: nil, inline: true, - options: confidences, include_blank: true, - field: "#{args[:field]}_range" } - )) + select_with_label(**filter_confidence_range_args(confidences, args)) end ].safe_join end + def filter_confidence_args(confidences, args) + args.except(:filter).merge( + { options: confidences, include_blank: true, inline: true } + ) + end + + def filter_confidence_range_args(confidences, args) + args.except(:filter).merge( + { field: "#{args[:field]}_range", label: :to.l, options: confidences, + include_blank: true, between: :optional, help: nil, inline: true } + ) + end + def filter_region_with_compass_fields(**args) tag.div(data: { controller: "map", map_open: true }) do [ form_location_input_find_on_map(form: args[:form], field: :region, value: args[:filter].region, label: "#{:REGION.t}:"), - filter_compass_and_map_row(form: args[:form], filter: args[:filter]) + filter_compass_input_and_map(form: args[:form], filter: args[:filter]) ].safe_join end end - def filter_compass_and_map_row(form:, filter:) + def filter_compass_input_and_map(form:, filter:) minimal_loc = filter_minimal_location(filter) - tag.div(class: "row") do + capture do [ - tag.div(class: filter_column_classes) do - form_compass_input_group(form:, obj: filter) - end, - tag.div(class: filter_column_classes) do - make_map(objects: [minimal_loc], editable: true, map_type: "location", - controller: nil) - end + form_compass_input_group(form:, obj: filter), + make_map(objects: [minimal_loc], editable: true, map_type: "location", + map_open: false, controller: nil) ].safe_join end end + # To be mappable, we need to instantiate a minimal location from the filter. def filter_minimal_location(filter) if filter.north.present? && filter.south.present? && filter.east.present? && filter.west.present? @@ -305,11 +321,15 @@ def filter_minimal_location(filter) end def filter_longitude_field(**args) - text_field_with_label(**args.merge(between: "(-180.0 to 180.0)")) + text_field_with_label( + **args.except(:filter).merge(between: "(-180.0 to 180.0)") + ) end def filter_latitude_field(**args) - text_field_with_label(**args.merge(between: "(-90.0 to 90.0)")) + text_field_with_label( + **args.except(:filter).merge(between: "(-90.0 to 90.0)") + ) end def filter_column_classes diff --git a/app/views/controllers/names/filters/new.erb b/app/views/controllers/names/filters/new.erb index dec74e1a4c..6acfab192a 100644 --- a/app/views/controllers/names/filters/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -1,6 +1,6 @@ <% -# add_page_title(:pattern_search) -@container = :double +add_page_title("Name Search") # temporary. This page is not a public page +@container = :wide %> <%= render(partial: "shared/filters_form", diff --git a/app/views/controllers/observations/filters/new.erb b/app/views/controllers/observations/filters/new.erb index dec74e1a4c..0548a8ffab 100644 --- a/app/views/controllers/observations/filters/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -1,6 +1,6 @@ <% -# add_page_title(:pattern_search) -@container = :double +add_page_title("Observation Search") # temporary. This page is not a public page +@container = :wide %> <%= render(partial: "shared/filters_form", From 55300633cff0120f9230332bf6e91493777f0821 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Fri, 27 Sep 2024 13:42:02 -0700 Subject: [PATCH 74/80] Use existing page titles --- app/views/controllers/names/filters/new.erb | 2 +- app/views/controllers/observations/filters/new.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/controllers/names/filters/new.erb b/app/views/controllers/names/filters/new.erb index 6acfab192a..3b0c4e3e78 100644 --- a/app/views/controllers/names/filters/new.erb +++ b/app/views/controllers/names/filters/new.erb @@ -1,5 +1,5 @@ <% -add_page_title("Name Search") # temporary. This page is not a public page +add_page_title(add_page_title(:title_for_name_search.l)) @container = :wide %> diff --git a/app/views/controllers/observations/filters/new.erb b/app/views/controllers/observations/filters/new.erb index 0548a8ffab..4310e327a0 100644 --- a/app/views/controllers/observations/filters/new.erb +++ b/app/views/controllers/observations/filters/new.erb @@ -1,5 +1,5 @@ <% -add_page_title("Observation Search") # temporary. This page is not a public page +add_page_title(:title_for_observation_search.l) @container = :wide %> From ab7a9bca78fba8c1d01deefcfa4e654431975c02 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 16:55:57 -0800 Subject: [PATCH 75/80] Get the forms in the controller tests --- test/controllers/names/filters_controller_test.rb | 1 + test/controllers/observations/filters_controller_test.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/test/controllers/names/filters_controller_test.rb b/test/controllers/names/filters_controller_test.rb index 88f53fa4df..b33d2b35cc 100644 --- a/test/controllers/names/filters_controller_test.rb +++ b/test/controllers/names/filters_controller_test.rb @@ -10,6 +10,7 @@ class FiltersControllerTest < FunctionalTestCase def test_existing_pattern @request.session["pattern"] = "something" @request.session["search_type"] = "name" + get(:new) end end end diff --git a/test/controllers/observations/filters_controller_test.rb b/test/controllers/observations/filters_controller_test.rb index e0dd9904f4..aec1fde241 100644 --- a/test/controllers/observations/filters_controller_test.rb +++ b/test/controllers/observations/filters_controller_test.rb @@ -10,6 +10,7 @@ class FiltersControllerTest < FunctionalTestCase def test_existing_pattern @request.session["pattern"] = "something" @request.session["search_type"] = "observation" + get(:new) end end end From 51891eb340356221b927f51fe21dfc518fe8b907 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 19:54:04 -0800 Subject: [PATCH 76/80] Fix and test "rehydration" of form from session[:pattern] --- app/classes/pattern_search/base.rb | 2 +- test/controllers/names/filters_controller_test.rb | 5 ++++- .../observations/filters_controller_test.rb | 3 ++- test/models/name_filter_test.rb | 12 ++++++++++++ test/models/observation_filter_test.rb | 12 ++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 test/models/name_filter_test.rb create mode 100644 test/models/observation_filter_test.rb diff --git a/app/classes/pattern_search/base.rb b/app/classes/pattern_search/base.rb index 9587133643..1c6c98ac43 100644 --- a/app/classes/pattern_search/base.rb +++ b/app/classes/pattern_search/base.rb @@ -82,7 +82,7 @@ def make_terms_available_to_faceted_form def lookup_param_name(var) # See if this var matches an English parameter name first. - return var if params[var].present? + return var if var == :pattern || params[var].present? # Then check if any of the translated parameter names match. params.each_key do |key| diff --git a/test/controllers/names/filters_controller_test.rb b/test/controllers/names/filters_controller_test.rb index b33d2b35cc..b2460c047c 100644 --- a/test/controllers/names/filters_controller_test.rb +++ b/test/controllers/names/filters_controller_test.rb @@ -7,10 +7,13 @@ # ------------------------------------------------------------ module Names class FiltersControllerTest < FunctionalTestCase - def test_existing_pattern + def test_existing_name_pattern + login("rolf") @request.session["pattern"] = "something" @request.session["search_type"] = "name" get(:new) + # assert_select("input[type=text]#name_filter_pattern", + # text: "something", count: 1) end end end diff --git a/test/controllers/observations/filters_controller_test.rb b/test/controllers/observations/filters_controller_test.rb index aec1fde241..f157884260 100644 --- a/test/controllers/observations/filters_controller_test.rb +++ b/test/controllers/observations/filters_controller_test.rb @@ -7,7 +7,8 @@ # ------------------------------------------------------------ module Observations class FiltersControllerTest < FunctionalTestCase - def test_existing_pattern + def test_existing_obs_pattern + login("rolf") @request.session["pattern"] = "something" @request.session["search_type"] = "observation" get(:new) diff --git a/test/models/name_filter_test.rb b/test/models/name_filter_test.rb new file mode 100644 index 0000000000..294f0cea06 --- /dev/null +++ b/test/models/name_filter_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require("test_helper") + +class NameFilterTest < UnitTestCase + def test_create_name_filter_from_session + pattern = "something" + terms = PatternSearch::Name.new(pattern).form_params + filter = NameFilter.new(terms) + assert_equal(pattern, filter.pattern) + end +end diff --git a/test/models/observation_filter_test.rb b/test/models/observation_filter_test.rb new file mode 100644 index 0000000000..947eaa07c5 --- /dev/null +++ b/test/models/observation_filter_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require("test_helper") + +class ObservationFilterTest < UnitTestCase + def test_create_observation_filter_from_session + pattern = "something" + terms = PatternSearch::Observation.new(pattern).form_params + filter = ObservationFilter.new(terms) + assert_equal(pattern, filter.pattern) + end +end From 4879fdcc8c261df1ecf66f19b30fb36cb2b71c8d Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 19:54:19 -0800 Subject: [PATCH 77/80] Don't print the Filter attribute on the inputs! --- app/helpers/filters_helper.rb | 2 +- app/views/controllers/shared/_filters_form.erb | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 530297adf7..4010f2eac4 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -124,7 +124,7 @@ def prepare_args_for_filter_field(args, field_type, component) args[:hidden_name] = filter_check_for_hidden_field_name(args) args = filter_prefill_or_select_values(args, field_type) - FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:model)) + FILTER_FIELD_HELPERS[field_type][:args].merge(args.except(:model, :filter)) end def filter_help_text(args, field_type) diff --git a/app/views/controllers/shared/_filters_form.erb b/app/views/controllers/shared/_filters_form.erb index ff7517acb4..8c6b3638a5 100644 --- a/app/views/controllers/shared/_filters_form.erb +++ b/app/views/controllers/shared/_filters_form.erb @@ -1,6 +1,11 @@ <%# locals: (filter: {}, field_columns: [], model: filter.class.search_type, local: true) -%> +<% +form_args = { model: filter, url: { action: :create }, + id: "#{model}_filter_form" } +# form_args[:model] = filter if filter&:id +%> -<%= form_with(model: filter, url: { action: :create }) do |form| %> +<%= form_with(**form_args) do |form| %> <%= tag.div(class: "row") do %> <% field_columns.each do |panels| %> From c09cb5a6c71235f4fa68bbc9b875e401d5e23673 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 20:13:08 -0800 Subject: [PATCH 78/80] Add notes about using Query params, and adjust controller tests --- app/models/name_filter.rb | 6 ++++++ test/controllers/names/filters_controller_test.rb | 5 +++-- test/controllers/observations/filters_controller_test.rb | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/name_filter.rb b/app/models/name_filter.rb index e6e841292f..21cd9956e1 100644 --- a/app/models/name_filter.rb +++ b/app/models/name_filter.rb @@ -4,6 +4,12 @@ class NameFilter < SearchFilter # Assign attributes from the PatternSearch::Observation.params hash, # adjusting for range fields and autocompleters with hidden id fields. + # To switch to Query params, assign attribute(values[0], :date) etc. + # and update the @field_columns hash in FiltersController accordingly. + # Then change the form to build a @query instead of a @filter, with + # `pattern` being but one of the query params, and have the hydrator + # in the FiltersController#new action check the query instead of the + # session[:pattern] PatternSearch::Name.params.map do |keyword, values| case values[1] when :parse_date_range diff --git a/test/controllers/names/filters_controller_test.rb b/test/controllers/names/filters_controller_test.rb index b2460c047c..a52e85a6fa 100644 --- a/test/controllers/names/filters_controller_test.rb +++ b/test/controllers/names/filters_controller_test.rb @@ -9,8 +9,9 @@ module Names class FiltersControllerTest < FunctionalTestCase def test_existing_name_pattern login("rolf") - @request.session["pattern"] = "something" - @request.session["search_type"] = "name" + # may need to do this in an integration test + # @request.session["pattern"] = "something" + # @request.session["search_type"] = "name" get(:new) # assert_select("input[type=text]#name_filter_pattern", # text: "something", count: 1) diff --git a/test/controllers/observations/filters_controller_test.rb b/test/controllers/observations/filters_controller_test.rb index f157884260..667276f97c 100644 --- a/test/controllers/observations/filters_controller_test.rb +++ b/test/controllers/observations/filters_controller_test.rb @@ -9,8 +9,9 @@ module Observations class FiltersControllerTest < FunctionalTestCase def test_existing_obs_pattern login("rolf") - @request.session["pattern"] = "something" - @request.session["search_type"] = "observation" + # may need to do this in an integration test + # @request.session["pattern"] = "something" + # @request.session["search_type"] = "observation" get(:new) end end From 5d78978ed955136da55f91512aa699d02a95d151 Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 20:45:39 -0800 Subject: [PATCH 79/80] Update filters_helper.rb --- app/helpers/filters_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 4010f2eac4..39d74023fe 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -81,7 +81,7 @@ def filter_field(form:, filter:, field:, model:, sections:) args = prepare_args_for_filter_field(args, field_type, component) # Re-add sections and model for conditional fields. if component == :filter_autocompleter_with_conditional_fields - args = args.merge(sections:, model:) + args = args.merge(sections:, model:, filter:) end return filter_region_with_compass_fields(**args) if field == :region From 605b20d38ab1cdf44ddce840642b92721d7b7c4d Mon Sep 17 00:00:00 2001 From: andrew nimmo Date: Sat, 1 Feb 2025 20:50:43 -0800 Subject: [PATCH 80/80] Update en.txt --- config/locales/en.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/locales/en.txt b/config/locales/en.txt index 8f8cc69a23..4307bf0d53 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -1194,6 +1194,7 @@ search_term_south: south search_term_user: user search_term_west: west + search_term_group_pattern: "[:PATTERN]" search_term_group_search: "[:SEARCH]" search_term_group_date: "[:DATE]" search_term_group_quality: "[:QUALITY]" @@ -3742,6 +3743,7 @@ observation_term_has_comments: Has any comments? observation_term_is_collection_location: Mushroom was growing at the location. ("[:form_observations_is_collection_location]" is checked.) + name_term_pattern: "[:PATTERN]" name_term_created: Date name was first used. name_term_modified: Date name was last modified. name_term_rank: Rank or range of ranks, e.g., "genus" or "species-form".