diff --git a/LICENSE.txt b/LICENSE.txt index 69978e57efb..23e45c00eae 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -17,4 +17,9 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Some icons found in Galaxy are from the Silk Icons set, available under +the Creative Commons Attribution 2.5 License, from: + +http://www.famfamfam.com/lab/icons/silk/ diff --git a/eggs.ini b/eggs.ini index b57d44b09ca..95faf8431ed 100644 --- a/eggs.ini +++ b/eggs.ini @@ -40,7 +40,7 @@ PasteDeploy = 1.3.1 PasteScript = 1.3.6 Routes = 1.6.3 simplejson = 1.5 -SQLAlchemy = 0.3.11 +SQLAlchemy = 0.4.7p1 Tempita = 0.1 twill = 0.9 WebError = 0.8a @@ -86,7 +86,7 @@ PasteDeploy = http://cheeseshop.python.org/packages/source/P/PasteDeploy/PasteDe PasteScript = http://cheeseshop.python.org/packages/source/P/PasteScript/PasteScript-1.3.6.tar.gz Routes = http://pypi.python.org/packages/source/R/Routes/Routes-1.6.3.tar.gz simplejson = http://cheeseshop.python.org/packages/source/s/simplejson/simplejson-1.5.tar.gz -SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.3.11.tar.gz +SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.4.7p1.tar.gz Tempita = http://pypi.python.org/packages/source/T/Tempita/Tempita-0.1.tar.gz twill = http://darcs.idyll.org/~t/projects/twill-0.9.tar.gz WebError = http://pypi.python.org/packages/source/W/WebError/WebError-0.8a.tar.gz diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 743af730b0f..a0f08799064 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -4,6 +4,7 @@ from galaxy import config, jobs, util, tools, web import galaxy.model import galaxy.model.mapping import galaxy.datatypes.registry +import galaxy.security class UniverseApplication( object ): """Encapsulates the state of a Universe application""" @@ -20,7 +21,7 @@ class UniverseApplication( object ): if self.config.database_connection: db_url = self.config.database_connection else: - db_url = "sqlite://%s?isolation_level=IMMEDIATE" % self.config.database + db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database # Setup the database engine and ORM self.model = galaxy.model.mapping.init( self.config.file_path, db_url, @@ -30,6 +31,8 @@ class UniverseApplication( object ): self.toolbox = tools.ToolBox( self.config.tool_config, self.config.tool_path, self ) #Load datatype converters self.datatypes_registry.load_datatype_converters( self.toolbox ) + #Load security policy + self.security_agent = self.model.security_agent # Start the job queue job_dispatcher = jobs.DefaultJobDispatcher( self ) self.job_queue = jobs.JobQueue( self, job_dispatcher ) diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 7e6b32088c1..8473eae5ac6 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -44,7 +44,7 @@ class Configuration( object ): self.job_scheduler_policy = kwargs.get("job_scheduler_policy", "FIFO") self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") ) self.job_working_directory = resolve_path( kwargs.get( "job_working_directory", "database/job_working_directory" ), self.root ) - self.admin_pass = kwargs.get('admin_pass',"galaxy") + self.admin_users = kwargs.get( "admin_users", "" ) self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-user-join@bx.psu.edu") self.error_email_to = kwargs.get( 'error_email_to', None ) @@ -64,6 +64,9 @@ class Configuration( object ): self.bugs_email = kwargs.get( 'bugs_email', None ) self.blog_url = kwargs.get( 'blog_url', None ) self.screencasts_url = kwargs.get( 'screencasts_url', None ) + self.library_import_dir = kwargs.get( 'library_import_dir', None ) + if self.library_import_dir is not None and not os.path.exists( self.library_import_dir ): + raise ConfigurationError( "library_import_dir specified in config (%s) does not exist" % self.library_import_dir ) # Parse global_conf and save the parser global_conf = kwargs.get( 'global_conf', None ) global_conf_parser = ConfigParser.ConfigParser() diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 93a8e8adfcb..cf5f430c7a7 100644 --- a/lib/galaxy/datatypes/data.py +++ b/lib/galaxy/datatypes/data.py @@ -45,6 +45,9 @@ class Data( object ): """Stores the set of display applications, and viewing methods, supported by this datatype """ supported_display_apps = {} + """If False, the peek is regenerated whenever a dataset of this type is copied""" + copy_safe_peek = True + def __init__(self, **kwd): """Initialize the datatype""" object.__init__(self, **kwd) @@ -180,12 +183,17 @@ class Data( object ): return "This display type (%s) is not implemented for this datatype (%s)." % ( type, dataset.ext) def get_display_links(self, dataset, type, app, base_url, **kwd): - """Returns a list of tuples of (name, link) for a particular display type """ - try: - if type in self.get_display_types(): - return getattr (self, self.supported_display_apps[type]['links_function']) (dataset, type, app, base_url, **kwd) - except: - log.exception('Function %s is referred to in datatype %s for generating links for type %s, but is not accessible' % (self.supported_display_apps[type]['links_function'], self.__class__.__name__, type) ) + """ + Returns a list of tuples of (name, link) for a particular display type + as long as the dataset is not associated with a role restricting its access. + We determine this by sending None as the user to the allow_action method. + """ + if app.security_agent.allow_action( None, dataset.permitted_actions.DATASET_ACCESS, dataset=dataset ): + try: + if type in self.get_display_types(): + return getattr (self, self.supported_display_apps[type]['links_function']) (dataset, type, app, base_url, **kwd) + except: + log.exception('Function %s is referred to in datatype %s for generating links for type %s, but is not accessible' % (self.supported_display_apps[type]['links_function'], self.__class__.__name__, type) ) return [] def get_converter_types(self, original_dataset, datatypes_registry): diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 90d44ab47f4..78bf6964e81 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -5,7 +5,7 @@ Image classes import data import logging from galaxy.datatypes.sniff import * -from urllib import urlencode +from urllib import urlencode, quote_plus import zipfile log = logging.getLogger(__name__) @@ -103,18 +103,22 @@ def create_applet_tag_peek( class_name, archive, params ): class Gmaj( data.Data ): """Class describing a GMAJ Applet""" file_ext = "gmaj.zip" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, - "buttonlabel": "Launch GMAJ", - "nobutton": "false", - "urlpause" :"100", - "debug": "false", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey } ) - } - class_name = "edu.psu.bx.gmaj.MajApplet.class" - archive = "/static/gmaj/gmaj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, + "buttonlabel": "Launch GMAJ", + "nobutton": "false", + "urlpause" :"100", + "debug": "false", + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ) + } + class_name = "edu.psu.bx.gmaj.MajApplet.class" + archive = "/static/gmaj/gmaj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the GMAJ applet." dataset.blurb = 'GMAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: @@ -175,17 +179,21 @@ class Html( data.Text ): class Laj( data.Text ): """Class describing a LAJ Applet""" file_ext = "laj" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "alignfile1": "display?id=%s" % dataset.id, - "buttonlabel": "Launch LAJ", - "title": "LAJ in Galaxy", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey } ), - "noseq": "true" - } - class_name = "edu.psu.cse.bio.laj.LajApplet.class" - archive = "/static/laj/laj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "alignfile1": "display?id=%s" % dataset.id, + "buttonlabel": "Launch LAJ", + "title": "LAJ in Galaxy", + "posturl": quote_plus( "history_add_to?%s" % "&".join( [ "%s=%s" % ( key, value ) for key, value in { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id }.items() ] ) ), + "noseq": "true" + } + class_name = "edu.psu.cse.bio.laj.LajApplet.class" + archive = "/static/laj/laj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the LAJ applet." dataset.blurb = 'LAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 15a32ea200a..b707301884c 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -97,11 +97,10 @@ class JobQueue( object ): model = self.app.model # Jobs in the NEW state won't be requeued unless we're tracking in the database if not self.track_jobs_in_database: - for job in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for job in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): log.debug( "no runner: %s is still in new state, adding to the jobs queue" %job.id ) self.queue.put( ( job.id, job.tool_id ) ) - for job in model.Job.select( (model.Job.c.state == model.Job.states.RUNNING) - | (model.Job.c.state == model.Job.states.QUEUED) ): + for job in model.Job.filter( (model.Job.c.state == model.Job.states.RUNNING) | (model.Job.c.state == model.Job.states.QUEUED) ).all(): if job.job_runner_name is not None: # why are we passing the queue to the wrapper? job_wrapper = JobWrapper( job.id, self.app.toolbox.tools_by_id[ job.tool_id ], self ) @@ -136,7 +135,7 @@ class JobQueue( object ): new_jobs = [] if self.track_jobs_in_database: model = self.app.model - for j in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for j in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): job = JobWrapper( j.id, self.app.toolbox.tools_by_id[ j.tool_id ], self ) new_jobs.append( job ) else: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index de157193e13..05dc038e448 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -13,6 +13,7 @@ from galaxy import util import tempfile import galaxy.datatypes.registry from galaxy.datatypes.metadata import MetadataCollection +from galaxy.security import RBACAgent import logging log = logging.getLogger( __name__ ) @@ -33,13 +34,21 @@ class User( object ): self.external = False # Relationships self.histories = [] + def set_password_cleartext( self, cleartext ): """Set 'self.password' to the digest of 'cleartext'.""" self.password = sha.new( cleartext ).hexdigest() def check_password( self, cleartext ): """Check if 'cleartext' matches 'self.password' when hashed.""" return self.password == sha.new( cleartext ).hexdigest() - + def all_roles( self ): + roles = [ ura.role for ura in self.roles ] + for group in [ uga.group for uga in self.groups ]: + for role in [ gra.role for gra in group.roles ]: + if role not in roles: + roles.append( role ) + return roles + class Job( object ): """ A job represents a request to run a tool given input datasets, tool @@ -101,194 +110,15 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -class HistoryDatasetAssociation( object ): - def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, - dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, - parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): - self.name = name or "Unnamed dataset" - self.id = id - self.hid = hid - self.info = info - self.blurb = blurb - self.peek = peek - self.extension = extension - self.designation = designation - self.metadata = metadata or dict() - if dbkey: #dbkey is stored in metadata, only set if non-zero, or else we could clobber one supplied by input 'metadata' - self.dbkey = dbkey - self.deleted = deleted - self.visible = visible - # Relationships - self.history = history - if not dataset and create_dataset: - dataset = Dataset() - dataset.flush() - self.dataset = dataset - self.parent_id = parent_id - self.validation_errors = validation_errors - self.copied_from_history_dataset_association = copied_from_history_dataset_association - - @property - def ext( self ): - return self.extension - - @property - def states( self ): - return self.dataset.states - - def get_dataset_state( self ): - return self.dataset.state - def set_dataset_state ( self, state ): - self.dataset.state = state - self.dataset.flush() #flush here, because hda.flush() won't flush the Dataset object - state = property( get_dataset_state, set_dataset_state ) - - def get_file_name( self ): - return self.dataset.get_file_name() - - def set_file_name (self, filename): - return self.dataset.set_file_name( filename ) - - file_name = property( get_file_name, set_file_name ) - - @property - def extra_files_path( self ): - return self.dataset.extra_files_path - - @property - def datatype( self ): - return datatypes_registry.get_datatype_by_extension( self.extension ) +class Group( object ): + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) + def __init__( self, name = None ): + self.name = name - def get_metadata( self ): - if not hasattr( self, '_metadata_collection' ): - self._metadata_collection = MetadataCollection( self ) - return self._metadata_collection - def set_metadata( self, bunch ): - # Needs to accept a MetadataCollection, a bunch, or a dict - self._metadata = self.metadata.make_dict_copy( bunch ) - metadata = property( get_metadata, set_metadata ) - - """ - This provide backwards compatibility with using the old dbkey - field in the database. That field now maps to "old_dbkey" (see mapping.py). - """ - def get_dbkey( self ): - dbkey = self.metadata.dbkey - if not isinstance(dbkey, list): dbkey = [dbkey] - #if dbkey in [["?"], [None], []]: dbkey = [self.old_dbkey] - if dbkey in [[None], []]: return "?" - return dbkey[0] - def set_dbkey( self, value ): - if "dbkey" in self.datatype.metadata_spec: - if not isinstance(value, list): - self.metadata.dbkey = [value] - else: - self.metadata.dbkey = value - #if isinstance(value, list): - # self.old_dbkey = value[0] - #else: - # self.old_dbkey = value - dbkey = property( get_dbkey, set_dbkey ) - - def change_datatype( self, new_ext ): - self.clear_associated_files() - datatypes_registry.change_datatype( self, new_ext ) - def get_size( self ): - """Returns the size of the data on disk""" - return self.dataset.get_size() - def set_size( self ): - """Returns the size of the data on disk""" - return self.dataset.set_size() - def has_data( self ): - """Detects whether there is any data""" - return self.dataset.has_data() - def get_raw_data( self ): - """Returns the full data. To stream it open the file_name and read/write as needed""" - return self.datatype.get_raw_data( self ) - def write_from_stream( self, stream ): - """Writes data from a stream""" - self.datatype.write_from_stream(self, stream) - def set_raw_data( self, data ): - """Saves the data on the disc""" - self.datatype.set_raw_data(self, data) - def get_mime( self ): - """Returns the mime type of the data""" - return datatypes_registry.get_mimetype_by_extension( self.extension.lower() ) - def set_peek( self ): - return self.datatype.set_peek( self ) - def init_meta( self, copy_from=None ): - return self.datatype.init_meta( self, copy_from=copy_from ) - def set_meta( self, **kwd ): - self.clear_associated_files( metadata_safe = True ) - return self.datatype.set_meta( self, **kwd ) - def set_readonly_meta( self, **kwd ): - return self.datatype.set_readonly_meta( self, **kwd ) - def missing_meta( self, **kwd ): - return self.datatype.missing_meta( self, **kwd ) - def as_display_type( self, type, **kwd ): - return self.datatype.as_display_type( self, type, **kwd ) - def display_peek( self ): - return self.datatype.display_peek( self ) - def display_name( self ): - return self.datatype.display_name( self ) - def display_info( self ): - return self.datatype.display_info( self ) - def get_converted_files_by_type( self, file_type ): - valid = [] - for assoc in self.implicitly_converted_datasets: - if not assoc.deleted and assoc.type == file_type: - valid.append( assoc.dataset ) - return valid - def clear_associated_files( self, metadata_safe = False, purge = False ): - #metadata_safe = True means to only clear when assoc.metadata_safe == False - for assoc in self.implicitly_converted_datasets: - if not metadata_safe or not assoc.metadata_safe: - assoc.clear( purge = purge ) - def get_child_by_designation(self, designation): - for child in self.children: - if child.designation == designation: - return child - return None - - def get_converter_types(self): - return self.datatype.get_converter_types( self, datatypes_registry) - - def find_conversion_destination( self, accepted_formats, **kwd ): - """Returns ( target_ext, exisiting converted dataset )""" - return self.datatype.find_conversion_destination( self, accepted_formats, datatypes_registry, **kwd ) - - def copy( self, copy_children = False, parent_id = None ): - des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) - des.flush() - des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id - if copy_children: - for child in self.children: - child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs - des.flush() - return des - - def add_validation_error( self, validation_error ): - self.validation_errors.append( validation_error ) - - def extend_validation_errors( self, validation_errors ): - self.validation_errors.extend(validation_errors) - - def mark_deleted( self, include_children=True ): - self.deleted = True - if include_children: - for child in self.children: - child.mark_deleted() - - def mark_undeleted( self, include_children=True ): - self.deleted = False - if include_children: - for child in self.children: - child.mark_undeleted() - def undeletable( self ): - if self.purged: - return False - return True +class UserGroupAssociation( object ): + def __init__( self, user, group ): + self.user = user + self.group = group class History( object ): def __init__( self, id=None, name=None, user=None ): @@ -362,6 +192,49 @@ class History( object ): # self.history = history # self.datasets = [] +class UserRoleAssociation( object ): + def __init__( self, user, role ): + self.user = user + self.role = role + +class GroupRoleAssociation( object ): + def __init__( self, group, role ): + self.group = group + self.role = role + +class Role( object ): + private_id = None + types = Bunch( + PRIVATE = 'private', + SYSTEM = 'system', + USER = 'user', + ADMIN = 'admin', + SHARING = 'sharing' + ) + def __init__( self, name="", description="", type="system", deleted=False ): + self.name = name + self.description = description + self.type = type + self.deleted = deleted + +class ActionDatasetRoleAssociation( object ): + def __init__( self, action, dataset, role ): + self.action = action + self.dataset = dataset + self.role = role + +class DefaultUserPermissions( object ): + def __init__( self, user, action, role ): + self.user = user + self.action = action + self.role = role + +class DefaultHistoryPermissions( object ): + def __init__( self, history, action, role ): + self.history = history + self.action = action + self.role = role + class Dataset( object ): states = Bunch( NEW = 'new', QUEUED = 'queued', @@ -370,6 +243,7 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) + permitted_actions = galaxy.security.get_permitted_actions( 'DATASET' ) file_path = "/tmp/" engine = None def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): @@ -454,8 +328,400 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class Old_Dataset( Dataset ): - pass +class DatasetInstance( object ): + """A base class for all 'dataset instances', HDAs, LDAs, etc""" + states = Dataset.states + permitted_actions = Dataset.permitted_actions + def __init__( self, id=None, hid=None, name=None, info=None, blurb=None, peek=None, extension=None, + dbkey=None, metadata=None, history=None, dataset=None, deleted=False, designation=None, + parent_id=None, validation_errors=None, visible=True, create_dataset = False ): + self.name = name or "Unnamed dataset" + self.id = id + self.info = info + self.blurb = blurb + self.peek = peek + self.extension = extension + self.dbkey = dbkey + self.designation = designation + self.metadata = metadata or dict() + self.deleted = deleted + self.visible = visible + # Relationships + if not dataset and create_dataset: + dataset = Dataset() + dataset.flush() + self.dataset = dataset + self.parent_id = parent_id + self.validation_errors = validation_errors + @property + def ext( self ): + return self.extension + def get_dataset_state( self ): + return self.dataset.state + def set_dataset_state ( self, state ): + self.dataset.state = state + self.dataset.flush() #flush here, because hda.flush() won't flush the Dataset object + state = property( get_dataset_state, set_dataset_state ) + def get_file_name( self ): + return self.dataset.get_file_name() + def set_file_name (self, filename): + return self.dataset.set_file_name( filename ) + file_name = property( get_file_name, set_file_name ) + @property + def extra_files_path( self ): + return self.dataset.extra_files_path + @property + def datatype( self ): + return datatypes_registry.get_datatype_by_extension( self.extension ) + def get_metadata( self ): + if not hasattr( self, '_metadata_collection' ): + self._metadata_collection = MetadataCollection( self ) + return self._metadata_collection + def set_metadata( self, bunch ): + # Needs to accept a MetadataCollection, a bunch, or a dict + self._metadata = self.metadata.make_dict_copy( bunch ) + metadata = property( get_metadata, set_metadata ) + # This provide backwards compatibility with using the old dbkey + # field in the database. That field now maps to "old_dbkey" (see mapping.py). + def get_dbkey( self ): + dbkey = self.metadata.dbkey + if not isinstance(dbkey, list): dbkey = [dbkey] + #if dbkey in [["?"], [None], []]: dbkey = [self.old_dbkey] + if dbkey in [[None], []]: return "?" + return dbkey[0] + def set_dbkey( self, value ): + if "dbkey" in self.datatype.metadata_spec: + if not isinstance(value, list): + self.metadata.dbkey = [value] + else: + self.metadata.dbkey = value + #if isinstance(value, list): + # self.old_dbkey = value[0] + #else: + # self.old_dbkey = value + dbkey = property( get_dbkey, set_dbkey ) + def change_datatype( self, new_ext ): + self.clear_associated_files() + datatypes_registry.change_datatype( self, new_ext ) + def get_size( self ): + """Returns the size of the data on disk""" + return self.dataset.get_size() + def set_size( self ): + """Returns the size of the data on disk""" + return self.dataset.set_size() + def has_data( self ): + """Detects whether there is any data""" + return self.dataset.has_data() + def get_raw_data( self ): + """Returns the full data. To stream it open the file_name and read/write as needed""" + return self.datatype.get_raw_data( self ) + def write_from_stream( self, stream ): + """Writes data from a stream""" + self.datatype.write_from_stream(self, stream) + def set_raw_data( self, data ): + """Saves the data on the disc""" + self.datatype.set_raw_data(self, data) + def get_mime( self ): + """Returns the mime type of the data""" + return datatypes_registry.get_mimetype_by_extension( self.extension.lower() ) + def set_peek( self ): + return self.datatype.set_peek( self ) + def init_meta( self, copy_from=None ): + return self.datatype.init_meta( self, copy_from=copy_from ) + def set_meta( self, **kwd ): + self.clear_associated_files( metadata_safe = True ) + return self.datatype.set_meta( self, **kwd ) + def set_readonly_meta( self, **kwd ): + return self.datatype.set_readonly_meta( self, **kwd ) + def missing_meta( self, **kwd ): + return self.datatype.missing_meta( self, **kwd ) + def as_display_type( self, type, **kwd ): + return self.datatype.as_display_type( self, type, **kwd ) + def display_peek( self ): + return self.datatype.display_peek( self ) + def display_name( self ): + return self.datatype.display_name( self ) + def display_info( self ): + return self.datatype.display_info( self ) + def get_converted_files_by_type( self, file_type ): + valid = [] + for assoc in self.implicitly_converted_datasets: + if not assoc.deleted and assoc.type == file_type: + valid.append( assoc.dataset ) + return valid + def clear_associated_files( self, metadata_safe = False, purge = False ): + raise 'Unimplemented' + def get_child_by_designation(self, designation): + for child in self.children: + if child.designation == designation: + return child + return None + def get_converter_types(self): + return self.datatype.get_converter_types( self, datatypes_registry) + def find_conversion_destination( self, accepted_formats, **kwd ): + """Returns ( target_ext, exisiting converted dataset )""" + return self.datatype.find_conversion_destination( self, accepted_formats, datatypes_registry, **kwd ) + def add_validation_error( self, validation_error ): + self.validation_errors.append( validation_error ) + def extend_validation_errors( self, validation_errors ): + self.validation_errors.extend(validation_errors) + def mark_deleted( self, include_children=True ): + self.deleted = True + if include_children: + for child in self.children: + child.mark_deleted() + def mark_undeleted( self, include_children=True ): + self.deleted = False + if include_children: + for child in self.children: + child.mark_undeleted() + def undeletable( self ): + if self.purged: + return False + return True + +class HistoryDatasetAssociation( DatasetInstance ): + def __init__( self, + hid = None, + history = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.hid = hid + # Relationships + self.history = history + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def copy( self, copy_children = False, parent_id = None, target_history = None ): + des = HistoryDatasetAssociation( hid=self.hid, + name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_history_dataset_association=self, + history = target_history ) + des.flush() + des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + + def to_library_dataset_folder_association( self, parent_id = None, target_folder = None ): + + des = LibraryFolderDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_history_dataset_association = self, + folder = target_folder ) + des.flush() + des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id + if target_folder: + target_folder.add_dataset( des ) + for child in self.children: + child_copy = child.to_library_dataset_folder_association( parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + + def clear_associated_files( self, metadata_safe = False, purge = False ): + #metadata_safe = True means to only clear when assoc.metadata_safe == False + for assoc in self.implicitly_converted_datasets: + if not metadata_safe or not assoc.metadata_safe: + assoc.clear( purge = purge ) + +class History( object ): + def __init__( self, id=None, name=None, user=None ): + self.id = id + self.name = name or "Unnamed history" + self.deleted = False + self.purged = False + self.genome_build = None + # Relationships + self.user = user + self.datasets = [] + self.galaxy_sessions = [] + def _next_hid( self ): + # TODO: override this with something in the database that ensures + # better integrity + if len( self.datasets ) == 0: + return 1 + else: + last_hid = 0 + for dataset in self.datasets: + if dataset.hid > last_hid: + last_hid = dataset.hid + return last_hid + 1 + def add_galaxy_session( self, galaxy_session, association=None ): + if association is None: + self.galaxy_sessions.append( GalaxySessionToHistoryAssociation( galaxy_session, self ) ) + else: + self.galaxy_sessions.append( association ) + def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ): + if isinstance( dataset, Dataset ): + dataset = HistoryDatasetAssociation( dataset = dataset ) + dataset.flush() + elif not isinstance( dataset, HistoryDatasetAssociation ): + raise TypeError, "You can only add Dataset and HistoryDatasetAssociation instances to a history." + if parent_id: + for data in self.datasets: + if data.id == parent_id: + dataset.hid = data.hid + break + else: + if set_hid: dataset.hid = self._next_hid() + else: + if set_hid: dataset.hid = self._next_hid() + dataset.history = self + if genome_build not in [None, '?']: + self.genome_build = genome_build + self.datasets.append( dataset ) + def copy( self, target_user = None ): + if not target_user: + target_user = self.user + des = History( user = target_user ) + des.flush() + des.name = self.name + for data in self.datasets: + new_data = data.copy( copy_children = True, target_history = des ) + des.add_dataset( new_data, set_hid = False ) + new_data.flush() + des.hid_counter = self.hid_counter + des.flush() + return des + +class Library( object ): + def __init__( self, name = None, description = None, root_folder = None ): + self.name = name or "Unnamed library" + self.description = description + self.root_folder = root_folder + +class LibraryFolder( object ): + def __init__( self, name = None, description = None, item_count = 0, order_id = None ): + self.name = name or "Unnamed folder" + self.description = description + self.item_count = item_count + self.order_id = order_id + self.genome_build = None + def add_dataset( self, dataset, genome_build=None ): + dataset.folder_id = self.id + dataset.order_id = self.item_count + self.item_count += 1 + if genome_build not in [None, '?']: + self.genome_build = genome_build + def add_folder( self, folder ): + folder.parent_id = self.id + folder.order_id = self.item_count + self.item_count += 1 + + @property + def active_components( self ): + return list( self.active_folders ) + list( self.active_datasets ) + +class LibraryFolderDatasetAssociation( DatasetInstance ): + def __init__( self, + folder = None, + order_id = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.folder = folder + self.order_id = order_id + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + def to_history_dataset_association( self, parent_id = None, target_history = None ): + if target_history: + hid = target_history._next_hid() + else: + hid = None + des = HistoryDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self, + history = target_history, + hid = hid ) + des.flush() + des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id + for child in self.children: + child_copy = child.to_history_dataset_association( parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def copy( self, copy_children = False, parent_id = None, target_folder = None ): + des = LibraryFolderDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self, + folder = target_folder ) + des.flush() + des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + if not self.datatype.copy_safe_peek: + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + def clear_associated_files( self, metadata_safe = False, purge = False ): + return + +class LibraryTag( object ): + def __init__( self, tag ): + self.tag = tag + +class LibraryTagFolderAssociation( object ): + def __init__( self, tag, folder ): + self.tag = tag + self.folder = folder + +class LibraryTagDatasetAssociation( object ): + def __init__( self, tag, dataset ): + self.tag = tag + self.dataset = dataset + +# class Query( object ): +# def __init__( self, name=None, state=None, tool_parameters=None, history=None ): +# self.name = name or "Unnamed query" +# self.state = state +# self.tool_parameters = tool_parameters +# # Relationships +# self.history = history +# self.datasets = [] + class ValidationError( object ): def __init__( self, message=None, err_type=None, attributes=None ): @@ -497,7 +763,16 @@ class Event( object ): self.message = message class GalaxySession( object ): - def __init__( self, id=None, user=None, remote_host=None, remote_addr=None, referer=None, current_history_id=None, session_key=None, is_valid=False, prev_session_id=None ): + def __init__( self, + id=None, + user=None, + remote_host=None, + remote_addr=None, + referer=None, + current_history_id=None, + session_key=None, + is_valid=False, + prev_session_id=None ): self.id = id self.user = user self.remote_host = remote_host @@ -544,7 +819,7 @@ class WorkflowStep( object ): self.tool_inputs = None self.tool_errors = None self.position = None - self.input_connections = None + self.input_connections = [] self.config = None class WorkflowStepConnection( object ): @@ -567,7 +842,10 @@ class StoredWorkflowMenuEntry( object ): class MetadataFile( object ): def __init__( self, dataset = None, name = None ): - self.dataset = dataset + if isinstance( dataset, HistoryDatasetAssociation ): + self.history_dataset = dataset + elif isinstance( dataset, LibraryFolderDatasetAssociation ): + self.library_dataset = dataset self.name = name @property def file_name( self ): diff --git a/lib/galaxy/model/custom_types.py b/lib/galaxy/model/custom_types.py index 77b80bfeab5..0b6ffa2a57e 100644 --- a/lib/galaxy/model/custom_types.py +++ b/lib/galaxy/model/custom_types.py @@ -18,16 +18,15 @@ class JSONType( TypeDecorator ): self.mutable = mutable super( JSONType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_bind_param( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) - return self.jsonifyer.loads( str(buf) ) - - def convert_bind_param( self, value, dialect ): + return self.jsonifyer.dumps( value ) + + def process_result_value( self, value, dialect ): if value is None: return None - return self.impl.convert_bind_param( self.jsonifyer.dumps(value), dialect ) + return self.jsonifyer.loads( str( value ) ) def copy_value( self, value ): if self.mutable: @@ -60,10 +59,10 @@ class MetadataType( JSONType ): self.mutable = mutable super( MetadataType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_result_value( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) + buf = value ret = None try: ret = self.pickler.loads( str(buf) ) @@ -77,7 +76,7 @@ class MetadataType( JSONType ): class TrimmedString( TypeDecorator ): impl = String - def convert_bind_param( self, value, dialect ): + def process_bind_param( self, value, dialect ): """Automatically truncate string values""" if self.impl.length and value is not None: value = value[0:self.impl.length] diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 7fc8aa957a4..dbe3b873785 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -5,23 +5,21 @@ are encapsulated here. import logging log = logging.getLogger( __name__ ) -import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) - import sys import datetime -from sqlalchemy.ext.sessioncontext import SessionContext -from sqlalchemy.ext.assignmapper import assign_mapper -from sqlalchemy.ext.orderinglist import ordering_list - -from sqlalchemy import * from galaxy.model import * +from galaxy.model.orm import * +from galaxy.model.orm.ext.assignmapper import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch +from galaxy.security import GalaxyRBACAgent -metadata = DynamicMetaData( threadlocal=False ) -context = SessionContext( create_session ) +metadata = MetaData() +context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) + +# For backward compatibility with "context.current" +context.current = Session dialect_to_egg = { "sqlite" : "pysqlite>=2", @@ -65,7 +63,6 @@ History.table = Table( "history", metadata, # Column( "state", String( 64 ) ), # Column( "tool_parameters", Pickle() ) ) - HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata, Column( "id", Integer, primary_key=True ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), @@ -73,6 +70,7 @@ HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), Column( "hid", Integer ), Column( "name", TrimmedString( 255 ) ), Column( "info", TrimmedString( 255 ) ), @@ -114,21 +112,139 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) +Group.table = Table( "galaxy_group", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT, index=True, unique=True ), + Column( "deleted", Boolean, index=True, default=False ) ) + +UserGroupAssociation.table = Table( "user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +UserRoleAssociation.table = Table( "user_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupRoleAssociation.table = Table( "group_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +Role.table = Table( "role", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT, index=True, unique=True ), + Column( "description", TEXT ), + Column( "type", TEXT, index=True ), + Column( "deleted", Boolean, index=True, default=False ) ) + +ActionDatasetRoleAssociation.table = Table( "action_dataset_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "action", TEXT ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ) ) + +DefaultUserPermissions.table = Table( "default_user_permissions", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "action", TEXT ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ) ) + +DefaultHistoryPermissions.table = Table( "default_history_permissions", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "action", TEXT ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ) ) + +LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "order_id", Integer ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id", use_alter=True, name='history_dataset_association_dataset_id_fkey' ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id", use_alter=True, name='library_folder_dataset_association_id_fkey' ), nullable=True ), + Column( "name", TrimmedString( 255 ) ), + Column( "info", TrimmedString( 255 ) ), + Column( "blurb", TrimmedString( 255 ) ), + Column( "peek" , TEXT ), + Column( "extension", TrimmedString( 64 ) ), + Column( "metadata", MetadataType(), key="_metadata" ), + Column( "parent_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), + Column( "designation", TrimmedString( 255 ) ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "visible", Boolean ) ) + +Library.table = Table( "library", metadata, + Column( "id", Integer, primary_key=True ), + Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT, index=True ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "description", TEXT ) ) + +LibraryFolder.table = Table( "library_folder", metadata, + Column( "id", Integer, primary_key=True ), + Column( "parent_id", Integer, ForeignKey( "library_folder.id" ), nullable = True, index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "description", TEXT ), + Column( "order_id", Integer ), + Column( "item_count", Integer ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "genome_build", TrimmedString( 40 ) ) ) + +LibraryTag.table = Table( "library_tag", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "text", TEXT ) ) + +LibraryTagFolderAssociation.table = Table( "library_tag_folder_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +LibraryTagDatasetAssociation.table = Table( "library_tag_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), index=True ), + Column( "tag_id", Integer, ForeignKey( "library_tag.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + Job.table = Table( "job", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), Column( "tool_id", String( 255 ) ), - Column( "tool_version", String, default="1.0.0" ), + Column( "tool_version", TEXT, default="1.0.0" ), Column( "state", String( 64 ) ), Column( "info", TrimmedString( 255 ) ), - Column( "command_line", String() ), + Column( "command_line", TEXT ), Column( "param_filename", String( 1024 ) ), Column( "runner_name", String( 255 ) ), - Column( "stdout", String() ), - Column( "stderr", String() ), - Column( "traceback", String() ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), Column( "job_runner_name", String( 255 ) ), Column( "job_runner_external_id", String( 255 ) ) ) @@ -188,7 +304,7 @@ StoredWorkflow.table = Table( "stored_workflow", metadata, Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), Column( "latest_workflow_id", Integer, ForeignKey( "workflow.id", use_alter=True, name='stored_workflow_latest_workflow_id_fk' ), index=True ), - Column( "name", String ), + Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), ) @@ -197,7 +313,7 @@ Workflow.table = Table( "workflow", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True, nullable=False ), - Column( "name", String ), + Column( "name", TEXT ), Column( "has_cycles", Boolean ), Column( "has_errors", Boolean ) ) @@ -208,8 +324,8 @@ WorkflowStep.table = Table( "workflow_step", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False ), Column( "type", String(64) ), - Column( "tool_id", String ), - Column( "tool_version", String ), # Reserved for future + Column( "tool_id", TEXT ), + Column( "tool_version", TEXT ), # Reserved for future Column( "tool_inputs", JSONType ), Column( "tool_errors", JSONType ), Column( "position", JSONType ), @@ -222,8 +338,8 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "id", Integer, primary_key=True ), Column( "output_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), Column( "input_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), - Column( "output_name", String ), - Column( "input_name", String) + Column( "output_name", TEXT ), + Column( "input_name", TEXT) ) StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata, @@ -242,6 +358,7 @@ MetadataFile.table = Table( "metadata_file", metadata, Column( "id", Integer, primary_key=True ), Column( "name", String ), Column( "hda_id", Integer, ForeignKey( "history_dataset_association.id" ), index=True, nullable=True ), + Column( "lda_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), index=True, nullable=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, index=True, default=now, onupdate=now ), Column( "deleted", Boolean, index=True, default=False ), @@ -257,27 +374,32 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab dataset=relation( Dataset, primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), - history=relation( - History, - primaryjoin=( History.table.c.id == HistoryDatasetAssociation.table.c.history_id ) ), + # .history defined in History mapper copied_to_history_dataset_associations=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id], uselist=False ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), children=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ) + backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ) ) ) assign_mapper( context, Dataset, Dataset.table, properties=dict( history_associations=relation( HistoryDatasetAssociation, - primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ) + primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ) ) ) @@ -302,13 +424,118 @@ assign_mapper( context, History, History.table, ) ) assign_mapper( context, User, User.table, - properties=dict( histories=relation( History, backref="user", + properties=dict( histories=relation( History, backref="user", order_by=desc(History.table.c.update_time) ), + active_histories=relation( History, primaryjoin=( ( History.table.c.user_id == User.table.c.id ) & ( not_( History.table.c.deleted ) ) ), order_by=desc( History.table.c.update_time ) ), stored_workflow_menu_entries=relation( StoredWorkflowMenuEntry, backref="user", cascade="all, delete-orphan", collection_class=ordering_list( 'order_index' ) ) ) ) +assign_mapper( context, Group, Group.table, + properties=dict( users=relation( UserGroupAssociation ) ) ) + +assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, + properties=dict( user=relation( User, backref = "groups" ), + group=relation( Group, backref = "members" ) ) ) + +assign_mapper( context, DefaultUserPermissions, DefaultUserPermissions.table, + properties=dict( user=relation( User, backref = "default_permissions" ), + role=relation( Role ) ) ) + +assign_mapper( context, DefaultHistoryPermissions, DefaultHistoryPermissions.table, + properties=dict( history=relation( History, backref = "default_permissions" ), + role=relation( Role ) ) ) + +assign_mapper( context, Role, Role.table, + properties=dict( + users=relation( UserRoleAssociation ), + groups=relation( GroupRoleAssociation ) + ) +) + +assign_mapper( context, UserRoleAssociation, UserRoleAssociation.table, + properties=dict( + user=relation( User, backref="roles" ), + non_private_roles=relation( User, + backref="non_private_roles", + primaryjoin=( ( User.table.c.id == UserRoleAssociation.table.c.user_id ) & ( UserRoleAssociation.table.c.role_id == Role.table.c.id ) & not_( Role.table.c.type == 'private' ) ) ), + role=relation( Role ) + ) +) + +assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, + properties=dict( + group=relation( Group, backref="roles" ), + role=relation( Role ) + ) +) + +assign_mapper( context, ActionDatasetRoleAssociation, ActionDatasetRoleAssociation.table, + properties=dict( + dataset=relation( Dataset, backref="actions" ), + role=relation( Role, backref="actions" ) + ) +) + +assign_mapper( context, Library, Library.table, + properties=dict( + root_folder=relation( LibraryFolder, + backref=backref( "library_root" ) ) + ) ) + +assign_mapper( context, LibraryFolder, LibraryFolder.table, + properties=dict( + folders=relation( + LibraryFolder, + primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ), remote_side=[LibraryFolder.table.c.id] ) ), + active_folders=relation( LibraryFolder, + primaryjoin=( ( LibraryFolder.table.c.parent_id == LibraryFolder.table.c.id ) & ( not_( LibraryFolder.table.c.deleted ) ) ), + order_by=asc( LibraryFolder.table.c.order_id ), + lazy=True, #"""sqlalchemy.exceptions.ArgumentError: Error creating eager relationship 'active_folders' on parent class '' to child class '': Cant use eager loading on a self referential relationship.""" + viewonly=True ), + active_datasets=relation( LibraryFolderDatasetAssociation, + primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.folder_id == LibraryFolder.table.c.id ) & ( not_( LibraryFolderDatasetAssociation.table.c.deleted ) ) ), + order_by=asc( LibraryFolderDatasetAssociation.table.c.order_id ), + lazy=False, + viewonly=True ), + tags=relation( + LibraryTagFolderAssociation, + primaryjoin=( LibraryFolder.table.c.id == LibraryTagFolderAssociation.table.c.folder_id ), + backref=backref( "folders" ) ) + ) ) + +assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAssociation.table, + properties=dict( + dataset=relation( Dataset ), + folder=relation( + LibraryFolder, + backref=backref( "datasets" ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_library_folder_dataset_association", primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + children=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + tags=relation( + LibraryTagDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + backref=backref( "datasets" ) ) + ) ) + +assign_mapper( context, LibraryTag, LibraryTag.table ) + +assign_mapper( context, LibraryTagFolderAssociation, LibraryTagFolderAssociation.table, + properties=dict( tag=relation( LibraryTag ), + folder=relation( LibraryFolder ) ) ) + +assign_mapper( context, LibraryTagDatasetAssociation, LibraryTagDatasetAssociation.table, + properties=dict( tag=relation( LibraryTag ), + dataset=relation( LibraryFolderDatasetAssociation ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -373,18 +600,19 @@ assign_mapper( context, StoredWorkflowMenuEntry, StoredWorkflowMenuEntry.table, properties=dict( stored_workflow=relation( StoredWorkflow ) ) ) assign_mapper( context, MetadataFile, MetadataFile.table, - properties=dict( dataset=relation( HistoryDatasetAssociation ) ) ) + properties=dict( history_dataset=relation( HistoryDatasetAssociation ), library_dataset=relation( LibraryFolderDatasetAssociation ) ) ) def db_next_hid( self ): """ Override __next_hid to generate from the database in a concurrency safe way. """ - conn = self.table.engine.contextual_connect() + conn = object_session( self ).connection() + table = self.table trans = conn.begin() try: - next_hid = select( [self.c.hid_counter], self.c.id == self.id, for_update=True ).scalar() - self.table.update( self.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) trans.commit() return next_hid except: @@ -413,18 +641,33 @@ def init( file_path, url, engine_options={}, create_tables=False ): # Create the database engine engine = create_engine( url, **engine_options ) # Connect the metadata to the database. - metadata.connect( engine ) - ## metadata.engine.echo = True + metadata.bind = engine + # Clear any existing contextual sessions and reconfigure + Session.remove() + Session.configure( bind=engine ) # Create tables if needed if create_tables: metadata.create_all() # metadata.engine.commit() # Pack everything into a bunch result = Bunch( **globals() ) - result.engine = metadata.engine - result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) - result.context = context + result.engine = engine + result.flush = lambda *args, **kwargs: Session.flush( *args, **kwargs ) + result.session = Session + # For backward compatibility with "model.context.current" + result.context = Session result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = GalaxyRBACAgent( result ) + # Create private roles if necessary. + if not result.Role.query().all(): + for user in result.User.query().all(): + role = Role( name = user.email, description = 'Private Role for ' + user.email, type = 'private' ) + role.flush() + ura = UserRoleAssociation( user = user, role = role ) + ura.flush() + dup = DefaultUserPermissions( user = user, action = result.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS.action, role = role ) + dup.flush() return result def get_suite(): diff --git a/lib/galaxy/model/mapping_tests.py b/lib/galaxy/model/mapping_tests.py index d0734a418a5..2a1c62b2328 100644 --- a/lib/galaxy/model/mapping_tests.py +++ b/lib/galaxy/model/mapping_tests.py @@ -22,13 +22,13 @@ class MappingTests( unittest.TestCase ): model.context.current.flush() model.context.current.clear() # Check - users = model.User.select() + users = model.User.query().all() assert len( users ) == 1 assert users[0].email == "james@foo.bar.baz" assert users[0].password == "password" assert len( users[0].histories ) == 1 assert users[0].histories[0].name == "History 1" - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == ( "H" * 255 ) assert hists[0].user == users[0] @@ -40,7 +40,7 @@ class MappingTests( unittest.TestCase ): hists[1].name = "History 2b" model.context.current.flush() model.context.current.clear() - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == "History 2b" # gvk TODO need to ad test for GalaxySessions, but not yet sure what they should look like. diff --git a/lib/galaxy/model/orm/__init__.py b/lib/galaxy/model/orm/__init__.py new file mode 100644 index 00000000000..433e1e26f45 --- /dev/null +++ b/lib/galaxy/model/orm/__init__.py @@ -0,0 +1,7 @@ +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) + +from sqlalchemy import * +from sqlalchemy.orm import * + +from sqlalchemy.ext.orderinglist import ordering_list diff --git a/lib/galaxy/model/orm/ext/__init__.py b/lib/galaxy/model/orm/ext/__init__.py new file mode 100644 index 00000000000..230b8aec908 --- /dev/null +++ b/lib/galaxy/model/orm/ext/__init__.py @@ -0,0 +1,3 @@ +""" +Galaxy specific SQLAlchemy extensions. +""" \ No newline at end of file diff --git a/lib/galaxy/model/orm/ext/assignmapper.py b/lib/galaxy/model/orm/ext/assignmapper.py new file mode 100644 index 00000000000..d2b7e2ca7b4 --- /dev/null +++ b/lib/galaxy/model/orm/ext/assignmapper.py @@ -0,0 +1,62 @@ +""" +This is similar to the assignmapper extensions in SQLAclhemy 0.3 and 0.4 but +with some compatibility fixes. It assumes that the session is a ScopedSession, +and thus has the "mapper" method to attach contextual mappers to a class. It +adds additional query and session methods to the class to support the +SQLAlchemy 0.3 style of access. The following methods which would normally be +accessed through "Object.query().method()" are available directly through the +object: + + 'get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances' + +Additionally, the following Session methods, which normally accept an instance +or list of instances, are available directly through the objects, e.g. +"Session.flush( [instance] )" can be performed as "instance.flush()": + + 'refresh', 'expire', 'delete', 'expunge', 'update' +""" + +__all__ = [ 'assign_mapper' ] + +from sqlalchemy import util, exceptions +import types +from sqlalchemy.orm import mapper, Query + +def _monkeypatch_query_method( name, session, class_ ): + def do(self, *args, **kwargs): + ## util.warn_deprecated('Query methods on the class are deprecated; use %s.query.%s instead' % (class_.__name__, name)) + return getattr( class_.query, name)(*args, **kwargs) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, classmethod(do)) + +def _monkeypatch_session_method(name, session, class_, make_list=False): + def do(self, *args, **kwargs): + if make_list: + self = [ self ] + return getattr(session, name)( self, *args, **kwargs ) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, do) + +def assign_mapper( session, class_, *args, **kwargs ): + m = class_.mapper = session.mapper( class_, *args, **kwargs ) + for name in ('get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances'): + _monkeypatch_query_method(name, session, class_) + for name in ('refresh', 'expire', 'delete', 'expunge', 'update'): + _monkeypatch_session_method(name, session, class_) + for name in ( 'flush', ): + _monkeypatch_session_method( name, session, class_, make_list=True ) + return m diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..afdb673c090 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,340 @@ +""" +Galaxy Security + +""" +import logging +from galaxy.util.bunch import Bunch +from galaxy.model.orm import * + +log = logging.getLogger(__name__) + +class Action( object ): + def __init__( self, action, description, model ): + self.action = action + self.description = description + self.model = model + +class RBACAgent: + """Class that handles galaxy security""" + permitted_actions = Bunch( + DATASET_EDIT_METADATA = Action( + "edit metadata", "Role members can edit this dataset's metadata in the library", "grant" ), + DATASET_MANAGE_PERMISSIONS = Action( + "manage permissions", "Role members can manage the roles associated with this dataset", "grant" ), + DATASET_ACCESS = Action( + "access", "Role members can import this dataset into their history for analysis", "restrict" ) + ) + def get_action( self, name, default=None ): + """ + Get a permitted action by its dict key or action name + """ + for k, v in self.permitted_actions.items(): + if k == name or v.action == name: + return v + return default + def get_actions( self ): + """ + Get all permitted actions as a list + """ + return self.permitted_actions.__dict__.values() + def allow_action( self, user, action, **kwd ): + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def guess_derived_permissions_for_datasets( self, datasets = [] ): + raise "Unimplemented Method" + def associate_components( self, **kwd ): + raise 'No valid method of associating provided components: %s' % kwd + def create_private_user_role( self, user ): + raise "Unimplemented Method" + def get_private_user_role( self, user ): + raise "Unimplemented Method" + def user_set_default_permissions( self, user, permissions = None, history = False, dataset = False ): + raise "Unimplemented Method" + def setup_new_user( self, user ): + self.create_private_user_role( user ) + self.user_set_default_permissions( user, history = True, dataset = True ) + #self.associate_components( user=user, group=self.get_public_group() ) + def history_set_default_permissions( self, history, permissions=None, dataset=False, bypass_manage_permission=False ): + raise "Unimplemented Method" + def set_dataset_permissions( self, dataset, permissions ): + raise "Unimplemented Method" + def get_component_associations( self, **kwd ): + raise "Unimplemented Method" + def components_are_associated( self, **kwd ): + return bool( self.get_component_associations( **kwd ) ) + def convert_permitted_action_strings( self, permitted_action_strings ): + """ + When getting permitted actions from an untrusted source like a + form, ensure that they match our actual permitted actions. + """ + return filter( lambda x: x is not None, [ self.permitted_actions.get( action_string ) for action_string in permitted_action_strings ] ) + +class GalaxyRBACAgent( RBACAgent ): + def __init__( self, model, permitted_actions=None ): + self.model = model + if permitted_actions: + self.permitted_actions = permitted_actions + def allow_action( self, user, action, **kwd ): + if 'dataset' in kwd: + return self.allow_dataset_action( user, action, kwd['dataset'] ) + raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + def allow_dataset_action( self, user, action, dataset ): + """Returns true when user has permission to perform an action""" + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + if not user: + if action == self.permitted_actions.DATASET_ACCESS and action.action not in [ adra.action for adra in dataset.actions ]: + return True # anons only get access, and only if there are no roles required for the access action + # other actions (or if the dataset has roles defined for the access action) fall through to the false below + elif action.action not in [ adra.action for adra in dataset.actions ]: + if action.model == 'restrict': + return True # implicit access to restrict-style actions if the dataset does not have the action + # grant-style actions fall through to the false below + else: + user_role_ids = sorted( [ r.id for r in user.all_roles() ] ) + perms = self.get_dataset_permissions( dataset ) + if action in perms.keys(): + # the filter() returns a list of the dataset's role ids + # of which the user is not a member. so an empty list + # means the user has all of the required roles. + if not filter( lambda x: x not in user_role_ids, [ r.id for r in perms[ action ] ] ): + return True # user has all of the roles required to perform the action + # fall through to the false. user is missing at least one required role + return False # default is to reject + def guess_derived_permissions_for_datasets( self, datasets=[] ): + """Returns a dict of { action : [ role, role, ... ] } for the output dataset based upon provided datasets""" + perms = {} + for dataset in datasets: + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + these_perms = {} + # initialize blank perms + for action in self.get_actions(): + these_perms[ action ] = [] + # collect this dataset's perms + these_perms = self.get_dataset_permissions( dataset ) + # join or intersect this dataset's permissions with others + for action, roles in these_perms.items(): + if action not in perms.keys(): + perms[ action ] = roles + else: + if action.model == 'grant': + # intersect existing roles with new roles + perms[ action ] = filter( lambda x: x in perms[ action ], roles ) + elif action.model == 'restrict': + # join existing roles with new roles + perms[ action ].extend( filter( lambda x: x not in perms[ action ], roles ) ) + return perms + def associate_components( self, **kwd ): + if 'user' in kwd: + if 'group' in kwd: + return self.associate_user_group( kwd['user'], kwd['group'] ) + elif 'role' in kwd: + return self.associate_user_role( kwd['user'], kwd['role'] ) + elif 'role' in kwd: + if 'group' in kwd: + return self.associate_group_role( kwd['group'], kwd['role'] ) + if 'action' in kwd: + if 'dataset' in kwd and 'role' in kwd: + return self.associate_action_dataset_role( kwd['action'], kwd['dataset'], kwd['role'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_user_group( self, user, group ): + assoc = self.model.UserGroupAssociation( user, group ) + assoc.flush() + return assoc + def associate_user_role( self, user, role ): + assoc = self.model.UserRoleAssociation( user, role ) + assoc.flush() + return assoc + def associate_group_role( self, group, role ): + assoc = self.model.GroupRoleAssociation( group, role ) + assoc.flush() + return assoc + def associate_action_dataset_role( self, action, dataset, role ): + assoc = self.model.ActionDatasetRoleAssociation( action, dataset, role ) + assoc.flush() + return assoc + def create_private_user_role( self, user ): + # Create private role + role = self.model.Role( name=user.email, description='Private Role for ' + user.email, type=self.model.Role.types.PRIVATE ) + role.flush() + # Add user to role + self.associate_components( role=role, user=user ) + return role + def get_private_user_role( self, user, auto_create=False ): + role = self.model.Role.filter( and_( self.model.Role.table.c.name == user.email, + self.model.Role.table.c.type == self.model.Role.types.PRIVATE ) ).first() + if not role: + if auto_create: + return self.create_private_user_role( user ) + else: + return None + return role + def user_set_default_permissions( self, user, permissions = {}, history = False, dataset = False ): + if user is None: + return None + if not permissions: + permissions = { self.permitted_actions.DATASET_MANAGE_PERMISSIONS : [ self.get_private_user_role( user, auto_create=True ) ] } + # Delete all of the previous defaults + for dup in user.default_permissions: + dup.delete() + dup.flush() + # Add the new defaults (if any) + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for role in roles: + dup = self.model.DefaultUserPermissions( user, action, role ) + dup.flush() + if history: + for history in user.active_histories: + self.history_set_default_permissions( history, permissions=permissions, dataset=dataset ) + def user_get_default_permissions( self, user ): + perms = {} + for action in self.get_actions(): + perms[ action ] = [] + for dup in user.default_permissions: + perms[ self.get_action( dup.action ) ].append( dup.role ) + return perms + def history_set_default_permissions( self, history, permissions = {}, dataset = False, bypass_manage_permission = False ): + if not history.user: + return None # default permissions on a userless history are none + if not permissions: + permissions = self.user_get_default_permissions( history.user ) + for dhp in history.default_permissions: + dhp.delete() + dhp.flush() + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for role in roles: + dhp = self.model.DefaultHistoryPermissions( history, action, role ) + dhp.flush() + if dataset: + for hda_in_history in history.datasets: + if len( hda_in_history.dataset.library_associations ): + continue # dataset has a library association, don't change the permissions + if len( [ hda for hda in hda_in_history.dataset.history_associations if hda.history not in history.user.histories ] ): + continue # dataset has a history association in a history the user doesn't own, don't change the permissions + # bypass is used to change permissions of datasets in a userless history when logging in + if bypass_manage_permission or self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=hda_in_history.dataset ): + self.set_dataset_permissions( hda_in_history.dataset, permissions ) + def history_get_default_permissions( self, history ): + perms = {} + for action in self.get_actions(): + perms[ action ] = [] + for dhp in history.default_permissions: + perms[ self.get_action( dhp.action ) ].append( dhp.role ) + return perms + def set_dataset_permissions( self, dataset, permissions={} ): + # to delete permission on an action, pass in a blank list of + # roles with that action. leaving an action out of the perm + # dict simply leaves those perms untouched (if they exist) + incoming_actions = [] + for action in permissions.keys(): + if isinstance( action, Action ): + action = action.action + incoming_actions.append( action ) + for adra in dataset.actions: + if adra.action in incoming_actions: + adra.delete() + adra.flush() + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for role in roles: + self.associate_components( action=action, dataset=dataset, role=role ) + def get_dataset_permissions( self, dataset ): + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + perms = {} + for action in self.model.Dataset.permitted_actions.__dict__.values(): + perms[ action ] = [] + for adra in dataset.actions: + perms[ self.get_action( adra.action ) ].append( adra.role ) + return perms + def copy_dataset_permissions( self, src, dst ): + if not isinstance( src, self.model.Dataset ): + src = src.dataset + if not isinstance( dst, self.model.Dataset ): + dst = dst.dataset + self.set_dataset_permissions( dst, self.get_dataset_permissions( src ) ) + def privately_share_dataset( self, dataset, users = [] ): + intersect = None + for user in users: + roles = [ ura.role for ura in user.roles if ura.role.type == self.model.Role.types.SHARING ] + if intersect is None: + intersect = roles + else: + new_intersect = [] + for role in roles: + if role in intersect: + new_intersect.append( role ) + intersect = new_intersect + sharing_role = None + if intersect: + for role in intersect: + if not filter( lambda x: x not in users, [ ura.user for ura in role.users ] ): + # only use a role if it contains ONLY the users we're sharing with + sharing_role = role + break + if sharing_role is None: + sharing_role = self.model.Role( name = "Sharing role for: " + ", ".join( [ u.email for u in users ] ), + type = self.model.Role.types.SHARING ) + sharing_role.flush() + for user in users: + self.associate_components( user=user, role=sharing_role ) + self.set_dataset_permissions( dataset, { self.permitted_actions.DATASET_ACCESS : [ sharing_role ] } ) + def set_entity_role_associations( self, roles=[], users=[], groups=[], delete_existing_assocs=True ): + for role in roles: + if delete_existing_assocs: + for a in role.users + role.groups: + a.delete() + a.flush() + for user in users: + self.associate_components( user=user, role=role ) + for group in groups: + self.associate_components( group=group, role=role ) + def get_component_associations( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' + if 'dataset' in kwd: + if 'action' in kwd: + return self.model.ActionDatasetRoleAssociation.filter_by( action = kwd['action'].action, dataset_id = kwd['dataset'].id ).first() + elif 'user' in kwd: + if 'group' in kwd: + return self.model.UserGroupAssociation.filter_by( group_id = kwd['group'].id, user_id = kwd['user'].id ).first() + elif 'role' in kwd: + return self.model.UserRoleAssociation.filter_by( role_id = kwd['role'].id, user_id = kwd['user'].id ).first() + elif 'group' in kwd: + if 'role' in kwd: + return self.model.GroupRoleAssociation.filter_by( role_id = kwd['role'].id, group_id = kwd['group'].id ).first() + raise 'No valid method of associating provided components: %s' % kwd + def check_folder_contents( self, user, entry ): + """ + Return true if there are any datasets under 'folder' that the + user has access permission on. We do this a lot and it's a + pretty inefficient method, optimizations are welcomed. + """ + if isinstance( entry, self.model.Library ): + return self.check_folder_contents( user, entry.root_folder ) + elif isinstance( entry, self.model.LibraryFolderDatasetAssociation ): + return self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=entry ) + elif isinstance( entry, self.model.LibraryFolder ): + for dataset in entry.active_datasets: + if self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=dataset ): + return True + for folder in entry.active_folders: + if self.check_folder_contents( user, folder ): + return True + return False + else: + raise 'Passed an illegal object to check_folder_contents: %s' % type( entry ) + +def get_permitted_actions( self, filter=None ): + '''Utility method to return a subset of RBACAgent's permitted actions''' + if filter is None: + return RBACAgent.permitted_actions + if not filter.endswith('_'): + filter += '_' + tmp_bunch = Bunch() + [tmp_bunch.__dict__.__setitem__(k, v) for k, v in RBACAgent.permitted_actions.items() if k.startswith(filter)] + return tmp_bunch diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 3c0eb30631c..0e04a0b4d86 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,7 +1085,7 @@ class Tool: redirect_url_params = redirect_url_params.replace( "\n", " " ).replace( "\r", " " ) return redirect_url_params - def parse_redirect_url( self, inp_data, param_dict ): + def parse_redirect_url( self, data, param_dict ): """Parse the REDIRECT_URL tool param""" # Tools that send data to an external application via a redirect must include the following 3 tool params: # REDIRECT_URL - the url to which the data is being sent @@ -1104,9 +1104,6 @@ class Tool: rup_dict[ p_name ] = p_val DATA_URL = param_dict.get( 'DATA_URL', None ) assert DATA_URL is not None, "DATA_URL parameter missing in tool config." - # Get the dataset - there should only be 1 - for name in inp_data.keys(): - data = inp_data[ name ] DATA_URL += "/%s/display" % str( data.id ) redirect_url += "?DATA_URL=%s" % DATA_URL # Add the redirect_url_params to redirect_url @@ -1215,6 +1212,7 @@ class Tool: else: visible = False ext = fields.pop(0).lower() child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + self.app.security_agent.copy_dataset_permissions( outdata.dataset, child_dataset.dataset ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1251,6 +1249,7 @@ class Tool: ext = fields.pop(0).lower() # Create new primary dataset primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + self.app.security_agent.copy_dataset_permissions( outdata.dataset, primary_data.dataset ) primary_data.flush() # Move data from temp location to dataset location shutil.move( filename, primary_data.file_name ) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index 833f78ea6b5..40fca521465 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -44,6 +44,9 @@ class DefaultToolAction( object ): assoc.dataset = new_data assoc.flush() data = new_data + # TODO, Nate: Make sure the permitted actions here are appropriate. + if data and not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset=data ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id return data if isinstance( input, DataToolParameter ): if isinstance( value, list ): @@ -80,6 +83,14 @@ class DefaultToolAction( object ): data = NoneDataset( datatypes_registry = trans.app.datatypes_registry ) if data.dbkey not in [None, '?']: input_dbkey = data.dbkey + + # Determine output dataset permission/roles list + existing_datasets = [ inp for inp in inp_data.values() if inp ] + if existing_datasets: + output_permissions = trans.app.security_agent.guess_derived_permissions_for_datasets( existing_datasets ) + else: + # No valid inputs, we will use history defaults + output_permissions = trans.app.security_agent.history_get_default_permissions( trans.history ) # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -133,6 +144,7 @@ class DefaultToolAction( object ): data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) # Commit the dataset immediately so it gets database assigned unique id data.flush() + trans.app.security_agent.set_dataset_permissions( data.dataset, output_permissions ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -192,6 +204,9 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: + # TODO, Nate: Make sure the permitted actions here are appropriate. + if not trans.app.security_agent.allow_action( trans.user, dataset.permitted_actions.DATASET_ACCESS, dataset=dataset ): + raise "User does not have permission to use a dataset (%s) provided for input." % data.id job.add_input_dataset( name, dataset ) else: job.add_input_dataset( name, None ) @@ -203,7 +218,15 @@ class DefaultToolAction( object ): # include something that can be retrieved from the params ( e.g., REDIRECT_URL ) to keep the job # from being queued. if 'REDIRECT_URL' in incoming: - redirect_url = tool.parse_redirect_url( inp_data, incoming ) + # Get the dataset - there should only be 1 + for name in inp_data.keys(): + dataset = inp_data[ name ] + redirect_url = tool.parse_redirect_url( dataset, incoming ) + # GALAXY_URL should be include in the tool params to enable the external application + # to send back to the current Galaxy instance + GALAXY_URL = incoming.get( 'GALAXY_URL', None ) + assert GALAXY_URL is not None, "GALAXY_URL parameter missing in tool config." + redirect_url += "&GALAXY_URL=%s" % GALAXY_URL # Job should not be queued, so set state to ok job.state = JOB_OK job.info = "Redirected to: %s" % redirect_url diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 25bdb29baed..90b98da39ce 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -36,12 +36,24 @@ class UploadToolAction( object ): return self.upload_empty( trans, "Error:", str( e ) ) if url_paste not in [ None, "" ]: if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + # If we were sent a DATA_URL from an external application in a post, NAME and INFO + # values should be in the request + if 'NAME' in incoming and incoming[ 'NAME' ] not in [ "None", None ]: + NAME = incoming[ 'NAME' ] + else: + NAME = '' + if 'INFO' in incoming and incoming[ 'INFO' ] not in [ "None", None ]: + INFO = incoming[ 'INFO' ] + else: + INFO = "uploaded url" url_paste = url_paste.replace( '\r', '' ).split( '\n' ) for line in url_paste: line = line.rstrip( '\r\n' ) if line: + if not NAME: + NAME = line try: - data_list.append( self.add_file( trans, urllib.urlopen( line ), line, file_type, dbkey, info="uploaded url", space_to_tab=space_to_tab ) ) + data_list.append( self.add_file( trans, urllib.urlopen( line ), NAME, file_type, dbkey, info=INFO, space_to_tab=space_to_tab ) ) except Exception, e: return self.upload_empty( trans, "Error:", str( e ) ) else: @@ -65,8 +77,9 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.name = err_code + data = trans.app.model.HistoryDatasetAssociation( create_dataset=True ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_permissions( trans.history ) ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -85,12 +98,12 @@ class UploadToolAction( object ): if not os.path.getsize( temp_name ) > 0: raise BadFileException( "you attempted to upload an empty file." ) - # See if we have a gzipped file, which, if it passes our restrictions, we'll decompress on the fly. + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress on the fly. is_gzipped, is_valid = self.check_gzip( temp_name ) if is_gzipped and not is_valid: raise BadFileException( "you attempted to upload an inappropriate file." ) elif is_gzipped and is_valid: - #We need to decompress the temp_name file + # We need to uncompress the temp_name file CHUNK_SIZE = 2**20 # 1Mb fd, uncompressed = tempfile.mkstemp() gzipped_file = gzip.GzipFile( temp_name ) @@ -159,6 +172,7 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_permissions( trans.history ) ) data.name = file_name data.dbkey = dbkey data.info = info diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 2b1cbb5abb0..882849f7bf2 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -3,16 +3,13 @@ Basic tool parameters. """ import logging, string, sys, os - from elementtree.ElementTree import XML, Element - from galaxy import config, datatypes, util from galaxy.web import form_builder - import validation, dynamic_options - # For BaseURLToolParameter from galaxy.web import url_for +import galaxy.model log = logging.getLogger(__name__) @@ -980,30 +977,16 @@ class DrillDownSelectToolParameter( ToolParameter ): class DataToolParameter( ToolParameter ): + # TODO, Nate: Make sure the following unit tests appropriately test the dataset security + # components. Add as many additional tests as necessary. """ Parameter that takes on one (or many) or a specific set of values. TODO: There should be an alternate display that allows single selects to be displayed as radio buttons and multiple selects as a set of checkboxes - >>> # Mock up a history (not connected to database) - >>> from galaxy.model import History, HistoryDatasetAssociation - >>> from galaxy.util.bunch import Bunch - >>> hist = History() - >>> hist.flush() - >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) ) - >>> p = DataToolParameter( None, XML( '' ) ) - >>> print p.name - blah - >>> print p.get_html( trans=Bunch( history=hist ) ) - + TODO: The following must be fixed to test correctly for the new security_check tag in the DataToolParameter ( the last test below is broken ) + Nate's next passs at the dataset security stuff will dramatically alter this anyway. """ def __init__( self, tool, elem ): @@ -1050,28 +1033,38 @@ class DataToolParameter( ToolParameter ): value = [ value ] field = form_builder.SelectField( self.name, self.multiple, None, self.refresh_on_change ) # CRUCIAL: the dataset_collector function needs to be local to DataToolParameter.get_html_field() - def dataset_collector( datasets, parent_hid ): - for i, data in enumerate( datasets ): + def dataset_collector( hdas, parent_hid ): + for i, hda in enumerate( hdas ): if parent_hid is not None: hid = "%s.%d" % ( parent_hid, i + 1 ) else: - hid = str( data.hid ) - if not data.deleted and data.state not in [data.states.ERROR, data.states.DISCARDED] and data.visible: - if self.options and data.get_dbkey() != filter_value: + hid = str( hda.hid ) + if not hda.dataset.state in [galaxy.model.Dataset.states.ERROR, galaxy.model.Dataset.states.DISCARDED] and \ + hda.visible and \ + trans.app.security_agent.allow_action( trans.user, hda.permitted_actions.DATASET_ACCESS, dataset=hda ): + # If we are sending data to an external application, then we need to make sure there are no roles + # associated with the dataset that restrict it's access from "public". We determine this by sending + # None as the user to the allow_action method. + if self.tool.tool_type == 'data_destination': + if not trans.app.security_agent.allow_action( None, hda.permitted_actions.DATASET_ACCESS, dataset=hda ): + continue + if self.options and hda.get_dbkey() != filter_value: continue - if isinstance( data.datatype, self.formats): - selected = ( value and ( data in value ) ) - field.add_option( "%s: %s" % ( hid, data.name[:30] ), data.id, selected ) + if isinstance( hda.datatype, self.formats): + selected = ( value and ( hda in value ) ) + field.add_option( "%s: %s" % ( hid, hda.name[:30] ), hda.id, selected ) else: - target_ext, converted_dataset = data.find_conversion_destination( self.formats, converter_safe = self.converter_safe( other_values, trans ) ) + target_ext, converted_dataset = hda.find_conversion_destination( self.formats, converter_safe = self.converter_safe( other_values, trans ) ) if target_ext: if converted_dataset: - data = converted_dataset - selected = ( value and ( data in value ) ) - field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected ) + hda = converted_dataset + if not trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=hda.dataset ): + continue + selected = ( value and ( hda in value ) ) + field.add_option( "%s: (as %s) %s" % ( hid, target_ext, hda.name[:30] ), hda.id, selected ) # Also collect children via association object - dataset_collector( data.children, hid ) - dataset_collector( history.datasets, None ) + dataset_collector( hda.children, hid ) + dataset_collector( history.active_datasets, None ) some_data = bool( field.options ) if some_data: if value is None or len( field.options ) == 1: diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fd9b4be90d8..3bbc17382c7 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,24 +1,1173 @@ - +import shutil, StringIO, operator, urllib, gzip, tempfile +from galaxy import util, datatypes from galaxy.web.base.controller import * -import logging, sets, time +from galaxy.datatypes import sniff +from galaxy.security import RBACAgent +import galaxy.model +from galaxy.model.orm import * +from xml.sax.saxutils import escape, unescape +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) +import sqlalchemy as sa +import logging log = logging.getLogger( __name__ ) +entities = { '@': 'FuNkYaT' } +unentities = { 'FuNkYaT' : '@' } +no_privilege_msg = "You must have Galaxy administrator privileges to use this feature." + class Admin( BaseController ): + def user_is_admin( self, trans ): + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + if not admin_users: + return False + user = trans.get_user() + if not user: + return False + if not user.email in admin_users: + return False + return True @web.expose def index( self, trans, **kwd ): - msg = '' - if 'action' in kwd: - if kwd['action'] == "tool_reload": - msg = self.tool_reload( **kwd ) - return trans.fill_template( 'admin_main.mako', toolbox=self.app.toolbox, msg=msg ) - - def tool_reload( self, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) - if params.passwd==self.app.config.admin_pass: - tool_id = params.tool_id - self.app.toolbox.reload( tool_id ) - msg = 'Reloaded tool: ' + tool_id + msg = params.msg + return trans.fill_template( '/admin/index.mako', msg=msg ) + @web.expose + def center( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + return trans.fill_template( '/admin/center.mako' ) + @web.expose + def reload_tool( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + @web.expose + def tool_reload( self, trans, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + tool_id = params.tool_id + self.app.toolbox.reload( tool_id ) + msg = 'Reloaded tool: ' + tool_id + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + + # Galaxy Role Stuff + @web.expose + def roles( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/dataset_security/roles.mako', + roles=trans.app.model.Role.query() \ + .filter( trans.app.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE ) \ + .order_by( trans.app.model.Role.table.c.name ).all(), + msg=msg ) + @web.expose + def create_role( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + users=trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + groups = trans.app.model.Group.query() \ + .filter( galaxy.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ) \ + .all() + return trans.fill_template( '/admin/dataset_security/role_create.mako', + users=users, + groups=groups, + msg=msg ) + @web.expose + def new_role( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + name = params.name + description = params.description + if not name or not description: + msg = "Please enter a name and a description" + trans.response.send_redirect( '/admin/create_role?msg=%s' % msg ) + elif trans.app.model.Role.filter_by( name=name ).first(): + msg = "A role with that name already exists" + trans.response.send_redirect( '/admin/create_role?msg=%s' % msg ) else: - msg = 'Invalid password' - return msg + # Create the role + role = galaxy.model.Role( name=name, + description=description, + type=trans.app.model.Role.types.ADMIN ) + role.flush() + # Add the users + users = listify( params.users ) + for user_id in users: + user = galaxy.model.User.get( user_id ) + # Create the UserRoleAssociation + ura = galaxy.model.UserRoleAssociation( user, role ) + ura.flush() + # Add the groups + groups = listify( params.groups ) + for group_id in groups: + group = galaxy.model.Group.get( group_id ) + # Create the GroupRoleAssociation + gra = galaxy.model.GroupRoleAssociation( group, role ) + gra.flush() + msg = "The new role has been created with %s associated users and %s associated groups" % ( str( len( users ) ), str( len( groups ) ) ) + trans.response.send_redirect( '/admin/roles?msg=%s' % msg ) + @web.expose + def role( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + role = trans.app.model.Role.get( int( params.role_id ) ) + in_users = [] + out_users = [] + in_groups = [] + out_groups = [] + for user in trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all(): + if user in [ x.user for x in role.users ]: + in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + for group in trans.app.model.Group.query().order_by( trans.app.model.Group.table.c.name ).all(): + if group in [ x.group for x in role.groups ]: + in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + # Build a list of tuples that are LibraryFolderDatasetAssociationss followed by a list of actions + # whose ActionDatasetRoleAssociation is associated with the Role + # [ ( LibraryFolderDatasetAssociation [ action, action ] ) ] + library_dataset_actions = {} + for adra in role.actions: + for lfda in trans.app.model.LibraryFolderDatasetAssociation \ + .filter( trans.app.model.LibraryFolderDatasetAssociation.dataset_id==adra.dataset_id ) \ + .all(): + root_found = False + folder_path = '' + folder = lfda.folder + while not root_found: + folder_path = '%s / %s' % ( folder.name, folder_path ) + if not folder.parent: + root_found = True + else: + folder = folder.parent + folder_path = '%s %s' % ( folder_path, lfda.name ) + library = trans.app.model.Library.filter( trans.app.model.Library.table.c.root_folder_id == folder.id ).first() + if library not in library_dataset_actions: + library_dataset_actions[ library ] = {} + try: + library_dataset_actions[ library ][ folder_path ].append( adra.action ) + except: + library_dataset_actions[ library ][ folder_path ] = [ adra.action ] + return trans.fill_template( '/admin/dataset_security/role.mako', + role=role, + in_users=in_users, + out_users=out_users, + in_groups=in_groups, + out_groups=out_groups, + library_dataset_actions=library_dataset_actions, + msg=msg ) + @web.expose + def role_members_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + role = galaxy.model.Role.get( int( params.role_id ) ) + in_users = [ trans.app.model.User.get( x ) for x in listify( params.in_users ) ] + for ura in role.users: + user = trans.app.model.User.get( ura.user_id ) + if user not in in_users: + # Delete DefaultUserPermissions for previously associated users that have been removed from the role + for dup in user.default_permissions: + if role == dup.role: + dup.delete() + dup.flush() + # Delete DefaultHistoryPermissions for previously associated users that have been removed from the role + for history in user.histories: + for dhp in history.default_permissions: + if role == dhp.role: + dhp.delete() + dhp.flush() + in_groups = [ trans.app.model.Group.get( x ) for x in listify( params.in_groups ) ] + trans.app.security_agent.set_entity_role_associations( roles=[ role ], users=in_users, groups=in_groups ) + role.refresh() + msg = "The role has been updated with %s associated users and %s associated groups" % ( str( len( in_users ) ), str( len( in_groups ) ) ) + trans.response.send_redirect( '/admin/roles?msg=%s' % msg ) + @web.expose + def mark_role_deleted( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + role = galaxy.model.Role.get( int( params.role_id ) ) + role.deleted = True + role.flush() + msg = "The role has been marked as deleted." + trans.response.send_redirect( '/admin/roles?msg=%s' % msg ) + @web.expose + def deleted_roles( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # Build a list of tuples which are roles followed by lists of groups and users + # [ ( role, [ group, group, group ], [ user, user ] ), ( role, [ group, group ], [ user ] ) ] + roles_groups_users = [] + roles = galaxy.model.Role.query() \ + .filter( galaxy.model.Role.table.c.deleted==True ) \ + .order_by( galaxy.model.Role.table.c.name ) \ + .all() + for role in roles: + groups = [] + for gra in role.groups: + groups.append( galaxy.model.Group.get( gra.group_id ) ) + users = [] + for ura in role.users: + users.append( galaxy.model.User.get( ura.user_id ) ) + roles_groups_users.append( ( role, groups, users ) ) + return trans.fill_template( '/admin/dataset_security/deleted_roles.mako', + roles_groups_users=roles_groups_users, + msg=msg ) + @web.expose + def undelete_role( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + role = galaxy.model.Role.get( int( params.role_id ) ) + role.deleted = False + role.flush() + msg = "The role has been marked as not deleted." + trans.response.send_redirect( '/admin/roles?msg=%s' % msg ) + @web.expose + def purge_role( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + role = galaxy.model.Role.get( int( params.role_id ) ) + # Delete UserRoleAssociations + for ura in role.users: + user = trans.app.model.User.get( ura.user_id ) + # Delete DefaultUserPermissions for associated users + for dup in user.default_permissions: + if role == dup.role: + dup.delete() + dup.flush() + # Delete DefaultHistoryPermissions for associated users + for history in user.histories: + for dhp in history.default_permissions: + if role == dhp.role: + dhp.delete() + dhp.flush() + ura.delete() + ura.flush() + # Delete GroupRoleAssociations + for gra in role.groups: + gra.delete() + gra.flush() + # Delete the Role + role.delete() + role.flush() + msg = "The role has been purged from the database." + trans.response.send_redirect( '/admin/deleted_roles?msg=%s' % msg ) + + # Galaxy Group Stuff + @web.expose + def groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # Build a list of tuples which are groups followed by lists of members and roles + # [ ( group, [ member, member, member ], [ role, role ] ), ( group, [ member, member ], [ role ] ) ] + groups_members_roles = [] + groups = galaxy.model.Group.query() \ + .filter( galaxy.model.Group.table.c.deleted==False ) \ + .order_by( galaxy.model.Group.table.c.name ) \ + .all() + for group in groups: + members = [] + for uga in group.members: + members.append( galaxy.model.User.get( uga.user_id ) ) + roles = [] + for gra in group.roles: + roles.append( galaxy.model.Role.get( gra.role_id ) ) + groups_members_roles.append( ( group, members, roles ) ) + return trans.fill_template( '/admin/dataset_security/groups.mako', + groups_members_roles=groups_members_roles, + msg=msg ) + @web.expose + def create_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + users=trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + roles = trans.app.model.Role.query() \ + .filter( and_( galaxy.model.Role.table.c.deleted == False, + galaxy.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE ) ) \ + .order_by( trans.app.model.Role.table.c.name ) \ + .all() + return trans.fill_template( '/admin/dataset_security/group_create.mako', + users=users, + roles=roles, + msg=msg ) + @web.expose + def new_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + name = params.name + if not name: + msg = "Please enter a name" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) + elif trans.app.model.Group.filter_by( name=name ).first(): + msg = "A group with that name already exists" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) + else: + # Create the group + group = galaxy.model.Group( name ) + group.flush() + # Add the members + members = listify( params.members ) + for user_id in members: + user = galaxy.model.User.get( user_id ) + # Create the UserGroupAssociation + uga = galaxy.model.UserGroupAssociation( user, group ) + uga.flush() + # Add the roles + roles = params.roles + if roles and not isinstance( roles, list ): + roles = [ roles ] + elif roles is None: + roles = [] + for role_id in roles: + role = galaxy.model.Role.get( role_id ) + # Create the GroupRoleAssociation + gra = galaxy.model.GroupRoleAssociation( group, role ) + gra.flush() + msg = "The new group has been created with %s members and %s associated roles" % ( str( len( members ) ), str( len( roles ) ) ) + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def group_members_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group = galaxy.model.Group.get( int( params.group_id ) ) + members = [] + for uga in group.members: + members.append ( galaxy.model.User.get( uga.user_id ) ) + return trans.fill_template( '/admin/dataset_security/group_members_edit.mako', + group=group, + members=members, + users=galaxy.model.User.query().order_by( galaxy.model.User.table.c.email ).all(), + msg=msg ) + @web.expose + def update_group_members( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + members = listify( params.members ) + group = galaxy.model.Group.get( group_id ) + # This is tricky since we have default association tables with + # records referring to members of this group. Because of this, + # we'll need to handle changes to the member list rather than the + # simpler approach of deleting all existing members and creating + # new records for user_ids in the received members param. + # First remove existing members that are not in the received members param + for uga in group.members: + if uga.user_id not in members: + # Delete the UserGroupAssociation + uga.delete() + uga.flush() + # Then add all new members to the group + for user_id in members: + user = galaxy.model.User.get( user_id ) + if user not in group.members: + uga = galaxy.model.UserGroupAssociation( user, group ) + uga.flush() + msg = "Group membership has been updated with a total of %s members" % len( members ) + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + # TODO: We probably don't want the following 2 methods since managing roles should be + # restricted to the Role page due to private roles and rules governing them + @web.expose + def group_roles_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group = galaxy.model.Group.get( int( params.group_id ) ) + group_roles = [] + for gra in group.roles: + group_roles.append ( galaxy.model.Role.get( gra.role_id ) ) + return trans.fill_template( '/admin/dataset_security/group_roles_edit.mako', + group=group, + group_roles=group_roles, + roles=galaxy.model.Role.query().order_by( galaxy.model.Role.table.c.name ).all(), + msg=msg ) + @web.expose + def update_group_roles( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + roles = listify( params.roles ) + group = galaxy.model.Group.get( group_id ) + # This is tricky since we have default association tables with + # records referring to members of this group. Because of this, + # we'll need to handle changes to the member list rather than the + # simpler approach of deleting all existing members and creating + # new records for user_ids in the received members param. + # First remove existing members that are not in the received members param + for gra in group.roles: + if gra.role_id not in roles: + # Delete the GroupRoleAssociation + gra.delete() + gra.flush() + # Then add all new roles to the group + for role_id in roles: + role = galaxy.model.Role.get( role_id ) + if role not in group.roles: + gra = galaxy.model.GroupRoleAssociation( group, role ) + gra.flush() + msg = "Group updated with a total of %s associated roles" % len( roles ) + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def mark_group_deleted( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group = galaxy.model.Group.get( int( params.group_id ) ) + group.deleted = True + group.flush() + msg = "The group has been marked as deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def deleted_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # Build a list of tuples which are groups followed by lists of members and roles + # [ ( group, [ member, member, member ], [ role, role ] ), ( group, [ member, member ], [ role ] ) ] + groups_members_roles = [] + groups = galaxy.model.Group.query() \ + .filter( galaxy.model.Group.table.c.deleted==True ) \ + .order_by( galaxy.model.Group.table.c.name ) \ + .all() + for group in groups: + members = [] + for uga in group.members: + members.append( galaxy.model.User.get( uga.user_id ) ) + roles = [] + for gra in group.roles: + roles.append( galaxy.model.Role.get( gra.role_id ) ) + groups_members_roles.append( ( group, members, roles ) ) + return trans.fill_template( '/admin/dataset_security/deleted_groups.mako', + groups_members_roles=groups_members_roles, + msg=msg ) + @web.expose + def undelete_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group = galaxy.model.Group.get( int( params.group_id ) ) + group.deleted = False + group.flush() + msg = "The group has been marked as not deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def purge_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group = galaxy.model.Group.get( int( params.group_id ) ) + # Delete UserGroupAssociations + for uga in group.users: + uga.delete() + uga.flush() + # Delete GroupRoleAssociations + for gra in group.roles: + gra.delete() + gra.flush() + # Delete the Group + group.delete() + group.flush() + msg = "The group has been purged from the database." + trans.response.send_redirect( '/admin/deleted_groups?msg=%s' % msg ) + + # Galaxy User Stuff + @web.expose + def users( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # Build a list of tuples which are users followed by lists of groups and roles + # [ ( user, [ group, group, group ], [ role, role ] ), ( user, [ group, group ], [ role ] ) ] + users_groups_roles = [] + users = trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + for user in users: + groups = [] + for uga in user.groups: + groups.append( galaxy.model.Group.get( uga.group_id ) ) + roles = [] + for ura in user.non_private_roles: + roles.append( galaxy.model.Role.get( ura.role_id ) ) + users_groups_roles.append( ( user, groups, roles ) ) + return trans.fill_template( '/admin/dataset_security/users.mako', + users_groups_roles=users_groups_roles, + msg=msg ) + @web.expose + def user( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + user_id = params.user_id + msg = params.msg + user = trans.app.model.User.get( user_id ) + # Get the groups and roles to which the user belongs + groups = trans.app.model.Group.query() \ + .select_from( ( outerjoin( trans.app.model.Group, trans.app.model.UserGroupAssociation ) ) \ + .outerjoin( trans.app.model.User ) ) \ + .filter( and_( trans.app.model.Group.deleted == False, trans.app.model.User.id == user_id ) ) \ + .order_by( trans.app.model.Group.table.c.name ) \ + .all() + roles = user.all_roles() + return trans.fill_template( '/admin/dataset_security/user.mako', + user=user, + groups=groups, + roles=roles, + msg=msg ) + @web.expose + def user_groups_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + user = galaxy.model.User.get( int( params.user_id ) ) + user_groups = [] + for uga in user.groups: + user_groups.append ( galaxy.model.Group.get( uga.group_id ) ) + groups = galaxy.model.Group.query() \ + .filter( galaxy.model.Group.table.c.deleted==False ) \ + .order_by( galaxy.model.Group.table.c.name ) \ + .all() + return trans.fill_template( '/admin/dataset_security/user_groups_edit.mako', + user=user, + user_groups=user_groups, + groups=groups, + msg=msg ) + @web.expose + def update_user_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + user_id = int( params.user_id ) + groups = listify( params.groups ) + user = galaxy.model.User.get( user_id ) + # First remove existing UserGroupAssociations that are not in the received groups param + for uga in user.groups: + if uga.group_id not in groups: + # Delete the UserGroupAssociation + uga.delete() + uga.flush() + # Then add all new groups to the user + for group_id in groups: + group = galaxy.model.Group.get( group_id ) + if group not in user.groups: + uga = galaxy.model.UserGroupAssociation( user, group ) + uga.flush() + msg = "The user now belongs to a total of %s groups" % len( groups ) + trans.response.send_redirect( '/admin/users?msg=%s' % msg ) + + # Galaxy Library Stuff + @web.expose + def library_browser( self, trans, **kwd ): + ## TODO: "show deleted libraries" toggle? + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if 'message' in kwd: + message = kwd['message'] + else: + message = None + return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all(), message = message ) + libraries = library_browser + @web.expose + def library( self, trans, id=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if not id and 'new' not in kwd: + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify a library" ) + if 'new' not in kwd: + library = trans.app.model.Library.get( id ) + params = util.Params( kwd ) + if 'new' in kwd: + if params.new == 'submitted': + library = trans.app.model.Library( name = util.restore_text( params.name ), description = util.restore_text( params.description ) ) + root_folder = trans.app.model.LibraryFolder( name = util.restore_text( params.name ), description = "" ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name="library", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Library" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + if 'root_folder' in kwd: + root_folder = library.root_folder + root_folder.name = util.restore_text( params.name ) + root_folder.flush() + library.name = util.restore_text( params.name ) + library.description = util.restore_text( params.description ) + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit library name and description", name = "library", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = library.name ) + .add_text( name = "description", label = "Description", value = library.description ) + .add_input( 'checkbox', 'Also change the root folder\'s name', 'root_folder' ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True + folder.flush() + delete_folder( library.root_folder ) + library.deleted = True + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + else: + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify an action" ) + @web.expose + def folder( self, trans, id=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if not id: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify a folder" ) + params = util.Params( kwd ) + folder = trans.app.model.LibraryFolder.get( id ) + if 'new' in kwd: + if params.new == 'submitted': + new_folder = trans.app.model.LibraryFolder( name = util.restore_text( params.name ), description = util.restore_text( params.description ) ) + # We are associating the last used genome_build with folders, so we will always + # initialize a new folder with the first dbkey in util.dbnames which is currently + # ? unspecified (?) + new_folder.genome_build = util.dbnames.default_value + folder.add_folder( new_folder ) + new_folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new folder", name="folder", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Folder" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + folder.name = util.restore_text( params.name ) + folder.description = util.restore_text( params.description ) + folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit folder name and description", name = "folder", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = folder.name ) + .add_text( name = "description", label = "Description", value = folder.description ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True + folder.flush() + delete_folder( folder ) + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + else: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify an action" ) + @web.expose + def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if isinstance( dbkey, list ): + last_used_build = dbkey[0] + else: + last_used_build = dbkey + if folder_id and not last_used_build: + folder = trans.app.model.LibraryFolder.get( folder_id ) + last_used_build = folder.genome_build + data_files = [] + params = util.Params( kwd ) + msg = params.msg + + # add_file method + def add_file( file_obj, name, extension, dbkey, last_used_build, roles, info='no info', space_to_tab=False ): + data_type = None + temp_name = sniff.stream_to_file( file_obj ) + + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress on the fly. + is_gzipped, is_valid = self.check_gzip( temp_name ) + if is_gzipped and not is_valid: + raise BadFileException( "you attempted to upload an inappropriate file." ) + elif is_gzipped and is_valid: + # We need to uncompress the temp_name file + CHUNK_SIZE = 2**20 # 1Mb + fd, uncompressed = tempfile.mkstemp() + gzipped_file = gzip.GzipFile( temp_name ) + while 1: + try: + chunk = gzipped_file.read( CHUNK_SIZE ) + except IOError: + os.close( fd ) + os.remove( uncompressed ) + raise BadFileException( 'problem decompressing gzipped data.' ) + if not chunk: + break + os.write( fd, chunk ) + os.close( fd ) + gzipped_file.close() + # Replace the gzipped file with the decompressed file + shutil.move( uncompressed, temp_name ) + name = name.rstrip( '.gz' ) + data_type = 'gzip' + + if space_to_tab: + line_count = sniff.convert_newlines_sep2tabs( temp_name ) + else: + line_count = sniff.convert_newlines( temp_name ) + if extension == 'auto': + data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) + else: + data_type = extension + dataset = trans.app.model.LibraryFolderDatasetAssociation( name=name, + info=info, + extension=data_type, + dbkey=dbkey, + create_dataset=True ) + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( dataset, genome_build=last_used_build ) + dataset.flush() + if roles: + for role in roles: + adra = galaxy.model.ActionDatasetRoleAssociation( RBACAgent.permitted_actions.DATASET_ACCESS.action, dataset.dataset, role ) + adra.flush() + shutil.move( temp_name, dataset.dataset.file_name ) + dataset.dataset.state = dataset.dataset.states.OK + dataset.init_meta() + if line_count is not None: + try: + dataset.set_peek( line_count=line_count ) + except: + dataset.set_peek() + else: + dataset.set_peek() + dataset.set_size() + if dataset.missing_meta(): + dataset.datatype.set_meta( dataset ) + trans.app.model.flush() + return dataset + # END add_file method + + # Dataset upload + if 'create_dataset' in kwd: + # Copied from upload tool action + last_dataset_created = None + data_file = kwd['file_data'] + url_paste = kwd['url_paste'] + server_dir = kwd.get( 'server_dir', 'None' ) + if data_file == '' and url_paste == '' and server_dir in [ 'None', '' ]: + if trans.app.config.library_import_dir is not None: + msg = 'Select a file, enter a URL or Text, or select a server directory.' + else: + msg = 'Select a file, enter a URL or enter Text.' + trans.response.send_redirect( web.url_for( action='dataset', folder_id=folder_id, msg=msg ) ) + space_to_tab = False + if 'space_to_tab' in kwd: + if kwd['space_to_tab'] not in ["None", None]: + space_to_tab = True + roles = [] + if 'roles' in kwd: + for role_id in listify( kwd['roles'] ): + roles.append( galaxy.model.Role.get( role_id ) ) + temp_name = "" + data_list = [] + created_datasets = [] + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + last_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, + roles, + info="uploaded file", + space_to_tab=space_to_tab ) + elif url_paste not in [ None, "" ]: + if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + url_paste = url_paste.replace( '\r', '' ).split( '\n' ) + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + last_dataset_created = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + last_used_build, + roles, + info="uploaded url", + space_to_tab=space_to_tab ) + created_datasets.append( last_dataset_created ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + last_dataset_created = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + last_used_build, + roles, + info="pasted entry", + space_to_tab=space_to_tab ) + elif server_dir not in [ None, "", "None" ]: + full_dir = os.path.join( trans.app.config.library_import_dir, server_dir ) + try: + files = os.listdir( full_dir ) + except: + log.debug( "Unable to get file list for %s" % full_dir ) + for file in files: + full_file = os.path.join( full_dir, file ) + if not os.path.isfile( full_file ): + continue + last_dataset_created = add_file( open( full_file, 'rb' ), + file, + extension, + dbkey, + last_used_build, + roles, + info="imported file", + space_to_tab=space_to_tab ) + created_datasets.append( last_dataset_created ) + if len( created_datasets ) > 1: + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "%i new datasets added to the library. Click here if you'd like to edit the permissions on these datasets." % ( + len( created_datasets ), + web.url_for( action='dataset', id=",".join( [ str(d.id) for d in created_datasets ] ) ) + ) + ) ) + elif last_dataset_created is not None: + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "New dataset added to the library. Click here if you'd like to edit the permissions or attributes on this dataset." % + web.url_for( action='dataset', id=last_dataset_created.id ) + ) ) + else: + return trans.show_error_message( 'Upload failed' ) + + # No dataset(s) specified, display upload form + elif not id: + # Send list of data formats to the form so the "extension" select list can be populated dynamically + file_formats = trans.app.datatypes_registry.upload_file_formats + # Send list of genome builds to the form so the "dbkey" select list can be populated dynamically + def get_dbkey_options( last_used_build ): + for dbkey, build_name in util.dbnames: + yield build_name, dbkey, ( dbkey==last_used_build ) + dbkeys = get_dbkey_options( last_used_build ) + # Send list of roles to the form so the dataset can be associated with 1 or more of them. + roles = trans.app.model.Role.query().order_by( trans.app.model.Role.c.name ).all() + return trans.fill_template( '/admin/library/new_dataset.mako', + folder_id=folder_id, + file_formats=file_formats, + dbkeys=dbkeys, + last_used_build=last_used_build, + roles=roles, + msg=msg ) + else: + if id.count( ',' ): + ids = id.split(',') + id = None + else: + ids = None + # id specified, display attributes form + if id: + lda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if not lda: + return trans.show_error_message( "Invalid dataset specified" ) + + # Copied from edit attributes for 'regular' datasets with some additions + p = util.Params(kwd, safe=False) + if p.update_roles: + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.Dataset.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in listify( p.get( k + '_in', [] ) ) ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + trans.app.security_agent.set_dataset_permissions( lda.dataset, permissions ) + lda.dataset.refresh() + elif p.change: + # The user clicked the Save button on the 'Change data type' form + trans.app.datatypes_registry.change_datatype( lda, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + lda.name = name + lda.info = info + # The following for loop will save all metadata_spec items + for name, spec in lda.datatype.metadata_spec.items(): + if spec.get("readonly"): + continue + optional = p.get("is_"+name, None) + if optional and optional == 'true': + # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) + setattr( lda.metadata, name, None ) + else: + setattr( lda.metadata, name, spec.unwrap( p.get ( name, None ) ) ) + + lda.metadata.dbkey = dbkey + lda.datatype.after_edit( lda ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + for name, spec in lda.datatype.metadata_spec.items(): + # We need to be careful about the attributes we are resetting + if name not in [ 'name', 'info', 'dbkey' ]: + if spec.get( 'default' ): + setattr( lda.metadata, name, spec.unwrap( spec.get( 'default' ) ) ) + lda.datatype.set_meta( lda ) + lda.datatype.after_edit( lda ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + elif p.delete: + lda.deleted = True + lda.flush() + trans.response.send_redirect( web.url_for( action='library_browser' ) ) + lda.datatype.before_edit( lda ) + if "dbkey" in lda.datatype.metadata_spec and not lda.metadata.dbkey: + # Copy dbkey into metadata, for backwards compatability + # This looks like it does nothing, but getting the dbkey + # returns the metadata dbkey unless it is None, in which + # case it resorts to the old dbkey. Setting the dbkey + # sets it properly in the metadata + lda.metadata.dbkey = lda.dbkey + # let's not overwrite the imported datatypes module with the variable datatypes? + ### the built-in 'id' is overwritten in lots of places as well + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + return trans.fill_template( "/admin/library/dataset.mako", + dataset=lda, + datatypes=ldatatypes, + err=None, + msg=msg ) + # multiple ids specfied, display multi permission form + elif ids: + ldas = [] + for id in [ int( id ) for id in ids ]: + lda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if lda is None: + return trans.show_error_message( 'You specified an invalid dataset' ) + ldas.append( lda ) + if len( ldas ) < 2: + return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) + if 'update_roles' in kwd: + p = util.Params( kwd ) + permissions = {} + for k, v in trans.app.model.Dataset.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in listify( p.get( k + '_in', [] ) ) ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + for lda in ldas: + trans.app.security_agent.set_dataset_permissions( lda.dataset, permissions ) + lda.dataset.refresh() + # Ensure that the permissions across all datasets are + # identical. Otherwise, we can't update together. + tmp = [] + for lda in ldas: + perms = trans.app.security_agent.get_dataset_permissions( lda.dataset ) + if perms not in tmp: + tmp.append( perms ) + if len( tmp ) != 1: + return trans.show_error_message( "The datasets you selected do not have identical permissions, so they can not be updated together" ) + else: + return trans.fill_template( "/admin/library/dataset.mako", + dataset=ldas ) + @web.expose + def add_dataset_to_folder_from_history( self, trans, ids="", folder_id=None, **kwd ): + if not isinstance( ids, list ): + if ids: + ids = ids.split( "," ) + else: + ids = [] + try: + folder = trans.app.model.LibraryFolder.get( folder_id ) + except: + folder = None + if folder is None: + return trans.show_error_message( "You must provide a valid target folder." ) + error_msg = ok_msg = "" + dataset_names = [] + if ids: + for data_id in ids: + data = trans.app.model.HistoryDatasetAssociation.get( data_id ) + if data: + data.to_library_dataset_folder_association( target_folder = folder ) + dataset_names.append( data.name ) + else: + error_msg += "A requested dataset (%s) was invalid. " % ( data_id ) + if dataset_names: + ok_msg = "Added datasets (%s) to the library folder." % ( ", ".join( dataset_names ) ) + return trans.fill_template( "/admin/library/add_dataset_from_history.mako", history=trans.get_history(), folder=folder, ok_msg=ok_msg, error_msg=error_msg ) + + def check_gzip( self, temp_name ): + """ + Utility method to check gzipped uploads + """ + temp = open( temp_name, "U" ) + magic_check = temp.read( 2 ) + temp.close() + if magic_check != util.gzip_magic: + return ( False, False ) + CHUNK_SIZE = 2**15 # 32Kb + gzipped_file = gzip.GzipFile( temp_name ) + chunk = gzipped_file.read( CHUNK_SIZE ) + gzipped_file.close() + #if self.check_html( temp_name, chunk=chunk ) or self.check_binary( temp_name, chunk=chunk ): + # return( True, False ) + return ( True, True ) + @web.expose + def datasets( self, trans, **kwd ): + """ + The datasets method is used by the dropdown box on the admin-side library browser. + """ + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + if 'with-selected' in kwd: + if not params.dataset_ids: + return trans.show_error_message( "At least one dataset must be selected." ) + dataset_ids = listify( params.dataset_ids ) + if params.action == 'edit': + trans.response.send_redirect( web.url_for( action = 'dataset', id = ",".join( dataset_ids ) ) ) + elif params.action == 'delete': + for id in dataset_ids: + d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + d.deleted = True + d.flush() + trans.response.send_redirect( web.url_for( action = 'library_browser' ) ) + else: + return trans.show_error_message( "Not implemented." ) + else: + return trans.show_error_message( "Galaxy can't operate on datasets without an operation." ) + @web.expose + def delete_dataset( self, trans, id=None, **kwd): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if id: + # id is a LibraryFolderDatasetAssociation.id + library_folder_dataset_assoc = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + self._delete_dataset( library_folder_dataset_assoc ) + trans.log_event( "Dataset id %s deleted from library folder id %s" % ( str( id ), str( library_folder_dataset_assoc.folder.id ) ) ) + trans.response.send_redirect( web.url_for( action = 'folder', id = library_folder_dataset_assoc.folder.id, msg = 'The dataset was deleted from the folder' ) ) + return trans.show_error_message( "You did not specify a dataset to delete." ) + + def _delete_dataset( self, library_folder_dataset_assoc ): + #dataset = library_folder_dataset_assoc.dataset + # TODO: assuming 1 to 1 mapping between Dataset -> LibraryFolders ( i.e., is can the same + # dataset record be shared across LibraryFolders? + # Confirm that things should be deleted as follows + + ### Deleting the base dataset will delete datasets that exist in user's histories + ### ( LDA.dataset == HDA.dataset ) + ### Shouldn't this be a separate option? + ### For Now, I am commenting out logic that acts on the dataset directly + ### -- Dan: + + + # Delete the LibraryFolderDatasetAssociation + library_folder_dataset_assoc.deleted = True + library_folder_dataset_assoc.flush() + + @web.expose + def delete_folder( self, trans, id=None, **kwd): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if id: + if 'confirm' not in kwd: + return trans.show_warn_message( 'Click here to confirm folder deletion.' % web.url_for( action = 'delete_folder', id = id, confirm=True ) ) + # id is a LibraryFolder.id + folder = trans.app.model.LibraryFolder.get( id ) + self._delete_folder( folder ) + + trans.log_event( "Folder id %s deleted." % id ) + + if folder.library_root: + trans.response.send_redirect( web.url_for( action = 'library', id = folder.library_root[0].id, msg = 'You have deleted the root folder.' ) ) + trans.response.send_redirect( web.url_for( action = 'folder', id = folder.parent_id, msg = 'The folder was deleted.' ) ) + return trans.show_error_message( "You did not specify a folder to delete." ) + + def _delete_folder( self, folder ): + for library_folder_dataset_association in folder.active_datasets: + self._delete_dataset( library_folder_dataset_association ) + for folder in folder.active_folders: + self._delete_folder( folder ) + folder.deleted = True + folder.flush() + + @web.expose + def delete_library( self, trans, id=None, **kwd): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if id: + if 'confirm' not in kwd: + return trans.show_warn_message( 'Click here to confirm library deletion.' % web.url_for( action = 'delete_library', id = id, confirm=True ) ) + # id is a LibraryFolder.id + library = trans.app.model.Library.get( id ) + self._delete_folder( library.root_folder ) + library.deleted = True + library.flush() + trans.log_event( "Library id %s deleted." % id ) + trans.response.send_redirect( web.url_for( action = 'libraries', msg = 'You have deleted the library %s.' % library.id ) ) + return trans.show_error_message( "You did not specify a library to delete." ) + +def listify( item, return_none=False ): + """ + Since single params are not a single item list + """ + if item is None: + return [] + elif isinstance( item, list ): + return item + else: + return [ item ] diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index e8b0542ee19..0e8b7369441 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -60,7 +60,7 @@ class ASync( BaseController ): if STATUS == 'OK': key = hmac.new( trans.app.config.tool_secret, "%d:%d" % ( data.id, data.history_id), sha ).hexdigest() if key != data_secret: - return "You do not have permision to alter data %s." % data_id + return "You do not have permission to alter data %s." % data_id # push the job into the queue data.state = data.blurb = data.states.RUNNING log.debug('executing tool %s' % tool.id) @@ -104,6 +104,7 @@ class ASync( BaseController ): #history.datasets.add_dataset( data ) data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_permissions( trans.history ) ) data.name = GALAXY_NAME data.dbkey = GALAXY_BUILD data.info = GALAXY_INFO diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index 83b2917f77e..151476547a6 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -103,31 +103,30 @@ class DatasetInterface( BaseController ): @web.expose def display(self, trans, dataset_id=None, filename=None, **kwd): """Catches the dataset id and displays file contents as directed""" - if filename is None or filename.lower() == "index": - try: - data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) - if data: - mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) - trans.response.set_content_type(mime) - trans.log_event( "Display dataset id: %s" % str(dataset_id) ) - try: - return open( data.file_name ) - except: - return "This item contains no content" - except: - pass - return "Invalid dataset specified" - else: - #display files from directory here - try: - file_path = os.path.join(trans.app.model.HistoryDatasetAssociation.get( dataset_id ).extra_files_path, filename) - mime, encoding = mimetypes.guess_type(file_path) - if mime is None: - mime = trans.app.datatypes_registry.get_mimetype_by_extension(".".split(file_path)[-1]) + data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) + if not data: + raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + if filename is None or filename.lower() == "index": + mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) trans.response.set_content_type(mime) - return open(file_path) - except: - raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % (filename) ) + trans.log_event( "Display dataset id: %s" % str( dataset_id ) ) + try: + return open( data.file_name ) + except: + raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) + else: + file_path = os.path.join( data.extra_files_path, filename ) + mime, encoding = mimetypes.guess_type( file_path ) + if mime is None: + mime = trans.app.datatypes_registry.get_mimetype_by_extension( ".".split( file_path )[-1] ) + trans.response.set_content_type( mime ) + try: + return open( file_path ) + except: + raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) + else: + return trans.show_error_message( "You are not privileged to access this dataset." ) def _undelete( self, trans, id ): history = trans.get_history() @@ -141,7 +140,7 @@ class DatasetInterface( BaseController ): # Mark undeleted data.mark_undeleted() self.app.model.flush() - trans.log_event( "Dataset id %s has been undeleted" % str(id) ) + trans.log_event( "Dataset id %s has been undeleted" % str(id) ) return True return False @@ -155,7 +154,6 @@ class DatasetInterface( BaseController ): if self._undelete( trans, id ): return "OK" raise "Error undeleting" - @web.expose def copy_datasets( self, trans, source_dataset_ids = "", target_history_ids = "", new_history_name="", do_copy = False ): diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py new file mode 100644 index 00000000000..5db40832306 --- /dev/null +++ b/lib/galaxy/web/controllers/library.py @@ -0,0 +1,24 @@ +from galaxy.web.base.controller import * +from galaxy.model.orm import * +import logging + +log = logging.getLogger( __name__ ) + +class Library( BaseController ): + @web.expose + def browse( self, trans, **kwd ): + return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all() ) + index = browse + @web.expose + def import_datasets( self, trans, import_ids=[], **kwd ): + if not import_ids: + return trans.show_error_message( "You must select at least one dataset to import" ) + if not isinstance( import_ids, list ): + import_ids = [ import_ids ] + history = trans.get_history() + for id in import_ids: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() + history.add_dataset( dataset ) + dataset.flush() + history.flush() + return trans.show_ok_message( "%i dataset(s) have been imported in to your history" % len( import_ids ), refresh_frames=['history'] ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index a0b54983ad6..6a3046587ff 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -1,15 +1,11 @@ """ Contains the main interface in the Universe class """ -from galaxy.web.base.controller import * - -import logging, os, sets, string, shutil -import re, socket - -from galaxy import util, datatypes, jobs, web, util - +import logging, os, sets, string, shutil, urllib, re, socket from cgi import escape, FieldStorage -import urllib +from galaxy import util, datatypes, jobs, web, util +from galaxy.web.base.controller import * +from galaxy.model.orm import * log = logging.getLogger( __name__ ) @@ -91,7 +87,7 @@ class RootController( BaseController ): return trans.fill_template("root/history_item.mako", data=data, hid=hid) else: return trans.show_error_message( "Must specify a dataset id.") - + @web.json def history_item_updates( self, trans, ids=None, states=None ): # Avoid caching @@ -137,22 +133,25 @@ class RootController( BaseController ): except: return "Dataset id '%s' is invalid" %str( id ) if data: - mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) - trans.response.set_content_type(mime) - if tofile: - fStat = os.stat(data.file_name) - trans.response.headers['Content-Length'] = int(fStat.st_size) - if toext[0:1] != ".": - toext = "." + toext - valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - fname = data.name - fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150] - trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext) - trans.log_event( "Display dataset id: %s" % str(id) ) - try: - return open( data.file_name ) - except: - return "This dataset contains no content" + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) + trans.response.set_content_type(mime) + if tofile: + fStat = os.stat(data.file_name) + trans.response.headers['Content-Length'] = int(fStat.st_size) + if toext[0:1] != ".": + toext = "." + toext + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = data.name + fname = ''.join(c in valid_chars and c or '_' for c in fname)[0:150] + trans.response.headers["Content-Disposition"] = "attachment; filename=GalaxyHistoryItem-%s-[%s]%s" % (data.hid, fname, toext) + trans.log_event( "Display dataset id: %s" % str(id) ) + try: + return open( data.file_name ) + except: + return "This dataset contains no content" + else: + return "You are not privileged to view this dataset." else: return "No dataset with id '%s'" % str( id ) @@ -164,9 +163,12 @@ class RootController( BaseController ): try: data = self.app.model.HistoryDatasetAssociation.get( parent_id ) if data: - child = data.get_child_by_designation(designation) + child = data.get_child_by_designation( designation ) if child: - return self.display(trans, id=child.id, tofile=tofile, toext=toext) + if trans.app.security_agent.allow_action( trans.user, child.permitted_actions.DATASET_ACCESS, dataset = child ): + return self.display( trans, id=child.id, tofile=tofile, toext=toext ) + else: + return "You are not privileged to access this dataset." except Exception: pass return "A child named %s could not be found for data %s" % ( designation, parent_id ) @@ -176,9 +178,12 @@ class RootController( BaseController ): """Returns a file in a format that can successfully be displayed in display_app""" data = self.app.model.HistoryDatasetAssociation.get( id ) if data: - trans.response.set_content_type(data.get_mime()) - trans.log_event( "Formatted dataset id %s for display at %s" % ( str(id), display_app ) ) - return data.as_display_type(display_app, **kwd) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + trans.response.set_content_type( data.get_mime() ) + trans.log_event( "Formatted dataset id %s for display at %s" % ( str( id ), display_app ) ) + return data.as_display_type( display_app, **kwd ) + else: + return "You are not privileged to access this dataset." else: return "No data with id=%d" % id @@ -194,77 +199,111 @@ class RootController( BaseController ): yield "No data with id=%d" % id @web.expose - def edit(self, trans, id=None, hid=None, **kwd): + def edit(self, trans, id=None, hid=None, lid=None, **kwd): """Returns data directly into the browser. Sets the mime-type according to the extension""" if hid is not None: history = trans.get_history() # TODO: hid handling data = history.datasets[ int( hid ) - 1 ] - elif id is None: - return trans.show_error_message( "Problem loading dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) - else: + elif lid is not None: + data = self.app.model.LibraryFolderDatasetAssociation.get( lid ) + elif id is not None: data = self.app.model.HistoryDatasetAssociation.get( id ) + else: + trans.log_event( "Problem loading dataset id %s with history id %s and library id %s." % ( str( id ), str( hid ), str( lid ) ) ) + return trans.show_error_message( "Problem loading dataset." ) if data is None: - return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) - - p = util.Params(kwd, safe=False) - - if p.change: - # The user clicked the Save button on the 'Change data type' form - trans.app.datatypes_registry.change_datatype( data, p.datatype ) - trans.app.model.flush() - elif p.save: - # The user clicked the Save button on the 'Edit Attributes' form - data.name = p.name - data.info = p.info + trans.log_event( "Problem retrieving dataset id %s with history id %s and library id %s." % ( str( id ), str( hid ), str( lid ) ) ) + return trans.show_error_message( "Problem retrieving dataset." ) + if id is not None and data.history.user is not None and data.history.user != trans.user: + return trans.show_error_message( "This instance of a dataset (%s) in a history does not belong to you." % ( data.id ) ) + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + p = util.Params(kwd, safe=False) - # The following for loop will save all metadata_spec items - for name, spec in data.metadata.spec.items(): - if spec.get("readonly"): - continue - optional = p.get("is_"+name, None) - if optional and optional == 'true': - # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) - setattr(data.metadata, name, None) - else: - setattr( data.metadata, name, spec.unwrap( p.get (name, None) ) ) + can_edit_metadata = lid is None or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) + if p.change: + # The user clicked the Save button on the 'Change data type' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + trans.app.datatypes_registry.change_datatype( data, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + data.name = p.name + data.info = p.info + + # The following for loop will save all metadata_spec items + for name, spec in data.datatype.metadata_spec.items(): + if spec.get("readonly"): + continue + optional = p.get("is_"+name, None) + if optional and optional == 'true': + # optional element... == 'true' actually means it is NOT checked (and therefore ommitted) + setattr(data.metadata, name, None) + else: + setattr( data.metadata, name, spec.unwrap( p.get (name, None) ) ) - data.datatype.after_edit( data ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) - elif p.detect: - # The user clicked the Auto-detect button on the 'Edit Attributes' form - for name, spec in data.metadata.spec.items(): - # We need to be careful about the attributes we are resetting - if name not in [ 'name', 'info', 'dbkey' ]: - if spec.get( 'default' ): - setattr( data.metadata, name, spec.unwrap( spec.get( 'default' ) ) ) - data.datatype.set_meta( data ) - data.datatype.after_edit( data ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) - elif p.convert_data: - """The user clicked the Convert button on the 'Convert to new format' form""" - target_type = kwd.get("target_type", None) - if target_type: - msg = data.datatype.convert_dataset(trans, data, target_type) - return trans.show_ok_message( msg, refresh_frames=['history'] ) - data.datatype.before_edit( data ) - - if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey: - # Copy dbkey into metadata, for backwards compatability - # This looks like it does nothing, but getting the dbkey - # returns the metadata dbkey unless it is None, in which - # case it resorts to the old dbkey. Setting the dbkey - # sets it properly in the metadata - data.metadata.dbkey = data.dbkey - # let's not overwrite the imported datatypes module with the variable datatypes? - ### the built-in 'id' is overwritten in lots of places as well - ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] - ldatatypes.sort() - trans.log_event( "Opened edit view on dataset %s" % str(id) ) - return trans.fill_template( "/dataset/edit_attributes.mako", data=data, - datatypes=ldatatypes, err=None ) + data.datatype.after_edit( data ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + for name, spec in data.metadata.spec.items(): + # We need to be careful about the attributes we are resetting + if name not in [ 'name', 'info', 'dbkey' ]: + if spec.get( 'default' ): + setattr( data.metadata, name, spec.unwrap( spec.get( 'default' ) ) ) + data.datatype.set_meta( data ) + data.datatype.after_edit( data ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) + elif p.convert_data: + if lid is not None: + return trans.show_error_message( "Data in the library cannot be converted. Please import it to a history and covert it." ) + """The user clicked the Convert button on the 'Convert to new format' form""" + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + target_type = kwd.get("target_type", None) + if target_type: + msg = data.datatype.convert_dataset(trans, data, target_type) + return trans.show_ok_message( msg, refresh_frames=['history'] ) + elif p.update_roles: + if not trans.user: + return trans.show_error_message( "You must be logged in if you want to change permissions." ) + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + permissions = {} + for k, v in trans.app.model.Dataset.permitted_actions.items(): + in_roles = p.get( k + '_in', [] ) + if not isinstance( in_roles, list ): + in_roles = [ in_roles ] + in_roles = [ trans.app.model.Role.get( x ) for x in in_roles ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + trans.app.security_agent.set_dataset_permissions( data.dataset, permissions ) + data.dataset.refresh() + else: + return trans.show_error_message( "You are not authorized to change this dataset's permissions" ) + data.datatype.before_edit( data ) + + if "dbkey" in data.datatype.metadata_spec and not data.metadata.dbkey: + # Copy dbkey into metadata, for backwards compatability + # This looks like it does nothing, but getting the dbkey + # returns the metadata dbkey unless it is None, in which + # case it resorts to the old dbkey. Setting the dbkey + # sets it properly in the metadata + data.metadata.dbkey = data.dbkey + # let's not overwrite the imported datatypes module with the variable datatypes? + ### the built-in 'id' is overwritten in lots of places as well + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + trans.log_event( "Opened edit view on dataset %s" % str(id) ) + return trans.fill_template( "/dataset/edit_attributes.mako", data=data, + datatypes=ldatatypes, err=None ) + else: + return trans.show_error_message( "You do not have permission to edit this dataset's (%s) attributes." % id ) @web.expose def delete( self, trans, id = None, show_deleted_on_refresh = False, **kwd): @@ -323,6 +362,8 @@ class RootController( BaseController ): self.app.job_stop_queue.put( data.creating_job_associations[0].job ) except IndexError: pass # upload tool will cause this since it doesn't have a job + else: + return "Dataset id '%s' is invalid" %str( id ) return "OK" ## ---- History management ----------------------------------------------- @@ -352,8 +393,13 @@ class RootController( BaseController ): if history: if history.user_id != None and user: assert user.id == history.user_id, "History does not belong to current user" - history_names.append(history.name) + # Delete DefaultHistoryPermissions + for dhp in history.default_permissions: + dhp.delete() + dhp.flush() + # Mark history as deleted in db history.deleted = True + history_names.append(history.name) # If deleting the current history, make a new current. if history == trans.get_history(): trans.new_history() @@ -402,6 +448,42 @@ class RootController( BaseController ): errors.append( "You must select at least one history to undelete." ) return self.history_available( trans, id=','.join( id ), show_deleted=True, ok_msg = ok_msg, error_msg = " ".join( errors ) ) + @web.expose + def history_undelete( self, trans, id=[], **kwd): + """Undeletes a list of histories, ensures that histories are owned by current user""" + history_names = [] + errors = [] + ok_msg = "" + if id: + if not isinstance( id, list ): + id = id.split( "," ) + user = trans.get_user() + for hid in id: + try: + int( hid ) + except: + errors.append( "Invalid history: %s" % str( hid ) ) + continue + history = self.app.model.History.get( hid ) + if history: + if history.user != user: + errors.append( "History does not belong to current user." ) + continue + if history.purged: + errors.append( "History has already been purged and can not be undeleted." ) + continue + history_names.append( history.name ) + history.deleted = False + else: + errors.append( "Not able to find history %s." % str( hid ) ) + trans.log_event( "History id %s marked as undeleted" % str(hid) ) + self.app.model.flush() + if history_names: + ok_msg = "Histories (%s) have been undeleted." % ", ".join( history_names ) + else: + errors.append( "You must select at least one history to undelete." ) + return self.history_available( trans, id=','.join( id ), show_deleted=True, ok_msg = ok_msg, error_msg = " ".join( errors ) ) + @web.expose def clear_history( self, trans ): """Clears the history for a user""" @@ -429,14 +511,45 @@ class RootController( BaseController ): if not email: return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() - send_to_user = trans.app.model.User.get_by( email = email ) + send_to_user = trans.app.model.User.filter_by( email=email ).first() + p = util.Params( kwd ) + if p.action and p.action == "no_share": + trans.response.send_redirect( url_for( action='history_options' ) ) if not send_to_user: send_to_err = "No such user" elif user.email == email: send_to_err = "You can't send histories to yourself" else: + # if we're not checking or changing permissions, skip this step + if not p.action or ( p.action and p.action != 'share' ): + # ugly + can_change = {} + cannot_change = {} + for history in histories: + for hda in history.active_datasets: + if not trans.app.security_agent.allow_action( send_to_user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=hda ): + if trans.app.security_agent.allow_action( user, trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=hda ) and \ + not hda.dataset.library_associations: + # don't change perms on datasets that exist in the library. + if p.action and p.action == "private": + trans.app.security_agent.privately_share_dataset( hda.dataset, users=[ user, send_to_user ] ) + elif p.action and p.action == "public": + trans.app.security_agent.set_dataset_permissions( hda.dataset, { trans.app.security_agent.permitted_actions.DATASET_ACCESS : [] } ) + elif history not in can_change: + can_change[history] = [ hda ] + else: + can_change[history].append( hda ) + else: + if p.action and p.action in [ "private", "public" ]: + pass # don't change stuff that the user doesn't have permission to change + elif history not in cannot_change: + cannot_change[history] = [ hda ] + else: + cannot_change[history].append( hda ) + if can_change or cannot_change: + return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err, can_change=can_change, cannot_change=cannot_change) for history in histories: - new_history = history.copy() + new_history = history.copy( target_user=send_to_user ) new_history.name = history.name+" from "+user.email new_history.user_id = send_to_user.id trans.log_event( "History share, id: %s, name: '%s': to new id: %s" % (str(history.id), history.name, str(new_history.id)) ) @@ -483,12 +596,12 @@ class RootController( BaseController ): if user: if import_history.user_id == user.id: return trans.show_error_message( "You cannot import your own history.") - new_history = import_history.copy() + new_history = import_history.copy( target_user=trans.user ) new_history.name = "imported: "+new_history.name new_history.user_id = user.id galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -505,7 +618,7 @@ class RootController( BaseController ): new_history.user_id = None galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -530,7 +643,7 @@ class RootController( BaseController ): if new_history: galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -590,11 +703,17 @@ class RootController( BaseController ): return trans.show_message( "

