diff --git a/lib/CXGN/Cvterm.pm b/lib/CXGN/Cvterm.pm index 6631fdbd0c..2925c4bbcc 100644 --- a/lib/CXGN/Cvterm.pm +++ b/lib/CXGN/Cvterm.pm @@ -415,6 +415,43 @@ sub _store_cvtermprop { +sub add_parent_terms { + my $self = shift; + my $parent_ids = shift; + + my $schema = $self->schema(); + my $cvterm_id = $self->cvterm_id(); + + my $relationship_cv = $schema->resultset("Cv::Cv")->find({ name => 'relationship' }); + die "No 'relationship' CV found in the database.\n" unless $relationship_cv; + my $rel_cv_id = $relationship_cv->cv_id(); + + my $variable_of = $schema->resultset("Cv::Cvterm")->find({ name => 'VARIABLE_OF', cv_id => $rel_cv_id }); + my $isa = $schema->resultset("Cv::Cvterm")->find({ name => 'is_a', cv_id => $rel_cv_id }); + die "No 'is_a' relationship type found.\n" unless $isa; + + my $type_id = $isa->cvterm_id(); + if ($variable_of) { + my $has_variable_of = $schema->resultset("Cv::CvtermRelationship")->search({ + subject_id => $cvterm_id, + type_id => $variable_of->cvterm_id(), + })->count(); + if ($has_variable_of > 0) { + $type_id = $variable_of->cvterm_id(); + } + } + + $schema->txn_do(sub { + for my $parent_id (@$parent_ids) { + $schema->resultset("Cv::CvtermRelationship")->find_or_create({ + subject_id => $cvterm_id, + object_id => $parent_id, + type_id => $type_id, + }); + } + }); +} + __PACKAGE__->meta->make_immutable; ########## diff --git a/lib/CXGN/Trait.pm b/lib/CXGN/Trait.pm index e442da7b97..759d36d5cd 100644 --- a/lib/CXGN/Trait.pm +++ b/lib/CXGN/Trait.pm @@ -795,11 +795,11 @@ sub delete_existing_synonyms { sub interactive_store { my $self = shift; - my $parent_term = shift; + my $parent_terms_json = shift; my $schema = $self->bcs_schema(); - my $parent_id; + my @parent_ids; my $name = $self->name() || die "No name found.\n"; my $definition = $self->definition() || die "No definition found.\n"; @@ -878,17 +878,20 @@ sub interactive_store { my $db_name = $h->fetchrow_array(); - if ($parent_term) { + my $parent_terms = []; + if ($parent_terms_json) { + $parent_terms = decode_json($parent_terms_json); + } + + if (@$parent_terms) { my $lt = CXGN::List::Transform->new(); - - my $transform = $lt->transform($schema, "traits_2_trait_ids", [$parent_term]); + my $transform = $lt->transform($schema, "traits_2_trait_ids", $parent_terms); - if (@{$transform->{missing}}>0) { - die "Parent term $parent_term could not be found in the database.\n"; + if (@{$transform->{missing}} > 0) { + die "Parent term(s) " . join(", ", @{$transform->{missing}}) . " could not be found in the database.\n"; } - my @parent_id_list = @{$transform->{transform}}; - $parent_id = $parent_id_list[0]; + @parent_ids = @{$transform->{transform}}; } else { my $ontology_obj = CXGN::Onto->new({ schema => $schema @@ -897,7 +900,7 @@ sub interactive_store { my $root_term_name = $root_nodes[0]->[1] =~ s/\w+:\d+ //r; - $parent_id = $schema->resultset("Cv::Cvterm")->find({ + push @parent_ids, $schema->resultset("Cv::Cvterm")->find({ name => $root_term_name, cv_id => $root_nodes[0]->[0] })->cvterm_id(); @@ -947,18 +950,20 @@ sub interactive_store { dbxref => "$zeroes"."$accession_num" })->cvterm_id(); - if ($format eq "ontology") { - $schema->resultset("Cv::CvtermRelationship")->find_or_create({ - object_id => $parent_id, - subject_id => $new_trait_id, - type_id => $isa_id - }); - } else { - $schema->resultset("Cv::CvtermRelationship")->find_or_create({ - object_id => $parent_id, - subject_id => $new_trait_id, - type_id => $variable_of_id - }); + foreach my $pid (@parent_ids) { + if ($format eq "ontology") { + $schema->resultset("Cv::CvtermRelationship")->find_or_create({ + object_id => $pid, + subject_id => $new_trait_id, + type_id => $isa_id + }); + } else { + $schema->resultset("Cv::CvtermRelationship")->find_or_create({ + object_id => $pid, + subject_id => $new_trait_id, + type_id => $variable_of_id + }); + } } $new_trait = $schema->resultset("Cv::Cvterm")->find({ diff --git a/lib/CXGN/Trait/Treatment.pm b/lib/CXGN/Trait/Treatment.pm index 4633dba74f..0c80152535 100644 --- a/lib/CXGN/Trait/Treatment.pm +++ b/lib/CXGN/Trait/Treatment.pm @@ -42,20 +42,26 @@ sub BUILD { sub store { my $self = shift; - my $parent_term = shift; + my $parent_terms_json = shift; my $schema = $self->bcs_schema(); - + + my $parent_terms = []; + if ($parent_terms_json) { + $parent_terms = decode_json($parent_terms_json); + } + if (!@$parent_terms) { + $parent_terms = ['Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000']; + } + my $lt = CXGN::List::Transform->new(); - - my $transform = $lt->transform($schema, "traits_2_trait_ids", [$parent_term]); + my $transform = $lt->transform($schema, "traits_2_trait_ids", $parent_terms); - if (@{$transform->{missing}}>0) { - die "Parent term $parent_term could not be found in the database.\n"; + if (@{$transform->{missing}} > 0) { + die "Parent term(s) " . join(", ", @{$transform->{missing}}) . " could not be found in the database.\n"; } - my @parent_id_list = @{$transform->{transform}}; - my $parent_id = $parent_id_list[0]; + my @parent_ids = @{$transform->{transform}}; my $name = $self->name() || die "No name found.\n"; my $definition = $self->definition() || die "No definition found.\n"; @@ -166,18 +172,20 @@ sub store { dbxref => "$zeroes"."$accession_num" })->cvterm_id(); - if ($format eq "ontology") { - $schema->resultset("Cv::CvtermRelationship")->find_or_create({ - object_id => $parent_id, - subject_id => $new_treatment_id, - type_id => $isa_id - }); - } else { - $schema->resultset("Cv::CvtermRelationship")->find_or_create({ - object_id => $parent_id, - subject_id => $new_treatment_id, - type_id => $variable_of_id - }); + foreach my $pid (@parent_ids) { + if ($format eq "ontology") { + $schema->resultset("Cv::CvtermRelationship")->find_or_create({ + object_id => $pid, + subject_id => $new_treatment_id, + type_id => $isa_id + }); + } else { + $schema->resultset("Cv::CvtermRelationship")->find_or_create({ + object_id => $pid, + subject_id => $new_treatment_id, + type_id => $variable_of_id + }); + } } $new_treatment = $schema->resultset("Cv::Cvterm")->find({ diff --git a/lib/SGN/Controller/AJAX/Cvterm.pm b/lib/SGN/Controller/AJAX/Cvterm.pm index 3d9000d432..e635e1af7a 100644 --- a/lib/SGN/Controller/AJAX/Cvterm.pm +++ b/lib/SGN/Controller/AJAX/Cvterm.pm @@ -22,6 +22,7 @@ use List::MoreUtils qw /any /; use Try::Tiny; use CXGN::Page::FormattingHelpers qw/ columnar_table_html commify_number /; use CXGN::Chado::Cvterm; +use CXGN::Cvterm; use Data::Dumper; use JSON; @@ -436,6 +437,210 @@ sub delete_cvtermprop_GET { $c->stash->{rest} = { message => "The cvterm prop was removed from the database." }; } +sub add_synonym : Path('/ajax/cvterm/add_synonym') : ActionClass('REST') { } + +sub add_synonym_POST { + my ($self, $c) = @_; + + if (!$c->user()) { + $c->stash->{rest} = { error => "You must be logged in to add synonyms." }; + return; + } + if (!$c->user()->check_roles('curator')) { + $c->stash->{rest} = { error => "You must have curator privileges to add synonyms." }; + return; + } + + my $cvterm_id = $c->req->param('cvterm_id'); + my $synonym = $c->req->param('synonym'); + + $synonym =~ s/[^[:ascii:]]//g; + $synonym =~ s/\|//g; + $synonym =~ s/^\s+|\s+$//g; + + if (!$synonym) { + $c->stash->{rest} = { error => "Synonym cannot be empty." }; + return; + } + + my $dbh = $c->dbc->dbh; + + my $check_sth = $dbh->prepare("SELECT cvtermsynonym_id FROM cvtermsynonym WHERE synonym ilike ?"); + $check_sth->execute($synonym); + my ($existing_id) = $check_sth->fetchrow_array(); + if ($existing_id) { + $c->stash->{rest} = { error => "The synonym '$synonym' already exists in the database." }; + return; + } + + eval { + my $cvterm = CXGN::Chado::Cvterm->new($dbh, $cvterm_id); + $cvterm->add_synonym($synonym); + }; + if ($@) { + $c->stash->{rest} = { error => "Error adding synonym: $@" }; + return; + } + + $c->stash->{rest} = { message => "Synonym '$synonym' added successfully." }; +} + + +sub delete_synonym : Path('/ajax/cvterm/delete_synonym') : ActionClass('REST') { } + +sub delete_synonym_POST { + my ($self, $c) = @_; + + if (!$c->user()) { + $c->stash->{rest} = { error => "You must be logged in to delete synonyms." }; + return; + } + if (!$c->user()->check_roles('curator')) { + $c->stash->{rest} = { error => "You must have curator privileges to delete synonyms." }; + return; + } + + my $cvterm_id = $c->req->param('cvterm_id'); + my $synonym = $c->req->param('synonym'); + + if (!$synonym) { + $c->stash->{rest} = { error => "Synonym cannot be empty." }; + return; + } + + my $dbh = $c->dbc->dbh; + + eval { + my $cvterm = CXGN::Chado::Cvterm->new($dbh, $cvterm_id); + $cvterm->delete_synonym($synonym); + }; + if ($@) { + $c->stash->{rest} = { error => "Error deleting synonym: $@" }; + return; + } + + $c->stash->{rest} = { message => "Synonym '$synonym' deleted successfully." }; +} + +sub cvterm_relationships : Path('/ajax/cvterm/cvterm_relationships') : ActionClass('REST') { } + +sub cvterm_relationships_GET { + my ($self, $c) = @_; + + my $cvterm_id = $c->req->param('cvterm_id'); + if (!$cvterm_id) { + $c->stash->{rest} = { error => "cvterm_id is required." }; + return; + } + + my $sth = $c->dbc->dbh->prepare( + "SELECT type.name AS relationship_name, + object.cvterm_id AS object_cvterm_id, + object.name AS object_name + FROM cvterm_relationship + JOIN cvterm AS type ON (type.cvterm_id = cvterm_relationship.type_id) + JOIN cvterm AS object ON (object.cvterm_id = cvterm_relationship.object_id) + WHERE subject_id = ?" + ); + + my @relationships; + eval { + $sth->execute($cvterm_id); + while (my ($rel_name, $obj_id, $obj_name) = $sth->fetchrow_array()) { + push @relationships, { + relationship_name => $rel_name, + object_cvterm_id => $obj_id, + object_name => $obj_name, + }; + } + }; + if ($@) { + $c->stash->{rest} = { error => "Error retrieving relationships: $@" }; + return; + } + + $c->stash->{rest} = { relationships => \@relationships }; +} + +sub add_parent_terms : Path('/ajax/cvterm/add_parent_terms') : ActionClass('REST') { } + +sub add_parent_terms_POST { + my ($self, $c) = @_; + + if (!$c->user()) { + $c->stash->{rest} = { error => "You must be logged in to add parent terms." }; + return; + } + if (!$c->user()->check_roles('curator')) { + $c->stash->{rest} = { error => "You must have curator privileges to add parent terms." }; + return; + } + + my $cvterm_id = $c->req->param('cvterm_id'); + my $parent_terms_json = $c->req->param('parent_terms'); + + my $parent_terms; + eval { $parent_terms = decode_json($parent_terms_json); }; + if ($@ || ref($parent_terms) ne 'ARRAY') { + $c->stash->{rest} = { error => "Invalid parent_terms parameter." }; + return; + } + + my $schema = $c->dbic_schema('Bio::Chado::Schema', 'sgn_chado'); + + my @parent_ids; + for my $term_str (@$parent_terms) { + my ($name, $accession_str) = split(/\|/, $term_str, 2); + unless (defined $accession_str) { + $c->stash->{rest} = { error => "Could not parse parent term '$term_str'. Expected format: name|DB:accession." }; + return; + } + my ($db_name_part, $accession) = split(/:/, $accession_str, 2); + unless (defined $db_name_part && defined $accession) { + $c->stash->{rest} = { error => "Could not parse accession from '$accession_str'." }; + return; + } + + my $dbxref = $schema->resultset("General::Dbxref")->find( + { + 'db.name' => $db_name_part, + 'me.accession' => $accession, + }, + { join => 'db' } + ); + unless ($dbxref && $dbxref->cvterm) { + $c->stash->{rest} = { error => "Parent term '$term_str' not found in the database." }; + return; + } + + my $parent_cvterm_id = $dbxref->cvterm->cvterm_id; + + my $existing = $schema->resultset("Cv::CvtermRelationship")->find({ + subject_id => $cvterm_id, + object_id => $parent_cvterm_id, + }); + next if $existing; + + push @parent_ids, $parent_cvterm_id; + } + + if (!@parent_ids) { + $c->stash->{rest} = { error => "All specified parent terms are already parents of this cvterm." }; + return; + } + + eval { + my $cvterm_obj = CXGN::Cvterm->new({ schema => $schema, cvterm_id => $cvterm_id }); + $cvterm_obj->add_parent_terms(\@parent_ids); + }; + if ($@) { + $c->stash->{rest} = { error => "Error adding parent terms: $@" }; + return; + } + + $c->stash->{rest} = { message => scalar(@parent_ids) . " parent term(s) added successfully." }; +} + #### 1;## #### diff --git a/lib/SGN/Controller/AJAX/Trait.pm b/lib/SGN/Controller/AJAX/Trait.pm index 8e5a945357..62a1fd5316 100644 --- a/lib/SGN/Controller/AJAX/Trait.pm +++ b/lib/SGN/Controller/AJAX/Trait.pm @@ -40,11 +40,12 @@ sub create_trait :Path('/ajax/trait/create') { my $categories = $c->req->param('categories') ? $c->req->param('categories') : undef; my $category_details = $c->req->param('category_details') ? $c->req->param('category_details') : undef; my $repeat_type = $c->req->param('repeat_type') ? $c->req->param('repeat_type') : undef; - my $parent_term = $c->req->param('parent_term') ? $c->req->param('parent_term') : undef; + my $parent_terms = $c->req->param('parent_terms') ? $c->req->param('parent_terms') : undef; $name =~ s/^\s+//; $name =~ s/\s+$//; $name =~ s/[^[:ascii:]]//g; + $name =~ s/\|//g; $definition =~ s/^\s+//; $definition =~ s/\s+$//; @@ -139,7 +140,7 @@ sub create_trait :Path('/ajax/trait/create') { $new_trait->default_value($default_value); } - $new_trait->interactive_store($parent_term); + $new_trait->interactive_store($parent_terms); }; if ($@) { diff --git a/lib/SGN/Controller/AJAX/Treatment.pm b/lib/SGN/Controller/AJAX/Treatment.pm index 6b2bca87a7..d559c5bea6 100644 --- a/lib/SGN/Controller/AJAX/Treatment.pm +++ b/lib/SGN/Controller/AJAX/Treatment.pm @@ -40,11 +40,12 @@ sub create_treatment :Path('/ajax/treatment/create') { my $categories = $c->req->param('categories') ? $c->req->param('categories') : undef; my $category_details = $c->req->param('category_details') ? $c->req->param('category_details') : undef; my $repeat_type = $c->req->param('repeat_type') ? $c->req->param('repeat_type') : undef; - my $parent_term = $c->req->param('parent_term') || 'Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000'; + my $parent_terms = $c->req->param('parent_terms') ? $c->req->param('parent_terms') : undef; $name =~ s/^\s+//; $name =~ s/\s+$//; $name =~ s/[^[:ascii:]]//g; + $name =~ s/\|//g; $definition =~ s/^\s+//; $definition =~ s/\s+$//; @@ -138,7 +139,7 @@ sub create_treatment :Path('/ajax/treatment/create') { $new_treatment->default_value($default_value); } - $new_treatment->store($parent_term); + $new_treatment->store($parent_terms); }; if ($@) { diff --git a/mason/chado/cvterm.mas b/mason/chado/cvterm.mas index c5d179b066..f4530686ed 100644 --- a/mason/chado/cvterm.mas +++ b/mason/chado/cvterm.mas @@ -115,7 +115,7 @@ my $edit_privs = $curator ; -<& /util/import_javascript.mas, classes => [qw [CXGN.AJAX.Ontology CXGN.Phenome.Qtl thickbox jquery jquery.dataTables] ] &> +<& /util/import_javascript.mas, classes => [qw [CXGN.AJAX.Ontology CXGN.Phenome.Qtl thickbox jquery jquery.dataTables jqueryui] ] &> + <&| /page/info_section.mas, title=>"Cvterm details" &> -% if ( $allow_edits ) { - +% if ( $allow_edits && $edit_privs ) { + % } @@ -216,11 +431,24 @@ my $edit_privs = $curator ; Obsolete: TRUE
% } -
Synonyms
+
+% if ( $allow_edits && $edit_privs ) { +[Edit] +
+% } +Synonyms
% foreach my $synonym (@synonyms) { <% $synonym %>
% } +
+% if ( $allow_edits && $edit_privs ) { +[Add parent terms] +
+% } + Relationships
+
+
Definition dbxrefs
% foreach my $da (@def_accessions) { <% $da %>
diff --git a/mason/tools/trait_designer.mas b/mason/tools/trait_designer.mas index 400628e573..760ccd1d6f 100644 --- a/mason/tools/trait_designer.mas +++ b/mason/tools/trait_designer.mas @@ -15,7 +15,7 @@
- +
@@ -84,9 +84,16 @@
+Will be the trait root term by default if none are added."> Chain to existing term(s):
- +
[Edit] [Delete]
[Edit] [Delete]
Term id <% $accession %>
Term name <% $cvterm_name %>
+ + + + +
+ + @@ -123,7 +130,11 @@ Will be the trait root term by default."> Chain to an existing term: }); categories = cat_list.join('/'); let category_details = cat_details_list.join('/'); - let parent_term = jQuery('#new_trait_parent_term').val(); + let parent_terms = []; + jQuery('#new_trait_parents li').each(function() { + parent_terms.push(jQuery(this).text()); + }); + let parent_terms_json = JSON.stringify(parent_terms); jQuery.ajax({ url: '/ajax/trait/create', @@ -136,7 +147,7 @@ Will be the trait root term by default."> Chain to an existing term: maximum : maximum, categories : categories, category_details : category_details, - parent_term : parent_term, + parent_terms : parent_terms_json, repeat_type : repeat_type }, success: function(response) { @@ -160,6 +171,24 @@ Will be the trait root term by default."> Chain to an existing term: source : '/ajax/cvterm/autocompleteslim' + "?db_name=<% $db_name %>" }); + jQuery('#new_trait_add_parent_btn').on('click', function(e) { + e.preventDefault(); + let parent_term = jQuery('#new_trait_parent_term').val().trim(); + if (parent_term) { + jQuery('
  • ').text(parent_term).appendTo('#new_trait_parents'); + jQuery('#new_trait_remove_parent_btn').show(); + jQuery('#new_trait_parent_term').val(''); + } + }); + + jQuery('#new_trait_remove_parent_btn').on('click', function(e) { + e.preventDefault(); + jQuery('#new_trait_parents li:last').remove(); + if (jQuery('#new_trait_parents li').length === 0) { + jQuery('#new_trait_remove_parent_btn').hide(); + } + }); + jQuery('#new_trait_format_select').on('change', function () { let trait_type = jQuery(this).val(); if (trait_type.toLowerCase() === 'numeric') { diff --git a/mason/tools/treatment_designer.mas b/mason/tools/treatment_designer.mas index 4ea2f1dda4..a03ad79428 100644 --- a/mason/tools/treatment_designer.mas +++ b/mason/tools/treatment_designer.mas @@ -16,7 +16,7 @@
    - +
    @@ -84,10 +84,17 @@
    - +
    - + + + + + +
    +
      +
      @@ -123,7 +130,11 @@ Will be the treatment root term by default."> Chain to an existing t }); categories = cat_list.join('/'); let category_details = cat_details_list.join('/'); - let parent_term = jQuery('#new_treatment_parent_term').val(); + let parent_terms = []; + jQuery('#new_treatment_parents li').each(function() { + parent_terms.push(jQuery(this).text()); + }); + let parent_terms_json = JSON.stringify(parent_terms); jQuery.ajax({ url: '/ajax/treatment/create', @@ -136,7 +147,7 @@ Will be the treatment root term by default."> Chain to an existing t maximum : maximum, categories : categories, category_details : category_details, - parent_term : parent_term, + parent_terms : parent_terms_json, repeat_type : repeat_type }, success: function(response) { @@ -160,6 +171,24 @@ Will be the treatment root term by default."> Chain to an existing t source : '/ajax/cvterm/autocompleteslim' + "?db_name=<% $db_name %>" }); + jQuery('#new_treatment_add_parent_btn').on('click', function(e) { + e.preventDefault(); + let parent_term = jQuery('#new_treatment_parent_term').val().trim(); + if (parent_term) { + jQuery('
    • ').text(parent_term).appendTo('#new_treatment_parents'); + jQuery('#new_treatment_remove_parent_btn').show(); + jQuery('#new_treatment_parent_term').val(''); + } + }); + + jQuery('#new_treatment_remove_parent_btn').on('click', function(e) { + e.preventDefault(); + jQuery('#new_treatment_parents li:last').remove(); + if (jQuery('#new_treatment_parents li').length === 0) { + jQuery('#new_treatment_remove_parent_btn').hide(); + } + }); + jQuery('#new_treatment_format_select').on('change', function () { let treatment_type = jQuery(this).val(); if (treatment_type === 'numeric') { diff --git a/t/unit_fixture/CXGN/Trial/TrialCreate.t b/t/unit_fixture/CXGN/Trial/TrialCreate.t index 4074553a39..5557e12e1f 100644 --- a/t/unit_fixture/CXGN/Trial/TrialCreate.t +++ b/t/unit_fixture/CXGN/Trial/TrialCreate.t @@ -603,7 +603,7 @@ ok(my $test_treatment = CXGN::Trait::Treatment->new({ format => 'numeric' }), 'create a test treatment'); -my $exp_treatment_root_term = 'Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000'; +my $exp_treatment_root_term = encode_json(['Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000']); ok($test_treatment->store($exp_treatment_root_term), 'store test treatment'); diff --git a/t/unit_fixture/CXGN/Uploading/TrialUpload.t b/t/unit_fixture/CXGN/Uploading/TrialUpload.t index 976ca38c1c..7801bd3585 100644 --- a/t/unit_fixture/CXGN/Uploading/TrialUpload.t +++ b/t/unit_fixture/CXGN/Uploading/TrialUpload.t @@ -1248,7 +1248,7 @@ for my $extension ("xls", "xlsx", "csv") { format => 'numeric' }), 'create a test treatment'); - my $exp_treatment_root_term = 'Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000'; + my $exp_treatment_root_term = encode_json(['Experimental treatment ontology|EXPERIMENT_TREATMENT:0000000']); ok(my $test_treatment_row = $test_treatment->store($exp_treatment_root_term), 'store test treatment');