Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
47b935a
removed legacy Solr core names and updated to the new collection names
mdorf Apr 1, 2026
58b9413
Gemfile and Gemfile.lock updates
mdorf Apr 1, 2026
90664c3
Merge branch 'develop' into feature/solrcloud-alias-indexing-codex
mdorf Apr 1, 2026
93635a3
Expand user search fields
jvendetti May 13, 2026
ae6eed3
Preserve username-only user search default
jvendetti May 13, 2026
98a6529
Update search collection test for bootstrap collections
mdorf May 14, 2026
3973a00
Merge branch 'feature/solrcloud-alias-indexing-codex' of github.com:n…
mdorf May 14, 2026
90ec0f1
Gemfile.lock update
mdorf May 14, 2026
0e2c21b
Support all user search fields shortcut
jvendetti May 15, 2026
d4e7165
Update Solr alias dependency revisions
mdorf May 19, 2026
8906ab2
Skip minitest reporters in RubyMine
jvendetti May 19, 2026
a2b2284
Fix some RuboCop warnings
jvendetti May 19, 2026
fed67fa
Ignore Serena project metadata
jvendetti May 19, 2026
77e1ac0
Update API Solr alias dependency revisions
mdorf May 19, 2026
e6321df
Update API read-only alias dependency revisions
mdorf May 19, 2026
1088ecd
Refactor user search field parsing
jvendetti May 19, 2026
5aa9d49
Merge pull request #226 from ncbo/feature/expand-user-search/225
jvendetti May 19, 2026
d243b1e
Merge branch 'develop' into feature/solrcloud-alias-indexing-codex
mdorf May 19, 2026
0c5d519
Merge pull request #207 from ncbo/feature/solrcloud-alias-indexing-codex
mdorf May 19, 2026
95124b0
Gemfile & Gemfile.lock update
mdorf May 19, 2026
3fc1032
Merge branch 'master' into develop
mdorf May 21, 2026
1884b1d
Gemfile & Gemfile.lock update
mdorf May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ test/data/ontology_files/repo/**/*
# Claude Code
.claude/

# Serena
.serena/

*.swp

config/environments/console.rb
Expand Down
12 changes: 6 additions & 6 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ gem 'rack-contrib'
gem 'kramdown'

# NCBO gems (can be from a local dev path or from rubygems/git)
gem 'goo', github: 'ncbo/goo', branch: 'main'
gem 'sparql-client', github: 'ncbo/sparql-client', branch: 'main'
gem 'ncbo_annotator', github: 'ncbo/ncbo_annotator', branch: 'master'
gem 'ncbo_cron', github: 'ncbo/ncbo_cron', branch: 'master'
gem 'ncbo_ontology_recommender', github: 'ncbo/ncbo_ontology_recommender', branch: 'master'
gem 'ontologies_linked_data', github: 'ncbo/ontologies_linked_data', branch: 'master'
gem 'goo', github: 'ncbo/goo', branch: 'development'
gem 'sparql-client', github: 'ncbo/sparql-client', branch: 'development'
gem 'ncbo_annotator', github: 'ncbo/ncbo_annotator', branch: 'develop'
gem 'ncbo_cron', github: 'ncbo/ncbo_cron', branch: 'develop'
gem 'ncbo_ontology_recommender', github: 'ncbo/ncbo_ontology_recommender', branch: 'develop'
gem 'ontologies_linked_data', github: 'ncbo/ontologies_linked_data', branch: 'develop'

group :development do
gem 'shotgun', github: 'syphax-bouazzouni/shotgun', branch: 'master'
Expand Down
22 changes: 11 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ GIT

GIT
remote: https://github.com/ncbo/goo.git
revision: ee72f43daa37f02362da1b747f037c442cfbf112
branch: main
revision: 0187c6c0ba3871fa8b67055c1a1adfdea38b2189
branch: development
specs:
goo (0.0.2)
addressable (~> 2.8)
Expand All @@ -26,8 +26,8 @@ GIT

