caruby-scat 1.2.1
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.
- data/.gitignore +17 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +158 -0
- data/History.md +9 -0
- data/LEGAL +4 -0
- data/LICENSE +22 -0
- data/README.md +131 -0
- data/Rakefile +41 -0
- data/bin/scat +7 -0
- data/bin/scatseed +10 -0
- data/caruby-scat.gemspec +30 -0
- data/conf/fields.yaml +22 -0
- data/conf/redis.conf +492 -0
- data/config.ru +15 -0
- data/features/edit.feature +29 -0
- data/features/step_definitions/scat_steps.rb +50 -0
- data/features/support/env.rb +18 -0
- data/features/support/seed.rb +2 -0
- data/lib/scat.rb +56 -0
- data/lib/scat/autocomplete.rb +74 -0
- data/lib/scat/cache.rb +90 -0
- data/lib/scat/configuration.rb +79 -0
- data/lib/scat/edit.rb +160 -0
- data/lib/scat/field.rb +99 -0
- data/lib/scat/version.rb +3 -0
- data/public/images/apple-touch-icon-114x114.png +0 -0
- data/public/images/apple-touch-icon-72x72.png +0 -0
- data/public/images/apple-touch-icon.png +0 -0
- data/public/images/favicon.ico +0 -0
- data/public/images/help.png +0 -0
- data/public/javascripts/tabs.js +29 -0
- data/public/robots.txt +5 -0
- data/public/stylesheets/base.css +342 -0
- data/public/stylesheets/layout.css +58 -0
- data/public/stylesheets/scat.css +64 -0
- data/public/stylesheets/skeleton.css +242 -0
- data/redis.rdb +0 -0
- data/spec/autocomplete_spec.rb +17 -0
- data/spec/edit_spec.rb +60 -0
- data/spec/spec_helper.rb +13 -0
- data/test/fixtures/seed.rb +25 -0
- data/views/edit.haml +12 -0
- data/views/layout.haml +40 -0
- metadata +216 -0
data/config.ru
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.require
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
require 'redis'
|
7
|
+
require 'jinx/helpers/log'
|
8
|
+
require 'scat'
|
9
|
+
|
10
|
+
# the logger
|
11
|
+
use Rack::CommonLogger, Jinx.logger(:app => 'Scat', :debug => true)
|
12
|
+
|
13
|
+
# start the application
|
14
|
+
run Scat::App
|
15
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
Feature: Edit
|
2
|
+
In order to use Scat
|
3
|
+
As a biobank technician
|
4
|
+
I submit an edit
|
5
|
+
|
6
|
+
Scenario: Visit edit
|
7
|
+
Given I am on the edit page
|
8
|
+
Then I should see the "Protocol" field
|
9
|
+
And I should see the "MRN" field
|
10
|
+
And I should see the "SPN" field
|
11
|
+
And I should see the "Diagnosis" field
|
12
|
+
And I should see the "Tissue Site" field
|
13
|
+
And I should see the "Quantity" field
|
14
|
+
And I should see the "Malignant" field
|
15
|
+
|
16
|
+
Scenario: Submit an edit
|
17
|
+
Given I am on the edit page
|
18
|
+
And the protocol "Scat" exists
|
19
|
+
When I fill in "Protocol" with "Scat"
|
20
|
+
And I fill in "MRN" with "123"
|
21
|
+
And I fill in "SPN" with "SP-123"
|
22
|
+
And I fill in "Diagnosis" with "[M]Adrenal cortical adenoma NOS"
|
23
|
+
And I fill in "Tissue Site" with "Adrenal gland, NOS"
|
24
|
+
And I fill in "Quantity" with "4"
|
25
|
+
And I check the "Malignant" checkbox
|
26
|
+
And I am authorized
|
27
|
+
And I click "Save"
|
28
|
+
Then the status should show the label
|
29
|
+
And the specimen should be saved
|
@@ -0,0 +1,50 @@
|
|
1
|
+
World(Rack::Test::Methods)
|
2
|
+
|
3
|
+
Given %r{I am on the (\w+) page} do |page|
|
4
|
+
visit("/#{page unless page =~ /^([Hh]ome|[Ee]dit)$/}")
|
5
|
+
end
|
6
|
+
|
7
|
+
# Authorization requires that the CaTissue API is configured with client
|
8
|
+
# access properties as described in the caRuby Tissue +README.md+ file.
|
9
|
+
Given %q{I am authorized} do
|
10
|
+
username = CaTissue.properties[:user].split('@').first
|
11
|
+
page.driver.browser.authorize(username, CaTissue.properties[:password])
|
12
|
+
end
|
13
|
+
|
14
|
+
Given %r{the protocol "([^"]*)" exists} do |title|
|
15
|
+
Scat::Seed.protocol_for(title).find(:create)
|
16
|
+
end
|
17
|
+
|
18
|
+
When %r{I fill in(?: the)? "([^"]*)"(?: field)? with "([^"]*)"} do |name, text|
|
19
|
+
fill_in Scat::Edit.instance.input_id(name), :with => text
|
20
|
+
end
|
21
|
+
|
22
|
+
When %r{I check(?: the)? "([^"]*)(?: checkbox)?"} do |name|
|
23
|
+
check Scat::Edit.instance.input_id(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
When %r{I click "([^"]*)"} do |name|
|
27
|
+
click_on name
|
28
|
+
end
|
29
|
+
|
30
|
+
Then %r{I should see the "([^"]*)" field} do |name|
|
31
|
+
find_field(Scat::Edit.instance.input_id(name)).visible?.should be true
|
32
|
+
end
|
33
|
+
|
34
|
+
Then %q{the status should show the label} do
|
35
|
+
find(:status).text.should match /label \d+\.$/
|
36
|
+
end
|
37
|
+
|
38
|
+
Then %q{the specimen should be saved} do
|
39
|
+
lbl = /label (\d+)\.$/.match(find(:status).text).captures.first
|
40
|
+
lbl.should_not be nil
|
41
|
+
CaTissue::Specimen.new(:label => lbl).find.should_not be nil
|
42
|
+
end
|
43
|
+
|
44
|
+
Then %r{^I should see "([^"]*)"$} do |text|
|
45
|
+
page.should have_content text
|
46
|
+
end
|
47
|
+
|
48
|
+
After do |scenario|
|
49
|
+
save_and_open_page if scenario.failed?
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.require(:test, :development)
|
4
|
+
|
5
|
+
require 'capybara/cucumber'
|
6
|
+
require 'jinx/helpers/log'
|
7
|
+
require 'scat'
|
8
|
+
|
9
|
+
ENV['RACK_ENV'] = 'test'
|
10
|
+
|
11
|
+
Capybara.app = Scat::App
|
12
|
+
|
13
|
+
# Open the logger.
|
14
|
+
Jinx.logger('test/results/log/scat.log', :debug)
|
15
|
+
|
16
|
+
def app
|
17
|
+
Scat::App
|
18
|
+
end
|
data/lib/scat.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'haml'
|
3
|
+
require 'catissue'
|
4
|
+
require 'casmall/authorization'
|
5
|
+
require 'scat/autocomplete'
|
6
|
+
require 'scat/edit'
|
7
|
+
|
8
|
+
module Scat
|
9
|
+
# The standard Scat application error.
|
10
|
+
class ScatError < RuntimeError; end
|
11
|
+
|
12
|
+
class App < Sinatra::Base
|
13
|
+
include CaSmall::Authorization, Autocomplete
|
14
|
+
|
15
|
+
set :root, File.dirname(__FILE__) + '/..'
|
16
|
+
|
17
|
+
if development? then
|
18
|
+
# Don't generate fancy HTML for stack traces.
|
19
|
+
disable :show_exceptions
|
20
|
+
# Allow errors to get out of the app so Cucumber can display them.
|
21
|
+
enable :raise_errors
|
22
|
+
end
|
23
|
+
|
24
|
+
# The authorization page name.
|
25
|
+
set :authorization_realm, 'Please enter your username and caTissue password'
|
26
|
+
|
27
|
+
enable :sessions
|
28
|
+
|
29
|
+
# Displays the edit form.
|
30
|
+
get '/' do
|
31
|
+
haml :edit
|
32
|
+
end
|
33
|
+
|
34
|
+
# Saves the specimen specified in the specimen form.
|
35
|
+
post '/' do
|
36
|
+
# Save the specimen.
|
37
|
+
protect! { Edit.instance.save(params.merge(:user => current_user), session) }
|
38
|
+
# Return to the edit form.
|
39
|
+
redirect back
|
40
|
+
end
|
41
|
+
|
42
|
+
# Displays the CVs for the given property attribute which match the given input
|
43
|
+
# value term.
|
44
|
+
get '/autocomplete/*' do |pa|
|
45
|
+
protect! { autocomplete(pa.to_sym, params[:term]) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the status field to the error message.
|
49
|
+
error do
|
50
|
+
e = env['sinatra.error']
|
51
|
+
logger.error(e)
|
52
|
+
request.params[:status] = e.message
|
53
|
+
redirect back
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'jinx/helpers/inflector'
|
2
|
+
require 'catissue/database/controlled_values'
|
3
|
+
require 'scat/cache'
|
4
|
+
|
5
|
+
module Scat
|
6
|
+
# Auto-completion mix-in.
|
7
|
+
module Autocomplete
|
8
|
+
# Fetches controlled caTissue values for the given attribute and value prefix.
|
9
|
+
# The supported attributes include the following:
|
10
|
+
# * +:clinical_diagnosis+ (+SpecimenCollectionGroup.clinical_diagnosis+)
|
11
|
+
# * +:tissue_site+ (+SpecimenCharacteristics.tissue_site+)
|
12
|
+
#
|
13
|
+
# @param [Symbol] attribute the CV attribute
|
14
|
+
# @param [String] prefix the leading value letters
|
15
|
+
# @return [String] the JSON representation of the matching values
|
16
|
+
def autocomplete(attribute, text)
|
17
|
+
# Start up the cache if necessary
|
18
|
+
@cache ||= Cache.new
|
19
|
+
# Compare lower case.
|
20
|
+
text_dc = text.downcase
|
21
|
+
# The search term is the first word in the text.
|
22
|
+
words = text_dc.split(' ')
|
23
|
+
term = words.first
|
24
|
+
logger.debug { "Scat is matching the cached #{attribute} values which contain #{term}..." }
|
25
|
+
# The hash key, e.g. dx:lymphoma.
|
26
|
+
key = KEY_PREFIX_HASH[attribute] + term
|
27
|
+
# The CVs which match the term.
|
28
|
+
cvs = @cache.get_all(key)
|
29
|
+
# Load the CVs if necessary.
|
30
|
+
cvs = load_controlled_values(attribute, term, key) if cvs.empty?
|
31
|
+
# The CVs which match the entire target.
|
32
|
+
matched = words.size == 1 ? cvs : cvs.select { |cv| cv.downcase[text_dc] }
|
33
|
+
logger.debug { "#{matched.empty? ? 'No' : matched.size} #{attribute} values match '#{text}'." }
|
34
|
+
matched.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
KEY_PREFIX_HASH = {
|
40
|
+
:clinical_diagnosis => 'dx:',
|
41
|
+
:tissue_site => 'ts:'
|
42
|
+
}
|
43
|
+
|
44
|
+
# The template to fetch CVs matching a PID and key term.
|
45
|
+
# The SQL cheats a little, since it theoretically could return a CV with null PID
|
46
|
+
# whose parentage is not directly or indirectly in the PID. However, this problem
|
47
|
+
# does not occur in practice and is relatively benign if it should occur, since at
|
48
|
+
# worst it includes a few obviously extraneous matches.
|
49
|
+
CV_TMPL = "select value
|
50
|
+
from catissue_permissible_value pv
|
51
|
+
where (public_id = '%s' or (public_id is null and parent_identifier is not null))
|
52
|
+
and value like '%%%s%%'
|
53
|
+
and not exists (select 1 from catissue_permissible_value where parent_identifier = pv.identifier)"
|
54
|
+
|
55
|
+
# @param (see #match)
|
56
|
+
# @param [Strimg] the cache key
|
57
|
+
# @return (see #match)
|
58
|
+
def load_controlled_values(attribute, term, key)
|
59
|
+
logger.debug { "Scat is loading the #{attribute} controlled values which match '#{term}'..." }
|
60
|
+
# The CV PID, e.g. the :tissue_site PID is Tissue_Site_PID.
|
61
|
+
pid = attribute.to_s.split('_').map { |s| s.capitalize_first }.join('_') << '_PID'
|
62
|
+
# The SQL is the template formatted with the PID and term.
|
63
|
+
sql = CV_TMPL % [pid, term]
|
64
|
+
# Cache each fetched CV.
|
65
|
+
cvs = CaTissue::Database.current.executor.query(sql).map do |rec|
|
66
|
+
cv = rec.first
|
67
|
+
@cache.add(key, cv)
|
68
|
+
cv
|
69
|
+
end
|
70
|
+
logger.debug { "Scat loaded #{cvs.size} #{attribute} #{term} controlled values." }
|
71
|
+
cvs
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/scat/cache.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Scat
|
4
|
+
# The session and global key => value cache wrapper.
|
5
|
+
class Cache
|
6
|
+
# @return [Redis] the Redis cache data store
|
7
|
+
# @raise [ScatError] if the cache could not be started
|
8
|
+
def datastore
|
9
|
+
@redis ||= discover
|
10
|
+
end
|
11
|
+
|
12
|
+
# Sets the given value to the cache tag set as follows:
|
13
|
+
# * If the key is nil, then the cache entry is the tag.
|
14
|
+
# * Otherwise, the cache entry is the tag hash entry for the given key.
|
15
|
+
# * If the value is nil, then the entry is removed from the cache.
|
16
|
+
# * Otherwise, the value is converted to a string, if necessary, and
|
17
|
+
# the cache entry is set to the value.
|
18
|
+
#
|
19
|
+
# @param [String, Symbol] tag the cache tag
|
20
|
+
# @param value the value to set
|
21
|
+
# @param [String, Symbol, nil] key the cache tag hash key
|
22
|
+
def set(tag, value, key=nil)
|
23
|
+
if value.nil? then
|
24
|
+
key ? datastore.hdel(tag, key) : datastore.rem(tag)
|
25
|
+
else
|
26
|
+
key ? datastore.hset(tag, key, value) : datastore.set(tag, value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the given value to the cache tag set. The value is converted to
|
31
|
+
# a string, if necessary.
|
32
|
+
#
|
33
|
+
# @param [String, Symbol] tag the cache tag
|
34
|
+
# @param value the value to add
|
35
|
+
def add(tag, value)
|
36
|
+
datastore.zadd(tag, 0, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [String, Symbol] tag the cache tag
|
40
|
+
# @param [Symbol, nil] the cached tag hash key
|
41
|
+
# @return [String, <String>] the matching value or values
|
42
|
+
def get(tag, key=nil)
|
43
|
+
key ? datastore.hget(tag, key) : datastore.get(tag)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [String, Symbol] tag the cache tag
|
47
|
+
# @return [<String>] the matching set
|
48
|
+
def get_all(tag)
|
49
|
+
datastore.zrange(tag, 0, -1)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
REDIS_SERVER = File.expand_path('redis-server', File.dirname(__FILE__) + '/../../ext')
|
55
|
+
|
56
|
+
REDIS_CONF = File.expand_path('redis.conf', File.dirname(__FILE__) + '/../../conf')
|
57
|
+
|
58
|
+
# Locates the Redis server on the default Redis port.
|
59
|
+
#
|
60
|
+
# @return [Redis] the Redis client
|
61
|
+
# @raise (see #start)
|
62
|
+
def discover
|
63
|
+
redis = Redis.current
|
64
|
+
redis.ping rescue start(redis)
|
65
|
+
redis
|
66
|
+
end
|
67
|
+
|
68
|
+
# Starts the Redis server on the default Redis port.
|
69
|
+
#
|
70
|
+
# @param [Redis] the Redis client
|
71
|
+
# @raise [ScatError] if the server command could not be executed
|
72
|
+
# @raise [Exception] if the server is not reachable
|
73
|
+
def start(redis)
|
74
|
+
logger.debug { "Scat is starting the Redis cache server..." }
|
75
|
+
unless system(REDIS_SERVER, REDIS_CONF) then
|
76
|
+
raise ScatError.new("Scat cannot start the Redis cache server.")
|
77
|
+
end
|
78
|
+
# Ping the server until loaded.
|
79
|
+
3.times do |n|
|
80
|
+
begin
|
81
|
+
redis.ping
|
82
|
+
logger.debug { "Scat started the Redis cache server." }
|
83
|
+
return redis
|
84
|
+
rescue
|
85
|
+
n < 2 ? sleep(0.5) : raise
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'jinx/helpers/hash'
|
3
|
+
require 'scat/field'
|
4
|
+
|
5
|
+
module Scat
|
6
|
+
# The Scat Configuration specifies the edit form fields.
|
7
|
+
class Configuration
|
8
|
+
# @param [String] the configuration file path
|
9
|
+
def initialize(file)
|
10
|
+
# the field name => Field hash
|
11
|
+
@fld_hash = parse(YAML.load_file(file))
|
12
|
+
logger.info("Scat fields: #{@fld_hash.values.pp_s}")
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Symbol, String] name the field name or label
|
16
|
+
# @return [Field] the corresponding field
|
17
|
+
def [](name_or_label)
|
18
|
+
# the standardized field name
|
19
|
+
key = case name_or_label
|
20
|
+
when Symbol then
|
21
|
+
name_or_label
|
22
|
+
when String then
|
23
|
+
Field.name_for(name_or_label).to_sym
|
24
|
+
else
|
25
|
+
raise ArgumentError.new("Scat field argument not supported: #{name_or_label.qp}")
|
26
|
+
end
|
27
|
+
@fld_hash[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the edit form field specifications.
|
31
|
+
#
|
32
|
+
# @return [<Field>] the specimen edit field specifications
|
33
|
+
def fields
|
34
|
+
@fld_hash.values
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [{String => String}] params the request parameters
|
38
|
+
# @return [{Class => {CaRuby::Property => Object}}] the caTissue class => { property => value } hash
|
39
|
+
def slice(params)
|
40
|
+
hash = Jinx::LazyHash.new { Hash.new }
|
41
|
+
params.each do |name, value|
|
42
|
+
next if value.nil?
|
43
|
+
# Skip the param if it is not obtained from the request.
|
44
|
+
field = @fld_hash[name.to_sym] || next
|
45
|
+
field.properties.each do |klass, prop|
|
46
|
+
# Convert non-string values.
|
47
|
+
if prop.type <= Integer or prop.type <= Java::JavaLang::Integer then
|
48
|
+
value = value.to_i
|
49
|
+
elsif prop.type <= Date or prop.type <= Java::JavaUtil::Date then
|
50
|
+
value = DateTime.parse(value)
|
51
|
+
elsif prop.type <= Numeric or prop.type <= Java::JavaLang::Number then
|
52
|
+
value = value.to_f
|
53
|
+
end
|
54
|
+
# Generalize a Specimen subclass.
|
55
|
+
klass = CaTissue::Specimen if klass < CaTissue::Specimen
|
56
|
+
# Add the property value.
|
57
|
+
hash[klass][prop.to_sym] = value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
hash
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Parses the field configuration as described in {Field}.
|
66
|
+
#
|
67
|
+
# @param [{String => String}] config the field label => spec hash
|
68
|
+
# @return [{Symbol} => Field] the symbol => Field hash
|
69
|
+
def parse(config)
|
70
|
+
hash = {}
|
71
|
+
config.each do |label, spec|
|
72
|
+
field = Field.new(label, spec)
|
73
|
+
hash[field.name.to_sym] = field
|
74
|
+
end
|
75
|
+
hash
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
data/lib/scat/edit.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'jinx/helpers/validation'
|
3
|
+
require 'scat/configuration'
|
4
|
+
require 'scat/authorization'
|
5
|
+
require 'scat/cache'
|
6
|
+
|
7
|
+
module Scat
|
8
|
+
# The Edit helper makes a +CaTissue::Specimen+ from the edit form parameters and creates
|
9
|
+
# the specimen in the database.
|
10
|
+
class Edit
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
@conf = Configuration.new(FIELD_CONFIG)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return (see Configuration#fields)
|
19
|
+
def fields
|
20
|
+
# Delegate to the configuration.
|
21
|
+
@conf.fields
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param (see Configuration#[])
|
25
|
+
# @return [String] the corresponding HTML input element id
|
26
|
+
def input_id(name_or_label)
|
27
|
+
field = @conf[name_or_label]
|
28
|
+
if field.nil? then raise ArgumentError.new("Scat field not found: #{name_or_label.qp}") end
|
29
|
+
field.input_id
|
30
|
+
end
|
31
|
+
|
32
|
+
# Saves the +CaTissue::Specimen+ object specified in the request parameters.
|
33
|
+
#
|
34
|
+
# @param [{Symbol => String}] params the request parameters
|
35
|
+
# @param [{String => String}] session the current Sinatra session
|
36
|
+
# @return [CaTissue::Specimen] the saved specimen
|
37
|
+
# @raise [ScatError] if the parameters are insufficient to build a specimen
|
38
|
+
# @raise [CaRuby::DatabaseError] if the save is unsuccessful
|
39
|
+
def save(params, session)
|
40
|
+
logger.debug { "Scat is saving the specimen with parameters #{params.qp}..." }
|
41
|
+
pcl_id = session[:protocol_id] if session[:protocol] == params[:protocol]
|
42
|
+
site_id = session[:site_id] if pcl_id
|
43
|
+
cpr_id = session[:cpr_id] if pcl_id and session[:mrn] == params[:mrn]
|
44
|
+
scg_id = session[:scg_id] if cpr_id and session[:spn] == params[:spn]
|
45
|
+
# the caTissue class => { property => value } hash
|
46
|
+
ph = @conf.slice(params)
|
47
|
+
# the collection protocol
|
48
|
+
pcl = to_protocol(ph[CaTissue::CollectionProtocol].merge({:identifier => pcl_id}))
|
49
|
+
# the current user
|
50
|
+
user = params[:user]
|
51
|
+
# the collection site
|
52
|
+
site = site_id ? CaTissue::Site.new(:identifier => site_id) : to_site(pcl, user)
|
53
|
+
# the patient
|
54
|
+
pnt = CaTissue::Participant.new(ph[CaTissue::Participant])
|
55
|
+
# the CPR parameters
|
56
|
+
reg_params = ph[CaTissue::ParticipantMedicalIdentifier].merge(:participant => pnt, :site => site)
|
57
|
+
# the CPR
|
58
|
+
reg = to_registration(pcl, reg_params)
|
59
|
+
reg.identifier = cpr_id
|
60
|
+
# the specimen parameters
|
61
|
+
spc_params = ph[CaTissue::Specimen].merge(ph[CaTissue::SpecimenCharacteristics])
|
62
|
+
# The current user is the biobank specimen receiver.
|
63
|
+
spc_params.merge!(:receiver => user)
|
64
|
+
# the specimen to save
|
65
|
+
spc = to_specimen(pcl, spc_params)
|
66
|
+
# the SCG parameters
|
67
|
+
scg_params = ph[CaTissue::SpecimenCollectionGroup].merge(:participant => pnt, :site => site)
|
68
|
+
# the SCG which contains the specimen
|
69
|
+
pcl.add_specimens(spc, scg_params)
|
70
|
+
# Save the specimen.
|
71
|
+
logger.debug { "Scat is saving #{spc} with content:\n#{spc.dump}" }
|
72
|
+
spc.save
|
73
|
+
# Format the status message.
|
74
|
+
session[:status] = "Created the specimen with label #{spc.label}."
|
75
|
+
# Capture the params in the session to refresh the form.
|
76
|
+
params.each { |a, v| session[a.to_sym] = v }
|
77
|
+
# Capture the ids.
|
78
|
+
scg = spc.specimen_collection_group
|
79
|
+
session[:scg_id] = scg.identifier
|
80
|
+
session[:site_id] = site.identifier
|
81
|
+
cpr = scg.registration
|
82
|
+
session[:cpr_id] = cpr.identifier
|
83
|
+
session[:protocol_id] = cpr.protocol.identifier
|
84
|
+
logger.debug { "Scat saved #{spc}." }
|
85
|
+
spc
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# The field configuration file name.
|
91
|
+
FIELD_CONFIG = File.dirname(__FILE__) + '/../../conf/fields.yaml'
|
92
|
+
|
93
|
+
# Builds the registration object specified in the given parameters.
|
94
|
+
#
|
95
|
+
# @param [{Symbol => String}] params the registration parameters
|
96
|
+
# @return [CaTissue::CollectionProtocolRegistration] the SCG to save
|
97
|
+
def to_protocol(params)
|
98
|
+
pcl = CaTissue::CollectionProtocol.new(params)
|
99
|
+
unless pcl.find then
|
100
|
+
raise ScatError.new("Protocol not found: #{pcl.title}")
|
101
|
+
end
|
102
|
+
pcl
|
103
|
+
end
|
104
|
+
|
105
|
+
# The collection site is the first match for the following criteria:
|
106
|
+
# * the first site of the current user
|
107
|
+
# * the first protocol site
|
108
|
+
# * the first protocol coordinator site
|
109
|
+
#
|
110
|
+
# @param [CaTissue::CollectionProtocol] protocol the collection protocol
|
111
|
+
# @param [CaTissue::User] user the current user
|
112
|
+
# @return [CaTissue::Site] the collection site
|
113
|
+
def to_site(protocol, user)
|
114
|
+
user.find unless user.fetched?
|
115
|
+
site = user.sites.first
|
116
|
+
return site if site
|
117
|
+
protocol.find unless protocol.fetched?
|
118
|
+
site = protocol.sites.first
|
119
|
+
return site if site
|
120
|
+
site = protocol.coordinators.detect_value { |coord| coord.sites.first } or
|
121
|
+
raise ScatError.new("Neither the user #{rcvr.email_address} nor the #{pcl.title} protocol administrators have an associated site.")
|
122
|
+
end
|
123
|
+
|
124
|
+
# Builds the registration object specified in the given parameters.
|
125
|
+
#
|
126
|
+
# @param [CaTissue::CollectionProtocol] protocol the collection protocol
|
127
|
+
# @param [{Symbol => String}] params the registration parameters
|
128
|
+
# @return [CaTissue::CollectionProtocolRegistration] the SCG to save
|
129
|
+
def to_registration(protocol, params)
|
130
|
+
# If there is an MRN, then make a PMI
|
131
|
+
if params.has_key?(:medical_record_number) then
|
132
|
+
CaTissue::ParticipantMedicalIdentifier.new(params)
|
133
|
+
end
|
134
|
+
# the patient
|
135
|
+
pnt = params[:participant]
|
136
|
+
# Register the patient.
|
137
|
+
protocol.register(pnt)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Builds the +CaTissue::Specimen+ object specified in the given parameters.
|
141
|
+
#
|
142
|
+
# The default edit form pathological status checkbox value is 'Malignant'.
|
143
|
+
# If the user unchecked it, then there is no pathological status parameter.
|
144
|
+
# In that case, the pathological status is 'Non-Malignant'.
|
145
|
+
#
|
146
|
+
# @param [CaTissue::CollectionProtocol] protocol the collection protocol
|
147
|
+
# @param [{Symbol => String}] params the Specimen parameters
|
148
|
+
# @return [CaTissue::Specimen] the specimen to save
|
149
|
+
def to_specimen(protocol, params)
|
150
|
+
params[:pathological_status] ||= 'Non-Malignant'
|
151
|
+
# The CPE is the sole protocol event.
|
152
|
+
cpe = protocol.events.first
|
153
|
+
# The specimen requirement is the sole event requirement.
|
154
|
+
rqmt = cpe.requirements.first
|
155
|
+
# Make the specimen.
|
156
|
+
CaTissue::Specimen.create_specimen(params.merge(:requirement => rqmt))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|