Skip to content

Commit

Permalink
Merge pull request #1769 from MushroomObserver/nimmo-name-resolver
Browse files Browse the repository at this point in the history
`NameResolver`
  • Loading branch information
nimmolo committed Jan 5, 2024
2 parents 8015bb9 + 4bdb40f commit e1972fd
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 124 deletions.
10 changes: 9 additions & 1 deletion app/controllers/observations/namings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,18 @@ def set_ivars_for_validation(naming_args, vote_args,
result
end

# Set the ivars for the form, and potentially form_name_feedback
# in the case the name is not resolved unambiguously
def resolve_name(given_name, approved_name, chosen_name)
@resolver = Naming::NameResolver.new(
given_name, approved_name, chosen_name
)
# NOTE: views could be refactored to access properties of the @resolver,
# e.g. `@resolver.valid_names`, instead of these ivars.
# All but success, @what, @name are only used by form_name_feedback.
(success, @what, @name, @names, @valid_names,
@parent_deprecated, @suggest_corrections) =
Name.resolve_name(given_name, approved_name, chosen_name)
@resolver.ivar_array
success && @name
end

Expand Down
11 changes: 8 additions & 3 deletions app/controllers/observations_controller/validators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ def validate_params(params)
def validate_name(params)
given_name = param_lookup([:naming, :name], "").to_s
chosen_name = param_lookup([:chosen_name, :name_id], "").to_s
(success, @what, @name, @names, @valid_names, @parent_deprecated,
@suggest_corrections) =
Name.resolve_name(given_name, params[:approved_name], chosen_name)
@resolver = Naming::NameResolver.new(
given_name, params[:approved_name], chosen_name
)
# NOTE: views could be refactored to access properties of the @resolver,
# e.g. `@resolver.valid_names`, instead of these ivars.
# All but success, @what, @name are only used by form_name_feedback.
(success, @what, @name, @names, @valid_names,
@parent_deprecated, @suggest_corrections) = @resolver.ivar_array
if @name
@naming.name = @name
else
Expand Down
120 changes: 0 additions & 120 deletions app/models/name/resolve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,126 +18,6 @@ def save_with_log(log = nil, args = {})
end

module ClassMethods
# Resolves the name using these heuristics:
# First time through:
# Only 'what' will be filled in.
# Prompts the user if not found.
# Gives user a list of options if matches more than one. ('names')
# Gives user a list of options if deprecated. ('valid_names')
# Second time through:
# 'what' is a new string if user typed new name, else same as old 'what'
# 'approved_name' is old 'what'
# 'chosen_name' hash on name.id: radio buttons
# Uses the name chosen from the radio buttons first.
# If 'what' has changed, then go back to "First time through" above.
# Else 'what' has been approved, create it if necessary.
#
# INPUTS:
# what params[:naming][:name] Text field.
# approved_name params[:approved_name] Last name user entered.
# chosen_name params[:chosen_name][:name_id] Name id from radio boxes.
# (User.current -- might be used by one or more things)
#
# RETURNS:
# success true: okay to use name; false: user needs to approve name.
# name Name object if it resolved without reservations.
# names List of choices if name matched multiple objects.
# valid_names List of choices if name is deprecated.
#
# rubocop:disable Metrics/MethodLength
def resolve_name(what, approved_name, chosen_name)
success = true
name = nil
names = nil
valid_names = nil
parent_deprecated = nil
suggest_corrections = false

what2 = what.to_s.tr("_", " ").strip_squeeze
approved_name2 = approved_name.to_s.tr("_", " ").strip_squeeze

unless what2.blank? || names_for_unknown.member?(what2.downcase)
success = false

ignore_approved_name = false
# Has user chosen among multiple matching names or among
# multiple approved names?
if chosen_name.blank?
what2 = fix_capitalized_species_epithet(what2)

# Look up name: can return zero (unrecognized), one
# (unambiguous match), or many (multiple authors match).
names = find_names_filling_in_authors(what2)
else
names = [find(chosen_name)]
# This tells it to check if this name is deprecated below EVEN
# IF the user didn't change the what field. This will solve
# the problem of multiple matching deprecated names discussed
# below.
ignore_approved_name = true
end

# Create temporary name object for it. (This will not save anything
# EXCEPT in the case of user supplying author for existing name that
# has no author.)
if names.empty? &&
(name = create_needed_names(approved_name2, what2))
names << name
end

# No matches -- suggest some correct names to make Debbie happy.
if names.empty?
if (parent = parent_if_parent_deprecated(what2))
valid_names = names_from_synonymous_genera(what2, parent)
parent_deprecated = parent
else
valid_names = suggest_alternate_spellings(what2)
suggest_corrections = true
end

# Only one match (or we just created an approved new name).
elsif names.length == 1
target_name = names.first
# Single matching name. Check if it's deprecated.
if target_name.deprecated &&
(ignore_approved_name || (approved_name != what))
# User has not explicitly approved the deprecated name: get list of
# valid synonyms. Will display them for user to choose among.
valid_names = target_name.approved_synonyms
else
# User has selected an unambiguous, accepted name... or they have
# chosen or approved of their choice. Either way, go with it.
name = target_name
# Fill in author, just in case user has chosen between two authors.
# If the form fails for some other reason and we don't do this, it
# will ask the user to choose between the authors *again* later.
what = name.real_search_name
# (This is the only way to get out of here with success.)
success = true
end

# Multiple matches.
elsif names.length > 1
if names.reject(&:deprecated).empty?
# Multiple matches, all of which are deprecated. Check if
# they all have the same set of approved names. Pain in the
# butt, but otherwise can get stuck choosing between
# Helvella infula Fr. and H. infula Schaeff. without anyone
# mentioning that both are deprecated by Gyromitra infula.
valid_set = Set.new
names.each do |n|
valid_set.merge(n.approved_synonyms)
end
valid_names = valid_set.sort_by(&:sort_name)
end
end
end

[success, what, name, names, valid_names, parent_deprecated,
suggest_corrections]
end
# rubocop:enable Metrics/MethodLength

def create_needed_names(input_what, output_what = nil)
names = []
if output_what.nil? || input_what == output_what
Expand Down
139 changes: 139 additions & 0 deletions app/models/naming/name_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# frozen_string_literal: true

# Resolves the name using these heuristics:
# First time through:
# Only 'what' will be filled in.
# Prompts the user if not found.
# Gives user a list of options if matches more than one. ('names')
# Gives user a list of options if deprecated. ('valid_names')
# Second time through:
# 'what' is a new string if user typed new name, else same as old 'what'
# 'approved_name' is old 'what'
# 'chosen_name' hash on name.id: radio buttons
# Uses the name chosen from the radio buttons first.
# If 'what' has changed, then go back to "First time through" above.
# Else 'what' has been approved, create it if necessary.
#
# INPUTS:
# what params[:naming][:name] Text field.
# approved_name params[:approved_name] Last name user entered.
# chosen_name params[:chosen_name][:name_id] Name id from radio boxes.
# (User.current -- might be used by one or more things)
#
# RETURNS:
# success true: okay to use name; false: user needs to approve name.
# name Name object if it resolved without reservations.
#
# Used by form_name_feedback if name not resolved:
# names List of choices if name matched multiple objects.
# valid_names List of choices if name is deprecated.
# parent_deprecated Boolean
# suggest_corrections Boolean
#
class Naming
class NameResolver
attr_reader :success, :name, :names, :valid_names, :parent_deprecated,
:suggest_corrections

def initialize(what, approved_name, chosen_name)
@success = true
@what = what
@name = nil
@names = nil
@valid_names = nil
@parent_deprecated = nil
@suggest_corrections = false

resolve(what, approved_name, chosen_name)
end

# rubocop:disable Metrics/MethodLength
def resolve(what, approved_name, chosen_name)
what2 = what.to_s.tr("_", " ").strip_squeeze
approved_name2 = approved_name.to_s.tr("_", " ").strip_squeeze
return if what2.blank? || Name.names_for_unknown.member?(what2.downcase)

@success = false

ignore_approved_name = false
# Has user chosen among multiple matching names or among
# multiple approved names?
if chosen_name.blank?
what2 = Name.fix_capitalized_species_epithet(what2)

# Look up name: can return zero (unrecognized), one
# (unambiguous match), or many (multiple authors match).
@names = Name.find_names_filling_in_authors(what2)
else
@names = [Name.find(chosen_name)]
# This tells it to check if this name is deprecated below EVEN
# IF the user didn't change the what field. This will solve
# the problem of multiple matching deprecated names discussed
# below.
ignore_approved_name = true
end

# Create temporary name object for it. (This will not save anything
# EXCEPT in the case of user supplying author for existing name that
# has no author.)
if @names.empty? &&
(@name = Name.create_needed_names(approved_name2, what2))
@names << name
end

# No matches -- suggest some correct names to make Debbie happy.
if @names.empty?
if (parent = Name.parent_if_parent_deprecated(what2))
@valid_names = Name.names_from_synonymous_genera(what2, parent)
@parent_deprecated = parent
else
@valid_names = Name.suggest_alternate_spellings(what2)
@suggest_corrections = true
end

# Only one match (or we just created an approved new name).
elsif @names.length == 1
target_name = names.first
# Single matching name. Check if it's deprecated.
if target_name.deprecated &&
(ignore_approved_name || (approved_name != what))
# User has not explicitly approved the deprecated name: get list of
# valid synonyms. Will display them for user to choose among.
@valid_names = target_name.approved_synonyms
else
# User has selected an unambiguous, accepted name... or they have
# chosen or approved of their choice. Either way, go with it.
@name = target_name
# Fill in author, just in case user has chosen between two authors.
# If the form fails for some other reason and we don't do this, it
# will ask the user to choose between the authors *again* later.
@what = name.real_search_name
# (This is the only way to get out of here with success.)
@success = true
end

# Multiple matches.
elsif @names.length > 1
if @names.reject(&:deprecated).empty?
# Multiple matches, all of which are deprecated. Check if
# they all have the same set of approved names. Pain in the
# butt, but otherwise can get stuck choosing between
# Helvella infula Fr. and H. infula Schaeff. without anyone
# mentioning that both are deprecated by Gyromitra infula.
valid_set = Set.new
@names.each do |n|
valid_set.merge(n.approved_synonyms)
end
@valid_names = valid_set.sort_by(&:sort_name)
end
end
end
# rubocop:enable Metrics/MethodLength

# Convenience method returning an array for mass ivar assignment
def ivar_array
[@success, @what, @name, @names, @valid_names, @parent_deprecated,
@suggest_corrections]
end
end
end

0 comments on commit e1972fd

Please sign in to comment.