psc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +13 -0
  2. data/.rvmrc +2 -0
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE +20 -0
  7. data/README.md +163 -0
  8. data/Rakefile +69 -0
  9. data/cucumber.yml +13 -0
  10. data/features/step_definitions/eval_steps.rb +14 -0
  11. data/features/step_definitions/setup_steps.rb +7 -0
  12. data/features/studies.feature +21 -0
  13. data/features/support/env.rb +33 -0
  14. data/features/support/int_psc.rb +165 -0
  15. data/int-psc/README-int-psc.md +57 -0
  16. data/int-psc/hsqldb/baseline.log +197 -0
  17. data/int-psc/hsqldb/baseline.properties +17 -0
  18. data/int-psc/hsqldb/baseline.script +295 -0
  19. data/int-psc/hsqldb/datasource.properties +18 -0
  20. data/int-psc/hsqldb/datasource.script +2205 -0
  21. data/int-psc/jetty/jetty-runner-7.4.0.v20110414.jar +0 -0
  22. data/int-psc/state/ABC 1200.xml +115 -0
  23. data/int-psc/state/int-psc-state.xml +64 -0
  24. data/lib/psc.rb +47 -0
  25. data/lib/psc/client.rb +35 -0
  26. data/lib/psc/connection.rb +96 -0
  27. data/lib/psc/faraday.rb +11 -0
  28. data/lib/psc/faraday/http_basic.rb +35 -0
  29. data/lib/psc/faraday/psc_token.rb +42 -0
  30. data/lib/psc/faraday/string_is_xml.rb +25 -0
  31. data/lib/psc/version.rb +3 -0
  32. data/meta.rakefile +30 -0
  33. data/psc.gemspec +33 -0
  34. data/spec/fixtures/studies-json.http +37 -0
  35. data/spec/middleware_helper.rb +18 -0
  36. data/spec/psc/client_spec.rb +39 -0
  37. data/spec/psc/connection_spec.rb +156 -0
  38. data/spec/psc/faraday/http_basic_spec.rb +15 -0
  39. data/spec/psc/faraday/psc_token_spec.rb +38 -0
  40. data/spec/psc/faraday/string_is_xml_spec.rb +49 -0
  41. data/spec/psc/version_spec.rb +11 -0
  42. data/spec/psc_spec.rb +16 -0
  43. data/spec/spec_helper.rb +15 -0
  44. data/tasks/int-psc.rake +84 -0
  45. data/tasks/psc/TODO +3 -0
  46. data/tasks/psc/state.rb +393 -0
  47. metadata +311 -0
