specify_cli 0.0.5

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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Gemfile.lock +117 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.rdoc +43 -0
  8. data/Rakefile +15 -0
  9. data/bin/specify_cli +248 -0
  10. data/lib/specify.rb +45 -0
  11. data/lib/specify/branch_parser.rb +85 -0
  12. data/lib/specify/cli.rb +11 -0
  13. data/lib/specify/cli/database_setup.rb +46 -0
  14. data/lib/specify/cli/stubs.rb +63 -0
  15. data/lib/specify/cli/viewset.rb +21 -0
  16. data/lib/specify/configuration.rb +12 -0
  17. data/lib/specify/configuration/config.rb +120 -0
  18. data/lib/specify/configuration/db_config.rb +162 -0
  19. data/lib/specify/configuration/host_config.rb +37 -0
  20. data/lib/specify/database.rb +140 -0
  21. data/lib/specify/models.rb +43 -0
  22. data/lib/specify/models/accession.rb +33 -0
  23. data/lib/specify/models/agent.rb +138 -0
  24. data/lib/specify/models/app_resource_data.rb +32 -0
  25. data/lib/specify/models/app_resource_dir.rb +43 -0
  26. data/lib/specify/models/auto_numbering_scheme.rb +94 -0
  27. data/lib/specify/models/collecting_event.rb +38 -0
  28. data/lib/specify/models/collection.rb +67 -0
  29. data/lib/specify/models/collection_object.rb +127 -0
  30. data/lib/specify/models/createable.rb +21 -0
  31. data/lib/specify/models/determination.rb +63 -0
  32. data/lib/specify/models/discipline.rb +61 -0
  33. data/lib/specify/models/division.rb +26 -0
  34. data/lib/specify/models/geography.rb +5 -0
  35. data/lib/specify/models/geography/administrative_division.rb +32 -0
  36. data/lib/specify/models/geography/geographic_name.rb +66 -0
  37. data/lib/specify/models/geography/geography.rb +23 -0
  38. data/lib/specify/models/institution.rb +13 -0
  39. data/lib/specify/models/locality.rb +50 -0
  40. data/lib/specify/models/preparation.rb +53 -0
  41. data/lib/specify/models/preparation_type.rb +30 -0
  42. data/lib/specify/models/record_set.rb +55 -0
  43. data/lib/specify/models/record_set_item.rb +29 -0
  44. data/lib/specify/models/taxonomy.rb +6 -0
  45. data/lib/specify/models/taxonomy/common_name.rb +14 -0
  46. data/lib/specify/models/taxonomy/rank.rb +31 -0
  47. data/lib/specify/models/taxonomy/taxon.rb +54 -0
  48. data/lib/specify/models/taxonomy/taxonomy.rb +21 -0
  49. data/lib/specify/models/tree_queryable.rb +55 -0
  50. data/lib/specify/models/updateable.rb +20 -0
  51. data/lib/specify/models/user.rb +104 -0
  52. data/lib/specify/models/view_set_object.rb +32 -0
  53. data/lib/specify/number_format.rb +60 -0
  54. data/lib/specify/services.rb +18 -0
  55. data/lib/specify/services/service.rb +51 -0
  56. data/lib/specify/services/stub_generator.rb +291 -0
  57. data/lib/specify/services/view_loader.rb +177 -0
  58. data/lib/specify/session.rb +77 -0
  59. data/lib/specify/user_type.rb +61 -0
  60. data/lib/specify/version.rb +19 -0
  61. data/man/specify_cli-database.1 +60 -0
  62. data/man/specify_cli-database.1.html +137 -0
  63. data/man/specify_cli-database.1.ronn +53 -0
  64. data/man/specify_cli-repository.1 +55 -0
  65. data/man/specify_cli-repository.1.html +128 -0
  66. data/man/specify_cli-repository.1.ronn +42 -0
  67. data/man/specify_cli-stubs.1 +177 -0
  68. data/man/specify_cli-stubs.1.html +239 -0
  69. data/man/specify_cli-stubs.1.ronn +147 -0
  70. data/man/specify_cli-viewset.1 +92 -0
  71. data/man/specify_cli-viewset.1.html +154 -0
  72. data/man/specify_cli-viewset.1.ronn +72 -0
  73. data/man/specify_cli.1 +213 -0
  74. data/man/specify_cli.1.html +252 -0
  75. data/man/specify_cli.1.ronn +157 -0
  76. data/spec/branch_parser_spec.rb +94 -0
  77. data/spec/cli/stubs_spec.rb +44 -0
  78. data/spec/configuration/config_spec.rb +269 -0
  79. data/spec/configuration/db_config_spec.rb +299 -0
  80. data/spec/configuration/host_config_spec.rb +64 -0
  81. data/spec/database_spec.rb +83 -0
  82. data/spec/examples.txt +217 -0
  83. data/spec/helpers.rb +15 -0
  84. data/spec/models/app_resource_data_spec.rb +38 -0
  85. data/spec/models/app_resource_dir_spec.rb +8 -0
  86. data/spec/models/auto_numbering_scheme_spec.rb +78 -0
  87. data/spec/models/collection_object_spec.rb +92 -0
  88. data/spec/models/collection_spec.rb +32 -0
  89. data/spec/models/discipline_spec.rb +31 -0
  90. data/spec/models/record_set_spec.rb +18 -0
  91. data/spec/models/user_spec.rb +182 -0
  92. data/spec/models/view_set_object_spec.rb +70 -0
  93. data/spec/number_format_spec.rb +43 -0
  94. data/spec/services/stub_generator_spec.rb +635 -0
  95. data/spec/services/view_loader_spec.rb +436 -0
  96. data/spec/session_spec.rb +105 -0
  97. data/spec/spec_helper.rb +116 -0
  98. data/spec/support/db.yml +12 -0
  99. data/spec/support/stub.yaml +17 -0
  100. data/spec/support/stub_locality.yaml +19 -0
  101. data/spec/support/viewsets/paleo.views.xml +30 -0
  102. data/spec/support/viewsets/paleo.xml +30 -0
  103. data/spec/user_type_spec.rb +79 -0
  104. data/specify_cli.gemspec +27 -0
  105. data/specify_cli.rdoc +1 -0
  106. metadata +246 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # Taxon is the _item_ class for the Specify::Model::Taxonomy _tree_. A
