Class: Queries::Person::Filter

Inherits:
Query::Filter show all
Includes:
Concerns::DataAttributes, Concerns::Notes, Concerns::Tags
Defined in:
lib/queries/person/filter.rb

Constant Summary collapse

PARAMS =
[
  :active_after_year,
  :active_before_year,
  :born_after_year,
  :born_before_year,
  :died_after_year,
  :died_before_year,
  :except_project_id,
  :first_name,
  :first_name_like,
  :last_name,
  :last_name_like,
  :last_name_starts_with,
  :levenshtein_cuttoff,
  :name,
  :only_project_id,
  :person_id,
  :prefix,
  :regex, # !! DO NOT EXPOSE TO EXTERNAL API
  :repeated_total,
  :suffix,
  :use_max,
  :use_min,

  exact: [],
  except_project_id: [],
  except_role: [],
  only_project_id: [],
  person_id: [],
  role: [],
  with: [],
  without: [],
].freeze

Constants inherited from Query::Filter

Query::Filter::FILTER_QUERIES, Query::Filter::SUBQUERIES

Instance Attribute Summary collapse

Attributes inherited from Query::Filter

#anatomical_part_query, #api, #asserted_distribution_query, #biological_association_query, #biological_associations_graph_query, #collecting_event_query, #collection_object_query, #content_query, #controlled_vocabulary_term_query, #conveyance_query, #data_attribute_query, #depiction_query, #descriptor_query, #document_query, #dwc_occurrence_query, #extract_query, #field_occurrence_query, #image_query, #loan_query, #object_global_id, #observation_query, #order_by, #otu_query, #page, #paginate, #params, #per, #person_query, #project_id, #recent, #recent_target, #roll_call, #sound_query, #taxon_name_query, #taxon_name_relationship_query, #venn, #venn_ignore_pagination, #venn_mode

Attributes inherited from Query

#query_string, #terms

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Query::Filter

#all, #all_and_clauses, #all_merge_clauses, #annotator_and_clauses, #annotator_merge_clauses, annotator_params, api_excluded_params, #apply_venn, #attribute_exact_facet, base_filter, base_query_name, base_query_to_h, #deep_permit, #disable_paging, included_annotator_facets, instantiated_base_filter, inverted_subqueries, #model_id_facet, #object_global_id_facet, #only_project?, #only_project_or_less?, #paging_state, params, #permitted_params, #process_url_into_params, query_name, #set_nested_queries, #set_paging, set_paging, #shared_and_clauses, #subquery_vector, #target_and_clauses, #venn_query

Methods inherited from Query

#alphabetic_strings, #alphanumeric_strings, base_name, #base_name, #base_query, #build_terms, #cached_facet, #end_wildcard, #levenshtein_distance, #match_ordered_wildcard_pieces_in_cached, #no_terms?, referenced_klass, #referenced_klass, #referenced_klass_except, #referenced_klass_intersection, #referenced_klass_union, #start_and_end_wildcard, #start_wildcard, #table, #wildcard_pieces

Constructor Details

#initialize(query_params = {}) ⇒ Filter

Returns a new instance of Filter.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/queries/person/filter.rb', line 164

def initialize(query_params = {})
  super

  @active_after_year = params[:active_after_year]
  @active_before_year = params[:active_before_year]
  @born_after_year = params[:born_after_year]
  @born_before_year = params[:born_before_year]
  @died_after_year = params[:died_after_year]
  @died_before_year = params[:died_before_year]
  @exact = params[:exact]
  @except_project_id = params[:except_project_id]
  @except_role = params[:except_role]
  @first_name = params[:first_name]
  @first_name_like = params[:first_name_like]
  @last_name = params[:last_name]
  @last_name_like = params[:last_name_like]
  @last_name_starts_with = params[:last_name_starts_with]
  @levenshtein_cuttoff = params[:levenshtein_cuttoff]
  @name = params[:name]
  @only_project_id = params[:only_project_id]
  @person_id = params[:person_id]
  @regex = params[:regex]
  @repeated_total = params[:repeated_total]
  @role = params[:role]
  @use_max = params[:use_max]
  @use_min = params[:use_min]
  @with = params[:with]
  @without = params[:without]

  set_tags_params(params)
  set_data_attributes_params(params)
  set_notes_params(params)
