diff --git a/.github/workflows/ci_rails.yml b/.github/workflows/ci_rails.yml
index d4d6eec05c..6a0937458c 100644
--- a/.github/workflows/ci_rails.yml
+++ b/.github/workflows/ci_rails.yml
@@ -73,7 +73,7 @@ jobs:
- name: Run tests
env:
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
- run: bundle exec rails test:all
+ run: bundle exec rails test
# https://github.com/marketplace/actions/coveralls-github-action
- name: Coveralls GitHub Action
diff --git a/.rubocop.yml b/.rubocop.yml
index 6a9e8e91ef..c63055ec44 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,4 +1,7 @@
inherit_from: .rubocop_todo.yml
+inherit_mode:
+ merge:
+ - Exclude
require:
- rubocop-performance
@@ -178,7 +181,7 @@ Rails/WhereExists:
Style/AccessorGrouping:
# https://github.com/MushroomObserver/mushroom-observer/pull/988
Enabled: false
- # The followin Exclude in case we re-enable the cop
+ # The following are Exclude in case we re-enable the cop
# Exclude files that individually comment accessors
# else Rubocop concatenates the accessors and comments on a single line.
# Disabling Style/AccessorGrouping around the accessors doesn't
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 5938c28a08..946aa3b9cd 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
# This configuration was generated by
-# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit`
-# on 2024-08-01 19:59:56 UTC using RuboCop version 1.65.1.
+# `rubocop --auto-gen-config --no-exclude-limit`
+# on 2024-08-08 16:15:46 UTC using RuboCop version 1.65.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -277,7 +277,7 @@ Metrics/AbcSize:
- 'script/refresh_sitemap'
- 'script/update_ip_stats.rb'
-# Offense count: 32
+# Offense count: 31
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
# AllowedMethods: refine
Metrics/BlockLength:
@@ -287,7 +287,6 @@ Metrics/BlockLength:
- 'app/controllers/concerns/descriptions/moves.rb'
- 'app/controllers/concerns/descriptions/publish.rb'
- 'app/extensions/symbol_extensions.rb'
- - 'app/helpers/forms_helper.rb'
- 'app/views/controllers/api2/_image.xml.builder'
- 'app/views/controllers/api2/_name.xml.builder'
- 'app/views/controllers/api2/_observation.xml.builder'
@@ -320,7 +319,7 @@ Metrics/ClassLength:
- 'app/models/abstract_model.rb'
- 'app/models/description.rb'
-# Offense count: 122
+# Offense count: 121
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
Metrics/CyclomaticComplexity:
Exclude:
@@ -403,7 +402,7 @@ Metrics/CyclomaticComplexity:
- 'script/bulk_name_change'
- 'script/update_ip_stats.rb'
-# Offense count: 20
+# Offense count: 19
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
@@ -416,7 +415,6 @@ Metrics/MethodLength:
- 'app/controllers/locations_controller.rb'
- 'app/controllers/names/eol_data/preview_controller.rb'
- 'app/extensions/symbol_extensions.rb'
- - 'app/helpers/forms_helper.rb'
- 'app/helpers/pagination_helper.rb'
- 'app/models/image.rb'
- 'app/models/language_exporter.rb'
@@ -446,7 +444,7 @@ Metrics/ParameterLists:
- 'test/controllers/projects/members_controller_test.rb'
- 'test/models/collapsible_map_test.rb'
-# Offense count: 94
+# Offense count: 93
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
Metrics/PerceivedComplexity:
Exclude:
diff --git a/Gemfile b/Gemfile
index eb9a4c326e..b13c7e96c9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -83,7 +83,7 @@ gem("blankslate")
# Simple version models and tables for classes
# Use our own fork, which stores enum attrs as integers in the db
gem("mo_acts_as_versioned", ">= 0.6.6",
- git: "https://github.com/MushroomObserver/acts_as_versioned/")
+ git: "https://github.com/MushroomObserver/acts_as_versioned")
# Use ActiveModel has_secure_password
gem("bcrypt")
diff --git a/Gemfile.lock b/Gemfile.lock
index 3614376bcb..7f14eefda8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,5 +1,5 @@
GIT
- remote: https://github.com/MushroomObserver/acts_as_versioned/
+ remote: https://github.com/MushroomObserver/acts_as_versioned
revision: 56891f7f915610e9f884e3a932bfb6934303b3b8
specs:
mo_acts_as_versioned (0.6.6)
@@ -78,7 +78,7 @@ GEM
bootstrap-sass (3.4.1)
autoprefixer-rails (>= 5.2.1)
sassc (>= 2.0.0)
- brakeman (6.1.2)
+ brakeman (6.2.1)
racc
browser (6.0.0)
builder (3.3.0)
@@ -99,7 +99,7 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chunky_png (1.4.0)
- concurrent-ruby (1.3.3)
+ concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crack (1.0.0)
bigdecimal
@@ -145,7 +145,7 @@ GEM
ffi (1.17.0-arm64-darwin)
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x86_64-linux-gnu)
- fugit (1.11.0)
+ fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.2.1)
@@ -308,7 +308,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
- rexml (3.3.4)
+ rexml (3.3.6)
strscan
rqrcode (2.2.0)
chunky_png (~> 1.0)
diff --git a/app/assets/stylesheets/Admin.scss b/app/assets/stylesheets/Admin.scss
index bf0e0ff2f9..823dac7709 100644
--- a/app/assets/stylesheets/Admin.scss
+++ b/app/assets/stylesheets/Admin.scss
@@ -9,7 +9,7 @@ $LOGO_BG_COLOR: yellow;
$LOGO_HOVER_FG_COLOR: purple;
$LOGO_HOVER_BG_COLOR: yellow;
-$LEFT_BAR_BORDER_COLOR: #545555; // gray
+$LEFT_BAR_BORDER_COLOR: #535354; // gray
$LEFT_BAR_BORDER_RADIUS: 0px;
$LEFT_BAR_HEADER_FG_COLOR: black;
$LEFT_BAR_HEADER_BG_COLOR: yellow;
diff --git a/app/assets/stylesheets/Agaricus.scss b/app/assets/stylesheets/Agaricus.scss
index c7d5dbcd37..8cc478c8e5 100644
--- a/app/assets/stylesheets/Agaricus.scss
+++ b/app/assets/stylesheets/Agaricus.scss
@@ -1,7 +1,7 @@
@import "defaults";
-$augustus_cap: #EaCe93; // #ECCF95
-$brasiliensis_gills_1: #A06463;
+$augustus_cap: #ECCF95;
+$brasiliensis_gills_1: #BF6262; // #A06463
$brasiliensis_gills_2: #743931;
$campestris_cap: #F6F0F2;
$cupreobrunneus_gills: #3B2821;
diff --git a/app/assets/stylesheets/Amanita.scss b/app/assets/stylesheets/Amanita.scss
index 44725ef795..199d8a63e1 100644
--- a/app/assets/stylesheets/Amanita.scss
+++ b/app/assets/stylesheets/Amanita.scss
@@ -13,7 +13,7 @@ $calyptroderma_middle_cap: #c18346;
$muscaria_background: #cc2616;
$muscaria_foreground: #fff8c6;
$velosa_background: #dd9d5f;
-$velosa_light_veil: #fbead3; // faebd4
+$velosa_light_veil: #f9e8d3; // faebd4
$velosa_dark_veil: #f4d5a6;
$novinupta_background: #d1afa5;
$pachycolea_background: #383138;
diff --git a/app/assets/stylesheets/BlackOnWhite.scss b/app/assets/stylesheets/BlackOnWhite.scss
index e538748a26..2809a5de38 100644
--- a/app/assets/stylesheets/BlackOnWhite.scss
+++ b/app/assets/stylesheets/BlackOnWhite.scss
@@ -4,7 +4,7 @@
$LOGO_BORDER_COLOR: #DDDDDD;
$LEFT_BAR_BORDER_COLOR: #DDDDDD;
-$TOP_BAR_BORDER_COLOR: #DFDfDD;
+$TOP_BAR_BORDER_COLOR: #D9D9D9;
$LIST_BORDER_COLOR: #DDDDDD;
$BUTTON_HOVER_BORDER_COLOR: #CCCCCC;
$BUTTON_BG_COLOR: #CCCCCC;
diff --git a/app/assets/stylesheets/Cantharellaceae.scss b/app/assets/stylesheets/Cantharellaceae.scss
index b9948d2268..22901b7443 100644
--- a/app/assets/stylesheets/Cantharellaceae.scss
+++ b/app/assets/stylesheets/Cantharellaceae.scss
@@ -11,7 +11,7 @@ $tubaeformis_hymenium: #c2914c;
$tubaeformis_bright_stipe: #ffb230;
$tubaeformis_dark_stipe: #4b2e0c;
$tubaeformis_light_stipe: #e5bb67;
-$cornucopioides_dark_hymenium: #11120b; // image 465 #10110b
+$cornucopioides_dark_hymenium: #13120d; // image 465 #10110b
$cornucopioides_light_hymenium: #9b9690;
$cornucopioides_dark_cap: #4f4337;
$cornucopioides_light_cap: #826c57;
diff --git a/app/assets/stylesheets/Hygrocybe.scss b/app/assets/stylesheets/Hygrocybe.scss
index dc7062c8db..7cbf4e55b7 100644
--- a/app/assets/stylesheets/Hygrocybe.scss
+++ b/app/assets/stylesheets/Hygrocybe.scss
@@ -1,6 +1,6 @@
@import "defaults";
-$conica_stain: #35362d; // #37372f
+$conica_stain: #34342c; // #37372f
$conica_cap_red: #a31404;
$conica_cap_orange: #dd6226;
$conica_cap_yellow: #ffbf01;
diff --git a/app/assets/stylesheets/Sudo.scss b/app/assets/stylesheets/Sudo.scss
index b091bf1cec..2687d767b8 100644
--- a/app/assets/stylesheets/Sudo.scss
+++ b/app/assets/stylesheets/Sudo.scss
@@ -1,6 +1,6 @@
@import "defaults";
-$BODY_BG_COLOR: #DE7500; // #DD7700
+$BODY_BG_COLOR: #DE7201; // #DD7700
$LOGO_BORDER_COLOR: black;
$LOGO_BORDER_WIDTH: 2px; // vs 1px in default
diff --git a/app/assets/stylesheets/mo/_autocomplete.scss b/app/assets/stylesheets/mo/_autocomplete.scss
index 885c5bb58f..af1909e111 100644
--- a/app/assets/stylesheets/mo/_autocomplete.scss
+++ b/app/assets/stylesheets/mo/_autocomplete.scss
@@ -6,16 +6,29 @@
.has-id-indicator {
display: none;
}
-.create-button {
- display: inline-block;
-}
-
.has-id {
.has-id-indicator {
display: inline-block;
}
+}
+
+.keep-btn {
+ display: none;
+}
+.create {
+ .keep-btn {
+ display: inline-block;
+ }
+}
+
+// initially we may not have id, but we also don't offer create
+// until they've typed something
+.create-button {
+ display: none;
+}
+.offer-create {
.create-button {
- display: none;
+ display: inline-block;
}
}
diff --git a/app/assets/stylesheets/mo/_help_tooltips.scss b/app/assets/stylesheets/mo/_help_tooltips.scss
index 5e2e32e92f..215f24aa06 100644
--- a/app/assets/stylesheets/mo/_help_tooltips.scss
+++ b/app/assets/stylesheets/mo/_help_tooltips.scss
@@ -17,7 +17,7 @@ $help-block-max-width: 600px;
.help-block {
color: $WELL_FG_COLOR;
- max-width: $help-block-max-width;
+ // max-width: $help-block-max-width;
}
.tooltip-inner {
diff --git a/app/assets/stylesheets/mo/_icons.scss b/app/assets/stylesheets/mo/_icons.scss
index 82c0b4ddc6..6e96d3a9ee 100644
--- a/app/assets/stylesheets/mo/_icons.scss
+++ b/app/assets/stylesheets/mo/_icons.scss
@@ -99,11 +99,13 @@
}
// This is for the stateful icon_link_to helper
-.icon-link {
+.icon-link,
+.panel-collapse-trigger {
.active-icon, .active-label {
display: none;
}
- &.active {
+ &.active,
+ &.collapsed {
.link-icon:not(.active-icon),
.sr-only:not(.active-label) {
display: none;
diff --git a/app/assets/stylesheets/mo/_maps.scss b/app/assets/stylesheets/mo/_maps.scss
index 58e2f0189a..ec76efb699 100644
--- a/app/assets/stylesheets/mo/_maps.scss
+++ b/app/assets/stylesheets/mo/_maps.scss
@@ -2,7 +2,7 @@
// thumbnail & google map styles
//
-.observation-form-map {
+.form-map {
height: 400px;
width: 100%;
background-position: center center;
diff --git a/app/controllers/admin/blocked_ips_controller.rb b/app/controllers/admin/blocked_ips_controller.rb
index dd9e67e39f..cc9ca6313d 100644
--- a/app/controllers/admin/blocked_ips_controller.rb
+++ b/app/controllers/admin/blocked_ips_controller.rb
@@ -4,7 +4,7 @@ module Admin
class BlockedIpsController < AdminController
# This page allows editing of blocked ips via params
# params[:add_okay] and params[:add_bad]
- # GETting this page with params[:report] will show info about a chosen IP
+ # Using params[:report] will show info about a chosen IP
def edit
@ip = params[:report] if validate_ip!(params[:report])
@blocked_ips = sort_by_ip(IpStats.read_blocked_ips)
@@ -14,6 +14,7 @@ def edit
# Render the page after an update
def update
+ strip_params!
process_blocked_ips_commands
@blocked_ips = sort_by_ip(IpStats.read_blocked_ips)
@okay_ips = sort_by_ip(IpStats.read_okay_ips)
@@ -23,6 +24,12 @@ def update
private
+ def strip_params!
+ [:add_bad, :remove_bad, :add_okay, :remove_okay].each do |param|
+ params[param] = params[param].strip if params[param]
+ end
+ end
+
def sort_by_ip(ips)
ips.sort_by do |ip|
ip.to_s.split(".").map { |n| n.to_i + 1000 }.map(&:to_s).join(" ")
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9c12d701d6..e1f6b95400 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -788,7 +788,12 @@ def flash_object_errors(obj)
def save_with_log(obj)
type_sym = obj.class.to_s.underscore.to_sym
if obj.save
- flash_notice(:runtime_created_at.t(type: type_sym))
+ notice = if obj.respond_to?(:text_name) && (name = obj.text_name)
+ :runtime_created_name.t(type: type_sym, value: name)
+ else
+ :runtime_created_at.t(type: type_sym)
+ end
+ flash_notice(notice)
true
else
flash_error(:runtime_no_save.t(type: type_sym))
diff --git a/app/controllers/concerns/locationable.rb b/app/controllers/concerns/locationable.rb
new file mode 100644
index 0000000000..e4a4d44413
--- /dev/null
+++ b/app/controllers/concerns/locationable.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+#
+# = Locationable Concern
+#
+# This is a module of reusable methods that can be included by controllers that
+# deal with associated locations, where the location may be created on the fly
+# in the create form. (Why not "locatable"? It could be otherwise locatable.)
+# - ObservationsController
+# - HerbariaController
+# - Account::ProfileController
+# - ProjectsController
+#
+# The controller must follow some conventions:
+# - send its main object ivar (@observation, etc) to the methods
+# - use a `@location` ivar
+# - set the `@user` ivar
+# - use the `@any_errors` ivar to halt the save process
+#
+# == Methods
+#
+# create_location_object_if_new(object):: Create a new Location associated with
+# the object, if necessary
+#
+# place_name_exists?(object):: Check if the location name exists in the db, and
+# if so, set the location_id
+#
+# try_to_save_location_if_new(object):: Save the location only if it is new
+#
+# save_location(object):: Save the location only (at this point rest of form is
+# okay)
+#
+################################################################################
+
+module Locationable
+ extend ActiveSupport::Concern
+
+ included do
+ # By now we should have an object (pass the ivar!), and maybe a "-1"
+ # location_id, indicating a new Location if accompanied by bounding box
+ # lat/lng. If the location name does not exist already, and the bounding box
+ # is present, create a new @location, and associate it with the @object.
+ def create_location_object_if_new(object)
+ # Resets the location_id to MO's existing Location if it already exists.
+ return false if place_name_exists?(object)
+
+ # Ensure we have the minimum necessary to create a new location
+ unless object.location_id == -1 &&
+ (place_name = params.dig(object.type_tag, :place_name)).present? &&
+ (north = params.dig(:location, :north)).present? &&
+ (south = params.dig(:location, :south)).present? &&
+ (east = params.dig(:location, :east)).present? &&
+ (west = params.dig(:location, :west)).present?
+ return false
+ end
+
+ # Ignore hidden attribute even if the obs is hidden, because saving a
+ # Location with `hidden: true` fuzzes the lat/lng bounds unpredictably.
+ attributes = { hidden: false, user_id: @user.id,
+ north:, south:, east:, west: }
+ # Add optional attributes. :notes not implemented yet.
+ [:high, :low, :notes].each do |key|
+ if (val = params.dig(:location, key)).present?
+ attributes[key] = val
+ end
+ end
+
+ @location = Location.new(attributes)
+ # With a Location instance, we can use the `display_name=` setter method,
+ # which figures out scientific/postal format of user input and sets
+ # location `name` and `scientific_name` accordingly.
+ @location.display_name = place_name
+ end
+
+ # Check if we somehow got a location name that exists in the db, but didn't
+ # get a location_id, or the location name is out of sync with the
+ # location_id. (This should not usually happen with the autocompleter). If
+ # it happens, match the obs to the existing Location by name. If the user
+ # was trying to create a new Location with the existing name, use the
+ # existing location and flash that we did that, returning `true` so we can
+ # bail on creating a "new" location, but go ahead with the observation save.
+ def place_name_exists?(object)
+ name = Location.user_format(@user, object.place_name)
+ location = Location.find_by(name: name)
+ if !object.location_id&.positive? && location ||
+ (location && (object.location_id != location&.id))
+ if object.location_id == -1
+ flash_warning(:runtime_location_already_exists.t(name: name))
+ end
+ object.location_id = location.id
+ return true
+ end
+
+ false
+ end
+
+ def try_to_save_location_if_new(object)
+ return if @any_errors || !@location&.new_record? || save_location(object)
+
+ @any_errors = true
+ end
+
+ # Save location only (at this point rest of form is okay).
+ def save_location(object)
+ if save_with_log(@location)
+ # Associate the location with the observation
+ object.location_id = @location.id
+ # flash_notice(:runtime_location_success.t(id: @location.id))
+ true
+ else
+ # Failed to create location
+ flash_object_errors(@location)
+ false
+ end
+ end
+ end
+end
diff --git a/app/controllers/herbaria_controller.rb b/app/controllers/herbaria_controller.rb
index 912470f780..b45fee660b 100644
--- a/app/controllers/herbaria_controller.rb
+++ b/app/controllers/herbaria_controller.rb
@@ -45,7 +45,10 @@
# See https://tinyurl.com/ynapvpt7
# View and modify Herbaria (displayed as "Fungaria")
+# rubocop:disable Metrics/ClassLength
class HerbariaController < ApplicationController
+ include ::Locationable
+
before_action :login_required
# only: [:create, :destroy, :edit, :new, :update]
before_action :store_location, only: [:create, :edit, :new, :show, :update]
@@ -99,24 +102,69 @@ def show
def new
@herbarium = Herbarium.new
+ respond_to do |format|
+ format.turbo_stream { render_modal_herbarium_form }
+ format.html
+ end
end
def edit
@herbarium = find_or_goto_index(Herbarium, params[:id])
- return unless @herbarium
- return unless make_sure_can_edit!
+ return unless @herbarium && make_sure_can_edit!
+
+ set_up_herbarium_for_edit
+ respond_to do |format|
+ format.turbo_stream { render_modal_herbarium_form }
+ format.html
+ end
+ end
+ def set_up_herbarium_for_edit
@herbarium.place_name = @herbarium.location.try(&:name)
@herbarium.personal = @herbarium.personal_user_id.present?
@herbarium.personal_user_name = @herbarium.personal_user.try(&:login)
end
+ def render_modal_herbarium_form
+ render(partial: "shared/modal_form",
+ locals: { title: modal_title, action: modal_form_action,
+ identifier: modal_identifier, local: false,
+ form: "herbaria/form" }) and return
+ end
+
+ def modal_identifier
+ case action_name
+ when "new", "create"
+ "herbarium"
+ when "edit", "update"
+ "herbarium_#{@herbarium.id}"
+ end
+ end
+
+ def modal_title
+ case action_name
+ when "new", "create"
+ :create_herbarium_title.l
+ when "edit", "update"
+ :edit_herbarium_title.l
+ end
+ end
+
+ def modal_form_action
+ case action_name
+ when "new", "create" then :create
+ when "edit", "update" then :update
+ end
+ end
+
# ---------- Actions to Modify data: (create, update, destroy, etc.) ---------
def create
@herbarium = Herbarium.new(herbarium_params)
normalize_parameters
- return render(:new) unless validate_herbarium!
+ create_location_object_if_new(@herbarium)
+ try_to_save_location_if_new(@herbarium)
+ return render(:new) unless validate_herbarium! && !@any_errors
@herbarium.save
@herbarium.add_curator(@user) if @herbarium.personal_user
@@ -130,7 +178,9 @@ def update
@herbarium.attributes = herbarium_params
normalize_parameters
- return unless validate_herbarium!
+ create_location_object_if_new(@herbarium)
+ try_to_save_location_if_new(@herbarium)
+ return unless validate_herbarium! && !@any_errors
@herbarium.save
redirect_to_create_location_or_referrer_or_show_location
@@ -265,6 +315,10 @@ def validate_admin_personal_user!
flash_notice(
:edit_herbarium_successfully_made_personal.t(user: user.login)
)
+ update_personal_herbarium(user)
+ end
+
+ def update_personal_herbarium(user)
@herbarium.curators.clear
@herbarium.add_curator(user)
@herbarium.personal_user_id = user.id
@@ -326,7 +380,7 @@ def user_can_destroy_herbarium?
def redirect_to_create_location_or_referrer_or_show_location
redirect_to_create_location || redirect_to_referrer ||
- redirect_with_query(herbarium_path(@herbarium))
+ show_modal_flash_or_show_herbarium
end
def redirect_to_create_location
@@ -339,11 +393,39 @@ def redirect_to_create_location
true
end
+ # this updates both the form and the flash
+ def reload_herbarium_modal_form_and_flash
+ render(
+ partial: "shared/modal_form_reload",
+ locals: { identifier: modal_identifier, form: "herbaria/form" }
+ ) and return true
+ end
+
+ # What to do if the save succeeds
+ def show_modal_flash_or_show_herbarium
+ respond_to do |format|
+ format.html do
+ redirect_with_query(herbarium_path(@herbarium)) and return
+ end
+ format.turbo_stream do
+ # Context here is the obs form.
+ flash_notice(
+ :runtime_created_name.t(type: :herbarium, value: @herbarium.name)
+ )
+ flash_notice(
+ :runtime_added_to.t(type: :herbarium, name: :observation)
+ )
+ render(partial: "herbaria/update_observation") and return
+ end
+ end
+ end
+
def herbarium_params
return {} unless params[:herbarium]
params.require(:herbarium).
- permit(:name, :code, :email, :mailing_address, :description,
+ permit(:name, :code, :email, :mailing_address, :description, :location_id,
:place_name, :personal, :personal_user_name)
end
end
+# rubocop:enable Metrics/ClassLength
diff --git a/app/controllers/herbarium_records_controller.rb b/app/controllers/herbarium_records_controller.rb
index 071778a4e8..b837a705bf 100644
--- a/app/controllers/herbarium_records_controller.rb
+++ b/app/controllers/herbarium_records_controller.rb
@@ -453,8 +453,7 @@ def render_herbarium_records_section_update
def reload_herbarium_record_modal_form_and_flash
render(
partial: "shared/modal_form_reload",
- locals: { identifier: "herbarium_record",
- form: "herbarium_records/form" }
+ locals: { identifier: modal_identifier, form: "herbarium_records/form" }
) and return true
end
end
diff --git a/app/controllers/observations/images_controller.rb b/app/controllers/observations/images_controller.rb
index 25fd19debc..9afbb3ca6b 100644
--- a/app/controllers/observations/images_controller.rb
+++ b/app/controllers/observations/images_controller.rb
@@ -10,38 +10,48 @@ class ImagesController < ApplicationController
before_action :login_required
before_action :pass_query_params
- # NEW: Upload Images to an Observation.
- # was ImageController#add_image
- def new
- return unless (@observation = find_observation!)
+ ###########################################################################
- return unless check_observation_permission!
+ # Form for editing date/license/notes on an observation image.
+ # Linked from: show_image/original
+ # Inputs: params[:id] (image)
+ # params[:comment][:summary]
+ # params[:comment][:comment]
+ # Outputs: @image, @licenses
+ def edit
+ return unless (@image = find_image!)
- @image = rough_cut_image
@licenses = current_license_names_and_ids
- init_project_vars_for_add_or_edit(@observation)
+ check_image_permission!
+ init_project_vars_for_add_or_edit(@image)
end
- # new (upload) image commits here
- def create
- return unless (@observation = find_observation!)
+ def update
+ return unless (@image = find_image!)
- return unless check_observation_permission!
+ @licenses = current_license_names_and_ids
+ check_image_permission!
- create_image
+ @image.attributes = permitted_image_params
+
+ if image_or_projects_updated
+ # redirect_with_query(image_path(@image.id))
+ render("images/show",
+ location: image_path(@image.id, q: get_query_param))
+ else
+ init_project_vars_for_reload(@image)
+ render(:edit, location: edit_image_path(@image.id))
+ end
end
private
- def find_observation!
- find_or_goto_index(Observation, params[:id].to_s)
+ def find_image!
+ find_or_goto_index(Image, params[:id].to_s)
end
- def check_observation_permission!
- return true if check_permission!(@observation)
-
- redirect_with_query(permanent_observation_path(id: @observation.id))
- false
+ def current_license_names_and_ids
+ License.available_names_and_ids(@image.license)
end
def init_project_vars_for_add_or_edit(obs_or_img)
@@ -54,73 +64,45 @@ def init_project_vars_for_add_or_edit(obs_or_img)
end
end
- def current_license_names_and_ids
- License.available_names_and_ids(@image.license)
+ def check_image_permission!
+ return if check_permission!(@image)
+
+ redirect_with_query(image_path(@image))
end
- def rough_cut_image
- @image = Image.new
- @image.license = @user.license
- @image.copyright_holder = @user.legal_name
- @image.user = @user
- # Set the default date to the date of the observation
- # Don't know how to correctly test this.
- @image.when = @observation.when
- @image
+ def permitted_image_params
+ params.require(:image).permit(permitted_image_args)
end
- def create_image
- if params[:upload].blank?
- flash_warning(:runtime_no_changes.t)
+ def image_or_projects_updated
+ if !image_data_changed?
+ update_projects_and_flash_notice!
+ true
+ elsif !@image.save
+ flash_object_errors(@image)
+ false
else
- # There are five upload spots on the form.
- # For as many as have an image, process the image.
- args = params[:image]
- i = 1
- while i < 5 || params[:upload]["image#{i}"].present?
- process_image(args, params[:upload]["image#{i}"])
- i += 1
- end
+ @image.log_update
+ flash_notice(:runtime_image_edit_success.t(id: @image.id))
+ update_related_projects(@image, params[:project])
+ true
end
- redirect_with_query(permanent_observation_path(id: @observation.id))
end
- def process_image(args, upload)
- return if upload.blank?
-
- @image = Image.new(args.permit(permitted_image_args))
- @image.created_at = Time.current
- @image.updated_at = @image.created_at
- @image.user = @user
- @image.image = upload
- @image.save
-
- # The 1st save (or !save) puts the image's original filename in the db,
- # whether or not the user wants it. So if we don't want it,
- # we must empty it and save a 2nd time.
- @image.original_name = "" if @user.keep_filenames == "toss"
- return flash_object_errors(@image) unless @image.save
-
- return revert_image_name_and_flash_errors unless
- @image.process_image(strip: @observation.gps_hidden)
-
- add_image_to_observation!
- end
-
- def add_image_to_observation!
- @observation.add_image(@image)
- @image.log_create_for(@observation)
- name = @image.original_name
- name = "##{@image.id}" if name.empty?
- flash_notice(:runtime_image_uploaded_image.t(name: name))
- update_related_projects(@image, params[:project])
+ def image_data_changed?
+ @image.when_changed? ||
+ @image.notes_changed? ||
+ @image.copyright_holder_changed? ||
+ @image.original_name_changed? ||
+ @image.license_id_changed?
end
- def revert_image_name_and_flash_errors
- name = @image.original_name
- name = "???" if name.empty?
- flash_error(:runtime_image_invalid_image.t(name: name))
- flash_object_errors(@image)
+ def update_projects_and_flash_notice!
+ if update_related_projects(@image, params[:project])
+ flash_notice(:runtime_image_edit_success.t(id: @image.id))
+ else
+ flash_notice(:runtime_no_changes.t)
+ end
end
def update_related_projects(img, checks)
@@ -173,89 +155,6 @@ def attach_images_to_projects_and_flash_notices(img, projects, checks)
any_changes
end
- public
-
- ###########################################################################
-
- # Form for editing date/license/notes on an observation image.
- # Linked from: show_image/original
- # Inputs: params[:id] (image)
- # params[:comment][:summary]
- # params[:comment][:comment]
- # Outputs: @image, @licenses
- def edit
- return unless (@image = find_image!)
-
- @licenses = current_license_names_and_ids
- check_image_permission!
- init_project_vars_for_add_or_edit(@image)
- end
-
- def update
- return unless (@image = find_image!)
-
- @licenses = current_license_names_and_ids
- check_image_permission!
-
- @image.attributes = permitted_image_params
-
- if image_or_projects_updated
- # redirect_with_query(image_path(@image.id))
- render("images/show",
- location: image_path(@image.id, q: get_query_param))
- else
- init_project_vars_for_reload(@image)
- render(:edit, location: edit_image_path(@image.id))
- end
- end
-
- private
-
- def find_image!
- find_or_goto_index(Image, params[:id].to_s)
- end
-
- def check_image_permission!
- return if check_permission!(@image)
-
- redirect_with_query(image_path(@image))
- end
-
- def permitted_image_params
- params.require(:image).permit(permitted_image_args)
- end
-
- def image_or_projects_updated
- if !image_data_changed?
- update_projects_and_flash_notice!
- true
- elsif !@image.save
- flash_object_errors(@image)
- false
- else
- @image.log_update
- flash_notice(:runtime_image_edit_success.t(id: @image.id))
- update_related_projects(@image, params[:project])
- true
- end
- end
-
- def image_data_changed?
- @image.when_changed? ||
- @image.notes_changed? ||
- @image.copyright_holder_changed? ||
- @image.original_name_changed? ||
- @image.license_id_changed?
- end
-
- def update_projects_and_flash_notice!
- if update_related_projects(@image, params[:project])
- flash_notice(:runtime_image_edit_success.t(id: @image.id))
- else
- flash_notice(:runtime_no_changes.t)
- end
- end
-
def init_project_vars_for_reload(obs_or_img)
# (Note: In practice, this is never called for add_image,
# so obs_or_img is always an image.)
@@ -302,6 +201,17 @@ def attach
private
+ def find_observation!
+ find_or_goto_index(Observation, params[:id].to_s)
+ end
+
+ def check_observation_permission!
+ return true if check_permission!(@observation)
+
+ redirect_with_query(permanent_observation_path(id: @observation.id))
+ false
+ end
+
# Attach an image to observation.
def attach_image_to_observation(image)
@observation.add_image(image)
@@ -315,59 +225,5 @@ def attach_image_to_observation(image)
# location: permanent_observation_path(id: @observation.id,
# q: get_query_param))
end
-
- public
-
- ############################################################################
-
- # REMOVE IMAGES: Uses shared partial "shared/images_to_remove"
- # Form used to remove one or more images from an observation (not destroy!)
- # Linked from: observations/show
- # Inputs:
- # params[:obj_id] (observation)
- # params[:selected][image_id] (value of "yes" means remove)
- # Outputs: @observation
- # Redirects to observations/show.
- # remove_images
- def remove
- @object = find_or_goto_index(Observation, params[:id].to_s)
- return unless @object
-
- return if check_permission!(@object)
-
- redirect_with_query(permanent_observation_path(@object.id))
- end
-
- # Callback to DETACH images from an observation, form :put commits here
- def detach
- @object = find_or_goto_index(Observation, params[:id].to_s)
- return unless @object
-
- unless check_permission!(@object)
- return redirect_with_query(permanent_observation_path(@object.id))
- end
-
- images = params[:selected] || [{ params[:image_id] => "yes" }]
- return unless images
-
- remove_images_from_object(images)
- end
-
- ############################################################################
-
- private
-
- def remove_images_from_object(images)
- images.each do |image_id, do_it|
- next unless do_it == "yes"
-
- next unless (image = Image.safe_find(image_id))
-
- @object.remove_image(image)
- image.log_remove_from(@object)
- flash_notice(:runtime_image_remove_success.t(id: image_id))
- end
- redirect_with_query(permanent_observation_path(@object.id))
- end
end
end
diff --git a/app/controllers/observations_controller/create.rb b/app/controllers/observations_controller/create.rb
index a29f5a2601..c9fe1bd5bc 100644
--- a/app/controllers/observations_controller/create.rb
+++ b/app/controllers/observations_controller/create.rb
@@ -3,6 +3,7 @@
module ObservationsController::Create
include ObservationsController::SharedFormMethods
include ObservationsController::Validators
+ include ::Locationable
# Form to create a new observation, naming, vote, and images.
# Linked from: left panel
@@ -37,7 +38,7 @@ def create
init_new_image_var(Time.zone.now)
rough_cut
- create_location_object_if_new # may set @location
+ create_location_object_if_new(@observation) # may set @location
@any_errors = false
validate_name
@@ -47,7 +48,7 @@ def create
validate_naming if @name
validate_vote if @name
validate_images
- try_to_save_location_if_new
+ try_to_save_location_if_new(@observation)
try_to_save_new_observation
return reload_new_form(params.dig(:naming, :reasons)) if @any_errors
diff --git a/app/controllers/observations_controller/edit_and_update.rb b/app/controllers/observations_controller/edit_and_update.rb
index f4a3d14cc1..1d7e753f79 100644
--- a/app/controllers/observations_controller/edit_and_update.rb
+++ b/app/controllers/observations_controller/edit_and_update.rb
@@ -4,6 +4,7 @@
module ObservationsController::EditAndUpdate
include ObservationsController::SharedFormMethods
include ObservationsController::Validators
+ include ::Locationable
# Form to edit an existing observation.
# Linked from: left panel
@@ -83,7 +84,7 @@ def update
@any_errors = false
update_permitted_observation_attributes # may set a new location_id
- create_location_object_if_new
+ create_location_object_if_new(@observation)
@observation.notes = notes_to_sym_and_compact
warn_if_unchecking_specimen_with_records_present!
strip_images! if @observation.gps_hidden
@@ -92,7 +93,7 @@ def update
validate_projects
detach_removed_images
try_to_upload_images
- try_to_save_location_if_new
+ try_to_save_location_if_new(@observation)
try_to_update_observation_if_there_are_changes
reload_edit_form and return if @any_errors
diff --git a/app/controllers/observations_controller/shared_form_methods.rb b/app/controllers/observations_controller/shared_form_methods.rb
index d8b5355100..62896bf8d4 100644
--- a/app/controllers/observations_controller/shared_form_methods.rb
+++ b/app/controllers/observations_controller/shared_form_methods.rb
@@ -123,87 +123,6 @@ def init_list_vars_for_reload
end
end
- ##############################################################################
- # Locations — may be created in the obs form
- #
- # By now we have an @observation, and maybe a "-1" location_id, indicating a
- # new Location if accompanied by bounding box lat/lng. If the location name
- # does not exist already, and the bounding box is present, create a new
- # @location, and associate it with the @observation
- def create_location_object_if_new
- # Resets the location_id to MO's existing Location if it already exists.
- return false if place_name_exists?
-
- # Ensure we have the minimum necessary to create a new location
- unless @observation.location_id == -1 &&
- (place_name = params.dig(:observation, :place_name)).present? &&
- (north = params.dig(:location, :north)).present? &&
- (south = params.dig(:location, :south)).present? &&
- (east = params.dig(:location, :east)).present? &&
- (west = params.dig(:location, :west)).present?
- return false
- end
-
- # Ignore hidden attribute even if the obs is hidden, because saving a
- # Location with `hidden: true` fuzzes the lat/lng bounds unpredictably.
- attributes = { hidden: false, user_id: @user.id,
- north:, south:, east:, west: }
- # Add optional attributes. :notes not implemented yet.
- [:high, :low, :notes].each do |key|
- if (val = params.dig(:location, key)).present?
- attributes[key] = val
- end
- end
-
- @location = Location.new(attributes)
- # With a Location instance, we can use the `display_name=` setter method,
- # which figures out scientific/postal format of user input and sets
- # location `name` and `scientific_name` accordingly.
- @location.display_name = place_name
- end
-
- # Check if we somehow got a location name that exists in the db, but didn't
- # get a location_id, or the location name is out of sync with the location_id.
- # (This should not usually happen with the autocompleter). If it happens,
- # match the obs to the existing Location by name. If the user was trying to
- # create a new Location with the existing name, use the existing location and
- # flash that we did that, returning `true` so we can bail on creating a "new"
- # location, but go ahead with the observation save.
- def place_name_exists?
- name = Location.user_format(@user, @observation.place_name)
- location = Location.find_by(name: name)
- if !@observation.location_id&.positive? && location ||
- (location && (@observation.location_id != location&.id))
- if @observation.location_id == -1
- flash_warning(:runtime_location_already_exists.t(name: name))
- end
- @observation.location_id = location.id
- return true
- end
-
- false
- end
-
- def try_to_save_location_if_new
- return if @any_errors || !@location&.new_record? || save_location
-
- @any_errors = true
- end
-
- # Save location only (at this point rest of form is okay).
- def save_location
- if save_with_log(@location)
- # Associate the location with the observation
- @observation.location_id = @location.id
- # flash_notice(:runtime_location_success.t(id: @location.id))
- true
- else
- # Failed to create location
- flash_object_errors(@location)
- false
- end
- end
-
# Save observation now that everything is created successfully.
def save_observation
return true if @observation.save
diff --git a/app/extensions/string_extensions.rb b/app/extensions/string_extensions.rb
index c16983359f..f78cce1a70 100644
--- a/app/extensions/string_extensions.rb
+++ b/app/extensions/string_extensions.rb
@@ -23,6 +23,8 @@
# gsub_html_special_chars:: auxiliary to html_to_ascii
# unescape_html:: Render special encoded characters as regular characters
# as_displayed:: Render everything humanly legible, for integration tests
+# id_of_nested_field:: Rails generates `observation_notes` for the ID of a
+# nested field like `observation[notes]`
# ---
# break_name:: Break a taxon name at the author
# small_author:: Wrap the author in a span
@@ -546,6 +548,12 @@ def as_displayed
strip_html.unescape_html.strip_squeeze
end
+ # Rails generates an id for a nested field like "foo[bar]" that's snake_case
+ # - no brackets. This gets you that string. (used in forms_helper)
+ def id_of_nested_field
+ gsub(/[\[\]]+/, "_").chop
+ end
+
# Insert a line break between the scientific name and the author
# (for styling taxonomic names legibly)
def break_name
diff --git a/app/helpers/autocompleter_helper.rb b/app/helpers/autocompleter_helper.rb
new file mode 100644
index 0000000000..b726e14c9a
--- /dev/null
+++ b/app/helpers/autocompleter_helper.rb
@@ -0,0 +1,186 @@
+# frozen_string_literal: true
+
+# rubocop:disable Metrics/AbcSize
+module AutocompleterHelper
+ # MO's autocompleter_field is a text_field that fetches suggestions from the
+ # db for the requested model. (For a textarea, pass textarea: true.)
+ #
+ # The stimulus controller handles keyboard and mouse interactions, does the
+ # fetching, and draws the dropdown menu. `args` allow incoming data attributes
+ # to deep_merge with controller data.
+ #
+ # We attempt to disable browser autocomplete via `autocomplete="off"` — the
+ # W3C standard API, but it has never been honored by Chrome or Safari. Chrome
+ # seems to be in a race to defeat the evolving hacks by developers to disable
+ # inappropriate autocompletes, and Safari is not much better - you just can't
+ # turn their crap off. (documented on SO)
+ #
+ def autocompleter_field(**args)
+ ac_args = {
+ placeholder: :start_typing.l, autocomplete: "off",
+ data: { autocompleter_target: "input" }
+ }.deep_merge(args.except(*autocompleter_outer_args))
+ ac_args[:class] = class_names("dropdown", args[:class])
+ # inner form-group wrap, because dropdown is position-absolute
+ ac_args[:wrap_data] = { autocompleter_target: "wrap" }
+ ac_args[:label_after] = autocompleter_label_after(args)
+ ac_args[:label_end] = autocompleter_label_end(args)
+ ac_args[:append] = autocompleter_append(args)
+
+ tag.div(id: args[:controller_id],
+ data: autocompleter_controller_data(args)) do
+ if args[:textarea] == true
+ concat(text_area_with_label(**ac_args))
+ else
+ concat(text_field_with_label(**ac_args))
+ end
+ concat(args[:append])
+ end
+ end
+
+ # Any arg not on this list gets sent to the text field/area.
+ def autocompleter_outer_args
+ [:controller_data, :controller_id, :type, :separator, :textarea,
+ :hidden_value, :hidden_data, :create_text, :keep_text, :edit_text,
+ :find_text, :create, :create_path, :map_outlet, :geocode_outlet].freeze
+ end
+
+ # This data goes on the outer div (controller element), not the input field.
+ def autocompleter_controller_data(args)
+ {
+ controller: :autocompleter,
+ type: args[:type],
+ separator: args[:separator],
+ autocompleter_map_outlet: args[:map_outlet],
+ autocompleter_geocode_outlet: args[:geocode_outlet]
+ }.deep_merge(args[:outer_data] || {})
+ end
+
+ def autocompleter_label_after(args)
+ capture do
+ [
+ autocompleter_has_id_indicator,
+ autocompleter_find_button(args),
+ autocompleter_keep_box_button(args),
+ autocompleter_edit_box_button(args)
+ ].safe_join
+ end
+ end
+
+ def autocompleter_label_end(args)
+ capture do
+ concat(autocompleter_create_button(args))
+ concat(autocompleter_modal_create_link(args))
+ end
+ end
+
+ def autocompleter_has_id_indicator
+ link_icon(:check, title: :autocompleter_has_id.l,
+ class: "ml-3 px-2 text-success has-id-indicator",
+ data: { autocompleter_target: "hasIdIndicator" })
+ end
+
+ def autocompleter_create_button(args)
+ return if !args[:create_text] || args[:create].present?
+
+ icon_link_to(
+ args[:create_text], "#",
+ id: "create_#{args[:type]}_btn", class: "ml-3 create-button",
+ icon: :plus, show_text: true, icon_class: "text-primary",
+ name: "create_#{args[:type]}",
+ data: { autocompleter_target: "createBtn",
+ action: "autocompleter#swapCreate:prevent" }
+ )
+ end
+
+ def autocompleter_modal_create_link(args)
+ return unless args[:create_text] && args[:create].present? &&
+ args[:create_path].present?
+
+ modal_link_to(
+ args[:create], args[:create_text], args[:create_path],
+ icon: :plus, show_text: true, icon_class: "text-primary",
+ name: "create_#{args[:type]}", class: "ml-3 create-link",
+ data: { autocompleter_target: "createBtn" }
+ )
+ end
+
+ def autocompleter_find_button(args)
+ return unless args[:find_text]
+
+ icon_link_to(
+ args[:find_text], "#",
+ icon: :find_on_map, show_text: false, icon_class: "text-primary",
+ name: "find_#{args[:type]}", class: "ml-3 find-btn d-none",
+ data: { map_target: "showBoxBtn",
+ action: "map#showBox:prevent" }
+ )
+ end
+
+ def autocompleter_keep_box_button(args)
+ return unless args[:keep_text]
+
+ icon_link_to(
+ args[:keep_text], "#",
+ icon: :apply, show_text: false, icon_class: "text-primary",
+ name: "keep_#{args[:type]}", class: "ml-3 keep-btn d-none",
+ data: { autocompleter_target: "keepBtn", map_target: "lockBoxBtn",
+ action: "map#toggleBoxLock:prevent form-exif#showFields" }
+ )
+ end
+
+ def autocompleter_edit_box_button(args)
+ return unless args[:keep_text]
+
+ icon_link_to(
+ args[:edit_text], "#",
+ icon: :edit, show_text: false, icon_class: "text-primary",
+ name: "edit_#{args[:type]}", class: "ml-3 edit-btn d-none",
+ data: { autocompleter_target: "editBtn", map_target: "editBoxBtn",
+ action: "map#toggleBoxLock:prevent form-exif#showFields" }
+ )
+ end
+
+ # minimum args :form, :type.
+ # 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?
+
+ model = autocompleter_type_to_model(args[:type])
+ data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {})
+ args[:form].hidden_field(:"#{model}_id", value: args[:hidden_value], data:)
+ end
+
+ 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
+ end
+
+ def autocompleter_dropdown
+ tag.div(class: "auto_complete dropdown-menu",
+ data: { autocompleter_target: "pulldown",
+ action: "scroll->autocompleter#scrollList:passive" }) do
+ tag.ul(class: "virtual_list", data: { autocompleter_target: "list" }) do
+ 10.times do |i|
+ concat(tag.li(class: "dropdown-item") do
+ link_to("", "#", data: {
+ row: i, action: "click->autocompleter#selectRow:prevent"
+ })
+ end)
+ end
+ end
+ end
+ end
+end
+# rubocop:enable Metrics/AbcSize
diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb
index 40049b853d..2f89fc7254 100644
--- a/app/helpers/content_helper.rb
+++ b/app/helpers/content_helper.rb
@@ -76,212 +76,4 @@ def content_tag_unless(condition, name, content_or_options_with_block = nil,
content_tag_if(!condition, name, content_or_options_with_block,
options, escape, &block)
end
-
- # Wrap an html object in '' tag. This has the effect of
- # giving it context help (mouse-over popup) in most modern browsers.
- #
- # <%= help_tooltip(label, title: "Click here to do something.") %>
- #
- def help_tooltip(label, **args)
- args[:data] ||= {}
- tag.span(label, title: args[:title],
- class: class_names("context-help", args[:class]),
- data: { toggle: "tooltip" }.merge(args[:data]))
- 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")
- end
-
- # make a help-block styled element, like a div, p
- def help_block(element = :div, string = "", **args, &block)
- content = block ? capture(&block) : string
- html_options = {
- class: class_names("help-block", args[:class])
- }.deep_merge(args.except(:class))
-
- content_tag(element, html_options) { content }
- end
-
- # draw a help block with an arrow
- def help_block_with_arrow(direction = nil, **args, &block)
- div_class = "well well-sm help-block position-relative"
- div_class += " mt-3" if direction == "up"
-
- tag.div(class: div_class, id: args[:id]) do
- concat(capture(&block).to_s)
- if direction
- arrow_class = "arrow-#{direction}"
- arrow_class += " hidden-xs" unless args[:mobile]
- concat(tag.div("", class: arrow_class))
- end
- end
- end
-
- def collapse_help_block(direction = nil, string = nil, **args, &block)
- div_class = "well well-sm help-block position-relative"
- div_class += " mt-3" if direction == "up"
- content = block ? capture(&block) : string
-
- tag.div(class: "collapse", id: args[:id]) do
- tag.div(class: div_class) do
- concat(content)
- if direction
- arrow_class = "arrow-#{direction}"
- arrow_class += " hidden-xs" unless args[:mobile]
- concat(tag.div("", class: arrow_class))
- end
- end
- end
- end
-
- def collapse_info_trigger(id, **args)
- link_to(link_icon(:question), "##{id}",
- class: class_names("info-collapse-trigger", args[:class]),
- role: "button", data: { toggle: "collapse" },
- aria: { expanded: "false", controls: id })
- end
-
- def panel_block(**args, &block)
- heading = panel_block_heading(args)
- footer = panel_block_footer(args)
- content = capture(&block).to_s
-
- tag.div(
- class: class_names("panel panel-default", args[:class]),
- **args.except(:class, :inner_class, :inner_id, :heading, :heading_links)
- ) do
- concat(heading)
- if content.present?
- concat(tag.div(class: class_names("panel-body", args[:inner_class]),
- id: args[:inner_id]) do
- concat(content)
- end)
- end
- concat(footer)
- end
- end
-
- def panel_block_heading(args)
- if args[:heading]
- tag.div(class: "panel-heading") do
- tag.h4(class: "panel-title") do
- els = [args[:heading]]
- if args[:heading_links].present?
- els << tag.span(args[:heading_links], class: "float-right")
- end
- els.safe_join
- end
- end
- else
- ""
- end
- end
-
- def panel_block_footer(args)
- if args[:footer]
- tag.div(class: "panel-footer") do
- args[:footer]
- end
- else
- ""
- end
- end
-
- def alert_block(level = :warning, string = "")
- content_tag(:div, string, class: "alert alert-#{level}")
- end
-
- # Create a div for notes.
- #
- # <%= notes_panel(html) %>
- #
- # <% notes_panel() do %>
- # Render stuff in here. Note lack of "=" in line above.
- # <% end %>
- #
- def notes_panel(msg = nil, &block)
- msg = capture(&block) if block
- result = tag.div(msg, class: "panel-body")
- wrapper = tag.div(result, class: "panel panel-default dotted-border")
- if block
- concat(wrapper)
- else
- wrapper
- end
- end
-
- # Bootstrap tablist
- def tab_nav(**args, &block)
- if args[:tabs]
- content = capture do
- args[:tabs].each do |tab|
- concat(tab_item(tab[:name], id: tab[:id], active: tab[:active]))
- end
- end
- elsif block
- content = capture(&block).to_s
- else
- content = ""
- end
- style = args[:style] || "pills"
-
- tag.ul(
- role: "tablist",
- class: class_names("nav nav-#{style}", args[:class]),
- **args.except(:class, :style)
- ) do
- content
- end
- end
-
- # Bootstrap "tab" item in ul/li tablist
- def tab_item(name, **args)
- active = args[:active] ? "active" : nil
- disabled = args[:disabled] ? "disabled" : nil
-
- tag.li(
- role: "presentation",
- class: class_names(active, disabled, args[:class])
- ) do
- tab_link(name, **args.except(:active, :disabled, :class))
- end
- end
-
- # Bootstrap tab - just the link. Use for independent tab (e.g. button).
- def tab_link(name, **args)
- classes = args[:button] ? "btn btn-default" : "nav-link"
-
- link_to(
- name, "##{args[:id]}-tab-pane",
- role: "tab", id: "#{args[:id]}-tab", class: classes,
- data: { toggle: "tab" }, aria: { controls: "#{args[:id]}-tab-pane" }
- )
- end
-
- # Bootstrap tabpanel wrapper
- def tab_content(**args, &block)
- content = capture(&block).to_s
-
- tag.div(class: class_names("tab-content", args[:class]),
- **args.except(:class)) do
- content
- end
- end
-
- # Bootstrap tabpanel
- def tab_panel(**args, &block)
- content = capture(&block).to_s
- active = args[:active] ? "in active" : nil
-
- tag.div(
- role: "tabpanel", id: "#{args[:id]}-tab-pane",
- class: class_names("tab-pane fade", active, args[:class]),
- aria: { labelledby: "#{args[:id]}-tab" },
- **args.except(:class, :id)
- ) do
- content
- end
- end
end
diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb
index 3cbb0f35f2..4cc398a318 100644
--- a/app/helpers/forms_helper.rb
+++ b/app/helpers/forms_helper.rb
@@ -5,8 +5,8 @@
# date_select_opts
# helpers for form tags
-# rubocop:disable Metrics/ModuleLength
-module FormsHelper
+
+module FormsHelper # rubocop:disable Metrics/ModuleLength
# Bootstrap submit button
# <%= submit_button(form: f, button: button.t, center: true) %>
def submit_button(**args)
@@ -92,14 +92,14 @@ def check_box_with_label(**args)
wrap_class = form_group_wrap_class(args, "checkbox")
tag.div(class: wrap_class) do
- args[:form].label(args[:field]) do
+ concat(args[:form].label(args[:field]) do
concat(args[:form].check_box(args[:field], opts))
concat(args[:label])
if args[:between].present?
concat(tag.div(class: "d-inline-block ml-3") { args[:between] })
end
- concat(args[:append]) if args[:append].present?
- end
+ end)
+ concat(args[:append]) if args[:append].present?
end
end
@@ -126,14 +126,14 @@ def radio_with_label(**args)
wrap_class = form_group_wrap_class(args, "radio")
tag.div(class: wrap_class) do
- args[:form].label("#{args[:field]}_#{args[:value]}") do
+ concat(args[:form].label("#{args[:field]}_#{args[:value]}") do
concat(args[:form].radio_button(args[:field], args[:value], opts))
concat(args[:label])
if args[:between].present?
concat(tag.div(class: "d-inline-block ml-3") { args[:between] })
end
- concat(args[:append]) if args[:append].present?
- end
+ end)
+ concat(args[:append]) if args[:append].present?
end
end
@@ -165,17 +165,7 @@ def text_field_with_label(**args)
label_opts[:class] = class_names(label_opts[:class], args[:label_class])
tag.div(class: wrap_class, data: wrap_data) do
- # The label row is complicated, many potential buttons here. `between`
- # comes right after the label on left, `between_end` is right justified
- concat(tag.div(class: "d-flex justify-content-between") do
- concat(tag.div do
- concat(args[:form].label(args[:field], args[:label], label_opts))
- concat(args[:between]) if args[:between].present?
- end)
- concat(tag.div do
- concat(args[:between_end]) if args[:between_end].present?
- end)
- end)
+ concat(text_label_row(args, label_opts))
if args[:addon].present? # text addon, not interactive
concat(tag.div(class: "input-group") do
concat(args[:form].text_field(args[:field], opts))
@@ -196,123 +186,6 @@ def text_field_with_label(**args)
end
end
- # MO's autocompleter_field is a text_field that fetches suggestions from the
- # db for the requested model. (For a textarea, pass textarea: true.) The
- # stimulus controller handles keyboard and mouse interactions, does the
- # fetching, and draws the dropdown menu. `args` allow incoming data attributes
- # to deep_merge with controller data. We attempt to disable browser
- # autocomplete via `autocomplete="off"` — the W3C standard API, but it
- # has never been honored by Chrome or Safari. Chrome seems to be in a race to
- # defeat the evolving hacks by developers to disable inappropriate
- # autocompletes, and Safari is not much better - you just can't turn their
- # crap off. (documented on SO)
- #
- def autocompleter_field(**args)
- ac_args = {
- placeholder: :start_typing.l, autocomplete: "off",
- data: { autocompleter_target: "input" }
- }.deep_merge(args.except(:type, :separator, :textarea,
- :hidden, :hidden_data, :create_text,
- :keep_text, :edit_text))
- ac_args[:class] = class_names("dropdown", args[:class])
- ac_args[:wrap_data] = { controller: :autocompleter, type: args[:type],
- separator: args[:separator],
- autocompleter_map_outlet: args[:map_outlet],
- autocompleter_target: "wrap" }
- ac_args[:between] = capture do
- concat(args[:between])
- concat(autocompleter_has_id_indicator)
- concat(autocompleter_find_button(args)) if args[:find_text]
- concat(autocompleter_keep_button(args)) if args[:keep_text]
- concat(autocompleter_hidden_field(**args)) if args[:form]
- end
- ac_args[:between_end] = capture do
- autocompleter_create_button(args) if args[:create_text]
- end
- ac_args[:append] = capture do
- concat(autocompleter_dropdown)
- concat(args[:append])
- end
-
- if args[:textarea] == true
- text_area_with_label(**ac_args)
- else
- text_field_with_label(**ac_args)
- end
- end
-
- def autocompleter_has_id_indicator
- link_icon(:check, title: :autocompleter_has_id.l,
- class: "ml-3 px-2 text-success has-id-indicator",
- data: { autocompleter_target: "hasIdIndicator" })
- end
-
- def autocompleter_create_button(args)
- icon_link_to(
- args[:create_text], "#",
- icon: :plus, show_text: true, icon_class: "text-primary",
- name: "create_#{args[:type]}", class: "ml-3 create-button",
- data: { autocompleter_target: "createBtn",
- action: "autocompleter#swapCreate:prevent" }
- )
- end
-
- def autocompleter_find_button(args)
- icon_link_to(
- args[:find_text], "#",
- icon: :find_on_map, show_text: false, icon_class: "text-primary",
- name: "find_#{args[:type]}", class: "ml-3 d-none",
- data: { map_target: "showBoxBtn",
- action: "map#showBox:prevent" }
- )
- end
-
- def autocompleter_keep_button(args)
- icon_link_to(
- args[:keep_text], "#",
- icon: :apply, show_text: false, icon_class: "text-primary",
- active_icon: :edit, active_content: args[:edit_text],
- name: "keep_#{args[:type]}", class: "ml-3 d-none",
- data: { autocompleter_target: "keepBtn", map_target: "lockBoxBtn",
- action: "map#toggleBoxLock:prevent" }
- )
- end
-
- # minimum args :form, :type.
- # Send :hidden to fill the id, :hidden_data to merge with hidden field data
- def autocompleter_hidden_field(**args)
- model = autocompleter_type_to_model(args[:type])
- data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {})
- args[:form].hidden_field(:"#{model}_id", value: args[:hidden], data:)
- end
-
- def autocompleter_type_to_model(type)
- case type
- when :region
- :location
- when :clade
- :name
- else
- type
- end
- end
-
- def autocompleter_dropdown
- tag.div(class: "auto_complete dropdown-menu",
- data: { autocompleter_target: "pulldown",
- action: "scroll->autocompleter#scrollList:passive" }) do
- tag.ul(class: "virtual_list", data: { autocompleter_target: "list" }) do
- 10.times do |i|
- concat(tag.li(class: "dropdown-item") do
- link_to("", "#", data: {
- row: i, action: "click->autocompleter#selectRow:prevent"
- })
- end)
- end
- end
- end
- end
-
# Bootstrap text_area
def text_area_with_label(**args)
args = auto_label_if_form_is_account_prefs(args)
@@ -327,13 +200,28 @@ def text_area_with_label(**args)
label_opts = field_label_opts(args)
tag.div(class: wrap_class, data: wrap_data) do
- concat(args[:form].label(args[:field], args[:label], label_opts))
- concat(args[:between]) if args[:between].present?
+ concat(text_label_row(args, label_opts))
concat(args[:form].text_area(args[:field], opts))
concat(args[:append]) if args[:append].present?
end
end
+ # The label row for autocompleters is potentially complicated, many buttons.
+ # 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
+ concat(tag.div do
+ concat(args[:form].label(args[:field], args[:label], label_opts))
+ concat(args[:between]) if args[:between].present?
+ concat(args[:label_after]) if args[:label_after].present?
+ end)
+ concat(tag.div do
+ concat(args[:label_end]) if args[:label_end].present?
+ end)
+ end
+ end
+
# Bootstrap select.
# Works for select_year but not date_select, which generates multiple selects
def select_with_label(**args)
@@ -508,6 +396,7 @@ def url_field_with_label(**args)
end
# Bootstrap file input field with client-side size validation.
+ # This could be redone as an input group with a "browse" button, in BS4.
def file_field_with_label(**args)
args = check_for_help_block(args)
opts = separate_field_options_from_args(args)
@@ -538,23 +427,6 @@ def file_field_with_label(**args)
end
end
- # To be retired in favor of the above:
- # Create stylable file input field with client-side size validation.
- def custom_file_field(obj, attr, opts = {})
- max_size = MO.image_upload_max_size
- max_size_in_mb = (max_size.to_f / 1024 / 1024).round
- file_field = file_field(
- obj,
- attr,
- opts.merge(
- max_upload_msg: :validate_image_file_too_big.l(max: max_size_in_mb),
- max_upload_size: max_size
- )
- )
- tag.span(:select_file.t + file_field, class: "file-field btn btn-default") +
- tag.span(:no_file_selected.t)
- end
-
# Unused
# def search_field_with_submit(args)
# opts = separate_field_options_from_args(args)
@@ -619,7 +491,7 @@ 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.t})") if args[pos] == key
+ args[pos] = help_note(:span, "(#{key.l})") if args[pos] == key
end
end
args
@@ -631,10 +503,14 @@ def check_for_help_block(args)
return args
end
- id = "#{args[:form].object_name}_#{args[:field]}_help"
+ id = [
+ args[:form].object_name.to_s.id_of_nested_field,
+ args[:field].to_s,
+ "help"
+ ].join("_")
args[:between] = capture do
- concat(collapse_info_trigger(id))
concat(args[:between])
+ concat(collapse_info_trigger(id))
end
args[:append] = capture do
concat(args[:append])
@@ -650,12 +526,11 @@ def check_for_help_block(args)
# be excluded separately (not here)
def separate_field_options_from_args(args, extras = [])
exceptions = [
- :form, :field, :label, :class, :width, :inline, :between, :between_end,
- :append, :help, :addon, :optional, :required, :monospace, :type,
- :wrap_data, :button, :button_data
+ :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
] + extras
args.clone.except(*exceptions)
end
end
-# rubocop:enable Metrics/ModuleLength
diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb
index 8048896771..c7a4b18c2a 100644
--- a/app/helpers/link_helper.rb
+++ b/app/helpers/link_helper.rb
@@ -81,16 +81,17 @@ def modal_link_to(identifier, name, path, args)
# Icon link with optional active state. (Tooltip title must be swapped in JS.)
# Now also accepts active state options: active_icon, active_content
# NOTE: Takes same args as link_to, e.g. *edit_description_tab(desc, type)
- # icon_link_to(text, path, **args)
+ # icon_link_to(text, path, **args). Can also print a button_to.
def icon_link_to(text = nil, path = nil, options = {}, &block)
return unless text
- link = block ? text : path # because positional
+ link_path = block ? text : path # because positional
content = block ? capture(&block) : text
opts = block ? path : options # because positional
icon_type = opts[:icon]
return link_to(link, opts) { content } if icon_type.blank?
+ # method = opts[:button] ? :button_to : :link_to
active_icon = opts[:active_icon]
active_content = options[:active_content]
stateful = active_icon && active_content
@@ -106,14 +107,19 @@ def icon_link_to(text = nil, path = nil, options = {}, &block)
data: { toggle: "tooltip", title: content, # needed for swapping only
active_title: opts[:active_content] }
}.deep_merge(opts.except(:class, :icon, :icon_class, :show_text,
- :active_icon, :active_content))
+ :active_icon, :active_content, :button_to))
- link_to(link, **link_opts) do
+ html = capture do
concat(link_icon(icon_type, class: icon_class))
concat(link_icon(active_icon, class: icon_active_class)) if stateful
concat(tag.span(content, class: label_class))
concat(tag.span(active_content, class: label_active_class)) if stateful
end
+ if opts[:button_to]
+ button_to(html, link_path, **link_opts)
+ else
+ link_to(html, link_path, **link_opts)
+ end
end
# NOTE: above re: MO tabs
@@ -137,11 +143,11 @@ def link_icon(type, **args)
if args[:title].present?
title = args[:title]
- args[:data] = { toggle: "tooltip", title: }.merge(args[:data] || {})
+ args[:data] = { toggle: "tooltip" }.merge(args[:data] || {})
text = tag.span(title, class: "sr-only")
end
- tag.span(text, **args.except(:title))
+ tag.span(text, **args)
end
# NOTE: Specific to glyphicons
@@ -156,6 +162,12 @@ def link_icon(type, **args)
x: "remove",
remove: "remove-circle",
send: "send",
+ log_in: "log-in",
+ log_out: "log-out",
+ admin: "text-background",
+ inbox: "inbox",
+ interests: "bullhorn",
+ settings: "cog",
ban: "ban-circle",
plus: "plus-sign",
minus: "minus-sign",
@@ -181,7 +193,11 @@ def link_icon(type, **args)
print: "print",
globe: "globe",
find_on_map: "screenshot",
- apply: "check"
+ apply: "check",
+ chevron_down: "chevron-down",
+ chevron_up: "chevron-up",
+ chevron_left: "chevron-left",
+ chevron_right: "chevron-right"
}.freeze
# button to destroy object
diff --git a/app/helpers/map_helper.rb b/app/helpers/map_helper.rb
index f96a851590..b2814975b9 100644
--- a/app/helpers/map_helper.rb
+++ b/app/helpers/map_helper.rb
@@ -17,6 +17,7 @@ def make_map(objects: [], **args)
controller: "map",
map_target: "mapDiv",
map_type: "info",
+ map_open: true,
editable: false,
controls: [:large_map, :map_type].to_json,
location_format: User.current_location_format # method has a default
diff --git a/app/helpers/observations_helper.rb b/app/helpers/observations_helper.rb
index 72b64af5af..b0600a6c49 100644
--- a/app/helpers/observations_helper.rb
+++ b/app/helpers/observations_helper.rb
@@ -151,11 +151,7 @@ def observation_map_coordinates(obs:)
def observation_show_image_links(obs:)
return "" unless check_permission(obs)
- [
- icon_link_with_query(*new_image_for_observation_tab(obs)),
- icon_link_with_query(*reuse_images_for_observation_tab(obs)),
- icon_link_with_query(*remove_images_from_observation_tab(obs))
- ].safe_join(" | ")
+ icon_link_with_query(*reuse_images_for_observation_tab(obs))
end
# The following sections of the observation_details partial are also needed as
diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb
new file mode 100644
index 0000000000..c6ca7e4cc4
--- /dev/null
+++ b/app/helpers/panel_helper.rb
@@ -0,0 +1,282 @@
+# frozen_string_literal: true
+
+# helpers for bootstrap panels
+module PanelHelper
+ # For a collapsing panel_block, pass an HTML id for args[:collapse] and
+ # args[:collapse_show] to show it open by default.
+ # Pass args[:panel_bodies] to create multiple panel bodies, or args[:content]
+ # to pass preformatted content instead of building `panel-body` here.
+ def panel_block(**args, &block)
+ heading = panel_heading(args)
+ footer = panel_footer(args)
+ content = block ? capture(&block).to_s : ""
+
+ tag.div(
+ class: class_names("panel panel-default", args[:class]),
+ **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(footer)
+ 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
+ end
+
+ def panel_heading(args)
+ return "" unless args[:heading]
+
+ tag.div(class: "panel-heading") do
+ if args[:collapse].present?
+ panel_heading_collapse_elements(args)
+ else
+ panel_heading_elements(args)
+ end
+ end
+ end
+
+ def panel_heading_elements(args)
+ els = [args[:heading]]
+ if args[:heading_links].present?
+ els << tag.span(args[:heading_links], class: "float-right")
+ end
+
+ tag.h4(class: "panel-title") { els.safe_join }
+ end
+
+ # The whole heading is a link that toggles the panel collapse.
+ # NOTE: args[:open] should be a boolean
+ def panel_heading_collapse_elements(args)
+ collapsed = args[:open] ? "" : "collapsed"
+
+ tag.h4(class: "panel-title") do
+ link_to(
+ "##{args[:collapse]}",
+ class: class_names("panel-collapse-trigger", collapsed),
+ role: "button", data: { toggle: "collapse" },
+ aria: { expanded: args[:open], controls: args[:collapse] }
+ ) do
+ [args[:heading],
+ tag.span(panel_collapse_icons, 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
+ end
+
+ # Some panels need multiple panel bodies.
+ def panel_bodies(args)
+ args[:panel_bodies].map do |body|
+ panel_body(args, body)
+ end.safe_join
+ end
+
+ def panel_collapse_body(args, content)
+ open = args[:open] ? "in" : ""
+
+ tag.div(class: class_names("panel-collapse collapse", open),
+ id: args[:collapse]) do
+ concat(panel_body(args, content))
+ end
+ end
+
+ def panel_body(args, content)
+ return "" if content.blank?
+ return content if args[:formatted_content]
+
+ tag.div(class: class_names("panel-body", args[:inner_class]),
+ id: args[:inner_id]) do
+ concat(content)
+ end
+ end
+
+ def panel_footer(args)
+ if args[:footer]
+ tag.div(class: "panel-footer") do
+ args[:footer]
+ end
+ else
+ ""
+ end
+ end
+
+ def alert_block(level = :warning, string = "")
+ content_tag(:div, string, class: "alert alert-#{level}")
+ end
+
+ # Create a div for notes.
+ #
+ # <%= notes_panel(html) %>
+ #
+ # <% notes_panel() do %>
+ # Render stuff in here. Note lack of "=" in line above.
+ # <% end %>
+ #
+ def notes_panel(msg = nil, &block)
+ msg = capture(&block) if block
+ result = tag.div(msg, class: "panel-body")
+ wrapper = tag.div(result, class: "panel panel-default dotted-border")
+ if block
+ concat(wrapper)
+ else
+ wrapper
+ end
+ end
+
+ # Help tooltip, note, block.
+ #
+ # Help tooltip is a span with a title attribute. This has the effect of
+ # giving it context help (mouse-over popup) in most modern browsers.
+ #
+ # <%= help_tooltip(label, title: "Click here to do something.") %>
+ #
+ def help_tooltip(label, **args)
+ args[:data] ||= {}
+ tag.span(label, title: args[:title],
+ class: class_names("context-help", args[:class]),
+ data: { toggle: "tooltip" }.merge(args[:data]))
+ 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")
+ end
+
+ # make a help-block styled element, like a div, p
+ def help_block(element = :div, string = "", **args, &block)
+ content = block ? capture(&block) : string
+ html_options = {
+ class: class_names("help-block", args[:class])
+ }.deep_merge(args.except(:class))
+
+ content_tag(element, html_options) { content }
+ end
+
+ # draw a help block with an arrow
+ def help_block_with_arrow(direction = nil, **args, &block)
+ div_class = "well well-sm mb-3 help-block position-relative"
+ div_class += " mt-3" if direction == "up"
+
+ tag.div(class: div_class, id: args[:id]) do
+ concat(capture(&block).to_s)
+ if direction
+ arrow_class = "arrow-#{direction}"
+ arrow_class += " hidden-xs" unless args[:mobile]
+ concat(tag.div("", class: arrow_class))
+ end
+ end
+ end
+
+ def collapse_help_block(direction = nil, string = nil, **args, &block)
+ div_class = "well well-sm mb-3 help-block position-relative"
+ div_class += " mt-3" if direction == "up"
+ content = block ? capture(&block) : string
+
+ tag.div(class: "collapse", id: args[:id]) do
+ tag.div(class: div_class) do
+ concat(content)
+ if direction
+ arrow_class = "arrow-#{direction}"
+ arrow_class += " hidden-xs" unless args[:mobile]
+ concat(tag.div("", class: arrow_class))
+ end
+ end
+ end
+ end
+
+ def collapse_info_trigger(id, **args)
+ link_to(link_icon(:question), "##{id}",
+ class: class_names("info-collapse-trigger", args[:class]),
+ role: "button", data: { toggle: "collapse" },
+ aria: { expanded: "false", controls: id })
+ end
+
+ # BOOTSTRAP TABBED CONTENT
+ #
+ # Bootstrap tablist
+ def tab_nav(**args, &block)
+ if args[:tabs]
+ content = capture do
+ args[:tabs].each do |tab|
+ concat(tab_item(tab[:name], id: tab[:id], active: tab[:active]))
+ end
+ end
+ elsif block
+ content = capture(&block).to_s
+ else
+ content = ""
+ end
+ style = args[:style] || "pills"
+
+ tag.ul(
+ role: "tablist",
+ class: class_names("nav nav-#{style}", args[:class]),
+ **args.except(:class, :style)
+ ) do
+ content
+ end
+ end
+
+ # Bootstrap "tab" item in ul/li tablist
+ def tab_item(name, **args)
+ active = args[:active] ? "active" : nil
+ disabled = args[:disabled] ? "disabled" : nil
+
+ tag.li(
+ role: "presentation",
+ class: class_names(active, disabled, args[:class])
+ ) do
+ tab_link(name, **args.except(:active, :disabled, :class))
+ end
+ end
+
+ # Bootstrap tab - just the link. Use for independent tab (e.g. button).
+ def tab_link(name, **args)
+ classes = args[:button] ? "btn btn-default" : "nav-link"
+
+ link_to(
+ name, "##{args[:id]}-tab-pane",
+ role: "tab", id: "#{args[:id]}-tab", class: classes,
+ data: { toggle: "tab" }, aria: { controls: "#{args[:id]}-tab-pane" }
+ )
+ end
+
+ # Bootstrap tabpanel wrapper
+ def tab_content(**args, &block)
+ content = capture(&block).to_s
+
+ tag.div(class: class_names("tab-content", args[:class]),
+ **args.except(:class)) do
+ content
+ end
+ end
+
+ # Bootstrap tabpanel
+ def tab_panel(**args, &block)
+ content = capture(&block).to_s
+ active = args[:active] ? "in active" : nil
+
+ tag.div(
+ role: "tabpanel", id: "#{args[:id]}-tab-pane",
+ class: class_names("tab-pane fade", active, args[:class]),
+ aria: { labelledby: "#{args[:id]}-tab" },
+ **args.except(:class, :id)
+ ) do
+ content
+ end
+ end
+end
diff --git a/app/helpers/tabs/herbaria_helper.rb b/app/helpers/tabs/herbaria_helper.rb
index 29c4a812d6..2adbb0f957 100644
--- a/app/helpers/tabs/herbaria_helper.rb
+++ b/app/helpers/tabs/herbaria_helper.rb
@@ -71,12 +71,12 @@ def herbaria_curator_request_tabs(herbarium:)
end
def new_herbarium_tab
- [:create_herbarium.t, add_query_param(new_herbarium_path),
+ [:create_herbarium.l, add_query_param(new_herbarium_path),
{ class: tab_id(__method__.to_s) }]
end
def edit_herbarium_tab(herbarium)
- [:edit_herbarium.t,
+ [:edit_herbarium.l,
add_query_param(edit_herbarium_path(herbarium.id)),
{ class: tab_id(__method__.to_s) }]
end
diff --git a/app/helpers/tabs/observations_helper.rb b/app/helpers/tabs/observations_helper.rb
index 7ee7909111..ec7410f2dc 100644
--- a/app/helpers/tabs/observations_helper.rb
+++ b/app/helpers/tabs/observations_helper.rb
@@ -105,24 +105,12 @@ def observation_hide_thumbnail_map_tab(obs)
{ class: tab_id(__method__.to_s), icon: :hide }]
end
- def new_image_for_observation_tab(obs)
- [:show_observation_add_images.l,
- new_image_for_observation_path(obs.id),
- { class: tab_id(__method__.to_s), icon: :add }]
- end
-
def reuse_images_for_observation_tab(obs)
[:show_observation_reuse_image.l,
reuse_images_for_observation_path(obs.id),
{ class: tab_id(__method__.to_s), icon: :reuse }]
end
- def remove_images_from_observation_tab(obs)
- [:show_observation_remove_images.l,
- remove_images_from_observation_path(obs.id),
- { class: tab_id(__method__.to_s), icon: :remove }]
- end
-
############################################
# INDEX
@@ -211,7 +199,8 @@ def observations_download_as_csv_tab(query)
# FORMS
def observation_form_new_tabs
- [new_inat_import_tab, new_herbarium_tab]
+ # [new_inat_import_tab, new_herbarium_tab]
+ []
end
def observation_form_edit_tabs(obs:)
@@ -251,17 +240,6 @@ def observation_images_edit_tabs(image:)
[object_return_tab(image)]
end
- def observation_images_new_tabs(obs:)
- [object_return_tab(obs),
- edit_observation_tab(obs)]
- end
-
- # Note this takes `obj:` not `obs:`
- def observation_images_remove_tabs(obj:)
- [object_return_tab(obj),
- edit_observation_tab(obj)]
- end
-
def observation_images_reuse_tabs(obs:)
[object_return_tab(obs),
edit_observation_tab(obs)]
@@ -285,10 +263,7 @@ def obs_change_tabs(obs)
end
def obs_details_links(obs)
- return print_labels_button(obs) unless check_permission(obs)
-
- [print_labels_button(obs),
- obs_change_links(obs)].safe_join(" | ")
+ print_labels_button(obs)
end
# Buttons in "Details" panel header
diff --git a/app/helpers/tabs/projects_helper.rb b/app/helpers/tabs/projects_helper.rb
index cb8b8b30f9..693731a987 100644
--- a/app/helpers/tabs/projects_helper.rb
+++ b/app/helpers/tabs/projects_helper.rb
@@ -84,9 +84,8 @@ def add_project_banner(project)
def add_background_image(image)
return unless image
- presenter = ImagePresenter.new(image, size: :large)
content_for(:background_image) do
- image_tag(presenter.img_src, class: "image-title")
+ image_tag(image.large_url, class: "image-title")
end
end
end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 98776d8d9f..e8e553e34a 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -13,11 +13,28 @@ import "@hotwired/turbo-rails"
// form, or button like delete/patch: set data-turbo="true" to opt in
// link_to with GET: set data-turbo-stream="true" to opt in
Turbo.setFormMode("optin")
-// https://stackoverflow.com/questions/77421369/turbo-response-to-render-javascript-alert/77434363#77434363
+// https://stackoverflow.com/a/77434363/3357635
// use: <%= turbo_stream.close_modal("modal_#{obs.id}_naming") %>
Turbo.StreamActions.close_modal = function () {
$("#" + this.templateContent.textContent).modal('hide')
};
+// https://stackoverflow.com/a/76744968/3357635
+Turbo.StreamActions.update_input = function () {
+ this.targetElements.forEach((target) => {
+ target.value = this.templateContent.textContent
+ });
+};
+// https://stackoverflow.com/a/77836101/3357635
+Turbo.StreamActions.add_class = function () {
+ this.targetElements.forEach((target) => {
+ target.classList.add(this.templateContent.textContent)
+ });
+}
+Turbo.StreamActions.remove_class = function () {
+ this.targetElements.forEach((target) => {
+ target.classList.remove(this.templateContent.textContent)
+ });
+}
import "@rails/request.js"
diff --git a/app/javascript/controllers/advanced-search_controller.js b/app/javascript/controllers/advanced-search_controller.js
index 74354491ff..88b73f7bbd 100644
--- a/app/javascript/controllers/advanced-search_controller.js
+++ b/app/javascript/controllers/advanced-search_controller.js
@@ -5,7 +5,7 @@ export default class extends Controller {
static targets = ["searchModel", "filter"]
connect() {
- this.element.dataset.stimulus = "connected";
+ this.element.dataset.stimulus = "advanced-search-connected";
this.disableUnusedFilters();
}
diff --git a/app/javascript/controllers/autocompleter_controller.js b/app/javascript/controllers/autocompleter_controller.js
index fdf36438d6..f81d551679 100644
--- a/app/javascript/controllers/autocompleter_controller.js
+++ b/app/javascript/controllers/autocompleter_controller.js
@@ -149,7 +149,7 @@ export default class extends Controller {
// The select target is not the element, but a
<%= form_tag({action: :test_upload_image, log_id: @log_entry.id}, {multipart: true}) do %>
- <% label_tag(:upload_image1, :IMAGE.t + " 1:") %> <%= custom_file_field(:upload, :image1) %>
- <% label_tag(:upload_image2, :IMAGE.t + " 2:") %> <%= custom_file_field(:upload, :image2) %>
- <% label_tag(:upload_image3, :IMAGE.t + " 3:") %> <%= custom_file_field(:upload, :image3) %>
- <% label_tag(:upload_image4, :IMAGE.t + " 4:") %> <%= custom_file_field(:upload, :image4) %>
+ <%= fields_for(:upload) do |f_u| %>
+ <% [1,2,3,4].each do |i| %>
+ <%= file_field_with_label(
+ form: f_u, field: "image#{i}".to_sym, class: "mt-3",
+ label: :image_add_image.t + " #{i}:",
+ ) %>
+ <% end %>
+ <% end %>
<%= submit_tag(:UPLOAD.l, class: "btn btn-default center-block mt-3") %>
diff --git a/app/views/controllers/layouts/application.html.erb b/app/views/controllers/layouts/application.html.erb
index 5032598c1a..cbb6ecee01 100644
--- a/app/views/controllers/layouts/application.html.erb
+++ b/app/views/controllers/layouts/application.html.erb
@@ -14,7 +14,7 @@ body_class = class_names(ctrlr_action, theme, location_format, logged_in)
<%= render(partial: "application/app/head") %>
-
+
<% if Rails.env == "production" %>
<%= render(partial: "application/app/gtm_iframe") %>
<% end %>
diff --git a/app/views/controllers/locations/_form.erb b/app/views/controllers/locations/_form.erb
index 78d32f7a29..15ae513ab1 100644
--- a/app/views/controllers/locations/_form.erb
+++ b/app/views/controllers/locations/_form.erb
@@ -22,7 +22,8 @@ when "edit", "update"
end
form_args = {
- model: @location, url: url_params, id: "location_form"
+ model: @location, url: url_params, id: "location_form",
+ data: { controller: "map", map_open: true }
}
if local_assigns[:local] == true
@@ -43,7 +44,7 @@ map_args = { editable: true, map_type: "location" }
<%# NOTE: All other Stimulus data is on the map div, but we need
the fields inside the controller scope, so map has controller: nil %>
- <%= tag.div(class: "row", data: { controller: "map" }) do %>
+ <%= tag.div(class: "row") do %>
<%= tag.div(class: "col-md-8 col-lg-6") do %>
<%= render(partial: "locations/form/fields",
locals: { f:, button:, location: @location,
diff --git a/app/views/controllers/locations/form/_bounds_hidden_fields.erb b/app/views/controllers/locations/form/_bounds_hidden_fields.erb
index 79a26adf3e..3b59f8e953 100644
--- a/app/views/controllers/locations/form/_bounds_hidden_fields.erb
+++ b/app/views/controllers/locations/form/_bounds_hidden_fields.erb
@@ -1,10 +1,10 @@
-<%# locals: (location: nil, target_type: :geocode_target) -%>
+<%# locals: (location: nil, target_controller: :geocode) -%>
<%= fields_for(:location) do |f_l| %>
<% %w[north south east west low high].each do |key| %>
<%= f_l.hidden_field(
:"#{key}", value: location&.send(key) || nil,
- data: { "#{target_type}_target".to_sym => "#{key}Input" }
+ data: { "#{target_controller}_target".to_sym => "#{key}Input" }
) %>
<% end %>
<% end %>
diff --git a/app/views/controllers/names/synonyms/deprecate/new.html.erb b/app/views/controllers/names/synonyms/deprecate/new.html.erb
index 86c7ea9f9e..ff1bb067f3 100644
--- a/app/views/controllers/names/synonyms/deprecate/new.html.erb
+++ b/app/views/controllers/names/synonyms/deprecate/new.html.erb
@@ -9,10 +9,10 @@ action = { controller: "/names/synonyms/deprecate", action: :create,
feedback_locals = {
button_name: :SUBMIT.l,
given_name: @given_name,
+ names: @names,
valid_names: @valid_names,
suggest_corrections: @suggest_corrections,
- parent_deprecated: @parent_deprecated,
- names: @names
+ parent_deprecated: @parent_deprecated
}
deprecate_comments_help = capture do
@@ -26,7 +26,7 @@ end
<%= submit_button(form: f, button: :SUBMIT.l, center: true) %>
<%= render(partial: "shared/form_name_feedback",
- locals: feedback_locals.merge({ f: f })) if @given_name.present? %>
+ locals: feedback_locals) if @given_name.present? %>
<%= autocompleter_field(
form: f, field: :proposed_name, type: :name,
diff --git a/app/views/controllers/observations/_form.html.erb b/app/views/controllers/observations/_form.html.erb
index 2ee258a907..8566ac3bfd 100644
--- a/app/views/controllers/observations/_form.html.erb
+++ b/app/views/controllers/observations/_form.html.erb
@@ -1,11 +1,22 @@
-<%# locals: (action:) -%>
<%
+# Declaring permitted locals in a magic comment doesn't work here (?)
create = (action == :create)
method = create ? :post : :patch
button_name = create ? :CREATE.l : :SAVE_EDITS.l
-include_naming = create ? true : false
has_specimen = create ? false : @observation.herbarium_records.length > 0
logging_optional = create ? false : true
+naming_locals = {
+ create:,
+ button_name:,
+ show_reasons: false,
+ unfocused: true,
+ name_help: :form_naming_name_help_leave_blank.t
+}
+notes_fields = @observation.form_notes_parts(@user)
+notes_open = (create && notes_fields.length > 1) || @observation.notes.present?
+show_projects = @projects.any? || @error_checked_projects.any? ||
+ @suspect_checked_projects.any?
+show_lists = @lists.any?
# Data for the form-images Stimulus controller.
# Controller element is the form, so image dropzone can cover the whole form.
@@ -19,19 +30,19 @@ image_upload_localization = {
show_on_map: :show_on_map.t,
something_went_wrong: :form_observations_upload_error.t
}.to_json
-data = {
+# Outlets are how the stimulus controllers call each others' methods.
+form_element_data = {
controller: "form-images form-exif map",
- action: [
- "map:reenableBtns@window->form-exif#reenableButtons",
- "form-exif:pointChanged@window->map#calculateMarker",
- "autocompleter:hiddenIdDataChanged@window->map#showBox",
- ].join(" "),
+ map_autocompleter_outlet: "#observation_location_autocompleter",
+ map_open: false,
+ form_exif_autocompleter_outlet: "#observation_location_autocompleter",
+ form_exif_map_outlet: "#observation_form",
+ action: "map:reenableBtns@window->form-exif#reenableButtons",
upload_max_size: max_size,
localization: image_upload_localization,
form_images_target: "form",
exif_used: create ? false : true
}
-projects_lists = @projects.any? || @lists.any?
%>
<%= form_with(
@@ -42,26 +53,69 @@ projects_lists = @projects.any? || @lists.any?
method:,
multipart: true,
id: "observation_form",
- data:
+ data: form_element_data
) do |f| %>
- <%= if @field_code
- tag.p("#{:form_observations_field_code.t} #{@field_code}")
+ <% if @field_code %>
+ <%= tag.p("#{:form_observations_field_code.t} #{@field_code}") %>
+ <%= hidden_field_tag(:field_code, @field_code) %>
+ <% end %>
+
+ <%= panel_block(
+ heading: "#{:IMAGES.l} + #{:show_observation_details.l}",
+ id: "observation_images_details", formatted_content: true,
+ collapse: "observation_images_details_inner", open: true
+ ) do
+ concat(render(partial: "observations/form/images", locals: { f: }))
+ concat(render(partial: "observations/form/details",
+ locals: { f:, action:, button_name:, location: @location,
+ logging_optional: }))
+ end %>
+
+ <%= panel_block(
+ heading: "#{:IDENTIFICATION.l} + #{:SPECIMEN.l}",
+ id: "observation_naming_specimen", # @given_name.present?
+ collapse: "observation_naming_specimen_inner", open: create
+ ) do
+ tag.div(class: "row") do
+ concat(tag.div(class: "col-xs-12 col-md-6") do
+ render(partial: "observations/namings/fields", locals: naming_locals)
+ end) if create
+ concat(tag.div(class: "col-xs-12 col-md-6") do
+ render(partial: "observations/form/specimen_section",
+ locals: { f:, action: })
+ end)
+ end
end %>
- <%= hidden_field_tag(:field_code, @field_code) %>
- <%= tab_content do %>
- <%= render(partial: "observations/form/record_step",
- locals: { f:, button_name:, location: @location,
- logging_optional: }) %>
+ <%= submit_button(form: f, button: button_name, center: true) %>
+
+ <%= panel_block(
+ heading: :NOTES.l, id: "observation_notes",
+ collapse: "observation_notes_inner", open: notes_open
+ ) do
+ render(partial: "shared/notes_fields",
+ locals: { form: f, fields: notes_fields })
+ end %>
- <%= render(partial: "observations/form/identify_step",
- locals: { f:, action:, button_name:, include_naming: }) %>
+ <% if show_projects %>
+ <%= panel_block(
+ heading: :PROJECTS.l, id: "observation_projects",
+ collapse: "observation_projects_inner",
+ open: @project_checks.any?
+ ) do
+ render(partial: "observations/form/projects", locals: { button_name: })
+ end %>
+ <% end %>
- <% if projects_lists %>
- <%= render(partial: "observations/form/add_to_step",
- locals: { f:, button_name: }) %>
- <% end %>
+ <% if show_lists %>
+ <%= panel_block(
+ heading: :SPECIES_LISTS.l, id: "observation_lists",
+ collapse: "observation_lists_inner",
+ open: @list_checks.any?
+ ) do
+ render(partial: "observations/form/species_lists")
+ end %>
<% end %>
<% end %>
diff --git a/app/views/controllers/observations/form/_add_to_step.erb b/app/views/controllers/observations/form/_add_to_step.erb
deleted file mode 100644
index 47f2d195d8..0000000000
--- a/app/views/controllers/observations/form/_add_to_step.erb
+++ /dev/null
@@ -1,30 +0,0 @@
-<%# locals: (f:, button_name:) -%>
-<%# Add to Project and List section of create_observation form %>
-
-<%= tab_panel(id: "add_to") do %>
- <%= panel_block(id: "observation_projects_and_lists",
- heading: "#{:PROJECTS.l} + #{:SPECIES_LISTS.l}") do %>
-
- <%= tag.div(class: "row mt-3") do %>
- <% if @projects.any? %>
- <%= tag.div(class: "col-xs-12 col-sm-6") do
- render(partial: "observations/form/projects",
- locals: { button_name: })
- end %>
- <% end %>
- <% if @lists.any? %>
- <%= tag.div(class: "col-xs-12 col-sm-6") do
- render(partial: "observations/form/species_lists")
- end %>
- <% end %>
- <% end %>
-
- <%= tag.div(class: "row mt-5 text-center", id: "step-nav-3") do
- [
- tab_link(:BACK.l, id: "identify", button: true),
- submit_button(form: f, button: button_name)
- ].safe_join(" ")
- end %>
- <% end %>
-<% end %>
-
diff --git a/app/views/controllers/observations/form/_details.html.erb b/app/views/controllers/observations/form/_details.html.erb
index 348ba95272..6587adacf8 100644
--- a/app/views/controllers/observations/form/_details.html.erb
+++ b/app/views/controllers/observations/form/_details.html.erb
@@ -1,18 +1,18 @@
-<%# locals: (f:, button_name:, location:, logging_optional:) -%>
+<%# locals: (f:, action:, button_name:, location:, logging_optional:, include_naming: false) -%>
<%# When and Where (location) section of create_observation form
- including location autocomplete, map, lat/long/alt %>
+ including location autocomplete, map, lat/long/alt, naming %>
<%
t_s = {
- lat: { abbr: :LAT.l, full: :LATITUDE.l },
- lng: { abbr: :LNG.l, full: :LONGITUDE.l },
- alt: { abbr: :ALT.l, full: :ALTITUDE.l }
+ lat: { abbr: :LAT.l, full: :LATITUDE.l, addon: "º" },
+ lng: { abbr: :LNG.l, full: :LONGITUDE.l, addon: "º" },
+ alt: { abbr: :ALT.l, full: :ALTITUDE.l, addon: "m" }
}
%>
<%= tag.div(class: "panel-body border-top", id: "observation_details") do %>
- <%= tag.div(class: "row mt-3") do %>
+ <%= tag.div(class: "row") do %>
<%= tag.div(class: "col-xs-12 col-md-6") do %>
<%= date_select_with_label(
@@ -21,127 +21,115 @@ t_s = {
<%= tag.div(id: "observation_where") do %>
- <%= tag.p do
- [tag.strong("#{:WHERE_GROUP.l}:"),
- collapse_info_trigger("geolocation_help")].safe_join(" ")
- end %>
-
-
- <%= tag.div(class: "row no-gutters",
- id: "observation_lat_lng_alt") do %>
- <% [:lat, :lng, :alt].each do |key| %>
- <%= tag.div(class: "col-xs-4") do
- text_field_with_label(
- form: f, field: key, class: "mb-0", addon: "º",
- label: [
- tag.span("#{t_s[key][:full]}:", class: "d-none d-sm-inline"),
- tag.span("#{t_s[key][:abbr]}:", class: "d-inline d-sm-none")
- ].safe_join,
- data: { map_target: "#{key}Input", action: "map#bufferInputs" }
- )
- end %>
- <% end %>
- <% end %>
-
- <%= collapse_help_block(nil, id: "geolocation_help") do %>
- <%= tag.p(:form_observations_click_point.l) %>
- <%= tag.p(:form_observations_lat_long_help.t) %>
- <% end %>
-
- <%= check_box_with_label(form: f, field: :gps_hidden,
- label: :form_observations_gps_hidden.l) %>
-
-
<%= render(partial: "shared/form_location_feedback",
locals: { button: button_name } ) %>
-
<%= autocompleter_field(
- form: f, field: :place_name, type: :location,
+ form: f, field: :place_name, type: :location, class: "mb-0",
label: [tag.span("#{:WHERE.l}:", class: "unconstrained-label"),
tag.span("#{:form_observations_locality_contains.l}:",
class: "constrained-label"),
tag.span("#{:form_observations_create_locality.l}:",
class: "create-label")].safe_join(" "),
help: observation_location_help,
- hidden: location&.id,
+ hidden_value: location&.id,
hidden_data: { map_target: "locationId",
north: location&.north, south: location&.south,
east: location&.east, west: location&.west },
- # These are button tooltips:
+ # These text strings are the button tooltips:
create_text: :form_observations_create_locality.l,
keep_text: :form_observations_use_locality.l,
edit_text: :form_observations_edit_locality.l,
# find_text: :form_locations_find_on_map.l,
- map_outlet: ".map-outlet",
+ # Be precise about which map controller to connect to:
+ map_outlet: "#observation_form",
+ controller_id: "observation_location_autocompleter",
+ # Action ok because there's only one form-exif controller on the page,
+ # and it should only affect this autocompleter:
data: {
map_target: "placeInput",
- action: [
- "map:pointChanged@window->autocompleter#swap",
- "form-exif:pointChanged@window->autocompleter#swap",
- "map:googlePrimer@window->autocompleter#refreshGooglePrimer"
- ].join(" ")
+ action: "form-exif:pointChanged@window->autocompleter#swap"
}
) %>
<%= render(partial: "locations/form/bounds_hidden_fields",
- locals: { location: @location, target_type: :map }) %>
+ locals: { location: @location, target_controller: :map }) %>
<%= check_box_with_label(
form: f, field: :is_collection_location,
label: :form_observations_is_collection_location.l,
- help: :form_observations_is_collection_location_help.t
+ help: :form_observations_is_collection_location_help.t,
+ class: "ml-5 mb-5"
) %>
+
+ <%= check_box_with_label(
+ form: f, field: :has_geolocation, # field is ignored
+ label: "#{:GEOLOCATION.l}:",
+ help: :form_observations_lat_long_help.t,
+ data: { toggle: "collapse", target: "#observation_geolocation",
+ form_exif_target: "collapseCheck" },
+ aria: { controls: "observation_geolocation",
+ expanded: @observation.lat }
+ ) %>
+
+ <%= tag.div(id: "observation_geolocation",
+ class: class_names("collapse", ("in" if @observation.lat)),
+ data: { form_exif_target: "collapseFields" }) do %>
+
+ <%= tag.p(:form_observations_click_point.l) %>
+
+ <%= tag.div(class: "row no-gutters",
+ id: "observation_lat_lng_alt") do %>
+ <% [:lat, :lng, :alt].each do |key| %>
+ <%= tag.div(class: "col-xs-4") do
+ text_field_with_label(
+ form: f, field: key, class: "mb-0", addon: t_s[key][:addon],
+ label: [
+ tag.span("#{t_s[key][:full]}:",
+ class: "d-none d-sm-inline"),
+ tag.span("#{t_s[key][:abbr]}:",
+ class: "d-inline d-sm-none")
+ ].safe_join,
+ data: { map_target: "#{key}Input",
+ action: "map#bufferInputs" }
+ )
+ end %>
+ <% end %>
+ <% end %>
+
+ <%= check_box_with_label(
+ form: f, field: :gps_hidden, label: :form_observations_gps_hidden.l,
+ class: "ml-5 mb-5"
+ ) %>
+
+ <%# collapse_help_block(nil, id: "geolocation_help") do %>
+ <%# tag.p(:form_observations_lat_long_help.t) %>
+ <% end %>
+
+
<% if logging_optional %>
<%= check_box_with_label(
form: f, field: :log_change, checked: "checked",
label: :form_observations_log_change.t
) %>
<% end %>
-
<% end %>
<% end %>
- <%= tag.div(class: "col-xs-12 col-md-6",
- id: "observation_geolocation") do %>
- <%= tag.div(
- "", id: "observation_form_map",
- class: "observation-form-map collapse",
- data: { indicator_url: asset_path('indicator.gif'),
- location_format: User.current_location_format,
- map_target: "mapDiv", editable: true,
- map_type: "observation" }
- ) %>
- <%= tag.div(class: "btn-group my-3", role: "group",
- data: { map_target: "controlWrap" }) do %>
- <%= js_button(
- button: [
- link_icon(:globe),
- tag.span(:form_observations_open_map.l, class: "map-show mx-2"),
- tag.span(:form_observations_hide_map.l, class: "map-hide mx-2")
- ].safe_join,
- name: "map_toggle", class: "map-toggle",
- data: { map_target: "toggleMapBtn", action: "map#toggleMap",
- toggle: "collapse", target: "#observation_form_map" },
- aria: { expanded: "false", controls: "observation_form_map" }
- ) %>
- <%= js_button(
- button: :form_observations_clear_map.l,
- name: "map_clear", class: "map-clear",
- data: { map_target: "mapClearBtn",
- action: "map#clearMap form-exif#reenableButtons" }
- ) %>
- <% end %>
+ <%= tag.div(class: "col-xs-12 col-md-6") do %>
+ <%= render(
+ partial: "shared/form_location_map",
+ locals: { id: "observation_form_map", map_type: "observation" }
+ ) %>
<% end %>
-
<% end %>
<% end %>
diff --git a/app/views/controllers/observations/form/_identify_step.erb b/app/views/controllers/observations/form/_identify_step.erb
deleted file mode 100644
index 3ffd41c860..0000000000
--- a/app/views/controllers/observations/form/_identify_step.erb
+++ /dev/null
@@ -1,38 +0,0 @@
-<%# locals: (f:, action:, button_name:, include_naming:) -%>
-
-<%= tab_panel(id: "identify") do %>
- <%= panel_block(id: "observation_identification",
- heading: "#{:IDENTIFICATION.l} + #{:NOTES.l}") do %>
- <%= tag.div(class: "row mt-3") do %>
- <%= tag.div(class: "col-xs-12 col-sm-6") do %>
- <% if include_naming
- naming_locals = {
- f: f,
- action: action,
- button_name: button_name,
- show_reasons: false,
- unfocused: true,
- name_help: :form_naming_name_help_leave_blank.t
- } %>
- <%= render(partial: "observations/namings/fields",
- locals: naming_locals) %>
- <% end %>
- <%= render(partial: "observations/form/specimen_section",
- locals: { f:, action: }) %>
- <% end %>
- <%= tag.div(class: "col-xs-12 col-sm-6") do %>
- <%= render(partial: "shared/notes_fields",
- locals: { form: f,
- fields: @observation.form_notes_parts(@user) }) %>
- <% end %>
- <% end %>
-
- <%= tag.div(class: "row mt-5 text-center", id: "step-nav-2") do
- [
- tab_link(:BACK.l, id: "record", button: true),
- tab_link(:NEXT.l, id: "add_to", button: true),
- submit_button(form: f, button: button_name)
- ].safe_join(" ")
- end %>
- <% end %>
-<% end %>
diff --git a/app/views/controllers/observations/form/_images.html.erb b/app/views/controllers/observations/form/_images.html.erb
index ed2b3db2d1..e7676c021a 100644
--- a/app/views/controllers/observations/form/_images.html.erb
+++ b/app/views/controllers/observations/form/_images.html.erb
@@ -12,13 +12,6 @@ i.e. not observation[good_image_ids]. It's a top-level field. --%>
<%= tag.div(id: "observation_images") do %>
- <%= hidden_field_tag(:good_image_ids,
- @good_images.map { |img| img.id }.join(" "),
- data: { form_images_target: "goodImageIds" }) %>
-
- <%= f.hidden_field(:thumb_image_id,
- data: { form_images_target: "thumbImageId" }) %>
-
<%= tag.div(class: "panel-body border-bottom") do
[
tag.h4(:IMAGES.l, class: "panel-title d-inline-block mr-4"),
@@ -32,7 +25,12 @@ i.e. not observation[good_image_ids]. It's a top-level field. --%>
data: { action: "change->form-images#addSelectedFiles" }
)
].safe_join
- end
+ end,
+ hidden_field_tag(:good_image_ids,
+ @good_images.map { |img| img.id }.join(" "),
+ data: { form_images_target: "goodImageIds" }),
+ f.hidden_field(:thumb_image_id,
+ data: { form_images_target: "thumbImageId" })
].safe_join
end %>
diff --git a/app/views/controllers/observations/form/_projects.html.erb b/app/views/controllers/observations/form/_projects.html.erb
index 63a002ba13..bbc8cbd610 100644
--- a/app/views/controllers/observations/form/_projects.html.erb
+++ b/app/views/controllers/observations/form/_projects.html.erb
@@ -20,7 +20,7 @@ if @suspect_checked_projects.any?
end
%>
-<%= tag.div(id: "observation_projects") do %>
+<%# tag.div(id: "observation_projects") do %>
<%= fields_for(:project) do |f_p| %>
<% if error_messages.present? || suspect_messages.present? %>
@@ -50,15 +50,7 @@ end
) %>
<% end %>
- <%= tag.div do
- [
- tag.strong("#{:PROJECTS.l}:", class: "mr-3"),
- collapse_info_trigger("project_help"),
- collapse_help_block(nil, id: "project_help") do
- :form_observations_project_help.t
- end
- ].safe_join
- end %>
+ <%= tag.p(:form_observations_project_help.t) %>
<%= tag.div(class: "overflow-scroll-checklist") do %>
<% @projects.each do |project| %>
@@ -71,5 +63,5 @@ end
<% end %>
<% end %>
-<% end %>
+<%# end %>
diff --git a/app/views/controllers/observations/form/_record_step.erb b/app/views/controllers/observations/form/_record_step.erb
deleted file mode 100644
index a49bdf2488..0000000000
--- a/app/views/controllers/observations/form/_record_step.erb
+++ /dev/null
@@ -1,26 +0,0 @@
-<%# locals: (f:, button_name:, location:, logging_optional:) -%>
-<%# NOTE: not using panel_block helper, because it has three panel-body divs %>
-
-<%= tab_panel(id: "record", active: true) do %>
- <%= tag.div(
- id: "observation_images_details", class: "panel panel-default"
- ) do %>
-
- <%= tag.div(class: "panel-heading") do
- tag.h4("#{:IMAGES.l} + #{:show_observation_details.l}",
- class: "panel-title")
- end %>
-
- <%= render(partial: "observations/form/images", locals: { f: f }) %>
-
- <%= render(partial: "observations/form/details",
- locals: { f:, button_name:, location:, logging_optional: }) %>
-
- <%= tag.div(class: "panel-body mt-4") do
- tag.div(class: "text-center", id: "step-nav-1") do
- tab_link(:NEXT.l, id: "identify", button: true)
- end
- end %>
-
- <% end %>
-<% end %>
diff --git a/app/views/controllers/observations/form/_species_lists.html.erb b/app/views/controllers/observations/form/_species_lists.html.erb
index 1172d3fc85..95651291bd 100644
--- a/app/views/controllers/observations/form/_species_lists.html.erb
+++ b/app/views/controllers/observations/form/_species_lists.html.erb
@@ -1,17 +1,9 @@
<%# species_list section of create_observation form %>
-<%= tag.div(id: "observation_projects") do %>
+<%# tag.div(id: "observation_lists") do %>
<%= fields_for(:list) do |f_l| %>
- <%= tag.div do
- [
- tag.strong("#{:SPECIES_LISTS.l}:", class: "mr-3"),
- collapse_info_trigger("species_lists_help"),
- collapse_help_block(nil, id: "species_lists_help") do
- :form_observations_list_help.t
- end
- ].safe_join
- end %>
+ <%= tag.p(:form_observations_list_help.t) %>
<%= tag.div(class: "overflow-scroll-checklist") do %>
<% @lists.each do |list| %>
@@ -23,4 +15,4 @@
<% end %>
<% end %>
-<% end %>
+<%# end %>
diff --git a/app/views/controllers/observations/form/_specimen_section.html.erb b/app/views/controllers/observations/form/_specimen_section.html.erb
index 4fae2a6b43..3935a87b98 100644
--- a/app/views/controllers/observations/form/_specimen_section.html.erb
+++ b/app/views/controllers/observations/form/_specimen_section.html.erb
@@ -3,17 +3,15 @@
<%# Specimen section of create_observation form, for collection_number and
herbarium_record. Fields hidden unless box checked. %>
-<%= tag.div(id: "observation_specimen_section",
- data: { controller: "specimen",
- user_pref: (!@user.try(&:hide_specimen_stuff?)) }) do %>
+<%= tag.div(id: "observation_specimen_section") do %>
- <%= tag.div(class: "mt-3") do %>
+ <%= tag.div do %>
<%= check_box_with_label(
- form: f, field: :specimen,
+ form: f, field: :specimen, class: "mt-0",
label: :form_observations_specimen_available.t,
help: :form_observations_specimen_available_help.t,
- data: { specimen_target: "checkbox",
- action: "change->specimen#hideShowFields" }
+ data: { toggle: "collapse", target: "#specimen_fields" },
+ aria: { controls: "specimen_fields", expanded: @observation.specimen }
) %>
<% if action == :update %>
@@ -26,11 +24,12 @@ herbarium_record. Fields hidden unless box checked. %>
<% if action == :create %>
- <%= tag.div(id: "specimen_fields",
- class: ("hidden" if !@observation.specimen),
- data: { specimen_target: "fields" }) do %>
- <%= render(partial: "observations/form/collection_number") %>
- <%= render(partial: "observations/form/herbarium_record") %>
+ <%= tag.div(
+ id: "specimen_fields",
+ class: class_names("collapse", ("in" if @observation.specimen))
+ ) do %>
+ <%= render(partial: "observations/form/specimen/collection_number") %>
+ <%= render(partial: "observations/form/specimen/herbarium_record") %>
<% end %>
<% end %>
diff --git a/app/views/controllers/observations/form/images/_camera_info.erb b/app/views/controllers/observations/form/images/_camera_info.erb
index a2e108c974..1d5e322454 100644
--- a/app/views/controllers/observations/form/images/_camera_info.erb
+++ b/app/views/controllers/observations/form/images/_camera_info.erb
@@ -1,24 +1,27 @@
<%# locals: (img_id: "", lat: "", lng: "", alt: "", date: "", file_name: "", file_size: "") -%>
+<%
+# on new form, lat will not be present until form-exif reads it from the image
+gps = tag.span(class: "exif_gps") do
+ [
+ tag.strong("#{:LAT.l}: ") + tag.span(lat, class: "exif_lat"),
+ tag.strong("#{:LNG.l}: ") + tag.span(lng, class: "exif_lng"),
+ tag.strong("#{:ALT.l}: ") + tag.span(alt, class: "exif_alt") + " m"
+ ].safe_join(", ")
+end
+no_gps = tag.span("#{:image_no_geolocation.l}", class: "exif_no_gps d-none")
+%>
+
<%=
-tag.div(class: "well well-sm position-relative",
- id: "camera_info_#{img_id}") do
+tag.div(class: "well well-sm position-relative", id: "camera_info_#{img_id}") do
[
label_tag("camera_info_#{img_id}", :image_camera_info.l),
tag.div(class: "form-group") do
[
tag.div do
- [
- tag.strong("#{:DATE.l}: "), # :form_images_camera_date.t
- carousel_exif_to_image_date_button(date: date)
- ].safe_join
- end,
- tag.div do
- [
- tag.strong("#{:LAT.l}: ") + tag.span(lat, class: "exif_lat"),
- tag.strong("#{:LNG.l}: ") + tag.span(lng, class: "exif_lng"),
- tag.strong("#{:ALT.l}: ") + tag.span(alt, class: "exif_alt") + " m"
- ].safe_join(", ")
+ [tag.strong("#{:DATE.l}: "), # :form_images_camera_date.t
+ carousel_exif_to_image_date_button(date: date)].safe_join
end,
+ tag.div { [gps, no_gps].safe_join },
carousel_transfer_exif_button(has_exif: date.present? || lat.present?)
].safe_join
end,
diff --git a/app/views/controllers/observations/form/_collection_number.html.erb b/app/views/controllers/observations/form/specimen/_collection_number.html.erb
similarity index 100%
rename from app/views/controllers/observations/form/_collection_number.html.erb
rename to app/views/controllers/observations/form/specimen/_collection_number.html.erb
diff --git a/app/views/controllers/observations/form/_herbarium_record.html.erb b/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb
similarity index 75%
rename from app/views/controllers/observations/form/_herbarium_record.html.erb
rename to app/views/controllers/observations/form/specimen/_herbarium_record.html.erb
index 26bfc0adec..0851b92772 100644
--- a/app/views/controllers/observations/form/_herbarium_record.html.erb
+++ b/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb
@@ -4,9 +4,11 @@
<% fields_for(:herbarium_record) do |fhr| %>
<%= autocompleter_field(
form: fhr, field: :herbarium_name, type: :herbarium,
- value: @herbarium_name, hidden: @herbarium_id,
+ value: @herbarium_name, hidden_value: @herbarium_id,
label: "#{:herbarium_record_herbarium_name.t}:",
- help: :form_observations_herbarium_record_help.t
+ help: :form_observations_herbarium_record_help.t,
+ create_text: :create_herbarium.l, create: "herbarium",
+ create_path: new_herbarium_path
) %>
<%= text_field_with_label(
form: fhr, field: :accession_number, value: @accession_number,
diff --git a/app/views/controllers/observations/images/edit.html.erb b/app/views/controllers/observations/images/edit.html.erb
index 71b739f452..7f040c481f 100644
--- a/app/views/controllers/observations/images/edit.html.erb
+++ b/app/views/controllers/observations/images/edit.html.erb
@@ -13,11 +13,11 @@ form_action = { controller: "/observations/images",
<%= form_with(model: @image, url: form_action, method: :put) do |f| %>
- <%= submit_button(form: f, button: :SAVE_EDITS.l, center: true) %>
-
<%= render(partial: "observations/images/form/fields_for_images",
locals: { f: f, leave_out_original_file_name: false }) %>
+ <%= submit_button(form: f, button: :SAVE_EDITS.l, center: true) %>
+
<% if @projects.any? %>
<%= content_tag(:p, :PROJECTS.t + ":", class: "font-weight-bold") %>
diff --git a/app/views/controllers/observations/images/form/_fields_for_upload.html.erb b/app/views/controllers/observations/images/form/_fields_for_upload.html.erb
deleted file mode 100644
index f7d8e1f688..0000000000
--- a/app/views/controllers/observations/images/form/_fields_for_upload.html.erb
+++ /dev/null
@@ -1,16 +0,0 @@
-
- <%= fields_for(:upload) do |f_u| %>
- <%= f_u.label(:image1, :image_add_image.t + " 1:") %>
- <%= custom_file_field(:upload, :image1) %>
- (<%= :image_add_default.t %>)
- <%= f_u.label(:image2, :image_add_image.t + " 2:") %>
- <%= custom_file_field(:upload, :image2) %>
- (<%= :image_add_optional.t %>)
- <%= f_u.label(:image3, :image_add_image.t + " 3:") %>
- <%= custom_file_field(:upload, :image3) %>
- (<%= :image_add_optional.t %>)
- <%= f_u.label(:image4, :image_add_image.t + " 4:") %>
- <%= custom_file_field(:upload, :image4) %>
- (<%= :image_add_optional.t %>)
- <% end %>
-
diff --git a/app/views/controllers/observations/images/new.html.erb b/app/views/controllers/observations/images/new.html.erb
deleted file mode 100644
index 60c190252f..0000000000
--- a/app/views/controllers/observations/images/new.html.erb
+++ /dev/null
@@ -1,31 +0,0 @@
-<%
-add_page_title(:image_add_title.t(name: @observation.unique_format_name))
-
-add_tab_set(observation_images_new_tabs(obs: @observation))
-%>
-
-<%= form_with(model: @image,
- url: add_query_param(action: :create, id: @observation.id),
- html: { multipart: true }) do |f| %>
-
- <%= fields_for(:observation) do |f_o| %>
- <%= f_o.hidden_field(:id) %>
- <% end %>
-
-
<%= :image_add_warning.tp %>
-
- <%= submit_button(form: f, button: :image_add_upload.l, center: true) %>
-
- <%= render(partial: "observations/images/form/fields_for_upload") %>
-
- <%= render(partial: "observations/images/form/fields_for_images",
- locals: { f: f, leave_out_original_file_name: true }) %>
-
- <% if @projects.any? %>
- <%= render(partial: "observations/images/form/project",
- collection: @projects) %>
- <% end %>
-
- <%= submit_button(form: f, button: :image_add_upload.l, center: true) %>
-
-<% end %>
diff --git a/app/views/controllers/observations/images/remove.html.erb b/app/views/controllers/observations/images/remove.html.erb
deleted file mode 100644
index a1366cf241..0000000000
--- a/app/views/controllers/observations/images/remove.html.erb
+++ /dev/null
@@ -1,19 +0,0 @@
-<%
-# REMOVE IMAGES FROM OBSERVATION
-# NOTE: Move template to observations/images/edit
-# Observations::ImagesController#edit
-# Move partial to shared/remove_images, send target_class as local
-# Note totally general title and tabs based on @object target_class
-add_page_title(:image_remove_title.t(name: @object.unique_format_name))
-
-add_tab_set(observation_images_remove_tabs(obj: @object))
-
-@container = :full
-form_action = { controller: "/observations/images",
- action: :detach,
- id: @object.id,
- q: get_query_param }
-%>
-
-<%= render(partial: "shared/images_to_remove",
- locals: { form_action: form_action }) %>
diff --git a/app/views/controllers/observations/namings/_fields.erb b/app/views/controllers/observations/namings/_fields.erb
index 3c1ac6ee3c..bc1b156e10 100644
--- a/app/views/controllers/observations/namings/_fields.erb
+++ b/app/views/controllers/observations/namings/_fields.erb
@@ -1,18 +1,17 @@
+<%# locals: (create:, button_name:, show_reasons: false, unfocused: false, name_help: :form_naming_name_help.t, context: "blank" ) -%>
+
<%
# This is included by obs form, naming new/edit form + lightbox identifier
-
-unfocused ||= false
-focus_on_name = !unfocused && (button_name != :CREATE.l || @given_name.empty?)
-focus_on_vote = !unfocused && (button_name == :CREATE.l && @given_name.present?)
+focus_on_name = !unfocused && (!create || @given_name.empty?)
+focus_on_vote = !unfocused && (create && @given_name.present?)
feedback_locals = {
- f: f,
button_name: button_name,
given_name: @given_name,
+ names: @names,
valid_names: @valid_names,
suggest_corrections: @suggest_corrections,
- parent_deprecated: @parent_deprecated,
- names: @names
+ parent_deprecated: @parent_deprecated
}
menu = unless @vote&.value&.nonzero?
Vote.opinion_menu
@@ -20,23 +19,19 @@ menu = unless @vote&.value&.nonzero?
Vote.confidence_menu
end
confidences = options_for_select(menu, @vote&.value)
-select_opts = { include_blank: ["new", "create"].include?(action_name) }
-context ||= "blank"
-name_help ||= :form_naming_name_help.t
+select_opts = { include_blank: create }
%>
-<%=
-[
- tag.div do
- render(partial: "shared/form_name_feedback",
- locals: feedback_locals) if @given_name.present?
- end,
- fields_for(:naming) do |f_n|
+<% if @given_name.present? %>
+ <%= tag.div do
+ render(partial: "shared/form_name_feedback", locals: feedback_locals)
+ end %>
+<% end %>
+
+<%= fields_for(:naming) do |f_n| %>
+ <% vote_reasons = tag.div(data: { autocompleter_target: "collapseFields" },
+ class: ("collapse" if context == "blank")) do
[
- autocompleter_field(
- form: f_n, field: :name, type: :name, label: "#{:WHAT.t}:",
- value: @given_name, autofocus: focus_on_name, help: name_help
- ),
f_n.fields_for(:vote) do |f_v|
select_with_label(form: f_v, field: :value,
options: confidences, select_opts: select_opts,
@@ -47,7 +42,12 @@ name_help ||= :form_naming_name_help.t
naming_form_reasons_fields(f_r, @reasons)
end
].safe_join
- end,
- hidden_field_tag(:context, context)
-].safe_join
-%>
+ end %>
+ <%= autocompleter_field(
+ form: f_n, field: :name, type: :name, label: "#{:WHAT.t}:",
+ value: @given_name, autofocus: focus_on_name, help: name_help,
+ append: vote_reasons
+ ) %>
+<% end %>
+
+<%= hidden_field_tag(:context, context) %>
diff --git a/app/views/controllers/observations/namings/_form.erb b/app/views/controllers/observations/namings/_form.erb
index 05a89d8eb0..2e860b9ff7 100644
--- a/app/views/controllers/observations/namings/_form.erb
+++ b/app/views/controllers/observations/namings/_form.erb
@@ -1,9 +1,12 @@
+<%# locals: (local: false, form_locals: {}) -%>
+
<%
# Fields must be separate because they're included in the obs form too.
# HTML ID's may seem overly precise, but there may be more than one of
# these forms open on a page if modal. IDs must be unique.
case action_name
when "new", "create"
+ create = true
button_name = :CREATE.l
method = :post
id = "obs_#{@observation.id}_naming_form"
@@ -11,6 +14,7 @@ when "new", "create"
approved_name: @given_name,
q: get_query_param)
when "edit", "update"
+ create = false
button_name = :SAVE_EDITS.l
method = :patch
id = "obs_#{@observation.id}_naming_#{@naming.id}_form"
@@ -20,23 +24,23 @@ when "edit", "update"
q: get_query_param)
end
+# `local` true means do not send via Turbo.
form_args = { model: @naming, url: url, method: method, id: id }
-if local_assigns[:local] == true
+if local
form_args = form_args.merge({ local: true })
else
form_args = form_args.deep_merge({ data: { turbo: true } })
end
-# Note: the form needs local_assigns[:form_locals].
-# I can't find where show_reasons is ever false - AN 20230801
-form_locals = local_assigns[:form_locals] || {}
+# `naming_locals`: modal forms can accept a `form_locals` local. The controller
+# may send `context` (i.e. where the form appears), which defaults to "blank".
+# `show_reasons` is false on the obs form, true on the naming form.
+naming_locals = { create:, button_name:, show_reasons: true, context: "blank" }.merge(form_locals)
%>
<%= form_with(**form_args) do |f| %>
+ <%= render(partial: "observations/namings/fields", locals: naming_locals) %>
<%= submit_button(form: f, button: button_name, center: true) %>
- <%= render(partial: "observations/namings/fields",
- locals: { f: f, button_name: button_name }.merge(form_locals)) %>
-
<% end # form %>
diff --git a/app/views/controllers/observations/namings/edit.html.erb b/app/views/controllers/observations/namings/edit.html.erb
index 715ab3e8c9..251b1e85bf 100644
--- a/app/views/controllers/observations/namings/edit.html.erb
+++ b/app/views/controllers/observations/namings/edit.html.erb
@@ -13,8 +13,8 @@ add_tab_set(naming_form_edit_tabs(obs: @observation))
<%= render(partial: "observations/namings/form",
- locals: { action: :update, show_reasons: true,
- local: true }) %>
+ locals: { local: true,
+ form_locals: { show_reasons: true } }) %>
diff --git a/app/views/controllers/observations/namings/new.html.erb b/app/views/controllers/observations/namings/new.html.erb
index c4ff406d8d..11016e6299 100644
--- a/app/views/controllers/observations/namings/new.html.erb
+++ b/app/views/controllers/observations/namings/new.html.erb
@@ -13,8 +13,8 @@ add_tab_set(naming_form_new_tabs(obs: @observation))
<%= render(partial: "observations/namings/form",
- locals: { action: :create, show_reasons: true,
- local: true }) %>
+ locals: { local: true,
+ form_locals: { show_reasons: true } }) %>
diff --git a/app/views/controllers/observations/show.html.erb b/app/views/controllers/observations/show.html.erb
index 767c1f46d6..0d56ab8dc6 100644
--- a/app/views/controllers/observations/show.html.erb
+++ b/app/views/controllers/observations/show.html.erb
@@ -4,6 +4,10 @@ add_page_title(show_obs_title(obs: @observation, owner_naming: @owner_naming))
add_owner_naming(@owner_naming)
add_pager_for(@observation)
add_interest_icons(@user, @observation)
+content_for(:edit_icons) do
+ tag.span(obs_change_links(@observation),
+ class: "h4 ml-3 d-inline-block text-nowrap")
+end
# add_tab_set(show_observation_tabs(obs: @observation, user: @user))
@container = :double
diff --git a/app/views/controllers/shared/_form_location_map.erb b/app/views/controllers/shared/_form_location_map.erb
new file mode 100644
index 0000000000..1831402983
--- /dev/null
+++ b/app/views/controllers/shared/_form_location_map.erb
@@ -0,0 +1,36 @@
+<%# locals: (id: "", map_type: "location") -%>
+
+<%#
+The stimulus controller for this map should be
+on an ancestor element that also contains the inputs
+%>
+
+<%= tag.div(
+ "", id:,
+ class: "form-map collapse",
+ data: { indicator_url: asset_path('indicator.gif'),
+ location_format: User.current_location_format,
+ map_target: "mapDiv", editable: true,
+ map_type: }
+) %>
+<%= tag.div(class: "btn-group my-3", role: "group",
+ data: { map_target: "controlWrap" }) do %>
+ <%= js_button(
+ button: [
+ link_icon(:globe),
+ tag.span(:form_observations_open_map.l, class: "map-show mx-2"),
+ tag.span(:form_observations_hide_map.l, class: "map-hide mx-2")
+ ].safe_join,
+ name: "map_toggle", class: "map-toggle",
+ data: { map_target: "toggleMapBtn",
+ action: "map#toggleMap form-exif#showFields",
+ toggle: "collapse", target: "##{id}" },
+ aria: { expanded: "false", controls: id }
+ ) %>
+ <%= js_button(
+ button: :form_observations_clear_map.l,
+ name: "map_clear", class: "map-clear",
+ data: { map_target: "mapClearBtn",
+ action: "map#clearMap form-exif#reenableButtons" }
+ ) %>
+<% end %>
diff --git a/app/views/controllers/shared/_form_name_feedback.erb b/app/views/controllers/shared/_form_name_feedback.erb
index 5ae008500f..be83f5e852 100644
--- a/app/views/controllers/shared/_form_name_feedback.erb
+++ b/app/views/controllers/shared/_form_name_feedback.erb
@@ -1,6 +1,8 @@
+<%# locals: (given_name:, button_name:, names:, valid_names:, suggest_corrections:, parent_deprecated:) -%>
+
<%#
-Handling deprecated, new & multiple Names
-Used by Name and Naming create and edit pages
+Feedback if the user entered deprecated, new or multiple Names
+Used by Observation, Name and Naming create and edit pages
Does two things:
Describes the issue - Deprecated, Parent Deprecated, or Not Recognized
Adds Help how to proceed - depends on issue and the button
@@ -12,18 +14,11 @@ names - Name(s) corresponding to given_name
valid_names - Name(s) that are valid synonyms
suggest_corrections - t/f whether to suggest correction(s)
parent_deprecated - t/f
-
-TODO:
-Differentiate warning (valid_names) from error (!valid_names)
-A conditional to watch is `if @params[:given_name].present?`.
-Also, views/observations/namings/fields is doing too much work,
-the multiple local_assigns for this partial should be set upstream.
%>
<%=
+##### Warnings #####
if valid_names
- ##### Warnings #####
-
tag.div(class: "alert alert-warning", id: "name_messages") do
concat(tag.div do
if suggest_corrections || names.blank?
diff --git a/app/views/controllers/shared/_modal_form_reload.erb b/app/views/controllers/shared/_modal_form_reload.erb
index 63f22cb15c..5f873dc9ae 100644
--- a/app/views/controllers/shared/_modal_form_reload.erb
+++ b/app/views/controllers/shared/_modal_form_reload.erb
@@ -1,7 +1,8 @@
+<%# locals: (identifier:, form:, form_locals: {}) -%>
<%
# What to do if a form submit doesn't succeed
-# Takes locals: { identifier:, form:, form_locals: }
-form_locals ||= {}
+# Takes locals: { identifier:, form:, form_locals: {} }
+# form_locals ||= {}
%>
<%= turbo_stream.update("modal_#{identifier}_flash") do
@@ -9,5 +10,5 @@ form_locals ||= {}
end %>
<%= turbo_stream.replace("#{identifier}_form") do
- render(partial: form, locals: { format: :turbo_stream }.merge(form_locals))
+ render(partial: form, locals: { local: false, form_locals: })
end %>
diff --git a/app/views/controllers/shared/_notes_fields.html.erb b/app/views/controllers/shared/_notes_fields.html.erb
index baeeb2d1df..78755e2197 100644
--- a/app/views/controllers/shared/_notes_fields.html.erb
+++ b/app/views/controllers/shared/_notes_fields.html.erb
@@ -1,6 +1,7 @@
-<%# Notes section of any form %>
+<%# locals: (form:, fields:) -%>
<%
+# Notes section of any form
# Users may have custom notes "parts". This prints a single large textarea and
# textile help block if there's only one notes part. Otherwise, it prints
# smaller textareas and a general textile help link.
@@ -24,7 +25,7 @@ end
indent = form.object_name == "observation" ? "" : "ml-5"
%>
-<%= tag.div(id: "#{form.object_name}_notes") do %>
+<%= tag.div(id: "#{form.object_name}_notes_fields") do %>
<%= general_help %>
<%= tag.div(class: indent) do %>
<%= form.fields_for(:notes) do |f_n| %>
diff --git a/config/initializers/turbo_stream_actions.rb b/config/initializers/turbo_stream_actions.rb
index fa3d929d6a..0d06e8c33e 100644
--- a/config/initializers/turbo_stream_actions.rb
+++ b/config/initializers/turbo_stream_actions.rb
@@ -1,11 +1,23 @@
# frozen_string_literal: true
-# https://stackoverflow.com/questions/77421369/turbo-response-to-render-javascript-alert/77434363#77434363
+# https://stackoverflow.com/a/77434363/3357635
# this is optional but makes it much cleaner
module CustomTurboStreamActions
def close_modal(id)
action(:close_modal, "#", id)
end
+ def update_input(id, value)
+ action(:update_input, id, value)
+ end
+
+ def add_class(id, class_name)
+ action(:add_class, id, class_name)
+ end
+
+ def remove_class(id, class_name)
+ action(:remove_class, id, class_name)
+ end
+
::Turbo::Streams::TagBuilder.include(self)
end
diff --git a/config/locales/en.txt b/config/locales/en.txt
index cc21359d8d..328f8e2e00 100644
--- a/config/locales/en.txt
+++ b/config/locales/en.txt
@@ -710,6 +710,8 @@
close: close
CREATE: Create
create: create
+ DEFINE: Define
+ define: define
DELETE: Delete
delete: delete
# deprecate a name
@@ -783,16 +785,16 @@
destroyed: destroyed
FILTERED: Filtered
filtered: filtered
+ IGNORING: Ignoring
+ ignoring: ignoring
MODIFIED: Modified
modified: modified
+ NEW: New
+ new: new
OPTIONAL: Optional
optional: optional
REQUIRED: Required
required: required
- WATCHING: Watching
- watching: watching
- IGNORING: Ignoring
- ignoring: ignoring
TRACKING: Tracking
tracking: tracking
UPDATED: Updated
@@ -801,6 +803,8 @@
updated_at: updated at
VIEWED: Viewed
viewed: viewed
+ WATCHING: Watching
+ watching: watching
# Common adjectives describing species names. (Note: "approved" seems to be
# interchangeable with "accepted" in our site -- both indicate that the
@@ -1683,10 +1687,10 @@
form_observations_open_map: "[:SHOW] [:map]"
form_observations_hide_map: "[:HIDE] [:map]"
form_observations_clear_map: "[:CLEAR] [:location]"
- form_observations_click_point: "Tip: select a locality to center the map, then click a point on the map to set a location."
+ form_observations_click_point: "Tip: select a locality to center the map, then click a point on the map to set a location. If you're defining a new locality, click \"Use these bounds\" so you can select a point within."
form_observations_locality_contains: "[:LOCALITIES] containing this point"
- form_observations_create_locality: "[:CREATE] [:locality]"
- form_observations_use_locality: "[:USE] this [:locality]"
+ form_observations_create_locality: "[:NEW] [:locality]"
+ form_observations_use_locality: "Use these bounds and select a point within"
form_observations_edit_locality: "[:EDIT] this [:locality]"
form_observations_notes_help: Please include any additional information you can think of about this observation that isn't clear from the photographs, e.g., habitat, substrate or nearby trees; distinctive texture, scent, taste, staining or bruising; results of chemical or microscopic analyses, etc.
form_observations_remove_image_confirm: Are you sure you want to remove this image? This will only remove this image from this observation. If it is attached to other observations, it will remain attached to them.
@@ -1755,6 +1759,7 @@
image_exif_copied: Info copied
image_use_exif: Use this info
image_camera_info: Camera info
+ image_no_geolocation: No geolocation found
image_file_name: File name
image_file_size: File size
@@ -2620,8 +2625,8 @@
show_herbarium_request_sent: Request has been sent to admins. We'll get back to you as soon as possible.
# herbaria/create
- create_herbarium: Create New Fungarium
- create_herbarium_title: Create New Fungarium
+ create_herbarium: New Fungarium
+ create_herbarium_title: New Fungarium
create_herbarium_personal: Check this box if this is your personal fungarium
create_herbarium_personal_help: Each user can have one personal fungarium that they have curator privileges for. By default it is called "[name]", but you can change that name to anything you like.
create_herbarium_code: Standard Abbreviation
@@ -3123,7 +3128,7 @@
# info/textile
sandbox_enter: Enter Textile you wish to test here
- sandbox_header: This page let's you play with the textile markup language. Click 'Test' to see what your markup looks like. You will need to copy the results from this page to where ever you want to use it.
+ sandbox_header: This page lets you play with the textile markup language. Click 'Test' to see what your markup looks like. You will need to copy the results from this page to wherever you want to use it.
sandbox_link_hobix_textile_reference: hobix Textile Reference
sandbox_link_hobix_textile_cheatsheet: hobix cheatsheet (Textile Quick Reference)
sandbox_link_textile_language_website: Textile Language website
diff --git a/config/routes.rb b/config/routes.rb
index 31f312c3e3..8db6dca36b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -602,18 +602,10 @@ def route_actions_hash
as: "new_question_for")
post("emails", to: "observations/emails#create",
as: "send_question_for")
- get("images/new", to: "observations/images#new",
- as: "new_image_for")
- post("images", to: "observations/images#create",
- as: "create_image_for")
get("images/reuse", to: "observations/images#reuse",
as: "reuse_images_for")
post("images/attach", to: "observations/images#attach",
as: "attach_image_to")
- get("images/remove", to: "observations/images#remove",
- as: "remove_images_from")
- put("images(/:image_id)/detach", to: "observations/images#detach",
- as: "detach_images_from")
end
collection do
diff --git a/test/controllers/admin/blocked_ips_controller_test.rb b/test/controllers/admin/blocked_ips_controller_test.rb
index 85bae39ccf..3e91205c9c 100644
--- a/test/controllers/admin/blocked_ips_controller_test.rb
+++ b/test/controllers/admin/blocked_ips_controller_test.rb
@@ -43,6 +43,15 @@ def test_blocked_ips
assert(time < File.mtime(MO.blocked_ips_file))
IpStats.reset!
assert_false(IpStats.blocked?(new_ip))
+
+ time = 1.minute.ago
+ File.utime(time.to_time, time.to_time, MO.blocked_ips_file)
+ patch(:update, params: { add_bad: " #{new_ip} " })
+ assert_no_flash
+ assert(time < File.mtime(MO.blocked_ips_file))
+ IpStats.reset!
+ assert_true(IpStats.blocked?(new_ip),
+ "It should ignore leading & trailing spaces in ip addr")
end
end
end
diff --git a/test/controllers/observations/images_controller_test.rb b/test/controllers/observations/images_controller_test.rb
index ac489adc64..9231413b8f 100644
--- a/test/controllers/observations/images_controller_test.rb
+++ b/test/controllers/observations/images_controller_test.rb
@@ -5,130 +5,6 @@
# tests of Images controller
module Observations
class ImagesControllerTest < FunctionalTestCase
- def test_add_image_to_obs_not_yours
- obs = observations(:coprinus_comatus_obs)
- requires_login(:new, id: obs.id)
- # qr = QueryRecord.last.id.alphabetize
- assert_form_action(action: :create, id: obs.id)
- # Check that image cannot be added to an observation the user doesn't own.
- obs_no_own = observations(:minimal_unknown_obs)
- post(:create, params: { id: obs_no_own.id })
- assert_redirected_to(permanent_observation_path(obs_no_own.id))
- end
-
- def test_add_images_empty
- login("rolf")
- obs = observations(:coprinus_comatus_obs)
- post(:create, params: { id: obs.id })
- assert_flash_text(/no changes/i)
- end
-
- def test_upload_image
- setup_image_dirs
- obs = observations(:coprinus_comatus_obs)
- updated_at = obs.updated_at
- proj = projects(:bolete_project)
- proj.observations << obs
- img_count = obs.images.size
- assert(img_count.positive?)
- assert(obs.thumb_image)
- file = Rack::Test::UploadedFile.new(
- Rails.root.join("test/images/Coprinus_comatus.jpg"), "image/jpeg"
- )
- params = {
- id: obs.id,
- image: {
- "when(1i)" => "2007",
- "when(2i)" => "3",
- "when(3i)" => "29",
- copyright_holder: "Douglas Smith",
- notes: "Some notes."
- },
- upload: {
- image1: file,
- image2: "",
- image3: "",
- image4: ""
- },
- project: {
- # This is a good test: Rolf doesn't belong to the Bolete project,
- # but we still want this image to attach to that project by default,
- # because the *observation* is attached to that project.
- "id_#{proj.id}" => "1"
- }
- }
- File.stub(:rename, false) do
- login("rolf", "testpassword")
- post(:create, params: params)
- end
- assert_equal(20, rolf.reload.contribution)
- assert(obs.reload.images.size == (img_count + 1))
- assert(updated_at != obs.updated_at)
- message = :runtime_image_uploaded_image.t(
- name: "##{obs.images.last.id}"
- )
- assert_flash_text(/#{message}/)
- img = Image.last
- assert_obj_arrays_equal([obs], img.observations)
- assert_obj_arrays_equal([proj], img.projects)
- assert_false(obs.gps_hidden)
- assert_false(img.gps_stripped)
- end
-
- def test_add_images_strip_gps
- login("rolf")
- obs = observations(:coprinus_comatus_obs)
- obs.update_attribute(:gps_hidden, true)
-
- setup_image_dirs
- fixture = "#{MO.root}/test/images/geotagged.jpg"
- fixture = Rack::Test::UploadedFile.new(fixture, "image/jpeg")
-
- post(:create,
- params: { id: obs.id,
- image: { "when(1i)" => "2007",
- "when(2i)" => "3",
- "when(3i)" => "29",
- copyright_holder: "Douglas Smith",
- notes: "Some notes." },
- upload: { image1: fixture,
- image2: "",
- image3: "",
- image4: "" } })
-
- img = Image.last
- assert_true(img.gps_stripped)
- end
-
- def test_add_images_process_image_fail
- login("rolf")
- obs = observations(:coprinus_comatus_obs)
- setup_image_dirs
- fixture = "#{MO.root}/test/images/geotagged.jpg"
- fixture = Rack::Test::UploadedFile.new(fixture, "image/jpeg")
- params = { id: obs.id,
- image: { "when(1i)" => "2007",
- "when(2i)" => "3",
- "when(3i)" => "29",
- copyright_holder: "Douglas Smith",
- notes: "Some notes." },
- upload: { image1: fixture,
- image2: "",
- image3: "",
- image4: "" } }
- image = images(:in_situ_image)
-
- # simulate process_image failure
- image.stub(:process_image, false) do
- Image.stub(:new, image) do
- post(:create, params: params)
- end
- end
-
- assert_flash_error("image.process_image failure should cause flash error")
- assert_redirected_to(permanent_observation_path(obs.id))
- end
-
def test_edit_image
image = images(:connected_coprinus_comatus_image)
params = { "id" => image.id.to_s }
@@ -273,28 +149,18 @@ def test_project_checkboxes
# so there is no way to test init_project_vars_for_reload().
login("rolf")
- get(:new, params: { id: obs1.id })
- assert_response(:redirect)
- get(:new, params: { id: obs2.id })
- assert_response(:redirect)
get(:edit, params: { id: img1.id })
assert_response(:redirect)
get(:edit, params: { id: img2.id })
assert_project_checks(proj1.id => :unchecked, proj2.id => :no_field)
login("mary")
- get(:new, params: { id: obs1.id })
- assert_project_checks(proj1.id => :unchecked, proj2.id => :unchecked)
- get(:new, params: { id: obs2.id })
- assert_project_checks(proj1.id => :unchecked, proj2.id => :checked)
get(:edit, params: { id: img1.id })
assert_project_checks(proj1.id => :unchecked, proj2.id => :checked)
get(:edit, params: { id: img2.id })
assert_response(:redirect)
login("dick")
- get(:new, params: { id: obs2.id })
- assert_project_checks(proj1.id => :no_field, proj2.id => :checked)
get(:edit, params: { id: img1.id })
assert_project_checks(proj1.id => :no_field, proj2.id => :checked)
get(:edit, params: { id: img2.id })
@@ -423,91 +289,5 @@ def test_reuse_image_strip_gps_worked
assert_not_equal(File.size(fixture),
File.size(img.full_filepath("orig")))
end
-
- def test_remove_images_page_access
- obs = observations(:coprinus_comatus_obs)
- params = { id: obs.id }
- assert_equal("rolf", obs.user.login)
- requires_user(
- :remove,
- { controller: "/observations", action: :show, id: obs.id },
- params
- )
- assert_form_action(action: :detach, id: obs.id)
- end
-
- def test_remove_images
- obs = observations(:detailed_unknown_obs)
- keep = images(:turned_over_image)
- remove = images(:in_situ_image)
- assert(obs.images.member?(keep))
- assert(obs.images.member?(remove))
- assert_equal(remove.id, obs.thumb_image_id)
-
- selected = {}
- selected[keep.id.to_s] = "no"
- selected[remove.id.to_s] = "yes"
- params = {
- id: obs.id.to_s,
- selected: selected
- }
- put_requires_login(:detach, params, "mary")
- assert_redirected_to(permanent_observation_path(obs.id))
- assert_equal(10, mary.reload.contribution)
- assert(obs.reload.images.member?(keep))
- assert_not(obs.images.member?(remove))
- assert_equal(keep.id, obs.thumb_image_id)
-
- selected = {}
- selected[keep.id.to_s] = "yes"
- params = {
- id: obs.id.to_s,
- selected: selected
- }
- put(:detach, params: params)
- assert_redirected_to(permanent_observation_path(obs.id))
- # Observation gets downgraded to 1 point because it no longer has images.
- # assert_equal(1, mary.reload.contribution)
- assert_equal(10, mary.reload.contribution)
- assert_not(obs.reload.images.member?(keep))
- assert_nil(obs.thumb_image_id)
- end
-
- def test_remove_images2
- obs = observations(:detailed_unknown_obs)
- images = obs.images
- assert(images.size > 1,
- "Use Observation fixture with multiple images for best coverage")
- user = obs.user
- selected = images.ids.each_with_object({}) do |item, hash|
- # "img_id" => "yes" (yes means detach that image˝)
- hash[item.to_s] = "yes"
- end
- params = { id: obs.id, selected: selected }
-
- login(user.login)
- put(:detach, params: params)
-
- assert_empty(obs.reload.images)
- end
-
- def test_remove_image_by_id
- obs = observations(:detailed_unknown_obs)
- images = obs.images
- assert(images.size > 1,
- "Use Observation fixture with multiple images for best coverage")
- user = obs.user
- login(user.login)
-
- params = { id: obs.id, image_id: "bad_id" }
- # It will just skip a bad image id.
- put(:detach, params: params)
- assert_equal(obs.images, obs.reload.images)
-
- params[:image_id] = images.first.id
- put(:detach, params: params)
-
- assert_equal(obs.images.except(obs.images.first), obs.reload.images)
- end
end
end
diff --git a/test/controllers/observations_controller/observations_controller_show_test.rb b/test/controllers/observations_controller/observations_controller_show_test.rb
index 2dcf866bdb..469cfa7a2c 100644
--- a/test/controllers/observations_controller/observations_controller_show_test.rb
+++ b/test/controllers/observations_controller/observations_controller_show_test.rb
@@ -298,10 +298,6 @@ def test_show_observation_edit_links
get(:show, params: { id: obs.id })
assert_select("a:match('href',?)", edit_observation_path(obs.id), count: 0)
assert_select(".destroy_observation_link_#{obs.id}", count: 0)
- assert_select("a:match('href',?)",
- new_image_for_observation_path(obs.id), count: 0)
- assert_select("a:match('href',?)",
- remove_images_from_observation_path(obs.id), count: 0)
assert_select("a:match('href',?)",
reuse_images_for_observation_path(obs.id), count: 0)
get(:edit, params: { id: obs.id })
@@ -314,10 +310,6 @@ def test_show_observation_edit_links
assert_select("a[href=?]", edit_observation_path(obs.id), minimum: 1)
# Destroy button is in a form, not a link_to
assert_select(".destroy_observation_link_#{obs.id}", minimum: 1)
- assert_select("a[href=?]",
- new_image_for_observation_path(obs.id), minimum: 1)
- assert_select("a[href=?]",
- remove_images_from_observation_path(obs.id), minimum: 1)
assert_select("a[href=?]",
reuse_images_for_observation_path(obs.id), minimum: 1)
get(:edit, params: { id: obs.id })
@@ -328,10 +320,6 @@ def test_show_observation_edit_links
assert_select("a[href=?]", edit_observation_path(obs.id), minimum: 1)
# Destroy button is in a form, not a link_to
assert_select(".destroy_observation_link_#{obs.id}", minimum: 1)
- assert_select("a[href=?]",
- new_image_for_observation_path(obs.id), minimum: 1)
- assert_select("a[href=?]",
- remove_images_from_observation_path(obs.id), minimum: 1)
assert_select("a[href=?]",
reuse_images_for_observation_path(obs.id), minimum: 1)
get(:edit, params: { id: obs.id })
diff --git a/test/system/herbarium_form_system_test.rb b/test/system/herbarium_form_system_test.rb
new file mode 100644
index 0000000000..3f5dedd452
--- /dev/null
+++ b/test/system/herbarium_form_system_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require("application_system_test_case")
+
+class HerbariumFormSystemTest < ApplicationSystemTestCase
+ def test_create_fungarium_new_location
+ # browser = page.driver.browser
+ rolf = users("rolf")
+ login!(rolf)
+
+ visit("/herbaria/new")
+ assert_selector("body.herbaria__new")
+ create_herbarium_with_new_location
+
+ # assert_no_selector("#modal_herbarium")
+ assert_selector("body.herbaria__show")
+ assert_selector("h1", text: "Herbarium des Cévennes (CEV)")
+ assert_selector("#herbarium_location",
+ text: "Génolhac, Gard, Occitanie, France")
+ end
+
+ def test_observation_form_create_fungarium_new_location
+ rolf = users("rolf")
+ login!(rolf)
+
+ visit("/observations/new")
+ assert_selector("body.observations__new")
+
+ assert_selector("#observation_naming_specimen")
+ scroll_to(find("#observation_naming_specimen"), align: :top)
+ check("observation_specimen")
+ assert_selector("#herbarium_record_herbarium_name")
+ assert_selector(".create-link", text: :create_herbarium.l)
+ click_link(:create_herbarium.l)
+
+ assert_selector("#modal_herbarium")
+ create_herbarium_with_new_location
+
+ assert_no_selector("#modal_herbarium")
+ assert_field("herbarium_record_herbarium_name",
+ with: "Herbarium des Cévennes")
+ end
+
+ def create_herbarium_with_new_location
+ assert_selector("#herbarium_place_name")
+ fill_in("herbarium_place_name", with: "genohlac gard france")
+ assert_link(:form_observations_create_locality.l)
+ click_link(:form_observations_create_locality.l)
+
+ assert_selector("#herbarium_place_name.geocoded")
+ assert_field("herbarium_place_name",
+ with: "Génolhac, Gard, Occitanie, France")
+
+ assert_field("herbarium_location_id", with: "-1", type: :hidden)
+ assert_field("location_north", with: "44.3726", type: :hidden)
+ assert_field("location_east", with: "3.985", type: :hidden)
+ assert_field("location_south", with: "44.3055", type: :hidden)
+ assert_field("location_west", with: "3.9113", type: :hidden)
+ assert_field("location_high", with: "1388.2098", type: :hidden)
+ assert_field("location_low", with: "287.8201", type: :hidden)
+
+ within("#herbarium_form") do
+ fill_in("herbarium_name", with: "Herbarium des Cévennes")
+ fill_in("herbarium_code", with: "CEV")
+ click_commit
+ end
+ end
+end
diff --git a/test/system/location_form_system_test.rb b/test/system/location_form_system_test.rb
index c9df241d3b..49225adfec 100644
--- a/test/system/location_form_system_test.rb
+++ b/test/system/location_form_system_test.rb
@@ -4,7 +4,6 @@
class LocationFormSystemTest < ApplicationSystemTestCase
def test_format_new_location_name
- skip("This test is inconsistent when run in CI")
# browser = page.driver.browser
rolf = users("rolf")
login!(rolf)
diff --git a/test/system/observation_comment_system_test.rb b/test/system/observation_comment_system_test.rb
index 5142da3731..079189f035 100644
--- a/test/system/observation_comment_system_test.rb
+++ b/test/system/observation_comment_system_test.rb
@@ -8,7 +8,6 @@ class ObservationCommentSystemTest < ApplicationSystemTestCase
# Katrina edits the comment, rolf should get the change immediately.
# Katrina deletes the comment, rolf should get the deletion immediately.
def test_add_and_edit_comment
- skip("This test is inconsistent when run in CI")
rolf = users("rolf")
katrina = users("katrina")
obs = observations(:coprinus_comatus_obs)
diff --git a/test/system/observation_form_system_test.rb b/test/system/observation_form_system_test.rb
index 8b71e12d34..3ca5ced922 100644
--- a/test/system/observation_form_system_test.rb
+++ b/test/system/observation_form_system_test.rb
@@ -27,9 +27,8 @@ def test_create_minimal_observation
assert_field("observation_place_name", with: locations.first.name)
# Move to the next step, Identification
- step_nav_1 = find("#step-nav-1")
- scroll_to(step_nav_1, align: :top)
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
fill_in("naming_name", with: "Elfin saddle")
# don't wait for the autocompleter - we know it's an elfin saddle!
@@ -50,9 +49,8 @@ def test_create_minimal_observation
assert_field("observation_place_name", with: locations.first.name)
# Move to the next step, Identification
- step_nav_1 = find("#step-nav-1")
- scroll_to(step_nav_1, align: :top)
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
assert_selector("#name_messages", text: "MO does not recognize the name")
fill_in("naming_name", with: "Coprinus com")
@@ -96,15 +94,17 @@ def test_trying_to_create_duplicate_location_just_uses_existing_location
assert_selector("[data-type='location']")
find(id: "observation_place_name").trigger("click")
# This should make the "create_locality" button appear.
+ # It works fine in headless mode.
+ assert_selector(".create-button span",
+ text: /#{:form_observations_create_locality.l}/)
click_on(:form_observations_create_locality.l)
assert_selector("[data-type='location_google']")
assert_field("observation_place_name", with: last_obs.where)
assert_field("observation_location_id", with: "", type: :hidden)
# Move to the next step, Identification
- step_nav_1 = find("#step-nav-1")
- scroll_to(step_nav_1, align: :top)
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
within("#observation_form") { click_commit }
@@ -119,7 +119,6 @@ def test_trying_to_create_duplicate_location_just_uses_existing_location
end
def test_autofill_location_from_geotagged_image_nothing_matches
- skip("This test is inconsistent when run in CI")
setup_image_dirs # in general_extensions
login!(katrina)
@@ -148,7 +147,9 @@ def test_autofill_location_from_geotagged_image_nothing_matches
assert_image_date_copied_to_obs(GEOTAGGED_EXIF)
sleep(0.5)
# we should have the new type of location_google autocompleter now
- assert_selector("[data-type='location_google'][data-stimulus='connected']")
+ assert_selector(
+ "[data-type='location_google'][data-stimulus='autocompleter-connected']"
+ )
# Place name should now have been filled by Google, no MO locations match
assert_field("observation_place_name", with: UNIVERSITY_PARK[:name],
wait: 6)
@@ -229,8 +230,6 @@ def test_autofill_location_from_geotagged_image_matching_location
end
def test_post_edit_and_destroy_with_details_and_location
- skip("This test is inconsistent when run in CI")
-
# browser = page.driver.browser
setup_image_dirs # in general_extensions
@@ -248,9 +247,8 @@ def test_post_edit_and_destroy_with_details_and_location
assert_field("observation_place_name", with: last_obs.where)
# Move to the next step, Identification
- step_nav_1 = find("#step-nav-1")
- scroll_to(step_nav_1, align: :top)
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
assert_field("naming_name", with: "")
assert(last_obs.is_collection_location)
@@ -259,9 +257,8 @@ def test_post_edit_and_destroy_with_details_and_location
assert_field(other_notes_id, with: "", visible: :all)
# Move to the previous step, Images/Details
- step_nav_2 = find("#step-nav-2")
- scroll_to(step_nav_2, align: :top)
- within(step_nav_2) { click_on(:BACK.l) }
+ images_details = find("#observation_images_details")
+ scroll_to(images_details, align: :top)
# Add the images separately, so we can be sure of the order. Otherwise,
# images appear in the order each upload finishes, which is unpredictable.
@@ -367,7 +364,8 @@ def test_post_edit_and_destroy_with_details_and_location
uncheck("observation_is_collection_location", visible: :all)
# Move to the next step, Identification
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
sleep(1)
specimen_section = find("#observation_specimen_section", visible: :all)
@@ -379,12 +377,15 @@ def test_post_edit_and_destroy_with_details_and_location
fill_in(other_notes_id, with: "Notes for observation", visible: :all)
# Move to the next step, Projects/Lists
- within(step_nav_2) { click_on(:NEXT.l) }
+ projects = find("#observation_projects")
+ scroll_to(projects, align: :top)
# Inherited project constraints maybe messing with this observation - clear
all('[id^="project_id_"]', visible: :all).each do |project_checkbox|
project_checkbox.trigger("click") if project_checkbox.checked?
end
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
# submit_observation_form_with_errors
within("#observation_form") { click_commit }
@@ -429,25 +430,30 @@ def test_post_edit_and_destroy_with_details_and_location
assert_image_exif_available(SO_PASA_EXIF)
end
- fill_in("observation_place_name", with: "")
+ fill_in("observation_place_name", with: "south pas")
click_on(:form_observations_create_locality.l)
# lat/lng does not match Google's Pasadena, but does match South Pasadena
assert_selector("[data-type='location_google']")
- # assert_selector(".auto_complete", wait: 6)
find("#observation_place_name").trigger("focus")
- assert_selector(".dropdown-item a[data-id='-1']",
- text: SOUTH_PASADENA[:name], visible: :all, wait: 6)
+ # assert_selector(".auto_complete", wait: 6)
+ # assert_selector(".dropdown-item a[data-id='-1']",
+ # text: SOUTH_PASADENA[:name], visible: :all, wait: 6)
# There may be more than one of these, click the first
- find(".dropdown-item a[data-id='-1']",
- text: SOUTH_PASADENA[:name], visible: :all).trigger("click")
-
+ # find(".dropdown-item a[data-id='-1']",
+ # text: SOUTH_PASADENA[:name], visible: :all).trigger("click")
+ assert_field("observation_place_name", with: SOUTH_PASADENA[:name])
+ sleep(1)
+ # debugger
# Check the hidden fields returned by Google
assert_hidden_location_fields_filled(SOUTH_PASADENA)
# Move to the next step, Identification
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
- assert_selector("[data-type='name'][data-stimulus='connected']")
+ assert_selector(
+ "[data-type='name'][data-stimulus='autocompleter-connected']"
+ )
fill_in("naming_name", with: "Agaricus campestris")
assert_field("naming_name", with: "Agaricus campestris")
select(Vote.confidence(Vote.next_best_vote), from: "naming_vote_value")
@@ -521,7 +527,8 @@ def test_post_edit_and_destroy_with_details_and_location
sleep(1)
# Move to the next step, Identification
- within(step_nav_1) { click_on(:NEXT.l) }
+ naming = find("#observation_naming_specimen")
+ scroll_to(naming, align: :top)
sleep(1)
obs_notes = find("#observation_notes")
@@ -634,9 +641,9 @@ def assert_date_is_now
end
def assert_geolocation_is_empty
- assert_field("observation_lat", with: "")
- assert_field("observation_lng", with: "")
- assert_field("observation_alt", with: "")
+ assert_field("observation_lat", with: "", visible: :all)
+ assert_field("observation_lng", with: "", visible: :all)
+ assert_field("observation_alt", with: "", visible: :all)
end
def assert_image_exif_available(image_data)