GIT
remote: https://github.com/ncbo/ncbo_annotator.git
revision: b216bdde2de471fc0f851556ffa2327333516e79
branch: master
revision: 5243ea8db4302ca756c66bf50233e88606830326
branch: develop
specs:
ncbo_annotator (0.0.1)
goo
Expand All @@ -37,8 +37,8 @@ GIT

GIT
remote: https://github.com/ncbo/ncbo_cron.git
revision: 18a6c183d5dd59b3adda58af90bab334fb911537
branch: master
revision: 91fb6b34e6f0bb827a6da721b083640b823d1083
branch: develop
specs:
ncbo_cron (0.0.1)
dante
Expand All @@ -55,8 +55,8 @@ GIT

GIT
remote: https://github.com/ncbo/ncbo_ontology_recommender.git
revision: e63da099a5d69cf08d2c1c0d58b2f80bd5d41bb0
branch: master
revision: 722a70afe43daea2be96ba46850d6c917e9d4132
branch: develop
specs:
ncbo_ontology_recommender (0.0.1)
goo
Expand All @@ -66,8 +66,8 @@ GIT

GIT
remote: https://github.com/ncbo/ontologies_linked_data.git
revision: 32023829506089164a27da1e6aa05b901a019079
branch: master
revision: d4492a5cf76d78e74b677cc3f05c97dcbd53ccb7
branch: develop
specs:
ontologies_linked_data (0.0.1)
activesupport
Expand All @@ -89,7 +89,7 @@ GIT
GIT
remote: https://github.com/ncbo/sparql-client.git
revision: 2ac20b217bb7ad2b11305befe0ee77d75e44eac5
branch: main
branch: development
specs:
sparql-client (3.2.2)
net-http-persistent (~> 4.0, >= 4.0.2)
Expand Down
86 changes: 66 additions & 20 deletions helpers/users_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
module Sinatra
module Helpers
module UsersHelper
USER_SEARCH_FIELDS = %i[username email firstName lastName].freeze
DEFAULT_USER_SEARCH_FIELDS = %i[username].freeze
ALL_USER_SEARCH_FIELDS = 'all'.freeze

def get_users
attributes, page, size, order_by, _ = settings_params(LinkedData::Models::User)
query = User.where.include(attributes)

if params['search']
filter = Goo::Filter.new(:username).regex(params['search'])
query = query.filter(filter)
query = query.filter(user_search_filter(params['search']))
end

query = query.order_by(order_by || { username: :asc })
Expand All @@ -18,24 +21,67 @@ def get_users
query.all
end

def user_search_filter(term)
field, *remaining_fields = user_search_fields
filter = Goo::Filter.new(field).regex(term)
remaining_fields.each do |search_field|
filter = filter.or(Goo::Filter.new(search_field).regex(term))
end
filter
end

def user_search_fields
fields = requested_user_search_fields
return DEFAULT_USER_SEARCH_FIELDS if fields.empty?

validate_all_user_search_fields!(fields)
return USER_SEARCH_FIELDS if fields == [ALL_USER_SEARCH_FIELDS]

fields = fields.map(&:to_sym)
validate_known_user_search_fields!(fields)

fields
end

def requested_user_search_fields
params['search_fields'].to_s.split(',').map(&:strip).reject(&:empty?)
end

def validate_all_user_search_fields!(fields)
return unless fields.include?(ALL_USER_SEARCH_FIELDS) && fields != [ALL_USER_SEARCH_FIELDS]

error_message = "Unsupported search_fields: #{params['search_fields']}. "
error_message += 'Use all by itself or list individual fields'
error 400, error_message
end

def validate_known_user_search_fields!(fields)
unknown_fields = fields - USER_SEARCH_FIELDS
return if unknown_fields.empty?