end

Instance Attribute Details

#active_after_yearString?

Returns:

  • (String, nil)


47
48
49
# File 'lib/queries/person/filter.rb', line 47

def active_after_year
  @active_after_year
end

#active_before_yearString?

Returns:

  • (String, nil)


50
51
52
# File 'lib/queries/person/filter.rb', line 50

def active_before_year
  @active_before_year
end

#born_after_yearString?

Returns:

  • (String, nil)


53
54
55
# File 'lib/queries/person/filter.rb', line 53

def born_after_year
  @born_after_year
end

#born_before_yearString?

Returns:

  • (String, nil)


56
57
58
# File 'lib/queries/person/filter.rb', line 56

def born_before_year
  @born_before_year
end

#died_after_yearString?

Returns:

  • (String, nil)


59
60
61
# File 'lib/queries/person/filter.rb', line 59

def died_after_year
  @died_after_year
end

#died_before_yearString?

Returns:

  • (String, nil)


62
63
64
# File 'lib/queries/person/filter.rb', line 62

def died_before_year
  @died_before_year
end

#exactArray

Returns values are attributes that should be wildcarded:

last_name, first_name,  suffix, prefix, name

When name then matches cached.

Returns:

  • (Array)

    values are attributes that should be wildcarded:

    last_name, first_name,  suffix, prefix, name
    

    When name then matches cached



76
77
78
# File 'lib/queries/person/filter.rb', line 76

def exact
  @exact
end

#except_project_idArray

Returns only return people with roles in this project(s) or roles through Sources in ProjectSources.

Returns:

  • (Array)

    only return people with roles in this project(s) or roles through Sources in ProjectSources



66
67
68
# File 'lib/queries/person/filter.rb', line 66

def except_project_id
  @except_project_id
end

#except_roleArray

Returns Exclude all People linked by this Role.

Parameters:

  • role (Array, String)

    A valid role name like 'Author' or array like ['TaxonDeterminer', 'SourceEditor']. See Role descendants.

Returns:

  • (Array)

    Exclude all People linked by this Role



82
83
84
# File 'lib/queries/person/filter.rb', line 82

def except_role
  @except_role
end

#first_nameString?

Returns also matches any AlternateValue.

Returns:

  • (String, nil)

    also matches any AlternateValue



86
87
88
# File 'lib/queries/person/filter.rb', line 86

def first_name
  @first_name
end

#first_name_likeString?

Returns Matches first_name using initial-expansion and part-sequence comparison. Each dot-or-space-delimited part of the input is matched positionally: a single-character part (initial) matches any word starting with that letter; a multi-character part matches exactly or as a bare initial. E.g. 'J.' matches 'John'; 'John K.' matches 'J. K.' but not 'Jack K.'. Also matches any AlternateValue.

Returns:

  • (String, nil)

    Matches first_name using initial-expansion and part-sequence comparison. Each dot-or-space-delimited part of the input is matched positionally: a single-character part (initial) matches any word starting with that letter; a multi-character part matches exactly or as a bare initial. E.g. 'J.' matches 'John'; 'John K.' matches 'J. K.' but not 'Jack K.'. Also matches any AlternateValue.



95
96
97
# File 'lib/queries/person/filter.rb', line 95

def first_name_like
  @first_name_like
end

#last_nameString?

Returns also matches any AlternateValue.

Returns:

  • (String, nil)

    also matches any AlternateValue



99
100
101
# File 'lib/queries/person/filter.rb', line 99

def last_name
  @last_name
end

#last_name_likeString?

Returns Matches last_name with word-level subset logic to handle maiden name variations. Forward: every word of the input appears as a whole word in the stored value, e.g. 'Smith' matches 'Smith Jones'. Backward: the stored last_name appears as a whole word in the input, e.g. input 'Smith Jones' matches stored 'Smith'. Also matches any AlternateValue.

