Skip to content

Commit

Permalink
Merge branch 'main' into query-scopes-folder
Browse files Browse the repository at this point in the history
  • Loading branch information
nimmolo committed Jan 28, 2025
2 parents 5d273a6 + 2510d12 commit ae51d09
Show file tree
Hide file tree
Showing 129 changed files with 5,728 additions and 4,757 deletions.
1,190 changes: 595 additions & 595 deletions .rubocop_todo.yml

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions app/classes/auto_complete.rb → app/classes/autocomplete.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true

#
# = AutoComplete base class
# = Autocomplete base class
#
# results = AutoCompleteName.new(string: 'Agaricus') # ...or...
# results = AutoComplete.subclass('name').new(string: 'Agaricus')
# results = AutocompleteName.new(string: 'Agaricus') # ...or...
# results = Autocomplete.subclass('name').new(string: 'Agaricus')
# render(json: ActiveSupport::JSON.encode(results)
#
################################################################################

class AutoComplete
class Autocomplete
attr_accessor :string, :matches, :all, :whole

PUNCTUATION = '[ -\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]'
Expand All @@ -19,7 +19,7 @@ def limit
end

def self.subclass(type)
"AutoComplete::For#{type.camelize}".constantize
"Autocomplete::For#{type.camelize}".constantize
rescue StandardError
raise("Invalid auto-complete type: #{type.inspect}")
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ByString < AutoComplete
class Autocomplete::ByString < Autocomplete
# Find minimal string whose matches are within the limit. This is designed
# to reduce the number of AJAX requests required if the user backspaces from
# the end of the text field string.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class AutoComplete::ByWord < AutoComplete
# Same as AutoCompleteByString#refine_token, except words are allowed
class Autocomplete::ByWord < Autocomplete
# Same as AutocompleteByString#refine_token, except words are allowed
# to be out of order.
def refine_token
# Get rid of trivial case immediately.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ForClade < AutoComplete::ByString
class Autocomplete::ForClade < Autocomplete::ByString
def rough_matches(letter)
# (this sort puts higher rank on top)
clades = Name.with_correct_spelling.with_rank_above_genus.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Note this gets a params[:user_id] but we're ignoring it here
class AutoComplete::ForHerbarium < AutoComplete::ByWord
class Autocomplete::ForHerbarium < Autocomplete::ByWord
def rough_matches(letter)
herbaria =
Herbarium.select(:code, :name, :id).distinct.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Or would this scatter the code?
# Thinking the scope could be useful, or it could use this class.
#
class AutoComplete::ForLocation < AutoComplete::ByWord
class Autocomplete::ForLocation < Autocomplete::ByWord
attr_accessor :reverse

def initialize(params)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Autocompleter for location names that encompass a given lat/lng.
class AutoComplete::ForLocationContaining < AutoComplete::ByWord
class Autocomplete::ForLocationContaining < Autocomplete::ByWord
attr_accessor :reverse, :lat, :lng

