Merge pull request #5269 from solgenomics/topic/sol_pan_genome
[sgn.git] / cgi-bin / maps / physical / clone_reg.pl
blobee197fb486ada4874c8085937b95434a6ff16045
1 use strict;
2 use warnings;
4 use Memoize;
6 use JSON;
8 use Tie::UrlEncoder;
9 our %urlencode;
11 use CXGN::Page;
12 use CXGN::Page::FormattingHelpers qw( info_section_html
13 modesel
14 columnar_table_html
15 simple_selectbox_html
16 info_table_html
17 tooltipped_text
18 truncate_string
20 use CXGN::Tools::List qw/distinct str_in evens/;
22 use CXGN::People::BACStatusLog;
23 use CXGN::Genomic::Search::Clone;
24 use CXGN::Search::CannedForms;
26 ########## DATA FIELD DEFINITIONS #######
27 my @data_fields_defs =
29 seq_proj => { column_name => 'Seq Chrom',
30 tooltip => 'The chromosome project (if any) that is sequencing the BAC',
32 seq_status => { tooltip => "The BAC's status in sequencing",
34 # gb_status => { column_name => 'GB Status',
35 # tooltip => "User-reported HTGS level of this BAC's sequence in GenBank, if any",
36 # },
37 il_proj => { column_name => 'IL Proj',
38 tooltip => "The sequencing project assigned to map this BAC to the Zamir IL bins",
39 value => 'il_proj_id',
40 display => 'il_proj_name',
42 il_chr => { column_name => 'IL Chr',
43 tooltip => "Chromosome this BAC matches (from IL mapping)",
44 value => 'il_chr',
46 il_bin => { column_name => 'IL Bin',
47 tooltip => "IL bin this BAC matches",
48 value => 'il_bin_id',
49 display => 'il_bin_name',
51 il_notes => { column_name => 'IL Notes',
52 tooltip => "Any special notes about this BAC's IL mapping results",
53 value => 'il_notes',
55 ver_int_read => { column_name => 'Ver IR',
56 tooltip => "Check this box if this BAC has been verified with an additional internal read",
58 ver_bac_end => { column_name => 'Ver BE',
59 tooltip => "Check this box if this BAC has been verified with an additional BAC end read",
62 ########## /DATA FIELD DEFINITIONS #######
63 ## data field def post-processing
64 #fills in defaults in the above data structure, so we don't have to
65 #write out every damn little thing
66 { my @it = @data_fields_defs;
67 while(my ($k,$v) = splice @it,0,2) {
68 $v->{column_name} ||= do {my @w = split /_/,$k; join ' ',map {ucfirst $_} @w};
69 $v->{value} ||= $k;
70 $v->{display} ||= $v->{value};
73 my %data_fields_defs = @data_fields_defs; #< make a hash of the same
74 #name for convenient access
77 ################################
78 ########## PAGE CODE START
79 ################################
81 #TODO:
82 # - do activated search term highlighting in search forms
84 #build some alternate HTML from the search results, adding edit
85 #controls if the user is logged in
87 my $dbh = CXGN::Genomic::Clone->db_Main;
89 my $page = CXGN::Page->new('BAC Registry Editor','Robert Buels');
90 $page->jsan_use(qw/MochiKit.Base MochiKit.Async MochiKit.Iter MochiKit.DOM MochiKit.Style MochiKit.Logging/);
91 $page->add_style( text => <<EOS );
92 td.columnar_table {
93 vertical-align: middle;
95 EOS
97 my ($person,$projects_json,@person_projects) = get_person($dbh);
99 $page->header(('BAC Registry Viewer/Editor') x 2);
102 #if logged in, add edit controls
103 sub get_person {
104 my $dbh = shift;
105 if(my $person_id = CXGN::Login->new($dbh)->has_session) {
106 my $p = CXGN::People::Person->new($dbh, $person_id);
108 return unless str_in($p->get_user_type,qw/sequencer curator/);
110 my @projects = #do {warn 'THIS IS BOGUS'; @{all_projects()}};
111 $p->get_projects_associated_with_person;
112 my $pjs = objToJson(\@projects);
114 return ($p,$pjs,@projects);
116 return;
121 #do a clone search for the BACs to edit here, using the Clone search
122 my $search = CXGN::Genomic::Search::Clone->new;
123 my $query = $search->new_query;
124 my %params = $page->get_all_encoded_arguments;
125 $query->from_request(\%params);
126 my $result = $search->do_search($query);
128 ####### now start the actual work
130 my @table_rows;
131 while(my $clone = $result->next_result) {
133 my $clone_data = $clone->reg_info_hashref;
135 my $clone_id = $clone->clone_id;
137 my $make_spans = sub {
138 my ($name,$data) = @_;
139 #make ID'd spans from a clone ID and some key-value pairs
140 #if there is more than one pair, the first one will be invisible
141 my $val = $data->{val} || '';
142 my $disp = $data->{disp} || $val;
143 $disp = '-' if $disp eq '';
144 return {
145 class => "clone_reg_edit edit_$name",
146 content => qq|<div style="position: relative"><span id="${name}_val_$clone_id" class="invisible">$val</span><span id="${name}_disp_$clone_id">$disp</span></div>|,
150 # fix up the il_notes field with truncation and a mouseover if it is
151 # too long
152 if( length(my $iln = $clone_data->{il_notes}->{disp}) > 15 ) {
153 $clone_data->{il_notes}->{disp} =
154 tooltipped_text(scalar(truncate_string($iln,8)),$iln)
157 push @table_rows,
159 qq|<span class="invisible">$clone_id</span><a href="/maps/physical/clone_info.pl?id=$clone_id">|.($clone->clone_name_with_chromosome || $clone->clone_name).'</a>',
160 map { $make_spans->($_ => $clone_data->{$_}) } evens @data_fields_defs
164 my $pagination = $search->pagination_buttons_html($query,$result);
165 my $page_size = $search->page_size_control_html($query);
166 my $stats = $result->time_html;
167 my $count = $result->total_results;
168 my $stat_string = qq|<b>BACs $stats</b>&nbsp;&nbsp;&nbsp;$page_size&nbsp;per&nbsp;page|;
170 if($person) {
171 print <<EOH;
172 <div id="instructions">
173 <dl><dt>Instructions</dt>
174 <dd>
175 To edit BAC registry information, select BACs to edit using
176 the controls at the bottom of the page, then page through and edit
177 the BACs by clicking the table cells below.
178 </dd>
179 </dl>
180 </div>
182 } else {
183 print <<EOH;
184 <div style="text-align: center; margin-bottom: 1em">
185 You must be <a href="/solpeople/login.pl">logged in</a> to edit BAC registry information.
186 </div>
189 print info_section_html( title => 'Edit Clones',
190 contents =>
191 qq|<div style="text-align: center">$stat_string $pagination</div>|
192 .columnar_table_html( headings =>
194 'Clone',
195 map {
196 qq|<span class="invisible">$_</span> |
197 .tooltipped_text($data_fields_defs{$_}{column_name},$data_fields_defs{$_}{tooltip})
198 } evens @data_fields_defs
201 data => \@table_rows,
202 __tableattrs => 'summary="" id="editingtable" cellspacing="0" align="center" style="margin-top: 1.2em; margin-bottom: 1.2em"',
203 __border => 1,
205 .qq|<div style="text-align: center; margin-bottom: 1em">$pagination</div>\n|
209 #searches through an array like [query obj,text],[query_obj,text]
210 #and returns an array like [[qstr,text],[qstr,text]], selected_index
211 #where the selected_index is the index in the array of the query
212 #that matches the current page query $query, or undef if none match
213 #this function is just for use in assembling the args to modesel() below
214 my $find_selected = sub {
215 my $selected;
216 my $index = 0;
217 my @qs = map {
218 my ($q,$t) = @$_;
219 $q->page($query->page);
220 $q->page_size($query->page_size);
222 if ($q->same_bindvals_as($query)) {
223 $selected = $index;
224 } else {
225 $q->page(0);
228 $index++;
230 [ '?'.$q->to_query_string, $t ]
231 } @_;
233 return \@qs, $selected;
236 my @seq_searches =
238 do {
239 my $q = $search->new_query;
240 $q->seq_project_name(q(ilike '%Tomato%'|| ? '%'),'unmapped');
241 [$q,'unmapped']
243 map {
244 my $chr = $_;
245 my $q = $search->new_query;
246 $q->seq_project_name(q(ilike '%Tomato%Chromosome ' || ? || ' %'),$chr);
247 [$q,$chr]
248 } 1..12
251 my @il_searches = map {
252 my ($pid,$country) = @$_;
253 my $q = $search->new_query;
254 $q->il_project_id('=?',$pid);
255 [$q,$country]
256 } @{ CXGN::People::Project->distinct_country_projects($dbh) };
258 my @search_sets =
259 ( 'BACs to be Sequenced on Chromosome:' =>
260 modesel( $find_selected->(@seq_searches) ),
261 'BACs to be IL Mapped by Project:' =>
262 modesel( $find_selected->(@il_searches) ),
265 print info_section_html( title => 'Sequencing Stats Overview',
266 contents => '<div id="stats_img_div" style="text-align: center">javascript required</div>'
269 print info_section_html( title => 'Select BACs',
270 contents =>
271 info_section_html(title => 'Predefined Sets',
272 is_subsection => 1,
273 contents =>
274 info_table_html( __border => 0,
275 @search_sets,
278 .info_section_html(title => 'Custom Set',
279 is_subsection => 1,
280 contents => '<form method="GET">'.$query->to_html.'</form>',
283 #make the javascript for loading the overview image
284 print <<EOJS;
285 <script language="JavaScript" type="text/javascript">
286 var update_status_image = function() {
287 var img_div = document.getElementById('stats_img_div');
288 img_div.innerHTML = '<img src="/documents/img/throbber.gif" /><br />updating...';
289 var xhr = MochiKit.Async.doSimpleXMLHttpRequest('clone_async.pl',{ action: 'project_stats_img_html' });
290 xhr.addCallbacks(function(req) { img_div.innerHTML = req.responseText },
291 function(req) { img_div.getElementById('stats_img_div').innerHTML = 'error fetching image' }
294 update_status_image(); //< update the status image on load
295 </script>
296 EOJS
298 #now print all the hidden edit forms
299 if($person) {
301 print editforms_html($dbh,$person);
303 print <<EOS;
304 <style>
305 td.clone_reg_edit {
306 height: 2.5em;
307 cursor: pointer;
309 </style>
312 #UI strategy:
313 # - mouseover highlight row, column, and intersection. highlight shows row locks
314 # - click and table cell turns into an edit control if the row is not locked
315 # - onchange, locks the table row and POSTs the change to clone_async.pl
316 # - on return of the POST, unlocks the row
318 #now here is all the JS to do the UI
320 #put the data field definitions in the javascript too
321 my $data_fields_defs_json = objToJson(\%data_fields_defs);
323 print <<EOJS;
324 <script language="JavaScript" type="text/javascript">
326 //import some useful stuff
327 var map = MochiKit.Base.map;
328 var partial = MochiKit.Base.partial;
329 var foreach = MochiKit.Iter.forEach;
330 var keys = MochiKit.Base.keys;
331 var values = MochiKit.Base.values;
332 var log = MochiKit.Logging.log;
334 //this is the color for mouseover-highlighted cells
335 var hilite_color = '#c5c5ee';
336 var hilite_color_overlap = '#b9b9e0';
337 var locked_color = '#f7878f';
339 //this is the table where our editing is done
340 var edit_table = document.getElementById('editingtable');
342 //functions for locking and unlocking rows
343 var locks = {};
344 var lock_clone = function(row) {
345 locks[row.clone_id] = 1;
346 foreach( row.tr.cells,
347 function(td) { td.style.backgroundColor = locked_color }
350 //create a little 'loading' throbber on the end of the row
351 var throbber = MochiKit.DOM.IMG({ src: '/documents/img/throbber.gif',
352 style: 'display: block; z-index: 3; position: absolute;'
354 var end_div = row.ver_bac_end.div;
355 var end_div_dims = MochiKit.Style.getElementDimensions(end_div);
356 MochiKit.Style.setElementPosition(throbber,{x: end_div_dims.w+10, y: 0});
357 MochiKit.DOM.appendChildNodes(end_div,throbber);
359 var unlock_clone = function(row) {
360 locks[row.clone_id] = 0;
361 foreach( row.tr.cells,
362 function(td) { td.style.backgroundColor = '' }
364 //get rid of the throbber
365 var end_div = row.ver_bac_end.div;
366 foreach( MochiKit.DOM.getElementsByTagAndClassName('img',null,end_div),
367 MochiKit.DOM.removeElement );
369 var is_locked = function(a) {
370 var clone_id = typeof(a) == 'object' ? get_cell_clone(a) : a;
371 return locks[clone_id] ? true : false;
374 //determines if a cell is not editable, due to its
375 //either being locked by an ongoing XHR, or because this
376 //user doesn't have permission to edit it
377 //also, the permissions are also checked server-side,
378 //so if you're reading this, don't get any funny ideas ;-)
379 var is_not_editable = function(cell,clone_id,fieldname) {
380 clone_id = clone_id || get_cell_clone(cell);
381 fieldname = fieldname || get_cell_field(cell);
383 //if the cell isn't locked, check some other permission conditions
384 var row = get_clone_elements(clone_id);
386 var seq_proj_in_projects_list = MochiKit.Base.findValue(person_projects,row.seq_proj.val.innerHTML.valueOf()) != -1;
387 var il_proj_in_projects_list = MochiKit.Base.findValue(person_projects,row.il_proj.val.innerHTML.valueOf()) != -1;
389 switch(fieldname) {
390 case 'seq_proj':
391 //proj must be null, or in the person's projects list
392 if(row.seq_proj.val.innerHTML != '' && !seq_proj_in_projects_list)
393 return 'this BAC is already being sequenced by another sequencing project, they must release their claim on this BAC before you can claim it';
394 break;
395 case 'seq_status':
396 case 'gb_status':
397 //seq_proj must be in the person's projects list
398 if(!seq_proj_in_projects_list)
399 return 'this BAC must be assigned to your sequencing project for you to edit its seq or GB status';
400 break;
401 case 'il_proj':
402 //il_proj must be null, or in the person's projects list
403 if(row.il_proj.val.innerHTML != '' && !il_proj_in_projects_list)
404 return 'this BAC is already being IL bin mapped by another sequencing project, they must release their claim on this BAC before you can claim it';
405 break;
406 case 'il_bin':
407 case 'il_notes':
408 case 'il_chr':
409 if(!il_proj_in_projects_list)
410 return 'this BAC must be assigned to your sequencing project before you can report IL mapping results for it';
411 break;
414 return;
417 var field_defs = $data_fields_defs_json;
418 var person_projects = $projects_json;
420 //get the span elements that hold and display data for the relevant clone,
421 //and their enclosing td's
422 var get_clone_elements = function(clone_id) {
423 var elements = { 'clone_id': clone_id };
424 foreach( keys(field_defs),
425 function( field ) {
426 elements[field] = { val: document.getElementById(field+'_val_'+clone_id),
427 disp: document.getElementById(field+'_disp_'+clone_id)
429 if(elements[field].disp) {
430 elements[field].div = elements[field].disp.parentNode;
431 elements[field].td = elements[field].div.parentNode;
432 elements.tr = elements[field].td.parentNode;
433 } else {
434 //grab the elements that the form is holding
435 var my_ed = get_editor(field);
436 if(my_ed.is_at_spot(clone_id)) {
437 elements[field] = my_ed.curr_spot;
442 return elements;
446 //*** Editor class, used to work with the editors for each data item
447 function Editor(n) {
448 this.name = n;
449 this.myform = document[n+'_edit'];
451 //get the element for the appropriate edit form
452 Editor.prototype.form = function() {
453 return this.myform;
455 //hide this editor form
456 Editor.prototype.hide = function(cell) {
457 if(this.curr_spot) {
458 var edform = this.form();
459 MochiKit.DOM.removeElement( edform );
460 document.getElementById('editor_corral').appendChild( edform );
462 //restore the data cells and the onclick handler in the cell we just vacated
463 this.curr_spot.td.appendChild(this.curr_spot.div);
465 //reinstall the table cell's onclick handler
466 this.curr_spot.td.onclick = td_onclick_edit
467 //release it
468 this.curr_spot = null;
471 //is this editor open in this spot?
472 Editor.prototype.is_at_spot = function(clone_id) {
473 return this.curr_spot && get_cell_clone(this.curr_spot.td) == clone_id;
475 //open the editor at a specific clone
476 Editor.prototype.open = function(clone_id) {
477 //hide any other open editors, including this one
478 hide_all_editors();
480 var row = get_clone_elements(clone_id);
481 this.curr_spot = row[this.name]; //< the two spans that hold this clone's data
483 //hide the data in the new cell
484 MochiKit.DOM.removeElement(this.curr_spot.div);
486 var edform = this.form();
488 //set the form's value
489 edform.clone_id.value = clone_id;
490 if(edform.val_input.type == 'checkbox') {
491 edform.val_input.checked = this.curr_spot.val.innerHTML == '1' ? true : false;
492 } else {
493 edform.val_input.value = this.curr_spot.val.innerHTML;
496 //swap the edit form into the td next to the display span
497 MochiKit.DOM.removeElement(edform);
498 this.curr_spot.td.appendChild(edform);
500 //set a checkbox's value again
501 if(edform.val_input.type == 'checkbox') {
502 edform.val_input.checked = this.curr_spot.val.innerHTML == '1' ? true : false;
505 //disable this table cell's onclick handler
506 this.curr_spot.td.onclick = null;
509 var editors = {};
510 //get the clone_id for any cell in the editing table
511 var get_cell_clone = function(td) {
512 if(td.cellclone) return td.cellclone;
513 return td.cellclone = td.parentNode.cells[0].childNodes[0].innerHTML;
515 var get_cell_field = function(td) {
516 return edit_table.rows[0].cells[td.cellIndex].childNodes[0].innerHTML;
519 //the onclick handler for an inactive table cell
520 var td_onclick_edit = function() {
521 //get the data field name from the invisible span in the head of this column
522 var name = get_cell_field(this);
523 var clone_id = get_cell_clone(this);
525 if(is_locked(clone_id)) {
526 return;
528 var not_editable_str = is_not_editable(this,clone_id,name);
529 if(not_editable_str) {
530 alert('cell not editable: '+not_editable_str);
531 return;
534 get_editor(name).open(clone_id);
536 var hide_all_editors = function() {
537 foreach( values(editors),
538 function(ed) {
539 ed.hide()
543 var get_editor = function(name) {
544 if(! editors[name]) {
545 editors[name] = new Editor(name);
547 return editors[name];
550 //this function turns row and column highlighting on and off,
551 //when given the cell that's under the mouse and whether the highlighting
552 //should be turned on or off
553 var table_highlight = function(onoff) {
554 var cell_coords = [this.parentNode.rowIndex,this.cellIndex];
555 var hilite = function(td,c) {
556 c = c || hilite_color;
557 if(is_locked(td))
558 return;
559 td.style.backgroundColor = onoff ? c : '';
562 if( is_not_editable(this) ) {
563 hilite(this,locked_color);
564 } else {
565 foreach( edit_table.rows[cell_coords[0]].cells, hilite );
566 //foreach( edit_table.rows, function(tr) { hilite(tr.cells[cell_coords[1]]) } );
567 hilite(this,hilite_color_overlap);
570 var table_highlight_on = partial(table_highlight,1);
571 var table_highlight_off = partial(table_highlight,0);
573 //find all the editing TD elements and install their event handlers
574 foreach( MochiKit.DOM.getElementsByTagAndClassName('td','clone_reg_edit',edit_table),
575 function(cell) {
576 cell.onclick = td_onclick_edit;
577 cell.onmouseover = table_highlight_on;
578 cell.onmouseout = table_highlight_off;
582 //set the width of all the TH heading elements to be wide enough to
583 //accomodate the editing forms in all the cells below
584 var set_col_widths = function() {
585 foreach( edit_table.rows[0].cells,
586 function(th) {
587 var name = th.childNodes[0].innerHTML;
588 if(! name) return;
589 var ed = get_editor(name);
590 var edform = ed.form();
591 var min_col_width = edform.offsetWidth + 3;
592 // log('for '+name+', th width is ' + th.offsetWidth + ' and min is '+min_col_width);
593 th.style.width = min_col_width+'px';
597 set_col_widths();
600 //do a POST XHR to alter the clone in the database,
601 //also update its display fields
602 var alter_clone = function(edname,clone_id,postcontent) {
603 //hide this editor
604 get_editor(edname).hide();
606 var row = get_clone_elements(clone_id);
607 lock_clone(row);
609 postcontent.clone_id = clone_id;
611 var xhr_opt =
612 { headers: [["Content-type","application/x-www-form-urlencoded"]],
613 method: 'POST',
614 sendContent: MochiKit.Base.queryString(postcontent)
617 var success_callback = partial(set_row_content,row);
618 var err_callback = partial(set_row_error,row);
620 //if the action involves the seq status, schedule the overview image to get updated
621 //when this comes back
622 if( postcontent.action == 'set_seq_proj'
623 || postcontent.action == 'set_gb_status'
624 || postcontent.action == 'set_seq_status'
626 var old_success = success_callback;
627 success_callback = function(req) {
628 old_success(req);
629 update_status_image();
633 var res = MochiKit.Async.doXHR('clone_async.pl',xhr_opt);
634 res.addCallbacks(success_callback, err_callback);
638 var set_row_content = function(row,req) {
639 var data;
640 // log('set_row_content callback');
641 try {
642 data = MochiKit.Async.evalJSONRequest(req);
643 // log('setting fields...');
644 foreach( keys(data),
645 function(field) {
646 // log('setting '+field+' contents');
647 if( row[field] ) {
648 row[field].val.innerHTML = data[field].val;
649 row[field].disp.innerHTML = data[field].disp;
653 // log('unlocking clone '+row.clone_id);
654 unlock_clone(row);
655 } catch(e) {
656 log(e);
657 set_row_error(row);
661 var set_row_error = function(row) {
662 // log('error callback on clone_id '+row.clone_id);
663 foreach( values(row),
664 function(spans) {
665 if(typeof(spans) != 'object' || ! spans.disp)
666 return;
668 spans.disp.innerHTML = 'error';
671 // log('unlocking clone '+row.clone_id);
673 unlock_clone(row);
676 </script>
677 EOJS
680 $page->footer;
682 ############ SUBROUTINES ##############
684 sub il_bin_list {
685 return @{our $il_list ||= shift->selectall_arrayref(<<EOQ)}
686 select genotype_region_id, name
687 from phenome.genotype_region gr
688 join sgn.linkage_group lg using(lg_id)
689 join sgn.map_version mv using(map_version_id)
690 join sgn.map m using(map_id)
691 where type = 'bin' and m.short_name like '%1992%' and m.short_name like '%EXPEN%';
695 sub editforms_html {
696 my ($dbh,$person) = @_;
698 return '' unless $person;
700 my @person_projects = #do {warn 'THIS IS BOGUS'; @{all_projects()}};
701 $person->get_projects_associated_with_person;
703 #lookup the name for each of the projects and parse out the chromosome numbers
704 @person_projects =
705 map {
706 my $proj_id = $_;
707 my ($chr_name) = $dbh->selectrow_array('select name from sgn_people.sp_project where sp_project_id = ? order by name',undef,$proj_id);
708 $chr_name =~ s/\D//g; #remove all non-digits
709 #and if nothing's left then it must be the unmapped one
710 $chr_name ||= 'unmapped';
711 [$proj_id, $chr_name]
712 } @person_projects;
714 my @ils = il_bin_list($dbh);
716 #the values of all these form fields will be set by javascript each
717 #time this form is popped up at a given location
718 my $clone_id_hidden = '<input name="clone_id" value="" type="hidden" />';
719 my $sel_edit = sub {
720 my ($name,@choices) = @_;
722 ( qq|<form name="${name}_edit" onsubmit="return false" style="display: inline">|,
724 $clone_id_hidden,
725 simple_selectbox_html(name => "val_input",
726 choices => \@choices,
727 params => { onchange => "alter_clone('$name',this.form.clone_id.value,{ action: 'set_$name', val: this.value})" },
730 '</form>',
733 my $cb_edit = sub {
734 my ($name,$checked) = @_;
735 $checked = $checked ? 'checked="checked"' : '';
736 ( qq|<form name="${name}_edit" onsubmit="return false" style="display: inline">|,
738 $clone_id_hidden,
739 qq|<input id="cb_$name" type="checkbox" name="val_input" onclick="alter_clone('$name',this.form.clone_id.value,{ action: 'set_$name', val: this.checked ? 1 : 0})"$checked/>|,
741 '</form>',
744 my $t_edit = sub {
745 no warnings 'uninitialized';
746 my ($name,$size,$text) = @_;
747 ( qq|<form name="${name}_edit" onsubmit="alter_clone('$name',this.clone_id.value,{action: 'set_$name', val: this.val_input.value}); return false" style="display: inline">|,
749 $clone_id_hidden,
750 qq|<input id="text_$name" type="text" value="$urlencode{$text}" size="$size" maxlength="200" name="val_input" />|,
752 '</form>',
755 return join '', map {my $s = $_; chomp $s; "$s\n"}
757 '<div id="editor_corral" style="position: absolute; left: -800px;">',
759 $sel_edit->('seq_proj',['','none'],@person_projects),
760 $sel_edit->('seq_status','none','not_sequenced','in_progress','complete'),
761 $sel_edit->('gb_status','none',map {"htgs$_"} 1..3),
762 $sel_edit->('il_proj',['','none'],grep {my $id = $_->[0]; str_in($id,map $_->[0],@person_projects)} @{CXGN::People::Project->distinct_country_projects($dbh)}),
763 $sel_edit->('il_chr',['','none'],1..12),
764 $sel_edit->('il_bin',['','none'],@ils),
765 $t_edit ->('il_notes',15),
766 $cb_edit ->('ver_int_read'),
767 $cb_edit ->('ver_bac_end'),
769 '</div>',
773 sub metadata {
774 our $metadata ||= CXGN::Metadata->new(); # metadata object
777 sub bac_status_log {
778 my $dbh = shift;
779 our $bac_status_log ||= CXGN::People::BACStatusLog->new($dbh); # bac ... status ... object