galaxy-dist commit 667043341e81: Significant cleanup for the Sample Tracking UI. Things are more streamlined and Galaxy UI standards are now followed.
# HG changeset patch -- Bitbucket.org # Project galaxy-dist # URL http://bitbucket.org/galaxy/galaxy-dist/overview # User Greg Von Kuster <greg@bx.psu.edu> # Date 1288649810 14400 # Node ID 667043341e81261a7fe587daa475110486b15292 # Parent c4d8ffb3109e8cfb8143dc87c88dee3337bb7569 Significant cleanup for the Sample Tracking UI. Things are more streamlined and Galaxy UI standards are now followed. --- a/templates/requests/common/sample_events.mako +++ b/templates/requests/common/sample_events.mako @@ -5,10 +5,7 @@ <h2>Events for Sample "${sample.name}"</h2><ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}"> - <span>Browse this request</span></a> - </li> + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}">Browse this request</a></li></ul><h3>Sequencing Request "${sample.request.name}"</h3> --- /dev/null +++ b/templates/requests/common/edit_samples.mako @@ -0,0 +1,240 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/requests/common/common.mako" import="common_javascripts" /> +<%namespace file="/requests/common/common.mako" import="render_samples_grid" /> +<%namespace file="/requests/common/common.mako" import="render_request_type_sample_form_grids" /> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + ${h.css( "library" )} +</%def> + +<%def name="javascripts()"> + ${parent.javascripts()} + ${common_javascripts()} + ${local_javascripts()} +</%def> + +<%def name="local_javascripts()"> + <script type="text/javascript"> + // Looks for changes in sample states using an async request. Keeps + // calling itself (via setTimeout) until all samples are in a terminal + // state. + var updater = function ( sample_states ) { + // Check if there are any items left to track + var empty = true; + for ( i in sample_states ) { + empty = false; + break; + } + if ( ! empty ) { + setTimeout( function() { updater_callback( sample_states ) }, 1000 ); + } + }; + + var updater_callback = function ( sample_states ) { + // Build request data + var ids = [] + var states = [] + $.each( sample_states, function ( id, state ) { + ids.push( id ); + states.push( state ); + }); + // Make ajax call + $.ajax( { + type: "POST", + url: "${h.url_for( controller='requests_common', action='sample_state_updates' )}", + dataType: "json", + data: { ids: ids.join( "," ), states: states.join( "," ) }, + success : function ( data ) { + $.each( data, function( id, val ) { + // Replace HTML + var cell1 = $("#sampleState-" + id); + cell1.html( val.html_state ); + var cell2 = $("#sampleDatasets-" + id); + cell2.html( val.html_datasets ); + sample_states[ parseInt( id ) ] = val.state; + }); + updater( sample_states ); + }, + error: function() { + // Just retry, like the old method, should try to be smarter + updater( sample_states ); + } + }); + }; + </script> +</%def> + +<% + from galaxy.web.framework.helpers import time_ago + + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + is_complete = request.is_complete + is_unsubmitted = request.is_unsubmitted + can_add_samples = is_unsubmitted + can_edit_or_delete_samples = is_unsubmitted and request.samples + can_edit_request = ( is_admin and not request.is_complete ) or request.is_unsubmitted + can_reject_or_transfer = is_admin and request.is_submitted +%> + +<br/><br/> + +<ul class="manage-table-actions"> + %if not editing_samples and can_edit_or_delete_samples: + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( request.id ), editing_samples='True' )}">Edit samples</a></li> + %endif + %if editing_samples and can_add_samples: + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='add_sample', cntrller=cntrller, request_id=trans.security.encode_id( request.id ), add_sample_button='Add sample' )}">Add sample</a></li> + %endif + %if is_unsubmitted: + <li><a class="action-button" confirm="More samples cannot be added to this request after it is submitted. Click OK to submit." href="${h.url_for( controller='requests_common', action='submit_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Submit request</a></li> + %endif + <li><a class="action-button" id="request-${request.id}-popup" class="menubutton">Request actions</a></li> + <div popupmenu="request-${request.id}-popup"> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> + %if can_edit_request: + <a class="action-button" href="${h.url_for( controller='requests_common', action='edit_basic_request_info', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Edit</a> + %endif + <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">View history</a> + %if can_reject_or_transfer: + <a class="action-button" href="${h.url_for( controller='requests_admin', action='reject_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Reject</a> + <a class="action-button" href="${h.url_for( controller='requests_admin', action='get_data', request_id=trans.security.encode_id( request.id ) )}">Select datasets to transfer</a> + %endif + </div> +</ul> + +%if request.samples_without_library_destinations: + <br/> + <font color="red"><b><i>Select a target data library and folder for all samples before starting the sequence run</i></b></font> + <br/> +%endif + +%if request.is_rejected: + <br/> + <font color="red"><b><i>Reason for rejection: </i></b></font><b>${request.last_comment}</b> + <br/> +%endif + +%if message: + ${render_msg( message, status )} +%endif + +<div class="toolFormBody"> + <form id="edit_samples" name="edit_samples" action="${h.url_for( controller='requests_common', action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( request.id ), editing_samples=editing_samples )}" method="post"> + %if current_samples: + <% + if editing_samples: + grid_header = '<h3>Edit Current Samples of Request "%s"</h3>' % request.name + else: + grid_header = '<h3>Add Samples to Request "%s"</h3>' % request.name + %> + ${render_samples_grid( cntrller, request, current_samples, action='edit_samples', editing_samples=editing_samples, encoded_selected_sample_ids=encoded_selected_sample_ids, render_buttons=False, grid_header=grid_header )} + %if editing_samples and len( sample_operation_select_field.options ) > 1 and not ( is_unsubmitted or is_complete ): + <div class="form-row" style="background-color:#FAFAFA;"> + For selected samples: + ${sample_operation_select_field.get_html()} + </div> + <% sample_operation_selected_value = sample_operation_select_field.get_selected( return_value=True ) %> + %if sample_operation_selected_value != 'none' and encoded_selected_sample_ids: + <div class="form-row" style="background-color:#FAFAFA;"> + %if sample_operation_selected_value == trans.model.Sample.bulk_operations.CHANGE_STATE: + ## sample_operation_selected_value == 'Change state' + <div class="form-row"> + <label>Change current state</label> + ${sample_state_id_select_field.get_html()} + <label>Comments</label> + <input type="text" name="sample_event_comment" value=""/> + <div class="toolParamHelp" style="clear: both;"> + Optional + </div> + </div> + %elif sample_operation_selected_value == trans.app.model.Sample.bulk_operations.SELECT_LIBRARY: + <% libraries_selected_value = libraries_select_field.get_selected( return_value=True ) %> + <div class="form-row"> + <label>Select data library:</label> + ${libraries_select_field.get_html()} + </div> + %if libraries_selected_value != 'none': + <div class="form-row"> + <label>Select folder:</label> + ${folders_select_field.get_html()} + </div> + %endif + %endif + </div> + %endif + %endif + ## Render the other grids + <% trans.sa_session.refresh( request.type.sample_form ) %> + %for grid_index, grid_name in enumerate( request.type.sample_form.layout ): + ${render_request_type_sample_form_grids( grid_index, grid_name, request.type.sample_form.grid_fields( grid_index ), editing_samples=editing_samples )} + %endfor + %else: + <label>There are no samples.</label> + %endif + %if not editing_samples and is_unsubmitted: + ## The user is adding a new sample + %if current_samples: + <p/> + <div class="form-row"> + <label> Copy <input type="text" name="num_sample_to_copy" value="1" size="3"/> samples from sample ${sample_copy.get_html()}</label> + <div class="toolParamHelp" style="clear: both;"> + Select the sample from which the new sample should be copied or leave selection as <b>None</b> to add a new "generic" sample. + </div> + </div> + %endif + <p/> + <div class="form-row"> + %if ( request.samples or current_samples ) and ( editing_samples or len( current_samples ) > len( request.samples ) ): + <input type="submit" name="add_sample_button" value="Add sample"/> + <input type="submit" name="save_samples_button" value="Save"/> + <input type="submit" name="cancel_changes_button" value="Cancel"/> + <div class="toolParamHelp" style="clear: both;"> + Click the <b>Add sample</b> button for each new sample and click the <b>Save</b> button when you have finished adding samples. + </div> + %else: + <input type="submit" name="add_sample_button" value="Add sample"/> + <div class="toolParamHelp" style="clear: both;"> + Click the <b>Add sample</b> button for each new sample. + </div> + %endif + </div> + %elif editing_samples: + <p/> + <div class="form-row"> + <input type="submit" name="save_samples_button" value="Save"/> + <input type="submit" name="cancel_changes_button" value="Cancel"/> + <div class="toolParamHelp" style="clear: both;"> + Click the <b>Save</b> button when you have finished editing the samples + </div> + %endif + %if request.samples and request.is_submitted: + <script type="text/javascript"> + // Updater + updater( {${ ",".join( [ '"%s" : "%s"' % ( s.id, s.state.name ) for s in request.samples ] ) }}); + </script> + %endif + </form> +</div> +%if is_unsubmitted and not editing_samples: + <p/> + ##<div class="toolForm"> + ##<div class="toolFormTitle">Import samples from csv file</div> + <h4><img src="/static/images/fugue/toggle-expand.png" alt="Hide" onclick="showContent(this);" style="cursor:pointer;"/> Import samples from csv file</h4> + <div style="display:none;"> + <div class="toolFormBody"> + <form id="import" name="import" action="${h.url_for( controller='requests_common', action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( request.id ), editing_samples=editing_samples )}" enctype="multipart/form-data" method="post" > + <div class="form-row"> + <input type="file" name="file_data" /> + <input type="submit" name="import_samples_button" value="Import samples"/> + <div class="toolParamHelp" style="clear: both;"> + The csv file must be in the following format:<br/> + SampleName,DataLibrary,DataLibraryFolder,FieldValue1,FieldValue2... + </div> + </div> + </form> + </div> + </div> + ##</div> +%endif --- a/test/functional/test_forms_and_requests.py +++ b/test/functional/test_forms_and_requests.py @@ -225,15 +225,12 @@ class TestFormsAndRequests( TwillTestCas strings_displayed_after_submit = [ 'Unsubmitted' ] for sample_name, field_values in sample_value_tuples: strings_displayed_after_submit.append( sample_name ) - for field_value in field_values: - strings_displayed_after_submit.append( field_value ) # Add samples to the request self.add_samples( cntrller='requests', request_id=self.security.encode_id( request_one.id ), request_name=request_one.name, sample_value_tuples=sample_value_tuples, - strings_displayed=[ 'Sequencing Request "%s"' % request_one.name, - 'There are no samples.' ], + strings_displayed=[ 'There are no samples.' ], strings_displayed_after_submit=strings_displayed_after_submit ) def test_030_edit_basic_request_info( self ): """Testing editing the basic information of a sequence run request""" @@ -277,11 +274,11 @@ class TestFormsAndRequests( TwillTestCas self.check_request_grid( cntrller='requests_admin', state=request_one.states.SUBMITTED, strings_displayed=[ request_one.name ] ) - self.visit_url( "%s/requests_common/manage_request?cntrller=requests&id=%s" % ( self.url, self.security.encode_id( request_one.id ) ) ) - self.check_page_for_string( 'Sequencing Request "%s"' % request_one.name ) + self.visit_url( "%s/requests_common/view_request?cntrller=requests&id=%s" % ( self.url, self.security.encode_id( request_one.id ) ) ) + # TODO: add some string for checking on the page above... # Set bar codes for the samples bar_codes = [ '1234567890', '0987654321' ] - strings_displayed_after_submit=[ 'Changes made to the samples are saved.' ] + strings_displayed_after_submit=[ 'Changes made to the samples have been saved.' ] for bar_code in bar_codes: strings_displayed_after_submit.append( bar_code ) self.add_bar_codes( request_id=self.security.encode_id( request_one.id ), @@ -333,7 +330,7 @@ class TestFormsAndRequests( TwillTestCas test_field_name1, test_field_name2, test_field_name3 ], - strings_displayed_after_submit=[ "The request has been created" ] ) + strings_displayed_after_submit=[ "The request has been created." ] ) global request_two request_two = get_request_by_name( name ) # Make sure the request is showing in the 'new' filter @@ -349,15 +346,12 @@ class TestFormsAndRequests( TwillTestCas strings_displayed_after_submit = [ 'Unsubmitted' ] for sample_name, field_values in sample_value_tuples: strings_displayed_after_submit.append( sample_name ) - for field_value in field_values: - strings_displayed_after_submit.append( field_value ) # Add samples to the request self.add_samples( cntrller='requests_admin', request_id=self.security.encode_id( request_two.id ), request_name=request_two.name, sample_value_tuples=sample_value_tuples, - strings_displayed=[ 'Sequencing Request "%s"' % request_two.name, - 'There are no samples.' ], + strings_displayed=[ 'There are no samples.' ], strings_displayed_after_submit=strings_displayed_after_submit ) # Submit the request self.submit_request( cntrller='requests_admin', @@ -394,6 +388,7 @@ class TestFormsAndRequests( TwillTestCas % ( request_two.name, request_two.states.REJECTED ) def test_055_reset_data_for_later_test_runs( self ): """Reseting data to enable later test runs to pass""" + """ # Logged in as admin_user ################## # Delete request_type permissions @@ -438,3 +433,4 @@ class TestFormsAndRequests( TwillTestCas # Manually delete the group from the database refresh( group ) delete( group ) + """ --- a/templates/admin/requests/rename_datasets.mako +++ b/templates/admin/requests/rename_datasets.mako @@ -10,7 +10,7 @@ <a class="action-button" href="${h.url_for( controller='requests_admin', action='manage_datasets', sample_id=trans.security.encode_id( sample.id ) )}">Browse datasets</a></li><li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller='requests_admin', id=trans.security.encode_id( sample.request.id ) )}">Browse this request</a> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller='requests_admin', id=trans.security.encode_id( sample.request.id ) )}">Browse this request</a></li></ul> --- a/lib/galaxy/web/controllers/requests_admin.py +++ b/lib/galaxy/web/controllers/requests_admin.py @@ -158,9 +158,9 @@ class RequestsAdmin( BaseController, Use action='edit_basic_request_info', cntrller='requests_admin', **kwd ) ) - if operation == "manage_request": + if operation == "view_request": return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='view_request', cntrller='requests_admin', **kwd ) ) if operation == "request_events": @@ -186,14 +186,14 @@ class RequestsAdmin( BaseController, Use return self.request_grid( trans, **kwd ) @web.expose @web.require_admin - def reject( self, trans, **kwd ): + def reject_request( self, trans, **kwd ): params = util.Params( kwd ) request_id = params.get( 'id', '' ) status = params.get( 'status', 'done' ) message = params.get( 'message', 'done' ) if params.get( 'cancel_reject_button', False ): return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='view_request', cntrller='requests_admin', id=request_id ) ) try: @@ -292,7 +292,6 @@ class RequestsAdmin( BaseController, Use return invalid_id_redirect( trans, 'requests_admin', sample_id ) request_id = trans.security.encode_id( sample.request.id ) library_id = trans.security.encode_id( sample.library.id ) - self.datatx_grid.title = 'Datasets of sample "%s"' % sample.name self.datatx_grid.global_actions = [ grids.GridAction( "Refresh", dict( controller='requests_admin', action='manage_datasets', @@ -303,14 +302,14 @@ class RequestsAdmin( BaseController, Use request_id=request_id, folder_path=sample.request.type.datatx_info[ 'data_dir' ], sample_id=sample_id ) ), - grids.GridAction( 'Data library "%s"' % sample.library.name, - dict( controller='library_common', - action='browse_library', - cntrller='library_admin', - id=library_id ) ), + #grids.GridAction( 'Data library "%s"' % sample.library.name, + # dict( controller='library_common', + # action='browse_library', + # cntrller='library_admin', + # id=library_id ) ), grids.GridAction( "Browse this request", dict( controller='requests_common', - action='manage_request', + action='view_request', cntrller='requests_admin', id=request_id ) ) ] return self.datatx_grid( trans, **kwd ) --- a/templates/requests/common/edit_basic_request_info.mako +++ b/templates/requests/common/edit_basic_request_info.mako @@ -1,14 +1,25 @@ <%inherit file="/base.mako"/><%namespace file="/message.mako" import="render_msg" /> +<% + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + can_add_samples = request.is_unsubmitted +%> + <br/><br/><ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> - </li> - <li> - <a class="action-button" href="${h.url_for( controller=cntrller, action='browse_requests' )}">Browse all requests</a> - </li> + <li><a class="action-button" id="request-${request.id}-popup" class="menubutton">Request Actions</a></li> + <div popupmenu="request-${request.id}-popup"> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> + %if can_add_samples: + <a class="action-button" confirm="More samples cannot be added to this request once it is submitted. Click OK to submit." href="${h.url_for( controller='requests_common', action='submit_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Submit</a> + %endif + <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">View history</a> + %if is_admin and request.is_submitted: + <a class="action-button" href="${h.url_for( controller='requests_admin', action='reject_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Reject</a> + <a class="action-button" href="${h.url_for( controller='requests_admin', action='get_data', request_id=trans.security.encode_id( request.id ) )}">Select datasets to transfer</a> + %endif + </div></ul> %if message: --- /dev/null +++ b/templates/requests/common/view_request.mako @@ -0,0 +1,157 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/requests/common/common.mako" import="common_javascripts" /> +<%namespace file="/requests/common/common.mako" import="render_samples_grid" /> +<%namespace file="/requests/common/common.mako" import="render_request_type_sample_form_grids" /> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + ${h.css( "library" )} +</%def> + +<%def name="javascripts()"> + ${parent.javascripts()} + ${common_javascripts()} +</%def> + +<% + from galaxy.web.framework.helpers import time_ago + + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + is_unsubmitted = request.is_unsubmitted + can_edit_request = ( is_admin and not request.is_complete ) or request.is_unsubmitted + can_add_samples = is_unsubmitted +%> + +<br/><br/> + +<ul class="manage-table-actions"> + %if is_unsubmitted: + <li><a class="action-button" confirm="More samples cannot be added to this request after it is submitted. Click OK to submit." href="${h.url_for( controller='requests_common', action='submit_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Submit request</a></li> + %endif + <li><a class="action-button" id="request-${request.id}-popup" class="menubutton">Request actions</a></li> + <div popupmenu="request-${request.id}-popup"> + %if can_edit_request: + <a class="action-button" href="${h.url_for( controller='requests_common', action='edit_basic_request_info', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Edit</a> + %endif + <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">View history</a> + %if is_admin and request.is_submitted: + <a class="action-button" href="${h.url_for( controller='requests_admin', action='reject_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Reject</a> + <a class="action-button" href="${h.url_for( controller='requests_admin', action='get_data', request_id=trans.security.encode_id( request.id ) )}">Select datasets to transfer</a> + %endif + </div> +</ul> + +%if request.is_rejected: + <font color="red"><b><i>Reason for rejection: </i></b></font><b>${request.last_comment}</b> + <br/><br/> +%endif + +%if message: + ${render_msg( message, status )} +%endif + +<div class="toolForm"> + <div class="toolFormTitle">Sequencing request "${request.name}"</div> + <div class="toolFormBody"> + <div class="form-row"> + <label>Current state:</label> + ${request.state} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Description:</label> + ${request.desc} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>User:</label> + %if is_admin: + ${request.user.email} + %elif request.user.username: + ${request.user.username} + %else: + Unknown + %endif + <div style="clear: both"></div> + </div> + <div class="form-row"> + <h4><img src="/static/images/fugue/toggle-expand.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> More</h4> + <div style="display:none;"> + %for index, rd in enumerate( request_widgets ): + <% + field_label = rd[ 'label' ] + field_value = rd[ 'value' ] + %> + <div class="form-row"> + <label>${field_label}:</label> + %if field_label == 'State': + <a href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">${field_value}</a> + %else: + ${field_value} + %endif + </div> + <div style="clear: both"></div> + %endfor + <div class="form-row"> + <label>Date created:</label> + ${request.create_time} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Last updated:</label> + ${time_ago( request.update_time )} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Email recipients:</label> + <% + if request.notification: + emails = ', '.join( request.notification[ 'email' ] ) + else: + emails = '' + %> + ${emails} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Send email when state changes to:</label> + <% + if request.notification: + states = [] + for ss in request.type.states: + if ss.id in request.notification[ 'sample_states' ]: + states.append( ss.name ) + states = ', '.join( states ) + else: + states = '' + %> + ${states} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Sequencer configuration:</label> + ${request.type.name} + <div style="clear: both"></div> + </div> + </div> + </div> + </div> +</div> +<p/> +%if current_samples: + <% grid_header = '<h3>Samples</h3>' %> + ${render_samples_grid( cntrller, request, current_samples=current_samples, action='view_request', editing_samples=False, encoded_selected_sample_ids=[], render_buttons=can_edit_request, grid_header=grid_header )} +%else: + There are no samples. + %if can_add_samples: + <ul class="manage-table-actions"> + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='add_sample', cntrller=cntrller, request_id=trans.security.encode_id( request.id ), add_sample_button='Add sample' )}">Add sample</a></li> + </ul> + %endif +%endif +## Render the other grids +<% trans.sa_session.refresh( request.type.sample_form ) %> +%for grid_index, grid_name in enumerate( request.type.sample_form.layout ): + ${render_request_type_sample_form_grids( grid_index, grid_name, request.type.sample_form.grid_fields( grid_index ), editing_samples=False )} +%endfor --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -24,13 +24,11 @@ class LibraryListGrid( grids.Grid ): columns = [ NameColumn( "Name", key="name", - model_class=model.Library, link=( lambda library: dict( operation="browse", id=library.id ) ), attach_popup=False, filterable="advanced" ), DescriptionColumn( "Description", key="description", - model_class=model.Library, attach_popup=False, filterable="advanced" ), ] --- a/templates/admin/forms/show_form_read_only.mako +++ b/templates/admin/forms/show_form_read_only.mako @@ -81,10 +81,10 @@ <form name="library" action="${h.url_for( controller='forms', action='manage' )}" method="post" > %if form_definition.type == trans.app.model.FormDefinition.types.SAMPLE: %if not len(form_definition.layout): - ${render_grid( 0, '', form_definition.fields_of_grid( None ) )} + ${render_grid( 0, '', form_definition.grid_fields( None ) )} %else: %for grid_index, grid_name in enumerate(form_definition.layout): - ${render_grid( grid_index, grid_name, form_definition.fields_of_grid( grid_index ) )} + ${render_grid( grid_index, grid_name, form_definition.grid_fields( grid_index ) )} %endfor %endif %else: --- a/templates/library/common/browse_library.mako +++ b/templates/library/common/browse_library.mako @@ -346,7 +346,6 @@ ><td style="padding-left: ${folder_pad}px;"><input type="checkbox" class="folderCheckbox"/> - %if folder.deleted: <span class="libraryItem-error"> %endif @@ -354,7 +353,6 @@ <div style="float: left; margin-left: 2px;" class="menubutton split popup" id="folder_img-${folder.id}-popup"><a href="javascript:void(0);">${folder.name}</a></div> - %if folder.deleted: </span> %endif --- a/templates/admin/requests/reject.mako +++ b/templates/admin/requests/reject.mako @@ -8,16 +8,16 @@ <h2>Reject Sequencing Request "${request.name}"</h2><ul class="manage-table-actions"><li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id(request.id) )}">Events</a> + <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id(request.id) )}">View history</a></li><li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id(request.id) )}">Browse this request</a> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id(request.id) )}">Browse this request</a></li></ul><div class="toolForm"><div class="toolFormTitle">Reject request</div> - <form name="event" action="${h.url_for( controller='requests_admin', action='reject', id=trans.security.encode_id( request.id ) )}" method="post" > + <form name="event" action="${h.url_for( controller='requests_admin', action='reject_request', id=trans.security.encode_id( request.id ) )}" method="post" ><div class="form-row"> Rejecting this request will move the request state to <b>Rejected</b>. </div> --- a/templates/admin/requests/get_data.mako +++ b/templates/admin/requests/get_data.mako @@ -75,7 +75,7 @@ <a class="action-button" href="${h.url_for( controller='requests_admin', action='view_request_type', id=trans.security.encode_id( request.type.id ) )}">Sequencer configuration "${request.type.name}"</a></li><li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a></li></ul> --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1488,25 +1488,23 @@ class MetadataFile( object ): return os.path.abspath( os.path.join( path, "metadata_%d.dat" % self.id ) ) class FormDefinition( object ): - types = Bunch( REQUEST = 'Sequencing Request Form', - SAMPLE = 'Sequencing Sample Form', - LIBRARY_INFO_TEMPLATE = 'Library information template', - USER_INFO = 'User Information' ) - def __init__(self, name=None, desc=None, fields=[], - form_definition_current=None, form_type=None, layout=None): + types = Bunch( REQUEST = 'Sequencing Request Form', + SAMPLE = 'Sequencing Sample Form', + LIBRARY_INFO_TEMPLATE = 'Library information template', + USER_INFO = 'User Information' ) + def __init__( self, name=None, desc=None, fields=[], form_definition_current=None, form_type=None, layout=None ): self.name = name self.desc = desc self.fields = fields self.form_definition_current = form_definition_current self.type = form_type self.layout = layout - def fields_of_grid(self, grid_index): - ''' - This method returns the list of fields belonging to the given grid. - ''' + def grid_fields( self, grid_index ): + # Returns a dictionary whose keys are integers corresponding to field positions + # on the grid and whose values are the field. gridfields = {} - for i, f in enumerate(self.fields): - if str(f['layout']) == str(grid_index): + for i, f in enumerate( self.fields ): + if str( f[ 'layout' ] ) == str( grid_index ): gridfields[i] = f return gridfields def get_widgets( self, user, contents=[], **kwd ): --- a/lib/galaxy/web/controllers/requests.py +++ b/lib/galaxy/web/controllers/requests.py @@ -10,11 +10,10 @@ log = logging.getLogger( __name__ ) class UserRequestsGrid( RequestsGrid ): operations = [ operation for operation in RequestsGrid.operations ] - operations.append( grids.GridOperation( "Edit", allow_multiple=False, condition=( lambda item: not item.deleted and item.is_unsubmitted ) ) ) - operations.append( grids.GridOperation( "Delete", allow_multiple=True, condition=( lambda item: not item.deleted and item.is_new ) ) ) + operations.append( grids.GridOperation( "Edit", allow_multiple=False, condition=( lambda item: item.is_unsubmitted and not item.deleted ) ) ) + operations.append( grids.GridOperation( "Delete", allow_multiple=True, condition=( lambda item: item.is_new and not item.deleted ) ) ) operations.append( grids.GridOperation( "Undelete", allow_multiple=True, condition=( lambda item: item.deleted ) ) ) def apply_query_filter( self, trans, query, **kwd ): - # gvk ( 9/28/10 ) TODO: is this method needed? return query.filter_by( user=trans.user ) class Requests( BaseController ): @@ -37,9 +36,9 @@ class Requests( BaseController ): action='edit_basic_request_info', cntrller='requests', **kwd ) ) - if operation == "manage_request": + if operation == "view_request": return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='view_request', cntrller='requests', **kwd ) ) if operation == "delete": @@ -70,12 +69,12 @@ class Requests( BaseController ): message = "%d requests (highlighted in red) were rejected. Click on the request name for details." % rejected kwd[ 'status' ] = status kwd[ 'message' ] = message - # show the create request button to the user, only when the user has permissions - # to at least one request_type (sequencer configuration) + # Allow the user to create a new request only if they have permission to access a + # (sequencer configuration) request type. if len( trans.user.accessible_request_types( trans ) ): self.request_grid.global_actions = [ grids.GridAction( "Create new request", dict( controller='requests_common', - action='create_request', - cntrller='requests' ) ) ] + action='create_request', + cntrller='requests' ) ) ] else: self.request_grid.global_actions = [] # Render the list view --- a/templates/requests/common/find_samples.mako +++ b/templates/requests/common/find_samples.mako @@ -76,7 +76,7 @@ <i>User: ${sample.request.user.email}</i> %endif <div class="toolParamHelp" style="clear: both;"> - <a href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}">Sequencing request: ${sample.request.name} | Type: ${sample.request.type.name} | State: ${sample.request.state}</a> + <a href="${h.url_for( controller='requests_common', action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}">Sequencing request: ${sample.request.name} | Type: ${sample.request.type.name} | State: ${sample.request.state}</a></div></div><br/> --- a/lib/galaxy/web/controllers/forms.py +++ b/lib/galaxy/web/controllers/forms.py @@ -86,7 +86,7 @@ class Forms( BaseController ): status='error', message="Invalid form ID") ) if operation == "view": - return self.__view( trans, **kwd ) + return self.view_form_definition( trans, **kwd ) elif operation == "delete": return self.__delete( trans, **kwd ) elif operation == "undelete": @@ -94,10 +94,12 @@ class Forms( BaseController ): elif operation == "edit": return self.edit( trans, **kwd ) return self.forms_grid( trans, **kwd ) - def __view(self, trans, **kwd): + @web.expose + def view_form_definition( self, trans, **kwd ): + form_definition_current_id = kwd.get( 'id', None ) try: - fdc = trans.sa_session.query( trans.app.model.FormDefinitionCurrent )\ - .get( trans.security.decode_id(kwd['id']) ) + fdc = trans.sa_session.query( trans.app.model.FormDefinitionCurrent ) \ + .get( trans.security.decode_id( form_definition_current_id ) ) except: return trans.response.send_redirect( web.url_for( controller='forms', action='manage', --- /dev/null +++ b/templates/requests/common/common.mako @@ -0,0 +1,332 @@ +<%namespace file="/requests/common/sample_state.mako" import="render_sample_state" /> + +<%def name="javascripts()"> + ${self.common_javascripts()} +</%def> + +<%def name="common_javascripts()"> + <script type="text/javascript"> + function showContent(vThis) + { + // http://www.javascriptjunkie.com + // alert(vSibling.className + " " + vDef_Key); + vParent = vThis.parentNode; + vSibling = vParent.nextSibling; + while (vSibling.nodeType==3) { + // Fix for Mozilla/FireFox Empty Space becomes a TextNode or Something + vSibling = vSibling.nextSibling; + }; + if(vSibling.style.display == "none") + { + vThis.src="/static/images/fugue/toggle.png"; + vThis.alt = "Hide"; + vSibling.style.display = "block"; + } else { + vSibling.style.display = "none"; + vThis.src="/static/images/fugue/toggle-expand.png"; + vThis.alt = "Show"; + } + return; + } + $(document).ready(function(){ + //hide the all of the element with class msg_body + $(".msg_body").hide(); + //toggle the component with class msg_body + $(".msg_head").click(function(){ + $(this).next(".msg_body").slideToggle(0); + }); + }); + + function checkAllFields() + { + var chkAll = document.getElementById('checkAll'); + var checks = document.getElementsByTagName('input'); + var boxLength = checks.length; + var allChecked = false; + var totalChecked = 0; + if ( chkAll.checked == true ) + { + for ( i=0; i < boxLength; i++ ) + { + if ( checks[i].name.indexOf( 'select_sample_' ) != -1) + { + checks[i].checked = true; + } + } + } + else + { + for ( i=0; i < boxLength; i++ ) + { + if ( checks[i].name.indexOf( 'select_sample_' ) != -1) + { + checks[i].checked = false + } + } + } + } + </script> +</%def> + +<%def name="render_editable_sample_row( is_admin, sample, current_sample_index, current_sample, encoded_selected_sample_ids )"> + <% + if sample: + is_complete = sample.request.is_complete + is_submitted = sample.request.is_submitted + is_unsubmitted = sample.request.is_unsubmitted + else: + is_complete = False + is_submitted = False + is_unsubmitted = False + %> + <% + if is_admin and is_submitted and editing_samples and trans.security.encode_id( sample.id ) in encoded_selected_sample_ids: + checked_str = "checked" + else: + checked_str = "" + %> + %if is_admin and is_submitted and editing_samples: + <td><input type="checkbox" name=select_sample_${sample.id} id="sample_checkbox" value="true" ${checked_str}/><input type="hidden" name=select_sample_${sample.id} id="sample_checkbox" value="true"/></td> + %endif + <td> + <input type="text" name="sample_${current_sample_index}_name" value="${current_sample['name']}" size="10"/> + <div class="toolParamHelp" style="clear: both;"> + <i>${' (required)' }</i> + </div> + </td> + %if sample and is_submitted or is_complete: + <td><input type="text" name="sample_${current_sample_index}_barcode" value="${current_sample['barcode']}" size="10"/></td> + %endif + %if sample: + %if is_unsubmitted: + <td>Unsubmitted</td> + %else: + <td><a href="${h.url_for( controller='requests_common', action='sample_events', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${sample.state.name}</a></td> + %endif + %else: + <td></td> + %endif + <td>${current_sample['library_select_field'].get_html()}</td> + <td>${current_sample['folder_select_field'].get_html()}</td> + %if is_submitted or is_complete: + <% + if sample: + label = str( len( sample.datasets ) ) + else: + label = 'add' + %> + <td><a href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${label}</a></td> + <td><a href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${label}</a></td> + %endif + %if sample and ( is_admin or is_unsubmitted ): + ## Delete button + <td><a class="action-button" href="${h.url_for( controller='requests_common', action='delete_sample', cntrller=cntrller, request_id=trans.security.encode_id( request.id ), sample_id=current_sample_index )}"><img src="${h.url_for('/static/images/delete_icon.png')}" style="cursor:pointer;"/></a></td> + %endif +</%def> + +<%def name="render_samples_grid( cntrller, request, current_samples, action, editing_samples=False, encoded_selected_sample_ids=[], render_buttons=False, grid_header='<h3>Samples</h3>' )"> + ## Displays the "Samples" grid + <% + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + is_complete = request.is_complete + is_submitted = request.is_submitted + is_unsubmitted = request.is_unsubmitted + can_add_samples = request.is_unsubmitted + can_edit_or_delete_samples = request.samples and ( is_admin or request.is_unsubmitted ) + %> + ${grid_header} + %if render_buttons and ( can_add_samples or can_edit_or_delete_samples ): + <ul class="manage-table-actions"> + %if can_add_samples: + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='add_sample', cntrller=cntrller, request_id=trans.security.encode_id( request.id ), add_sample_button='Add sample' )}">Add sample</a></li> + %endif + %if can_edit_or_delete_samples: + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( request.id ), editing_samples='True' )}">Edit samples</a></li> + %endif + </ul> + %endif + <table class="grid"> + <thead> + <tr> + %if is_admin and is_submitted and editing_samples: + <th><input type="checkbox" id="checkAll" name=select_all_samples_checkbox value="true" onclick='checkAllFields(1);'/><input type="hidden" name=select_all_samples_checkbox value="true"/></th> + %endif + <th>Name</th> + %if is_submitted or is_complete: + <th>Barcode</th> + %endif + <th>State</th> + <th>Data Library</th> + <th>Folder</th> + %if is_submitted or is_complete: + <th>Datasets Selected</th> + <th>Datasets Transferred</th> + %endif + <th> + %if editing_samples: + Delete + %endif + </th> + </tr> + <thead> + <tbody> + <% trans.sa_session.refresh( request ) %> + ## current_samples is a dictionary whose keys are: + ## name, barcode, library, folder, field_values, library_select_field, folder_select_field + %for current_sample_index, current_sample in enumerate( current_samples ): + <% + current_sample_name = current_sample[ 'name' ] + current_sample_barcode = current_sample[ 'barcode' ] + current_sample_library = current_sample[ 'library' ] + if current_sample_library: + if cntrller == 'requests': + library_cntrller = 'library' + elif is_admin: + library_cntrller = 'library_admin' + else: + library_cntrller = None + current_sample_folder = current_sample[ 'folder' ] + try: + sample = request.samples[ current_sample_index ] + except: + sample = None + %> + %if editing_samples: + <tr>${render_editable_sample_row( is_admin, sample, current_sample_index, current_sample, encoded_selected_sample_ids )}</tr> + %elif sample: + <tr> + <td>${current_sample_name}</td> + %if is_submitted or is_complete: + <td>${current_sample_barcode}</td> + %endif + %if is_unsubmitted: + <td>Unsubmitted</td> + %else: + <td><a id="sampleState-${sample.id}" href="${h.url_for( controller='requests_common', action='sample_events', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${render_sample_state( sample )}</a></td> + %endif + %if current_sample_library and library_cntrller is not None: + <td><a href="${h.url_for( controller='library_common', action='browse_library', cntrller=library_cntrller, id=trans.security.encode_id( current_sample_library.id ) )}">${current_sample_library.name}</a></td> + %else: + <td></td> + %endif + %if current_sample_folder: + <td>${current_sample_folder.name}</td> + %else: + <td></td> + %endif + %if is_submitted or is_complete: + <td><a id="sampleDatasets-${sample.id}" href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${len( sample.datasets )}</a></td> + <td><a id="sampleDatasets-${sample.id}" href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${len( sample.transferred_dataset_files )}</a></td> + %endif + </tr> + %else: + <tr>${render_editable_sample_row( is_admin, None, current_sample_index, current_sample, encoded_selected_sample_ids )}</tr> + %endif + %endfor + </tbody> + </table> +</%def> + +<%def name="render_sample_form( index, sample_name, sample_values, fields_dict, display_only )"> + <tr> + <td>${sample_name}</td> + %for field_index, field in fields_dict.items(): + <% + field_type = field[ 'type' ] + %> + <td> + %if display_only: + %if sample_values[field_index]: + %if field_type == 'WorkflowField': + %if str(sample_values[field_index]) != 'none': + <% workflow = trans.sa_session.query( trans.app.model.StoredWorkflow ).get( int( sample_values[ field_index ] ) ) %> + <a href="${h.url_for( controller='workflow', action='run', id=trans.security.encode_id( workflow.id ) )}">${workflow.name}</a> + %endif + %else: + ${sample_values[ field_index ]} + %endif + %else: + <i>None</i> + %endif + %else: + %if field_type == 'TextField': + <input type="text" name="sample_${index}_field_${field_index}" value="${sample_values[field_index]}" size="7"/> + %elif field_type == 'SelectField': + <select name="sample_${index}_field_${field_index}" last_selected_value="2"> + %for option_index, option in enumerate(field['selectlist']): + %if option == sample_values[field_index]: + <option value="${option}" selected>${option}</option> + %else: + <option value="${option}">${option}</option> + %endif + %endfor + </select> + %elif field_type == 'WorkflowField': + <select name="sample_${index}_field_${field_index}"> + %if str(sample_values[field_index]) == 'none': + <option value="none" selected>Select one</option> + %else: + <option value="none">Select one</option> + %endif + %for option_index, option in enumerate(request.user.stored_workflows): + %if not option.deleted: + %if str(option.id) == str(sample_values[field_index]): + <option value="${option.id}" selected>${option.name}</option> + %else: + <option value="${option.id}">${option.name}</option> + %endif + %endif + %endfor + </select> + %elif field_type == 'CheckboxField': + <input type="checkbox" name="sample_${index}_field_${field_index}" value="Yes"/> + %endif + <div class="toolParamHelp" style="clear: both;"> + <i>${'('+field['required']+')' }</i> + </div> + %endif + </td> + %endfor + </tr> +</%def> + +<%def name="render_request_type_sample_form_grids( grid_index, grid_name, fields_dict, editing_samples )"> + <% + if not grid_name: + grid_name = "Sample form layout " + grid_index + %> + <h4><img src="/static/images/fugue/toggle-expand.png" alt="Hide" onclick="showContent(this);" style="cursor:pointer;"/> ${grid_name}</h4> + <div style="display:none;"> + <table class="grid"> + <thead> + <tr> + <th>Name</th> + %for index, field in fields_dict.items(): + <th> + ${field['label']} + ## TODO: help comments in the grid header are UGLY! + ## If they are needed display them more appropriately, + ## if they are not, delete this commented code. + ##<div class="toolParamHelp" style="clear: both;"> + ## <i>${field['helptext']}</i> + ##</div> + </th> + %endfor + <th></th> + </tr> + <thead> + <tbody> + <% trans.sa_session.refresh( request ) %> + %for sample_index, sample in enumerate( current_samples ): + <% + if editing_samples or sample_index >= len( request.samples ): + display_only = False + else: + display_only = True + %> + ${render_sample_form( sample_index, sample['name'], sample['field_values'], fields_dict, display_only )} + %endfor + </tbody> + </table> + </div> +</%def> --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -1489,17 +1489,17 @@ class TwillTestCase( unittest.TestCase ) for check_str in strings_displayed_after_submit: self.check_page_for_string( check_str ) def add_samples( self, cntrller, request_id, request_name, sample_value_tuples, strings_displayed=[], strings_displayed_after_submit=[] ): - self.visit_url( "%s/requests_common/manage_request?cntrller=%s&id=%s" % ( self.url, cntrller, request_id ) ) + self.visit_url( "%s/requests_common/edit_samples?cntrller=%s&id=%s&editing_samples=False" % ( self.url, cntrller, request_id ) ) for check_str in strings_displayed: self.check_page_for_string( check_str ) # Simulate clicking the add-sample_button on the form. (gvk: 9/21/10 - TODO : There must be a bug in the mako template # because twill cannot find any forms on the page, but I cannot find it although I've spent time cleaning up the # template code and looking for any problems. - url = "%s/requests_common/manage_request?cntrller=%s&id=%s" % ( self.url, cntrller, request_id ) + url = "%s/requests_common/edit_samples?cntrller=%s&id=%s&editing_samples=False" % ( self.url, cntrller, request_id ) # This should work, but although twill does not thorw any exceptions, the button click never occurs - # There are multiple forms on this page, and we'll only be using the form named manage_request. + # There are multiple forms on this page, and we'll only be using the form named edit_samples. # for sample_index, sample_value_tuple in enumerate( sample_value_tuples ): - # # Add the following form value to the already populated hidden field so that the manage_request + # # Add the following form value to the already populated hidden field so that the edit_samples # # form is the current form # tc.fv( "1", "id", request_id ) # tc.submit( 'add_sample_button' ) @@ -1530,7 +1530,7 @@ class TwillTestCase( unittest.TestCase ) for check_str in strings_displayed_after_submit: self.check_page_for_string( check_str ) def reject_request( self, request_id, request_name, comment, strings_displayed=[], strings_displayed_after_submit=[] ): - self.visit_url( "%s/requests_admin/reject?id=%s" % ( self.url, request_id ) ) + self.visit_url( "%s/requests_admin/reject_request?id=%s" % ( self.url, request_id ) ) for check_str in strings_displayed: self.check_page_for_string( check_str ) tc.fv( "1", "comment", comment ) @@ -1540,7 +1540,7 @@ class TwillTestCase( unittest.TestCase ) def add_bar_codes( self, request_id, request_name, bar_codes, samples, strings_displayed_after_submit=[] ): # We have to simulate the form submission here since twill barfs on the page # gvk - 9/22/10 - TODO: make sure the mako template produces valid html - url = "%s/requests_common/manage_request?cntrller=requests_admin&id=%s&managing_samples=True" % ( self.url, request_id ) + url = "%s/requests_common/edit_samples?cntrller=requests_admin&id=%s&editing_samples=True" % ( self.url, request_id ) for index, field_value in enumerate( bar_codes ): sample_field_name = "sample_%i_name" % index sample_field_value = samples[ index ].name.replace( ' ', '+' ) @@ -1555,15 +1555,15 @@ class TwillTestCase( unittest.TestCase ) strings_displayed=[], strings_displayed_after_submit=[] ): # We have to simulate the form submission here since twill barfs on the page # gvk - 9/22/10 - TODO: make sure the mako template produces valid html - url = "%s/requests_common/manage_request?cntrller=requests_admin&id=%s" % ( self.url, request_id ) + url = "%s/requests_common/edit_samples?cntrller=requests_admin&id=%s" % ( self.url, request_id ) url += "&comment=%s&sample_state_id=%s" % ( comment, self.security.encode_id( new_sample_state_id ) ) # select_sample_%i=true must be included twice for each sample to simulate a CheckboxField checked setting. for sample_id in sample_ids: url += "&select_sample_%i=true&select_sample_%i=true" % ( sample_id, sample_id ) url += "&sample_operation=Change%20state&refresh=true" - url += "&change_state_button=Save" + url += "&save_changes_button=Save&editing_samples=True" self.visit_url( url ) - self.check_page_for_string( 'Sequencing Request "%s"' % request_name ) + self.check_page_for_string( 'Edit Current Samples of Request "%s"' % request_name ) for sample_id, sample_name in zip( sample_ids, sample_names ): self.visit_url( "%s/requests_common/sample_events?cntrller=requests_admin&sample_id=%s" % ( self.url, self.security.encode_id( sample_id ) ) ) self.check_page_for_string( 'Events for Sample "%s"' % sample_name ) --- a/templates/requests/common/events.mako +++ b/templates/requests/common/events.mako @@ -1,20 +1,36 @@ <%inherit file="/base.mako"/><%namespace file="/message.mako" import="render_msg" /> -<h2>History of Sequencing Request "${request.name}"</h2> +<% + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + can_edit_request = ( is_admin and not request.is_complete ) or request.is_unsubmitted + can_add_samples = request.is_unsubmitted +%> + +<br/><br/><ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> - </li> - <li> - <a class="action-button" href="${h.url_for( controller=cntrller, action='browse_requests' )}">Browse all requests</a> - </li> + <li><a class="action-button" id="request-${request.id}-popup" class="menubutton">Request Actions</a></li> + <div popupmenu="request-${request.id}-popup"> + <a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Browse this request</a> + %if can_edit_request: + <a class="action-button" href="${h.url_for( controller='requests_common', action='edit_basic_request_info', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Edit</a> + %endif + %if can_add_samples: + <a class="action-button" confirm="More samples cannot be added to this request once it is submitted. Click OK to submit." href="${h.url_for( controller='requests_common', action='submit_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Submit</a> + %endif + %if is_admin and request.is_submitted: + <a class="action-button" href="${h.url_for( controller='requests_admin', action='reject_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Reject</a> + <a class="action-button" href="${h.url_for( controller='requests_admin', action='get_data', request_id=trans.security.encode_id( request.id ) )}">Select datasets to transfer</a> + %endif + </div></ul> %if message: ${render_msg( message, status )} %endif +<h2>History of Sequencing Request "${request.name}"</h2> + <div class="toolForm"><table class="grid"><thead> --- a/lib/galaxy/web/controllers/requests_common.py +++ b/lib/galaxy/web/controllers/requests_common.py @@ -70,14 +70,14 @@ class RequestsGrid( grids.Grid ): columns = [ NameColumn( "Name", key="name", - link=( lambda item: iff( item.deleted, None, dict( operation="manage_request", id=item.id ) ) ), + link=( lambda item: iff( item.deleted, None, dict( operation="view_request", id=item.id ) ) ), attach_popup=True, filterable="advanced" ), DescriptionColumn( "Description", key='desc', filterable="advanced" ), SamplesColumn( "Samples", - link=( lambda item: iff( item.deleted, None, dict( operation="manage_request", id=item.id ) ) ) ), + link=( lambda item: iff( item.deleted, None, dict( operation="edit_samples", id=item.id ) ) ) ), TypeColumn( "Sequencer", link=( lambda item: iff( item.deleted, None, dict( operation="view_type", id=item.type.id ) ) ) ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), @@ -148,7 +148,7 @@ class RequestsCommon( BaseController, Us except TypeError, e: # We must have an email address rather than an encoded user id # This is because the galaxy.base.js creates a search+select box - # when there are more than 20 items in a selectfield + # when there are more than 20 items in a SelectField. user = trans.sa_session.query( trans.model.User ) \ .filter( trans.model.User.table.c.email==util.restore_text( user_id ) ) \ .first() @@ -178,7 +178,8 @@ class RequestsCommon( BaseController, Us message=message , status='done' ) ) elif params.get( 'add_sample_button', False ): - return self.__add_sample( trans, cntrller, request, **kwd ) + request_id = trans.security.encode_id( request.id ) + return self.add_sample( trans, cntrller, request_id, **kwd ) request_type_select_field = self.__build_request_type_id_select_field( trans, selected_value=request_type_id ) # Widgets to be rendered on the request form widgets = [] @@ -194,7 +195,7 @@ class RequestsCommon( BaseController, Us widgets += request_type.request_form.get_widgets( user, **kwd ) # In case there is an error on the form, make sure to populate widget fields with anything the user # may have already entered. - self.populate_widgets_from_kwd( trans, widgets, **kwd ) + widgets = self.populate_widgets_from_kwd( trans, widgets, **kwd ) if request_type is not None or status == 'error': # Either the user selected a request_type or an error exists on the form. if is_admin: @@ -214,6 +215,29 @@ class RequestsCommon( BaseController, Us message=message, status=status ) @web.expose + @web.require_login( "view request" ) + def view_request( self, trans, cntrller, **kwd ): + params = util.Params( kwd ) + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + request_id = params.get( 'id', None ) + try: + request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( request_id ) ) + except: + return invalid_id_redirect( trans, cntrller, request_id ) + sample_state_id = params.get( 'sample_state_id', None ) + # Get the user entered sample information + current_samples = self.__get_sample_widgets( trans, request, request.samples, **kwd ) + request_widgets = self.__get_request_widgets( trans, request.id ) + return trans.fill_template( '/requests/common/view_request.mako', + cntrller=cntrller, + request=request, + request_widgets=request_widgets, + current_samples=current_samples, + status=status, + message=message ) + @web.expose @web.require_login( "edit sequencing requests" ) def edit_basic_request_info( self, trans, cntrller, **kwd ): params = util.Params( kwd ) @@ -226,7 +250,7 @@ class RequestsCommon( BaseController, Us return invalid_id_redirect( trans, cntrller, request_id ) name = util.restore_text( params.get( 'name', '' ) ) desc = util.restore_text( params.get( 'desc', '' ) ) - if params.get( 'edit_basic_request_info_button', False ) or params.get( 'edit_samples_button', False ): + if params.get( 'edit_basic_request_info_button', False ): if not name: status = 'error' message = 'Enter the name of the request' @@ -244,7 +268,7 @@ class RequestsCommon( BaseController, Us widgets = widgets + request.type.request_form.get_widgets( request.user, request.values.content, **kwd ) # In case there is an error on the form, make sure to populate widget fields with anything the user # may have already entered. - self.populate_widgets_from_kwd( trans, widgets, **kwd ) + widgets = self.populate_widgets_from_kwd( trans, widgets, **kwd ) return trans.fill_template( 'requests/common/edit_basic_request_info.mako', cntrller=cntrller, request_type=request.type, @@ -371,8 +395,8 @@ class RequestsCommon( BaseController, Us status=status, message=message ) ) @web.expose - @web.require_login( "sequencing request page" ) - def manage_request( self, trans, cntrller, **kwd ): + @web.require_login( "manage samples" ) + def edit_samples( self, trans, cntrller, **kwd ): params = util.Params( kwd ) is_admin = cntrller == 'requests_admin' and trans.user_is_admin() message = util.restore_text( params.get( 'message', '' ) ) @@ -382,80 +406,64 @@ class RequestsCommon( BaseController, Us request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( request_id ) ) except: return invalid_id_redirect( trans, cntrller, request_id ) - sample_state_id = params.get( 'sample_state_id', None ) + # This method is called when the user is adding new samples as well as + # editing existing samples, so we use the editing_samples flag to keep + # track of what's occurring. + # TODO: CRITICAL: We need another round of code fixes to abstract out + # adding samples vs editing samples. We need to eliminate the need for + # this editing_samples flag since it is not maintainable. Greg will do + # this work as soon as possible. + editing_samples = util.string_as_bool( params.get( 'editing_samples', False ) ) + if params.get( 'cancel_changes_button', False ): + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + id=request_id, + editing_samples=editing_samples ) ) + # Get all libraries for which the current user has permission to add items. + libraries = request.user.accessible_libraries( trans, [ trans.app.security_agent.permitted_actions.LIBRARY_ADD ] ) # Get the user entered sample information - current_samples, managing_samples, libraries = self.__get_sample_info( trans, request, **kwd ) - selected_samples = self.__get_selected_samples( trans, request, **kwd ) - selected_value = params.get( 'sample_operation', 'none' ) - if selected_value != 'none' and not selected_samples: + current_samples = self.__get_sample_widgets( trans, request, request.samples, **kwd ) + encoded_selected_sample_ids = self.__get_encoded_selected_sample_ids( trans, request, **kwd ) + sample_operation = params.get( 'sample_operation', 'none' ) + def handle_error( **kwd ): + kwd[ 'status' ] = 'error' + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + **kwd ) ) + if not encoded_selected_sample_ids and sample_operation != 'none': + # Probably occurred due to refresh_on_change...is there a better approach? + kwd[ 'sample_operation' ] = 'none' message = 'Select at least one sample before selecting an operation.' - status = 'error' - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=request_id, - status=status, - message=message ) ) - sample_operation_select_field = self.__build_sample_operation_select_field( trans, is_admin, request, selected_value ) - sample_operation_selected_value = sample_operation_select_field.get_selected( return_value=True ) + kwd[ 'message' ] = message + handle_error( **kwd ) if params.get( 'import_samples_button', False ): # Import sample field values from a csv file return self.__import_samples( trans, cntrller, request, current_samples, libraries, **kwd ) elif params.get( 'add_sample_button', False ): - return self.__add_sample( trans, cntrller, request, **kwd ) + return self.add_sample( trans, cntrller, request_id, **kwd ) elif params.get( 'save_samples_button', False ): - return self.__save_sample( trans, cntrller, request, current_samples, **kwd ) - elif params.get( 'edit_samples_button', False ): - managing_samples = True - elif params.get( 'cancel_changes_button', False ): - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=request_id ) ) - pass - elif params.get( 'change_state_button', False ): - sample_event_comment = util.restore_text( params.get( 'sample_event_comment', '' ) ) - new_state = trans.sa_session.query( trans.model.SampleState ).get( trans.security.decode_id( sample_state_id ) ) - self.update_sample_state(trans, cntrller, selected_samples, new_state, comment=sample_event_comment ) - return trans.response.send_redirect( web.url_for( controller='requests_common', - cntrller=cntrller, - action='update_request_state', - request_id=request_id ) ) - elif params.get( 'cancel_change_state_button', False ): - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=request_id ) ) - elif params.get( 'change_lib_button', False ): - library_id = params.get( 'sample_0_library_id', None ) - try: - library = trans.sa_session.query( trans.model.Library ).get( trans.security.decode_id( library_id ) ) - except: - invalid_id_redirect( trans, cntrller, library_id ) - folder_id = params.get( 'sample_0_folder_id', None ) - try: - folder = trans.sa_session.query( trans.model.LibraryFolder ).get( trans.security.decode_id( folder_id ) ) - except: - invalid_id_redirect( trans, cntrller, folder_id ) - for sample_id in selected_samples: - sample = trans.sa_session.query( trans.model.Sample ).get( trans.security.decode_id( sample_id ) ) - sample.library = library - sample.folder = folder - trans.sa_session.add( sample ) - trans.sa_session.flush() - trans.sa_session.refresh( request ) - message = 'Changes made to the selected samples have been saved. ' - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=request_id, - status=status, - message=message ) ) - elif params.get( 'cancel_change_lib_button', False ): - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=trans.security.encode_id( request.id ) ) ) + if encoded_selected_sample_ids: + # This gets tricky because we need the list of samples to include the same number + # of objects that that current_samples ( i.e., request.samples ) has. We'll first + # get the set of samples corresponding to the checked sample ids. + samples = [] + selected_samples = [] + for encoded_sample_id in encoded_selected_sample_ids: + sample = trans.sa_session.query( trans.model.Sample ).get( trans.security.decode_id( encoded_sample_id ) ) + selected_samples.append( sample ) + # Now build the list of samples, inserting None for samples that have not been checked. + for sample in request.samples: + if sample in selected_samples: + samples.append( sample ) + else: + samples.append( None ) + # The __save_samples method requires samples widgets, not sample objects + samples = self.__get_sample_widgets( trans, request, samples, **kwd ) + else: + samples = current_samples + return self.__save_samples( trans, cntrller, request, samples, **kwd ) request_widgets = self.__get_request_widgets( trans, request.id ) sample_copy = self.__build_copy_sample_select_field( trans, current_samples ) libraries_select_field, folders_select_field = self.__build_library_and_folder_select_fields( trans, @@ -464,12 +472,13 @@ class RequestsCommon( BaseController, Us libraries, None, **kwd ) - # Build the sample_state_id_select_field SelectField + sample_operation_select_field = self.__build_sample_operation_select_field( trans, is_admin, request, sample_operation ) + sample_state_id = params.get( 'sample_state_id', None ) sample_state_id_select_field = self.__build_sample_state_id_select_field( trans, request, sample_state_id ) - return trans.fill_template( '/requests/common/manage_request.mako', + return trans.fill_template( '/requests/common/edit_samples.mako', cntrller=cntrller, request=request, - selected_samples=selected_samples, + encoded_selected_sample_ids=encoded_selected_sample_ids, request_widgets=request_widgets, current_samples=current_samples, sample_copy=sample_copy, @@ -478,7 +487,7 @@ class RequestsCommon( BaseController, Us libraries_select_field=libraries_select_field, folders_select_field=folders_select_field, sample_state_id_select_field=sample_state_id_select_field, - managing_samples=managing_samples, + editing_samples=editing_samples, status=status, message=message ) @web.expose @@ -489,7 +498,7 @@ class RequestsCommon( BaseController, Us except: if cntrller == 'api': trans.response.status = 400 - return "Malformed sample id ( %s ) specified, unable to decode." % str( sample_id ) + return "Invalid sample id ( %s ) specified, unable to decode." % str( sample_id ) else: return invalid_id_redirect( trans, cntrller, sample_id ) event = trans.model.SampleEvent( sample, new_state, comment ) @@ -516,7 +525,7 @@ class RequestsCommon( BaseController, Us if ok_for_now: request.deleted = True trans.sa_session.add( request ) - # delete all the samples belonging to this request + # Delete all the samples belonging to this request for s in request.samples: s.deleted = True trans.sa_session.add( s ) @@ -546,7 +555,7 @@ class RequestsCommon( BaseController, Us if ok_for_now: request.deleted = False trans.sa_session.add( request ) - # undelete all the samples belonging to this request + # Undelete all the samples belonging to this request for s in request.samples: s.deleted = False trans.sa_session.add( s ) @@ -655,9 +664,10 @@ class RequestsCommon( BaseController, Us if cntrller == 'api': return 200, message return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='edit_samples', cntrller=cntrller, id=request_id, + editing_samples=True, status=status, message=message ) ) final_state = False @@ -673,8 +683,8 @@ class RequestsCommon( BaseController, Us event = trans.model.RequestEvent( request, state, comments ) trans.sa_session.add( event ) trans.sa_session.flush() - # check if an email notification is configured to be sent when the samples - # are in this state + # See if an email notification is configured to be sent when the samples + # are in this state. retval = request.send_email_notification( trans, common_state, final_state ) if retval: message = comments + retval @@ -683,113 +693,10 @@ class RequestsCommon( BaseController, Us if cntrller == 'api': return 200, message return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='edit_samples', cntrller=cntrller, - id=trans.security.encode_id(request.id), - status='done', - message=message ) ) - def __save_sample( self, trans, cntrller, request, current_samples, **kwd ): - # Save all the new/unsaved samples entered by the user - params = util.Params( kwd ) - message = util.restore_text( params.get( 'message', '' ) ) - status = params.get( 'status', 'done' ) - managing_samples = util.string_as_bool( params.get( 'managing_samples', False ) ) - is_admin = cntrller == 'requests_admin' and trans.user_is_admin() - selected_value = params.get( 'sample_operation', 'none' ) - # Check for duplicate sample names - message = '' - for index in range( len( current_samples ) - len( request.samples ) ): - sample_index = index + len( request.samples ) - current_sample = current_samples[ sample_index ] - sample_name = current_sample[ 'name' ] - if not sample_name.strip(): - message = 'Enter the name of sample number %i' % sample_index - break - count = 0 - for i in range( len( current_samples ) ): - if sample_name == current_samples[ i ][ 'name' ]: - count += 1 - if count > 1: - message = "This request has %i samples with the name (%s). Samples belonging to a request must have unique names." % ( count, sample_name ) - break - if message: - selected_samples = self.__get_selected_samples( trans, request, **kwd ) - request_widgets = self.__get_request_widgets( trans, request.id ) - sample_copy = self.__build_copy_sample_select_field( trans, current_samples ) - sample_operation_select_field = self.__build_sample_operation_select_field( trans, is_admin, request, selected_value ) - status = 'error' - return trans.fill_template( '/requests/common/manage_request.mako', - cntrller=cntrller, - request=request, - selected_samples=selected_samples, - request_widgets=request_widgets, - current_samples=current_samples, - sample_copy=sample_copy, - managing_samples=managing_samples, - sample_operation_select_field=sample_operation_select_field, - status=status, - message=message ) - if not managing_samples: - for index in range( len( current_samples ) - len( request.samples ) ): - sample_index = len( request.samples ) - current_sample = current_samples[ sample_index ] - form_values = trans.model.FormValues( request.type.sample_form, current_sample[ 'field_values' ] ) - trans.sa_session.add( form_values ) - trans.sa_session.flush() - s = trans.model.Sample( current_sample[ 'name' ], - '', - request, - form_values, - current_sample[ 'barcode' ], - current_sample[ 'library' ], - current_sample[ 'folder' ] ) - trans.sa_session.add( s ) - trans.sa_session.flush() - else: - message = 'Changes made to the samples are saved. ' - for sample_index in range( len( current_samples ) ): - sample = request.samples[ sample_index ] - current_sample = current_samples[ sample_index ] - sample.name = current_sample[ 'name' ] - sample.library = current_sample[ 'library' ] - sample.folder = current_sample[ 'folder' ] - if request.is_submitted: - bc_message = self.__validate_barcode( trans, sample, current_sample[ 'barcode' ] ) - if bc_message: - status = 'error' - message += bc_message - else: - if not sample.bar_code: - # If this is a 'new' (still in its first state) sample - # change the state to the next - if sample.state.id == request.type.states[0].id: - event = trans.model.SampleEvent( sample, - request.type.states[1], - 'Sample added to the system' ) - trans.sa_session.add( event ) - trans.sa_session.flush() - # Now check if all the samples' barcode has been entered. - # If yes then send notification email if configured - common_state = request.samples_have_common_state - if common_state: - if common_state.id == request.type.states[1].id: - event = trans.model.RequestEvent( request, - request.states.SUBMITTED, - "All samples are in %s state." % common_state.name ) - trans.sa_session.add( event ) - trans.sa_session.flush() - request.send_email_notification( trans, request.type.states[1] ) - sample.bar_code = current_samples[sample_index]['barcode'] - trans.sa_session.add( sample ) - trans.sa_session.flush() - form_values = trans.sa_session.query( trans.model.FormValues ).get( sample.values.id ) - form_values.content = current_sample[ 'field_values' ] - trans.sa_session.add( form_values ) - trans.sa_session.flush() - return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', - cntrller=cntrller, - id=trans.security.encode_id( request.id ), + id=request_id, + editing_samples=True, status=status, message=message ) ) @web.expose @@ -876,25 +783,33 @@ class RequestsCommon( BaseController, Us cntrller=cntrller, events_list=events_list, sample=sample ) - def __add_sample( self, trans, cntrller, request, **kwd ): + @web.expose + @web.require_login( "add sample" ) + def add_sample( self, trans, cntrller, request_id, **kwd ): params = util.Params( kwd ) message = util.restore_text( params.get( 'message', '' ) ) status = params.get( 'status', 'done' ) - managing_samples = util.string_as_bool( params.get( 'managing_samples', False ) ) + try: + request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( request_id ) ) + except: + return invalid_id_redirect( trans, cntrller, request_id ) is_admin = cntrller == 'requests_admin' and trans.user_is_admin() # Get the widgets for rendering the request form request_widgets = self.__get_request_widgets( trans, request.id ) - current_samples, managing_samples, libraries = self.__get_sample_info( trans, request, **kwd ) + current_samples = self.__get_sample_widgets( trans, request, request.samples, **kwd ) if not current_samples: # Form field names are zero-based. sample_index = 0 else: sample_index = len( current_samples ) if params.get( 'add_sample_button', False ): + # Get all libraries for which the current user has permission to add items + libraries = request.user.accessible_libraries( trans, [ trans.app.security_agent.permitted_actions.LIBRARY_ADD ] ) num_samples_to_add = int( params.get( 'num_sample_to_copy', 1 ) ) # See if the user has selected a sample to copy. copy_sample_index = int( params.get( 'copy_sample_index', -1 ) ) for index in range( num_samples_to_add ): + id_index = len( current_samples ) + 1 if copy_sample_index != -1: # The user has selected a sample to copy. library_id = current_samples[ copy_sample_index][ 'library_select_field' ].get_selected( return_value=True ) @@ -902,7 +817,7 @@ class RequestsCommon( BaseController, Us name = current_samples[ copy_sample_index ][ 'name' ] + '_%i' % ( len( current_samples ) + 1 ) field_values = [ val for val in current_samples[ copy_sample_index ][ 'field_values' ] ] else: - # The user has not selected a sample to copy (may just be adding a sample). + # The user has not selected a sample to copy, just adding a new generic sample. library_id = None folder_id = None name = 'Sample_%i' % ( len( current_samples ) + 1 ) @@ -910,7 +825,7 @@ class RequestsCommon( BaseController, Us # Build the library_select_field and folder_select_field for the new sample being added. library_select_field, folder_select_field = self.__build_library_and_folder_select_fields( trans, user=request.user, - sample_index=len( current_samples ), + sample_index=id_index, libraries=libraries, sample=None, library_id=library_id, @@ -926,101 +841,21 @@ class RequestsCommon( BaseController, Us field_values=field_values, library_select_field=library_select_field, folder_select_field=folder_select_field ) ) - selected_samples = self.__get_selected_samples( trans, request, **kwd ) - selected_value = params.get( 'sample_operation', 'none' ) - sample_operation_select_field = self.__build_sample_operation_select_field( trans, is_admin, request, selected_value ) + encoded_selected_sample_ids = self.__get_encoded_selected_sample_ids( trans, request, **kwd ) + sample_operation = params.get( 'sample_operation', 'none' ) + sample_operation_select_field = self.__build_sample_operation_select_field( trans, is_admin, request, sample_operation ) sample_copy = self.__build_copy_sample_select_field( trans, current_samples ) - return trans.fill_template( '/requests/common/manage_request.mako', + return trans.fill_template( '/requests/common/edit_samples.mako', cntrller=cntrller, request=request, - selected_samples=selected_samples, + encoded_selected_sample_ids=encoded_selected_sample_ids, request_widgets=request_widgets, current_samples=current_samples, sample_operation_select_field=sample_operation_select_field, sample_copy=sample_copy, - managing_samples=managing_samples, + editing_samples=False, message=message, status=status ) - def __get_sample_info( self, trans, request, **kwd ): - """ - Retrieves all user entered sample information and returns a - list of all the samples and their field values. - """ - params = util.Params( kwd ) - managing_samples = util.string_as_bool( params.get( 'managing_samples', False ) ) - # Bet all data libraries accessible to this user - libraries = request.user.accessible_libraries( trans, [ trans.app.security_agent.permitted_actions.LIBRARY_ADD ] ) - # Build the list of widgets which will be used to render each sample row on the request page - current_samples = [] - for index, sample in enumerate( request.samples ): - library_select_field, folder_select_field = self.__build_library_and_folder_select_fields( trans, - request.user, - index, - libraries, - sample, - **kwd ) - current_samples.append( dict( name=sample.name, - barcode=sample.bar_code, - library=sample.library, - folder=sample.folder, - field_values=sample.values.content, - library_select_field=library_select_field, - folder_select_field=folder_select_field ) ) - if not managing_samples: - sample_index = len( request.samples ) - else: - sample_index = 0 - while True: - library_id = params.get( 'sample_%i_library_id' % sample_index, None ) - folder_id = params.get( 'sample_%i_folder_id' % sample_index, None ) - if params.get( 'sample_%i_name' % sample_index, False ): - # Data library - try: - library = trans.sa_session.query( trans.model.Library ).get( trans.security.decode_id( library_id ) ) - #library_id = library.id - except: - library = None - if library is not None: - # Folder - try: - folder = trans.sa_session.query( trans.model.LibraryFolder ).get( trans.security.decode_id( folder_id ) ) - #folder_id = folder.id - except: - if library: - folder = library.root_folder - else: - folder = None - else: - folder = None - sample_info = dict( name=util.restore_text( params.get( 'sample_%i_name' % sample_index, '' ) ), - barcode=util.restore_text( params.get( 'sample_%i_barcode' % sample_index, '' ) ), - library=library, - folder=folder) - sample_info[ 'field_values' ] = [] - for field_index in range( len( request.type.sample_form.fields ) ): - sample_info[ 'field_values' ].append( util.restore_text( params.get( 'sample_%i_field_%i' % ( sample_index, field_index ), '' ) ) ) - if not managing_samples: - sample_info[ 'library_select_field' ], sample_info[ 'folder_select_field' ] = self.__build_library_and_folder_select_fields( trans, - request.user, - sample_index, - libraries, - None, - library_id, - folder_id, - **kwd ) - current_samples.append( sample_info ) - else: - sample_info[ 'library_select_field' ], sample_info[ 'folder_select_field' ] = self.__build_library_and_folder_select_fields( trans, - request.user, - sample_index, - libraries, - request.samples[ sample_index ], - **kwd ) - current_samples[ sample_index ] = sample_info - sample_index += 1 - else: - break - return current_samples, managing_samples, libraries @web.expose @web.require_login( "delete sample from sequencing request" ) def delete_sample( self, trans, cntrller, **kwd ): @@ -1032,7 +867,7 @@ class RequestsCommon( BaseController, Us request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( request_id ) ) except: return invalid_id_redirect( trans, cntrller, request_id ) - current_samples, managing_samples, libraries = self.__get_sample_info( trans, request, **kwd ) + current_samples = self.__get_sample_widgets( trans, request, request.samples, **kwd ) sample_index = int( params.get( 'sample_id', 0 ) ) sample_name = current_samples[sample_index]['name'] sample = request.has_sample( sample_name ) @@ -1042,9 +877,10 @@ class RequestsCommon( BaseController, Us trans.sa_session.flush() message = 'Sample (%s) has been deleted.' % sample_name return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( request.id ), + editing_samples=True, status=status, message=message ) ) @web.expose @@ -1059,12 +895,12 @@ class RequestsCommon( BaseController, Us sample = trans.sa_session.query( trans.model.Sample ).get( trans.security.decode_id( sample_id ) ) except: return invalid_id_redirect( trans, cntrller, sample_id ) - # check if a library and folder has been set for this sample yet. + # See if a library and folder have been set for this sample. if not sample.library or not sample.folder: status = 'error' message = "Set a data library and folder for sequencing request (%s) to transfer datasets." % sample.name return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ), status=status, @@ -1101,7 +937,6 @@ class RequestsCommon( BaseController, Us SampleName,DataLibrary,DataLibraryFolder,Field1,Field2.... """ params = util.Params( kwd ) - managing_samples = util.string_as_bool( params.get( 'managing_samples', False ) ) file_obj = params.get( 'file_data', '' ) try: reader = csv.reader( file_obj.file ) @@ -1137,23 +972,211 @@ class RequestsCommon( BaseController, Us folder_select_field=folder_select_field, field_values=row[3:] ) ) except Exception, e: - status = 'error' - message = 'Error thrown when importing samples file: %s' % str( e ) + if str( e ) == "'unicode' object has no attribute 'file'": + message = "Select a file" + else: + message = 'Error attempting to create samples from selected file: %s.' % str( e ) + message += ' Make sure the selected csv file uses the format: SampleName,DataLibrary,DataLibraryFolder,FieldValue1,FieldValue2...' return trans.response.send_redirect( web.url_for( controller='requests_common', - action='manage_request', + action='add_sample', cntrller=cntrller, - id=trans.security.encode_id( request.id ), - status=status, + request_id=trans.security.encode_id( request.id ), + add_sample_button='Add sample', + status='error', message=message ) ) request_widgets = self.__get_request_widgets( trans, request.id ) sample_copy = self.__build_copy_sample_select_field( trans, current_samples ) - return trans.fill_template( '/requests/common/manage_request.mako', + return trans.fill_template( '/requests/common/edit_samples.mako', cntrller=cntrller, request=request, request_widgets=request_widgets, current_samples=current_samples, sample_copy=sample_copy, - managing_samples=managing_samples ) + editing_samples=False ) + def __save_samples( self, trans, cntrller, request, samples, **kwd ): + # Here we handle saving all new samples added by the user as well as saving + # changes to any subset of the request's samples. + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + editing_samples = util.string_as_bool( params.get( 'editing_samples', False ) ) + is_admin = cntrller == 'requests_admin' and trans.user_is_admin() + sample_operation = params.get( 'sample_operation', 'none' ) + # Check for duplicate sample names within the request + self.__validate_sample_names( trans, cntrller, request, samples, **kwd ) + if editing_samples: + library = None + folder = None + def handle_error( **kwd ): + kwd[ 'status' ] = 'error' + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + **kwd ) ) + # Here we handle saving changes to single samples as well as saving changes to + # selected sets of samples. If samples are selected, the sample_operation param + # will have a value other than 'none', and the samples param will be a list of + # encoded sample ids. There are currently only 2 multi-select operations; + # 'Change state' and 'Select data library and folder'. If sample_operation is + # 'none, then the samples param will be a list of sample objects. + if sample_operation == 'Change state': + sample_state_id = params.get( 'sample_state_id', None ) + if sample_state_id in [ None, 'none' ]: + message = "Select a new state from the <b>Change current state</b> list before clicking the <b>Save</b> button." + kwd[ 'message' ] = message + del kwd[ 'save_changes_button' ] + handle_error( **kwd ) + sample_event_comment = util.restore_text( params.get( 'sample_event_comment', '' ) ) + new_state = trans.sa_session.query( trans.model.SampleState ).get( trans.security.decode_id( sample_state_id ) ) + # Send the encoded sample_ids to update_sample_state. + # TODO: make changes necessary to just send the samples... + encoded_selected_sample_ids = self.__get_encoded_selected_sample_ids( trans, request, **kwd ) + # Make sure all samples have a unique barcode if the state is changing + for sample_index in range( len( samples ) ): + current_sample = samples[ sample_index ] + if current_sample is None: + # We have a None value because the user did not select this sample + # on which to perform the action. + continue + request_sample = request.samples[ sample_index ] + bc_message = self.__validate_barcode( trans, request_sample, current_sample[ 'barcode' ] ) + if bc_message: + #status = 'error' + message += bc_message + kwd[ 'message' ] = message + del kwd[ 'save_samples_button' ] + handle_error( **kwd ) + self.update_sample_state( trans, cntrller, encoded_selected_sample_ids, new_state, comment=sample_event_comment ) + return trans.response.send_redirect( web.url_for( controller='requests_common', + cntrller=cntrller, + action='update_request_state', + request_id=trans.security.encode_id( request.id ) ) ) + elif sample_operation == 'Select data library and folder': + library_id = params.get( 'sample_0_library_id', 'none' ) + folder_id = params.get( 'sample_0_folder_id', 'none' ) + library, folder = self.__get_library_and_folder( trans, library_id, folder_id ) + self.__update_samples( trans, request, samples, **kwd ) + # See if all the samples' barcodes are in the same state, + # and if so send email if configured to. + common_state = request.samples_have_common_state + if common_state and common_state.id == request.type.states[1].id: + event = trans.model.RequestEvent( request, + request.states.SUBMITTED, + "All samples are in %s state." % common_state.name ) + trans.sa_session.add( event ) + trans.sa_session.flush() + request.send_email_notification( trans, request.type.states[1] ) + message = 'Changes made to the samples have been saved. ' + else: + # Saving a newly created sample. + for index in range( len( samples ) - len( request.samples ) ): + sample_index = len( request.samples ) + current_sample = samples[ sample_index ] + form_values = trans.model.FormValues( request.type.sample_form, current_sample[ 'field_values' ] ) + trans.sa_session.add( form_values ) + trans.sa_session.flush() + s = trans.model.Sample( current_sample[ 'name' ], + '', + request, + form_values, + current_sample[ 'barcode' ], + current_sample[ 'library' ], + current_sample[ 'folder' ] ) + trans.sa_session.add( s ) + trans.sa_session.flush() + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + id=trans.security.encode_id( request.id ), + editing_samples=editing_samples, + status=status, + message=message ) ) + def __update_samples( self, trans, request, sample_widgets, **kwd ): + # Determine if the values in kwd require updating the request's samples. The list of + # sample_widgets must have the same number of objects as request.samples, but some of + # the objects can be None. Those that are not None correspond to samples selected by + # the user for performing an action on multiple samples simultaneously. + def handle_error( **kwd ): + kwd[ 'status' ] = 'error' + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + **kwd ) ) + params = util.Params( kwd ) + sample_operation = params.get( 'sample_operation', 'none' ) + if sample_operation != 'none': + # These values will be in kwd if the user checked 1 or more checkboxes for performing this action + # on a set of samples. + library_id = params.get( 'sample_0_library_id', 'none' ) + folder_id = params.get( 'sample_0_folder_id', 'none' ) + for index, obj in enumerate( sample_widgets ): + if obj is not None: + # obj will be None if the user checked sample check boxes and selected an action + # to perform on multiple samples, but did not select certain samples. + sample_updated = False + # If this sample has values in kwd, then kwd will include a + # key whose value is this sample's ( possibly changed ) name. An + # example of this key is 'sample_0_name'. + for k, v in kwd.items(): + name_key = 'sample_%i_name' % index + if k == name_key: + sample_updated = True + break + if sample_updated: + id_index = index + 1 + if sample_operation == 'none': + # We are handling changes to a single sample. + library_id = params.get( 'sample_%i_library_id' % id_index, 'none' ) + folder_id = params.get( 'sample_%i_folder_id' % id_index, 'none' ) + # Update the corresponding sample's values as well as the sample_widget. + sample = request.samples[ index ] + sample.name = util.restore_text( params.get( 'sample_%i_name' % index, '' ) ) + # The bar_code field requires special handling because after a request is submitted, the + # state of a sample cannot be changed without a bar_code assocaited with the sample. + bar_code = util.restore_text( params.get( 'sample_%i_barcode' % index, '' ) ) + if not bar_code and not sample.bar_code: + # If this is a 'new' (still in its first state) sample, create an event + if sample.state.id == request.states[0].id: + event = trans.model.SampleEvent( sample, + request.type.states[1], + 'Sample added to the system' ) + trans.sa_session.add( event ) + trans.sa_session.flush() + elif bar_code: + bc_message = self.__validate_barcode( trans, sample, bar_code ) + if bc_message: + kwd[ 'message' ] = bc_message + del kwd[ 'save_samples_button' ] + handle_error( **kwd ) + sample.bar_code = bar_code + library, folder = self.__get_library_and_folder( trans, library_id, folder_id ) + sample.library = library + sample.folder = folder + field_values = [] + for field_index in range( len( request.type.sample_form.fields ) ): + field_values.append( util.restore_text( params.get( 'sample_%i_field_%i' % ( index, field_index ), '' ) ) ) + form_values = trans.sa_session.query( trans.model.FormValues ).get( sample.values.id ) + form_values.content = field_values + trans.sa_session.add_all( ( sample, form_values ) ) + trans.sa_session.flush() + def __get_library_and_folder( self, trans, library_id, folder_id ): + try: + library = trans.sa_session.query( trans.model.Library ).get( trans.security.decode_id( library_id ) ) + except: + library = None + if library and folder_id == 'none': + folder = library.root_folder + elif library and folder_id != 'none': + try: + folder = trans.sa_session.query( trans.model.LibraryFolder ).get( trans.security.decode_id( folder_id ) ) + except: + if library: + folder = library.root_folder + else: + folder = None + else: + folder = None + return library, folder # ===== Methods for handling form definition widgets ===== def __get_request_widgets( self, trans, id ): """Get the widgets for the request""" @@ -1179,27 +1202,104 @@ class RequestsCommon( BaseController, Us value=request.values.content[ index ], helptext=field[ 'helptext' ] + ' (' + required_label + ')' ) ) return request_widgets - def __get_samples_widgets( self, trans, request, libraries, **kwd ): - """Get the widgets for all of the samples currently associated with the request""" - # The current_samples_widgets list is a list of dictionaries - current_samples_widgets = [] - for index, sample in enumerate( request.samples ): - # Build the library_select_field and folder_select_field for each existing sample - library_select_field, folder_select_field = self.__build_library_and_folder_select_fields( trans, + def __get_sample_widgets( self, trans, request, samples, **kwd ): + """ + Returns a list of dictionaries, each representing the widgets that define a sample on a form. + The widgets are populated from kwd based on the set of samples received. The set of samples + corresponds to a reques.samples list, but if the user checked specific check boxes on the form, + those samples that were not check will have None objects in the list of samples. In this case, + the corresponding sample_widget is populated from the db rather than kwd. + """ + params = util.Params( kwd ) + sample_operation = params.get( 'sample_operation', 'none' ) + # This method is called when the user is adding new samples as well as + # editing existing samples, so we use the editing_samples flag to keep + # track of what's occurring. + editing_samples = util.string_as_bool( params.get( 'editing_samples', False ) ) + sample_widgets = [] + if sample_operation != 'none': + # The sample_operatin param has a value other than 'none', and a specified + # set of samples was received. + library_id = util.restore_text( params.get( 'sample_0_library_id', 'none' ) ) + folder_id = util.restore_text( params.get( 'sample_0_folder_id', 'none' ) ) + # Build the list of widgets which will be used to render each sample row on the request page + if not request: + return sample_widgets + # Get the list of libraries for which the current user has permission to add items. + libraries = request.user.accessible_libraries( trans, [ trans.app.security_agent.permitted_actions.LIBRARY_ADD ] ) + # Build the list if sample widgets, populating the values from kwd. + for index, sample in enumerate( samples ): + id_index = index + 1 + if sample is None: + # Id sample is None, then we'll use the sample from the request object since it will + # not have updated =values from kwd. + sample = request.samples[ index ] + name = sample.name + bar_code = sample.bar_code + library = sample.library + folder = sample.folder + field_values = sample.values.content, + else: + # Update the sample attributes from kwd + name = util.restore_text( params.get( 'sample_%i_name' % index, sample.name ) ) + bar_code = util.restore_text( params.get( 'sample_%i_barcode' % index, sample.bar_code ) ) + library_id = util.restore_text( params.get( 'sample_%i_library_id' % id_index, '' ) ) + if not library_id and sample.library: + library_id = trans.security.encode_id( sample.library.id ) + folder_id = util.restore_text( params.get( 'sample_%i_folder_id' % id_index, '' ) ) + if not folder_id and sample.folder: + folder_id = trans.security.encode_id( sample.folder.id ) + library, folder = self.__get_library_and_folder( trans, library_id, folder_id ) + field_values = [] + for field_index in range( len( request.type.sample_form.fields ) ): + field_values.append( util.restore_text( params.get( 'sample_%i_field_%i' % ( index, field_index ), '' ) ) ) + library_select_field, folder_select_field = self.__build_library_and_folder_select_fields( trans=trans, user=request.user, - sample_index=index, + sample_index=id_index, libraries=libraries, sample=sample, + library_id=library_id, + folder_id=folder_id, **kwd ) - # Append the dictionary for the current sample to the current_samples_widgets list - current_samples_widgets.append( dict( name=sample.name, - barcode=sample.bar_code, - library=sample.library, - folder=sample.folder, - field_values=sample.values.content, - library_select_field=library_select_field, - folder_select_field=folder_select_field ) ) - return current_samples_widgets + sample_widgets.append( dict( name=name, + barcode=bar_code, + library=library, + folder=folder, + field_values=field_values, + library_select_field=library_select_field, + folder_select_field=folder_select_field ) ) + # There may be additional new samples on the form that have not yet been associated with the request. + # TODO: factor this code so it is not duplicating what's above. + index = len( samples ) + while True: + name = util.restore_text( params.get( 'sample_%i_name' % index, '' ) ) + if not name: + break + id_index = index + 1 + bar_code = util.restore_text( params.get( 'sample_%i_barcode' % index, '' ) ) + library_id = util.restore_text( params.get( 'sample_%i_library_id' % id_index, '' ) ) + folder_id = util.restore_text( params.get( 'sample_%i_folder_id' % id_index, '' ) ) + library, folder = self.__get_library_and_folder( trans, library_id, folder_id ) + field_values = [] + for field_index in range( len( request.type.sample_form.fields ) ): + field_values.append( util.restore_text( params.get( 'sample_%i_field_%i' % ( index, field_index ), '' ) ) ) + library_select_field, folder_select_field = self.__build_library_and_folder_select_fields( trans=trans, + user=request.user, + sample_index=id_index, + libraries=libraries, + sample=None, + library_id=library_id, + folder_id=folder_id, + **kwd ) + sample_widgets.append( dict( name=name, + barcode=bar_code, + library=library, + folder=folder, + field_values=field_values, + library_select_field=library_select_field, + folder_select_field=folder_select_field ) ) + index += 1 + return sample_widgets # ===== Methods for building SelectFields used on various request forms ===== def __build_copy_sample_select_field( self, trans, current_samples ): copy_sample_index_select_field = SelectField( 'copy_sample_index' ) @@ -1240,26 +1340,30 @@ class RequestsCommon( BaseController, Us # The libraries dictionary looks like: { library : '1,2' }, library : '3' }. Its keys are the libraries that # should be displayed for the current user and its values are strings of comma-separated folder ids that should # NOT be displayed. - # - # TODO: all object ids received in the params must be encoded. params = util.Params( kwd ) library_select_field_name= "sample_%i_library_id" % sample_index folder_select_field_name = "sample_%i_folder_id" % sample_index if not library_id: - library_id = params.get( library_select_field_name, 'none' ) + library_id = params.get( library_select_field_name, None ) + if not folder_id: + folder_id = params.get( folder_select_field_name, None ) selected_library = None selected_hidden_folder_ids = [] showable_folders = [] - if sample and sample.library and library_id == 'none': - library_id = str( sample.library.id ) + if library_id not in [ None, 'none' ]: + # If we have a selected library, get the list of it's folders that are not accessible to the current user + for library, hidden_folder_ids in libraries.items(): + encoded_id = trans.security.encode_id( library.id ) + if encoded_id == str( library_id ): + selected_library = library + selected_hidden_folder_ids = hidden_folder_ids.split( ',' ) + break + elif sample and sample.library and library_id == 'none': + # The user previously selected a library but is now resetting the selection to 'none' + selected_library = None + elif sample and sample.library: + library_id = trans.security.encode_id( sample.library.id ) selected_library = sample.library - # If we have a selected library, get the list of it's folders that are not accessible to the current user - for library, hidden_folder_ids in libraries.items(): - encoded_id = trans.security.encode_id( library.id ) - if encoded_id == str( library_id ): - selected_library = library - selected_hidden_folder_ids = hidden_folder_ids.split( ',' ) - break # sample_%i_library_id SelectField with refresh on change enabled library_select_field = build_select_field( trans, libraries.keys(), @@ -1275,20 +1379,12 @@ class RequestsCommon( BaseController, Us selected_library, [ trans.app.security_agent.permitted_actions.LIBRARY_ADD ], selected_hidden_folder_ids ) - if sample: - # The user is editing the request, and may have previously selected a folder - if sample.folder: - selected_folder_id = sample.folder.id + if folder_id: + selected_folder_id = folder_id + elif sample and sample.folder: + selected_folder_id = trans.security.encode_id( sample.folder.id ) else: - # If a library is selected but not a folder, use the library's root folder - if sample.library: - selected_folder_id = sample.library.root_folder.id - else: - # The user just selected a folder - selected_folder_id = params.get( folder_select_field_name, 'none' ) - elif folder_id: - # TODO: not sure when this would be passed - selected_folder_id = folder_id + selected_folder_id = trans.security.encode_id( selected_library.root_folder.id ) else: selected_folder_id = 'none' # TODO: Change the name of the library root folder to "Library root" to clarify to the @@ -1328,9 +1424,35 @@ class RequestsCommon( BaseController, Us message += '<b>' + ef + '</b> ' return message return None + def __validate_sample_names( self, trans, cntrller, request, current_samples, **kwd ): + # Check for duplicate sample names for all samples of the request. + editing_samples = util.string_as_bool( kwd.get( 'editing_samples', False ) ) + message = '' + for index in range( len( current_samples ) - len( request.samples ) ): + sample_index = index + len( request.samples ) + current_sample = current_samples[ sample_index ] + sample_name = current_sample[ 'name' ] + if not sample_name.strip(): + message = 'Enter the name of sample number %i' % sample_index + break + count = 0 + for i in range( len( current_samples ) ): + if sample_name == current_samples[ i ][ 'name' ]: + count += 1 + if count > 1: + message = "You tried to add %i samples with the name (%s). Samples belonging to a request must have unique names." % ( count, sample_name ) + break + if message: + del kwd[ 'save_samples_button' ] + kwd[ 'message' ] = message + kwd[ 'status' ] = 'error' + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller=cntrller, + **kwd ) ) def __validate_barcode( self, trans, sample, barcode ): """ - Makes sure that the barcode about to be assigned to a sample is gobally unique. + Makes sure that the barcode about to be assigned to a sample is globally unique. That is, barcodes must be unique across requests in Galaxy sample tracking. """ message = '' @@ -1338,13 +1460,18 @@ class RequestsCommon( BaseController, Us for index in range( len( sample.request.samples ) ): # Check for empty bar code if not barcode.strip(): - message = 'Fill in the barcode for sample (%s).' % sample.name - break + if sample.state.id == sample.request.type.states[0].id: + # The user has not yet filled in the barcode value, but the sample is + # 'new', so all is well. + break + else: + message = "Fill in the barcode for sample (%s) before changing it's state." % sample.name + break # TODO: Add a unique constraint to sample.bar_code table column # Make sure bar code is unique - for sample_has_bar_code in trans.sa_session.query( trans.model.Sample ) \ - .filter( trans.model.Sample.table.c.bar_code == barcode ): - if sample_has_bar_code and sample_has_bar_code.id != sample.id: + for sample_with_barcode in trans.sa_session.query( trans.model.Sample ) \ + .filter( trans.model.Sample.table.c.bar_code == barcode ): + if sample_with_barcode and sample_with_barcode.id != sample.id: message = '''The bar code (%s) associated with the sample (%s) belongs to another sample. Bar codes must be unique across all samples, so use a different bar code for this sample.''' % ( barcode, sample.name ) @@ -1360,15 +1487,15 @@ class RequestsCommon( BaseController, Us elif len( email ) > 255: error = "(%s) exceeds maximum allowable length. " % str( email ) return error - # ===== Other miscellaneoud utility methods ===== - def __get_selected_samples( self, trans, request, **kwd ): - selected_samples = [] + # ===== Other miscellaneous utility methods ===== + def __get_encoded_selected_sample_ids( self, trans, request, **kwd ): + encoded_selected_sample_ids = [] for sample in request.samples: if CheckboxField.is_checked( kwd.get( 'select_sample_%i' % sample.id, '' ) ): - selected_samples.append( trans.security.encode_id( sample.id ) ) - return selected_samples + encoded_selected_sample_ids.append( trans.security.encode_id( sample.id ) ) + return encoded_selected_sample_ids -# ===== Miscellaneoud utility methods outside of the RequestsCommon class ===== +# ===== Miscellaneous utility methods outside of the RequestsCommon class ===== def invalid_id_redirect( trans, cntrller, obj_id, action='browse_requests' ): status = 'error' message = "Invalid request id (%s)" % str( obj_id ) --- a/templates/requests/common/dataset_transfer.mako +++ b/templates/requests/common/dataset_transfer.mako @@ -1,47 +1,46 @@ <%inherit file="/base.mako"/><%namespace file="/message.mako" import="render_msg" /> +<br/><br/> + +<ul class="manage-table-actions"> + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">Refresh</a></li> + <li><a class="action-button" href="${h.url_for( controller='library_common', action='browse_library', cntrller=cntrller, id=trans.security.encode_id( sample.library.id ) )}">Target Data Library</a></li> + <li><a class="action-button" href="${h.url_for( controller='requests_common', action='view_request', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}">Browse this request</a></li> +</ul> + %if message: ${render_msg( message, status )} %endif -<h2>Datasets of Sample "${sample.name}"</h2> - -<ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">Refresh</a> - </li> - <li> - <a class="action-button" href="${h.url_for( controller='library_common', action='browse_library', cntrller=cntrller, id=trans.security.encode_id( sample.library.id ) )}">${sample.library.name} Data Library</a> - </li> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ) )}">Browse this request</a> - </li> -</ul> - -%if dataset_files: - <div class="form-row"> - <table class="grid"> - <thead> - <tr> - <th>Name</th> - <th>Size</th> - <th>Status</th> - </tr> - <thead> - <tbody> - %for dataset_file in dataset_files: - <tr> - <td>${dataset_file.name}</td> - <td>${dataset_file.size}</td> - <td>${dataset_file.status}</td> - </tr> - %endfor - </tbody> - </table> +<div class="toolForm"> + <div class="toolFormTitle">Sample "${sample.name}"</div> + <div class="toolFormBody"> + %if dataset_files: + <div class="form-row"> + <table class="grid"> + <thead> + <tr> + <th>Name</th> + <th>Size</th> + <th>Status</th> + </tr> + <thead> + <tbody> + %for dataset_file in dataset_files: + <tr> + <td>${dataset_file.name}</td> + <td>${dataset_file.size}</td> + <td>${dataset_file.status}</td> + </tr> + %endfor + </tbody> + </table> + </div> + %else: + <div class="form-row"> + There are no datasets associated with this sample. + </div> + %endif </div> -%else: - <div class="form-row"> - There are no datasets associated with this sample. - </div> -%endif +</div> --- a/templates/requests/common/manage_request.mako +++ /dev/null @@ -1,615 +0,0 @@ -<%inherit file="/base.mako"/> -<%namespace file="/message.mako" import="render_msg" /> -<%namespace file="/requests/common/sample_state.mako" import="render_sample_state" /> -<%namespace file="/requests/common/sample_datasets.mako" import="render_sample_datasets" /> - -<%def name="stylesheets()"> - ${parent.stylesheets()} - ${h.css( "library" )} -</%def> - -<%def name="javascripts()"> - ${parent.javascripts()} - <script type="text/javascript"> - function showContent(vThis) - { - // http://www.javascriptjunkie.com - // alert(vSibling.className + " " + vDef_Key); - vParent = vThis.parentNode; - vSibling = vParent.nextSibling; - while (vSibling.nodeType==3) { - // Fix for Mozilla/FireFox Empty Space becomes a TextNode or Something - vSibling = vSibling.nextSibling; - }; - if(vSibling.style.display == "none") - { - vThis.src="/static/images/fugue/toggle.png"; - vThis.alt = "Hide"; - vSibling.style.display = "block"; - } else { - vSibling.style.display = "none"; - vThis.src="/static/images/fugue/toggle-expand.png"; - vThis.alt = "Show"; - } - return; - } - - $(document).ready(function(){ - //hide the all of the element with class msg_body - $(".msg_body").hide(); - //toggle the component with class msg_body - $(".msg_head").click(function(){ - $(this).next(".msg_body").slideToggle(0); - }); - }); - - // Looks for changes in sample states using an async request. Keeps - // calling itself (via setTimeout) until all samples are in a terminal - // state. - var updater = function ( sample_states ) { - // Check if there are any items left to track - var empty = true; - for ( i in sample_states ) { - empty = false; - break; - } - if ( ! empty ) { - setTimeout( function() { updater_callback( sample_states ) }, 1000 ); - } - }; - - var updater_callback = function ( sample_states ) { - // Build request data - var ids = [] - var states = [] - $.each( sample_states, function ( id, state ) { - ids.push( id ); - states.push( state ); - }); - // Make ajax call - $.ajax( { - type: "POST", - url: "${h.url_for( controller='requests_common', action='sample_state_updates' )}", - dataType: "json", - data: { ids: ids.join( "," ), states: states.join( "," ) }, - success : function ( data ) { - $.each( data, function( id, val ) { - // Replace HTML - var cell1 = $("#sampleState-" + id); - cell1.html( val.html_state ); - var cell2 = $("#sampleDatasets-" + id); - cell2.html( val.html_datasets ); - sample_states[ parseInt( id ) ] = val.state; - }); - updater( sample_states ); - }, - error: function() { - // Just retry, like the old method, should try to be smarter - updater( sample_states ); - } - }); - }; - - function checkAllFields() - { - var chkAll = document.getElementById('checkAll'); - var checks = document.getElementsByTagName('input'); - var boxLength = checks.length; - var allChecked = false; - var totalChecked = 0; - if ( chkAll.checked == true ) - { - for ( i=0; i < boxLength; i++ ) - { - if ( checks[i].name.indexOf( 'select_sample_' ) != -1) - { - checks[i].checked = true; - } - } - } - else - { - for ( i=0; i < boxLength; i++ ) - { - if ( checks[i].name.indexOf( 'select_sample_' ) != -1) - { - checks[i].checked = false - } - } - } - } - - function stopRKey(evt) { - var evt = (evt) ? evt : ((event) ? event : null); - var node = (evt.target) ? evt.target : ((evt.srcElement) ? evt.srcElement : null); - if ((evt.keyCode == 13) && (node.type=="text")) {return false;} - } - document.onkeypress = stopRKey - </script> -</%def> - -<% is_admin = cntrller == 'requests_admin' and trans.user_is_admin() %> - -<div class="grid-header"> - <h2>Sequencing Request "${request.name}"</h2> - - <ul class="manage-table-actions"> - <li><a class="action-button" id="seqreq-${request.id}-popup" class="menubutton">Sequencing Request Actions</a></li> - <div popupmenu="seqreq-${request.id}-popup"> - %if request.is_unsubmitted and request.samples: - <a class="action-button" confirm="More samples cannot be added to this request once it is submitted. Click OK to submit." href="${h.url_for( controller='requests_common', action='submit_request', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Submit</a> - %endif - <a class="action-button" href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">History</a> - %if is_admin: - %if request.is_submitted: - <a class="action-button" href="${h.url_for( controller='requests_admin', action='reject', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Reject</a> - <a class="action-button" href="${h.url_for( controller='requests_admin', action='get_data', request_id=trans.security.encode_id( request.id ) )}">Select datasets to transfer</a> - %endif - %endif - </div> - <li><a class="action-button" href="${h.url_for( controller=cntrller, action='browse_requests' )}">Browse requests</a></li> - </ul> - - <div class="toolParamHelp" style="clear: both;"> - <b>Sequencer</b>: ${request.type.name} - %if is_admin: - | <b>User</b>: ${request.user.email} - %endif - %if request.is_submitted: - | <b>State</b>: <i>${request.state}</i> - %else: - | <b>State</b>: ${request.state} - %endif - </div> -</div> - -%if request.samples_without_library_destinations: - ${render_msg( "Select a target data library and folder for all the samples before starting the sequence run", "warning" )} -%endif - -%if request.is_rejected: - ${render_msg( "Reason for rejection: " + request.last_comment, "warning" )} -%endif - -%if message: - ${render_msg( message, status )} -%endif - -<h4><img src="/static/images/fugue/toggle-expand.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> Request Information</h4> -<div style="display:none;"> - <div class="form-row"> - <ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller='requests_common', action='edit_basic_request_info', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">Edit request informaton</a> - </li> - </ul> - </div> - <table class="grid" border="0"> - <tbody> - <tr> - <td valign="top" width="50%"> - <div class="form-row"> - <label>Description:</label> - ${request.desc} - </div> - <div style="clear: both"></div> - %for index, rd in enumerate( request_widgets ): - <% - field_label = rd[ 'label' ] - field_value = rd[ 'value' ] - %> - <div class="form-row"> - <label>${field_label}:</label> - %if field_label == 'State': - <a href="${h.url_for( controller='requests_common', action='request_events', cntrller=cntrller, id=trans.security.encode_id( request.id ) )}">${field_value}</a> - %else: - ${field_value} - %endif - </div> - <div style="clear: both"></div> - %endfor - </td> - <td valign="top" width="50%"> - <div class="form-row"> - <label>Date created:</label> - ${request.create_time} - </div> - <div class="form-row"> - <label>Date updated:</label> - ${request.update_time} - </div> - <div class="form-row"> - <label>Email notification recipients:</label> - <% - if request.notification: - emails = ', '.join( request.notification[ 'email' ] ) - else: - emails = '' - %> - ${emails} - </div> - <div style="clear: both"></div> - <div class="form-row"> - <label>Email notification on sample states:</label> - <% - if request.notification: - states = [] - for ss in request.type.states: - if ss.id in request.notification[ 'sample_states' ]: - states.append( ss.name ) - states = ', '.join( states ) - else: - states = '' - %> - ${states} - </div> - <div style="clear: both"></div> - </td> - </tr> - </tbody> - </table> -</div> -<br/> -<form id="manage_request" name="manage_request" action="${h.url_for( controller='requests_common', action='manage_request', cntrller=cntrller, id=trans.security.encode_id( request.id ), managing_samples=managing_samples )}" method="post"> - %if current_samples: - <% sample_operation_selected_value = sample_operation_select_field.get_selected( return_value=True ) %> - ## first render the basic info grid - ${render_basic_info_grid()} - %if not request.is_new and not managing_samples and len( sample_operation_select_field.options ) > 1: - <div class="form-row" style="background-color:#FAFAFA;"> - For selected samples: - ${sample_operation_select_field.get_html()} - </div> - %if sample_operation_selected_value != 'none' and selected_samples: - <div class="form-row" style="background-color:#FAFAFA;"> - %if sample_operation_selected_value == trans.model.Sample.bulk_operations.CHANGE_STATE: - ## sample_operation_selected_value == 'Change state' - <div class="form-row"> - <label>Change current state</label> - ${sample_state_id_select_field.get_html()} - <label>Comments</label> - <input type="text" name="sample_event_comment" value=""/> - <div class="toolParamHelp" style="clear: both;"> - Optional - </div> - </div> - <div class="form-row"> - <input type="submit" name="change_state_button" value="Save"/> - <input type="submit" name="cancel_change_state_button" value="Cancel"/> - </div> - %elif sample_operation_selected_value == trans.app.model.Sample.bulk_operations.SELECT_LIBRARY: - <% libraries_selected_value = libraries_select_field.get_selected( return_value=True ) %> - <div class="form-row"> - <label>Select data library:</label> - ${libraries_select_field.get_html()} - </div> - %if libraries_selected_value != 'none': - <div class="form-row"> - <label>Select folder:</label> - ${folders_select_field.get_html()} - </div> - <div class="form-row"> - <input type="submit" name="change_lib_button" value="Save"/> - <input type="submit" name="cancel_change_lib_button" value="Cancel"/> - </div> - %endif - %endif - </div> - %endif - %endif - ## Render the other grids - <% trans.sa_session.refresh( request.type.sample_form ) %> - %for grid_index, grid_name in enumerate( request.type.sample_form.layout ): - ${render_grid( grid_index, grid_name, request.type.sample_form.fields_of_grid( grid_index ) )} - %endfor - %else: - <label>There are no samples.</label> - %endif - %if request.samples and request.is_submitted: - <script type="text/javascript"> - // Updater - updater({${ ",".join( [ '"%s" : "%s"' % ( s.id, s.state.name ) for s in request.samples ] ) }}); - </script> - %endif - %if not managing_samples: - <table class="grid"> - <tbody> - <tr> - <div class="form-row"> - %if request.is_unsubmitted: - <td> - %if current_samples: - <label>Copy </label> - <input type="integer" name="num_sample_to_copy" value="1" size="3"/> - <label>samples from sample</label> - ${sample_copy.get_html()} - %endif - <input type="submit" name="add_sample_button" value="Add New"/> - </td> - %endif - <td> - %if current_samples and len( current_samples ) <= len( request.samples ): - <input type="submit" name="edit_samples_button" value="Edit samples"/> - %endif - </td> - </div> - </tr> - </tbody> - </table> - %endif - %if request.samples or current_samples: - %if managing_samples: - <div class="form-row"> - <input type="submit" name="save_samples_button" value="Save"/> - <input type="submit" name="cancel_changes_button" value="Cancel"/> - </div> - %elif len( current_samples ) > len( request.samples ): - <div class="form-row"> - <input type="submit" name="save_samples_button" value="Save"/> - <input type="submit" name="cancel_changes_button" value="Cancel"/> - </div> - %endif - %endif -</form> -<br/> -%if request.is_unsubmitted: - <form id="import" name="import" action="${h.url_for( controller='requests_common', action='manage_request', managing_samples=managing_samples, id=trans.security.encode_id( request.id ) )}" enctype="multipart/form-data" method="post" > - <h4><img src="/static/images/fugue/toggle-expand.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> Import samples</h4> - <div style="display:none;"> - <input type="file" name="file_data" /> - <input type="submit" name="import_samples_button" value="Import samples"/> - <br/> - <div class="toolParamHelp" style="clear: both;"> - The csv file must be in the following format:<br/> - SampleName,DataLibrary,DataLibraryFolder,FieldValue1,FieldValue2... - </div> - </div> - </form> -%endif - -<%def name="render_grid( grid_index, grid_name, fields_dict )"> - <br/> - <% if not grid_name: - grid_name = "Grid "+ grid_index - %> - <div> - %if managing_samples or len( current_samples ) > len( request.samples ): - <h4><img src="/static/images/fugue/toggle.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> ${grid_name}</h4> - <div> - %else: - <h4><img src="/static/images/fugue/toggle-expand.png" alt="Hide" onclick="showContent(this);" style="cursor:pointer;"/> ${grid_name}</h4> - <div style="display:none;"> - %endif - <table class="grid"> - <thead> - <tr> - <th>Name</th> - %for index, field in fields_dict.items(): - <th> - ${field['label']} - <div class="toolParamHelp" style="clear: both;"> - <i>${field['helptext']}</i> - </div> - </th> - %endfor - <th></th> - </tr> - <thead> - <tbody> - <% trans.sa_session.refresh( request ) %> - %for sample_index, sample in enumerate( current_samples ): - %if managing_samples: - <tr>${render_sample_form( sample_index, sample['name'], sample['field_values'], fields_dict)}</tr> - %else: - <tr> - %if sample_index in range( len( request.samples ) ): - ${render_sample( sample_index, sample['name'], sample['field_values'], fields_dict )} - %else: - ${render_sample_form( sample_index, sample['name'], sample['field_values'], fields_dict)} - %endif - </tr> - %endif - %endfor - </tbody> - </table> - </div> - </div> -</%def> - -## This function displays the "Basic Information" grid -<%def name="render_basic_info_grid()"> - <h3>Sample Information</h3> - <table class="grid"> - <thead> - <tr> - <th><input type="checkbox" id="checkAll" name=select_all_samples_checkbox value="true" onclick='checkAllFields(1);'><input type="hidden" name=select_all_samples_checkbox value="true"></th> - <th>Name</th> - <th>Barcode</th> - <th>State</th> - <th>Data Library</th> - <th>Folder</th> - %if request.is_submitted or request.is_complete: - <th>Datasets Transferred</th> - %endif - <th></th> - </tr> - <thead> - <tbody> - <% trans.sa_session.refresh( request ) %> - %for sample_index, info in enumerate( current_samples ): - <% - if sample_index in range( len(request.samples ) ): - sample = request.samples[sample_index] - else: - sample = None - %> - %if managing_samples: - <tr>${show_basic_info_form( sample_index, sample, info )}</tr> - %else: - <tr> - %if sample_index in range( len( request.samples ) ): - %if trans.security.encode_id( sample.id ) in selected_samples: - <td><input type="checkbox" name=select_sample_${sample.id} id="sample_checkbox" value="true" checked><input type="hidden" name=select_sample_${sample.id} id="sample_checkbox" value="true"></td> - %else: - <td><input type="checkbox" name=select_sample_${sample.id} id="sample_checkbox" value="true"><input type="hidden" name=select_sample_${sample.id} id="sample_checkbox" value="true"></td> - %endif - <td>${info['name']}</td> - <td>${info['barcode']}</td> - %if sample.request.is_unsubmitted: - <td>Unsubmitted</td> - %else: - <td><a id="sampleState-${sample.id}" href="${h.url_for( controller='requests_common', action='sample_events', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${render_sample_state( sample )}</a></td> - %endif - %if info['library']: - %if cntrller == 'requests': - <td><a href="${h.url_for( controller='library_common', action='browse_library', cntrller='library', id=trans.security.encode_id( info['library'].id ) )}">${info['library'].name}</a></td> - %elif is_admin: - <td><a href="${h.url_for( controller='library_common', action='browse_library', cntrller='library_admin', id=trans.security.encode_id( info['library'].id ) )}">${info['library'].name}</a></td> - %endif - %else: - <td></td> - %endif - %if info['folder']: - <td>${info['folder'].name}</td> - %else: - <td></td> - %endif - %if request.is_submitted or request.is_complete: - <td><a id="sampleDatasets-${sample.id}" href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}"> - ${render_sample_datasets( sample )} - </a></td> - %endif - %else: - ${show_basic_info_form( sample_index, sample, info )} - %endif - %if request.is_unsubmitted or request.is_rejected: - <td> - %if sample: - %if sample.request.is_unsubmitted: - <a class="action-button" href="${h.url_for( controller='requests_common', cntrller=cntrller, action='delete_sample', request_id=trans.security.encode_id( request.id ), sample_id=sample_index )}"><img src="${h.url_for('/static/images/delete_icon.png')}" style="cursor:pointer;"/></a> - %endif - %endif - </td> - %endif - </tr> - %endif - %endfor - </tbody> - </table> -</%def> - -<%def name="show_basic_info_form( sample_index, sample, info )"> - <td></td> - <td> - <input type="text" name="sample_${sample_index}_name" value="${info['name']}" size="10"/> - <div class="toolParamHelp" style="clear: both;"> - <i>${' (required)' }</i> - </div> - </td> - %if cntrller == 'requests': - %if sample: - %if sample.request.is_unsubmitted: - <td></td> - %else: - <td><input type="text" name="sample_${sample_index}_barcode" value="${info['barcode']}" size="10"/></td> - %endif - %else: - <td></td> - %endif - %elif is_admin: - %if sample: - %if sample.request.is_unsubmitted: - <td></td> - %else: - <td><input type="text" name="sample_${sample_index}_barcode" value="${info['barcode']}" size="10"/></td> - %endif - %else: - <td></td> - %endif - %endif - %if sample: - %if sample.request.is_unsubmitted: - <td>Unsubmitted</td> - %else: - <td><a href="${h.url_for( controller='requests_common', action='sample_events', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${sample.state.name}</a></td> - %endif - %else: - <td></td> - %endif - <td>${info['library_select_field'].get_html()}</td> - <td>${info['folder_select_field'].get_html()}</td> - %if request.is_submitted or request.is_complete: - <% - if sample: - label = str( len( sample.datasets ) ) - else: - label = 'Add' - %> - <td><a href="${h.url_for( controller='requests_common', action='view_dataset_transfer', cntrller=cntrller, sample_id=trans.security.encode_id( sample.id ) )}">${label}</a></td> - %endif -</%def> - -<%def name="render_sample( index, sample_name, sample_values, fields_dict )"> - <td> - ${sample_name} - </td> - %for field_index, field in fields_dict.items(): - <td> - %if sample_values[field_index]: - %if field['type'] == 'WorkflowField': - %if str(sample_values[field_index]) != 'none': - <% workflow = trans.sa_session.query( trans.app.model.StoredWorkflow ).get( int(sample_values[field_index]) ) %> - <a href="${h.url_for( controller='workflow', action='run', id=trans.security.encode_id(workflow.id) )}">${workflow.name}</a> - %endif - %else: - ${sample_values[field_index]} - %endif - %else: - <i>None</i> - %endif - </td> - %endfor -</%def> - -<%def name="render_sample_form( index, sample_name, sample_values, fields_dict )"> - <td>${sample_name}</td> - %for field_index, field in fields_dict.items(): - <td> - %if field['type'] == 'TextField': - <input type="text" name="sample_${index}_field_${field_index}" value="${sample_values[field_index]}" size="7"/> - %elif field['type'] == 'SelectField': - <select name="sample_${index}_field_${field_index}" last_selected_value="2"> - %for option_index, option in enumerate(field['selectlist']): - %if option == sample_values[field_index]: - <option value="${option}" selected>${option}</option> - %else: - <option value="${option}">${option}</option> - %endif - %endfor - </select> - %elif field['type'] == 'WorkflowField': - <select name="sample_${index}_field_${field_index}"> - %if str(sample_values[field_index]) == 'none': - <option value="none" selected>Select one</option> - %else: - <option value="none">Select one</option> - %endif - %for option_index, option in enumerate(request.user.stored_workflows): - %if not option.deleted: - %if str(option.id) == str(sample_values[field_index]): - <option value="${option.id}" selected>${option.name}</option> - %else: - <option value="${option.id}">${option.name}</option> - %endif - %endif - %endfor - </select> - %elif field['type'] == 'CheckboxField': - <input type="checkbox" name="sample_${index}_field_${field_index}" value="Yes"/> - %endif - <div class="toolParamHelp" style="clear: both;"> - <i>${'('+field['required']+')' }</i> - </div> - </td> - %endfor -</%def>
participants (1)
-
commits-noreply@bitbucket.org