locabulary 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.fasterer.yml +4 -0
- data/.hound.yml +1 -0
- data/.started-issues +0 -0
- data/Gemfile +0 -3
- data/README.md +4 -0
- data/Rakefile +11 -1
- data/data/administrative_units.json +27 -2
- data/data/college.json +10 -10
- data/data/spec.json +24 -0
- data/data/underclass_level.json +1 -1
- data/lib/locabulary.rb +85 -93
- data/lib/locabulary/commands/active_hierarchical_roots_command.rb +78 -0
- data/lib/locabulary/commands/active_items_for_command.rb +58 -0
- data/lib/locabulary/commands/build_ordered_hierarchical_tree_command.rb +80 -0
- data/lib/locabulary/exceptions.rb +11 -8
- data/lib/locabulary/facet_wrapper_for_item.rb +58 -0
- data/lib/locabulary/item.rb +40 -0
- data/lib/locabulary/items.rb +6 -27
- data/lib/locabulary/items/administrative_unit.rb +9 -37
- data/lib/locabulary/items/base.rb +43 -3
- data/lib/locabulary/json_creator.rb +4 -5
- data/lib/locabulary/schema.rb +4 -4
- data/lib/locabulary/utility.rb +42 -0
- data/lib/locabulary/version.rb +1 -1
- data/locabulary.gemspec +3 -2
- data/script/build-multi-commit-message +2 -0
- data/script/close-issue +125 -0
- data/script/start-issue +174 -0
- metadata +43 -16
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'date'
|
|
3
|
+
require 'locabulary/items'
|
|
4
|
+
require 'locabulary/exceptions'
|
|
5
|
+
|
|
6
|
+
module Locabulary
|
|
7
|
+
module Commands
|
|
8
|
+
# Responsible for transforming the flat data for the given :predicate_name
|
|
9
|
+
# into a hierarchy.
|
|
10
|
+
class ActiveHierarchicalRootsCommand
|
|
11
|
+
def self.cache
|
|
12
|
+
@cache ||= {}
|
|
13
|
+
end
|
|
14
|
+
private_class_method :cache
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
def self.reset_cache!
|
|
18
|
+
@cache = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
# @since 0.5.0
|
|
23
|
+
#
|
|
24
|
+
# @param options [Hash]
|
|
25
|
+
# @option options [String] :predicate_name
|
|
26
|
+
# @option options [Date] :as_of (Date.today)
|
|
27
|
+
#
|
|
28
|
+
# @note A concession about the as_of; This is not a live Utility. The data has a
|
|
29
|
+
# low churn rate. And while the date is important, I'm not as concerned
|
|
30
|
+
# about the local controlled vocabulary exposing a date that has expired.
|
|
31
|
+
# When we next deploy the server changes, the deactivated will go away.
|
|
32
|
+
def self.call(options = {})
|
|
33
|
+
predicate_name = options.fetch(:predicate_name)
|
|
34
|
+
cache[predicate_name] ||= new(options).call
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(options = {})
|
|
38
|
+
@predicate_name = options.fetch(:predicate_name)
|
|
39
|
+
@as_of = options.fetch(:as_of) { Date.today }
|
|
40
|
+
@builder = Item.builder_for(predicate_name: predicate_name)
|
|
41
|
+
@utility_service = options.fetch(:utility_service) { default_utility_service }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call
|
|
45
|
+
items = []
|
|
46
|
+
hierarchy_graph_keys = {}
|
|
47
|
+
top_level_slugs = Set.new
|
|
48
|
+
utility_service.with_active_extraction_for(predicate_name, as_of) do |data|
|
|
49
|
+
item = builder.call(data.merge('predicate_name' => predicate_name))
|
|
50
|
+
items << item
|
|
51
|
+
top_level_slugs << item.root_slug
|
|
52
|
+
hierarchy_graph_keys[item.term_label] = item
|
|
53
|
+
end
|
|
54
|
+
associate_parents_and_childrens_for(hierarchy_graph_keys, items)
|
|
55
|
+
top_level_slugs.map { |slug| hierarchy_graph_keys.fetch(slug) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
attr_reader :predicate_name, :as_of, :builder, :utility_service
|
|
61
|
+
|
|
62
|
+
def default_utility_service
|
|
63
|
+
require 'locabulary/utility'
|
|
64
|
+
Utility
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def associate_parents_and_childrens_for(hierarchy_graph_keys, items)
|
|
68
|
+
items.each do |item|
|
|
69
|
+
begin
|
|
70
|
+
hierarchy_graph_keys.fetch(item.parent_term_label).add_child(item) unless item.parent_slugs.empty?
|
|
71
|
+
rescue KeyError => error
|
|
72
|
+
raise Exceptions::MissingHierarchicalParentError.new(predicate_name, error)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'locabulary/items'
|
|
4
|
+
require 'locabulary/utility'
|
|
5
|
+
|
|
6
|
+
module Locabulary
|
|
7
|
+
module Commands
|
|
8
|
+
# Responsible for extracting a non-hierarchical sorted array of Locabulary::Item for the given predicate_name
|
|
9
|
+
#
|
|
10
|
+
# @see Locabulary::Item
|
|
11
|
+
class ActiveItemsForCommand
|
|
12
|
+
def self.cache
|
|
13
|
+
@cache ||= {}
|
|
14
|
+
end
|
|
15
|
+
private_class_method :cache
|
|
16
|
+
|
|
17
|
+
# @api private
|
|
18
|
+
# @since 0.5.0
|
|
19
|
+
def self.reset_cache!
|
|
20
|
+
@cache = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @api private
|
|
24
|
+
# @since 0.5.0
|
|
25
|
+
#
|
|
26
|
+
# @param options [Hash]
|
|
27
|
+
# @option options [String] :predicate_name
|
|
28
|
+
# @option options [Date] :as_of (Date.today)
|
|
29
|
+
#
|
|
30
|
+
# @note A concession about the as_of; This is not a live Utility. The data has a
|
|
31
|
+
# low churn rate. And while the date is important, I'm not as concerned
|
|
32
|
+
# about the local controlled vocabulary exposing a date that has expired.
|
|
33
|
+
# When we next deploy the server changes, the deactivated will go away.
|
|
34
|
+
def self.call(options = {})
|
|
35
|
+
predicate_name = options.fetch(:predicate_name)
|
|
36
|
+
cache[predicate_name] ||= new(options).call
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(options = {})
|
|
40
|
+
@predicate_name = options.fetch(:predicate_name)
|
|
41
|
+
@as_of = options.fetch(:as_of) { Date.today }
|
|
42
|
+
@builder = Item.builder_for(predicate_name: predicate_name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call
|
|
46
|
+
collector = []
|
|
47
|
+
Utility.with_active_extraction_for(predicate_name, as_of) do |data|
|
|
48
|
+
collector << builder.call(data.merge('predicate_name' => predicate_name))
|
|
49
|
+
end
|
|
50
|
+
collector.sort
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_reader :predicate_name, :as_of, :builder
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'locabulary'
|
|
3
|
+
require 'locabulary/exceptions'
|
|
4
|
+
require 'locabulary/item'
|
|
5
|
+
require 'locabulary/facet_wrapper_for_item'
|
|
6
|
+
|
|
7
|
+
module Locabulary
|
|
8
|
+
module Commands
|
|
9
|
+
# Responsible for building a hierarchical tree from faceted items, and ordering the nodes as per the presentation sequence for the
|
|
10
|
+
# associated predicate_name.
|
|
11
|
+
class BuildOrderedHierarchicalTreeCommand
|
|
12
|
+
# @api private
|
|
13
|
+
# @since 0.5.0
|
|
14
|
+
#
|
|
15
|
+
# @param options [Hash]
|
|
16
|
+
# @option predicate_name [String]
|
|
17
|
+
# @option faceted_items [Array<#hits, #value>]
|
|
18
|
+
# @option faceted_item_hierarchy_delimiter [String]
|
|
19
|
+
#
|
|
20
|
+
# @return [Array<FacetWrapperForItem>]
|
|
21
|
+
def self.call(options = {})
|
|
22
|
+
new(options).call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(options = {})
|
|
26
|
+
@predicate_name = options.fetch(:predicate_name)
|
|
27
|
+
@faceted_items = options.fetch(:faceted_items)
|
|
28
|
+
@faceted_item_hierarchy_delimiter = options.fetch(:faceted_item_hierarchy_delimiter)
|
|
29
|
+
@builder = Item.builder_for(predicate_name: predicate_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call
|
|
33
|
+
items = []
|
|
34
|
+
hierarchy_graph_keys = {}
|
|
35
|
+
top_level_slugs = Set.new
|
|
36
|
+
faceted_items.each do |faceted_item|
|
|
37
|
+
item = build_item(faceted_item)
|
|
38
|
+
items << item
|
|
39
|
+
top_level_slugs << item.root_slug
|
|
40
|
+
hierarchy_graph_keys[item.term_label] = item
|
|
41
|
+
end
|
|
42
|
+
associate_parents_and_childrens_for(hierarchy_graph_keys, items)
|
|
43
|
+
top_level_slugs.map { |slug| hierarchy_graph_keys.fetch(slug) }.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :builder, :predicate_name, :faceted_items, :faceted_item_hierarchy_delimiter
|
|
49
|
+
|
|
50
|
+
def associate_parents_and_childrens_for(hierarchy_graph_keys, items)
|
|
51
|
+
items.each do |item|
|
|
52
|
+
begin
|
|
53
|
+
hierarchy_graph_keys.fetch(item.parent_term_label).add_child(item) unless item.parent_slugs.empty?
|
|
54
|
+
rescue KeyError => error
|
|
55
|
+
raise Exceptions::MissingHierarchicalParentError.new(predicate_name, error)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_item(faceted_node)
|
|
61
|
+
term_label = faceted_node.value
|
|
62
|
+
locabulary_item = find_locabulary_item(predicate_name: predicate_name, term_label: term_label)
|
|
63
|
+
if locabulary_item
|
|
64
|
+
FacetWrapperForItem.build_for_faceted_node_and_locabulary_item(faceted_node: faceted_node, locabulary_item: locabulary_item)
|
|
65
|
+
else
|
|
66
|
+
FacetWrapperForItem.build_for_faceted_node(faceted_node: faceted_node, predicate_name: predicate_name, term_label: term_label)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_locabulary_item(*args)
|
|
71
|
+
Locabulary.item_for(*args)
|
|
72
|
+
rescue Exceptions::ItemNotFoundError, Exceptions::MissingPredicateNameError
|
|
73
|
+
# Either the predicate name didn't exist or the item didn't exist for the given predicate name
|
|
74
|
+
# Given that we are building from an alternate source, this is an acceptable error. It is possible
|
|
75
|
+
# that we want to consider a developer notification but not abort the whole process.
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -4,6 +4,17 @@ module Locabulary
|
|
|
4
4
|
class RuntimeError < ::RuntimeError
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
# There was a problem finding an item
|
|
8
|
+
class ItemNotFoundError < RuntimeError
|
|
9
|
+
def initialize(predicate_name, label)
|
|
10
|
+
super("Unable to find label=#{label.inspect} for predicate_name=#{predicate_name.inspect}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# An error occurred in attempting to find a given predicate_name in the data store
|
|
15
|
+
class MissingPredicateNameError < RuntimeError
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
# There is a problem with the hierarchy; A child is being defined without a defined parent.
|
|
8
19
|
class MissingHierarchicalParentError < RuntimeError
|
|
9
20
|
attr_reader :predicate_name, :error
|
|
@@ -13,13 +24,5 @@ module Locabulary
|
|
|
13
24
|
super("Expected #{predicate_name.inspect} to have a welformed tree. Error: #{error}")
|
|
14
25
|
end
|
|
15
26
|
end
|
|
16
|
-
|
|
17
|
-
# There is a problem with the hierarchy; Instead of a tree we have a multitude of trees
|
|
18
|
-
class TooManyHierarchicalRootsError < RuntimeError
|
|
19
|
-
attr_reader :predicate_name, :roots
|
|
20
|
-
def initialize(predicate_name, roots)
|
|
21
|
-
super("Expected fewer root slugs for #{predicate_name.inspect}. Roots encountered: #{roots.inspect}")
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
27
|
end
|
|
25
28
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require 'delegate'
|
|
3
|
+
|
|
4
|
+
module Locabulary
|
|
5
|
+
# A wrapper for a Locabulary::Items::Base that includes information from
|
|
6
|
+
# the SOLR Utility.
|
|
7
|
+
class FacetWrapperForItem < SimpleDelegator
|
|
8
|
+
# @api public
|
|
9
|
+
# @since 0.5.0
|
|
10
|
+
#
|
|
11
|
+
# In some cases, we may have a facet with a term_label that is not found in the existing data storage.
|
|
12
|
+
# Instead of throwing an exception, we can make a reasonable approximation for that item based on the given parameters.
|
|
13
|
+
#
|
|
14
|
+
# @see Locabulary::FacetedHierarchicalTreeSorter
|
|
15
|
+
#
|
|
16
|
+
# @param options [Hash]
|
|
17
|
+
# @option predicate_name [String]
|
|
18
|
+
# @option faceted_node [#qvalue, #value, #hits]
|
|
19
|
+
# @option term_label [String]
|
|
20
|
+
# @return Locabulary::FacetWrapperForItem
|
|
21
|
+
def self.build_for_faceted_node(options = {})
|
|
22
|
+
predicate_name = options.fetch(:predicate_name)
|
|
23
|
+
faceted_node = options.fetch(:faceted_node)
|
|
24
|
+
term_label = options.fetch(:term_label)
|
|
25
|
+
locabulary_item = Locabulary::Item.build(predicate_name: predicate_name, term_label: term_label, default_presentation_sequence: nil)
|
|
26
|
+
new(faceted_node: faceted_node, locabulary_item: locabulary_item)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api public
|
|
30
|
+
# @since 0.5.0
|
|
31
|
+
#
|
|
32
|
+
# In some cases, we have a facet and a locabulary item and this is the public method for building the wrapped object.
|
|
33
|
+
#
|
|
34
|
+
# @see Locabulary::FacetedHierarchicalTreeSorter
|
|
35
|
+
# @param options [Hash]
|
|
36
|
+
# @option predicate_name [String]
|
|
37
|
+
# @option faceted_node [#qvalue, #value, #hits]
|
|
38
|
+
# @option term_label [String]
|
|
39
|
+
# @return Locabulary::FacetWrapperForItem
|
|
40
|
+
def self.build_for_faceted_node_and_locabulary_item(options = {})
|
|
41
|
+
new(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Don't access .new directly; Use the above builders
|
|
45
|
+
private_class_method :new
|
|
46
|
+
|
|
47
|
+
def initialize(options = {})
|
|
48
|
+
@__faceted_node__ = options.fetch(:faceted_node)
|
|
49
|
+
@__locabulary_item__ = options.fetch(:locabulary_item)
|
|
50
|
+
super(@__locabulary_item__)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :__faceted_node__, :__locabulary_item__
|
|
54
|
+
|
|
55
|
+
extend Forwardable
|
|
56
|
+
def_delegators :__faceted_node__, :qvalue, :value, :hits
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
|
2
|
+
require 'locabulary/items'
|
|
3
|
+
|
|
4
|
+
module Locabulary
|
|
5
|
+
# A container of builder methods for items
|
|
6
|
+
module Item
|
|
7
|
+
# @api public
|
|
8
|
+
# @since 0.2.1
|
|
9
|
+
#
|
|
10
|
+
# A Factory method that is responsible for building the appropriate object given a :predicate_name and additional attributes.
|
|
11
|
+
#
|
|
12
|
+
# @param attributes [Hash]
|
|
13
|
+
# @option predicate_name [String]
|
|
14
|
+
# @return [Locabulary::Item]
|
|
15
|
+
# @see Locabulary::Items
|
|
16
|
+
def self.build(attributes = {})
|
|
17
|
+
predicate_name = attributes.fetch(:predicate_name) { attributes.fetch('predicate_name') }
|
|
18
|
+
builder_for(predicate_name: predicate_name).call(attributes)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @api public
|
|
22
|
+
# @since 0.2.1
|
|
23
|
+
#
|
|
24
|
+
# Responsible for finding the appropriate Factory method for building a Locabulary::Item
|
|
25
|
+
#
|
|
26
|
+
# @param options [Hash]
|
|
27
|
+
# @option predicate_name [String] Used for lookup of the correct Locabulary::Item type
|
|
28
|
+
# @return [#call] A builder method (`.new` for the given constant)
|
|
29
|
+
def self.builder_for(options = {})
|
|
30
|
+
predicate_name = options.fetch(:predicate_name)
|
|
31
|
+
possible_class_name_for_predicate_name = predicate_name.singularize.classify
|
|
32
|
+
klass = begin
|
|
33
|
+
Items.const_get(possible_class_name_for_predicate_name)
|
|
34
|
+
rescue NameError
|
|
35
|
+
Items::Base
|
|
36
|
+
end
|
|
37
|
+
klass.method(:new)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/locabulary/items.rb
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
require 'locabulary/items/base'
|
|
2
|
-
require '
|
|
2
|
+
require 'locabulary/items/administrative_unit'
|
|
3
3
|
module Locabulary
|
|
4
|
-
#
|
|
4
|
+
# @since 0.5.0
|
|
5
|
+
#
|
|
6
|
+
# A container module for data structures that conform to the Locabulary::Items::Base interface
|
|
7
|
+
#
|
|
8
|
+
# @see Locabulary::Item for additional interaction
|
|
5
9
|
module Items
|
|
6
|
-
module_function
|
|
7
|
-
|
|
8
|
-
# @api public
|
|
9
|
-
# @since 0.2.1
|
|
10
|
-
def build(options = {})
|
|
11
|
-
predicate_name = options.fetch(:predicate_name) { options.fetch('predicate_name') }
|
|
12
|
-
builder_for(predicate_name: predicate_name).call(options)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @api public
|
|
16
|
-
# @since 0.2.1
|
|
17
|
-
#
|
|
18
|
-
# @param options [Hash]
|
|
19
|
-
# @option predicate_name [String] Used for lookup of the correct Locabulary::Item type
|
|
20
|
-
def builder_for(options = {})
|
|
21
|
-
predicate_name = options.fetch(:predicate_name)
|
|
22
|
-
possible_class_name_for_predicate_name = Hanami::Utils::String.new(predicate_name).singularize.classify
|
|
23
|
-
klass = begin
|
|
24
|
-
Items.const_get(possible_class_name_for_predicate_name)
|
|
25
|
-
rescue NameError
|
|
26
|
-
Items::Base
|
|
27
|
-
end
|
|
28
|
-
klass.method(:new)
|
|
29
|
-
end
|
|
30
10
|
end
|
|
31
11
|
end
|
|
32
|
-
require 'locabulary/items/administrative_unit'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
1
2
|
require 'locabulary/exceptions'
|
|
2
3
|
require 'locabulary/items/base'
|
|
3
4
|
|
|
@@ -27,45 +28,16 @@ module Locabulary
|
|
|
27
28
|
|
|
28
29
|
public
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def children
|
|
36
|
-
@children.sort
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def add_child(*input)
|
|
40
|
-
@children += input
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
HIERARCHY_SEPARATOR = '::'.freeze
|
|
44
|
-
def slugs
|
|
45
|
-
term_label.split(HIERARCHY_SEPARATOR)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def parent_slugs
|
|
49
|
-
slugs[0..-2]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def parent_term_label
|
|
53
|
-
parent_slugs.join(HIERARCHY_SEPARATOR)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def root_slug
|
|
57
|
-
slugs[0]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def selectable?
|
|
61
|
-
children.count == 0
|
|
62
|
-
end
|
|
63
|
-
|
|
31
|
+
NON_DEPARTMENTAL_SLUG = "Non-Departmental".freeze
|
|
32
|
+
# NOTE: The whitespace characters are "thin spaces", U+200A
|
|
33
|
+
HUMAN_FRIENDLY_HIERARCHY_DELIMITER = ' — '.freeze
|
|
64
34
|
def selectable_label
|
|
65
|
-
slugs[1
|
|
35
|
+
if slugs[-1] == NON_DEPARTMENTAL_SLUG
|
|
36
|
+
slugs[-2..-1].join(HUMAN_FRIENDLY_HIERARCHY_DELIMITER)
|
|
37
|
+
else
|
|
38
|
+
slugs[-1]
|
|
39
|
+
end
|
|
66
40
|
end
|
|
67
|
-
|
|
68
|
-
alias selectable_id id
|
|
69
41
|
end
|
|
70
42
|
end
|
|
71
43
|
end
|