# HG changeset patch -- Bitbucket.org # Project galaxy-dist # URL http://bitbucket.org/galaxy/galaxy-dist/overview # User rc # Date 1282835869 14400 # Node ID 7b9f46307153d0bba1b2253204ad28695d373313 # Parent d24fca10862b5b36ba3dbe8a9f318e2e77ef60f2 lims: email notification enhancements - changed the notify field in the request table to JSONType to store recipient email address and other config details - notification of specific sample state changes - changes made to barcode scan AMQP handler to update sample state to also send email when required - notification to multiple recipients - request events history records notifications - UI enhancements to the edit request page to allow users to change notification configurations UI enhancements to the request page to show the request type state, user & notification details --- a/scripts/galaxy_messaging/server/galaxyweb_interface.py +++ b/scripts/galaxy_messaging/server/galaxyweb_interface.py @@ -105,3 +105,19 @@ class GalaxyWebInterface(object): s = ( "!" * ( 8 - len(s) % 8 ) ) + s # Encrypt return id_cipher.encrypt( s ).encode( 'hex' ) + + def update_request_state(self, request_id, sample_id): + params = urllib.urlencode(dict( cntrller='requests_admin', + request_id=request_id, + sample_id=sample_id)) + url = self.base_url + "/requests_admin/update_request_state" + f = self.opener.open(url, params) + print url + print params + x = f.read() + + + + + + --- a/templates/requests/common/new_request.mako +++ b/templates/requests/common/new_request.mako @@ -85,7 +85,7 @@ </div><div style="clear: both"></div></div> - </form> - </div> + </form> + </div></div> %endif --- a/scripts/galaxy_messaging/server/amqp_consumer.py +++ b/scripts/galaxy_messaging/server/amqp_consumer.py @@ -16,11 +16,18 @@ import xml.dom.minidom import subprocess from galaxydb_interface import GalaxyDbInterface +new_path = [ os.path.join( os.getcwd(), "scripts/galaxy_messaging/server" ) ] +new_path.extend( sys.path[1:] ) # remove scripts/ from the path +sys.path = new_path +from galaxyweb_interface import GalaxyWebInterface + assert sys.version_info[:2] >= ( 2, 4 ) new_path = [ os.path.join( os.getcwd(), "lib" ) ] new_path.extend( sys.path[1:] ) # remove scripts/ from the path sys.path = new_path + + from galaxy import eggs import pkg_resources pkg_resources.require( "amqplib" ) @@ -66,6 +73,7 @@ def get_value_index(dom, tag_name, index def recv_callback(msg): global config + global webconfig # check the meesage type. msg_type = msg.properties['application_headers'].get('msg_type') log.debug('\nMESSAGE RECVD: '+str(msg_type)) @@ -88,12 +96,20 @@ def recv_callback(msg): log.debug('Barcode: '+barcode) log.debug('State: '+state) # update the galaxy db - galaxy = GalaxyDbInterface(dbconnstr) - sample_id = galaxy.get_sample_id(field_name='bar_code', value=barcode) + galaxydb = GalaxyDbInterface(dbconnstr) + sample_id = galaxydb.get_sample_id(field_name='bar_code', value=barcode) if sample_id == -1: log.debug('Invalid barcode.') return - galaxy.change_state(sample_id, state) + galaxydb.change_state(sample_id, state) + # update the request state + galaxyweb = GalaxyWebInterface(webconfig.get("universe_wsgi_config", "host"), + webconfig.get("universe_wsgi_config", "port"), + webconfig.get("data_transfer_user_login_info", "email"), + webconfig.get("data_transfer_user_login_info", "password"), + config.get("app:main", "id_secret")) + galaxyweb.update_request_state(galaxydb.get_request_id(sample_id), sample_id) + galaxyweb.logout() def main(): if len(sys.argv) < 2: @@ -108,6 +124,15 @@ def main(): for option in config.options("galaxy_amqp"): amqp_config[option] = config.get("galaxy_amqp", option) log.debug(str(amqp_config)) + # web server config + global webconfig + webconfig = ConfigParser.ConfigParser() + webconfig.read('transfer_datasets.ini') + + + + + conn = amqp.Connection(host=amqp_config['host']+":"+amqp_config['port'], userid=amqp_config['userid'], password=amqp_config['password'], --- a/templates/requests/common/sample_events.mako +++ b/templates/requests/common/sample_events.mako @@ -17,6 +17,13 @@ %endif <div class="toolForm"> + <div class="form-row"> + <div class="toolParamHelp" style="clear: both;"> + <b>Possible states: </b> + <% states = " > ".join([ ss.name for ss in sample.request.type.states ]) %> + ${states} + </div> + </div><table class="grid"><thead><tr> --- a/templates/requests/common/edit_request.mako +++ b/templates/requests/common/edit_request.mako @@ -37,48 +37,86 @@ <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='show', id=trans.security.encode_id(request.id))}"><span>Browse this request</span></a></li> + <li> + <a class="action-button" href="${h.url_for( controller=cntrller, action='list')}"> + <span>Browse all requests</span></a> + </li></ul><div class="toolForm"> - <div class="toolFormTitle">Edit request "${request.name}"</div> - %if len(select_request_type.options) == 1: - There are no request types created for a new request. - %else: - <div class="toolFormBody"> - <form name="edit_request" id="edit_request" action="${h.url_for( controller='requests_common', cntrller=cntrller, action='edit', id=trans.security.encode_id(request.id))}" method="post" > + <div class="toolFormTitle">Edit sequencing request "${request.name}"</div> + <div class="toolFormBody"> + <form name="edit_request" id="edit_request" action="${h.url_for( controller='requests_common', cntrller=cntrller, action='edit', id=trans.security.encode_id(request.id))}" method="post" > + %for i, field in enumerate(widgets): <div class="form-row"> - <label> - Select Request Type: - </label> - ${select_request_type.get_html()} + <label>${field['label']}</label> + ${field['widget'].get_html()} + %if field['label'] == 'Data library' and new_library: + ${new_library.get_html()} + %endif + <div class="toolParamHelp" style="clear: both;"> + ${field['helptext']} + </div> + <div style="clear: both"></div></div> - - %if select_request_type.get_selected() != ('Select one', 'none'): - %for i, field in enumerate(widgets): - <div class="form-row"> - <label>${field['label']}</label> - ${field['widget'].get_html()} - %if field['label'] == 'Data library' and new_library: - ${new_library.get_html()} - %endif - <div class="toolParamHelp" style="clear: both;"> - ${field['helptext']} - </div> - <div style="clear: both"></div> - </div> - %endfor - <div class="form-row"> - <div style="float: left; width: 250px; margin-right: 10px;"> - <input type="hidden" name="refresh" value="true" size="40"/> - </div> - <div style="clear: both"></div> - </div> - <div class="form-row"> - <input type="submit" name="save_changes_request_button" value="Save changes"/> - ##<input type="submit" name="edit_samples_button" value="Edit samples"/> - </div> - %endif + %endfor + <div class="form-row"> + <div style="float: left; width: 250px; margin-right: 10px;"> + <input type="hidden" name="refresh" value="true" size="40"/> + </div> + <div style="clear: both"></div> + </div> + <div class="form-row"> + <input type="submit" name="save_changes_request_button" value="Save"/> + </div></form></div> + <div class="toolFormTitle">Email notification settings</div> + <div class="toolFormBody"> + <form name="settings" id="settings" action="${h.url_for( controller='requests_common', cntrller=cntrller, action='email_settings', id=trans.security.encode_id(request.id))}" method="post" > + <% + email_user = '' + email_additional = [] + for e in request.notification['email']: + if e == request.user.email: + email_user = 'checked' + else: + email_additional.append(e) + emails = '\r\n'.join(email_additional) + + %> + + <div class="form-row"> + <label>Send to:</label> + <input type="checkbox" name="email_user" value="true" ${email_user}>${request.user.email} (Sequencing request owner)<input type="hidden" name="email_user" value="true"> + </div> + <div class="form-row"> + <label>Additional email addresses:</label> + <textarea name="email_additional" rows="3" cols="40">${emails}</textarea> + <div class="toolParamHelp" style="clear: both;"> + Enter one email address per line + </div> + </div> + <div class="form-row"> + <label>Select sample state(s) to send email notification:</label> + %for ss in request.type.states: + <% + email_state = '' + if ss.id in request.notification['sample_states']: + email_state = 'checked' + %> + <input type="checkbox" name=sample_state_${ss.id} value="true" ${email_state} >${ss.name}<input type="hidden" name=sample_state_${ss.id} value="true"> + <br/> + %endfor + <div class="toolParamHelp" style="clear: both;"> + Email notification would be sent when all the samples of this sequencing request are in the selected state(s). + </div> + </div> + + <div class="form-row"> + <input type="submit" name="save_button" value="Save"/> + </div> + </form> + </div> + </div> -%endif --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -15,7 +15,7 @@ from galaxy.datatypes.metadata import Me from galaxy.security import RBACAgent, get_permitted_actions from galaxy.util.hash_util import * from galaxy.web.form_builder import * -import logging +import logging, smtplib, socket log = logging.getLogger( __name__ ) from sqlalchemy.orm import object_session import pexpect @@ -1520,18 +1520,27 @@ class Request( object ): REJECTED = 'Rejected', COMPLETE = 'Complete' ) def __init__(self, name=None, desc=None, request_type=None, user=None, - form_values=None, notify=None): + form_values=None, notification=None): self.name = name self.desc = desc self.type = request_type self.values = form_values self.user = user - self.notify = notify + self.notification = notification self.samples_list = [] def state(self): if self.events: return self.events[0].state return None + def common_state(self): + ''' + This method returns the state of this request's sample when they are all + in one common state. If not this returns None + ''' + for s in self.samples: + if s.current_state().id != self.samples[0].current_state().id: + return False + return self.samples[0].current_state() def last_comment(self): if self.events: if self.events[0].comment: @@ -1560,7 +1569,66 @@ class Request( object ): if not s.library: samples.append(s.name) return samples + def send_email_notification(self, trans, common_state, final_state=False): + # check if an email notification is configured to be sent when the samples + # are in this state + if common_state.id not in self.notification['sample_states']: + return + comments = '' + # send email + if self.notification['email'] and trans.app.config.smtp_server is not None: + host = trans.request.host.split(':')[0] + if host in ['localhost', '127.0.0.1']: + host = socket.getfqdn() + + body = """ +Galaxy Sample Tracking Notification +=================================== +User: %(user)s + +Sequencing request: %(request_name)s +Sequencing request type: %(request_type)s +Sequencing request state: %(request_state)s + +Number of samples: %(num_samples)s +All samples in state: %(sample_state)s + +""" + values = dict(user=self.user.email, + request_name=self.name, + request_type=self.type.name, + request_state=self.state(), + num_samples=str(len(self.samples)), + sample_state=common_state.name, + create_time=self.create_time, + submit_time=self.create_time) + body = body % values + # check if this is the final state of the samples + if final_state: + txt = "Sample Name -> Data Library/Folder\r\n" + for s in self.samples: + txt = txt + "%s -> %s/%s\r\n" % (s.name, s.library.name, s.folder.name) + body = body + txt + to = self.notification['email'] + frm = 'galaxy-no-reply@' + host + subject = "Galaxy Sample Tracking notification: '%s' sequencing request" % self.name + message = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s" % (frm, ", ".join(to), subject, body) + try: + s = smtplib.SMTP() + s.connect(trans.app.config.smtp_server) + s.sendmail(frm, to, message) + s.quit() + comments = "Email notification sent to %s." % ", ".join(to).strip().strip(',') + except: + comments = "Email notification failed." + # update the request history with the email notification event + elif not trans.app.config.smtp_server: + comments = "Email notification failed as SMTP server not set in config file" + if comments: + event = trans.app.model.RequestEvent(self, self.state(), comments) + trans.sa_session.add(event) + trans.sa_session.flush() class RequestEvent( object ): def __init__(self, request=None, request_state=None, comment=''): @@ -1581,6 +1649,8 @@ class RequestType( object ): self.request_form = request_form self.sample_form = sample_form self.datatx_info = datatx_info + def last_state(self): + return self.states[-1] class RequestTypePermissions( object ): def __init__( self, action, request_type, role ): --- a/lib/galaxy/web/controllers/requests_common.py +++ b/lib/galaxy/web/controllers/requests_common.py @@ -13,7 +13,7 @@ from sqlalchemy.sql import select import pexpect import ConfigParser, threading, time from amqplib import client_0_8 as amqp -import csv +import csv, smtplib, socket log = logging.getLogger( __name__ ) @@ -151,9 +151,6 @@ class RequestsCommon( BaseController, Us util.restore_text( params.get( 'desc', '' ) )), helptext='(Optional)')) widgets = widgets + request_type.request_form.get_widgets( user, **kwd ) - widgets.append(dict(label='Send email notification when the sequencing request is complete', - widget=CheckboxField('email_notify', False), - helptext='Email would be sent to the lab admin and the user for whom this request has been created.')) return trans.fill_template( '/requests/common/new_request.mako', cntrller=cntrller, select_request_type=select_request_type, @@ -188,17 +185,18 @@ class RequestsCommon( BaseController, Us ''' params = util.Params( kwd ) cntrller = util.restore_text( params.get( 'cntrller', 'requests' ) ) - request_type = trans.sa_session.query( trans.app.model.RequestType ).get( int( params.select_request_type ) ) if request: user = request.user + request_type = request.type else: + request_type = trans.sa_session.query( trans.app.model.RequestType ).get( int( params.select_request_type ) ) if cntrller == 'requests_admin' and trans.user_is_admin(): user = trans.sa_session.query( trans.app.model.User ).get( int( params.get( 'select_user', '' ) ) ) elif cntrller == 'requests': user = trans.user name = util.restore_text(params.get('name', '')) desc = util.restore_text(params.get('desc', '')) - notify = CheckboxField.is_checked( params.get('email_notify', '') ) + notification = dict(email=[user.email], sample_states=[request_type.last_state().id], body='', subject='') # fields values = [] for index, field in enumerate(request_type.request_form.fields): @@ -223,7 +221,7 @@ class RequestsCommon( BaseController, Us trans.sa_session.flush() if not request: request = trans.app.model.Request(name, desc, request_type, - user, form_values, notify) + user, form_values, notification) trans.sa_session.add( request ) trans.sa_session.flush() trans.sa_session.refresh( request ) @@ -240,12 +238,66 @@ class RequestsCommon( BaseController, Us request.desc = desc request.type = request_type request.user = user - request.notify = notify + request.notification = notification request.values = form_values trans.sa_session.add( request ) trans.sa_session.flush() return request - + @web.expose + @web.require_login( "create/submit sequencing requests" ) + def email_settings(self, trans, **kwd): + params = util.Params( kwd ) + cntrller = util.restore_text( params.get( 'cntrller', 'requests' ) ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + try: + request = trans.sa_session.query( trans.app.model.Request ).get( trans.security.decode_id( params.get( 'id', None ) ) ) + except: + return trans.response.send_redirect( web.url_for( controller=cntrller, + action='list', + status='error', + message="Invalid request ID") ) + email_user = CheckboxField.is_checked( params.get('email_user', '') ) + email_additional = params.get('email_additional', '').split('\r\n') + if email_user or email_additional: + emails = [] + if email_user: + emails.append(request.user.email) + for e in email_additional: + emails.append(util.restore_text(e)) + # check if valid email addresses + invalid = '' + for e in emails: + if len( e ) == 0 or "@" not in e or "." not in e: + invalid = e + break + if invalid: + message = "<b>%s</b> is not a valid email address." % invalid + return trans.response.send_redirect( web.url_for( controller='requests_common', + cntrller=cntrller, + action='edit', + show=True, + id=trans.security.encode_id(request.id), + message=message , + status='error' ) ) + else: + email_states = [] + for i, ss in enumerate(request.type.states): + if CheckboxField.is_checked( params.get('sample_state_%i' % ss.id, '') ): + email_states.append(ss.id) + request.notification = dict(email=emails, sample_states=email_states, + body='', subject='') + trans.sa_session.flush() + trans.sa_session.refresh( request ) + message = 'The changes made to the sequencing request has been saved' + return trans.response.send_redirect( web.url_for( controller='requests_common', + cntrller=cntrller, + action='show', + id=trans.security.encode_id(request.id), + message=message , + status='done') ) + + @web.expose @web.require_login( "create/submit sequencing requests" ) def edit(self, trans, **kwd): @@ -262,9 +314,8 @@ class RequestsCommon( BaseController, Us message="Invalid request ID") ) if params.get('show', False) == 'True': return self.__edit_request(trans, **kwd) - elif params.get('save_changes_request_button', False) == 'Save changes' \ + elif params.get('save_changes_request_button', False) == 'Save' \ or params.get('edit_samples_button', False) == 'Edit samples': - request_type = trans.sa_session.query( trans.app.model.RequestType ).get( int( params.select_request_type ) ) if not util.restore_text(params.get('name', '')): message = 'Please enter the <b>Name</b> of the request' kwd['status'] = 'error' @@ -276,7 +327,7 @@ class RequestsCommon( BaseController, Us **kwd) ) request = self.__save_request(trans, request, **kwd) message = 'The changes made to the request named %s has been saved' % request.name - if params.get('save_changes_request_button', False) == 'Save changes': + if params.get('save_changes_request_button', False) == 'Save': return trans.response.send_redirect( web.url_for( controller=cntrller, cntrller=cntrller, action='list', @@ -310,7 +361,7 @@ class RequestsCommon( BaseController, Us cntrller = util.restore_text( params.get( 'cntrller', 'requests' ) ) message = util.restore_text( params.get( 'message', '' ) ) status = params.get( 'status', 'done' ) - select_request_type = self.__select_request_type(trans, request.type.id) + #select_request_type = self.__select_request_type(trans, request.type.id) # list of widgets to be rendered on the request form widgets = [] if util.restore_text( params.get( 'name', '' ) ): @@ -328,12 +379,9 @@ class RequestsCommon( BaseController, Us widget=TextField('desc', 40, desc), helptext='(Optional)')) widgets = widgets + request.type.request_form.get_widgets( request.user, request.values.content, **kwd ) - widgets.append(dict(label='Send email notification once the sequencing request is complete', - widget=CheckboxField('email_notify', request.notify), - helptext='')) return trans.fill_template( 'requests/common/edit_request.mako', cntrller=cntrller, - select_request_type=select_request_type, + #select_request_type=select_request_type, request_type=request.type, request=request, widgets=widgets, @@ -399,6 +447,7 @@ class RequestsCommon( BaseController, Us trans.sa_session.add( event ) trans.sa_session.add( request ) trans.sa_session.flush() + request.send_email_notification(trans, new_state) return trans.response.send_redirect( web.url_for( controller=cntrller, action='list', id=trans.security.encode_id(request.id), @@ -486,6 +535,22 @@ class RequestsCommon( BaseController, Us events_list=events_list, request=request) @web.expose @web.require_login( "create/submit sequencing requests" ) + def settings(self, trans, **kwd): + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + cntrller = params.get( 'cntrller', 'requests' ) + try: + request = trans.sa_session.query( trans.app.model.Request ).get( trans.security.decode_id(kwd['id']) ) + except: + return trans.response.send_redirect( web.url_for( controller=cntrller, + action='list', + status='error', + message="Invalid request ID") ) + return trans.fill_template( '/requests/common/settings.mako', + cntrller=cntrller, request=request) + + @web.expose + @web.require_login( "create/submit sequencing requests" ) def show(self, trans, **kwd): params = util.Params( kwd ) message = util.restore_text( params.get( 'message', '' ) ) @@ -807,6 +872,17 @@ class RequestsCommon( BaseController, Us '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.common_state() + if common_state: + if common_state.id == request.type.states[1].id: + event = trans.app.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() @@ -962,21 +1038,16 @@ class RequestsCommon( BaseController, Us # list of widgets to be rendered on the request form request_details = [] # main details - request_details.append(dict(label='User', - value=str(request.user.email), - helptext='')) request_details.append(dict(label='Description', value=request.desc, helptext='')) - request_details.append(dict(label='Type', - value=request.type.name, - helptext='')) - request_details.append(dict(label='State', - value=request.state(), - helptext='')) request_details.append(dict(label='Date created', value=request.create_time, helptext='')) + request_details.append(dict(label='Date updated', + value=request.update_time, + helptext='')) + # form fields for index, field in enumerate(request.type.request_form.fields): if field['required']: @@ -996,13 +1067,6 @@ class RequestsCommon( BaseController, Us request_details.append(dict(label=field['label'], value=request.values.content[index], helptext=field['helptext']+' ('+req+')')) - if request.notify: - notify = 'Yes' - else: - notify = 'No' - request_details.append(dict(label='Send email notification once the sequencing request is complete', - value=notify, - helptext='')) return request_details @web.expose @web.require_login( "create/submit sequencing requests" ) @@ -1089,3 +1153,5 @@ class RequestsCommon( BaseController, Us dataset_files=sample.datasets, message=message, status=status, files=[], folder_path=folder_path ) + + --- a/templates/requests/common/show_request.mako +++ b/templates/requests/common/show_request.mako @@ -131,21 +131,35 @@ function showContent(vThis) document.onkeypress = stopRKey </script> -%if request.submitted(): - <% samples_not_ready = request.sequence_run_ready() %> - %if samples_not_ready: - ${render_msg( "Select a target library and folder for all the samples before starting the sequence run", "warning" )} - %endif +<% samples_not_ready = request.sequence_run_ready() %> +%if samples_not_ready: + ${render_msg( "Select a target data library and folder for all the samples before starting the sequence run", "warning" )} %endif %if request.rejected(): ${render_msg( "Reason for rejection: "+request.last_comment(), "warning" )} %endif +%if message: + ${render_msg( message, status )} +%endif + <div class="grid-header"><h2>Sequencing Request "${request.name}"</h2> + <div class="toolParamHelp" style="clear: both;"> + <b>Type</b>: ${request.type.name} + %if cntrller == 'requests_admin': + | <b>User</b>: ${request.user.email} + %endif + %if request.state() == request.states.SUBMITTED: + | <b>State</b>: <i>${request.state()}</i> + %else: + | <b>State</b>: ${request.state()} + %endif + </div> + </div> - +<br/><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"> @@ -153,10 +167,10 @@ function showContent(vThis) <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=cntrller, action='list', operation='Submit', id=trans.security.encode_id(request.id) )}"><span>Submit</span></a> %endif + <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='events', id=trans.security.encode_id(request.id) )}"> + <span>History</span></a><a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='Edit', id=trans.security.encode_id(request.id))}"><span>Edit</span></a> - <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='events', id=trans.security.encode_id(request.id) )}"> - <span>History</span></a> %if cntrller == 'requests_admin' and trans.user_is_admin(): %if request.submitted(): <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='reject', id=trans.security.encode_id(request.id))}"> @@ -173,41 +187,71 @@ function showContent(vThis) </ul> -%if message: - ${render_msg( message, status )} -%endif <div> -<h3><img src="/static/images/fugue/toggle-expand.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> Request Information</h3> -<div style="display:none;" > - %for index, rd in enumerate(request_details): - <div class="form-row"> - <label>${rd['label']}</label> - %if not rd['value']: - <i>None</i> - %else: - %if rd['label'] == 'State': - <a href="${h.url_for( controller=cntrller, action='list', operation='events', id=trans.security.encode_id(request.id) )}">${rd['value']}</a> - %else: - ${rd['value']} - %endif - %endif - </div> - <div style="clear: both"></div> - %endfor - <div class="form-row"> - <ul class="manage-table-actions"> - <li> - <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='Edit', id=trans.security.encode_id(request.id))}"> - <span>Edit request details</span></a> - </li> - </ul> - </div> + <h4><img src="/static/images/fugue/toggle-expand.png" alt="Show" onclick="showContent(this);" style="cursor:pointer;"/> Request Information</h4> + <div style="display:none;" > + <table class="grid" border="0"> + <tbody> + <tr> + <td valign="top" width="50%"> + %for index, rd in enumerate(request_details): + <div class="form-row"> + <label>${rd['label']}:</label> + %if not rd['value']: + <i>None</i> + %else: + %if rd['label'] == 'State': + <a href="${h.url_for( controller=cntrller, action='list', operation='events', id=trans.security.encode_id(request.id) )}">${rd['value']}</a> + %else: + ${rd['value']} + %endif + %endif + </div> + <div style="clear: both"></div> + %endfor + </td> + <td valign="top" width="50%"> + <div class="form-row"> + <label>Email notification recipient(s):</label> + <% emails = ', '.join(request.notification['email']) %> + %if emails: + ${emails} + %else: + <i>None</i> + %endif + </div> + <div style="clear: both"></div> + <div class="form-row"> + <label>Email notification on sample state(s):</label> + <% + states = [] + for ss in request.type.states: + if ss.id in request.notification['sample_states']: + states.append(ss.name) + states = ', '.join(states) + %> + %if states: + ${states} + %else: + <i>None</i> + %endif + </div> + <div style="clear: both"></div> + </td> + </tr> + </tbody> + </table> + <div class="form-row"> + <ul class="manage-table-actions"> + <li> + <a class="action-button" href="${h.url_for( controller=cntrller, action='list', operation='Edit', id=trans.security.encode_id(request.id))}"> + <span>Edit request information</span></a> + </li> + </ul> + </div> + </div></div> -</div> - - - <br/> @@ -395,12 +439,7 @@ function showContent(vThis) <td>${info['name']}</td><td>${info['barcode']}</td> %if sample.request.unsubmitted(): - ##<td>Unsubmitted</td> - <td> - <div id="history-name-container"> - <div id="history-name" class="tooltip editable-text" title="Click to rename history">Unsubmitted</div> - </div> - </td> + <td>Unsubmitted</td> %else: <td id="sampleState-${sample.id}">${render_sample_state( cntrller, sample )}</td> %endif --- a/lib/galaxy/web/controllers/requests_admin.py +++ b/lib/galaxy/web/controllers/requests_admin.py @@ -136,6 +136,7 @@ class RequestsGrid( grids.Grid ): grids.GridOperation( "Reject", allow_multiple=False, condition=( lambda item: not item.deleted and item.submitted() ) ), grids.GridOperation( "Delete", allow_multiple=True, condition=( lambda item: not item.deleted ) ), grids.GridOperation( "Undelete", condition=( lambda item: item.deleted ) ), + grids.GridOperation( "Purge", allow_multiple=False, confirm="This will permanently delete the sequencing request. Click OK to proceed.", condition=( lambda item: item.deleted ) ), ] global_actions = [ grids.GridAction( "Create new request", dict( controller='requests_common', @@ -317,11 +318,6 @@ class RequestsAdmin( BaseController ): cntrller='requests_admin', action='events', **kwd ) ) - if operation == "settings": - return trans.response.send_redirect( web.url_for( controller='requests_common', - cntrller='requests_admin', - action='settings', - **kwd ) ) elif operation == "reject": return self.__reject_request( trans, **kwd ) elif operation == "view_type": @@ -514,41 +510,52 @@ class RequestsAdmin( BaseController ): id=trans.security.encode_id(request.id), message='Bar codes have been saved for this request', status='done')) - def __set_request_state( self, trans, request ): - # check if all the samples of the current request are in the final state - complete = True - for s in request.samples: - if s.current_state().id != request.type.states[-1].id: - complete = False - if request.complete() and not complete: - comments = "One or more samples moved back from the %s state" % request.type.states[-1].name - event = trans.app.model.RequestEvent(request, request.states.SUBMITTED, comments) - trans.sa_session.add( event ) - trans.sa_session.flush() - elif complete: - # change the request state to 'Complete' - comments = "All samples of this request are in the last sample state (%s)." % request.type.states[-1].name - event = trans.app.model.RequestEvent(request, request.states.COMPLETE, comments) - trans.sa_session.add( event ) - trans.sa_session.flush() - # now that the request is complete send the email notification to the - # the user - if request.notify and trans.app.config.smtp_server is not None: - host = trans.request.host.split(':')[0] - if host == 'localhost': - host = socket.getfqdn() - body = "The '%s' sequencing request (type: %s) is now complete. Datasets from all the samples are now available for analysis or download from the respective data libraries in Galaxy." % ( request.name, request.type.name ) - to = [request.user.email] - frm = 'galaxy-no-reply@' + host - subject = "Galaxy Sample Tracking: '%s' sequencing request in complete." % request.name - message = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s" % (frm, ", ".join(to), subject, body) - try: - s = smtplib.SMTP() - s.connect( trans.app.config.smtp_server ) - s.sendmail( frm, [ to ], message ) - s.close() - except: - pass + @web.expose + @web.require_admin + def update_request_state( self, trans, **kwd ): + params = util.Params( kwd ) + try: + request = trans.sa_session.query( trans.app.model.Request ).get( int( params.get( 'request_id', None ) ) ) + sample_id = int(params.get('sample_id', False)) + except: + return trans.response.send_redirect( web.url_for( controller='requests_admin', + action='list', + status='error', + message="Invalid request ID", + **kwd) ) + # check if all the samples of the current request are in the sample state + common_state = request.common_state() + if not common_state: + # if the current request state is complete and one of its samples moved from + # the final sample state, then move the request state to In-progress + if request.complete(): + event = trans.app.model.RequestEvent(request, request.states.SUBMITTED, "One or more samples' state moved from the final sample state.") + trans.sa_session.add( event ) + trans.sa_session.flush() + return trans.response.send_redirect( web.url_for( controller='requests_common', + cntrller='requests_admin', + action='sample_events', + sample_id=sample_id)) + final_state = False + if common_state.id == request.type.last_state().id: + # since all the samples are in the final state, change the request state to 'Complete' + comments = "All samples of this request are in the last sample state (%s)." % request.type.last_state().name + state = request.states.COMPLETE + final_state = True + else: + comments = "All samples are in %s state." % common_state.name + state = request.states.SUBMITTED + event = trans.app.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 +# if common_state.id in request.notification['sample_states']: + request.send_email_notification(trans, common_state, final_state) + return trans.response.send_redirect( web.url_for( controller='requests_common', + cntrller='requests_admin', + action='sample_events', + sample_id=sample_id)) @web.expose @web.require_admin @@ -573,10 +580,10 @@ class RequestsAdmin( BaseController ): event = trans.app.model.SampleEvent(sample, new_state, comments) trans.sa_session.add( event ) trans.sa_session.flush() - self.__set_request_state( trans, sample.request ) - return trans.response.send_redirect( web.url_for( controller='requests_common', + return trans.response.send_redirect( web.url_for( controller='requests_admin', cntrller='requests_admin', - action='sample_events', + action='update_request_state', + request_id=sample.request.id, sample_id=sample.id)) # # Data transfer from sequencer @@ -1290,3 +1297,6 @@ class RequestsAdmin( BaseController ): roles=roles, status=status, message=message) + + + --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -1503,7 +1503,7 @@ class TwillTestCase( unittest.TestCase ) def edit_request( self, request_id, name, new_name, new_desc, new_fields): self.home() self.visit_url( "%s/requests/list?operation=Edit&id=%s" % (self.url, self.security.encode_id(request_id) ) ) - self.check_page_for_string( 'Edit request "%s"' % name ) + self.check_page_for_string( 'Edit sequencing request "%s"' % name ) tc.fv( "1", "name", new_name ) tc.fv( "1", "desc", new_desc ) for index, field_value in enumerate(new_fields): --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -560,7 +560,7 @@ Request.table = Table('request', metadat Column( "update_time", DateTime, default=now, onupdate=now ), Column( "name", TrimmedString( 255 ), nullable=False ), Column( "desc", TEXT ), - Column( "notify", Boolean, default=False ), + Column( "notification", JSONType() ), Column( "form_values_id", Integer, ForeignKey( "form_values.id" ), index=True ), Column( "request_type_id", Integer, ForeignKey( "request_type.id" ), index=True ), Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0057_request_notify.py @@ -0,0 +1,69 @@ +""" +Migration script to modify the 'notify' field in the 'request' table from a boolean +to a JSONType +""" + +from sqlalchemy import * +from sqlalchemy.orm import * +from migrate import * +from migrate.changeset import * +from sqlalchemy.exc import * + +from galaxy.model.custom_types import * +from galaxy.util.json import from_json_string, to_json_string + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) +db_session = scoped_session( sessionmaker( bind=migrate_engine, autoflush=False, autocommit=True ) ) + + +def upgrade(): + print __doc__ + metadata.reflect() + try: + Request_table = Table( "request", metadata, autoload=True ) + except NoSuchTableError, e: + Request_table = None + log.debug( "Failed loading table 'request'" ) + + + if Request_table: + # create the column again as JSONType + try: + col = Column( "notification", JSONType() ) + col.create( Request_table ) + assert col is Request_table.c.notification + except Exception, e: + log.debug( "Creating column 'notification' in the 'request' table failed: %s" % ( str( e ) ) ) + + cmd = "SELECT id, user_id, notify FROM request" + result = db_session.execute( cmd ) + for r in result: + id = int(r[0]) + notify_old = r[1] + notify_new = dict(email=[], sample_states=[], body='', subject='') + cmd = "update request set notification='%s' where id=%i" % (to_json_string(notify_new), id) + db_session.execute( cmd ) + + cmd = "SELECT id, notification FROM request" + result = db_session.execute( cmd ) + for r in result: + print r, str(r[1]) + rr = from_json_string(str(r[1])) + print r[0], type(rr), rr, rr['email'], rr['sample_states'] + + # remove the 'notify' column + try: + Request_table.c.notify.drop() + except Exception, e: + log.debug( "Deleting column 'notify' from the 'request' table failed: %s" % ( str( e ) ) ) + + + +def downgrade(): + pass --- a/scripts/galaxy_messaging/server/galaxydb_interface.py +++ b/scripts/galaxy_messaging/server/galaxydb_interface.py @@ -57,6 +57,12 @@ class GalaxyDbInterface(object): #log.debug('Sample ID: %i' % sample_id) return sample_id return -1 + + def get_request_id(self, sample_id): + query = select(columns=[self.sample_table.c.request_id], + whereclause=self.sample_table.c.id==sample_id) + request_id = query.execute().fetchall()[0][0] + return request_id def current_state(self, sample_id): '''