specify_cli 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +117 -0
- data/LICENSE.txt +21 -0
- data/README.rdoc +43 -0
- data/Rakefile +15 -0
- data/bin/specify_cli +248 -0
- data/lib/specify.rb +45 -0
- data/lib/specify/branch_parser.rb +85 -0
- data/lib/specify/cli.rb +11 -0
- data/lib/specify/cli/database_setup.rb +46 -0
- data/lib/specify/cli/stubs.rb +63 -0
- data/lib/specify/cli/viewset.rb +21 -0
- data/lib/specify/configuration.rb +12 -0
- data/lib/specify/configuration/config.rb +120 -0
- data/lib/specify/configuration/db_config.rb +162 -0
- data/lib/specify/configuration/host_config.rb +37 -0
- data/lib/specify/database.rb +140 -0
- data/lib/specify/models.rb +43 -0
- data/lib/specify/models/accession.rb +33 -0
- data/lib/specify/models/agent.rb +138 -0
- data/lib/specify/models/app_resource_data.rb +32 -0
- data/lib/specify/models/app_resource_dir.rb +43 -0
- data/lib/specify/models/auto_numbering_scheme.rb +94 -0
- data/lib/specify/models/collecting_event.rb +38 -0
- data/lib/specify/models/collection.rb +67 -0
- data/lib/specify/models/collection_object.rb +127 -0
- data/lib/specify/models/createable.rb +21 -0
- data/lib/specify/models/determination.rb +63 -0
- data/lib/specify/models/discipline.rb +61 -0
- data/lib/specify/models/division.rb +26 -0
- data/lib/specify/models/geography.rb +5 -0
- data/lib/specify/models/geography/administrative_division.rb +32 -0
- data/lib/specify/models/geography/geographic_name.rb +66 -0
- data/lib/specify/models/geography/geography.rb +23 -0
- data/lib/specify/models/institution.rb +13 -0
- data/lib/specify/models/locality.rb +50 -0
- data/lib/specify/models/preparation.rb +53 -0
- data/lib/specify/models/preparation_type.rb +30 -0
- data/lib/specify/models/record_set.rb +55 -0
- data/lib/specify/models/record_set_item.rb +29 -0
- data/lib/specify/models/taxonomy.rb +6 -0
- data/lib/specify/models/taxonomy/common_name.rb +14 -0
- data/lib/specify/models/taxonomy/rank.rb +31 -0
- data/lib/specify/models/taxonomy/taxon.rb +54 -0
- data/lib/specify/models/taxonomy/taxonomy.rb +21 -0
- data/lib/specify/models/tree_queryable.rb +55 -0
- data/lib/specify/models/updateable.rb +20 -0
- data/lib/specify/models/user.rb +104 -0
- data/lib/specify/models/view_set_object.rb +32 -0
- data/lib/specify/number_format.rb +60 -0
- data/lib/specify/services.rb +18 -0
- data/lib/specify/services/service.rb +51 -0
- data/lib/specify/services/stub_generator.rb +291 -0
- data/lib/specify/services/view_loader.rb +177 -0
- data/lib/specify/session.rb +77 -0
- data/lib/specify/user_type.rb +61 -0
- data/lib/specify/version.rb +19 -0
- data/man/specify_cli-database.1 +60 -0
- data/man/specify_cli-database.1.html +137 -0
- data/man/specify_cli-database.1.ronn +53 -0
- data/man/specify_cli-repository.1 +55 -0
- data/man/specify_cli-repository.1.html +128 -0
- data/man/specify_cli-repository.1.ronn +42 -0
- data/man/specify_cli-stubs.1 +177 -0
- data/man/specify_cli-stubs.1.html +239 -0
- data/man/specify_cli-stubs.1.ronn +147 -0
- data/man/specify_cli-viewset.1 +92 -0
- data/man/specify_cli-viewset.1.html +154 -0
- data/man/specify_cli-viewset.1.ronn +72 -0
- data/man/specify_cli.1 +213 -0
- data/man/specify_cli.1.html +252 -0
- data/man/specify_cli.1.ronn +157 -0
- data/spec/branch_parser_spec.rb +94 -0
- data/spec/cli/stubs_spec.rb +44 -0
- data/spec/configuration/config_spec.rb +269 -0
- data/spec/configuration/db_config_spec.rb +299 -0
- data/spec/configuration/host_config_spec.rb +64 -0
- data/spec/database_spec.rb +83 -0
- data/spec/examples.txt +217 -0
- data/spec/helpers.rb +15 -0
- data/spec/models/app_resource_data_spec.rb +38 -0
- data/spec/models/app_resource_dir_spec.rb +8 -0
- data/spec/models/auto_numbering_scheme_spec.rb +78 -0
- data/spec/models/collection_object_spec.rb +92 -0
- data/spec/models/collection_spec.rb +32 -0
- data/spec/models/discipline_spec.rb +31 -0
- data/spec/models/record_set_spec.rb +18 -0
- data/spec/models/user_spec.rb +182 -0
- data/spec/models/view_set_object_spec.rb +70 -0
- data/spec/number_format_spec.rb +43 -0
- data/spec/services/stub_generator_spec.rb +635 -0
- data/spec/services/view_loader_spec.rb +436 -0
- data/spec/session_spec.rb +105 -0
- data/spec/spec_helper.rb +116 -0
- data/spec/support/db.yml +12 -0
- data/spec/support/stub.yaml +17 -0
- data/spec/support/stub_locality.yaml +19 -0
- data/spec/support/viewsets/paleo.views.xml +30 -0
- data/spec/support/viewsets/paleo.xml +30 -0
- data/spec/user_type_spec.rb +79 -0
- data/specify_cli.gemspec +27 -0
- data/specify_cli.rdoc +1 -0
- 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
|