PageRenderTime 395ms CodeModel.GetById 21ms app.highlight 343ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/galaxy/web/framework/__init__.py

https://bitbucket.org/ialbert/galaxy-genetrack
Python | 692 lines | 589 code | 14 blank | 89 comment | 52 complexity | a50ed1323a738185bd6f52a268742a61 MD5 | raw file
  1"""
  2Galaxy web application framework
  3"""
  4
  5import pkg_resources
  6
  7import os, sys, time, socket, random, string
  8pkg_resources.require( "Cheetah" )
  9from Cheetah.Template import Template
 10import base
 11import pickle
 12from galaxy import util
 13
 14pkg_resources.require( "simplejson" )
 15import simplejson
 16
 17import helpers
 18
 19pkg_resources.require( "PasteDeploy" )
 20from paste.deploy.converters import asbool
 21
 22pkg_resources.require( "Mako" )
 23import mako.template
 24import mako.lookup
 25import mako.runtime
 26
 27pkg_resources.require( "Babel" )
 28from babel.support import Translations
 29
 30pkg_resources.require( "SQLAlchemy >= 0.4" )
 31from sqlalchemy import and_
 32            
 33import logging
 34log = logging.getLogger( __name__ )
 35
 36url_for = base.routes.url_for
 37
 38UCSC_SERVERS = (
 39    'hgw1.cse.ucsc.edu',
 40    'hgw2.cse.ucsc.edu',
 41    'hgw3.cse.ucsc.edu',
 42    'hgw4.cse.ucsc.edu',
 43    'hgw5.cse.ucsc.edu',
 44    'hgw6.cse.ucsc.edu',
 45    'hgw7.cse.ucsc.edu',
 46    'hgw8.cse.ucsc.edu',
 47)
 48
 49def expose( func ):
 50    """
 51    Decorator: mark a function as 'exposed' and thus web accessible
 52    """
 53    func.exposed = True
 54    return func
 55    
 56def json( func ):
 57    def decorator( self, trans, *args, **kwargs ):
 58        trans.response.set_content_type( "text/javascript" )
 59        return simplejson.dumps( func( self, trans, *args, **kwargs ) )
 60    if not hasattr(func, '_orig'):
 61        decorator._orig = func
 62    decorator.exposed = True
 63    return decorator
 64
 65def require_login( verb="perform this action" ):
 66    def argcatcher( func ):
 67        def decorator( self, trans, *args, **kwargs ):
 68            if trans.get_user():
 69                return func( self, trans, *args, **kwargs )
 70            else:
 71                return trans.show_error_message(
 72                    "You must be <a target='galaxy_main' href='%s'>logged in</a> to %s</div>"
 73                    % ( url_for( controller='user', action='login' ), verb ) )      
 74        return decorator
 75    return argcatcher
 76    
 77def require_admin( func ):
 78    def decorator( self, trans, *args, **kwargs ):
 79        admin_users = trans.app.config.get( "admin_users", "" ).split( "," )
 80        if not admin_users:
 81            return trans.show_error_message( "You must be logged in as an administrator to access this feature, but no administrators are set in the Galaxy configuration." )
 82        user = trans.get_user()
 83        if not user:
 84            return trans.show_error_message( "You must be logged in as an administrator to access this feature." )
 85        if not user.email in admin_users:
 86            return trans.show_error_message( "You must be an administrator to access this feature." )
 87        return func( self, trans, *args, **kwargs )
 88    return decorator
 89
 90NOT_SET = object()
 91
 92class MessageException( Exception ):
 93    """
 94    Exception to make throwing errors from deep in controllers easier
 95    """
 96    def __init__( self, err_msg, type="info" ):
 97        self.err_msg = err_msg
 98        self.type = type
 99        