Returns:

  • (String, nil)

    Matches last_name with word-level subset logic to handle maiden name variations. Forward: every word of the input appears as a whole word in the stored value, e.g. 'Smith' matches 'Smith Jones'. Backward: the stored last_name appears as a whole word in the input, e.g. input 'Smith Jones' matches stored 'Smith'. Also matches any AlternateValue.



108
109
110
# File 'lib/queries/person/filter.rb', line 108

def last_name_like
  @last_name_like
end

#last_name_starts_withObject

Returns the value of attribute last_name_starts_with.



110
111
112
# File 'lib/queries/person/filter.rb', line 110

def last_name_starts_with
  @last_name_starts_with
end

#levenshtein_cuttoffInteger?

Returns matches cached, records less than this edit distance are returned !! requires name or is ignored.

Returns:

  • (Integer, nil)

    matches cached, records less than this edit distance are returned !! requires name or is ignored



115
116
117
# File 'lib/queries/person/filter.rb', line 115

def levenshtein_cuttoff
  @levenshtein_cuttoff
end

#nameString?

Returns where against cached.

Returns:

  • (String, nil)

    where against cached



119
120
121
# File 'lib/queries/person/filter.rb', line 119

def name
  @name
end

#only_project_idArray

Returns only return people with roles in this project(s) or roles through Sources in ProjectSources.

Returns:

  • (Array)

    only return people with roles in this project(s) or roles through Sources in ProjectSources



70
71
72
# File 'lib/queries/person/filter.rb', line 70

def only_project_id
  @only_project_id
end

#person_idArray

Returns:

  • (Array)


44
45
46
# File 'lib/queries/person/filter.rb', line 44

def person_id
  @person_id
end

#prefixString?

Returns also matches any AlternateValue.

Returns:

  • (String, nil)

    also matches any AlternateValue



123
124
125
# File 'lib/queries/person/filter.rb', line 123

def prefix
  @prefix
end

#regexString?

Returns a regular expression, Postgres compatible, matches against cached.

Returns:

  • (String, nil)

    a regular expression, Postgres compatible, matches against cached



131
132
133
# File 'lib/queries/person/filter.rb', line 131

def regex
  @regex
end

#repeated_totalString?

Returns the number of times this name must be an identical match must be 2 or higher or will be ignored.

Returns:

  • (String, nil)

    the number of times this name must be an identical match must be 2 or higher or will be ignored



136
137
138
# File 'lib/queries/person/filter.rb', line 136

def repeated_total
  @repeated_total
end

#roleArray

Parameters:

  • role (Array, String)

    A valid role name like 'Author' or array like ['TaxonDeterminer', 'SourceEditor']. See Role descendants.

Returns:

  • (Array)


141
142
143
# File 'lib/queries/person/filter.rb', line 141

def role
  @role
end

#suffixString?

Returns also matches any AlternateValue.

Returns:

  • (String, nil)

    also matches any AlternateValue



127
128
129
# File 'lib/queries/person/filter.rb', line 127

def suffix
  @suffix
end

#use_maxString?

Returns the maximum number of roles the Person must be in, further scoped to only counting role when provided.

Returns:

  • (String, nil)

    the maximum number of roles the Person must be in, further scoped to only counting role when provided



145
146
147
# File 'lib/queries/person/filter.rb', line 145

def use_max
  @use_max
end

#use_minString?

Returns the minimum number of roles the Person must be in, further scoped to only counting role when provided.

Returns:

  • (String, nil)

    the minimum number of roles the Person must be in, further scoped to only counting role when provided



149
150
151
# File 'lib/queries/person/filter.rb', line 149

def use_min
  @use_min
end

#withArray

Parameters:

  • with (Array of strings)

    legal values are first_name, prefix, suffix only return names where the field provided is not nil

Returns:

  • (Array)


155
156
157
# File 'lib/queries/person/filter.rb', line 155

def with
  @with
end

#withoutArray

Parameters:

  • without (Array of strings)

    legal values are first_name, prefix, suffix only return names where the field provided is nil

Returns:

  • (Array)