%s" % change_msg, refresh_frames=['history'] ) @web.expose - def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",**kwd ): + def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",copy_access_from=None,**kwd ): """Adds a POSTed file to a History""" try: history = trans.app.model.History.get( history_id ) data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) + if copy_access_from: + copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) + trans.app.security_agent.copy_dataset_permissions( copy_access_from.dataset, data.dataset ) + else: + permissions = trans.app.security_agent.history_get_default_permissions( history ) + trans.app.security_agent.set_dataset_permissions( data.dataset, permissions ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -611,9 +730,31 @@ class RootController( BaseController ): data.flush() trans.log_event("Added dataset %d to history %d" %(data.id, trans.history.id)) return trans.show_ok_message("Dataset "+str(data.hid)+" added to history "+str(history_id)+".") - except: + except Exception, e: + trans.log_event( "Failed to add dataset to history: %s" % ( e ) ) return trans.show_error_message("Adding File to History has Failed") + @web.expose + def history_set_default_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the current history""" + if trans.user: + if 'update_roles' in kwd: + history = trans.get_history() + p = util.Params( kwd ) + permissions = {} + for k, v in trans.app.model.Dataset.permitted_actions.items(): + in_roles = p.get( k + '_in', [] ) + if not isinstance( in_roles, list ): + in_roles = [ in_roles ] + in_roles = [ trans.app.model.Role.get( x ) for x in in_roles ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + trans.app.security_agent.history_set_default_permissions( history, permissions ) + return trans.show_ok_message( 'Default history permissions have been changed.' ) + return trans.fill_template( 'history/permissions.mako' ) + else: + #user not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change a history's default permissions." ) + @web.expose def dataset_make_primary( self, trans, id=None): """Copies a dataset and makes primary""" @@ -630,7 +771,7 @@ class RootController( BaseController ): return trans.show_error_message( "

Failed to make secondary dataset primary.

" ) @web.expose - def masthead( self, trans ): + def masthead( self, trans, active_view=None ): brand = trans.app.config.get( "brand", "" ) if brand: brand ="/%s" % brand @@ -638,8 +779,15 @@ class RootController( BaseController ): bugs_email = trans.app.config.get( "bugs_email", "mailto:galaxy-bugs@bx.psu.edu" ) blog_url = trans.app.config.get( "blog_url", "http://g2.trac.bx.psu.edu/blog" ) screencasts_url = trans.app.config.get( "screencasts_url", "http://g2.trac.bx.psu.edu/wiki/ScreenCasts" ) + admin_user = "false" + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + user = trans.get_user() + if user: + user_email = trans.get_user().email + if user_email in admin_users: + admin_user = "true" return trans.fill_template( "/root/masthead.mako", brand=brand, wiki_url=wiki_url, - blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url ) + blog_url=blog_url,bugs_email=bugs_email, screencasts_url=screencasts_url, admin_user=admin_user, active_view=active_view ) @web.expose def dataset_errors( self, trans, id=None, **kwd ): @@ -682,3 +830,4 @@ class RootController( BaseController ): @web.expose def generate_error( self, trans ): raise Exception( "Fake error!" ) + diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 41890611abc..90cbfe0dfe4 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -1,9 +1,9 @@ """ Contains the user interface in the Universe class """ - from galaxy.web.base.controller import * - +from galaxy.model.orm import * +from galaxy import util import logging, os, string from random import choice @@ -53,7 +53,7 @@ class User( BaseController ): email_err = "Please enter a real email address" elif len( email) > 255: email_err = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_err = "User with that email already exists" elif email != conf_email: conf_email_err = "Email addresses do not match." @@ -73,7 +73,7 @@ class User( BaseController ): email_error = password_error = None # Attempt login if email or password: - user = trans.app.model.User.get_by( email = email ) + user = trans.app.model.User.filter_by( email=email ).first() if not user: email_error = "No such user" elif user.external: @@ -108,7 +108,7 @@ class User( BaseController ): email_error = "Please enter a real email address" elif len( email) > 255: email_error = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_error = "User with that email already exists" elif len( password ) < 6: password_error = "Please use a password of at least 6 characters" @@ -118,6 +118,7 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() + trans.app.security_agent.setup_new_user( user ) trans.set_user( user ) trans.ensure_valid_galaxy_session() """ @@ -143,7 +144,7 @@ class User( BaseController ): @web.expose def reset_password(self, trans, email=None, **kwd): error = '' - reset_user = trans.app.model.User.get_by( email = email ) + reset_user = trans.app.model.User.filter_by( email=email ).first() user = trans.get_user() if reset_user: if user and user.id != reset_user.id: @@ -166,3 +167,23 @@ class User( BaseController ): return trans.show_form( web.FormBuilder( web.url_for(), "Reset Password", submit_text="Submit" ) .add_text( "email", "Email", value=email, error=error ) ) + + @web.expose + def set_default_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the new histories""" + if trans.user: + if 'update_roles' in kwd: + p = util.Params( kwd ) + permissions = {} + for k, v in trans.app.model.Dataset.permitted_actions.items(): + in_roles = p.get( k + '_in', [] ) + if not isinstance( in_roles, list ): + in_roles = [ in_roles ] + in_roles = [ trans.app.model.Role.get( x ) for x in in_roles ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + trans.app.security_agent.user_set_default_permissions( trans.user, permissions ) + return trans.show_ok_message( 'Default new history permissions have been changed.' ) + return trans.fill_template( 'user/permissions.mako' ) + else: + # User not logged in, history group must be only public + return trans.show_error_message( "You must be logged in to change your default permitted actions." ) diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 5719202814e..45899c1b179 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -17,8 +17,12 @@ from galaxy.model.mapping import desc class WorkflowController( BaseController ): @web.expose - @web.require_login( "use Galaxy workflows" ) def index( self, trans ): + return trans.fill_template( "workflow/index.mako" ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def list( self, trans ): """ Render workflow main page (management of existing workflows) """ @@ -33,7 +37,7 @@ class WorkflowController( BaseController ): .filter( model.StoredWorkflow.c.deleted == False ) \ .order_by( desc( model.StoredWorkflow.c.update_time ) ) \ .all() - return trans.fill_template( "workflow/index.mako", + return trans.fill_template( "workflow/list.mako", workflows = workflows, shared_by_others = shared_by_others ) @@ -44,7 +48,7 @@ class WorkflowController( BaseController ): # Load workflow from database stored = get_stored_workflow( trans, id ) if email: - other = model.User.get_by( email=email ) + other = model.User.filter_by( email=email ).first() if not other: mtype = "error" msg = ( "User '%s' does not exist" % email ) @@ -61,10 +65,10 @@ class WorkflowController( BaseController ): share.stored_workflow = stored share.user = other session = trans.sa_session - session.save( share ) + session.save_or_update( share ) session.flush() trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) ) - return self.index( trans ) + return self.list( trans ) return trans.fill_template( "workflow/share.mako", message = msg, messagetype = mtype, @@ -79,7 +83,7 @@ class WorkflowController( BaseController ): stored.name = new_name trans.sa_session.flush() trans.set_message( "Workflow renamed to '%s'." % new_name ) - return self.index( trans ) + return self.list( trans ) else: return form( url_for( id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ .add_text( "new_name", "Workflow Name", value=stored.name ) @@ -104,11 +108,11 @@ class WorkflowController( BaseController ): new_stored.user = user # Persist session = trans.sa_session - session.save( new_stored ) + session.save_or_update( new_stored ) session.flush() # Display the management page trans.set_message( 'Clone created with name "%s"' % new_stored.name ) - return self.index( trans ) + return self.list( trans ) @web.expose @web.require_login( "create workflows" ) @@ -129,11 +133,11 @@ class WorkflowController( BaseController ): stored_workflow.latest_workflow = workflow # Persist session = trans.sa_session - session.save( stored_workflow ) + session.save_or_update( stored_workflow ) session.flush() # Display the management page trans.set_message( "Workflow '%s' created" % stored_workflow.name ) - return self.index( trans ) + return self.list( trans ) else: return form( url_for(), "Create new workflow", submit_text="Create" ) \ .add_text( "workflow_name", "Workflow Name", value="Unnamed workflow" ) @@ -150,7 +154,7 @@ class WorkflowController( BaseController ): stored.flush() # Display the management page trans.set_message( "Workflow '%s' deleted" % stored.name ) - return self.index( trans ) + return self.list( trans ) @web.expose @web.require_login( "edit workflows" ) @@ -306,7 +310,7 @@ class WorkflowController( BaseController ): workflow.stored_workflow = stored stored.latest_workflow = workflow # Persist - trans.sa_session.save( stored ) + trans.sa_session.save_or_update( stored ) trans.sa_session.flush() # Return something informative errors = [] @@ -431,11 +435,10 @@ class WorkflowController( BaseController ): stored.name = workflow_name workflow.stored_workflow = stored stored.latest_workflow = workflow - trans.sa_session.save( stored ) + trans.sa_session.save_or_update( stored ) trans.sa_session.flush() # Index page with message - trans.template_context['message'] = "Workflow '%s' created" % workflow_name - return self.index( trans ) + return trans.show_message( "Workflow '%s' created from current history." % workflow_name ) ## return trans.show_ok_message( "

Workflow '%s' created.

Click to load in workflow editor

" ## % ( workflow_name, web.url_for( action='editor', id=trans.security.encode_id(stored.id) ) ) ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index a8f837dbf5c..237cb4409a8 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -24,7 +24,7 @@ pkg_resources.require( "Mako" ) import mako.template import mako.lookup -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) from sqlalchemy import desc import logging @@ -172,7 +172,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.current_history_id: history = self.app.model.History.get( galaxy_session.current_history_id ) if history and not history.deleted: @@ -197,9 +197,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): return self.new_history() return self.__history def new_history( self ): - history = self.app.model.History() + history = self.app.model.History( user = self.user ) # Make sure we have an id history.flush() + self.app.security_agent.history_set_default_permissions( history ) # Immediately associate the new history with self self.__history = history # Make sure we have a valid session to associate with the new history @@ -216,7 +217,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.user_id = self.user.id try: # See if we have already associated the history with the session - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=history.id ).first() except: association = None history.add_galaxy_session( galaxy_session, association=association ) @@ -265,12 +266,14 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): """Return the user in $HTTP_REMOTE_USER and create if necessary""" # remote_user middleware ensures HTTP_REMOTE_USER exists try: - user = self.app.model.User.selectone_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ) + user = self.app.model.User.filter_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ).first() + assert user is not None except: user = self.app.model.User( email=self.environ[ 'HTTP_REMOTE_USER' ] ) user.set_password_cleartext( 'external' ) user.external = True user.flush() + self.app.security_agent.setup_new_user( user ) self.log_event( "Automatically created account '%s'" % user.email ) return user def get_cookie_user( self ): @@ -281,7 +284,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.user_id: user = self.app.model.User.get( galaxy_session.user_id ) if user: @@ -321,7 +324,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): session_key = self.security.decode_session_key( secure_id ) try: # Retrive the galaxy_session id via the unique session_key - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid: self.__galaxy_session = galaxy_session except: @@ -382,7 +385,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if self.history is not None: # See if we have already associated the session with the history try: - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=self.history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=self.history.id ).first() except: association = None galaxy_session.add_history( self.history, association=association ) @@ -431,6 +434,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.flush() self.__galaxy_session = galaxy_session if history is not None and user is not None: + if not history.user: + # This user will now acquire previously non-owned history, so set permitted actions to user's default + history.user = user + self.app.security_agent.history_set_default_permissions( history, dataset=True, bypass_manage_permission=True ) history.user_id = user.id history.flush() self.__history = history @@ -522,8 +529,8 @@ class FormBuilder( object ): self.action = action self.submit_text = submit_text self.inputs = [] - def add_input( self, type, name, label, value=None, error=None, help=None ): - self.inputs.append( FormInput( type, label, name, value, error, help ) ) + def add_input( self, type, name, label, value=None, error=None, help=None, use_label=True ): + self.inputs.append( FormInput( type, label, name, value, error, help, use_label ) ) return self def add_text( self, name, label, value=None, error=None, help=None ): return self.add_input( 'text', label, name, value, error, help ) @@ -534,13 +541,14 @@ class FormInput( object ): """ Simple class describing a form input element """ - def __init__( self, type, name, label, value=None, error=None, help=None ): + def __init__( self, type, name, label, value=None, error=None, help=None, use_label=True ): self.type = type self.name = name self.label = label self.value = value self.error = error self.help = help + self.use_label = use_label class FormData( object ): """ diff --git a/lib/galaxy/webapps/reports/controllers/jobs.py b/lib/galaxy/webapps/reports/controllers/jobs.py index bf7935d9647..a502ca48532 100644 --- a/lib/galaxy/webapps/reports/controllers/jobs.py +++ b/lib/galaxy/webapps/reports/controllers/jobs.py @@ -5,7 +5,7 @@ import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/system.py b/lib/galaxy/webapps/reports/controllers/system.py index 33e7cc8f84b..e4017544460 100644 --- a/lib/galaxy/webapps/reports/controllers/system.py +++ b/lib/galaxy/webapps/reports/controllers/system.py @@ -2,8 +2,9 @@ import operator, os from datetime import datetime, timedelta from galaxy.webapps.reports.base.controller import * import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload, desc +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload +from sqlalchemy import desc import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index b99546f14dd..af9b0550b17 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -2,8 +2,9 @@ from datetime import * import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model +from galaxy.model.orm import * import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) @@ -13,15 +14,7 @@ class Users( BaseController ): def registered_users( self, trans, **kwd ): params = util.Params( kwd ) msg = '' - engine = galaxy.model.mapping.metadata.engine - s = """ - SELECT - count(id) AS num_users - FROM - galaxy_user - """ - rows = engine.text( s ).execute().fetchall() - num_users = rows[0].num_users + num_users = galaxy.model.User.query().count() return trans.fill_template( 'registered_users.mako', num_users=num_users, msg=msg ) @web.expose def registered_users_per_month( self, trans, **kwd ): diff --git a/scripts/cleanup_datasets/cleanup_datasets.py b/scripts/cleanup_datasets/cleanup_datasets.py index 0d653981d00..97ca3f8e8f8 100644 --- a/scripts/cleanup_datasets/cleanup_datasets.py +++ b/scripts/cleanup_datasets/cleanup_datasets.py @@ -13,8 +13,8 @@ from galaxy import eggs import galaxy.model.mapping import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload assert sys.version_info[:2] >= ( 2, 4 ) diff --git a/static/images/silk/book.png b/static/images/silk/book.png new file mode 100644 index 00000000000..b0f4dd7928c Binary files /dev/null and b/static/images/silk/book.png differ diff --git a/static/images/silk/book_open.png b/static/images/silk/book_open.png new file mode 100644 index 00000000000..7d863f94974 Binary files /dev/null and b/static/images/silk/book_open.png differ diff --git a/static/images/silk/folder.png b/static/images/silk/folder.png new file mode 100644 index 00000000000..784e8fa4823 Binary files /dev/null and b/static/images/silk/folder.png differ diff --git a/static/images/silk/folder_page.png b/static/images/silk/folder_page.png new file mode 100644 index 00000000000..1ef6e11438f Binary files /dev/null and b/static/images/silk/folder_page.png differ diff --git a/static/images/silk/resultset_bottom.png b/static/images/silk/resultset_bottom.png new file mode 100644 index 00000000000..22848b0061c Binary files /dev/null and b/static/images/silk/resultset_bottom.png differ diff --git a/static/images/silk/resultset_next.png b/static/images/silk/resultset_next.png new file mode 100644 index 00000000000..e252606d3e6 Binary files /dev/null and b/static/images/silk/resultset_next.png differ diff --git a/static/images/welcomePhoto.jpg b/static/images/welcomePhoto.jpg index 91abdf31496..33fc6a470df 100644 Binary files a/static/images/welcomePhoto.jpg and b/static/images/welcomePhoto.jpg differ diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index a8fba0e69c0..28a0f87f546 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -283,6 +283,11 @@ table.colored tr background: $table_row_bg; } +table.colored tr.odd_row +{ + background: $odd_row_bg; +} + div.debug { margin: 10px; @@ -350,6 +355,16 @@ ul.toolParameterExpandableCollapsable list-style: none; } +ul.manage-table-actions { + float: right; + margin-top: -2.5em; +} +ul.manage-table-actions li { + display: block; + float: left; + margin-left: 0.5em; +} + .action-button { background: #eeeeee; color: #333; @@ -416,4 +431,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 4de4530b076..74a7b7c68ec 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -16,6 +16,7 @@ img border: 0; } + a:link, a:visited, a:active { color: #303030; @@ -282,6 +283,11 @@ table.colored tr background: white; } +table.colored tr.odd_row +{ + background: #DADFEF; +} + div.debug { margin: 10px; @@ -293,7 +299,7 @@ div.debug div.odd_row { - background: #FFFF99; + background: #DADFEF; } #footer { @@ -348,6 +354,16 @@ ul.toolParameterExpandableCollapsable list-style: none; } +ul.manage-table-actions { + float: right; + margin-top: -2.5em; +} +ul.manage-table-actions li { + display: block; + float: left; + margin-left: 0.5em; +} + .action-button { background: #eeeeee; color: #333; @@ -414,4 +430,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/static/june_2007_style/blue/history.css b/static/june_2007_style/blue/history.css index 3a2243bf20d..1dd55a464cd 100644 --- a/static/june_2007_style/blue/history.css +++ b/static/june_2007_style/blue/history.css @@ -80,6 +80,13 @@ div.historyItem-running */ } +div.historyItem-noPermission +{ + filter: alpha(opacity=60); + -moz-opacity: .60; + opacity: .60; +} + div.historyItem-queued { } @@ -101,4 +108,4 @@ pre.peek th { color: white; background: #023858; -} \ No newline at end of file +} diff --git a/static/june_2007_style/blue/library.css b/static/june_2007_style/blue/library.css new file mode 100644 index 00000000000..76f589af067 --- /dev/null +++ b/static/june_2007_style/blue/library.css @@ -0,0 +1,65 @@ +.libraryRow { + background-color: #d2c099; +} + +.datasetHighlighted { + background-color: #C1C9E5; +} + +div.historyItemBody { + padding: 4px 4px 2px 4px; +} + +li.folderRow, +li.datasetRow +{ + border-top: solid 1px #ddd; +} + +li.folderRow:hover, +li.datasetRow:hover +{ + background-color: #C1C9E5; +} + +img.expanderIcon { + padding-right: 4px; +} + +input.datasetCheckbox, +li, ul +{ + padding: 0; + margin: 0; +} + +.rowTitle +{ + padding: 2px; +} + +ul { + list-style: none; +} + +.libraryTitle th { + text-align: left; +} + +pre.peek +{ + background: white; + color: black; + width: 100%; + overflow: auto; +} + +pre.peek th +{ + color: white; + background: #023858; +} + +a.expandLink { + text-decoration: none; +} diff --git a/static/june_2007_style/blue/masthead.css b/static/june_2007_style/blue/masthead.css index f206e7f1b44..b89faddb0a8 100644 --- a/static/june_2007_style/blue/masthead.css +++ b/static/june_2007_style/blue/masthead.css @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: #eeeeee; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: #222532; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: #222532; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/blue/panel_layout.css b/static/june_2007_style/blue/panel_layout.css index b7ec445bc43..7dda79706a8 100644 --- a/static/june_2007_style/blue/panel_layout.css +++ b/static/june_2007_style/blue/panel_layout.css @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -146,7 +159,6 @@ table.column-layout .unified-panel-header { height: 2em; - overflow: hidden; z-index: 1000; background: #cccccc; background-image: url(panel_header_bg.png); @@ -160,18 +172,17 @@ table.column-layout color: #333; font-weight: bold; } - + .unified-panel-header-inner { padding-top: 0.45em; } - + .menu-bg { background: #C1C9E5 url(menu_bg.png) top repeat-x; } div.unified-panel-body { position: absolute; - z-index: 1; top: 2em; bottom: 0; width: 100%; @@ -197,6 +208,7 @@ div.unified-panel-body { color: black; background: #aaaaaa; } + .panel-header-button:active { color: white; background: #aaaaaa; @@ -240,7 +252,7 @@ div.popupmenu-item:hover { } #overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 20000; } @@ -271,6 +283,34 @@ div.popupmenu-item:hover { padding: 5px; } -.dialog-box body { - overflow: scroll; + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: #303030; + padding: 0px; + padding-left: 26px; + background-color: #FFCCCC; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: #FFFFCC; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: #CCFFCC; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: #CCCCFF; } \ No newline at end of file diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 447ca3f98bc..5b9f320b296 100644 --- a/static/june_2007_style/blue_colors.ini +++ b/static/june_2007_style/blue_colors.ini @@ -15,7 +15,7 @@ form_border=#d8b365 #form_body_bg=#FFFFFF form_body_bg_top=#FFFFFF form_body_bg_bottom=#FFFFFF -odd_row_bg=#FFFF99 +odd_row_bg=#DADFEF # Messages error_message_border=#AA6666 error_message_bg=#FFCCCC @@ -48,6 +48,7 @@ masthead_bg=#2C3143 masthead_text=#eeeeee masthead_bg_hatch=- masthead_link=#eeeeee +masthead_active_tab_bg=#222532 # ---- Layout ----------------------------------------------------------------- # Overall background color (including space between panels) layout_bg=#eee diff --git a/static/june_2007_style/history.css.tmpl b/static/june_2007_style/history.css.tmpl index 30c285b20fb..362cc3a3b23 100644 --- a/static/june_2007_style/history.css.tmpl +++ b/static/june_2007_style/history.css.tmpl @@ -80,6 +80,13 @@ div.historyItem-running */ } +div.historyItem-noPermission +{ + filter: alpha(opacity=60); + -moz-opacity: .60; + opacity: .60; +} + div.historyItem-queued { } @@ -101,4 +108,4 @@ pre.peek th { color: white; background: $peek_table_header; -} \ No newline at end of file +} diff --git a/static/june_2007_style/library.css.tmpl b/static/june_2007_style/library.css.tmpl new file mode 100644 index 00000000000..5b4a736a70d --- /dev/null +++ b/static/june_2007_style/library.css.tmpl @@ -0,0 +1,65 @@ +.libraryRow { + background-color: $form_title_bg_bottom; +} + +.datasetHighlighted { + background-color: $menu_bg_over; +} + +div.historyItemBody { + padding: 4px 4px 2px 4px; +} + +li.folderRow, +li.datasetRow +{ + border-top: solid 1px #ddd; +} + +li.folderRow:hover, +li.datasetRow:hover +{ + background-color: $menu_bg_over; +} + +img.expanderIcon { + padding-right: 4px; +} + +input.datasetCheckbox, +li, ul +{ + padding: 0; + margin: 0; +} + +.rowTitle +{ + padding: 2px; +} + +ul { + list-style: none; +} + +.libraryTitle th { + text-align: left; +} + +pre.peek +{ + background: white; + color: black; + width: 100%; + overflow: auto; +} + +pre.peek th +{ + color: white; + background: $peek_table_header; +} + +a.expandLink { + text-decoration: none; +} diff --git a/static/june_2007_style/make_style.py b/static/june_2007_style/make_style.py index 4ba96651c52..1bc2adf9212 100755 --- a/static/june_2007_style/make_style.py +++ b/static/june_2007_style/make_style.py @@ -1,5 +1,9 @@ #!/usr/bin/env python +from galaxy import eggs +import pkg_resources +pkg_resources.require("Cheetah") + import sys from Cheetah.Template import Template import string @@ -13,8 +17,8 @@ def run( cmd ): templates = [ ( "base.css.tmpl", "base.css" ), ( "panel_layout.css.tmpl", "panel_layout.css" ), - ( "panel_layout_ie.css.tmpl", "panel_layout_ie.css" ), ( "masthead.css.tmpl", "masthead.css"), + ( "library.css.tmpl", "library.css"), ( "history.css.tmpl", "history.css" ), ( "tool_menu.css.tmpl", "tool_menu.css" ), ( "reset.css.tmpl", "reset.css" ) ] @@ -64,7 +68,8 @@ for line in open( vars ): for input, output in templates: print input ,"->", output open( os.path.join( out_dir, output ), "w" ).write( str( Template( file=input, searchList=[context] ) ) ) - + +""" for rule, output in images: t = string.Template( rule ).substitute( context ) print t, "->", output @@ -74,3 +79,4 @@ for src, bg, out in shared_images: t = "./png_over_color.py shared_images/%s %s %s" % ( src, context[bg], os.path.join( out_dir, out ) ) print t run( t.split() ) +""" diff --git a/static/june_2007_style/masthead.css.tmpl b/static/june_2007_style/masthead.css.tmpl index 0c759f2aba6..a6d3b7ea5c7 100644 --- a/static/june_2007_style/masthead.css.tmpl +++ b/static/june_2007_style/masthead.css.tmpl @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: $masthead_link; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: $masthead_active_tab_bg; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: $masthead_active_tab_bg; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/panel_layout.css.tmpl b/static/june_2007_style/panel_layout.css.tmpl index 0479767977b..4df44dba18a 100644 --- a/static/june_2007_style/panel_layout.css.tmpl +++ b/static/june_2007_style/panel_layout.css.tmpl @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -268,4 +281,37 @@ div.popupmenu-item:hover { .dialog-box .body, .dialog-box .buttons { padding: 5px; +} + +## Messages for message box, slightly different style + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: $base_text; + padding: 0px; + padding-left: 26px; + background-color: $error_message_bg; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: $warn_message_bg; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: $done_message_bg; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: $info_message_bg; } \ No newline at end of file diff --git a/static/welcome.html b/static/welcome.html index 7a5064c6af7..5bd2ab245cd 100644 --- a/static/welcome.html +++ b/static/welcome.html @@ -10,28 +10,27 @@
-
- Galaxy and 2008 Meeting Season +
+ Workflows are finally here!
+ Watch how you can (Click link to play)...
    -
  • Beyond Genome | San Francisco | June 9 - 11
  • -
  • ISMB 2008 | Toronto | July 19 - 23
  • -
  • Genome Informatics | Hinxton, UK | September 10 - 14
  • -
  • ASHG | Philadelphia | November 11 - 15
  • -
+
  • Create Workflows by Example: Convert your Galaxy History into a Workflow.
  • +
  • Edit Workflows: I want to repeat an analysis...but...with different parameters.
  • +
  • Workflows from scratch: Drag, drag, drag...
  • +
    - Download new Galaxy brochure here. -
    - + For more screencasts click here. +

    - Unsequenced Genomes of the World | June 2008 + Unsequenced Genomes of the World | October 2008


    - Przewalski's Horse (Equus przewalskii) | Escondido, California + Costa's hummingbird (Calypte costae) | Kings Canyon NP, California

    diff --git a/templates/admin/center.mako b/templates/admin/center.mako new file mode 100644 index 00000000000..9c1ec7ac59f --- /dev/null +++ b/templates/admin/center.mako @@ -0,0 +1,125 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Galaxy Administration + +

    Administration

    + +

    Choose a task from the menu on the left.

    +

    + Security - Data security in Galaxy is a new feature, so familiarize yourself with the details, which can be found + here or in our data security page. The data security + process incorporates users, groups and roles, and enables the application of certain restrictions on datasets. The default process + is to not apply any security restrictions to datasets, and data becomes more restricted with each new "permission restriction" applied + to it. With no restrictions, users should not see any difference in the way Galaxy has always worked. The general definitions of + these entities are: +

      +
    • + Users - registered Galaxy users that have created a Galaxy account. Users can be members of a group and can + be associated with 1 or more roles. If a user is not authenticated during a Galaxy session, they will not have access to any + of the security features, and datasets they create during that session will have no security restrictions applied to them + (i.e., they will be considered "public"). +
    • +

      +
    • + Groups - a set of 0 or more users which are considered members of the group. Groups can be associated with 0 + or more roles, simplifying the process of applying "permission restrictions" to the data between a select group of users. +
    • +

      +
    • + Roles - associate users and groups with specific permissions on datasets. For example, users in groups A and B + can be associated with role C which gives them the "access" permission on datasets D, E and F. Roles have a type which is currently + one of the following: +
        +
      • + private - every user is associated automatically with their own private role, and administrators cannot + manage them. +
      • +
      • + user - this is currently not used, but eventually any registered user will be able to create a new role + and this will be it's type. +
      • +
      • + sharing - a role created automatically during a Galaxy session that enables a user to share data with + another user. This can generally be considered a temporary role, but this role type is evolving. +
      • +
      • admin - a role created by a Galaxy administrator.
      • +
      +
    • +

      +
    • + Permissions - for any dataset, applying one of the following role "permission restrictions" will restrict the + use of the dataset. +
        +
      • + access - users associated with the role can import this dataset into their history for analysis. +

        + If no roles with the 'access' permission restriction are associated with a dataset, the dataset is "public" and may + be shared with anyone. Public library datasets will be accessible to all users (as well as anyone not logged in + during a Galaxy session) from the list of libraries displayed when the "Access Libraries stored locally" tool is used. +

        +

        + Associating a dataset with a role that includes the "access" permission restriction narrows the set of users that can + access it. For example, if 'Role A' includes the "access" permission restriction and 'Role A' is associated with the dataset, + only those users and groups who are associated with 'Role A' may access the dataset. +

        +

        + If multiple roles that include the "access" permission restriction are associated with a dataset, access to the + dataset is derived from the intersection of the users associated with the roles. For example, if 'Role A' and 'Role B' are + associated with a dataset, only those users and groups who are associated with both 'Role A' AND 'Role B' may access the dataset. +

        +

        + In order for a user to make a dataset private to just themselves, they should associate the dataset with their private role + (the role identical to their Galaxy user name / email address). Associating additional roles that include the 'access' + permission restriction is not advised since the behavior will probably not be as expected. Role permission restrictions + are always logically "ANDed" together, so only the user will be able to access the dataset since they are the only member of + their "private role". To make a dataset private to themselves and one or more other users, the user should create a new role + (note: this functionality is still under development for non administrator users) and associate the dataset with that role, + not their "private role". +

        +

        + Private data (data associated with roles that include the "access" permission restriction) must be made public in order + to be used with external sources, such as the "view at UCSC" link, or the "Perform genome analysis and prediction with EpiGRAPH" + tool. Being "made public" means removing the association of all roles that include the "access" permission restriction from + the dataset. +

        +

      • +
      • edit metadata - users associated with the role can edit this dataset's metadata in the dataset library.
      • +
      • + manage permissions - users associated with the role can manage the roles associated with this dataset. If no + roles that include the 'manage permissions' are associated with the dataset, only administrators can modify it's permission + restrictions. +
      • +
      +
    • +
    +

    +

    The menu on the left provides the following features

    +
      +
    • + Manage users - provides a view of the registered users and all groups and non-private roles associated + with each user. +
    • +

      +
    • + Manage groups - provides a view of all groups along with the members of the group and the roles associated with + each group (both private and non-private roles). Non-private roles include a link to a page that allows you to manage the users + and groups that are associated with the role. The page also includes a view of the library datasets that are associated with the + role and the various "permission restrictions" applied to each dataset. +
    • +

      +
    • + Manage roles - provides a view of all non-private roles along with the role type, and the users and groups that + are associated with the role. +
    • +

      +
    • + Manage libraries - Dataset libraries enable a Galaxy administrator to upload datasets into a library. Only + administrators can create dataset libraries and maintain their contents ( datasets ) and the security applied to them. From + the Galaxy analysis view, the "Access Libraries stored locally" tool in the "Get Data" tool + section allows users to access the datasets in a library ( if the user is not restricted from accessing the datasets by the + security rules applied to them ) by "uploading" them into their histories. This "uploading" process will not make a copy of + the dataset, however, but will be a sort of pointer to the dataset on disk. This approach allows for multiple users to have + access to a single ( possibly very large ) dataset on disk. +
    • +
    +

    diff --git a/templates/admin/dataset_security/deleted_groups.mako b/templates/admin/dataset_security/deleted_groups.mako new file mode 100644 index 00000000000..446cb58a336 --- /dev/null +++ b/templates/admin/dataset_security/deleted_groups.mako @@ -0,0 +1,111 @@ +<%inherit file="/base.mako"/> + +## Render a row +<%def name="render_row( group, members, roles, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${group.name} + +
    + Undelete + Purge +
    + + +
      + %for user in members: +
    • ${user.email}
    • + %endfor +
    + + +
      + %for role in roles: +
    • ${role.name}
    • + %endfor +
    + %if not anchored: + +
    top
    + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Deleted Groups

    + +%if len( groups_members_roles ) == 0: + There are no deleted Galaxy groups +%else: + + <% + render_quick_find = len( groups_members_roles ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + %for ctr, group_tuple in enumerate( groups_members_roles ): + <% + group = group_tuple[0] + members = group_tuple[1] + roles = group_tuple[2] + %> + %if render_quick_find and not group.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and group.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if group.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( group, members, roles, ctr, True, '' )} + %endif + %endfor +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    NameMembersRoles
    +%endif diff --git a/templates/admin/dataset_security/deleted_roles.mako b/templates/admin/dataset_security/deleted_roles.mako new file mode 100644 index 00000000000..8f3803d4930 --- /dev/null +++ b/templates/admin/dataset_security/deleted_roles.mako @@ -0,0 +1,111 @@ +<%inherit file="/base.mako"/> + +## Render a row +<%def name="render_row( role, groups, users, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${role.name} + +
    + Undelete + Purge +
    + + +
      + %for group in groups: +
    • ${group.name}
    • + %endfor +
    + + +
      + %for user in users: +
    • ${user.email}
    • + %endfor +
    + %if not anchored: + +
    top
    + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Deleted Roles

    + +%if len( roles_groups_users ) == 0: + There are no deleted Galaxy roles +%else: + + <% + render_quick_find = len( roles_groups_users ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + %for ctr, role_tuple in enumerate( roles_groups_users ): + <% + role = role_tuple[0] + groups = role_tuple[1] + users = role_tuple[2] + %> + %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( role, groups, users, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( role, groups, users, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if role.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( role, groups, users, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( role, groups, users, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( role, groups, users, ctr, True, '' )} + %endif + %endfor +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    NameAssociated GroupsAssociated Users
    +%endif diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako new file mode 100644 index 00000000000..88c61d41840 --- /dev/null +++ b/templates/admin/dataset_security/group_create.mako @@ -0,0 +1,115 @@ +<%inherit file="/base.mako"/> + +## Render a user row +<%def name="render_user_row( user, ctr )"> + %if ctr % 2 == 1: + + %else: + + %endif + ${user.email} + + + +## Render a role row +<%def name="render_role_row( role, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + %if not anchored: +
    top
    + ${role.name} + %else: + ${role.name} + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Create Group

    + + +
    + + + <% + render_quick_find = len( users ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + ## Render users + + ## Render roles + + + +
    Name:
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    Check to add userCheck to add role
    + + %for ctr, user in enumerate( users ): + ${render_user_row( user, ctr )} + %endfor +
    +
    + <% curr_anchor = 'A' %> + + %for ctr, role in enumerate( roles ): + %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_role_row( role, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_role_row( role, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if role.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_role_row( role, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_role_row( role, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_role_row( role, ctr, True, '' )} + %endif + %endfor +
    +
    +
    diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako new file mode 100644 index 00000000000..5afe2ae809f --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,73 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Permitted Actions on Datasets +
    +
    + Libraries  |   + Groups  |   + Users +
    +

    Manage Permitted Actions on Datasets for Group '${group.name}'

    + + %if msg: + + %endif + + %if len( group.datasets ) == 0: + + %else: + + + + + <% ctr = 0 %> + + + %for gda in gdas: + <% permissions = trans.app.security_agent.get_dataset_permissions( gda, group.id ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + + <% ctr += 1 %> + %endfor + + + %endif +

    ${msg}

     
    The group you selected has no associated datasets.
    Association Names/InfoPermitted Actions
    + %if gda.dataset.library_associations: + Library name(s):
    + %endif +
      + %for da in gda.dataset.library_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    + %if gda.dataset.history_associations: + History name(s):
    + %endif +
      + %for da in gda.dataset.history_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    +
    + %for pa in trans.app.model.Dataset.permitted_actions: + <% pa_val = trans.app.security_agent.permitted_actions.__dict__[pa] %> + + ${pa_val}
    ${trans.app.security_agent.get_permitted_action_description(pa)}
    +
    + %endfor +
    +
    +
    diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako new file mode 100644 index 00000000000..a7f8f5223ca --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +## Render a row +<%def name="render_row( user, ctr, anchored, curr_anchor, check )"> + %if ctr % 2 == 1: + + %else: + + %endif + + %if check: + ${user.email} + %else: + ${user.email} + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Members of group '${group.name}'

    + +%if len( users ) == 0: + There are no Galaxy users +%else: +
    + + <% + render_quick_find = len( users ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + %for ctr, user in enumerate( users ): + <% check = False %> + %for member in members: + %if member.email == user.email: + <% + check = True + break + %> + %endif + %endfor + %if render_quick_find and not user.email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and user.email.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( user, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( user, ctr, anchored, curr_anchor, check )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if user.email.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( user, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( user, ctr, anchored, curr_anchor, check )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( user, ctr, True, '', check )} + %endif + %endfor + +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    Check to add member
    +
    +%endif + diff --git a/templates/admin/dataset_security/group_roles_edit.mako b/templates/admin/dataset_security/group_roles_edit.mako new file mode 100644 index 00000000000..825ea2b4543 --- /dev/null +++ b/templates/admin/dataset_security/group_roles_edit.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +## Render a row +<%def name="render_row( role, ctr, anchored, curr_anchor, check )"> + %if ctr % 2 == 1: + + %else: + + %endif + + %if check: + ${role.name} + %else: + ${role.name} + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Roles associated with group '${group.name}'

    + +%if len( roles ) == 0: + There are no Galaxy roles +%else: +
    + + <% + render_quick_find = len( roles ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + %for ctr, role in enumerate( roles ): + <% check = False %> + %for group_role in group_roles: + %if group_role.name == role.name: + <% + check = True + break + %> + %endif + %endfor + %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( role, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( role, ctr, anchored, curr_anchor, check )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if role.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( role, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( role, ctr, anchored, curr_anchor, check )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( role, ctr, True, '', check )} + %endif + %endfor + +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    Check to add role
    +
    +%endif + diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako new file mode 100644 index 00000000000..3b92b8ccf99 --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,128 @@ +<%inherit file="/base.mako"/> + +<% + import galaxy.model +%> + +## Render a row +<%def name="render_row( group, members, roles, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${group.name} + +
    + Change members + Mark group deleted +
    + + + + + +
      + %for role in roles: +
    • + %if not role.type == galaxy.model.Role.types.PRIVATE: + ${role.name} + %else: + ${role.name} + %endif +
    • + %endfor +
    + %if not anchored: + +
    top
    + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Groups

    + + + +%if len( groups_members_roles ) == 0: + There are no Galaxy groups +%else: + + <% + render_quick_find = len( groups_members_roles ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + %for ctr, group_tuple in enumerate( groups_members_roles ): + <% + group = group_tuple[0] + members = group_tuple[1] + roles = group_tuple[2] + %> + %if render_quick_find and not group.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and group.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if group.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( group, members, roles, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( group, members, roles, ctr, True, '' )} + %endif + %endfor +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    NameMembersAssociated Roles
    +%endif diff --git a/templates/admin/dataset_security/index.mako b/templates/admin/dataset_security/index.mako new file mode 100644 index 00000000000..1a013ec1fc6 --- /dev/null +++ b/templates/admin/dataset_security/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Dataset Security +
    +
    Dataset Security
    + + %if msg: + + %endif + + +

    ${msg}

    Groups
    Users
    +
    diff --git a/templates/admin/dataset_security/role.mako b/templates/admin/dataset_security/role.mako new file mode 100644 index 00000000000..63add3c2bb3 --- /dev/null +++ b/templates/admin/dataset_security/role.mako @@ -0,0 +1,112 @@ +<%inherit file="/base.mako"/> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<%def name="render_select( name, options )"> + + + + + +
    +
    Role '${role.name}'
    +
    +
    +
    +
    + Users associated with '${role.name}'
    + ${render_select( "in_users", in_users )}
    + +
    +
    + Users not associated with '${role.name}'
    + ${render_select( "out_users", out_users )}
    + +
    +
    +
    +
    + Groups associated with '${role.name}'
    + ${render_select( "in_groups", in_groups )}
    + +
    +
    + Groups not associated with '${role.name}'
    + ${render_select( "out_groups", out_groups )}
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +%if len( library_dataset_actions ) > 0: +

    Library datasets associated with role '${role.name}'

    + + + + +
    +
      + %for ctr, library, in enumerate( library_dataset_actions.keys() ): +
    • + + ${library.name} +
        + %for folder_path, permissions in library_dataset_actions[ library ].items(): +
      • + + ${folder_path} +
          + % for permission in permissions: +
            +
          • ${permission}
          • +
          + %endfor +
        +
      • + %endfor +
      +
    • + %endfor +
    +
    +%endif diff --git a/templates/admin/dataset_security/role_create.mako b/templates/admin/dataset_security/role_create.mako new file mode 100644 index 00000000000..d1e026b670e --- /dev/null +++ b/templates/admin/dataset_security/role_create.mako @@ -0,0 +1,118 @@ +<%inherit file="/base.mako"/> + +## Render a user row +<%def name="render_user_row( user, ctr )"> + %if ctr % 2 == 1: + + %else: + + %endif + ${user.email} + + + +## Render a group row +<%def name="render_group_row( group, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + %if not anchored: +
    top
    + ${group.name} + %else: + ${group.name} + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Create Role

    + + +
    + + + + + + <% + render_quick_find = len( users ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + ## Render users + + ## Render roles + + + +
    Name: Description:
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    Check to add userCheck to add group
    + + %for ctr, user in enumerate( users ): + ${render_user_row( user, ctr )} + %endfor +
    +
    + <% curr_anchor = 'A' %> + + %for ctr, group in enumerate( groups ): + %if render_quick_find and not group.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and group.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_group_row( group, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_group_row( group, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if group.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_group_row( group, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_group_row( group, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_group_row( group, ctr, True, '' )} + %endif + %endfor +
    +
    +
    diff --git a/templates/admin/dataset_security/roles.mako b/templates/admin/dataset_security/roles.mako new file mode 100644 index 00000000000..a606c748f1a --- /dev/null +++ b/templates/admin/dataset_security/roles.mako @@ -0,0 +1,119 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape + import galaxy.model +%> + +## Render a row +<%def name="render_row( role, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${role.name} + +
    + Change associated users and groups + Mark role deleted +
    + + ${role.type} + + + + + + %if not anchored: + +
    top
    + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Non-private Roles

    + + + +%if len( roles ) == 0: + There are no non-private Galaxy roles +%else: + + <% + render_quick_find = len( roles ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + + %for ctr, role in enumerate( roles ): + %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( role, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( role, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if role.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( role, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( role, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( role, ctr, True, '' )} + %endif + %endfor +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    NameTypeUsersGroups
    +%endif diff --git a/templates/admin/dataset_security/user.mako b/templates/admin/dataset_security/user.mako new file mode 100644 index 00000000000..49f0b2e07b7 --- /dev/null +++ b/templates/admin/dataset_security/user.mako @@ -0,0 +1,64 @@ +<%inherit file="/base.mako"/> + +<% + import galaxy.model +%> + +## Render a role +<%def name="render_role( user, role )"> +
  • + %if not role.type == galaxy.model.Role.types.PRIVATE: + ${role.name} + %else: + ${role.name} + %endif + +
    + Remove user from role +
    +
  • + + +## Render a group +<%def name="render_group( user, group )"> +
  • + ${group.name} + +
    + Remove user from group +
    +
  • + + +%if msg: +
    ${msg}
    +%endif + +

    User '${user.email}'

    + +%if len( groups ) == 0 and len( roles ) == 0: + User '${user.email}' belongs to no groups and is associated with no roles +%else: + + + + + + + + + +
    GroupsRoles
    +
      + %for group in groups: + ${render_group( user, group )} + %endfor +
    +
    +
      + %for role in roles: + ${render_role( user, role )} + %endfor +
    +
    +%endif diff --git a/templates/admin/dataset_security/user_groups_edit.mako b/templates/admin/dataset_security/user_groups_edit.mako new file mode 100644 index 00000000000..222657f2d8a --- /dev/null +++ b/templates/admin/dataset_security/user_groups_edit.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +## Render a row +<%def name="render_row( group, ctr, anchored, curr_anchor, check )"> + %if ctr % 2 == 1: + + %else: + + %endif + + %if check: + ${group.name} + %else: + ${group.name} + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    User '${user.email}' Associated Groups

    + +%if len( groups ) == 0: + User ${user.email} is not a member of any groups +%else: +
    + + <% + render_quick_find = len( groups ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + %for ctr, group in enumerate( groups ): + <% check = False %> + %for user_group in user_groups: + %if user_group.id == group.id: + <% + check = True + break + %> + %endif + %endfor + %if render_quick_find and not group.name.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and group.name.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( group, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( group, ctr, anchored, curr_anchor, check )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if group.name.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( group, ctr, anchored, curr_anchor, check )} + <% anchored = True %> + %else: + ${render_row( group, ctr, anchored, curr_anchor, check )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( group, ctr, True, '', check )} + %endif + %endfor + +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    Check to add member
    +
    +%endif + diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..cfff791eb12 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,114 @@ +<%inherit file="/base.mako"/> + +<% + import galaxy.model +%> + +## Render a row +<%def name="render_row( user, groups, roles, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${user.email} + +
    + Change associated groups +
    + + + + + + + %if not anchored: + +
    top
    + %endif + + + + +%if msg: +
    ${msg}
    +%endif + +

    Users

    + +%if len( users_groups_roles ) == 0: + There are no Galaxy users +%else: + + <% + render_quick_find = len( users_groups_roles ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + + + %for ctr, user_tuple in enumerate( users_groups_roles ): + <% + user = user_tuple[0] + groups = user_tuple[1] + roles = user_tuple[2] + %> + %if render_quick_find and not user.email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and user.email.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if user.email.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( user, groups, roles, ctr, True, '' )} + %endif + %endfor +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
    EmailGroupsAssociated Non-private Roles
    +%endif diff --git a/templates/admin/index.mako b/templates/admin/index.mako new file mode 100644 index 00000000000..46070c21522 --- /dev/null +++ b/templates/admin/index.mako @@ -0,0 +1,113 @@ +<%inherit file="/base_panels.mako"/> + +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=False + self.active_view="admin" +%> + + +<%def name="stylesheets()"> + + ${parent.stylesheets()} + + ## TODO: Clean up these styles and move into panel_layout.css (they are + ## used here and in the editor). + + + + +<%def name="left_panel()"> +
    +
    Administration
    +
    +
    +
    +
    +
    + Security +
    + +
    +
    + Data +
    + +
    +
    + Tools +
    + +
    +
    +
    + + +<%def name="center_panel()"> + + + + diff --git a/templates/admin/library/add_dataset_from_history.mako b/templates/admin/library/add_dataset_from_history.mako new file mode 100644 index 00000000000..4bb21b01200 --- /dev/null +++ b/templates/admin/library/add_dataset_from_history.mako @@ -0,0 +1,30 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Add Dataset to Library from History +%if error_msg: +

    +

    ${error_msg}
    +
    +

    +%endif +%if ok_msg: +

    +

    ${ok_msg}
    +
    +

    +%endif +

    +

    +
    Active Datasets in your current history (${history.name})
    +
    +
    + + %for dataset in history.active_datasets: +
    + ${dataset.hid}: ${dataset.name} +
    + %endfor + +
    +
    +
    diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako new file mode 100644 index 00000000000..6aefc75d365 --- /dev/null +++ b/templates/admin/library/browser.mako @@ -0,0 +1,184 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + ##if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + ## return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
  • +
    + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif + +
    +
    + Add new dataset to this folder + Copy a dataset from your history to this folder + Create a new subfolder in this folder + Rename this folder + %if subfolder: + Remove this folder and its contents + %endif +
    +
  • + %if subfolder: +
      + %else: +
        + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + ##%if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
      • ${render_dataset( dataset )}
      • + ##%endif + %endfor +
      + + +

      Libraries

      + +%if message: +<% + try: + messagetype + except: + messagetype = "done" +%> +

      +

      + ${message} +
      +

      +%endif + +

      +
      +
        +%if libraries: +%for library in libraries: + ##%if trans.app.security_agent.check_folder_contents( trans.user, library ): +
      • + + + + +
        + + ${library.name} + %if library.description: + - ${library.description} + %endif + + + FormatDbInfo
      • +
          + ${render_folder( library.root_folder, 0 )} +
        +
        + ##%endif +%endfor +
      +
      + With selected datasets: + + +
      +
      +%else: +There are no libraries. +%endif diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako new file mode 100644 index 00000000000..459d34fc564 --- /dev/null +++ b/templates/admin/library/common.mako @@ -0,0 +1,100 @@ +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + ##%if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): + ##
      + ##%else: +
      + ##%endif + + ## Header row for history items (name, state, action buttons) + +
      +
      + + %if data_state == 'running': +
      + %elif data_state != 'ok': +
      + %endif +
      + <%doc> +
      + display data + edit attributes + delete +
      + + + + + + +
      + + ${data.display_name()} + + + ${data.ext}${data.dbkey}${data.info}
      +
      + + ## Body for history items, extra info and actions, data "peek" + +
      +
      + ${data.blurb} +
      +
      + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
      + %if data.peek != "no peek": +
      ${data.display_peek()}
      + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
      + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
      + %endif + %endif +
      +
      + diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako new file mode 100644 index 00000000000..b8ff9597b82 --- /dev/null +++ b/templates/admin/library/dataset.mako @@ -0,0 +1,99 @@ +<%inherit file="/base.mako"/> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + + +<%def name="title()">Edit Dataset Attributes + +<%def name="datatype( dataset, datatypes )"> + + + +%if isinstance( dataset, list ): + ${render_permission_form( dataset[0].dataset, h.url_for( action='dataset' ), 'id', ",".join( [ str(d.id) for d in dataset ] ), trans.app.model.Role.query().all() )} +%else: + ${render_permission_form( dataset.dataset, h.url_for( action='dataset' ), 'id', dataset.id, trans.app.model.Role.query().all() )} +%endif + +%if not isinstance( dataset, list ): +
      +
      Edit Attributes
      +
      +
      + +
      + +
      + +
      +
      +
      +
      + +
      + +
      +
      +
      + %for name, spec in dataset.metadata.spec.items(): + %if spec.visible: +
      + +
      + ${dataset.metadata.get_html_by_name( name )} +
      +
      +
      + %endif + %endfor +
      + +
      +
      +
      + +
      + +
      +
      + This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
      +
      +
      +
      +

      +

      +
      Change data type
      +
      +
      + +
      + +
      + ${datatype( dataset, datatypes )} +
      +
      + This will change the datatype of the existing dataset + but not modify its contents. Use this if Galaxy + has incorrectly guessed the type of your dataset. +
      +
      +
      +
      + +
      +
      +
      +
      +

      +%endif +Return to the library browser diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako new file mode 100644 index 00000000000..8a5c4366e7d --- /dev/null +++ b/templates/admin/library/new_dataset.mako @@ -0,0 +1,101 @@ +<%inherit file="/base.mako"/> + +<% import os %> + +<%def name="title()">Create New Library Dataset +%if msg: +

      ${msg}

      +%endif +
      +
      Create a new Library Dataset
      +
      +
      + +
      + +
      +
      + Upload a single file. Use the "Server Directory" feature below to upload an entire directory of files. +
      +
      +
      +
      + +
      +
      + Specify a list of URLs (one per line) or paste the contents of a file. +
      +
      +
      + %if trans.app.config.library_import_dir is not None: +
      + +
      + +
      +
      + Upload all files in a subdirectory of ${trans.app.config.library_import_dir} on the Galaxy server. +
      +
      +
      + %endif +
      + +
      Yes
      +
      + Use this option if you are manually entering intervals. +
      +
      +
      +
      + +
      + +
      +
      +
      +
      + +
      + +
      +
      +
      +
      +
      + + +
      +
      + Multi-select list - hold the appropriate key while clicking to select multiple roles. More restrictions can be applied after the upload is complete. Selecting no roles makes a dataset public. +
      +
      +
      +
      + +
      +
      +
      +
      diff --git a/templates/admin/reload_tool.mako b/templates/admin/reload_tool.mako new file mode 100644 index 00000000000..3b8e568cfd4 --- /dev/null +++ b/templates/admin/reload_tool.mako @@ -0,0 +1,29 @@ +<%inherit file="/base.mako"/> + +%if msg: +
      ${msg}
      +%endif + +
      +
      Reload Tool
      +
      +
      +
      + + +
      +
      + +
      +
      +
      +
      diff --git a/templates/admin_main.mako b/templates/admin_main.mako deleted file mode 100644 index f9c26121124..00000000000 --- a/templates/admin_main.mako +++ /dev/null @@ -1,33 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Galaxy Administration - - - - - - - - -
      -

      Galaxy Administration

      - %if msg: -

      ${msg}

      - %endif -
      -
      -

      Admin password:

      -

      - Reload tool: - - -

      -
      -
      - diff --git a/templates/base_panels.mako b/templates/base_panels.mako index 29a2dee470e..0d275ed44c2 100644 --- a/templates/base_panels.mako +++ b/templates/base_panels.mako @@ -1,6 +1,18 @@ ## This needs to be on the first line, otherwise IE6 goes into quirks mode +<% + self.has_left_panel=True + self.has_right_panel=True + self.message_box_visible=False + self.message_box_class="" + self.active_view=None +%> + +<%def name="init()"> +## Override + + ## Default title <%def name="title()">Galaxy @@ -8,6 +20,22 @@ <%def name="stylesheets()"> + ## Default javascripts @@ -27,19 +55,30 @@ ## Masthead <%def name="masthead()"> - + + + +## Messagebox +<%def name="message_box_content()"> ## Document + + ${self.init()} + ${self.title()} ${self.javascripts()} @@ -53,17 +92,26 @@
      ${self.masthead()}
      +
      + %if self.message_box_visible: + ${self.message_box_content()} + %endif +
      + %if self.has_left_panel:
      ${self.left_panel()}
      + %endif
      ${self.center_panel()}
      + %if self.has_right_panel:
      + %endif ## Allow other body level elements ${next.body()} diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 39936d24f4c..2dfb0fa22f7 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -1,5 +1,5 @@ <%inherit file="/base.mako"/> -<%def name="title()">History Item Attributes +<%def name="title()">Edit Dataset Attributes <%def name="datatype( dataset, datatypes )"> @@ -15,11 +15,19 @@ +<% +if isinstance( data, trans.app.model.HistoryDatasetAssociation ): + id_name = 'id' +elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + id_name = 'lid' +%> + +%if ( id_name == 'id' or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) ):
      Edit Attributes
      - +
      - +
      @@ -73,39 +81,41 @@

      - <% converters = data.get_converter_types() %> - %if len( converters ) > 0: -

      -
      Convert to new format
      -
      - - -
      - -
      - + %if id_name == 'id': + <% converters = data.get_converter_types() %> + %if len( converters ) > 0: +
      +
      Convert to new format
      +
      + + +
      + +
      + +
      + +
      + This will create a new dataset with the contents of this + dataset converted to a new format. +
      +
      - -
      - This will create a new dataset with the contents of this - dataset converted to a new format. +
      +
      -
      -
      -
      - -
      - -
      -
      - -

      + +

      +
      + +

      + %endif %endif @@ -113,7 +123,7 @@

      Change data type
      - +
      -

      +

      +%else:

      -
      Copy History Item
      +
      View Attributes
      - Click here to make a copy of this history item. +
      + Name: ${data.name} +
      + Info: ${data.info} +
      + Data Format: ${data.ext} +
      + %for element in metadata: + ${element.spec.desc}: ${element.value[0]} +
      + %endfor +
      -

      + +

      +%endif + +%if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ): + +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +${render_permission_form( data.dataset, h.url_for( action='edit' ), id_name, data.id, trans.user.all_roles() )} + +%elif trans.user: + +

      +
      View permissions
      +
      +
      + %if data.dataset.actions: +
        + %for action, roles in trans.app.security_agent.get_dataset_permissions( data.dataset ).items(): + %if roles: +
      • ${action.description}
      • +
          + %for role in roles: +
        • ${role.name}
        • + %endfor +
        + %endif + %endfor +
      + %else: +

      This dataset is accessible by everyone (it is public).

      + %endif +
      +
      +
      + +%endif diff --git a/templates/dataset/security_common.mako b/templates/dataset/security_common.mako new file mode 100644 index 00000000000..73e90557708 --- /dev/null +++ b/templates/dataset/security_common.mako @@ -0,0 +1,85 @@ +<%def name="render_select( current_actions, action_key, action, all_roles )"> + <% + in_roles = [] + for a in current_actions: + if a.action == action.action: + in_roles.append( a.role ) + out_roles = filter( lambda x: x not in in_roles, all_roles ) + %> +

      ${action.description}

      +
      + Roles associated:
      +
      + +
      +
      + Roles not associated:
      +
      + +
      + + +<%def name="render_permission_form( obj, form_url, id_name, id, all_roles )"> +<% + if isinstance( obj, trans.app.model.User ): + current_actions = obj.default_permissions + elif isinstance( obj, trans.app.model.History ): + current_actions = obj.default_permissions + elif isinstance( obj, trans.app.model.Dataset ): + current_actions = obj.actions + else: + current_actions = obj.dataset.actions +%> + + + +
      +
      Associate with roles and set permissions
      +
      + + +
      +
      + %for k, v in trans.app.model.Dataset.permitted_actions.items(): +
      + ${render_select( current_actions, k, v, all_roles )} +
      + %endfor +
      + +
      + +
      +
      +

      + + diff --git a/templates/form.mako b/templates/form.mako index c003de02585..42fdb745ba3 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -21,9 +21,11 @@ $(function(){ cls += " form-row-error" %>

      + %if input.use_label: + %endif
      diff --git a/templates/history/options.mako b/templates/history/options.mako index 8725637305f..c9724ae3724 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -17,6 +17,7 @@
    • Create a new empty history
    • %endif
    • Construct workflow from the current history
    • +
    • Change default permissions for the current history
    • Share current history
    • %endif
    • Show deleted datasets in history
    • diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..6d119ab8e98 --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,7 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default Permissions on New Datasets in This History +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + +%if trans.user: + ${render_permission_form( trans.history, h.url_for(), 'id', None, trans.user.all_roles() )} +%endif diff --git a/templates/history/share.mako b/templates/history/share.mako index 5e64e66756a..8728587cb45 100644 --- a/templates/history/share.mako +++ b/templates/history/share.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%def name="title()">Share histories +%if not can_change and not cannot_change:
      Share Histories
      @@ -27,4 +28,90 @@
      -
      \ No newline at end of file +
      +%else: + +
      + %for history in histories: + + %endfor + +
      + The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access. These datasets are shown below. Datasets which the user already has permission to access are not shown. +
      +

      + %if can_change: +

      + The following datasets can be shared with ${email} by updating their permissions: +

      + + + %for history, datasets in can_change.items(): + + + + + %endfor +
      HistoriesDatasets
      ${history.name} + %for dataset in datasets: + ${dataset.name}
      + %endfor +
      +

      +

      + %endif + %if cannot_change: +

      + The following datasets cannot be shared with ${email} because you do not have permission to change the permissions on them. +

      + + + %for history, datasets in cannot_change.items(): + + + + + %endfor +
      HistoriesDatasets
      ${history.name} + %for dataset in datasets: + ${dataset.name}
      + %endfor +
      +

      +

      + %endif +

      + How would you like to proceed? +

      + %if can_change: + Set datasets above to public access + %if cannot_change: + (where possible) + %endif +
      + Set datasets above to private access for me and the user(s) with whom I am sharing + %if cannot_change: + (where possible) + %endif +
      + %endif + Share anyway + %if can_change: + (don't change any permissions) + %endif +
      + Don't share
      +
      +
      +

      +
      +%endif diff --git a/templates/library/browser.mako b/templates/library/browser.mako new file mode 100644 index 00000000000..ef0f94a5cfe --- /dev/null +++ b/templates/library/browser.mako @@ -0,0 +1,144 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
    • +
      + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif +
      +
    • + %if subfolder: +
        + %else: +
          + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + %if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
        • ${render_dataset( dataset )}
        • + %endif + %endfor +
        + + +

        Libraries

        +
        +
          +%for library in libraries: + %if trans.app.security_agent.check_folder_contents( trans.user, library ): +
        • + + + + +
          + + ${library.name} + %if library.description: + - ${library.description} + %endif + FormatDbInfo
        • +
            + ${render_folder( library.root_folder, 0 )} +
          +
          + %endif +%endfor +
        + +
        diff --git a/templates/library/common.mako b/templates/library/common.mako new file mode 100644 index 00000000000..9861833113e --- /dev/null +++ b/templates/library/common.mako @@ -0,0 +1,107 @@ +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
        + %else: +
        + %endif + + ## Header row for history items (name, state, action buttons) + +
        +
        + + %if data_state == 'running': +
        + %elif data_state != 'ok': +
        + %endif +
        + <%doc> +
        + display data + edit attributes + delete +
        + + + + + + +
        +
        + + view or edit attributes + +
        + + ${data.display_name()} +
        ${data.ext}${data.dbkey}${data.info}
        +
        + + ## Body for history items, extra info and actions, data "peek" + +
        + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
        You do not have permission to view this dataset.
        + %else: +
        + ${data.blurb} +
        +
        + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
        + %if data.peek != "no peek": +
        ${data.display_peek()}
        + %endif + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
        + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
        + %endif + %endif +
        +
        + diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 139bf19dfb1..f09229dbd1c 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -6,7 +6,11 @@ else: data_state = data.state %> -
        + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
        + %else: +
        + %endif %if data.deleted:
        @@ -36,7 +40,9 @@ ## Body for history items, extra info and actions, data "peek"
        - %if data_state == "queued": + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
        You do not have permission to view this dataset.
        + %elif data_state == "queued":
        Job is waiting to run
        %elif data_state == "running":
        Job is currently running
        diff --git a/templates/root/index.mako b/templates/root/index.mako index f573571162b..bcc6b3d8ee7 100644 --- a/templates/root/index.mako +++ b/templates/root/index.mako @@ -1,5 +1,13 @@ <%inherit file="/base_panels.mako"/> +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=True + self.active_view="analysis" +%> + + <%def name="left_panel()">
        Tools
        diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 58d8f1776db..a28931773ad 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -9,32 +9,56 @@ - + +
        Galaxy${brand}
        - - Info: report bugs - | wiki - | screencasts - | blog + View: + analysis + | workflow + %if admin_user == "true": + | admin + %endif + + +     + + Info: report bugs + | wiki + | screencasts +     + %if app.config.use_remote_user: Logged in as ${t.user.email} %else: %if t.user: - Logged in as ${t.user.email}: manage - | logout + Logged in as ${t.user.email}: manage + | logout %else: - Account: create - | login + Account: create + | login %endif %endif   +
        diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index c2d77210905..04af8526c4c 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -110,4 +110,4 @@ - \ No newline at end of file + diff --git a/templates/user/index.mako b/templates/user/index.mako index 2b11262cece..04303c9b0a4 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,7 @@ %else: @@ -16,4 +17,4 @@
      • Login
      • Create new account
      -%endif \ No newline at end of file +%endif diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..6e00a3f1a53 --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,7 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default Permissions on New Histories +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + +%if trans.user: + ${render_permission_form( trans.user, h.url_for(), 'id', None, trans.user.all_roles() )} +%endif diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 18d7b862fed..3b3cf7ee055 100644 --- a/templates/workflow/editor.mako +++ b/templates/workflow/editor.mako @@ -1,6 +1,18 @@ <%inherit file="/base_panels.mako"/> -<%def name="title()">Galaxy Workflow Editor +<%def name="init()"> +<% + self.active_view="workflow" + self.message_box_visible=True + self.message_box_class="warning" +%> + + +<%def name="message_box_content()"> + Workflow support is currently in beta testing. + Workflows may not work with all tools, may fail unexpectedly, and may + not be compatible with future updates to Galaxy. + <%def name="late_javascripts()"> @@ -229,7 +241,7 @@ } var close_editor = function() { - <% next_url = h.url_for( controller='root', m_c='workflow' ) %> + <% next_url = h.url_for( controller='workflow', action='index' ) %> if ( workflow && workflow.has_changes ) { do_close = function() { window.onbeforeunload = undefined; @@ -301,11 +313,12 @@ <%def name="stylesheets()"> - ${parent.stylesheets()} - - ## Also include "base.css" for styling tool menu and forms (details) + ## Include "base.css" for styling tool menu and forms (details) + ## But make sure styles for the layout take precedence + ${parent.stylesheets()} +