12 use CXGN
::Page
::FormattingHelpers
qw( info_section_html
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",
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)",
46 il_bin
=> { column_name
=> 'IL Bin',
47 tooltip
=> "IL bin this BAC matches",
49 display
=> 'il_bin_name',
51 il_notes
=> { column_name
=> 'IL Notes',
52 tooltip
=> "Any special notes about this BAC's IL mapping results",
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};
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 ################################
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 );
93 vertical-align: middle;
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
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);
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
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 '';
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
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)
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> $page_size per page|;
172 <div id="instructions
">
173 <dl><dt>Instructions</dt>
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.
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.
189 print info_section_html
( title
=> 'Edit Clones',
191 qq|<div style
="text-align: center">$stat_string $pagination</div
>|
192 .columnar_table_html
( headings
=>
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"',
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 {
219 $q->page($query->page);
220 $q->page_size($query->page_size);
222 if ($q->same_bindvals_as($query)) {
230 [ '?'.$q->to_query_string, $t ]
233 return \
@qs, $selected;
239 my $q = $search->new_query;
240 $q->seq_project_name(q
(ilike
'%Tomato%'|| ?
'%'),'unmapped');
245 my $q = $search->new_query;
246 $q->seq_project_name(q
(ilike
'%Tomato%Chromosome ' || ?
|| ' %'),$chr);
251 my @il_searches = map {
252 my ($pid,$country) = @
$_;
253 my $q = $search->new_query;
254 $q->il_project_id('=?',$pid);
256 } @
{ CXGN
::People
::Project
->distinct_country_projects($dbh) };
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',
271 info_section_html
(title
=> 'Predefined Sets',
274 info_table_html
( __border
=> 0,
278 .info_section_html
(title
=> 'Custom Set',
280 contents
=> '<form method="GET">'.$query->to_html.'</form>',
283 #make the javascript for loading the overview image
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
298 #now print all the hidden edit forms
301 print editforms_html
($dbh,$person);
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);
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
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;
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';
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';
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';
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';
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),
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;
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;
446 //*** Editor class, used to work with the editors for each data item
449 this.myform = document[n+'_edit'];
451 //get the element for the appropriate edit form
452 Editor.prototype.form = function() {
455 //hide this editor form
456 Editor.prototype.hide = function(cell) {
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
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
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;
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;
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)) {
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);
534 get_editor(name).open(clone_id);
536 var hide_all_editors = function() {
537 foreach( values(editors),
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;
559 td.style.backgroundColor = onoff ? c : '';
562 if( is_not_editable(this) ) {
563 hilite(this,locked_color);
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),
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,
587 var name = th.childNodes[0].innerHTML;
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';
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) {
604 get_editor(edname).hide();
606 var row = get_clone_elements(clone_id);
609 postcontent.clone_id = clone_id;
612 { headers: [["Content-type","application/x-www-form-urlencoded"]],
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) {
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) {
640 // log('set_row_content callback');
642 data = MochiKit.Async.evalJSONRequest(req);
643 // log('setting fields...');
646 // log('setting '+field+' contents');
648 row[field].val.innerHTML = data[field].val;
649 row[field].disp.innerHTML = data[field].disp;
653 // log('unlocking clone '+row.clone_id);
661 var set_row_error = function(row) {
662 // log('error callback on clone_id '+row.clone_id);
663 foreach( values(row),
665 if(typeof(spans) != 'object' || ! spans.disp)
668 spans.disp.innerHTML = 'error';
671 // log('unlocking clone '+row.clone_id);
682 ############ SUBROUTINES ##############
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%';
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
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]
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" />';
720 my ($name,@choices) = @_;
722 ( qq|<form name
="${name}_edit" onsubmit
="return false" style
="display: inline">|,
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})" },
734 my ($name,$checked) = @_;
735 $checked = $checked ?
'checked="checked"' : '';
736 ( qq|<form name
="${name}_edit" onsubmit
="return false" style
="display: inline">|,
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/>|,
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">|,
750 qq|<input id
="text_$name" type
="text" value
="$urlencode{$text}" size
="$size" maxlength
="200" name
="val_input" />|,
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'),
774 our $metadata ||= CXGN
::Metadata
->new(); # metadata object
779 our $bac_status_log ||= CXGN
::People
::BACStatusLog
->new($dbh); # bac ... status ... object