100def error( message ):
101    raise MessageException( message, type='error' )
102
103def form( *args, **kwargs ):
104    return FormBuilder( *args, **kwargs )
105    
106class WebApplication( base.WebApplication ):
107    def __init__( self, galaxy_app, session_cookie='galaxysession' ):
108        base.WebApplication.__init__( self )
109        self.set_transaction_factory( lambda e: UniverseWebTransaction( e, galaxy_app, self, session_cookie ) )
110        # Mako support
111        self.mako_template_lookup = mako.lookup.TemplateLookup(
112            directories = [ galaxy_app.config.template_path ] ,
113            module_directory = galaxy_app.config.template_cache,
114            collection_size = 500,
115            output_encoding = 'utf-8' )
116        # Security helper
117        self.security = galaxy_app.security
118    def handle_controller_exception( self, e, trans, **kwargs ):
119        if isinstance( e, MessageException ):
120            return trans.show_message( e.err_msg, e.type )
121    def make_body_iterable( self, trans, body ):
122        if isinstance( body, FormBuilder ):
123            body = trans.show_form( body )
124        return base.WebApplication.make_body_iterable( self, trans, body )
125    
126class UniverseWebTransaction( base.DefaultWebTransaction ):
127    """
128    Encapsulates web transaction specific state for the Universe application
129    (specifically the user's "cookie" session and history)
130    """
131    def __init__( self, environ, app, webapp, session_cookie ):
132        self.app = app
133        self.webapp = webapp
134        self.security = webapp.security
135        # FIXME: the following 3 attributes are not currently used
136        #        Remove them if they are not going to be...
137        self.__user = NOT_SET
138        self.__history = NOT_SET
139        self.__galaxy_session = NOT_SET
140        base.DefaultWebTransaction.__init__( self, environ )
141        self.setup_i18n()
142        self.sa_session.clear()
143        self.debug = asbool( self.app.config.get( 'debug', False ) )
144        # Flag indicating whether we are in workflow building mode (means
145        # that the current history should not be used for parameter values
146        # and such).
147        self.workflow_building_mode = False
148        # Always have a valid galaxy session
149        self.__ensure_valid_session( session_cookie )
150        # Prevent deleted users from accessing Galaxy
151        if self.app.config.use_remote_user and self.galaxy_session.user.deleted:
152            self.response.send_redirect( url_for( '/static/user_disabled.html' ) )
153        if self.app.config.require_login:
154            self.__ensure_logged_in_user( environ )
155    def setup_i18n( self ):
156        if 'HTTP_ACCEPT_LANGUAGE' in self.environ:
157            # locales looks something like: ['en', 'en-us;q=0.7', 'ja;q=0.3']
158            locales = self.environ['HTTP_ACCEPT_LANGUAGE'].split( ',' )
159            locales = [ l.split( ';' )[0] for l in locales ]
160        else:
161            # Default to English
162            locales = 'en'
163        t = Translations.load( dirname='locale', locales=locales, domain='ginga' )
164        self.template_context.update ( dict( _=t.ugettext, n_=t.ugettext, N_=t.ungettext ) )
165    @property
166    def sa_session( self ):
167        """
168        Returns a SQLAlchemy session -- currently just gets the current
169        session from the threadlocal session context, but this is provided
170        to allow migration toward a more SQLAlchemy 0.4 style of use.
171        """
172        return self.app.model.context.current
173    def log_event( self, message, tool_id=None, **kwargs ):
174        """
175        Application level logging. Still needs fleshing out (log levels and such)
176        Logging events is a config setting - if False, do not log.
177        """
178        if self.app.config.log_events:
179            event = self.app.model.Event()
180            event.tool_id = tool_id
181            try:
182                event.message = message % kwargs
183            except:
184                event.message = message
185            try:
186                event.history = self.get_history()
187            except:
188                event.history = None
189            try:
190                event.history_id = self.history.id
191            except:
192                event.history_id = None
193            try:
194                event.user = self.user
195            except:
196                event.user = None
197            try:
198                event.session_id = self.galaxy_session.id   
199            except:
200                event.session_id = None
201            event.flush()
202    def get_cookie( self, name='galaxysession' ):
203        """Convenience method for getting a session cookie"""
204        try:
205            # If we've changed the cookie during the request return the new value
206            if name in self.response.cookies:
207                return self.response.cookies[name].value
208            else:
209                return self.request.cookies[name].value
210        except:
211            return None
212    def set_cookie( self, value, name='galaxysession', path='/', age=90, version='1' ):
213        """Convenience method for setting a session cookie"""
214        # The galaxysession cookie value must be a high entropy 128 bit random number encrypted 
215        # using a server secret key.  Any other value is invalid and could pose security issues.
216        self.response.cookies[name] = value
217        self.response.cookies[name]['path'] = path
218        self.response.cookies[name]['max-age'] = 3600 * 24 * age # 90 days
219        tstamp = time.localtime ( time.time() + 3600 * 24 * age )
220        self.response.cookies[name]['expires'] = time.strftime( '%a, %d-%b-%Y %H:%M:%S GMT', tstamp ) 
221        self.response.cookies[name]['version'] = version
222    #@property
223    #def galaxy_session( self ):
224    #    if not self.__galaxy_session:
225    #        self.__ensure_valid_session()
226    #    return self.__galaxy_session  
227    def __ensure_valid_session( self, session_cookie ):
228        """
229        Ensure that a valid Galaxy session exists and is available as
230        trans.session (part of initialization)
231        
232        Support for universe_session and universe_user cookies has been
233        removed as of 31 Oct 2008.
234        """
235        sa_session = self.sa_session
236        # Try to load an existing session
237        secure_id = self.get_cookie( name=session_cookie )
238        galaxy_session = None
239        prev_galaxy_session = None
240        user_for_new_session = None
241        invalidate_existing_session = False
242        # Track whether the session has changed so we can avoid calling flush
243        # in the most common case (session exists and is valid).
244        galaxy_session_requires_flush = False
245        if secure_id:
246            # Decode the cookie value to get the session_key
247            session_key = self.security.decode_session_key( secure_id )
248            try:
249                # Make sure we have a valid UTF-8 string 
250                session_key = session_key.encode( 'utf8' )
251            except UnicodeDecodeError:
252                # We'll end up creating a new galaxy_session
253                session_key = None
254            if session_key:
255                # Retrieve the galaxy_session id via the unique session_key
256                galaxy_session = self.app.model.GalaxySession.filter( and_( self.app.model.GalaxySession.table.c.session_key==session_key,
257                                                                            self.app.model.GalaxySession.table.c.is_valid==True ) ).first()
258        # If remote user is in use it can invalidate the session, so we need to to check some things now.
259        if self.app.config.use_remote_user:
260            assert "HTTP_REMOTE_USER" in self.environ, \
261                "use_remote_user is set but no HTTP_REMOTE_USER variable"
262            remote_user_email = self.environ[ 'HTTP_REMOTE_USER' ]    
263            if galaxy_session:
264                # An existing session, make sure correct association exists
265                if galaxy_session.user is None:
266                    # No user, associate
267                    galaxy_session.user = self.__get_or_create_remote_user( remote_user_email )
268                    galaxy_session_requires_flush = True
269                elif galaxy_session.user.email != remote_user_email:
270                    # Session exists but is not associated with the correct remote user
271                    invalidate_existing_session = True
272                    user_for_new_session = self.__get_or_create_remote_user( remote_user_email )
273                    log.warning( "User logged in as '%s' externally, but has a cookie as '%s' invalidating session",
274                                 remote_user_email, galaxy_session.user.email )
275            else:
276                # No session exists, get/create user for new session
277                user_for_new_session = self.__get_or_create_remote_user( remote_user_email )
278        else:
279            if galaxy_session is not None and galaxy_session.user and galaxy_session.user.external:
280                # Remote user support is not enabled, but there is an existing
281                # session with an external user, invalidate
282                invalidate_existing_session = True
283                log.warning( "User '%s' is an external user with an existing session, invalidating session since external auth is disabled",
284                             galaxy_session.user.email )
285            elif galaxy_session is not None and galaxy_session.user is not None and galaxy_session.user.deleted:
286                invalidate_existing_session = True
287                log.warning( "User '%s' is marked deleted, invalidating session" % galaxy_session.user.email )
288        # Do we need to invalidate the session for some reason?
289        if invalidate_existing_session:
290            prev_galaxy_session = galaxy_session
291            prev_galaxy_session.is_valid = False
292            galaxy_session = None
293        # No relevant cookies, or couldn't find, or invalid, so create a new session
294        if galaxy_session is None:
295            galaxy_session = self.__create_new_session( prev_galaxy_session, user_for_new_session )
296            galaxy_session_requires_flush = True
297            self.galaxy_session = galaxy_session
298            self.__update_session_cookie( name=session_cookie )
299        else:
300            self.galaxy_session = galaxy_session
301        # Do we need to flush the session?
302        if galaxy_session_requires_flush:
303            objects_to_flush = [ galaxy_session ]
304            # FIXME: If prev_session is a proper relation this would not
305            #        be needed.
306            if prev_galaxy_session:
307                objects_to_flush.append( prev_galaxy_session )            
308            sa_session.flush( objects_to_flush )
309        # If the old session was invalid, get a new history with our new session
310        if invalidate_existing_session:
311            self.new_history()
312    def __ensure_logged_in_user( self, environ ):
313        allowed_paths = (
314            url_for( controller='root', action='index' ),
315            url_for( controller='root', action='tool_menu' ),
316            url_for( controller='root', action='masthead' ),
317            url_for( controller='root', action='history' ),
318            url_for( controller='user', action='login' ),
319            url_for( controller='user', action='create' ),
320            url_for( controller='user', action='reset_password' ),
321            url_for( controller='library', action='browse' )
322        )
323        display_as = url_for( controller='root', action='display_as' )
324        if self.galaxy_session.user is None:
325            if self.app.config.ucsc_display_sites and self.request.path == display_as:
326                try:
327                    host = socket.gethostbyaddr( self.environ[ 'REMOTE_ADDR' ] )[0]
328                except( socket.error, socket.herror, socket.gaierror, socket.timeout ):
329                    host = None
330                if host in UCSC_SERVERS:
331                    return
332            if self.request.path not in allowed_paths:
333                self.response.send_redirect( url_for( controller='root', action='index' ) )
334    def __create_new_session( self, prev_galaxy_session=None, user_for_new_session=None ):
335        """
336        Create a new GalaxySession for this request, possibly with a connection
337        to a previous session (in `prev_galaxy_session`) and an existing user
338        (in `user_for_new_session`).
339        
340        Caller is responsible for flushing the returned session.
341        """
342        session_key = self.security.get_new_session_key()
343        galaxy_session = self.app.model.GalaxySession(
344            session_key=session_key,
345            is_valid=True, 
346            remote_host = self.request.remote_host,
347            remote_addr = self.request.remote_addr,
348            referer = self.request.headers.get( 'Referer', None ) )
349        if prev_galaxy_session:
350            # Invalidated an existing session for some reason, keep track
351            galaxy_session.prev_session_id = prev_galaxy_session.id
352        if user_for_new_session:
353            # The new session should be associated with the user
354            galaxy_session.user = user_for_new_session
355        return galaxy_session
356    def __get_or_create_remote_user( self, remote_user_email ):
357        """
358        Return the user in $HTTP_REMOTE_USER and create if necessary
359        """
360        # remote_user middleware ensures HTTP_REMOTE_USER exists
361        user = self.app.model.User.filter( self.app.model.User.table.c.email==remote_user_email ).first()
362        if user:
363            # GVK: June 29, 2009 - This is to correct the behavior of a previous bug where a private
364            # role and default user / history permissions were not set for remote users.  When a
365            # remote user authenticates, we'll look for this information, and if missing, create it.
366            if not self.app.security_agent.get_private_user_role( user ):
367                self.app.security_agent.create_private_user_role( user )
368            if not user.default_permissions:
369                self.app.security_agent.user_set_default_permissions( user, history=True, dataset=True )
370        elif user is None:
371            random.seed()
372            user = self.app.model.User( email=remote_user_email )
373            user.set_password_cleartext( ''.join( random.sample( string.letters + string.digits, 12 ) ) )
374            user.external = True
375            user.flush()
376            self.app.security_agent.create_private_user_role( user )
377            # We set default user permissions, before we log in and set the default history permissions
378            self.app.security_agent.user_set_default_permissions( user )
379            #self.log_event( "Automatically created account '%s'", user.email )
380        return user
381    def __update_session_cookie( self, name='galaxysession' ):
382        """
383        Update the session cookie to match the current session.
384        """
385        self.set_cookie( self.security.encode_session_key( self.galaxy_session.session_key ), name=name )
386    def handle_user_login( self, user ):
387        """
388        Login a new user (possibly newly created)
389           - create a new session
390           - associate new session with user
391           - if old session had a history and it was not associated with a user, associate it with the new session, 
392             otherwise associate the current session's history with the user
393        """
394        # Set the previous session
395        prev_galaxy_session = self.galaxy_session
396        prev_galaxy_session.is_valid = False
397        # Define a new current_session
398        self.galaxy_session = self.__create_new_session( prev_galaxy_session, user )
399        # Associated the current user's last accessed history (if exists) with their new session
400        history = None
401        try:
402            users_last_session = user.galaxy_sessions[0]
403            last_accessed = True
404        except:
405            users_last_session = None
406            last_accessed = False
407        if users_last_session and users_last_session.current_history:
408            history = users_last_session.current_history
409        if not history:
410            if prev_galaxy_session.current_history:
411                if prev_galaxy_session.current_history.user is None or prev_galaxy_session.current_history.user == user:
412                    # If the previous galaxy session had a history, associate it with the new
413                    # session, but only if it didn't belong to a different user.
414                    history = prev_galaxy_session.current_history
415            elif self.galaxy_session.current_history:
416                history = self.galaxy_session.current_history
417            else:
418                history = self.get_history( create=True )
419        if history not in self.galaxy_session.histories:
420            self.galaxy_session.add_history( history )
421        if history.user is None:
422            history.user = user
423        self.galaxy_session.current_history = history
424        if not last_accessed:
425            # Only set default history permissions if current history is not from a previous session
426            self.app.security_agent.history_set_default_permissions( history, dataset=True, bypass_manage_permission=True )
427        self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session, history ] )
428        # This method is not called from the Galaxy reports, so the cookie will always be galaxysession
429        self.__update_session_cookie( name='galaxysession' )
430    def handle_user_logout( self ):
431        """
432        Logout the current user:
433           - invalidate the current session
434           - create a new session with no user associated
435        """
436        prev_galaxy_session = self.galaxy_session
437        prev_galaxy_session.is_valid = False
438        self.galaxy_session = self.__create_new_session( prev_galaxy_session )
439        self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session ] )
440        # This method is not called from the Galaxy reports, so the cookie will always be galaxysession
441        self.__update_session_cookie( name='galaxysession' )
442        
443    def get_galaxy_session( self ):
444        """
445        Return the current galaxy session
446        """
447        return self.galaxy_session
448
449    def get_history( self, create=False ):
450        """
451        Load the current history, creating a new one only if there is not 
452        current history and we're told to create"
453        """
454        history = self.galaxy_session.current_history
455        if not history:
456            if util.string_as_bool( create ):
457                history = self.new_history()
458            else:
459                # Perhaps a bot is running a tool without having logged in to get a history
460                log.debug( "Error: this request returned None from get_history(): %s" % self.request.browser_url )
461                return None
462        return history
463    def set_history( self, history ):
464        if history and not history.deleted:
465            self.galaxy_session.current_history = history
466        self.sa_session.flush( [ self.galaxy_session ] )
467    history = property( get_history, set_history )
468    def new_history( self, name=None ):
469        """
470        Create a new history and associate it with the current session and
471        its associated user (if set).
472        """
473        # Create new history
474        history = self.app.model.History()
475        if name:
476            history.name = name
477        # Associate with session
478        history.add_galaxy_session( self.galaxy_session )
479        # Make it the session's current history
480        self.galaxy_session.current_history = history
481        # Associate with user
482        if self.galaxy_session.user:
483            history.user = self.galaxy_session.user
484        # Track genome_build with history
485        history.genome_build = util.dbnames.default_value
486        # Set the user's default history permissions
487        self.app.security_agent.history_set_default_permissions( history )
488        # Save
489        self.sa_session.flush( [ self.galaxy_session, history ] )
490        return history
491
492    def get_user( self ):
493        """Return the current user if logged in or None."""
494        return self.galaxy_session.user
495    def set_user( self, user ):
496        """Set the current user."""
497        self.galaxy_session.user = user
498        self.sa_session.flush( [ self.galaxy_session ] )
499    user = property( get_user, set_user )
500
501    def get_user_and_roles( self ):
502        user = self.get_user()
503        if user:
504            roles = user.all_roles()
505        else:
506            roles = []
507        return user, roles
508
509    def user_is_admin( self ):
510        admin_users = self.app.config.get( "admin_users", "" ).split( "," )
511        if self.user and admin_users and self.user.email in admin_users:
512            return True
513        return False
514
515    def get_toolbox(self):
516        """Returns the application toolbox"""
517        return self.app.toolbox
518    @base.lazy_property
519    def template_context( self ):
520        return dict()
521    @property
522    def model( self ):
523        return self.app.model
524    def make_form_data( self, name, **kwargs ):
525        rval = self.template_context[name] = FormData()
526        rval.values.update( kwargs )
527        return rval
528    def set_message( self, message ):
529        """
530        Convenience method for setting the 'message' element of the template
531        context.
532        """
533        self.template_context['message'] = message
534    def show_message( self, message, type='info', refresh_frames=[], cont=None ):
535        """
536        Convenience method for displaying a simple page with a single message.
537        
538        `type`: one of "error", "warning", "info", or "done"; determines the
539                type of dialog box and icon displayed with the message
540                
541        `refresh_frames`: names of frames in the interface that should be 
542                          refreshed when the message is displayed
543        """
544        return self.fill_template( "message.mako", message_type=type, message=message, refresh_frames=refresh_frames, cont=cont )
545    def show_error_message( self, message, refresh_frames=[] ):
546        """
547        Convenience method for displaying an error message. See `show_message`.
548        """
549        return self.show_message( message, 'error', refresh_frames )
550    def show_ok_message( self, message, refresh_frames=[] ):
551        """
552        Convenience method for displaying an ok message. See `show_message`.
553        """
554        return self.show_message( message, 'done', refresh_frames )
555    def show_warn_message( self, message, refresh_frames=[] ):
556        """
557        Convenience method for displaying an warn message. See `show_message`.
558        """
559        return self.show_message( message, 'warning', refresh_frames )
560    def show_form( self, form, header=None, template="form.mako" ):
561        """
562        Convenience method for displaying a simple page with a single HTML
563        form.
564        """    
565        return self.fill_template( template, form=form, header=header )
566    def fill_template(self, filename, **kwargs):
567        """
568        Fill in a template, putting any keyword arguments on the context.
569        """
570        # call get_user so we can invalidate sessions from external users,
571        # if external auth has been disabled.
572        self.get_user()
573        if filename.endswith( ".mako" ):
574            return self.fill_template_mako( filename, **kwargs )
575        else:
576            template = Template( file=os.path.join(self.app.config.template_path, filename), 
577                                 searchList=[kwargs, self.template_context, dict(caller=self, t=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app)] )
578            return str( template )
579    def fill_template_mako( self, filename, **kwargs ):
580        template = self.webapp.mako_template_lookup.get_template( filename )
581        template.output_encoding = 'utf-8' 
582        data = dict( caller=self, t=self, trans=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app )
583        data.update( self.template_context )
584        data.update( kwargs )
585        return template.render( **data )
586    def stream_template_mako( self, filename, **kwargs ):
587        template = self.webapp.mako_template_lookup.get_template( filename )
588        template.output_encoding = 'utf-8' 
589        data = dict( caller=self, t=self, trans=self, h=helpers, util=util, request=self.request, response=self.response, app=self.app )
590        data.update( self.template_context )
591        data.update( kwargs )
592        ## return template.render( **data )
593        def render( environ, start_response ):
594            response_write = start_response( self.response.wsgi_status(), self.response.wsgi_headeritems() )
595            class StreamBuffer( object ):
596                def write( self, d ):
597                    response_write( d.encode( 'utf-8' ) )
598            buffer = StreamBuffer()
599            context = mako.runtime.Context( buffer, **data )
600            template.render_context( context )
601            return []
602        return render
603    def fill_template_string(self, template_string, context=None, **kwargs):
604        """
605        Fill in a template, putting any keyword arguments on the context.
606        """
607        template = Template( source=template_string,
608                             searchList=[context or kwargs, dict(caller=self)] )
609        return str(template)
610
611    @property
612    def db_builds( self ):
613        """
614        Returns the builds defined by galaxy and the builds defined by
615        the user (chromInfo in history).
616        """
617        dbnames = list()
618        datasets = self.app.model.HistoryDatasetAssociation \
619            .filter_by(deleted=False, history_id=self.history.id, extension="len").all()
620        if len(datasets) > 0:
621            dbnames.append( (util.dbnames.default_value, '--------- User Defined Builds ----------') )
622        for dataset in datasets:
623            dbnames.append( (dataset.dbkey, dataset.name) )
624        dbnames.extend( util.dbnames )
625        return dbnames
626        
627    def db_dataset_for( self, dbkey ):
628        """
629        Returns the db_file dataset associated/needed by `dataset`, or `None`.
630        """
631        datasets = self.app.model.HistoryDatasetAssociation \
632            .filter_by(deleted=False, history_id=self.history.id, extension="len").all()
633        for ds in datasets:
634            if dbkey == ds.dbkey:
635                return ds
636        return None
637    
638    def request_types(self):
639        if self.app.model.RequestType.query().all():
640            return True
641        return False
642        
643class FormBuilder( object ):
644    """
645    Simple class describing an HTML form
646    """
647    def __init__( self, action="", title="", name="form", submit_text="submit" ):
648        self.title = title
649        self.name = name
650        self.action = action
651        self.submit_text = submit_text
652        self.inputs = []
653    def add_input( self, type, name, label, value=None, error=None, help=None, use_label=True  ):
654        self.inputs.append( FormInput( type, label, name, value, error, help, use_label ) )
655        return self
656    def add_text( self, name, label, value=None, error=None, help=None  ):
657        return self.add_input( 'text', label, name, value, error, help )
658    def add_password( self, name, label, value=None, error=None, help=None  ):
659        return self.add_input( 'password', label, name, value, error, help )
660        
661class FormInput( object ):
662    """
663    Simple class describing a form input element
664    """
665    def __init__( self, type, name, label, value=None, error=None, help=None, use_label=True ):
666        self.type = type
667        self.name = name
668        self.label = label
669        self.value = value
670        self.error = error
671        self.help = help
672        self.use_label = use_label
673    
674class FormData( object ):
675    """
676    Class for passing data about a form to a template, very rudimentary, could
677    be combined with the tool form handling to build something more general.
678    """
679    def __init__( self ):
680        self.values = Bunch()
681        self.errors = Bunch()
682        
683class Bunch( dict ):
684    """
685    Bunch based on a dict
686    """
687    def __getattr__( self, key ):
688        if key not in self: raise AttributeError, key
689        return self[key]
690    def __setattr__( self, key, value ):
691        self[key] = value
692