161
162
163
# File 'lib/queries/person/filter.rb', line 161

def without
  @without
end

Class Method Details

.api_except_paramsObject



198
199
200
# File 'lib/queries/person/filter.rb', line 198

def self.api_except_params
  [:regex]
end

Instance Method Details

#active_after_year_facetObject



259
260
261
262
263
# File 'lib/queries/person/filter.rb', line 259

def active_after_year_facet
  return nil if active_after_year.nil?
  table[:year_active_start].gt(active_after_year)
    .or(table[:year_active_end].gt(active_after_year))
end

#active_before_year_facetObject



265
266
267
268
269
# File 'lib/queries/person/filter.rb', line 265

def active_before_year_facet
  return nil if active_before_year.nil?
  table[:year_active_start].lt(active_before_year)
    .or(table[:year_active_end].lt(active_before_year))
end

#and_clausesObject



504
505
506
507
508
509
510
511
512
513
514
515
516
517
# File 'lib/queries/person/filter.rb', line 504

def and_clauses
  clauses = [
    active_after_year_facet,
    active_before_year_facet,
    born_after_year_facet,
    born_before_year_facet,
    died_after_year_facet,
    died_before_year_facet,
    last_name_starts_with_facet,
    name_facet,
    with_facet,
    without_facet,
  ]
end

#born_after_year_facetObject



239
240
241
242
# File 'lib/queries/person/filter.rb', line 239

def born_after_year_facet
  return nil if born_after_year.nil?
  table[:year_born].gt(born_after_year)
end

#born_before_year_facetObject



244
245
246
247
# File 'lib/queries/person/filter.rb', line 244

def born_before_year_facet
  return nil if born_before_year.nil?
  table[:year_born].lt(born_before_year)
end

#build_first_name_like_pattern(input) ⇒ Object

Builds a PostgreSQL regex pattern for positional initial-expansion matching. Each part of the input is matched in sequence:

single char -> matches any word starting with that letter
(e.g. 'j' -> 'j\w*\.?')
multiple chars -> matches exactly OR as a bare initial
(e.g. 'john' -> '(?:john|j\.?)')

Parts are joined by a flexible whitespace/period separator. Trailing name parts (e.g. middle names absent from input) are allowed via '(\s.*)?$'. When all input parts are full names (no initials), each part after the first is optional, so e.g. 'John Stuart' also matches stored 'J.' or 'J. S.'. When any input part is an initial, all parts are required (the caller is being explicit).



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/queries/person/filter.rb', line 322

def build_first_name_like_pattern(input)
  parts = input.downcase.gsub(/[.\-]/, ' ').split.reject(&:empty?)
  return nil if parts.empty?

  regex_parts = parts.map do |part|
    if part.length == 1
      "#{::Regexp.escape(part)}\\w*\\.?"
    else
      # ('?:' is for non-matching grouping)
      "(?:#{::Regexp.escape(part)}|#{::Regexp.escape(part[0])}\\.?)"
    end
  end

  if parts.length > 1 && parts.all? { |p| p.length > 1 }
    # Build right-to-left: each non-first part becomes an optional
    # continuation so a stored value with fewer parts (e.g. 'J.') still
    # matches.
    # Separator is zero-or-more so fused forms (e.g. 'Yalin' for 'Ya-Lin')
    # also match.
    tail = "([\\s.\\-]*)?"
    regex_parts.reverse.each_with_index do |rp, i|
      tail = "#{rp}#{tail}"
      tail = "([\\s.\\-]*#{tail})?" unless i == regex_parts.length - 1
    end
    "^#{tail}$"
  else
    "^#{regex_parts.join('[\\s.\\-]+')}([\\s.\\-].*)?$"
  end
end

#died_after_year_facetObject



249
250
251
252
# File 'lib/queries/person/filter.rb', line 249

def died_after_year_facet
  return nil if died_after_year.nil?
  table[:year_died].gt(died_after_year)
end

#died_before_year_facetObject



254
255
256
257
# File 'lib/queries/person/filter.rb', line 254

def died_before_year_facet
  return nil if died_before_year.nil?
  table[:year_died].lt(died_before_year)
