qa 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/LICENSE +15 -0
  2. data/README.md +174 -0
  3. data/Rakefile +10 -0
  4. data/app/assets/javascripts/qa/application.js +13 -0
  5. data/app/assets/stylesheets/qa/application.css +13 -0
  6. data/app/controllers/qa/application_controller.rb +4 -0
  7. data/app/controllers/qa/terms_controller.rb +76 -0
  8. data/app/helpers/qa/application_helper.rb +2 -0
  9. data/app/models/qa/mesh_tree.rb +37 -0
  10. data/app/models/qa/subject_mesh_term.rb +45 -0
  11. data/app/views/layouts/qa/application.html.erb +14 -0
  12. data/config/authorities.yml +1 -0
  13. data/config/authorities/states.yml +101 -0
  14. data/config/initializers/authorities.rb +1 -0
  15. data/config/oclcts-authorities.yml +24 -0
  16. data/config/routes.rb +7 -0
  17. data/db/migrate/20130917200611_create_qa_subject_mesh_terms.rb +11 -0
  18. data/db/migrate/20130917201026_create_qa_mesh_tree.rb +10 -0
  19. data/db/migrate/20130918141523_add_term_lower_to_qa_subject_mesh_terms.rb +7 -0
  20. data/lib/qa.rb +9 -0
  21. data/lib/qa/authorities.rb +12 -0
  22. data/lib/qa/authorities/base.rb +50 -0
  23. data/lib/qa/authorities/lcsh.rb +46 -0
  24. data/lib/qa/authorities/loc.rb +200 -0
  25. data/lib/qa/authorities/local.rb +69 -0
  26. data/lib/qa/authorities/mesh.rb +20 -0
  27. data/lib/qa/authorities/mesh_tools.rb +6 -0
  28. data/lib/qa/authorities/mesh_tools/mesh_data_parser.rb +65 -0
  29. data/lib/qa/authorities/mesh_tools/mesh_importer.rb +41 -0
  30. data/lib/qa/authorities/oclcts.rb +63 -0
  31. data/lib/qa/authorities/tgnlang.rb +40 -0
  32. data/lib/qa/data/TGN_LANGUAGES.xml +7435 -0
  33. data/lib/qa/engine.rb +5 -0
  34. data/lib/qa/version.rb +3 -0
  35. data/lib/tasks/qa_tasks.rake +4 -0
  36. data/spec/controllers/terms_controller_spec.rb +70 -0
  37. data/spec/fixtures/authorities/authority_A.yml +10 -0
  38. data/spec/fixtures/authorities/authority_B.yml +7 -0
  39. data/spec/fixtures/authorities/authority_C.yml +4 -0
  40. data/spec/fixtures/lcsh-response.txt +1 -0
  41. data/spec/fixtures/loc-response.txt +147 -0
  42. data/spec/fixtures/mesh.txt +334 -0
  43. data/spec/fixtures/oclcts-response-mesh-1.txt +28 -0
  44. data/spec/fixtures/oclcts-response-mesh-2.txt +253 -0
  45. data/spec/fixtures/oclcts-response-mesh-3.txt +28 -0
  46. data/spec/internal/Gemfile +48 -0
  47. data/spec/internal/Gemfile.lock +156 -0
  48. data/spec/internal/README.rdoc +28 -0
  49. data/spec/internal/Rakefile +6 -0
  50. data/spec/internal/app/assets/javascripts/application.js +16 -0
  51. data/spec/internal/app/assets/stylesheets/application.css +13 -0
  52. data/spec/internal/app/controllers/application_controller.rb +5 -0
  53. data/spec/internal/app/helpers/application_helper.rb +2 -0
  54. data/spec/internal/app/views/layouts/application.html.erb +14 -0
  55. data/spec/internal/bin/bundle +3 -0
  56. data/spec/internal/bin/rails +4 -0
  57. data/spec/internal/bin/rake +4 -0
  58. data/spec/internal/config.ru +4 -0
  59. data/spec/internal/config/application.rb +23 -0
  60. data/spec/internal/config/boot.rb +4 -0
  61. data/spec/internal/config/database.yml +25 -0
  62. data/spec/internal/config/environment.rb +5 -0
  63. data/spec/internal/config/environments/development.rb +29 -0
  64. data/spec/internal/config/environments/production.rb +80 -0
  65. data/spec/internal/config/environments/test.rb +36 -0
  66. data/spec/internal/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/internal/config/initializers/filter_parameter_logging.rb +4 -0
  68. data/spec/internal/config/initializers/inflections.rb +16 -0
  69. data/spec/internal/config/initializers/mime_types.rb +5 -0
  70. data/spec/internal/config/initializers/secret_token.rb +12 -0
  71. data/spec/internal/config/initializers/session_store.rb +3 -0
  72. data/spec/internal/config/initializers/wrap_parameters.rb +14 -0
  73. data/spec/internal/config/locales/en.yml +23 -0
  74. data/spec/internal/config/oclcts-authorities.yml +24 -0
  75. data/spec/internal/config/routes.rb +60 -0
  76. data/spec/internal/db/development.sqlite3 +0 -0
  77. data/spec/internal/db/migrate/20130930151844_create_qa_subject_mesh_terms.qa.rb +12 -0
  78. data/spec/internal/db/migrate/20130930151845_create_qa_mesh_tree.qa.rb +11 -0
  79. data/spec/internal/db/migrate/20130930151846_add_term_lower_to_qa_subject_mesh_terms.qa.rb +8 -0
  80. data/spec/internal/db/schema.rb +34 -0
  81. data/spec/internal/db/seeds.rb +7 -0
  82. data/spec/internal/db/test.sqlite3 +0 -0
  83. data/spec/internal/lib/generators/test_app_generator.rb +20 -0
  84. data/spec/internal/log/development.log +193 -0
  85. data/spec/internal/public/404.html +58 -0
  86. data/spec/internal/public/422.html +58 -0
  87. data/spec/internal/public/500.html +57 -0
  88. data/spec/internal/public/favicon.ico +0 -0
  89. data/spec/internal/public/robots.txt +5 -0
  90. data/spec/internal/test/test_helper.rb +15 -0
  91. data/spec/lib/authorities_lcsh_spec.rb +61 -0
  92. data/spec/lib/authorities_loc_spec.rb +35 -0
  93. data/spec/lib/authorities_local_spec.rb +89 -0
  94. data/spec/lib/authorities_mesh_spec.rb +38 -0
  95. data/spec/lib/authorities_oclcts_spec.rb +49 -0
  96. data/spec/lib/authorities_tgnlang_spec.rb +22 -0
  97. data/spec/lib/mesh_data_parser_spec.rb +125 -0
  98. data/spec/models/subject_mesh_term_spec.rb +49 -0
  99. data/spec/spec_helper.rb +60 -0
  100. data/spec/support/lib/generators/test_app_generator.rb +20 -0
  101. metadata +396 -0
