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 = 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::Storage', # auto_location '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 # storage locations (for auto-calculation of next rack/tray): if ( my $yml = $self->get_yaml_file('storage_locations') ) { # p $yml; $self->tt_params( storage_locations => $yml ); } 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{REMOTE_ADDR} eq $ip ) # test & devel server OK: || grep $ENV{$_}, qw(DEVEL_SERVER HARNESS_ACTIVE); $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' ); # warn Dumper $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 = do { my %args = ( plateId => $plate_id ); if ( my $base_location = $vars->{rack_location} ) { my $location = $self->auto_location($base_location); # warn $location; # add storage_location to lexical $xtr_96_data for later access: $args{storage_location} = $xtr_96_data->{storage_location} = $location; } $self->model('Storage')->new_storage_rack(%args); }; # $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}; # add storage location (if supplied): $result->{storage_location} = $xtr_96_data->{storage_location}; return $self->tt_process({ data => $result }); } # ------------------------------------------------------------------------------ sub _get_xtr_96_data { my $self = shift; $self->_debug_path(); my ($host, $port) = @_; return $self->_dummy_data() if $ENV{SERVER_PORT} == 8000; # uncomment to use test data # return $self->_test_data() if $ENV{HARNESS_ACTIVE}; # TODO: for storage.t my @args = ( # Timeout => 15, done in waitfor() now otherwise processes delay until $secs Errmode => 'return', # don't die (default) # Input_Log => './xtr-96.log', # uncomment to debug ); my $t = Net::Telnet->new(@args); my @host = ( Host => $host, Port => $port ); unless ( $t->open(@host) ) { # returns 1 on success, 0 on failure: $self->flash( error => $t->errmsg ); # failure msg in errmsg() return 0; } # initiate plate read: $t->cmd('get'); my $re = qr{H12,(.*)$}; # end of output = H12, my ($prematch, $match) = $t->waitfor(Match => "/$re/", Timeout => undef); # no timeout $self->flash( error => $t->errmsg ) if $t->errmsg; $t->close; return 0 unless $prematch && $match; # assume error captured in $telnet->errmsg my $data = $prematch . $match; # p $data; my ($header, $body) = split "\n", $data; # p [$header, $body]; my ($plateId) = $header =~ /Rack Identifier = ([A-Z0-9]+)/; my %h = ( plateId => $plateId ); { no warnings 'uninitialized'; # happens if process timeout waiting for reader # my %plate_data = ( $str =~ /^([A-H][0-9]{2})\,\s?(.+?)$/mg ); # if $str is list format my %plate_data = split /,\s?/, $body; # p %plate_data; $h{plate_data} = \%plate_data; } 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 $self = shift; #=begin # uncomment to get 96 vials from request_storage: my $q = [ # uncomment one: # vial_location => undef # not assigned to a rack vial_location => { ne => undef } # is assigned to a rack ]; my $data = do { my %args = ( query => $q, limit => 96 ); $self->model('Base')->get_objects('RequestStorage', {} ); }; # warn Dumper $_->as_tree for @$data; my @cells = map { $_ . '01' .. $_ . '12' } ( 'A' .. 'H' ); # warn Dumper \@cells; my %h; @h{@cells} = map $_->vialId, @$data; # warn Dumper \%h; return { # same format as xtr-96 output: plate_data => \%h, plateId => DateTime->now->epoch, }; #=cut =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 $plateId = 'SA00084427'; # SA00098711 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, } }