Skip to content

Commit

Permalink
Merge branch 'main' into create-obs-reorg
Browse files Browse the repository at this point in the history
  • Loading branch information
nimmolo committed Jun 21, 2024
2 parents 249e3f0 + 977aeb9 commit 5b03dd5
Show file tree
Hide file tree
Showing 19 changed files with 300 additions and 183 deletions.
2 changes: 1 addition & 1 deletion app/assets/stylesheets/BlackOnWhite.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

$LOGO_BORDER_COLOR: #DDDDDD;
$LEFT_BAR_BORDER_COLOR: #DDDDDD;
$TOP_BAR_BORDER_COLOR: #DDDDDD;
$TOP_BAR_BORDER_COLOR: #DDDEDD;
$LIST_BORDER_COLOR: #DDDDDD;
$BUTTON_HOVER_BORDER_COLOR: #CCCCCC;
$BUTTON_BG_COLOR: #CCCCCC;
Expand Down
5 changes: 5 additions & 0 deletions app/assets/stylesheets/mo/_autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ li.dropdown-item {
color: $dropdown-link-color;
text-decoration: none;

// &[data-deprecated="true"] {
// opacity: 0.81;
// font-style: italic;
// }

&:hover {
color: $dropdown-link-color;
background-color: $dropdown-link-hover-bg;
Expand Down
36 changes: 18 additions & 18 deletions app/classes/auto_complete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#
# = AutoComplete base class
#
# auto = AutoCompleteName.new('Agaricus') # ...or...
# auto = AutoComplete.subclass('name').new('Agaricus')
# render(:inline => auto.matching_strings.join("\n"))
# results = AutoCompleteName.new(string: 'Agaricus') # ...or...
# results = AutoComplete.subclass('name').new(string: 'Agaricus')
# render(json: ActiveSupport::JSON.encode(results)
#
################################################################################

Expand All @@ -30,37 +30,37 @@ def initialize(params = {})
self.whole = params[:whole].present?
end

def matching_strings
# returns an array of { name:, id: } objects
def matching_records
# unless 'whole', use the first letter of the string to define the matches
token = whole ? string : string[0]
self.matches = rough_matches(token)
self.matches = rough_matches(token) || [] # defined in type-subclass
clean_matches
return matches if all

minimal_string = refine_matches # defined in subclass
truncate_matches
[minimal_string] + matches
# [[minimal_string, nil]] + matches
unless all
minimal_string = refine_token # defined in subclass
matches.unshift({ name: minimal_string, id: 0 })
truncate_matches
end

matches
end

private

def clean_matches
matches.map! do |str|
str.sub(/\s*[\r\n]\s*.*/m, "").sub(/\A\s+/, "").sub(/\s+\Z/, "")
matches.map! do |obj|
obj[:name] = obj[:name].sub(/\s*[\r\n]\s*.*/m, "").
sub(/\A\s+/, "").sub(/\s+\Z/, "")
obj
end
# matches.map! do |str, id|
# clean = str.sub(/\s*[\r\n]\s*.*/m, "").sub(/\A\s+/, "").sub(/\s+\Z/, "")
# [clean, id]
# end
matches.uniq!
end

def truncate_matches
return unless matches.length > limit

matches.slice!(limit..-1)
matches.push("...")
# matches.push(["...", nil])
matches.push({ name: "...", id: nil })
end
end
8 changes: 4 additions & 4 deletions app/classes/auto_complete/by_string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ class AutoComplete::ByString < AutoComplete
# with the correct first letter. Applies additional letters one at a time
# until the number of matches falls below limit.
#
# Returns the final (minimal) string actually used, and changes matches in
# place. The array 'matches' is guaranteed to be <= limit.
def refine_matches
# Returns the final (minimal) [string, id] actually used, and changes matches
# in place. The array 'matches' is guaranteed to be <= limit.
def refine_token
# Get rid of trivial case immediately.
return string[0] if matches.length <= limit

Expand All @@ -20,7 +20,7 @@ def refine_matches
string.chars.each do |letter|
used += letter
regex = /(^|#{PUNCTUATION})#{used}/i
matches.select! { |m| m.match(regex) }
matches.select! { |obj| obj[:name].match(regex) }
break if matches.length <= limit
end
used
Expand Down
14 changes: 7 additions & 7 deletions app/classes/auto_complete/by_word.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

class AutoComplete::ByWord < AutoComplete
# Same as AutoCompleteByString#refine_matches, except words are allowed
# Same as AutoCompleteByString#refine_token, except words are allowed
# to be out of order.
def refine_matches
def refine_token
# Get rid of trivial case immediately.
return string[0] if matches.length <= limit
return string[0] if matches&.length&.<= limit

# Apply words in order, requiring full word-match on all but last.
words = string.split
Expand All @@ -17,14 +17,14 @@ def refine_matches
word.chars.each do |letter|
part += letter
regex = /(^|#{PUNCTUATION})#{part}/i
matches.select! { |m| m.match(regex) }
return used + part if matches.length <= limit
matches&.select! { |obj| obj[:name].match(regex) }
return used + part if matches&.length&.<= limit
end
if n < words.length
used += "#{word} "
regex = /(^|#{PUNCTUATION})#{word}(#{PUNCTUATION}|$)/i
matches.select! { |m| m.match(regex) }
return used if matches.length <= limit
matches&.select! { |obj| obj[:name].match(regex) }
return used if matches&.length&.<= limit
else
used += word
return used
Expand Down
13 changes: 10 additions & 3 deletions app/classes/auto_complete/for_clade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
class AutoComplete::ForClade < AutoComplete::ByString
def rough_matches(letter)
# (this sort puts higher rank on top)
Name.with_correct_spelling.with_rank_above_genus.
where(Name[:text_name].matches("#{letter}%")).order(rank: :desc).
select(:text_name, :rank).map(&:text_name).uniq
clades = Name.with_correct_spelling.with_rank_above_genus.
where(Name[:text_name].matches("#{letter}%")).order(rank: :desc).
select(:text_name, :rank, :id, :deprecated).
pluck(:text_name, :id, :deprecated).uniq(&:first)

clades.map! do |name, id, deprecated|
{ name: name, id: id, deprecated: deprecated || false }
end
clades.sort_by! { |clade| clade[:name] }
clades.uniq { |clade| clade[:name] }
end
end
11 changes: 6 additions & 5 deletions app/classes/auto_complete/for_herbarium.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
class AutoComplete::ForHerbarium < AutoComplete::ByWord
def rough_matches(letter)
herbaria =
Herbarium.select(:code, :name).distinct.
Herbarium.select(:code, :name, :id).distinct.
where(Herbarium[:name].matches("#{letter}%").
or(Herbarium[:name].matches("% #{letter}%")).
or(Herbarium[:code].matches("#{letter}%"))).
order(
Arel.when(Herbarium[:code].is_null).then(Herbarium[:name]).
else(Herbarium[:code]).asc, Herbarium[:name].asc
).pluck(:code, :name)
).pluck(:code, :name, :id)

herbaria.map do |code, name|
code.empty? ? name : "#{code} - #{name}"
end.sort
herbaria.map! do |code, name, id|
{ name: code.empty? ? name : "#{code} - #{name}", id: id }
end
herbaria.sort_by! { |herb| herb[:name] }
end
end
19 changes: 14 additions & 5 deletions app/classes/auto_complete/for_location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,25 @@ def initialize(params)
self.reverse = (params[:format] == "scientific")
end

# Using observation.where gives the possibility of strings with no ID.
def rough_matches(letter)
matches =
locations =
Observation.select(:where).distinct.
where(Observation[:where].matches("#{letter}%").
or(Observation[:where].matches("% #{letter}%"))).pluck(:where) +
or(Observation[:where].matches("% #{letter}%"))).
pluck(:where, :location_id) +
Location.select(:name).distinct.
where(Location[:name].matches("#{letter}%").
or(Location[:name].matches("% #{letter}%"))).pluck(:name)
or(Location[:name].matches("% #{letter}%"))).pluck(:name, :id)

matches.map! { |m| Location.reverse_name(m) } if reverse
matches.sort.uniq
# matches without id are "where" strings only.
# give them an id: 0, and sort by unique name
locations.map! do |loc, id|
format = reverse ? Location.reverse_name(loc) : loc
{ name: format, id: id.nil? ? 0 : id }
end
# Sort by name and prefer those with a non-zero ID
locations.sort_by! { |loc| [loc[:name], -loc[:id]] }
locations.uniq { |loc| loc[:name] }
end
end
20 changes: 15 additions & 5 deletions app/classes/auto_complete/for_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

class AutoComplete::ForName < AutoComplete::ByString
def rough_matches(letter)
# (this sort puts genera and higher on top, everything else
# on bottom, and sorts alphabetically within each group)
Name.with_correct_spelling.select(:text_name).distinct.
where(Name[:text_name].matches("#{letter}%")).
pluck(:text_name).sort_by { |x| (x.match?(" ") ? "b" : "a") + x }.uniq
names = Name.with_correct_spelling.
select(:text_name, :id, :deprecated).distinct.
where(Name[:text_name].matches("#{letter}%")).
pluck(:text_name, :id, :deprecated)

names.map! do |name, id, deprecated|
dep_string = deprecated.nil? ? "false" : deprecated.to_s
{ name: name, id: id, deprecated: dep_string }
end
# This sort puts genera and higher on top, everything else on bottom,
# and sorts alphabetically within each group, and non-deprecated dups first
names.sort_by! do |name|
[(name[:name].match?(" ") ? "b" : "a") + name[:name], name[:deprecated]]
end
names.uniq { |name| name[:name] }
end
end
10 changes: 6 additions & 4 deletions app/classes/auto_complete/for_project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

class AutoComplete::ForProject < AutoComplete::ByWord
def rough_matches(letter)
Project.select(:title).distinct.
where(Project[:title].matches("#{letter}%").
or(Project[:title].matches("% #{letter}%"))).
order(title: :asc).pluck(:title)
projects = Project.select(:title, :id).distinct.
where(Project[:title].matches("#{letter}%").
or(Project[:title].matches("% #{letter}%"))).
order(title: :asc).pluck(:title, :id)

projects.map! { |name, id| { name: name, id: id } }
end
end
14 changes: 11 additions & 3 deletions app/classes/auto_complete/for_region.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ def initialize(params)
self.reverse = (params[:format] == "scientific")
end

# Using observation.where gives the possibility of strings with no ID.
# Trying to match "region" means matching the end of the postal format string.
# "scientific" format users will have the country first, so reverse words
def rough_matches(words)
words = Location.reverse_name(words) if reverse
matches = Observation.in_region(words).pluck(:where)
regions = Observation.in_region(words).pluck(:where, :location_id)

matches.map! { |m| Location.reverse_name(m) } if reverse
matches.sort.uniq
regions.map! do |reg, id|
format = reverse ? Location.reverse_name(reg) : loc
{ name: format, id: id.nil? ? 0 : id }
end
# Sort by name and prefer those with a non-zero ID
regions.sort_by! { |reg| [reg[:name], -reg[:id]] }
regions.uniq { |reg| reg[:name] }
end
end
10 changes: 6 additions & 4 deletions app/classes/auto_complete/for_species_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

class AutoComplete::ForSpeciesList < AutoComplete::ByWord
def rough_matches(letter)
SpeciesList.select(:title).distinct.
where(SpeciesList[:title].matches("#{letter}%").
or(SpeciesList[:title].matches("% #{letter}%"))).
order(title: :asc).pluck(:title)
lists = SpeciesList.select(:title, :id).distinct.
where(SpeciesList[:title].matches("#{letter}%").
or(SpeciesList[:title].matches("% #{letter}%"))).
order(title: :asc).pluck(:title, :id)

lists.map! { |name, id| { name:, id: } }
end
end
12 changes: 7 additions & 5 deletions app/classes/auto_complete/for_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

class AutoComplete::ForUser < AutoComplete::ByString
def rough_matches(letter)
users = User.select(:login, :name).distinct.
users = User.select(:login, :name, :id).distinct.
where(User[:login].matches("#{letter}%").
or(User[:name].matches("#{letter}%")).
or(User[:name].matches("% #{letter}%"))).
order(login: :asc).pluck(:login, :name)
order(login: :asc).pluck(:login, :name, :id)

users.map do |login, name|
name.empty? ? login : "#{login} <#{name}>"
end.sort
users.map! do |login, name, id|
user_name = name.empty? ? login : "#{login} <#{name}>"
{ name: user_name, id: id }
end
users.sort_by! { |user| user[:name] }
end
end
11 changes: 4 additions & 7 deletions app/controllers/autocompleters_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@ def new
private

def auto_complete_results
case @type
when "location", "location_containing", "region"
params[:format] = @user&.location_format
when "herbarium"
params[:user_id] = @user&.id
end
# add useful params that the controller knows about
params[:format] = @user&.location_format
params[:user_id] = @user&.id

::AutoComplete.subclass(@type).new(params).matching_strings
::AutoComplete.subclass(@type).new(params).matching_records
end

# callback on `around_action`
Expand Down
Loading

0 comments on commit 5b03dd5

Please sign in to comment.