# include Mappable::BoxMethods
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ForName < AutoComplete::ByString
class Autocomplete::ForName < Autocomplete::ByString
def rough_matches(letter)
names = Name.with_correct_spelling.
select(:text_name, :id, :deprecated).distinct.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ForProject < AutoComplete::ByWord
class Autocomplete::ForProject < Autocomplete::ByWord
def rough_matches(letter)
projects = Project.select(:title, :id).distinct.
where(Project[:title].matches("#{letter}%").
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# This requires Stimulus delaying the fetch until we have a complete word.
class AutoComplete::ForRegion < AutoComplete::ByWord
class Autocomplete::ForRegion < Autocomplete::ByWord
attr_accessor :reverse

def initialize(params)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ForSpeciesList < AutoComplete::ByWord
class Autocomplete::ForSpeciesList < Autocomplete::ByWord
def rough_matches(letter)
lists = SpeciesList.select(:title, :id).distinct.
where(SpeciesList[:title].matches("#{letter}%").
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class AutoComplete::ForUser < AutoComplete::ByString
class Autocomplete::ForUser < Autocomplete::ByString
def rough_matches(letter)
users = User.verified.select(:login, :name, :id).distinct.
where(User[:login].matches("#{letter}%").
Expand Down
2 changes: 1 addition & 1 deletion app/classes/inat/obs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def blurred_west

def to_rad(degrees) = degrees * Math::PI / 180.0

# copied from AutoComplete::ForLocationContaining
# copied from Autocomplete::ForLocationContaining
def location_box(loc)
Mappable::Box.new(north: loc[:north], south: loc[:south],
east: loc[:east], west: loc[:west])
Expand Down
8 changes: 5 additions & 3 deletions app/classes/inat/page_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,18 @@ def next_request(**args)
order: "asc", order_by: "id",
# obss of only the iNat user with iNat login @inat_import.inat_username
user_login: nil,
iconic_taxa: ICONIC_TAXA
iconic_taxa: ICONIC_TAXA,
without_field: "Mushroom Observer URL"
}.merge(args)
query = URI.encode_www_form(query_args)

# ::Inat.new(operation: query, token: @inat_import.token).body
# Nimmo 2024-06-19 jdc. Moving the request from the inat class to here.
# RestClient::Request.execute wasn't available in the class
headers = { authorization: "Bearer #{@importer.token}", accept: :json }
::RestClient::Request.execute(
method: :get, url: "#{API_BASE}/observations?#{query}", headers: headers
method: :get,
url: "#{API_BASE}/observations?#{query_args.to_query}",
headers: headers
)
rescue ::RestClient::ExceptionWithResponse => e
@importer.add_response_error(e.response)
Expand Down
120 changes: 120 additions & 0 deletions app/classes/lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

# Lookup
#
# A flexible looker-upper of records. It can handle any identifiers we're likely
# to throw at it: a string, ID, instance, or a mixed array of any of those. The
# `lookup_method` has to be configured in the Lookup child class, because the
# lookup column names are different for each model.
#
# Primarily used to get a clean set of ids for ActiveRecord query params.
# For example, indexes like "Observations for (given) Projects" can be filtered
# for more than one project at a time: "NEMF 2023" and "NEMF 2024".
# The observation query needs the project IDs, and Lookup just allows callers
# to send whatever param type is available. This is handy in the API and
# in searches.
#
# Create an instance of a child class with a string, instance or id, or a mixed
# array of any of these. Returns an array of ids, instances or strings (names)
# via instance methods `ids`, `instances` and `titles`.
#
# Use:
# project_ids = Lookup::Projects.new(["NEMF 2023", "NEMF 2024"]).ids
# Observation.where(project: project_ids)
#
# fred_ids = Lookup::Users.new(["Fred", "Freddie", "Freda", "Anni Frid"]).ids
# Image.where(user: fred_ids)
#
# Instance methods:
# (all return arrays)
#
# ids: Array of ids of records matching the values sent to the instance
# instances: Array of instances of those records
# titles: Array of names of those records, via @title_column set in subclass
# (A `names` method seemed too confusing, because Lookup::Names...)
#
# Class constants:
# (defined in subclass)
#
# MODEL:
# TITLE_COLUMN:
#
class Lookup
attr_reader :vals, :params

def initialize(vals, params = {})
unless defined?(self.class::MODEL)
raise("Lookup is only usable via the subclasses, like Lookup::Names.")
end

@model = self.class::MODEL
@title_column = self.class::TITLE_COLUMN
@vals = prepare_vals(vals)
@params = params
end

def prepare_vals(vals)
return [] if vals.blank?

[vals].flatten
end

def ids
@ids ||= lookup_ids
end

def instances
@instances ||= lookup_instances
end

def titles
@titles ||= lookup_titles
end

def lookup_ids
return [] if @vals.blank?

evaluate_values_as_ids
end

# Could just look them up from the ids, but vals may already have instances
def lookup_instances
return [] if @vals.blank?

evaluate_values_as_instances
end

def lookup_titles
return [] if @vals.blank?

instances.map(&:"#{@title_column}")
end

def evaluate_values_as_ids
@vals.map do |val|
if val.is_a?(@model)
val.id
elsif val.is_a?(AbstractModel)
raise("Passed a #{val.class} to LookupIDs for #{@model}.")
elsif /^\d+$/.match?(val.to_s)
val
else
lookup_method(val).map(&:id) # each lookup returns an array
end
end.flatten.uniq.compact
end

def evaluate_values_as_instances
@vals.map do |val|
if val.is_a?(@model)
val
elsif val.is_a?(AbstractModel)
raise("Passed a #{val.class} to LookupIDs for #{@model}.")
elsif /^\d+$/.match?(val.to_s)
@model.find(val.to_i)
else
lookup_method(val)
end
end.flatten.uniq.compact
end
end
14 changes: 14 additions & 0 deletions app/classes/lookup/external_sites.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class Lookup::ExternalSites < Lookup
MODEL = ExternalSite
TITLE_COLUMN = :name

def initialize(vals, params = {})
super
end

def lookup_method(name)
ExternalSite.where(name: name)
end
end
14 changes: 14 additions & 0 deletions app/classes/lookup/herbaria.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class Lookup::Herbaria < Lookup
MODEL = Herbarium
TITLE_COLUMN = :name

def initialize(vals, params = {})
super
end

def lookup_method(name)
Herbarium.where(name: name)
end
end
14 changes: 14 additions & 0 deletions app/classes/lookup/herbarium_records.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class Lookup::HerbariumRecords < Lookup
MODEL = HerbariumRecord
TITLE_COLUMN = :id

def initialize(vals, params = {})
super
end

def lookup_method(name)
HerbariumRecord.where(id: name)
end
end
17 changes: 17 additions & 0 deletions app/classes/lookup/locations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

class Lookup::Locations < Lookup
MODEL = Location
TITLE_COLUMN = :name

def initialize(vals, params = {})
super
end

def lookup_method(name)
# Downcases and removes all punctuation, so it's a multi-string search
# e.g. "sonoma co california usa"
pattern = Location.clean_name(name.to_s).clean_pattern
Location.name_contains(pattern)
end
end
Loading

0 comments on commit ae51d09

Please sign in to comment.