@@ -0,0 +1 @@
1
+ AUTHORITIES_CONFIG = YAML.load_file(File.expand_path("../../authorities.yml", __FILE__))
@@ -0,0 +1,24 @@
1
+ url-pattern:
2
+ prefix-query: http://tspilot.oclc.org/{authority-id}/?query=oclcts.rootHeading+exact+%22{query}*%22&version=1.1&operation=searchRetrieve&recordSchema=http%3A%2F%2Fzthes.z3950.org%2Fxml%2F1.0%2F&maximumRecords=10&startRecord=1&resultSetTTL=300&recordPacking=xml&recordXPath=&sortKeys=
3
+ id-lookup: http://tspilot.oclc.org/{authority-id}/?query=dc.identifier+exact+%22{id}%22&version=1.1&operation=searchRetrieve&recordSchema=http%3A%2F%2Fzthes.z3950.org%2Fxml%2F1.0%2F&maximumRecords=10&startRecord=1&resultSetTTL=300&recordPacking=xml&recordXPath=&sortKeys=
4
+ authorities:
5
+ lcgft:
6
+ name: Library of Congress Genre/Form Terms for Library and Archival Materials (LCGFT)
7
+ bisacsh:
8
+ name: Book Industry Study Group Subject Headings (BISAC®). Used with permission.
9
+ fast:
10
+ name: Faceted Application of Subject Terminology (FAST subject headings)
11
+ gsafd:
12
+ name: Form and genre headings for fiction and drama
13
+ lcshac:
14
+ name: Library of Congress AC Subject Headings
15
+ lcsh:
16
+ name: Library of Congress Subject Headings
17
+ mesh:
18
+ name: Medical Subject Headings (MeSH®)
19
+ lctgm:
20
+ name: "Thesaurus for graphic materials: TGM I, Subject terms"
21
+ gmgpc:
22
+ name: "Thesaurus for graphic materials: TGM II, Genre terms"
23
+ meta:
24
+ name: Controlled Vocabulary Metadata
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Qa::Engine.routes.draw do
2
+ match "/search/:vocab" => "terms#search", :via=>:get
3
+ match "/search/:vocab/:sub_authority" => "terms#search", :via=>:get
4
+ match "/terms/:vocab" => "terms#index", :via=>:get
5
+ match "/terms/:vocab/:sub_authority" => "terms#index", :via=>:get
6
+ match "/terms" => "terms#index", :via=>:get
7
+ end
@@ -0,0 +1,11 @@
1
+ class CreateQaSubjectMeshTerms < ActiveRecord::Migration
2
+ def change
3
+ create_table :qa_subject_mesh_terms do |t|
4
+ t.string :term_id
5
+ t.string :term
6
+ t.text :synonyms
7
+ end
8
+ add_index :qa_subject_mesh_terms, :term_id
9
+ add_index :qa_subject_mesh_terms, :term
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ class CreateQaMeshTree < ActiveRecord::Migration
2
+ def change
3
+ create_table :qa_mesh_trees do |t|
4
+ t.string :term_id
5
+ t.string :tree_number
6
+ end
7
+ add_index :qa_mesh_trees, :term_id
8
+ add_index :qa_mesh_trees, :tree_number
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ class AddTermLowerToQaSubjectMeshTerms < ActiveRecord::Migration
2
+ def change
3
+ add_column :qa_subject_mesh_terms, :term_lower, :string
4
+ add_index :qa_subject_mesh_terms, :term_lower
5
+ remove_index :qa_subject_mesh_terms, :term
6
+ end
7
+ end
data/lib/qa.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "qa/engine"
2
+ require "active_record"
3
+ require "activerecord-import"
4
+
5
+ module Qa
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Authorities
9
+ end
@@ -0,0 +1,12 @@
1
+ module Qa::Authorities
2
+ extend ActiveSupport::Autoload
3
+
4
+ autoload :Base
5
+ autoload :Lcsh
6
+ autoload :Loc
7
+ autoload :Local
8
+ autoload :Mesh
9
+ autoload :MeshTools
10
+ autoload :Oclcts
11
+ autoload :Tgnlang
12
+ end
@@ -0,0 +1,50 @@
1
+ require 'curl'
2
+
3
+ module Qa::Authorities
4
+ class Base
5
+ attr_accessor :response, :query_url, :raw_response
6
+
7
+ def initialize(q, sub_authority='')
8
+ # Implement Me and set self.query_url
9
+
10
+ if self.query_url == nil
11
+ raise Exception 'query url in your authorities lib is not set (implement in initialize)'
12
+ end
13
+
14
+ #Default implementation assumed query_url is set
15
+ http = Curl.get(self.query_url) do |http|
16
+ http.headers['Accept'] = 'application/json'
17
+ end
18
+
19
+ self.raw_response = JSON.parse(http.body_str)
20
+ end
21
+
22
+ def self.authority_valid?(sub_authority)
23
+ sub_authority == nil || sub_authorities.include?(sub_authority)
24
+ end
25
+
26
+ def self.sub_authorities
27
+ [] #Overwrite if you have sub_authorities
28
+ end
29
+
30
+ def parse_authority_response
31
+ # Overwrite me unless your raw response needs no parsing
32
+ self.response = self.raw_response
33
+
34
+ end
35
+
36
+ def get_full_record(id)
37
+ # implement me
38
+ {"id"=>id}.to_json
39
+ end
40
+
41
+ # Parse the result from LOC, and return an JSON array of terms that match the query.
42
+ def results
43
+ self.response.to_json
44
+ end
45
+
46
+ # TODO: there's other info in the self.response that might be worth making access to, such as
47
+ # RDF links, etc.
48
+
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ require 'uri'
2
+
3
+ module Qa::Authorities
4
+ class Lcsh < Qa::Authorities::Base
5
+
6
+ # Initialze the Lcsh class with a query and get the http response from LOC's server.
7
+ # This is set to a JSON object
8
+ def initialize(q, sub_authority='')
9
+ self.query_url= "http://id.loc.gov/authorities/suggest/?q=" + q
10
+
11
+ super
12
+ end
13
+
14
+ # Format response to the correct JSON structure
15
+ def parse_authority_response
16
+ self.response = build_response
17
+ end
18
+
19
+ def query
20
+ self.raw_response[0]
21
+ end
22
+
23
+ def suggestions
24
+ self.raw_response[1]
25
+ end
26
+
27
+ def urls_for_suggestions
28
+ self.raw_response[3]
29
+ end
30
+
31
+ private
32
+
33
+ def build_response a = Array.new
34
+ self.suggestions.each_index do |i|
35
+ a << {"id"=>get_id_from_url(urls_for_suggestions[i]), "label"=>suggestions[i]}
36
+ end
37
+ return a
38
+ end
39
+
40
+ def get_id_from_url url
41
+ uri = URI(url)
42
+ return uri.path.split(/\//).last
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,200 @@
1
+ require 'uri'
2
+
3
+ module Qa::Authorities
4
+ class Loc < Qa::Authorities::Base
5
+
6
+ # Initialze the Loc class with a query and get the http response from LOC's server.
7
+ # This is set to a JSON object
8
+ def initialize(q, sub_authority='')
9
+ q += '*'
10
+ authority_url = sub_authorityURL(sub_authority)
11
+ self.query_url = "http://id.loc.gov/search/?q=#{q}&q=#{authority_url}&format=json"
12
+
13
+ super
14
+ end
15
+
16
+ def sub_authorityURL(sub_authority)
17
+ vocab_base_url = 'cs%3Ahttp%3A%2F%2Fid.loc.gov%2Fvocabulary%2F'
18
+ authority_base_url = 'cs%3Ahttp%3A%2F%2Fid.loc.gov%2Fauthorities%2F'
19
+ datatype_base_url = 'cs%3Ahttp%3A%2F%2Fid.loc.gov%2Fdatatypes%2F'
20
+ vocab_preservation_base_url = 'cs%3Ahttp%3A%2F%2Fid.loc.gov%2Fvocabulary%2Fpreservation%2F'
21
+
22
+ case sub_authority
23
+ when 'subjects'
24
+ return authority_base_url + URI.escape(sub_authority)
25
+ when 'names'
26
+ return authority_base_url + URI.escape(sub_authority)
27
+ when 'classification'
28
+ return authority_base_url + URI.escape(sub_authority)
29
+ when 'childrensSubjects'
30
+ return authority_base_url + URI.escape(sub_authority)
31
+ when 'genreForms'
32
+ return authority_base_url + URI.escape(sub_authority)
33
+ when 'graphicMaterials'
34
+ return vocab_base_url + URI.escape(sub_authority)
35
+ when 'organizations'
36
+ return vocab_base_url + URI.escape(sub_authority)
37
+ when 'relators'
38
+ return vocab_base_url + URI.escape(sub_authority)
39
+ when 'countries'
40
+ return vocab_base_url + URI.escape(sub_authority)
41
+ when 'geographicAreas'
42
+ return vocab_base_url + URI.escape(sub_authority)
43
+ when 'languages'
44
+ return vocab_base_url + URI.escape(sub_authority)
45
+ when 'iso639-1'
46
+ return vocab_base_url + URI.escape(sub_authority)
47
+ when 'iso639-2'
48
+ return vocab_base_url + URI.escape(sub_authority)
49
+ when 'iso639-5'
50
+ return vocab_base_url + URI.escape(sub_authority)
51
+ when 'edtf'
52
+ return datatype_base_url + URI.escape(sub_authority)
53
+ when 'preservation'
54
+ return vocab_base_url + URI.escape(sub_authority)
55
+ when 'actionsGranted'
56
+ return vocab_base_url + URI.escape(sub_authority)
57
+ when 'agentType'
58
+ return vocab_base_url + URI.escape(sub_authority)
59
+ when 'contentLocationType'
60
+ return vocab_preservation_base_url + URI.escape(sub_authority)
61
+ when 'copyrightStatus'
62
+ return vocab_preservation_base_url + URI.escape(sub_authority)
63
+ when 'cryptographicHashFunctions'
64
+ return vocab_preservation_base_url + URI.escape(sub_authority)
65
+ when 'environmentCharacteristic'
66
+ return vocab_preservation_base_url + URI.escape(sub_authority)
67
+ when 'environmentPurpose'
68
+ return vocab_preservation_base_url + URI.escape(sub_authority)
69
+ when 'eventRelatedAgentRole'
70
+ return vocab_preservation_base_url + URI.escape(sub_authority)
71
+ when 'eventRelatedObjectRole'
72
+ return vocab_preservation_base_url + URI.escape(sub_authority)
73
+ when 'eventType'
74
+ return vocab_preservation_base_url + URI.escape(sub_authority)
75
+ when 'formatRegistryRole'
76
+ return vocab_preservation_base_url + URI.escape(sub_authority)
77
+ when 'hardwareType'
78
+ return vocab_preservation_base_url + URI.escape(sub_authority)
79
+ when 'inhibitorTarget'
80
+ return vocab_preservation_base_url + URI.escape(sub_authority)
81
+ when 'inhibitorType'
82
+ return vocab_preservation_base_url + URI.escape(sub_authority)
83
+ when 'objectCategory'
84
+ return vocab_preservation_base_url + URI.escape(sub_authority)
85
+ when 'preservationLevelRole'
86
+ return vocab_preservation_base_url + URI.escape(sub_authority)
87
+ when 'relationshipSubType'
88
+ return vocab_preservation_base_url + URI.escape(sub_authority)
89
+ when 'relationshipType'
90
+ return vocab_preservation_base_url + URI.escape(sub_authority)
91
+ when 'rightsBasis'
92
+ return vocab_preservation_base_url + URI.escape(sub_authority)
93
+ when 'rightsRelatedAgentRole'
94
+ return vocab_preservation_base_url + URI.escape(sub_authority)
95
+ when 'signatureEncoding'
96
+ return vocab_preservation_base_url + URI.escape(sub_authority)
97
+ when 'signatureMethod'
98
+ return vocab_preservation_base_url + URI.escape(sub_authority)
99
+ when 'softwareType'
100
+ return vocab_preservation_base_url + URI.escape(sub_authority)
101
+ when 'storageMedium'
102
+ return vocab_preservation_base_url + URI.escape(sub_authority)
103
+ else
104
+ return ''
105
+ end
106
+ end
107
+
108
+ def self.sub_authorities
109
+ ['iso639-2', 'subjects', 'names', 'classification', 'childrensSubjects', 'genreForms', 'graphicMaterials', 'organizations', 'relators', 'countries', 'geographicAreas', 'languages', 'iso639-5', 'edtf', 'preservation', 'actionsGranted', 'agentType', 'contentLocationType', 'copyrightStatus', 'cryptographicHashFunctions', 'environmentCharacteristic', 'environmentPurpose', 'eventRelatedAgentRole', 'eventRelatedObjectRole', 'eventType', 'formatRegistryRole', 'hardwareType', 'inhibitorTarget', 'inhibitorType', 'objectCategory', 'preservationLevelRole', 'relationshipSubType', 'relationshipType', 'rightsBasis', 'rightsRelatedAgentRole', 'signatureEncoding', 'signatureMethod', 'softwareType', 'storageMedium']
110
+ end
111
+
112
+
113
+ def parse_authority_response
114
+ result = []
115
+ self.raw_response.each do |single_response|
116
+ if single_response[0] == "atom:entry"
117
+ id = nil
118
+ label = ''
119
+ single_response.each do |result_part|
120
+ if(result_part[0] == 'atom:title')
121
+ label = result_part[2]
122
+ end
123
+
124
+ if(result_part[0] == 'atom:id')
125
+ id = result_part[2]
126
+ end
127
+
128
+ end
129
+
130
+ id ||= label
131
+ result << {"id"=>id, "label"=>label}
132
+
133
+ end
134
+ end
135
+ self.response = result
136
+ end
137
+
138
+ def get_full_record(id)
139
+ full_record = nil
140
+ parsed_result = {}
141
+ self.raw_response.each do |single_response|
142
+ if single_response[0] == "atom:entry"
143
+
144
+ single_response.each do |result_part|
145
+ if(result_part[0] == 'atom:title')
146
+ if id == result_part[2]
147
+ full_record = single_response
148
+ end
149
+ end
150
+
151
+ if(result_part[0] == 'atom:id')
152
+ if id == result_part[2]
153
+ full_record = single_response
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+
163
+ if full_record != nil
164
+ full_record.each do |section|
165
+ if section.class == Array
166
+ label = section[0].split(':').last.to_s
167
+ case label
168
+ when 'title'
169
+ parsed_result[label] = section[2]
170
+ when 'link'
171
+ if section[1]['type'] != nil
172
+ parsed_result[label + "||#{section[1]['type']}"] = section[1]["href"]
173
+ else
174
+ parsed_result[label] = section[1]["href"]
175
+ end
176
+ when 'id'
177
+ parsed_result[label] = section[2]
178
+ when 'author'
179
+ author_list = []
180
+ #FIXME: Find example with two authors to better understand this data.
181
+ author_list << section[2][2]
182
+ parsed_result[label] = author_list
183
+ when 'updated'
184
+ parsed_result[label] = section[2]
185
+ when 'created'
186
+ parsed_result[label] = section[2]
187
+ end
188
+
189
+ end
190
+ end
191
+ else
192
+ raise Exception 'Lookup without using a result search first not implemented yet'
193
+ end
194
+
195
+ parsed_result
196
+
197
+ end
198
+
199
+ end
200
+ end
@@ -0,0 +1,69 @@
1
+ module Qa::Authorities
2
+ class Local < Qa::Authorities::Base
3
+
4
+ attr_accessor :response
5
+
6
+ def initialize(q, sub_authority)
7
+ begin
8
+ sub_authority_hash = YAML.load(File.read(File.join(Qa::Authorities::Local.sub_authorities_path, "#{sub_authority}.yml")))
9
+ rescue
10
+ sub_authority_hash = {}
11
+ end
12
+ @terms = normalize_terms(sub_authority_hash.fetch(:terms, []))
13
+ if q.blank?
14
+ self.response = @terms
15
+ else
16
+ sub_terms = []
17
+ @terms.each { |term| sub_terms << term if term[:term].downcase.start_with?(q.downcase) }
18
+ self.response = sub_terms
19
+ end
20
+ end
21
+
22
+ def normalize_terms(terms)
23
+ normalized_terms = []
24
+ terms.each do |term|
25
+ if term.is_a? String
26
+ normalized_terms << { :id => term, :term => term }
27
+ else
28
+ term[:id] = term[:id] || term[:term]
29
+ normalized_terms << term
30
+ end
31
+ end
32
+ normalized_terms
33
+ end
34
+
35
+ def parse_authority_response
36
+ parsed_response = []
37
+ self.response.each do |res|
38
+ parsed_response << { :id => res[:id], :label => res[:term] }
39
+ end
40
+ self.response = parsed_response
41
+ end
42
+
43
+ def get_full_record(id)
44
+ target_term = {}
45
+ @terms.each do |term|
46
+ if term[:id] == id
47
+ target_term = term
48
+ end
49
+ end
50
+ target_term.to_json
51
+ end
52
+
53
+ def self.sub_authorities_path
54
+ config_path = AUTHORITIES_CONFIG[:local_path]
55
+ if config_path.starts_with?(File::Separator)
56
+ config_path
57
+ else
58
+ File.join(Rails.root, config_path)
59
+ end
60
+ end
61
+
62
+ def self.sub_authorities
63
+ sub_auths = []
64
+ Dir.foreach(Qa::Authorities::Local.sub_authorities_path) { |file| sub_auths << File.basename(file, File.extname(file)) }
65
+ sub_auths
66
+ end
67
+
68
+ end
69
+ end