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.
- 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
|