end

#except_project_id_facetObject



487
488
489
490
491
492
493
494
495
496
497
# File 'lib/queries/person/filter.rb', line 487

def except_project_id_facet
  return nil if except_project_id.empty?

  w1 = role_table[:project_id].not_in(except_project_id)
  w2 = ::ProjectSource.arel_table[:project_id].not_in(except_project_id)

  a = ::Person.joins(:roles).where(w1.to_sql)
  b = ::Person.joins(sources: [:project_sources]).where( w2.to_sql)

  referenced_klass_union([a,b])
end

#except_role_facetObject



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/queries/person/filter.rb', line 404

def except_role_facet
  return nil if except_role.empty?
  #  ::Person.left_outer_joins(:roles)
  #    .where( roles: {person_id: nil})
  #    .or( ::Person.left_outer_joins(:roles).where.not( role_table[:type].in(except_role)) )
  #    .distinct

  # ::Person.joins("LEFT JOIN roles on roles.person_id = people.id AND roles.type IN (#{except_role.collect{|r| "'#{r}'"}.join(',')})")
  #   .where( roles: {id: nil})
  #   .distinct

  ::Person.joins(
    table.join(role_table, Arel::Nodes::OuterJoin).on(
      table[:id].eq(role_table[:person_id]).and(role_table[:type]).in(except_role)
    ).join_sources
  ).merge(
    ::Role.where(id: nil)
  ).distinct
end

#first_name_like_facetObject



352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/queries/person/filter.rb', line 352

def first_name_like_facet
  return nil if first_name_like.blank?

  pattern = build_first_name_like_pattern(first_name_like)
  return nil if pattern.nil?

  ::Person.left_outer_joins(:alternate_values)
    .where(
      "people.first_name ~* ? OR (alternate_values.alternate_value_object_attribute = 'first_name' AND alternate_values.value ~* ?)",
      pattern, pattern
    )
    .distinct
end

#last_name_like_facetObject



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/queries/person/filter.rb', line 366

def last_name_like_facet
  return nil if last_name_like.blank?

  words = last_name_like.downcase.gsub('-', ' ').strip.split(/\s+/).reject(&:empty?)
  return nil if words.empty?

  # Forward: every input word must appear as a whole word in the stored
  # value.
  # \m and \M are PostgreSQL word-boundary markers, ~* is case-insensitive.
  forward_clauses = words.map { "(people.last_name ~* ? OR (alternate_values.alternate_value_object_attribute = 'last_name' AND alternate_values.value ~* ?))" }
  forward_values  = words.flat_map { |w|
    pat = "\\m#{::Regexp.escape(w)}\\M"
    [pat, pat]
  }

  q = ::Person.left_outer_joins(:alternate_values)
  forward = q.where(forward_clauses.join(' AND '), *forward_values)

  return forward.distinct if words.length == 1

  # Multi-word input: also add backward direction — the stored last_name
  # exactly equals one of the input words, e.g. stored 'Smith' is found
  # by input 'Smith Jones'.
  backward_clause = "LOWER(people.last_name) = ANY(ARRAY[#{words.map { '?' }.join(',')}])"

  forward.or(q.where(backward_clause, *words)).distinct
end

#last_name_starts_with_facetObject



394
395
396
397
# File 'lib/queries/person/filter.rb', line 394

def last_name_starts_with_facet
  return nil if last_name_starts_with.blank? || levenshtein_cuttoff.present?
  table[:last_name].matches(last_name_starts_with + '%')
end

#levenshtein_facetObject



471
472
473
474
475
476
# File 'lib/queries/person/filter.rb', line 471

def levenshtein_facet
  return nil unless levenshtein_cuttoff && (name.present?)
  ::Person.where(
    levenshtein_distance(:cached, name).lteq(levenshtein_cuttoff).to_sql
  )
end

#merge_clausesObject



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/queries/person/filter.rb', line 519