@@ -0,0 +1,11 @@
1
+ require File.expand_path("../../spec_helper.rb", __FILE__)
2
+
3
+ describe Psc, "::VERSION" do
4
+ it "exists" do
5
+ lambda { Psc::VERSION }.should_not raise_error
6
+ end
7
+
8
+ it "has 3 or 4 dot separated parts" do
9
+ Psc::VERSION.split('.').size.should be_between(3, 4)
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe Psc do
4
+ describe '.xml' do
5
+ it "provides an xml builder configured for PSC XML" do
6
+ Psc.xml('period', :id => 'foo') { |xml| xml.tag!('planned-activity') }.should ==
7
+ "<period id=\"foo\" xmlns=\"http://bioinformatics.northwestern.edu/ns/psc\">\n <planned-activity/>\n</period>\n"
8
+ end
9
+ end
10
+
11
+ describe '.xml_namespace' do
12
+ it 'is a hash suitable for use with nokogiri xpath' do
13
+ Psc.xml_namespace['psc'].should == 'http://bioinformatics.northwestern.edu/ns/psc'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+
4
+ require 'rspec'
5
+ require 'webmock/rspec'
6
+ require File.expand_path("../middleware_helper.rb", __FILE__)
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
9
+ require 'psc'
10
+
11
+ RSpec.configure do
12
+ def http_fixture(name)
13
+ File.new(File.expand_path("../fixtures/#{name}.http", __FILE__))
14
+ end
15
+ end
@@ -0,0 +1,84 @@
1
+ require 'faraday'
2
+
3
+ require File.expand_path('../psc/state.rb', __FILE__)
4
+ require File.expand_path('../../features/support/int_psc.rb', __FILE__)
5
+
6
+ namespace 'int-psc' do
7
+ desc 'Ensure a psc.war is available for tests. Downloads the latest nightly if not.'
8
+ task :war => IntPsc.warfile
9
+
10
+ namespace :war do
11
+ directory IntPsc.path('downloads')
12
+
13
+ file IntPsc.path('downloads', 'archive.zip') => IntPsc.path('downloads') do |task|
14
+ sh [
15
+ 'curl',
16
+ '-o', task.name,
17
+ "'https://ctms-ci.nubic.northwestern.edu/hudson/job/PSC%20nightly%20distribution/lastSuccessfulBuild/artifact/*zip*/archive.zip'"
18
+ ].join(' ')
19
+ end
20
+
21
+ file IntPsc.warfile do
22
+ # only download if a psc.war wasn't separately provided
23
+ task(IntPsc.path('downloads', 'archive.zip')).invoke
24
+
25
+ cd IntPsc.path('downloads') do
26
+ sh "unzip -o archive.zip"
27
+ distpkg = Dir['archive/psc/target/artifacts/psc*.zip'].first
28
+ fail "No dist package in downloaded archive" unless distpkg
29
+ sh "unzip -o '#{distpkg}'"
30
+ pkgwar = Dir['psc-*/psc.war'].first
31
+ fail "No war in dist package" unless pkgwar
32
+
33
+ mkdir_p File.dirname(IntPsc.warfile)
34
+ cp pkgwar, IntPsc.warfile
35
+ end
36
+ end
37
+ end
38
+
39
+ task :clean_baseline do
40
+ rm_rf IntPsc.path('hsqldb')
41
+ end
42
+
43
+ task :recreate_baseline => [IntPsc.warfile, :clean_baseline] do
44
+ IntPsc.run('baseline') do
45
+ puts
46
+ puts "Please run through PSC's setup flow and create an all-powerful user with the"
47
+ puts "credentials superuser/superuser. PSC is running at"
48
+ puts
49
+ puts " #{IntPsc.url}"
50
+ puts
51
+ puts "When complete, come back here and press any key."
52
+ puts
53
+ STDIN.getc
54
+ end
55
+ end
56
+
57
+ task :clean_datasource do
58
+ cd IntPsc.path('hsqldb') do
59
+ Dir['baseline.*'].each { |fn| cp fn, fn.sub(/^baseline/, 'datasource') }
60
+ end
61
+ end
62
+
63
+ desc 'Recreate the PSC integrated test instance from the state data in int-psc/state'
64
+ task :rebuild => [IntPsc.warfile, :clean_datasource] do
65
+ IntPsc.run do |int_psc|
66
+ int_psc.apply_state_and_mark_readonly
67
+ end
68
+ # the copied log file is not needed in the locked database
69
+ rm path('hsqldb', 'datasource.log')
70
+ end
71
+
72
+ desc 'Start up the integrated test PSC instance to poke around'
73
+ task :examine do
74
+ IntPsc.run do
75
+ puts "Integrated test PSC running at #{IntPsc.url}.\nPress any key to shut down."
76
+ STDIN.getc
77
+ end
78
+ end
79
+
80
+ desc 'Purge the logs for the integration test PSC instance'
81
+ task :clean_logs do
82
+ rm_rf IntPsc.path('deploy-base', 'logs')
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ # TODO: state.rb is copied from PSC's demo creator tool for
2
+ # bootstrapping purposes. Eventually, it should be cleaned up and
3
+ # added as feature of psc.rb.
@@ -0,0 +1,393 @@
1
+ require 'faraday'
2
+ require 'nokogiri'
3
+ require 'builder'
4
+ require 'highline'
5
+
6
+ module Psc
7
+ class State
8
+ attr_accessor :sites, :templates, :registrations
9
+
10
+ def apply(connection)
11
+ StateApplier.new(self).apply(connection)
12
+ end
13
+
14
+ def template(ident)
15
+ templates.detect { |t| t.assigned_identifier == ident } or fail "No template #{ident}"
16
+ end
17
+
18
+ def self.from_file(path)
19
+ self.new.tap do |state|
20
+ doc = Nokogiri::XML(open(path))
21
+ state.sites = read_sites(doc)
22
+ state.templates = read_templates(doc, File.dirname(path))
23
+ state.registrations = read_registrations(doc)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def self.parse_date(s)
30
+ case s
31
+ when /(\d{4})-(\d{1,2})-(\d{1,2})/
32
+ Date.new($1.to_i, $2.to_i, $3.to_i)
33
+ when /^\d+$/
34
+ RelativeDate.new s.to_i
35
+ end
36
+ end
37
+
38
+ def self.read_sites(doc)
39
+ doc.xpath('/psc-state/site').collect do |site_elt|
40
+ Site.new(site_elt['name'], site_elt['assigned-identifier'])
41
+ end
42
+ end
43
+
44
+ def self.read_templates(doc, basedir)
45
+ doc.xpath('/psc-state/template').collect do |t_elt|
46
+ ident = t_elt['assigned-identifier']
47
+ Template.create(ident,
48
+ :filename =>
49
+ if filename = t_elt['file']
50
+ if filename =~ %r{^/}
51
+ filename
52
+ else
53
+ File.join(basedir, filename)
54
+ end
55
+ else
56
+ File.join(basedir, "#{ident}.xml")
57
+ end,
58
+ :participating_sites => t_elt.xpath('participating-site').collect { |ps_elt|
59
+ ParticipatingSite.create(
60
+ ps_elt['assigned-identifier'],
61
+ :approval => case ps_elt['approval']
62
+ when "false"
63
+ false
64
+ else
65
+ parse_date(ps_elt['approval'])
66
+ end
67
+ )
68
+ })
69
+ end
70
+ end
71
+
72
+ def self.read_registrations(doc)
73
+ doc.xpath('/psc-state/registration').collect do |reg_elt|
74
+ Registration.create(
75
+ :subject => read_subject(reg_elt.xpath('subject').first),
76
+ :study_sites => reg_elt.xpath('study-site').collect { |study_elt|
77
+ StudySite.create(
78
+ %w(template site primary_coordinator study_subject_identifier desired_assignment_identifier).
79
+ inject({}) { |h, k| h[k] = study_elt[k.gsub('_', '-')]; h }.
80
+ merge(
81
+ :scheduled_segments => study_elt.xpath('scheduled-segment').collect { |seg_elt|
82
+ ScheduledSegment.create(seg_elt['segment'],
83
+ :mode => seg_elt['mode'],
84
+ :start => parse_date(seg_elt['start'])
85
+ )
86
+ })
87
+ )
88
+ })
89
+ end
90
+ end
91
+
92
+ def self.read_subject(subject_elt)
93
+ Subject.create(
94
+ %w(first_name last_name person_id gender).inject({}) { |h, k|
95
+ h[k] = subject_elt[k.sub('_', '-')]; h
96
+ }.merge(
97
+ :birth_date => parse_date(subject_elt['birth-date']),
98
+ :properties => subject_elt.xpath('subject-property').
99
+ collect { |sp_elt| [sp_elt['name'], sp_elt['value']] }
100
+ )
101
+ )
102
+ end
103
+ end
104
+
105
+ module BulkSettable
106
+ def self.included(struct_class)
107
+ struct_class.send(:extend, ClassMethods)
108
+ end
109
+
110
+ def attributes=(map)
111
+ map.each do |k, v|
112
+ setter = "#{k}="
113
+ if self.respond_to?(setter)
114
+ self.send setter, v
115
+ end
116
+ end
117
+ end
118
+
119
+ module ClassMethods
120
+ def create(*args)
121
+ attrs = args.pop
122
+ i = self.new(*args)
123
+ i.attributes = attrs
124
+ i
125
+ end
126
+ end
127
+ end
128
+
129
+ class RelativeDate
130
+ include Comparable
131
+
132
+ attr_reader :days
133
+
134
+ def initialize(days)
135
+ @days = days
136
+ end
137
+
138
+ def <=>(other)
139
+ self.days <=> other.days
140
+ end
141
+
142
+ def to_date
143
+ Date.today + days
144
+ end
145
+
146
+ def to_s
147
+ to_date.to_s
148
+ end
149
+ end
150
+
151
+ class Site < Struct.new(:name, :assigned_identifier)
152
+ include BulkSettable
153
+
154
+ def apply(connection)
155
+ connection.put(Psc.build_uri_path('sites', assigned_identifier),
156
+ Psc.xml('site', 'site-name' => name, 'assigned-identifier' => assigned_identifier).to_s,
157
+ 'Content-Type' => 'text/xml')
158
+ end
159
+ end
160
+
161
+ class Template < Struct.new(:assigned_identifier)
162
+ include BulkSettable
163
+
164
+ attr_accessor :filename, :participating_sites
165
+
166
+ def apply(connection)
167
+ connection.put(Psc.build_uri_path('studies', assigned_identifier, 'template'),
168
+ File.read(filename), 'Content-Type' => 'text/xml')
169
+ (participating_sites || []).each do |ps|
170
+ ps.apply(connection, self)
171
+ end
172
+ end
173
+
174
+ def document
175
+ @document ||= Nokogiri::XML(open(filename, 'r'))
176
+ end
177
+
178
+ def resolve_segment(ident)
179
+ s = (
180
+ segment_index.detect { |s|
181
+ ident.downcase.split(/\s*:\s*/) == [s[:epoch_name].downcase, s[:name].downcase]
182
+ } ||
183
+ segment_index.detect { |s| ident == s[:id] } ||
184
+ segment_index.detect { |s| ident == s[:name] }
185
+ )
186
+ raise "Unable to resolve segment #{ident.inspect}" unless s
187
+ s[:id]
188
+ end
189
+
190
+ private
191
+
192
+ def segment_index
193
+ # TODO: this will not resolve the enclosing epoch correctly for
194
+ # segments added in amendments.
195
+ @ss_index ||= document.css('epoch').inject({}) { |h, ep_elt|
196
+ (h[ep_elt['name']] ||= []).push(
197
+ *ep_elt.css('study-segment').collect { |ss_elt|
198
+ { :id => ss_elt['id'], :name => ss_elt['name'] }
199
+ })
200
+ h
201
+ }.collect { |epoch, segments|
202
+ segments.each { |seg| seg[:epoch_name] = epoch }; segments
203
+ }.flatten
204
+ end
205
+ end
206
+
207
+ class ParticipatingSite < Struct.new(:assigned_identifier)
208
+ include BulkSettable
209
+
210
+ attr_accessor :approval
211
+
212
+ def approval
213
+ @approval = RelativeDate.new(0) if @approval.nil?
214
+ @approval
215
+ end
216
+
217
+ def apply(connection, template)
218
+ study_site_path = Psc.build_uri_path(
219
+ 'studies', template.assigned_identifier, 'sites', assigned_identifier)
220
+ connection.put study_site_path, "<study-site-link/>", 'Content-Type' => 'text/xml'
221
+ if approval
222
+ template.document.css('amendment').
223
+ collect { |am_elt| [am_elt['date'], am_elt['name']].compact }.
224
+ sort { |a, b| a[0] <=> b[0] }.
225
+ collect { |pair| pair.join('~') }.
226
+ each do |am_ident|
227
+ connection.post "#{study_site_path}/approvals",
228
+ Psc.xml('amendment-approval', :date => approval.to_s, :amendment => am_ident).to_s,
229
+ 'Content-Type' => 'text/xml'
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ class Registration
236
+ include BulkSettable
237
+ attr_accessor :subject, :study_sites
238
+
239
+ def apply(connection, state)
240
+ study_sites.each do |study_site|
241
+ study_site.apply(connection, subject, state)
242
+ end
243
+ end
244
+ end
245
+
246
+ class Subject
247
+ include BulkSettable
248
+ attr_accessor :first_name, :last_name, :gender, :birth_date, :person_id, :properties
249
+
250
+ def build_on(xml_builder)
251
+ xml_builder.subject(
252
+ 'first-name' => first_name,
253
+ 'last-name' => last_name,
254
+ 'gender' => gender,
255
+ 'person-id' => person_id,
256
+ 'birth-date' => birth_date.to_s
257
+ ) { |subj_elt|
258
+ (properties || []).each { |prop|
259
+ subj_elt.property(:name => prop[0], :value => prop[1])
260
+ }
261
+ }
262
+ end
263
+ end
264
+
265
+ class StudySite
266
+ include BulkSettable
267
+ attr_accessor :template, :site, :study_subject_identifier, :primary_coordinator,
268
+ :desired_assignment_identifier, :scheduled_segments
269
+
270
+ def apply(connection, subject, state)
271
+ template_details = state.template(template)
272
+
273
+ schedule_resp = connection.post(
274
+ Psc.build_uri_path('studies', template, 'sites', site, 'subject-assignments'),
275
+ Psc.xml(
276
+ 'registration',
277
+ {
278
+ 'subject-coordinator-name' => primary_coordinator,
279
+ 'desired-assignment-id' => desired_assignment_identifier,
280
+ 'study-subject-id' => study_subject_identifier,
281
+ 'first-study-segment-id' => template_details.resolve_segment(
282
+ scheduled_segments.first.identifier),
283
+ 'date' => (scheduled_segments.first.start || Psc::RelativeDate.new(0)).to_s
284
+ }.reject { |k, v| !v }
285
+ ) { |reg_elt| subject.build_on(reg_elt) },
286
+ 'Content-Type' => 'text/xml'
287
+ )
288
+
289
+ schedule_url = schedule_resp.headers['Location']
290
+
291
+ scheduled_segments[1, scheduled_segments.size].each { |ss|
292
+ ss.apply(connection, template_details, schedule_url)
293
+ }
294
+ end
295
+ end
296
+
297
+ class ScheduledSegment < Struct.new(:identifier)
298
+ include BulkSettable
299
+ attr_accessor :start, :mode
300
+
301
+ def mode
302
+ @mode ||= "per-protocol"
303
+ end
304
+
305
+ def apply(connection, template, schedule_url)
306
+ connection.post(schedule_url,
307
+ Psc.xml('next-scheduled-study-segment',
308
+ 'study-segment-id' => template.resolve_segment(identifier),
309
+ 'start-date' => start.to_s,
310
+ 'mode' => mode
311
+ ), 'Content-Type' => 'text/xml')
312
+ end
313
+ end
314
+
315
+ class StateApplier
316
+ def initialize(state, out=DefaultOutput.new)
317
+ @state = state
318
+ @out = out
319
+ end
320
+
321
+ def apply(connection)
322
+ c = ConnectionProxy.new(connection, @out)
323
+ puts (@state.sites || []).collect { |s|
324
+ @out.monitor("Adding site #{s.assigned_identifier}") { s.apply(c) }
325
+ }.compact.tap { |result| return false if result.size > 0 }
326
+
327
+ (@state.templates || []).collect { |t|
328
+ @out.monitor("Adding template #{t.assigned_identifier}") { t.apply(c) }
329
+ }.compact.tap { |result| return false if result.size > 0 }
330
+
331
+ (@state.registrations || []).collect { |r|
332
+ @out.monitor("Registering #{r.subject.first_name} #{r.subject.last_name}") {
333
+ r.apply(c, @state)
334
+ }
335
+ }.compact.tap { |result| return false if result.size > 0 }
336
+ end
337
+
338
+ class ConnectionProxy
339
+ def initialize(conn, out)
340
+ @conn = conn
341
+ @out = out
342
+ end
343
+
344
+ def make_request(method, *args)
345
+ @out.trace("#{method.to_s.upcase} #{args.first}")
346
+ resp = @conn.send(method, *args)
347
+ if resp.respond_to?(:status) && resp.status > 299
348
+ raise resp.body
349
+ end
350
+ resp
351
+ end
352
+
353
+ def method_missing(m, *args)
354
+ if %w(get put post delete patch).include?(m.to_s)
355
+ make_request(m, *args)
356
+ else
357
+ super
358
+ end
359
+ end
360
+ end
361
+
362
+ class DefaultOutput
363
+ def initialize
364
+ @hl = HighLine.new
365
+ HighLine.color_scheme = HighLine::SampleColorScheme.new
366
+ end
367
+
368
+ def monitor(msg)
369
+ @hl.say("<%= color('*', :info) %> #{msg}")
370
+ begin
371
+ yield
372
+ nil
373
+ rescue => e
374
+ @hl.say("<%= color(' -', :error) %> #{e}")
375
+ false
376
+ end
377
+ end
378
+
379
+ def trace(msg)
380
+ @hl.say("<%= color(' +', :info) %> #{msg}")
381
+ end
382
+ end
383
+ end
384
+
385
+ def self.xml(root_name, root_attributes={}, &block)
386
+ root_attributes['xmlns'] = 'http://bioinformatics.northwestern.edu/ns/psc'
387
+ Builder::XmlMarkup.new(:indent => 2).tag!(root_name, root_attributes, &block)
388
+ end
389
+
390
+ def self.build_uri_path(*parts)
391
+ parts.collect { |p| URI.encode p }.join('/')
392
+ end
393
+ end