package LIMS::Controller::Storage;
=begin # FluidX XTR-96 plate reader:
requires xtr_96_addr ip:port in settings file to provide access
System settings:
Winsock: Add <CRLF> = OFF; Enable Winsock = ON; Port = 2001
Barcode: Integral reader (ensure Matrix ID auto orientate off)
Results settings:
Header information: Single Field; Label: Rack Identifier (checked)
Results Format: Tube IDs and TubeRack Coords
Spacing of results: commas only
Separation of records: same line
Grouping of records: by Column
Results capture: Require RackID with tube readings
=cut
use LIMS::ControllerClass; # inherits LIMS::Base and provides LIMS::Local::Debug::p
with 'LIMS::Controller::Roles::RecordHandler';
__PACKAGE__->meta->make_immutable(inline_constructor => 0);
use Net::Telnet;
use Data::Dumper;
# ------------------------------------------------------------------------------
# default() should never be called direct - redirect to start page:
sub default : StartRunmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
return $self->redirect( $self->query->url );
}
# ------------------------------------------------------------------------------
sub load : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $errs = shift; $self->stash( errs => $errs );
my $request_id = $self->param('id')
|| return $self->error('no id passed to ' . $self->get_current_runmode);
# get request data:
my $request_data = $self->model('Request')->get_single_request($request_id);
# get existing storage data:
my $storage = $self->model('Storage')->get_request_storage($request_id);
# vials in storage NOT signed out:
my @available = map $_->vialId, grep ! $_->signed_out, @$storage;
# get specimen map for request:
my $specimen_map = $self->specimen_map([ $request_id ]); # warn Dumper $specimen_map;
my %tt_params = (
specimen_map => $specimen_map,
request => $request_data,
available => \@available,
storage => $storage,
);
{ # menu options:
my $opts = $self->_get_menu_options();
$tt_params{menu_options} = $opts;
}
# yaml config to allow new tests to be requested (eg xNA extraction):
if ( my $yml = $self->get_yaml_file('storage') ) { # p $yml;
# only allowed 1 lab section as method for result_update expects section id:
my ($section) = keys %$yml; # p $section;
my $o = $self->model('LabTest')->get_lab_tests; # warn Dumper $_ for @$o;
{ # lab tests:
my %h = map +($_->field_label => $_->id),
grep $_->lab_section->section_name eq $section, @$o; # p %h;
# take hash slice:
my $lab_tests = $yml->{$section}; # p $lab_tests;
my %tests = map +($_ => $h{$_}), @$lab_tests; # p %tests;
$tt_params{lab_tests} = \%tests;
}
{ # lab section details:
my %h =
map +($_->lab_section->section_name => $_->lab_section_id), @$o;
my %lab_section = ( name => $section, id => $h{$section} ); # p %lab_section;
$tt_params{lab_section} = \%lab_section;
}
{ # request-lab-test data for $section:
my %h = ( section_name => $section, request_id => $request_id );
my $o = $self->model('LabTest')
->get_request_lab_tests_for_section(\%h);
my %map = map +(
$_->lab_test->field_label => $_->status->description
), @$o; # p %map;
$tt_params{request_lab_tests} = \%map; # p $request_lab_test;
}
{ # record status (RecordHandler::is_record_complete):
my $is_locked = $self->is_record_complete($request_data); # p $is_locked;
$tt_params{is_locked} = $is_locked; # will be undef if status != complete
}
}
$self->tt_params(%tt_params);
return $self->render_view($self->tt_template_name, $errs);
}
# ------------------------------------------------------------------------------
sub input : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $errs = shift;
my $request_id = $self->param('id')
|| return $self->error('no id passed to ' . $self->get_current_runmode);
my $dfv = $self->check_rm('load', $self->validate('storage_input') )
|| return $self->dfv_error_page;
my $params = $dfv->valid; # print p $params;
$params->{request_id} = $request_id;
my $rtn = $self->model('Storage')->input_storage($params);
return $self->error( $rtn ) if $rtn;
# insert flash message
$self->flash( info => $self->messages('action')->{create_success} );
return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}
# ------------------------------------------------------------------------------
sub output : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $errs = shift;
my $request_id = $self->param('id')
|| return $self->error('no id passed to ' . $self->get_current_runmode);
my $dfv = $self->check_rm('load', $self->validate('storage_output') )
|| return $self->dfv_error_page;
my $params = $dfv->valid; # warn Dumper $params;
$params->{request_id} = $request_id;
my $rtn = $self->model('Storage')->output_storage($params); # 'OK' or href error
if (ref $rtn eq 'HASH') { # will be error
return $self->forward('load', $rtn);
}
# insert flash message:
$self->flash( info => $self->messages('action')->{edit_success} );
return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}
# ------------------------------------------------------------------------------
sub edit : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $errs = shift; $self->stash( errs => $errs );
my $vial_id = $self->param('id')
|| return $self->error('no id passed to ' . $self->get_current_runmode);
my $request_id = $self->param('Id')
|| return $self->error('no Id passed to ' . $self->get_current_runmode);
my $request = $self->model('Request')->get_single_request($request_id);
$self->tt_params( request => $request );
{ # menu options:
my $opts = $self->_get_menu_options();
$self->tt_params( menu_options => $opts );
}
my $data = $self->model('Storage')->get_storage_vial($vial_id);
$self->tt_params( data => $data );
return $self->tt_process($errs);
}
# ------------------------------------------------------------------------------
sub delete : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $vial_id = $self->param('id')
|| return $self->error('no vialId passed to ' . $self->get_current_runmode);
my $request_id = $self->param('Id')
|| return $self->error('no request_id passed to ' . $self->get_current_runmode);
# need confirmation to delete record (scanned vialId matches id of vial selected):
if ( my $vialId = $self->query->param('vialId') ) {
my $messages = $self->messages('storage');
unless ( $vial_id eq $vialId ) { # warn 'here';
return $self->tt_process({ error => $messages->{vial_mismatch} });
}
my $rtn = $self->model('Storage')->delete_storage_vial($vial_id);
return $self->error($rtn) if $rtn;
# set flash success & redirect:
$self->flash( info => $messages->{delete_success} );
$self->redirect( $self->query->url . '/storage/=/' . $request_id );
}
else { # just load error for confirmation:
$self->tt_params( vial_id => $vial_id );
return $self->tt_process();
}
}
# ------------------------------------------------------------------------------
sub update_storage_vial : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $errs = shift;
# capture param 'Id' - 'id' = vialId in case validation failure -> edit()
my $request_id = $self->param('Id')
|| return $self->error('no request_id passed to ' . $self->get_current_runmode);
my $dfv = $self->check_rm('edit', $self->validate('storage_update') )
|| return $self->dfv_error_page; # warn Dumper $dfv;
my $params = $dfv->valid; # p $params;
$params->{request_id} = $request_id;
my $rtn = $self->model('Storage')->update_storage_vial($params);
return $self->error( $rtn ) if $rtn;
# insert flash message
$self->flash( info => $self->messages('action')->{edit_success} );
return $self->redirect( $self->query->url . '/storage/=/' . $request_id );
}
# ------------------------------------------------------------------------------
sub read_xtr_96 : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $cfg = $self->cfg('settings'); # p $cfg->{xtr_96_addr};
# connection info, or return:
my ($host, $port) = split ':', $cfg->{xtr_96_addr}; # p [$host, $port];
return $self->error('require host & port') unless ($host && $port);
# request to commence plate scan:
if ( $self->query->param('scan') ) {
my $rtn_url = $self->query->url . '/storage/read_xtr_96';
my $msgs = $self->messages('storage');
# get data or return (any failure will be in flash msg):
my $xtr_96 = $self->_get_xtr_96_data($host, $port) # hashref of plate_data & plateId
|| return $self->redirect( $rtn_url ); # p($xtr_96);
{ # get storage_rack & request_storage db table data (if exists):
my $plate_data = $xtr_96->{plate_data}; # p($plate_data);
my $plateId = $xtr_96->{plateId};
unless ($plate_data) {
$self->flash(error => $msgs->{no_plate_data});
return $self->redirect( $rtn_url );
}
unless ($plateId) {
$self->flash(error => $msgs->{no_plate_id});
return $self->redirect( $rtn_url );
}
my $storage_rack = $self->model('Storage')->get_storage_rack($plateId); # hashref
my @locations = keys %{ $plate_data }; # p(@locations); p($storage_rack);
my @vial_ids = values %{ $plate_data }; # p(@vial_ids);
# need map of vialId => location:
my %location_map; @location_map{@vial_ids} = @locations;
my %args = $storage_rack # if exists, use rack id, otherwise vial_id's:
? ( rack_id => $storage_rack->{id} ) # get all vials from rack
: ( vial_ids => \@vial_ids ); # get all vials matching input
my $o = $self->model('Storage')->get_rack_contents(\%args);
# need hash of vialId's from request_storage where signed_out is null:
my %rack_data = map { $_->vialId => $location_map{$_->vialId} }
grep { ! $_->signed_out } @$o; # p(%rack_data);
# do we have any vials in rack which do NOT exist in storage:
my $have_missing = grep { ! $rack_data{$_} }
grep $_ ne 'No Tube', @vial_ids; # eg unrecognised vial, or 'No Read'
my $have_empty = grep $_ eq 'No Tube', @vial_ids; # just highlight on view
my %h = (
storage_rack => $storage_rack,
have_missing => $have_missing,
have_empty => $have_empty,
rack_data => \%rack_data,
); # p %h;
$xtr_96->{storage} = \%h;
}
$self->tt_params( data => $xtr_96 );
# store in session for retrieval after 'import':
$self->session->param( _xtr_96_data => $xtr_96 );
}
else { # ensure only PC attached to xtr_96 can activate scan:
my ($ip, $port) = split ':', $self->cfg('settings')->{xtr_96_addr};
my $ok = $ENV{DEVEL_SERVER} || ( $ENV{REMOTE_ADDR} eq $ip );
$self->tt_params(
remote_addr => $ENV{REMOTE_ADDR},
xtr_96_addr => $ip,
can_scan => $ok,
);
}
return $self->tt_process();
}
# ------------------------------------------------------------------------------
sub import_xtr_96_data : Runmode {
my $self = shift; $self->_debug_path($self->get_current_runmode);
my $xtr_96_data = $self->session->dataref->{_xtr_96_data}
|| return $self->error( 'no xtr-96 data retrieved from session' ); # p($xtr_96_data);
# get storage_rack data (if exists):
my $storage_data = $xtr_96_data->{storage}; # p($storage_data);
# check no missing data (shouldn't happen - should not have been able to submit):
if ( $storage_data->{have_missing} ) {
my $url = $self->query->url . '/storage/read_xtr_96?scan=1';
return $self->redirect( $url );
}
my ($result, $plate_id);
my $vars = $self->query->Vars(); # warn Dumper $vars;
# void plate - remove vial_location & plateId from vials:
if ( $vars->{void_plate} ) {
my $plateId = $vars->{_plateId}; # distinct from $plate_id
unless ( $vars->{confirm_void_plate} ) { # need confirmation
return $self->tt_process('storage/void_plate.tt');
}
my $rtn = $self->model('Storage')->void_plate($plateId);
if ($rtn) { # error
return $self->error($rtn);
}
else {
my $msg = $self->messages('storage')->{void_success}; # sprintf:
$self->flash( info => sprintf $msg, $plateId );
return $self->redirect( $self->query->url . '/storage/read_xtr_96');
}
}
elsif ( $vars->{import} ) { # new import:
$plate_id = $xtr_96_data->{plateId} # eg SA00098711
|| return $self->error( 'no plate id retrieved from session' ); # p($plate_id);
# insert new entry in storage_racks, returns storage_racks.id:
my $rack_id = $self->model('Storage')->new_storage_rack($plate_id);
# $rack_data: href vialId => location
my $rack_data = $storage_data->{rack_data}; # p($rack_data);
my %h = ( rack_id => $rack_id, plate => $rack_data ); # p(%args);
$result = $self->model('Storage')->update_storage_rack(\%h); # p($result);
$self->tt_params( action => 'scanned in' );
}
# if export data, sign out all vials:
elsif ( $vars->{export} ) { # warn Dumper $rack;
my $rack = $storage_data->{storage_rack}; # warn Dumper $rack;
$plate_id = $rack->{plateId};
return $self->error( "plateId $plate_id is not active" )
unless $rack->{is_active} eq 'yes'; # should never happen
my $rack_data = $storage_data->{rack_data};
my @vial_ids = keys %{ $rack_data }; # warn Dumper \@vial_ids;
# sign out all vials in rack:
my %h = (
vial_id => \@vial_ids,
rack_id => $rack->{id},
);
$result = $self->model('Storage')->sign_out_storage_rack(\%h); # p($result);
$self->tt_params( action => 'signed out' );
}
else { # just set $result to msg for return below:
$result = 'unknown action (not one of: import, export or void plate)';
}
# $result should be hashref, or err str:
return $self->error($result) unless ref $result eq 'HASH';
{ # get request data from $result:
my @request_ids = keys %{ $result->{requests} }; # requests = href of req.ids
my $data = $self->model('Request')->get_requests(\@request_ids); # arrayref
# replace $result->{requests}{request_id} index count with request table row:
$result->{requests}->{$_->id} = $_->as_tree for @$data;
}
# add plate ID:
$result->{plateId} = $plate_id;
# add original scanned plate data from session:
$result->{scan_data} = $xtr_96_data->{plate_data};
return $self->tt_process({ data => $result });
}
# ------------------------------------------------------------------------------
sub _get_xtr_96_data {
my $self = shift; $self->_debug_path();
my ($host, $port) = @_;
return _dummy_data() if $ENV{SERVER_PORT} == 8000; # uncomment to use test data
my @args = (
Timeout => 15,
Errmode => 'return', # don't die (default)
# Input_Log => './xtr-96.log', # uncomment to debug
);
my $telnet = Net::Telnet->new(@args);
my @host = ( Host => $host, Port => $port );
unless ( $telnet->open(@host) ) { # returns 1 on success, 0 on failure:
$self->flash( error => $telnet->errmsg ); # failure msg in errmsg()
return 0;
}
$telnet->cmd('get');
# direct manipulation of buffer easier than documented methods!!
my $ref = $telnet->buffer; # p $ref;
$telnet->close;
my ($header, $body) = split "\n", ${$ref}; # p [$header, $body];
my ($plateId) = $header =~ /Rack Identifier = ([A-Z0-9]+)/;
# my %plate_data = ( $str =~ /^([A-H][0-9]{2})\,\s?(.+?)$/mg ); # if $str is list format
my %data = split /,\s?/, $body; # p %data;
my %h = (
plate_data => \%data,
plateId => $plateId,
); # p %h;
return \%h;
}
sub _get_menu_options {
my $self = shift;
my $yaml = $self->get_yaml_file('menu_options'); # p $yaml;
my $opts = $yaml->{storage_options};
return $opts;
}
1;
#===============================================================================
=begin # to clear request_storage & storage_racks tables:
UPDATE request_storage SET rack_id = NULL;
UPDATE request_storage SET vial_location = NULL;
UPDATE request_storage SET signed_out = NULL;
SET FOREIGN_KEY_CHECKS=0;
TRUNCATE TABLE storage_racks;
SET FOREIGN_KEY_CHECKS=1;
=cut
sub _dummy_data { # dummy data to spare plate reader ...
my $plateId = 'SA00084423'; # SA00098711
=begin to get more:
select vialId, vial_location
from request_storage
where vial_location is not null and signed_out is null
group by vial_location
order by vial_location
select vial_location, vialId
from request_storage rs
join storage_racks sr on rs.rack_id = sr.id
where sr.plateId = 'SA00084423'
order by vial_location
=cut
my %data = (
'A01' => '1020850304',
'A02' => '1020850312',
'A03' => '1020850320',
'A04' => '1020850328',
'A05' => '1020850336',
'A06' => '1020850344',
'A07' => '1020850352',
'A08' => '1020850360',
'A09' => '1020850368',
'A10' => '1020850321',
'A11' => '1020850384',
'A12' => '1020850392',
'B01' => '1020850305',
'B02' => '1020850313',
'B03' => '1020850348',
'B04' => '1020850383',
'B05' => '1020850391',
'B06' => '1020850345',
'B07' => '1020850353',
'B08' => '1020850361',
'B09' => '1020850369',
'B10' => '1020850367',
'B11' => '1020850385',
'B12' => '1020850393',
'C01' => '1020850306',
'C02' => '1020850314',
'C03' => '1020850322',
'C04' => '1020850359',
'C05' => '1020850338',
'C06' => '1020850346',
'C07' => '1020850354',
'C08' => '1020850362',
'C09' => '1020850370',
'C10' => '1020850378',
'C11' => '1020850386',
'C12' => '1020850394',
'D01' => '1020850319',
'D02' => '1020850315',
'D03' => '1020850323',
'D04' => '1020850331',
'D05' => '1020850355',
'D06' => '1020850347',
'D07' => '1020850397',
'D08' => '1020850398',
'D09' => '1020850371',
'D10' => '1020850379',
'D11' => '1020850387',
'D12' => '1020850395',
'E01' => '1020850308',
'E02' => '1020850316',
'E03' => '1020850324',
'E04' => '1020850332',
'E05' => '1020850340',
'E06' => '1020850311',
'E07' => '1020850356',
'E08' => '1020850364',
'E09' => '1020850372',
'E10' => '1020850380',
'E11' => '1020850388',
'E12' => '1020850382',
'F01' => '1020850390',
'F02' => '1020850317',
'F03' => '1020850325',
'F04' => '1020850358',
'F05' => '1020850333',
'F06' => '1020850357',
'F07' => '1020850374',
'F08' => '1020850365',
'F09' => '1020850373',
'F10' => '1020850381',
'F11' => '1020850406',
'F12' => '1020850414',
'G01' => '1020850422',
'G02' => '1020850405',
'G03' => '1020850413',
'G04' => '1020850421',
'G05' => '1020850429',
'G06' => '1020850437',
'G07' => '1020850445',
'G08' => '1020850453',
'G09' => '1020850461',
'G10' => '1020850469',
'G11' => '1020850477',
'G12' => '1020850485',
'H01' => '1020850493',
'H02' => '1020850404',
'H03' => '1020850412',
'H04' => '1020850420',
'H05' => '1020850428',
'H06' => '1020850436',
'H07' => '1020850444',
'H08' => '1020850452',
'H09' => '1020850460',
'H10' => '1020850468',
'H11' => '1020850476',
'H12' => '1020850484',
); # warn Dumper \%data;
return { # same format as xtr-96 output:
plate_data => \%data,
plateId => $plateId,
}
}