def merge_clauses
  [
    except_project_id_facet,
    except_role_facet,
    first_name_like_facet,
    last_name_like_facet,
    levenshtein_facet,
    name_part_facet(:first_name),
    name_part_facet(:last_name),
    name_part_facet(:prefix),
    name_part_facet(:suffix),
    only_project_id_facet,
    regex_facet,
    repeated_total_facet,
    role_facet,
    use_facet,
  ]
end

#name_facetObject



299
300
301
302
303
304
305
306
# File 'lib/queries/person/filter.rb', line 299

def name_facet
  return nil if name.nil? || levenshtein_cuttoff.present?
  if exact.include?('name')
    table[:cached].eq(name)
  else
    table[:cached].matches('%' + name + '%')
  end
end

#name_part_facet(part = :last_name) ⇒ Object



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/queries/person/filter.rb', line 271

def name_part_facet(part = :last_name)
  v = send(part)

  return nil if v.nil?

  q = ::Person.left_outer_joins(:alternate_values)
  a = ::AlternateValue.arel_table

  w1, w2 = nil, nil

  if exact.include?(part.to_s)
    w = '%' + v + '%'
    w1 = table[part].matches(w)
    w2 = a[:value].matches(w)
  else
    w1 = table[part].eq(v)
    w2 = a[:value].eq(v)
  end

  q.where( w1.or(w2).to_sql ).distinct
end

#only_project_id_facetObject



478
479
480
481
482
483
484
485
# File 'lib/queries/person/filter.rb', line 478

def only_project_id_facet
  return nil if only_project_id.empty?

  a = ::Person.joins(:roles).where(roles: {project_id: only_project_id})
  b = ::Person.joins(sources: [:project_sources]).where( project_sources: {project_id: only_project_id})

  referenced_klass_union([a,b])
end

#project_id_facetObject

Applies specificly to model, there is no such thing in Person



500
501
502
# File 'lib/queries/person/filter.rb', line 500

def project_id_facet
  nil
end

#regex_facetObject



294
295
296
297
# File 'lib/queries/person/filter.rb', line 294

def regex_facet
  return nil if regex.blank?
  ::Person.where('cached ~* ?', regex)
end

#repeated_total_facetObject



424
425
426
427
428
429
# File 'lib/queries/person/filter.rb', line 424

def repeated_total_facet
  return nil if repeated_total.blank? || repeated_total.to_i < 2
  ::Person.where(
    cached: ::Person.select(:cached).group(:cached).having('COUNT(cached) > ?', repeated_total)
  )
end

#role_facetObject



399
400
401
402
# File 'lib/queries/person/filter.rb', line 399

def role_facet
  return nil if role.empty?
  ::Person.joins(:roles).where( role_table[:type].in(role) ).distinct
end

#role_tableArel::Table

Returns:

  • (Arel::Table)


227
228
229
# File 'lib/queries/person/filter.rb', line 227

def role_table
  ::Role.arel_table
end

#use_facetObject



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/queries/person/filter.rb', line 431

def use_facet
  return nil if (use_min.blank? && use_max.blank?)
  min_max = [use_min&.to_i, use_max&.to_i ].compact

  q = ::Person.joins(:roles)
    .group('people.id, roles.person_id')
    .having("COUNT(roles.person_id) >= #{min_max[0]}")

  if !role.empty?
    q = q.where(role_table[:type].in(role))
  end

  # Untested
  q = q.having("COUNT(roles.person_id) <= #{min_max[1]}") if min_max[1]

  ::Person.from('(' + q.to_sql + ') as people').distinct
end

#with_facetObject



449
450
451
452
453
454
455
456
457
458
# File 'lib/queries/person/filter.rb', line 449

def with_facet
  return nil if with.empty?
  a = with.shift
  q = table[a.to_sym].not_eq(nil)

  with.each do |f|
    q = q.and(table[f.to_sym].not_eq(nil))
  end
  q
end

#without_facetObject



460
461
462
463
464
465
466
467
468
469
# File 'lib/queries/person/filter.rb', line 460

def without_facet
  return nil if without.empty?
  a = without.shift
  q = table[a.to_sym].eq(nil)

  without.each do |f|
    q = q.and(table[f.to_sym].eq(nil))
  end
  q
end