Class: Identifier

Inherits:
ApplicationRecord show all
Includes:
Housekeeping, Shared::DualAnnotator, Shared::IsData, Shared::Labels, Shared::PolymorphicAnnotator
Defined in:
app/models/identifier.rb

Overview

An Identifier is the information that can be use to differentiate concepts. If an identifier differentiates individuals of all types it is “Global”. If an identifier differentiates individuals of one type, within a specific subset of that type, it is “Local”.

Local identifiers have a namespace, a string that preceeds the variable portion of the identifier.

Note this definition is presently very narrow, and that an identifier can in practice be used for a lot more than differentiation (i.e. it can often be resolved etc.).

!! Identifiers should always be created in the context of the the object they identify, see spec/lib/identifier_spec.rb for examples !!

See `build_cached_numeric_identifier`.
   This does account for identifiers like:
     123,123
     123,123.12
     123.12
     .12
   This does not account for identifiers like (though this could be hacked in if it becomes necessary by ordering alphanumerics into decimal additions to the float):
     123,123a
     123a
     123.123a

Direct Known Subclasses

Global, Local, Unknown

Defined Under Namespace

Classes: Global, Local, Unknown

Constant Summary

Constants included from Shared::DualAnnotator

Shared::DualAnnotator::ALWAYS_COMMUNITY

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar

Methods included from Shared::Labels

#labeled?

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods included from Shared::PolymorphicAnnotator

#annotated_object_is_persisted?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#cachedString

The full identifier, for display, i.e. namespace + identifier (local), or identifier (global).

Returns:

  • (String)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#cached_numeric_identifierFloat?

Returns If ‘identifier` contains a numeric string, then record this as a float. !! This should never be exposed, it’s used for internal next/previous options only.

Returns:

  • (Float, nil)

    If ‘identifier` contains a numeric string, then record this as a float. !! This should never be exposed, it’s used for internal next/previous options only.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#identifierString

The string identifying the object. Must be unique within the Namespace if provided. Same as rs.tdwg.org/dwc/terms/catalogNumber, but broadened in scope to be used for any data.

Returns:

  • (String)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#identifier_object_idString

The type of the identified object, used in a polymorphic relationship.

Returns:

  • (String)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#namespace_idInteger

The Namespace for this identifier.

Returns:

  • (Integer)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#project_idInteger

The project ID.

Returns:

  • (Integer)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

#typeString

The Rails STI subclass of this identifier.

Returns:

  • (String)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/identifier.rb', line 58

class Identifier < ApplicationRecord
  acts_as_list scope: [:project_id, :identifier_object_type, :identifier_object_id ], add_new_at: :top

  include Shared::DualAnnotator
  include Shared::PolymorphicAnnotator

  polymorphic_annotates('identifier_object')

  include Housekeeping # TODO: potential circular dependency constraint when this is before above.
  include Shared::Labels
  include Shared::IsData

  after_save :set_cached, unless: Proc.new {|n| errors.any? }

  belongs_to :namespace, inverse_of: :identifiers  # only applies to Identifier::Local, here for create purposes

  validates_presence_of :type, :identifier

  validates :identifier, presence: true

  # TODO: DRY to IsData? Test.
  scope :with_type_string, -> (base_string) {where('type LIKE ?', "#{base_string}")}

  scope :prefer, -> (type) { order(Arel.sql(<<~SQL)) }
    CASE WHEN identifiers.type = '#{type}' THEN 1 \
    WHEN identifiers.type != '#{type}' THEN 2 END ASC, \
    position ASC
  SQL

  scope :visible, -> (project_id) { where("identifiers.project_id = ? OR identifiers.type ILIKE 'Identifier::Global%'", project_id) }

  scope :local, -> {where("identifiers.type ILIKE 'Identifier::Local%'") }
  scope :global, -> {where("identifiers.type ILIKE 'Identifier::Global%'") }

  # @return [String, Identifer]
  def self.prototype_identifier(project_id, created_by_id)
    identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
    identifiers.empty? ? '12345678' : identifiers.last.identifier
  end

  # @return [String]
  def type_name
    self.class.name.demodulize.downcase
  end

  def is_local?
    false
  end

  def is_global?
    false
  end

  protected

  # See subclasses
  def build_cached
    nil
  end

  def build_cached_numeric_identifier
    return nil if is_global?
    if a = identifier.match(/\A[\d\.\,]+\z/)
      b = a.to_s.gsub(',', '')
      b.to_f
    else
      nil
    end
  end

  def set_cached
    update_columns(
      cached: build_cached,
      cached_numeric_identifier: build_cached_numeric_identifier
    )
  end
end

Class Method Details

.prototype_identifier(project_id, created_by_id) ⇒ String, Identifer

Returns:

  • (String, Identifer)


93
94
95
96
# File 'app/models/identifier.rb', line 93

def self.prototype_identifier(project_id, created_by_id)
  identifiers = Identifier.where(project_id:, created_by_id:).limit(1)
  identifiers.empty? ? '12345678' : identifiers.last.identifier
end

Instance Method Details

#build_cachedObject (protected)

See subclasses



114
115
116
# File 'app/models/identifier.rb', line 114

def build_cached
  nil
end

#build_cached_numeric_identifierObject (protected)



118
119
120
121
122
123
124
125
126
# File 'app/models/identifier.rb', line 118

def build_cached_numeric_identifier
  return nil if is_global?
  if a = identifier.match(/\A[\d\.\,]+\z/)
    b = a.to_s.gsub(',', '')
    b.to_f
  else
    nil
  end
end

#is_global?Boolean

Returns:

  • (Boolean)


107
108
109
# File 'app/models/identifier.rb', line 107

def is_global?
  false
end

#is_local?Boolean

Returns:

  • (Boolean)


103
104
105
# File 'app/models/identifier.rb', line 103

def is_local?
  false
end

#set_cachedObject (protected)



128
129
130
131
132
133
# File 'app/models/identifier.rb', line 128

def set_cached
  update_columns(
    cached: build_cached,
    cached_numeric_identifier: build_cached_numeric_identifier
  )
end

#type_nameString

Returns:

  • (String)


99
100
101
# File 'app/models/identifier.rb', line 99

def type_name
  self.class.name.demodulize.downcase
end