unsupported_fields = unknown_fields.join(', ')
allowed_fields = USER_SEARCH_FIELDS.join(', ')
error 400, "Unsupported search_fields: #{unsupported_fields}. Allowed fields: #{allowed_fields}"
end

def filter_for_user_onts(obj)
return obj unless obj.is_a?(Enumerable)
return obj unless env["REMOTE_USER"]
return obj if env["REMOTE_USER"].customOntology.empty?
return obj if params["ignore_custom_ontologies"]
return obj unless env['REMOTE_USER']
return obj if env['REMOTE_USER'].customOntology.empty?
return obj if params['ignore_custom_ontologies']

user = env["REMOTE_USER"]
user = env['REMOTE_USER']

if obj.first.is_a?(LinkedData::Models::Ontology)
obj = obj.select {|o| user.custom_ontology_id_set.include?(o.id.to_s)}
obj = obj.select { |o| user.custom_ontology_id_set.include?(o.id.to_s) }
end

obj
end

def send_reset_token(email, username)
user = LinkedData::Models::User.where(email: email, username: username).include(LinkedData::Models::User.attributes).first
error 404, "User not found" unless user
error 404, 'User not found' unless user
reset_token = token(36)
user.resetToken = reset_token
user.resetTokenExpireTime = Time.now.to_i + 1.hours.to_i
Expand All @@ -46,22 +92,22 @@ def send_reset_token(email, username)
end

def token(len)
chars = ("a".."z").to_a + ("A".."Z").to_a + ("1".."9").to_a
token = ""
1.upto(len) { |i| token << chars[rand(chars.size-1)] }
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('1'..'9').to_a
token = ''
1.upto(len) { |i| token << chars[rand(chars.size - 1)] }
token
end

def reset_password(email, username, token)
user = LinkedData::Models::User.where(email: email, username: username).include(User.goo_attrs_to_load(includes_param) + [:resetToken, :passwordHash, :resetTokenExpireTime]).first

error 404, "User not found" unless user
error 404, 'User not found' unless user

user.show_apikey = true
token_accepted = token.eql?(user.resetToken)
if token_accepted
error 401, "Invalid password reset token" if user.resetTokenExpireTime.nil?
error 401, "The password reset token expired" if user.resetTokenExpireTime < Time.now.to_i
error 401, 'Invalid password reset token' if user.resetTokenExpireTime.nil?
error 401, 'The password reset token expired' if user.resetTokenExpireTime < Time.now.to_i
user.resetToken = nil
user.resetTokenExpireTime = nil
user.save(override_security: true) if user.valid?
Expand All @@ -72,20 +118,20 @@ def reset_password(email, username, token)
end

def oauth_authenticate(params)
access_token = params["access_token"]
provider = params["token_provider"]
access_token = params['access_token']
provider = params['token_provider']
user = LinkedData::Models::User.oauth_authenticate(access_token, provider)
error 401, "Access token invalid"if user.nil?
error 401, 'Access token invalid' if user.nil?
user
end

def login_password_authenticate(params)
user_id = params["user"]
user_password = params["password"]
user_id = params['user']
user_password = params['password']
user = User.find(user_id).include(User.goo_attrs_to_load(includes_param) + [:passwordHash]).first
authenticated = false
authenticated = user.authenticate(user_password) unless user.nil?
error 401, "Username/password combination invalid" unless authenticated
error 401, 'Username/password combination invalid' unless authenticated

user
end
Expand Down
4 changes: 2 additions & 2 deletions test/controllers/test_search_models_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def test_show_all_collection
get '/admin/search/collections'
assert last_response.ok?
res = MultiJson.load(last_response.body)
required = %w[ontology_data ontology_metadata prop_search_core1 term_search_core1]
allowed_extra = %w[term_search test_solr]
required = %w[ontology_data ontology_metadata property_search_bootstrap term_search_bootstrap]
allowed_extra = %w[property_search term_search test_solr]
collections = res["collections"]
assert_empty required - collections
assert_empty collections - (required + allowed_extra)
Expand Down
78 changes: 78 additions & 0 deletions test/controllers/test_users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ def before_suite
User.new(username: username, email: "#{username}@example.org", password: "pass_word").save
end

