diff --git a/js/source/legacy/CXGN/Trial.js b/js/source/legacy/CXGN/Trial.js index 2980c91a4b..db3b53e14e 100644 --- a/js/source/legacy/CXGN/Trial.js +++ b/js/source/legacy/CXGN/Trial.js @@ -221,10 +221,10 @@ function delete_field_map() { jQuery('#working_modal').modal("hide"); if (response.error) { - alert("Error Deleting Field Map: "+response.error); + alert("Error deleting field map: "+response.error); } else { - //alert("Field map deletion Successful..."); - jQuery('#delete_field_map_dialog_message').dialog("open"); + alert("Field map deletion successful."); + location.reload(); } }, error: function () { @@ -493,7 +493,11 @@ delete_field_map(); jQuery('#delete_field_map_hm_link').click(function () { - jQuery('#delete_field_map_dialog').dialog("open"); + if (confirm("Are you sure you want to delete the spatial layout of this trial?")) { + delete_field_map(); + } else { + return; + } }); jQuery("#delete_field_map_dialog_message").dialog({ @@ -635,34 +639,43 @@ jQuery(document).ready(function ($) { $("#trial_coord_upload_spreadsheet_info_dialog" ).modal("show"); }); - $('#upload_trial_coordinates_form').iframePostForm({ - json: true, - post: function () { + $('#upload_trial_coordinates_form').iframePostForm({ + json: true, + post: function () { var uploadedtrialcoordFile = $("#trial_coordinates_uploaded_file").val(); - $('#working_modal').modal("show"); + $('#working_modal').modal("show"); if (uploadedtrialcoordFile === '') { - $('#working_modal').modal("hide"); - alert("No file selected"); + $('#working_modal').modal("hide"); + alert("No file selected"); } - }, - complete: function (response) { - $('#working_modal').modal("hide"); - if (response.error_string) { - $("#upload_trial_coord_error_display tbody").html(''); - $("#upload_trial_coord_error_display tbody").append(response.error_string); - jQuery('#upload_trial_coord_error_display').modal('show'); + }, + complete: function (response) { + $('#working_modal').modal("hide"); - return; + // Check if response exists and is valid + if (!response || typeof response !== 'object') { + alert("Error: No response received from server. Please try again."); + return; } + if (response.error) { - alert(response.error); - return; + $("#upload_trial_coord_error_display tbody").html(''); + $("#upload_trial_coord_error_display tbody").append(response.error); + jQuery('#upload_trial_coord_error_display').modal('show'); + return; } + if (response.success) { - $('#trial_coord_upload_success_dialog_message').modal("show"); - //alert("File uploaded successfully"); + $('#trial_coord_upload_success_dialog_message').modal("show"); + //alert("File uploaded successfully"); + return; } - } + + // Catch-all for unexpected responses + console.error("Unexpected server response:", response); + alert("An unexpected error occurred. The server response was not in the expected format. Please check the console for details or contact support."); + + } }); function upload_trial_coord_file() { diff --git a/lib/CXGN/Job.pm b/lib/CXGN/Job.pm index 04f0b0de1e..9c717f617c 100644 --- a/lib/CXGN/Job.pm +++ b/lib/CXGN/Job.pm @@ -217,7 +217,7 @@ The command submitted to be run. =cut -has 'cmd' => (isa => 'Str', is => 'rw'); +has 'cmd' => (isa => 'Maybe[Str]', is => 'rw'); =head2 logfile() diff --git a/lib/CXGN/Trial/ParseUpload/Plugin/TrialSpatialLayoutGeneric.pm b/lib/CXGN/Trial/ParseUpload/Plugin/TrialSpatialLayoutGeneric.pm new file mode 100644 index 0000000000..b8cedcbd7c --- /dev/null +++ b/lib/CXGN/Trial/ParseUpload/Plugin/TrialSpatialLayoutGeneric.pm @@ -0,0 +1,180 @@ +package CXGN::Trial::ParseUpload::Plugin::TrialSpatialLayoutGeneric; + +use Moose::Role; +use List::MoreUtils qw(uniq); +use CXGN::File::Parse; +use SGN::Model::Cvterm; +use CXGN::List::Validate; +use CXGN::Stock; +use CXGN::Project; +use Data::Dumper; + +my @REQUIRED_COLUMNS = qw|plot_name row_number col_number|; +my @OPTIONAL_COLUMNS = qw||; +# Any additional columns are unsupported and will return an error + +sub _validate_with_plugin { + my $self = shift; + my $filename = $self->get_filename(); + my $schema = $self->get_chado_schema(); + my $trial_id = $self->get_trial_id(); + + # List validator + my $validator = CXGN::List::Validate->new(); + + # Encountered Error and Warning Messages + my %errors; + my @error_messages; + my %warnings; + my @warning_messages; + + # Read and parse the upload file + my $parser = CXGN::File::Parse->new( + file => $filename, + required_columns => \@REQUIRED_COLUMNS, + optional_columns => \@OPTIONAL_COLUMNS + ); + my $parsed = $parser->parse(); + my $parsed_errors = $parsed->{'errors'}; + my $parsed_data = $parsed->{'data'}; + my $parsed_values = $parsed->{'values'}; + my $additional_columns = $parsed->{'additional_columns'}; + + # Return file parsing errors + if ( $parsed_errors && scalar(@$parsed_errors) > 0 ) { + $errors{'error_messages'} = $parsed_errors; + $self->_set_parse_errors(\%errors); + return; + } + + # Unsupported column headers + if ( $additional_columns && scalar(@$additional_columns) > 0 ) { + $errors{'error_messages'} = [ 'The following column headers are not supported: ' . join(', ', @$additional_columns) ]; + $self->_set_parse_errors(\%errors); + return; + } + + # Maps to track row/col positions + my %seen_positions; # check that each row_number/col_number pair is used only once + + foreach my $data (@$parsed_data) { + my $row = $data->{'_row'}; + my $plot_name = $data->{'plot_name'}; + my $row_number = $data->{'row_number'}; + my $col_number = $data->{'col_number'}; + + # Row Number: must be a positive integer + if (!($row_number =~ /^\d+?$/)) { + push @error_messages, "Row $row: row_number $row_number must be a positive integer."; + } + + # Col Number: must be a positive integer + if (!($col_number =~ /^\d+?$/)) { + push @error_messages, "Row $row: col_number $col_number must be a positive integer."; + } + + # Track row/col positions to check for duplicates + my $position_key = "$row_number-$col_number"; + if ( !exists $seen_positions{$position_key} ) { + $seen_positions{$position_key} = [$plot_name]; + } + else { + push @{$seen_positions{$position_key}}, $plot_name; + } + } + + # Plots must exist + my @plot_names = @{$parsed_values->{'plot_name'}}; + my @missing_plots = @{$validator->validate($schema, 'plots', \@plot_names)->{'missing'}}; + + # Report missing plots + if (scalar(@missing_plots) > 0) { + push @error_messages, "Plot name(s) ".join(', ',@missing_plots)." do not exist in the database as plots."; + } + + # Check that all plots belong to the same trial + my $trial = CXGN::Project->new({ + bcs_schema => $schema, + trial_id => $trial_id + }); + + my %trial_plots = map {$_->[1] => 1} @{$trial->get_plots()}; + + my @mismatched_plots; + + foreach my $plot (@plot_names) { + if (!defined($trial_plots{$plot})) { + push @mismatched_plots, $plot; + } + } + + if (scalar(@mismatched_plots) > 0) { + push @error_messages, "All plots must belong to ".$trial->get_name().". "; + } + + # Check for unique row/col positions + foreach my $position_key (keys %seen_positions) { + my $plots = $seen_positions{$position_key}; + my $count = scalar(@$plots); + if ( $count > 1 ) { + my @pos = split('-', $position_key); + push @error_messages, "Position row=" . $pos[0] . " col=" . $pos[1] . " is assigned to multiple plots: " . join(', ', @$plots) . ". Each position can only be occupied once."; + } + } + + if (scalar(@error_messages) >= 1) { + $errors{'error_messages'} = \@error_messages; + $self->_set_parse_errors(\%errors); + return; + } + + $self->_set_validated_data($parsed); + return 1; #returns true if validation is passed +} + +sub _parse_with_plugin { + my $self = shift; + my $schema = $self->get_chado_schema(); + my $parsed = $self->_get_validated_data(); + my $data = $parsed->{'data'}; + + # Get plot stock type cvterm + my $plot_cvterm_id = SGN::Model::Cvterm->get_cvterm_row($schema, 'plot', 'stock_type')->cvterm_id(); + + my %spatial_layout_data; + + foreach my $d (@$data) { + my $plot_name = $d->{'plot_name'}; + my $row_number = $d->{'row_number'}; + my $col_number = $d->{'col_number'}; + + # Get plot stock_id from plot_name + my $rs = $schema->resultset("Stock::Stock")->search({ + 'uniquename' => $plot_name, + 'type_id' => $plot_cvterm_id, + 'is_obsolete' => { '!=' => 't' } + }); + + if ($rs->count() > 0) { + my $plot_stock = $rs->first(); + my $plot_id = $plot_stock->stock_id(); + + # Store the spatial layout information + $spatial_layout_data{$plot_id} = { + plot_name => $plot_name, + row_number => $row_number, + col_number => $col_number + }; + } + } + + # print STDERR Dumper \%spatial_layout_data; + + $self->_set_parsed_data({ + spatial_layout => \%spatial_layout_data + }); + + return 1; +} + +1; diff --git a/lib/SGN/Controller/AJAX/TrialMetadata.pm b/lib/SGN/Controller/AJAX/TrialMetadata.pm index 8380dc732a..e030871f1d 100644 --- a/lib/SGN/Controller/AJAX/TrialMetadata.pm +++ b/lib/SGN/Controller/AJAX/TrialMetadata.pm @@ -40,6 +40,8 @@ use List::Util 'sum'; use CXGN::Trial::TrialLayout; use CXGN::BreedersToolbox::Projects; use Sort::Key::Natural qw(natkeysort); +use CXGN::Trial::ParseUpload; +use CXGN::Job; BEGIN { extends 'Catalyst::Controller::REST' } @@ -3676,41 +3678,95 @@ sub upload_trial_coordinates : Path('/ajax/breeders/trial/coordsupload') Args(0) return; } + # print STDERR "Proceeding with $archived_filename_with_path \n"; + $md5 = $uploader->get_md5($archived_filename_with_path); unlink $upload_tempfile; - my $error_string = ''; - # open file and remove return of line - open(my $F, "< :encoding(UTF-8)", $archived_filename_with_path) || die "Can't open archive file $archived_filename_with_path"; my $schema = $c->dbic_schema("Bio::Chado::Schema", undef, $user_id); - my $header = <$F>; - while (<$F>) { - chomp; - $_ =~ s/\r//g; - my ($plot,$row,$col) = split /\t/ ; - my $rs = $schema->resultset("Stock::Stock")->search({uniquename=> $plot }); - if ($rs->count()== 1) { - my $r = $rs->first(); - print STDERR "The plots $plot was found.\n Loading row $row col $col\n"; - $r->create_stockprops({row_number => $row, col_number => $col}); - } - else { - print STDERR "WARNING! $plot was not found in the database.\n"; - $error_string .= "WARNING! $plot was not found in the database."; - } - } - if ($error_string){ - $c->stash->{rest} = {error_string => $error_string}; + my $trial_obj = CXGN::Project->new({ + bcs_schema => $schema, + trial_id => $trial_id + }); + + my $job = CXGN::Job->new({ + sp_person_id => $user_id, + schema => $schema, + people_schema => $c->dbic_schema("CXGN::People::Schema"), + name => $trial_obj->get_name()." spatial layout upload", + job_type => 'upload', + cmd => "", + finish_logfile => $c->config->{job_finish_log}, + submit_page => $c->request->headers->referer, + results_page => $c->request->headers->referer + }); + + $job->update_status("submitted"); + + my $parser = CXGN::Trial::ParseUpload->new({ + chado_schema => $schema, + trial_id => $trial_id, + filename => $archived_filename_with_path + }); + $parser->load_plugin('TrialSpatialLayoutGeneric'); + + my $parsed_data = $parser->parse(); + + if (!$parsed_data || $parser->has_parse_errors()) { + my $return_error = ''; + + if (! $parser->has_parse_errors() ){ + $return_error = "Parsing failed, but we could not get parsing errors..."; + } + else { + my $parse_errors = $parser->get_parse_errors(); + + foreach my $error_string (@{$parse_errors->{'error_messages'}}){ + $return_error=$return_error.$error_string."
"; + } + } + + $c->stash->{rest} = {error => $return_error}; + $job->additional_args({ + error => $return_error + }); + $job->update_status("failed"); $c->detach(); + return; + } + + # store all row/column data here + eval { + foreach my $plot_id (keys(%{$parsed_data->{spatial_layout}})) { + my $plot_name = $parsed_data->{spatial_layout}->{$plot_id}->{plot_name}; + my $row = $parsed_data->{spatial_layout}->{$plot_id}->{row_number}; + my $col = $parsed_data->{spatial_layout}->{$plot_id}->{col_number}; + + my $rs = $schema->resultset("Stock::Stock")->search({uniquename=> $plot_name }); + my $r = $rs->first(); + $r->create_stockprops({row_number => $row, col_number => $col}); + } + }; + if ($@) { + $c->stash->{rest} = {error => "The upload was successful, but an error occurred trying to save the row and column data: $@\n"}; + $job->additional_args({ + error => $@ + }); + $job->update_status("failed"); + return; } + # print STDERR Dumper $parsed_data->{spatial_layout}; + my $dbh = $c->dbc->dbh(); my $bs = CXGN::BreederSearch->new( { dbh=>$dbh, dbname=>$c->config->{dbname}, } ); - my $refresh = $bs->refresh_matviews($c->config->{dbhost}, $c->config->{dbname}, $c->config->{dbuser}, $c->config->{dbpass}, 'phenotypes', 'concurrent', $c->config->{basepath}); + my $refresh = $bs->refresh_matviews($c->config->{dbhost}, $c->config->{dbname}, $c->config->{dbuser}, $c->config->{dbpass}, 'stockprop', 'concurrent', $c->config->{basepath}); my $trial_layout = CXGN::Trial::TrialLayout->new({ schema => $c->dbic_schema("Bio::Chado::Schema", undef, $user_id), trial_id => $trial_id, experiment_type => 'field_layout' }); $trial_layout->generate_and_cache_layout(); + $job->update_status("finished"); + $c->stash->{rest} = {success => 1}; } diff --git a/mason/breeders_toolbox/trial.mas b/mason/breeders_toolbox/trial.mas index c73c5c75e0..f19869e230 100644 --- a/mason/breeders_toolbox/trial.mas +++ b/mason/breeders_toolbox/trial.mas @@ -171,9 +171,9 @@ $project_id => undef % my $subtitle = 'View and edit the spatial layout of the experiment. Also view a heatmap for phenotyped traits.'; % my $layout_buttons = ''; % if ($has_col_and_row_numbers){ -% $layout_buttons = '

   This experiment has spatial layout info uploaded!

'; +% $layout_buttons = '

   This experiment has spatial layout info uploaded!

'; % } else { -% $layout_buttons = '

   This experiment does not have spatial layout info!

'; +% $layout_buttons = '

   This experiment does not have spatial layout info!

'; % } % my $design_section_buttons = ''; diff --git a/mason/breeders_toolbox/trial/phenotype_heatmap.mas b/mason/breeders_toolbox/trial/phenotype_heatmap.mas index 04dc9140bb..0f0b34f5f5 100644 --- a/mason/breeders_toolbox/trial/phenotype_heatmap.mas +++ b/mason/breeders_toolbox/trial/phenotype_heatmap.mas @@ -341,13 +341,6 @@ $trial_stock_type => undef --> - -