galaxy-dist commit b7ac22fab158: First pass of bug fixes, anbd additional UI cleanup for Sample Tracking.
# 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 1288713844 14400 # Node ID b7ac22fab1588a565a07acc4972564c2b9198489 # Parent 667043341e81261a7fe587daa475110486b15292 First pass of bug fixes, anbd additional UI cleanup for Sample Tracking. --- a/templates/requests/common/edit_samples.mako +++ b/templates/requests/common/edit_samples.mako @@ -73,9 +73,10 @@ 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_or_delete_samples = request.samples and not is_complete can_edit_request = ( is_admin and not request.is_complete ) or request.is_unsubmitted can_reject_or_transfer = is_admin and request.is_submitted + can_submit = request.samples and is_unsubmitted %><br/><br/> @@ -87,7 +88,7 @@ %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: + %if can_submit: <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> @@ -209,12 +210,12 @@ 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 + ##%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: --- a/lib/galaxy/web/controllers/requests.py +++ b/lib/galaxy/web/controllers/requests.py @@ -36,6 +36,12 @@ class Requests( BaseController ): action='edit_basic_request_info', cntrller='requests', **kwd ) ) + if operation == "edit_samples": + kwd[ 'editing_samples' ] = True + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller='requests', + **kwd ) ) if operation == "view_request": return trans.response.send_redirect( web.url_for( controller='requests_common', action='view_request', --- a/lib/galaxy/web/controllers/requests_common.py +++ b/lib/galaxy/web/controllers/requests_common.py @@ -46,17 +46,6 @@ class RequestsGrid( grids.Grid ): .filter( model.RequestEvent.table.c.id.in_( select( columns=[ func.max( model.RequestEvent.table.c.id ) ], from_obj=model.RequestEvent.table, group_by=model.RequestEvent.table.c.request_id ) ) ) - def get_accepted_filters( self ): - """ Returns a list of accepted filters for this column. """ - # TODO: is this method necessary? - accepted_filter_labels_and_vals = [ model.Request.states.get( state ) for state in model.Request.states ] - accepted_filter_labels_and_vals.append( "All" ) - accepted_filters = [] - for val in accepted_filter_labels_and_vals: - label = val.lower() - args = { self.key: val } - accepted_filters.append( grids.GridColumnFilter( label, args ) ) - return accepted_filters # Grid definition title = "Sequencing Requests" @@ -64,7 +53,6 @@ class RequestsGrid( grids.Grid ): model_class = model.Request default_sort_key = "-update_time" num_rows_per_page = 50 - preserve_state = True use_paging = True default_filter = dict( state="All", deleted="False" ) columns = [ @@ -379,14 +367,24 @@ class RequestsCommon( BaseController, Us sample_event_comment = "" event = trans.model.RequestEvent( request, request.states.SUBMITTED, sample_event_comment ) trans.sa_session.add( event ) - # change the state of each of the samples of thus request - new_state = request.type.states[0] + # Change the state of each of the samples of this request + # request.type.states is the list of SampleState objects configured + # by the admin for this RequestType. + trans.sa_session.add( event ) + trans.sa_session.flush() + # Samples will not have an associated SampleState until the request is submitted, at which + # time all samples of the request will be set to the first SampleState configured for the + # request's RequestType configured by the admin. + initial_sample_state_after_request_submitted = request.type.states[0] for sample in request.samples: - event = trans.model.SampleEvent( sample, new_state, 'Samples created.' ) + event_comment = 'Request submitted and sample state set to %s.' % request.type.states[0].name + event = trans.model.SampleEvent( sample, + initial_sample_state_after_request_submitted, + event_comment ) trans.sa_session.add( event ) trans.sa_session.add( request ) trans.sa_session.flush() - request.send_email_notification( trans, new_state ) + request.send_email_notification( trans, initial_sample_state_after_request_submitted ) message = 'The request has been submitted.' return trans.response.send_redirect( web.url_for( controller=cntrller, action='browse_requests', @@ -519,8 +517,6 @@ class RequestsCommon( BaseController, Us try: request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( id ) ) except: - message += "Invalid request ID (%s). " % str( id ) - status = 'error' ok_for_now = False if ok_for_now: request.deleted = True @@ -549,8 +545,6 @@ class RequestsCommon( BaseController, Us try: request = trans.sa_session.query( trans.model.Request ).get( trans.security.decode_id( id ) ) except: - message += "Invalid request ID (%s). " % str( id ) - status = 'error' ok_for_now = False if ok_for_now: request.deleted = False @@ -903,6 +897,7 @@ class RequestsCommon( BaseController, Us action='edit_samples', cntrller=cntrller, id=trans.security.encode_id( sample.request.id ), + editing_samples=True, status=status, message=message ) ) if is_admin: @@ -1024,7 +1019,7 @@ class RequestsCommon( BaseController, Us 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' ] + del kwd[ 'save_samples_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 ) ) @@ -1056,32 +1051,38 @@ class RequestsCommon( BaseController, Us 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] ) + # Samples will not have an associated SampleState until the request is submitted, at which + # time all samples of the request will be set to the first SampleState configured for the + # request's RequestType defined by the admin. + if request.is_submitted: + # 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. + # Saving a newly created sample. The sample will not have an associated SampleState + # until the request is submitted, at which time all samples of the request will be + # set to the first SampleState configured for the request's RequestType configured + # by the admin ( i.e., the sample's SampleState would be set to request.type.states[0] ). 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' ] ) + s = trans.model.Sample( name=current_sample[ 'name' ], + desc='', + request=request, + form_values=form_values, + bar_code='', + library=current_sample[ 'library' ], + folder=current_sample[ 'folder' ] ) trans.sa_session.add( s ) trans.sa_session.flush() return trans.response.send_redirect( web.url_for( controller='requests_common', @@ -1113,49 +1114,55 @@ class RequestsCommon( BaseController, Us 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' ) + sample = request.samples[ index ] + # See if any values in kwd are different from the values already associated with this sample. + 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. + 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 associated with the sample. Bar + # codes can only be added to a sample after the request is submitted. Also, a samples will + # not have an associated SampleState until the request is submitted, at which time the sample + # is automatically associated with the first SamplesState configured by the admin for the + # request's RequestType. + bar_code = util.restore_text( params.get( 'sample_%i_barcode' % index, '' ) ) + if 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 ) + if not sample.bar_code: + # If the sample's associated SampleState is still the initial state + # configured by the admin for the request's RequestType, this must be + # the first time a bar code was added to the sample, so change it's state + # to the next associated SampleState. + if sample.state.id == request.type.states[0].id: + event = trans.app.model.SampleEvent(sample, + request.type.states[1], + 'Bar code associated with the sample' ) 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 ) + 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 ), '' ) ) ) + form_values = trans.sa_session.query( trans.model.FormValues ).get( sample.values.id ) + form_values.content = field_values + if sample.name != name or \ + sample.bar_code != bar_code or \ + sample.library != library or \ + sample.folder != folder or \ + form_values.content != field_values: + # Information about this sample has been changed. + sample.name = name 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() --- a/lib/galaxy/web/controllers/requests_admin.py +++ b/lib/galaxy/web/controllers/requests_admin.py @@ -9,8 +9,6 @@ import logging, os, pexpect, ConfigParse log = logging.getLogger( __name__ ) - - class AdminRequestsGrid( RequestsGrid ): class UserColumn( grids.TextColumn ): def get_value( self, trans, grid, request ): @@ -51,6 +49,7 @@ class RequestTypeGrid( grids.Grid ): return request_type.sample_form.name # Grid definition + webapp = "galaxy" title = "Sequencer Configurations" template = "admin/requests/grid.mako" model_class = model.RequestType @@ -103,6 +102,7 @@ class DataTransferGrid( grids.Grid ): def get_value( self, trans, grid, sample_dataset ): return sample_dataset.status # Grid definition + webapp = "galaxy" title = "Sample Datasets" template = "admin/requests/grid.mako" model_class = model.SampleDataset @@ -129,9 +129,19 @@ class DataTransferGrid( grids.Grid ): visible=False, filterable="standard" ) ) operations = [ - grids.GridOperation( "Start Transfer", allow_multiple=True, condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ) ), - grids.GridOperation( "Rename", allow_multiple=True, allow_popup=False, condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ) ), - grids.GridOperation( "Delete", allow_multiple=True, condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ) ), + grids.GridOperation( "Transfer", + allow_multiple=True, + condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ), + url_args=dict( webapp="galaxy" ) ), + grids.GridOperation( "Rename", + allow_multiple=True, + allow_popup=False, + condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ), + url_args=dict( webapp="galaxy" ) ), + grids.GridOperation( "Delete", + allow_multiple=True, + condition=( lambda item: item.status in [ model.SampleDataset.transfer_status.NOT_STARTED ] ), + url_args=dict( webapp="galaxy" ) ) ] def apply_query_filter( self, trans, query, **kwd ): sample_id = kwd.get( 'sample_id', None ) @@ -158,6 +168,12 @@ class RequestsAdmin( BaseController, Use action='edit_basic_request_info', cntrller='requests_admin', **kwd ) ) + if operation == "edit_samples": + kwd[ 'editing_samples' ] = True + return trans.response.send_redirect( web.url_for( controller='requests_common', + action='edit_samples', + cntrller='requests_admin', + **kwd ) ) if operation == "view_request": return trans.response.send_redirect( web.url_for( controller='requests_common', action='view_request', @@ -273,7 +289,7 @@ class RequestsAdmin( BaseController, Use break if no_datasets_transferred: status = 'error' - message = 'A dataset can be renamed only if it is in the "Not Started" state.' + message = 'A dataset can be renamed only if it has been transferred.' return trans.response.send_redirect( web.url_for( controller='requests_admin', action='manage_datasets', sample_id=trans.security.encode_id( selected_sample_datasets[0].sample.id ), @@ -282,7 +298,7 @@ class RequestsAdmin( BaseController, Use return trans.fill_template( '/admin/requests/rename_datasets.mako', sample=selected_sample_datasets[0].sample, id_list=id_list ) - elif operation == "start transfer": + elif operation == "transfer": self.__start_datatx( trans, selected_sample_datasets[0].sample, selected_sample_datasets ) # Render the grid view sample_id = params.get( 'sample_id', None ) @@ -292,7 +308,7 @@ 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.global_actions = [ grids.GridAction( "Refresh", + self.datatx_grid.global_actions = [ grids.GridAction( "Refresh page", dict( controller='requests_admin', action='manage_datasets', sample_id=sample_id ) ), @@ -302,11 +318,11 @@ 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( "Browse target data library", + dict( controller='library_common', + action='browse_library', + cntrller='library_admin', + id=library_id ) ), grids.GridAction( "Browse this request", dict( controller='requests_common', action='view_request', --- a/templates/requests/common/view_request.mako +++ b/templates/requests/common/view_request.mako @@ -18,15 +18,18 @@ 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_edit_request = ( is_admin and not request.is_complete ) or request.is_unsubmitted can_add_samples = is_unsubmitted + can_edit_or_delete_samples = request.samples and not is_complete + can_submit = request.samples and is_unsubmitted %><br/><br/><ul class="manage-table-actions"> - %if is_unsubmitted: + %if can_submit: <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> @@ -66,13 +69,12 @@ </div><div class="form-row"><label>User:</label> - %if is_admin: - ${request.user.email} - %elif request.user.username: - ${request.user.username} - %else: - Unknown - %endif + ${request.user.email} + <div style="clear: both"></div> + </div> + <div class="form-row"> + <label>Sequencer configuration:</label> + ${request.type.name} <div style="clear: both"></div></div><div class="form-row"> @@ -129,11 +131,6 @@ ${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> @@ -141,7 +138,7 @@ <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 )} + ${render_samples_grid( cntrller, request, current_samples=current_samples, action='view_request', editing_samples=False, encoded_selected_sample_ids=[], render_buttons=can_edit_or_delete_samples, grid_header=grid_header )} %else: There are no samples. %if can_add_samples: --- a/templates/grid_base.mako +++ b/templates/grid_base.mako @@ -752,11 +752,18 @@ %if grid.global_actions: <ul class="manage-table-actions"> - %for action in grid.global_actions: - <li> - <a class="action-button" href="${h.url_for( **action.url_args )}">${action.label}</a> - </li> - %endfor + %if len( grid.global_actions ) < 4: + %for action in grid.global_actions: + <li><a class="action-button" href="${h.url_for( **action.url_args )}">${action.label}</a></li> + %endfor + %else: + <li><a class="action-button" id="action-8675309-popup" class="menubutton">Actions</a></li> + <div popupmenu="action-8675309-popup"> + %for action in grid.global_actions: + <a class="action-button" href="${h.url_for( **action.url_args )}">${action.label}</a> + %endfor + </div> + %endif </ul> %endif --- a/templates/requests/common/common.mako +++ b/templates/requests/common/common.mako @@ -88,26 +88,26 @@ %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> + <td valign="top"><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> + <td valign="top"><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> + <td valign="top"><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> + <td valign="top">${current_sample['library_select_field'].get_html()}</td> + <td valign="top">${current_sample['folder_select_field'].get_html()}</td> %if is_submitted or is_complete: <% if sample: @@ -115,12 +115,12 @@ 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> + <td valign="top"><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 valign="top"><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> + <td valign="top"><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> @@ -132,7 +132,7 @@ 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 ) + can_edit_or_delete_samples = request.samples and not is_complete %> ${grid_header} %if render_buttons and ( can_add_samples or can_edit_or_delete_samples ):
participants (1)
-
commits-noreply@bitbucket.org