search_users = [
{ username: 'username_marker_account', email: 'search@example.org', firstName: 'Username', lastName: 'Search' },
{ username: 'email_account', email: 'mailmarker@example.org', firstName: 'Email', lastName: 'Search' },
{ username: 'first_name_account', email: 'search@example.org', firstName: 'given_marker', lastName: 'Search' },
{ username: 'last_name_account', email: 'search@example.org', firstName: 'Last', lastName: 'family_marker' }
]

@@users.concat(search_users.map do |user_params|
User.new(user_params.merge(password: 'pass_word')).save
end)

# Test data
@@username = "test_user"
end
Expand Down Expand Up @@ -60,6 +71,64 @@ def test_all_users_include_all_is_paged
assert users_page["collection"].all? { |u| u.key?("created") }
end

def test_search_users_by_username
users_page = search_users('username_marker')
usernames = users_page['collection'].map { |user| user['username'] }

assert_equal ['username_marker_account'], usernames
assert_equal 1, users_page['totalCount']
assert_equal 1, users_page['pageCount']
end

def test_search_users_defaults_to_username_only
users_page = search_users('mailmarker')

assert_equal [], users_page['collection']
assert_equal 0, users_page['totalCount']
assert_equal 0, users_page['pageCount']
end

def test_search_users_by_email
users_page = search_users('mailmarker', search_fields: 'all')
usernames = users_page['collection'].map { |user| user['username'] }

assert_equal ['email_account'], usernames
assert_equal 1, users_page['totalCount']
assert_equal 1, users_page['pageCount']
end

def test_search_users_by_first_name
users_page = search_users('given_marker', search_fields: 'all')
usernames = users_page['collection'].map { |user| user['username'] }

assert_equal ['first_name_account'], usernames
assert_equal 1, users_page['totalCount']
assert_equal 1, users_page['pageCount']
end

def test_search_users_by_last_name
users_page = search_users('family_marker', search_fields: 'all')
usernames = users_page['collection'].map { |user| user['username'] }

assert_equal ['last_name_account'], usernames
assert_equal 1, users_page['totalCount']
assert_equal 1, users_page['pageCount']
end

def test_search_users_no_match
users_page = search_users('not_a_search_match', search_fields: 'all')

assert_equal [], users_page['collection']
assert_equal 0, users_page['totalCount']
assert_equal 0, users_page['pageCount']
end

def test_search_users_rejects_all_with_other_fields
get '/users', search: 'mailmarker', search_fields: 'all,email'

assert_equal 400, last_response.status
end

def test_single_user
user = 'fred'
get "/users/#{user}"
Expand Down Expand Up @@ -243,6 +312,15 @@ def _delete_user(username)
LinkedData::Models::User.find(@@username).first&.delete
end

def search_users(term, search_fields: nil)
query_params = { search: term, pagesize: 100 }
query_params[:search_fields] = search_fields if search_fields

get '/users', query_params
assert last_response.ok?
MultiJson.load(last_response.body)
end

def _create_admin_user(apikey: nil)
user = {email: "#{@@username}@example.org", password: "pass_the_word", role: ['ADMINISTRATOR']}
_delete_user(@@username)
Expand Down
6 changes: 5 additions & 1 deletion test/test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
require 'oj'
require 'json-schema'
require 'minitest/reporters'
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(:color => true), Minitest::Reporters::MeanTimeReporter.new]
unless ENV['RM_INFO']
Minitest::Reporters.use! [
Minitest::Reporters::SpecReporter.new(color: true),
Minitest::Reporters::MeanTimeReporter.new]
end
MAX_TEST_REDIS_SIZE = 10_000

# Check to make sure you want to run if not pointed at localhost
Expand Down
Loading