specify_cli 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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