6
+ # Taxon holds information about the items to be classified, i.e. concepts
7
+ # of taxonomic names. Each Taxon belongs to a Specify::Model::Rank, that
8
+ # represents its formal Linnean classification rank.
9
+ #
10
+ # A Taxon has a _parent_ (another instance of Taxon) unless it is the root
11
+ # taxon of the _tree_ and can have _children_ (other instances of Taxon).
12
+ class Taxon < Sequel::Model(:taxon)
13
+ include Createable
14
+ include Updateable
15
+
16
+ many_to_one :taxonomy,
17
+ key: :TaxonTreeDefID
18
+ many_to_one :rank,
19
+ key: :TaxonTreeDefItemID
20
+ many_to_one :parent,
21
+ class: self,
22
+ key: :ParentID
23
+ many_to_one :accepted_name,
24
+ class: self,
25
+ key: :AcceptedID
26
+ one_to_many :synonyms,
27
+ class: self,
28
+ key: :AcceptedID
29
+ one_to_many :children,
30
+ class: self,
31
+ key: :ParentID
32
+ one_to_many :common_names,
33
+ key: :TaxonID
34
+
35
+ # Assigns new instances that are created from a Specify::Model::Rank to
36
+ # the rank's Specify::Model::Taxonomy. Assigns a GUID for the record.
37
+ def before_create
38
+ self.taxonomy = rank&.taxonomy || parent.rank&.taxonomy
39
+ self[:GUID] = SecureRandom.uuid
40
+ super
41
+ end
42
+
43
+ # Returns +true+ if +self+ has _children_.
44
+ def children?
45
+ !children.empty?
46
+ end
47
+
48
+ # Returns a String with the taxon name.
49
+ def name
50
+ self[:Name]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # Taxnomy is the _tree_ class for the <em>taxonomy tree</em>. Taxonomies
6
+ # hold all Specify::Model::Rank and Specify::Model::Taxon instances
7
+ # belonging to a taxonomy used by a Specify::Model::Discipline.
8
+ class Taxonomy < Sequel::Model(:taxontreedef)
9
+ include TreeQueryable
10
+ include Updateable
11
+
12
+ one_to_many :disciplines,
13
+ key: :TaxonTreeDefID
14
+ one_to_many :ranks,
15
+ key: :TaxonTreeDefID
16
+ one_to_many :names,
17
+ class: 'Specify::Model::Taxon',
18
+ key: :TaxonTreeDefID
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # TreeQueryable is a mixin that provides methods to query _trees_
6
+ # such as Specify::Model::Taxnoomy and Specify::Model::Geography.
7
+ #
8
+ # Trees are nested hierarchies that are represented by three classes
9
+ # - an _item_ class, holding information about the items to be classified in
10
+ # a _tree_, such as taxonomic or geographic names.
11
+ # - a _rank_ class, which designates items as belonging to a formal rank or
12
+ # level within the tree
13
+ # - the _tree_ class itself, which identifies all items and ranks belonging
14
+ # to one tree.
15
+ #
16
+ # For taxonomies, the _tree_ class is Specify::Model::Taxonomy,
17
+ # the _item_ class is Specify::Model::Taxon, the _rank_ class is
18
+ # Specify::Model::Rank.
19
+ #
20
+ # For geographies, the _tree_ class is Specify::Model::Geography, the
21
+ # _item_ class is Specify::Model::GeographicName, the _rank_ class is
22
+ # Specify::Model::AdministrativeDivision.
23
+ module TreeQueryable
24
+ AMBIGUOUS_MATCH_ERROR = 'Ambiguous results during tree search'
25
+
26
+ # Returns the _rank_ instance in a tree for +rank_name+ (a String).
27
+ #
28
+ # _rank_ classes are Specify::Model::AdministrativeDivision (for
29
+ # Specify::Model::Geography), and Specify::Model::Rank (for
30
+ # Specify::Model::Taxonomy).
31
+ def rank(rank_name)
32
+ ranks_dataset.first(Name: rank_name.capitalize)
33
+ end
34
+
35
+ # Preforms a tree search, traversing a hierarchy from highest to lowest
36
+ # _rank_. +hash+ is a Hash with the structure
37
+ # <tt>{ 'rank' => 'name' }</tt> where +rank+ is an existing _rank_ name,
38
+ # +name+ an existing _item_ name with that rank. Give key value paris in
39
+ # descencing order of rank:
40
+ # { 'rank 1' => 'name',
41
+ # 'rank 2' => 'name'
42
+ # 'rank n' => 'name' }
43
+ def search_tree(hash)
44
+ hash.reduce(nil) do |item, (rank_name, name)|
45
+ searchset = item&.children_dataset || names_dataset
46
+ item = searchset.where(Name: name,
47
+ rank: rank(rank_name))
48
+ next item.first unless item.count > 1
49
+ raise AMBIGUOUS_MATCH_ERROR +
50
+ " for #{name}: #{item.to_a.map(&:FullName)}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # Updateable is a mixin that provides the standard #before_update hook for
6
+ # Specify::Model classes.
7
+ #
8
+ # Most tables in the _Specify_ schema have a _Version_ (an Integer) that is
9
+ # incremented for each modification and a modification timestamp. The
10
+ # #before_update hook will set these.
11
+ module Updateable
12
+ # Sets the _Version_ and modification timestamp of a record.
13
+ def before_update
14
+ self[:Version] += 1
15
+ self[:TimestampModified] = Time.now
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # Users represent _Specify_ users.
6
+ class User < Sequel::Model(:specifyuser)
7
+ include Updateable
8
+
9
+ one_to_many :agents,
10
+ key: :SpecifyUserID
11
+ one_to_many :app_resource_dirs,
12
+ key: :SpecifyUserID
13
+
14
+ # Returns +true+ if +collection+ (a Specify::Model::Collection) is the
15
+ # same #login_collection.
16
+ def collection_valid?(collection)
17
+ login_collection == collection
18
+ end
19
+
20
+ # Creates a string representation of +self+.
21
+ def inspect
22
+ "#{self} user name: #{self.Name}, logged in: #{self.IsLoggedIn}"
23
+ end
24
+
25
+ # Logs the user in to +collection+ (a Specify::Model::Collection).
26
+ #
27
+ # Returns a Hash with the Specify::Model::Collection as key, the timestamp
28
+ # for the login as the value.
29
+ def log_in(collection)
30
+ logged_in?(collection) || new_login(collection)
31
+ end
32
+
33
+ # Logs the user out.
34
+ #
35
+ # Returns the timestamp for the logout.
36
+ def log_out
37
+ return true unless self[:IsLoggedIn]
38
+ self[:LoginOutTime] = Time.now
39
+ self[:IsLoggedIn] = false
40
+ self[:LoginCollectionName] = nil
41
+ self[:LoginDisciplineName] = nil
42
+ save
43
+ self[:LoginOutTime]
44
+ end
45
+
46
+ # Returns a Hash with the Specify::Model::Collection as key, the timestamp
47
+ # for the login as the value if +self+ is logged in to +collection+ (a
48
+ # Specify::Model::Collection), +nil+ otherwise.
49
+ def logged_in?(collection)
50
+ return nil unless self[:IsLoggedIn]
51
+ raise LoginError::INCONSISTENT_LOGIN unless collection_valid? collection
52
+ { collection => self[:LoginOutTime] }
53
+ end
54
+
55
+ # Returns the Specify::Model::Agent for +self+ in the
56
+ # Specify::Model::Division division to which the login_discipline belongs.
57
+ def logged_in_agent
58
+ agents_dataset.first(division: login_discipline.division)
59
+ end
60
+
61
+ # Returns the Specify::Model::Collection that +self+ is logged in to.
62
+ def login_collection
63
+ login_discipline.collections_dataset
64
+ .first CollectionName: self[:LoginCollectionName]
65
+ end
66
+
67
+ # Returns the Specify::Model::Discipline that +self+ is logged in to.
68
+ def login_discipline
69
+ Discipline.first Name: self[:LoginDisciplineName]
70
+ end
71
+
72
+ # Returns a String with the _Specify_ username for +self+.
73
+ def name
74
+ self[:Name]
75
+ end
76
+
77
+ # Registers a new login in +collection+ (a Specify::Model::Collection).
78
+ def new_login(collection)
79
+ login_time = Time.now
80
+ self[:LoginOutTime] = login_time
81
+ self[:IsLoggedIn] = true
82
+ self[:LoginCollectionName] = collection[:CollectionName]
83
+ self[:LoginDisciplineName] = collection.discipline[:Name]
84
+ save
85
+ { collection => self[:LoginOutTime] }
86
+ end
87
+
88
+ # Returns the Specify::Model::AppResourceDir for +self+ in +collection+
89
+ # (a Specify::Model::Collection).
90
+ def view_set_dir(collection)
91
+ app_resource_dirs_dataset.first(collection: collection,
92
+ discipline: collection.discipline,
93
+ UserType: self[:UserType],
94
+ IsPersonal: true)
95
+ end
96
+
97
+ # Returns the Specify::Model::ViewSetObject for +self+ in +collection+ (a
98
+ # Specify::Model::Collection)
99
+ def view_set(collection)
100
+ view_set_dir(collection)&.view_set_object
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Model
5
+ # ViewSetObjects represent Specify user forms (views). The actual views are
6
+ # _.views.xml_ files that are stored as blobs in the database.
7
+ class ViewSetObject < Sequel::Model(:spviewsetobj)
8
+ include Createable
9
+ include Updateable
10
+
11
+ many_to_one :app_resource_dir,
12
+ class: 'Specify::Model::AppResourceDir',
13
+ key: :SpAppResourceDirID
14
+ one_to_one :app_resource_data,
15
+ class: 'Specify::Model::AppResourceData',
16
+ key: :SpViewSetObjID
17
+ many_to_one :created_by,
18
+ class: 'Specify::Model::Agent',
19
+ key: :CreatedByAgentID
20
+ many_to_one :modified_by,
21
+ class: 'Specify::Model::Agent',
22
+ key: :ModifiedByAgentID
23
+
24
+ # Persists +file+ (a _.views.xml_ file) as a blob in the database.
25
+ def import(file)
26
+ app_resource_data.import file
27
+ app_resource_dir.save
28
+ save
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ # NumberFormats represent auto numbering formatters in a Specify::Database.
5
+ # Number formats in _Specify_ are applied to Strings, to ensure they conform
6
+ # to a defined format.
7
+ #
8
+ # If the NumberFormat can be auto-incremented, it will have a serial number
9
+ # part, the _incrementer_.
10
+ class NumberFormat
11
+ # The number of digits (length) of the serial number part.
12
+ attr_accessor :incrementer_length
13
+
14
+ # Not implemented
15
+ def self.from_xml(format_node)
16
+ # TODO: implement
17
+ end
18
+
19
+ # Returns a new NumberFormat
20
+ #
21
+ # +options+ is not implemented; if empty, the NumberFormat will be numeric,
22
+ # i.e. consist only of the _incrementer_.
23
+ def initialize(incrementer_length = 9, options = {})
24
+ @incrementer_length = incrementer_length
25
+ @options = options
26
+ end
27
+
28
+ # Not implemented
29
+ def self.parse(format_string)
30
+ # TODO: implement
31
+ end
32
+
33
+ # Returns a new formatted String for +number+
34
+ def create(number)
35
+ number.to_s.rjust(incrementer_length, '0') if numeric?
36
+ end
37
+
38
+ # Returns the serial number part (_incrementer_) of the formatted
39
+ # +number_string+.
40
+ def incrementer(number_string)
41
+ return number_string.to_i if numeric?
42
+ end
43
+
44
+ # Returns +true+ if the +self+ is a numeric NumberFormat.
45
+ def numeric?
46
+ @options.empty?
47
+ end
48
+
49
+ # Returns a String template for +self+, where # marks a digit of the
50
+ # _incrementer_ (serial number part).
51
+ def template
52
+ return '#' * incrementer_length if numeric?
53
+ end
54
+
55
+ # Returns a Regexp to match the match the _incrementer_ in the NumberFormat.
56
+ def to_regexp
57
+ return /^(?<incrementer>\d{#{incrementer_length}})$/ if numeric?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'services/service'
4
+ require_relative 'services/stub_generator'
5
+ require_relative 'services/view_loader'
6
+
7
+ module Specify
8
+ # The Service module contains classes that will carry out tasks on a
9
+ # Specify::Database. Service classes are subclasses of Service::Service.
10
+ module Service
11
+ ACCESSION_NOT_FOUND_ERROR = 'Accession not found: '
12
+ USER_NOT_FOUND_ERROR = 'User not found: '
13
+ GEOGRAPHY_NOT_FOUND_ERROR = 'Geography not found: '
14
+ LOCALITY_NOT_FOUND_ERROR = 'Locality not found: '
15
+ TAXON_NOT_FOUND_ERROR = 'Taxon not found: '
16
+ PREPTYPE_NOT_FOUND_ERROR = 'Preparation type not found: '
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Service
5
+ # Superclass for service classes.
6
+ class Service
7
+ # The Specify::Session#agent for #session.
8
+ attr_reader :agent
9
+
10
+ # The Specify::Model::Collection for #session.
11
+ attr_reader :collection
12
+
13
+ # The Specify::Model::Discipline for #session.
14
+ attr_reader :discipline
15
+
16
+ # The Specify::Model::Division for #session.
17
+ attr_reader :division
18
+
19
+ # The Specify::Session for +self+ (the session that the Service uses
20
+ # during work with a Specify::Database).
21
+ attr_reader :session
22
+
23
+ # Returns a new Service.
24
+ #
25
+ # +host+: the hostname for the MySQL/MariaDB server.
26
+ #
27
+ # +database+: a String with a Specify::Database#database.
28
+ #
29
+ # +collection+: a String with an existing Specify::Model::Collection#name.
30
+ #
31
+ # +config+: a YAML file containing the database configuration.
32
+ #
33
+ # +specify_user+: a String with an existing Specify::Model::User#name.
34
+ def initialize(host:, database:, collection:, config:, specify_user: nil)
35
+ @config = Configuration::DBConfig.new(host, database, config)
36
+ @db = Database.new database, @config.connection
37
+ session_user = specify_user || @config.session_user
38
+ @session = @db.start_session session_user, collection
39
+ @collection = @session.collection
40
+ @discipline = @session.discipline
41
+ @division = @session.division
42
+ @agent = @session.session_agent
43
+ end
44
+
45
+ # Returns the Sequel::Database instance for the current connection.
46
+ def database
47
+ @db.connection
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Specify
4
+ module Service
5
+ # A StubGenerator creates Specify::Model::CollectionObject stub records
6
+ # (mostly empty records with a minmum of information) in collection in a
7
+ # Specify::Database.
8
+ class StubGenerator < Service
9
+ # An existing Specify::Model::Accession.
10
+ attr_reader :accession
11
+
12
+ # An existing Specify::Model::Agent.
13
+ attr_reader :cataloger
14
+
15
+ # An existing Specify::Model::GeographicName.
16
+ attr_reader :collecting_geography
17
+
18
+ # An existing Specify::Model::Locality.
19
+ attr_reader :collecting_locality
20
+
21
+ # String; the name for the #record_set that will be created for the
22
+ # generated Specify::Model::CollectionObject records.
23
+ attr_accessor :dataset_name
24
+
25
+ # String; the name of the Specify::Model::Locality that will be created if
26
+ # no existing Specify::Model::Locality is passed via #collecting_data=.
27
+ attr_accessor :default_locality_name
28
+
29
+ # Integer. See Specify::Model::Preparation#count.
30
+ attr_reader :preparation_count
31
+
32
+ # An existing Specify::Model::PreparationType.
33
+ attr_reader :preparation_type
34
+
35
+ # A Specify::Model::RecordSet.
36
+ attr_reader :record_set
37
+
38
+ # An existing Specify::Model::Taxon.
39
+ attr_reader :taxon
40
+
41
+ # Returns a new StubGenerator with attributes from a YAML +file+.
42
+ #
43
+ # +file+ should have the structure:
44
+ # ---
45
+ # :stub_generator:
46
+ # :host: <hostname>
47
+ # :database: <database name>
48
+ # :collection: <collection name>
49
+ # :config: <database configuration file>
50
+ # dataset_name: <record set name>
51
+ # accession: <accession number>
52
+ # cataloger: <specify user name>
53
+ # collecting_data:
54
+ # <1st rank>: <name>
55
+ # <2nd rank>: <name>
56
+ # <3rd rank>: <name>
57
+ # :locality: <name>
58
+ # default_locality_name: <name>
59
+ # determination:
60
+ # <1st rank>: <name>
61
+ # <2nd rank>: <name>
62
+ # <3rd rank>: <name>
63
+ # preparation:
64
+ # :type: <preparation type>
65
+ # :count: <preparation count>
66
+ #
67
+ # Items prefixed with +:+ in the example above will be deserialized as
68
+ # Ruby symbols and need to be prefixed with +:+ in the file. Leave out any
69
+ # items that are not to be set. The section +:stub_generator:+ is
70
+ # required.
71
+ def self.load_yaml(file)
72
+ unwrap Psych.load_file(file)
73
+ end
74
+
75
+ # Returns a new StubGenerator with attributes from +hash+.
76
+ #
77
+ # +hash+ should have the structure
78
+ # {
79
+ # stub_generator: {
80
+ # host: <hostname>,
81
+ # database: <database name>,
82
+ # collection: <collection name>,
83
+ # config: <database configuration file>
84
+ # },
85
+ # dataset_name => <record set name>,
86
+ # accession => <accession number>,
87
+ # cataloger => <specify user name>,
88
+ # collecting_data => {
89
+ # <1st rank> => <name>,
90
+ # <2nd rank> => <name>,
91
+ # <3rd rank> => <name>,
92
+ # locality: <name>
93
+ # },
94
+ # default_locality_name => <name>,
95
+ # determination => {
96
+ # <1st rank> => <name>,
97
+ # <2nd rank> => <name>,
98
+ # <3rd rank> => <name>
99
+ # },
100
+ # preparation => {
101
+ # type: <preparation type>,
102
+ # count: <preparation count>
103
+ # }
104
+ # }
105
+ # Items that are symbols in the example above need to be symbols in the
106
+ # +hash+ passed. Leave out any items that are not to be set. The key
107
+ # +:stub_generator+ is required.
108
+ def self.unwrap(hash)
109
+ new hash.delete(:stub_generator) do |stubs|
110
+ hash.each do |key, value|
111
+ setter = (key + '=').to_sym
112
+ puts "#{setter}#{value}"
113
+ next unless value
114
+ stubs.public_send(setter, value)
115
+ end
116
+ end
117
+ end
118
+
119
+ # Returns a new StubGenerator.
120
+ def initialize(collection:,
121
+ config:,
122
+ host:,
123
+ database:,
124
+ specify_user: nil)
125
+ super
126
+ @accession = nil
127
+ @cataloger = agent
128
+ @collecting_geography = nil
129
+ @collecting_locality = nil
130
+ @default_locality_name = 'not cataloged, see label'
131
+ @dataset_name = "stub record set #{Time.now}"
132
+ @preparation_type = nil
133
+ @preparation_count = nil
134
+ @record_set = nil
135
+ @taxon = nil
136
+ yield(self) if block_given?
137
+ end
138
+
139
+ # Sets #accession to the Specify::Model::Accession with +accession_number+
140
+ def accession=(accession_number)
141
+ @accession = division.accessions_dataset
142
+ .first AccessionNumber: accession_number
143
+ raise ACCESSION_NOT_FOUND_ERROR + accession_number unless accession
144
+ end
145
+
146
+ # Sets #cataloger to the Specify::Model::Agent representing the
147
+ # Specify::Model::User with +user_name+ in #division.
148
+ def cataloger=(user_name)
149
+ cataloger_user = Model::User.first(Name: user_name)
150
+ raise USER_NOT_FOUND_ERROR + user_name unless cataloger_user
151
+ @cataloger = cataloger_user.agents_dataset.first division: division
152
+ end
153
+
154
+ # Sets #collecting_geography and #collecting_locality.
155
+ #
156
+ # +vals+ is a Hash with the structure <tt>{ 'rank' => 'name',
157
+ # :locality => 'name' }</tt> where +rank+ is an existing
158
+ # Specify::Model::AdministrativeDivision#name, +name+ an existing
159
+ # Specify::Model::GeographicName#name with that rank. +:locality+ is not a
160
+ # geographic rank and must be given as a symbol. When traversing a tree
161
+ # hierarchy, give key value paris in descencing order of rank:
162
+ # { 'Country' => 'United States',
163
+ # 'State' => 'Kansas',
164
+ # 'County' => 'Douglas County',
165
+ # :locality => 'Freestate Brewery' }
166
+ def collecting_data=(vals)
167
+ locality = vals.delete :locality
168
+ unless vals.empty?
169
+ @collecting_geography = geography.search_tree(vals)
170
+ unless @collecting_geography
171
+ missing_geo = vals.values.join(', ')
172
+ raise GEOGRAPHY_NOT_FOUND_ERROR + missing_geo
173
+ end
174
+ end
175
+ return unless locality
176
+ @collecting_locality = find_locality locality
177
+ raise LOCALITY_NOT_FOUND_ERROR + locality unless collecting_locality
178
+ end
179
+
180
+ # Returns #collecting_locality or #default_locality if
181
+ # #collecting_locality is +nil+ but #collecting_geography is not;
182
+ # Will create the Specify::Model::GeographicName for #default_locality
183
+ # if it does not exist in #localities.
184
+ def collecting_locality!
185
+ return collecting_locality if collecting_locality
186
+ return unless collecting_geography
187
+ default_locality!
188
+ end
189
+
190
+ # Creates +count+ records for Specify::Model::CollectionObject with the
191
+ # attributes of +self+.
192
+ def create(count)
193
+ @record_set = collection.add_record_set Name: dataset_name,
194
+ user: cataloger.user
195
+ count.times do
196
+ stub = create_stub
197
+ @record_set.add_record_set_item collection_object: stub
198
+ end
199
+ end
200
+
201
+ # Returns the Specify::Model::GeographicName for #default locality if it
202
+ # exists.
203
+ def default_locality
204
+ find_locality default_locality_name
205
+ end
206
+
207
+ # Returns the Specify::Model::GeographicName for #default locality.
208
+ # Creates the record if it does not exist in #localities.
209
+ def default_locality!
210
+ return default_locality if default_locality
211
+ default_locality ||
212
+ discipline.add_locality(LocalityName: default_locality_name,
213
+ geographic_name: collecting_geography)
214
+ end
215
+
216
+ # Sets #taxon, to which stub records will be determined.
217
+ # +vals+ is a Hash with the structure <tt>{ 'rank' => 'name' }</tt> where
218
+ # +rank+ is an existing Specify::Model::Rank#name, +name+ an existing
219
+ # Specify::Model::Taxon#name with that rank. When traversing a tree
220
+ # hierarchy, give key value paris in descencing order of rank:
221
+ # { 'Phylum' => 'Arthropoda',
222
+ # 'Class' => 'Trilobita',
223
+ # 'Order' => 'Asaphida',
224
+ # 'Family' => 'Asaphidae' }
225
+ def determination=(vals)
226
+ @taxon = taxonomy.search_tree vals
227
+ raise TAXON_NOT_FOUND_ERROR + vals.to_s unless taxon
228
+ end
229
+
230
+ # Returns the Specify::Model::Locality for +locality_name+ in #localities.
231
+ def find_locality(locality_name)
232
+ locality_matches = localities.where LocalityName: locality_name
233
+ raise Model::AMBIGUOUS_MATCH_ERROR if locality_matches.count > 1
234
+ locality_matches.first
235
+ end
236
+
237
+ # Returns the Specify::Model::CollectionObject records in #record_set
238
+ # (the records created by #create).
239
+ def generated
240
+ record_set&.collection_objects
241
+ end
242
+
243
+ # Returns the Specify::Model::Geography for #discipline.
244
+ def geography
245
+ discipline.geography
246
+ end
247
+
248
+ # Returns a Sequel::Dataset of Specify::Model::Locality records in
249
+ # #collecting_geography or #division if #collecting_geography is +nil+.
250
+ def localities
251
+ @collecting_geography&.localities_dataset ||
252
+ discipline.localities_dataset
253
+ end
254
+
255
+ # Sets #preparation_type and #preparation_count. +type+ must be an
256
+ # existing Specify::Model::PreparationType#name. +count+ should be an
257
+ # Integer.
258
+ #
259
+ # Returns an array with the #preparation_type and #preparation_count.
260
+ def preparation=(type:, count: nil)
261
+ @preparation_type = collection.preparation_types_dataset
262
+ .first Name: type
263
+ raise PREPTYPE_NOT_FOUND_ERROR + type unless preparation_type
264
+ @preparation_count = count
265
+ [preparation_type, preparation_count].compact
266
+ end
267
+
268
+ # Returns the Specify::Model::Taxonomy for #discipline.
269
+ def taxonomy
270
+ discipline.taxonomy
271
+ end
272
+
273
+ private
274
+
275
+ def create_stub
276
+ co = collection.add_collection_object(cataloger: cataloger)
277
+ co.accession = accession
278
+ co.geo_locate(locality: collecting_locality!) if collecting_locality!
279
+ co.identify(taxon: taxon) if taxon
280
+ make_preparation(co) if preparation_type
281
+ co.save
282
+ end
283
+
284
+ def make_preparation(collection_object)
285
+ collection_object.add_preparation collection: collection,
286
+ preparation_type: preparation_type,
287
+ CountAmt: preparation_count
288
+ end
289
+ end
290
+ end
291
+ end