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/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index a3b173adfac..c7e15a4bde7 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -5,8 +5,10 @@ + + @@ -17,6 +19,7 @@ + @@ -38,6 +41,7 @@ + @@ -141,6 +145,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + interval_to_coverage.py $input1 $output1 + -1 ${input1.metadata.chromCol},${input1.metadata.startCol},${input1.metadata.endCol},${input1.metadata.strandCol} + -2 ${output1.metadata.chromCol},${output1.metadata.positionCol},${output1.metadata.forwardCol},${output1.metadata.reverseCol} + + + + + + + + + + + + diff --git a/lib/galaxy/datatypes/coverage.py b/lib/galaxy/datatypes/coverage.py new file mode 100644 index 00000000000..60a3d12a1d4 --- /dev/null +++ b/lib/galaxy/datatypes/coverage.py @@ -0,0 +1,30 @@ +""" +Coverage datatypes + +""" +import pkg_resources +pkg_resources.require( "bx-python" ) + +import logging, os, sys, time, sets, tempfile, shutil +import data +from galaxy import util +from galaxy.datatypes.sniff import * +from galaxy.web import url_for +from cgi import escape +import urllib +from bx.intervals.io import * +from galaxy.datatypes import metadata +from galaxy.datatypes.metadata import MetadataElement +from galaxy.datatypes.tabular import Tabular + +log = logging.getLogger(__name__) + +class LastzCoverage( Tabular ): + file_ext = "coverage" + + MetadataElement( name="chromCol", default=1, desc="Chrom column", param=metadata.ColumnParameter ) + MetadataElement( name="positionCol", default=2, desc="Position column", param=metadata.ColumnParameter ) + MetadataElement( name="forwardCol", default=3, desc="Forward or aggregate read column", param=metadata.ColumnParameter ) + MetadataElement( name="reverseCol", desc="Optional reverse read column", param=metadata.ColumnParameter, optional=True, no_value=0 ) + MetadataElement( name="columns", default=3, desc="Number of columns", readonly=True, visible=False ) + diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 5cee678b67c..d9df85a6739 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) @@ -192,12 +195,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/genetics.py b/lib/galaxy/datatypes/genetics.py index 10d4ad9dc28..6edfbc64264 100644 --- a/lib/galaxy/datatypes/genetics.py +++ b/lib/galaxy/datatypes/genetics.py @@ -139,10 +139,10 @@ class SNPMatrix(Rgenetics): else: dataset.peek = 'file does not exist' dataset.blurb = 'file purged from disk' - def sniff( self, filename ): - """ - """ - return True + #def sniff( self, filename ): + # """ + # """ + # return True class Lped(Rgenetics): """fake class to distinguish different species of Rgenetics data collections diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index dab8e4a636e..3d498578933 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__) @@ -123,20 +123,25 @@ 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 ): if not dataset.dataset.purged: - 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 ) - dataset.blurb = 'GMAJ Multiple Alignment Viewer' + 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 ) + dataset.blurb = 'GMAJ Multiple Alignment Viewer' + 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' else: dataset.peek = 'file does not exist' dataset.blurb = 'file purged from disk' @@ -203,19 +208,23 @@ 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 ): if not dataset.dataset.purged: - 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 ) - dataset.blurb = 'LAJ Multiple Alignment Viewer' + 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' else: dataset.peek = 'file does not exist' dataset.blurb = 'file purged from disk' diff --git a/lib/galaxy/datatypes/qualityscore.py b/lib/galaxy/datatypes/qualityscore.py index 188cd413b0d..cdd66b287f4 100644 --- a/lib/galaxy/datatypes/qualityscore.py +++ b/lib/galaxy/datatypes/qualityscore.py @@ -32,5 +32,43 @@ class QualityScore ( data.Text ): except: return "Quality score file (%s)" % ( data.nice_size( dataset.get_size() ) ) +class SolidQualityScore( data.Text ): + """ + Quality scores generated by ABI SOLiD + """ + file_ext = "solidqual" - \ No newline at end of file + def set_peek( self, dataset ): + dataset.peek = data.get_file_peek( dataset.file_name ) + dataset.blurb = data.nice_size( dataset.get_size() ) + def sniff( self, filename ): + """ + >>> fname = get_test_fname( 'sequence.fasta' ) + >>> SolidQualityScore().sniff( fname ) + False + >>> fname = get_test_fname( 'sequence.solidqual' ) + >>> SolidQualityScore().sniff( fname ) + True + """ + try: + fh = open( filename ) + while True: + line = fh.readline() + if not line: + break #EOF + line = line.strip() + if line and not line.startswith( '#' ): #first non-empty non-comment line + if line.startswith( '>' ): + line = fh.readline().strip() + if line == '' or line.startswith( '>' ): + break + try: + [ int( x ) for x in line.split() ] + except: + break + return True + else: + break #we found a non-empty line, but it's not a header + except: + pass + return False diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index b7cd9b58b7c..816851d7437 100644 --- a/lib/galaxy/datatypes/registry.py +++ b/lib/galaxy/datatypes/registry.py @@ -3,7 +3,7 @@ Provides mapping between extensions and datatypes, mime-types, etc. """ import os import logging -import data, tabular, interval, images, sequence, qualityscore, genetics, xml +import data, tabular, interval, images, sequence, qualityscore, genetics, xml, coverage, tracks import galaxy.util from galaxy.util.odict import odict @@ -94,12 +94,14 @@ class Registry( object ): 'bed' : interval.Bed(), 'binseq.zip' : images.Binseq(), 'blastxml' : xml.BlastXml(), + 'coverage' : coverage.LastzCoverage(), 'customtrack' : interval.CustomTrack(), 'csfasta' : sequence.csFasta(), 'fasta' : sequence.Fasta(), 'fastqsolexa' : sequence.FastqSolexa(), 'gff' : interval.Gff(), - 'gff3' : interval.Gff3(), + 'gff3' : interval.Gff3(), + 'genetrack' : tracks.GeneTrack(), 'interval' : interval.Interval(), 'laj' : images.Laj(), 'lav' : sequence.Lav(), diff --git a/lib/galaxy/datatypes/sequence.py b/lib/galaxy/datatypes/sequence.py index 13144ef652c..10d54cffa03 100644 --- a/lib/galaxy/datatypes/sequence.py +++ b/lib/galaxy/datatypes/sequence.py @@ -5,6 +5,7 @@ Image classes import data import logging import re +import string from cgi import escape from galaxy.datatypes.metadata import MetadataElement from galaxy.datatypes import metadata @@ -102,15 +103,37 @@ class csFasta( Sequence ): Color-space sequence: >2_15_85_F3 T213021013012303002332212012112221222112212222 - - TODO: - add sniff function - """ - - return False - - + >>> fname = get_test_fname( 'sequence.fasta' ) + >>> csFasta().sniff( fname ) + False + >>> fname = get_test_fname( 'sequence.csfasta' ) + >>> csFasta().sniff( fname ) + True + """ + try: + fh = open( filename ) + while True: + line = fh.readline() + if not line: + break #EOF + line = line.strip() + if line and not line.startswith( '#' ): #first non-empty non-comment line + if line.startswith( '>' ): + line = fh.readline().strip() + if line == '' or line.startswith( '>' ): + break + elif line[0] not in string.ascii_uppercase: + return False + elif len( line ) > 1 and not re.search( '^\d+$', line[1:] ): + return False + return True + else: + break #we found a non-empty line, but it's not a header + except: + pass + return False + class FastqSolexa( Sequence ): """Class representing a FASTQ sequence ( the Solexa variant )""" file_ext = "fastqsolexa" diff --git a/lib/galaxy/datatypes/sniff.py b/lib/galaxy/datatypes/sniff.py index da913abf86f..2dab19a60d1 100644 --- a/lib/galaxy/datatypes/sniff.py +++ b/lib/galaxy/datatypes/sniff.py @@ -25,6 +25,22 @@ def stream_to_file( stream, suffix='', prefix='', dir=None, text=False ): os.close(fd) return temp_name +def check_newlines( fname, bytes_to_read=52428800 ): + """ + Determines if there are any non-POSIX newlines in the first + number_of_bytes (by default, 50MB) of the file. + """ + CHUNK_SIZE = 2 ** 20 + f = open( fname, 'r' ) + for chunk in f.read( CHUNK_SIZE ): + if f.tell() > bytes_to_read: + break + if chunk.count( '\r' ): + f.close() + return True + f.close() + return False + def convert_newlines( fname ): """ Converts in place a file from universal line endings diff --git a/lib/galaxy/datatypes/tracks.py b/lib/galaxy/datatypes/tracks.py new file mode 100644 index 00000000000..1c5a9291bef --- /dev/null +++ b/lib/galaxy/datatypes/tracks.py @@ -0,0 +1,30 @@ +""" +Datatype classes for tracks/track views within galaxy. +""" + +import data +import logging +import re +from cgi import escape +from galaxy.datatypes.metadata import MetadataElement +from galaxy.datatypes import metadata +import galaxy.model +from galaxy import util +from galaxy.web import url_for +from sniff import * + +log = logging.getLogger(__name__) + +class GeneTrack( data.Binary ): + file_ext = "genetrack" + + MetadataElement( name="hdf", default="data.hdf", desc="HDF DB", readonly=True, visible=True, no_value=0 ) + MetadataElement( name="sqlite", default="features.sqlite", desc="SQLite Features DB", readonly=True, visible=True, no_value=0 ) + MetadataElement( name="label", default="Custom", desc="Track Label", readonly=True, visible=True, no_value="Custom" ) + + def __init__(self, **kwargs): + super(GeneTrack, self).__init__(**kwargs) + self.add_display_app( 'genetrack', 'View in ', '', 'genetrack_link' ) + + def genetrack_link( self, dataset, type, app, base_url ): + return [('GeneTrack', url_for(controller='genetrack', action='index', dataset_id=dataset.id ))] \ No newline at end of file diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 1586d90c067..51431381063 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__ ) @@ -31,15 +32,25 @@ class User( object ): self.email = email self.password = password self.external = False + self.deleted = False + self.purged = 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 @@ -130,207 +141,16 @@ 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 ) - - def get_metadata( self ): - if not hasattr( self, '_metadata_collection' ) or self._metadata_collection.parent != self: #using weakref to store parent (to prevent circ ref), does a Session.clear() cause parent to be invalidated, while still copying over this non-database attribute? - 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.set_size() - 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 ) - # In some instances peek relies on dataset_id ( e.g., gmaj.zip for viewing MAFs ) - des.set_peek() - 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 ): +class Group( object ): + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) + def __init__( self, name = None ): + self.name = name 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 ): @@ -405,6 +225,65 @@ 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 ActionLibraryItemRoleAssociation( object ): + def __init__( self, action, library_item, role ): + self.action = action + + if isinstance( library_item, LibraryDataset ): + self.library_dataset = library_item + elif isinstance( library_item, Library ): + self.library = library_item + elif isinstance( library_item, LibraryFolder ): + self.folder = library_item + elif isinstance( library_item, LibraryFolderDatasetAssociation ): + self.library_folder_dataset_association = library_item + else: + raise "Unknown library item type specified: %s" % library_item.__class__.__name__ + 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', @@ -413,6 +292,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 ): @@ -498,8 +378,556 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class Old_Dataset( Dataset ): +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.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 + 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' ) or self._metadata_collection.parent != self: #using weakref to store parent (to prevent circ ref), does a Session.clear() cause parent to be invalidated, while still copying over this non-database attribute? + 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 + + @property + def source_library_dataset( self ): + def get_source( dataset ): + if isinstance( dataset, LibraryFolderDatasetAssociation ): + if dataset.library_dataset: + return ( dataset, dataset.library_dataset ) + if dataset.copied_from_library_folder_dataset_association: + source = get_source( dataset.copied_from_library_folder_dataset_association ) + if source: + return source + if dataset.copied_from_history_dataset_association: + source = get_source( dataset.copied_from_history_dataset_association ) + if source: + return source + return ( None, None ) + return get_source( self ) + +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.set_size() + 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: + new_data = LibraryDataset( library_folder_dataset_association = des ) + target_folder.add_dataset( new_data ) + new_data.flush() + 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, copied_from = 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 + + def get_library_item_info_templates( self, template_list = [] ): + if self.library_item_info_template_associations: + template_list.extend( [ library_item_info_template_association.library_item_info_template for library_item_info_template_association in self.library_item_info_template_associations if library_item_info_template_association.library_item_info_template not in template_list ] ) + return template_list + +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 ): + #this should create a LibraryDataset if lfda is passed + 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 + + def get_library_item_info_templates( self, template_list = [] ): + if self.library_item_info_template_associations: + template_list.extend( [ library_item_info_template_association.library_item_info_template for library_item_info_template_association in self.library_item_info_template_associations if library_item_info_template_association.library_item_info_template not in template_list ] ) + if self.parent: + self.parent.get_library_item_info_templates( template_list ) + elif self.library_root: + for library_root in self.library_root: + library_root.get_library_item_info_templates( template_list ) + return template_list + + @property + def active_components( self ): + return list( self.active_folders ) + list( self.active_datasets ) + +class LibraryDataset( object ): + #This class acts as a proxy to the currently selected LFDA + def __init__( self, + folder = None, + order_id = None, + name = None, + info = None, + library_folder_dataset_association = None, + create_dataset=False, + **kwd + ): + self.folder = folder + self.order_id = order_id + self.name = name + self.info = info + if create_dataset and not library_folder_dataset_association: + #self.flush() #we need to flush self, so that the lfda will have a ld id to point to + library_folder_dataset_association = LibraryFolderDatasetAssociation( name=name, info=info, create_dataset=create_dataset, **kwd ) + self.library_folder_dataset_association = library_folder_dataset_association + + def set_library_folder_dataset_association( self, dataset ): + self.library_folder_dataset_association = dataset + dataset.library_dataset = self + dataset.flush() + self.flush() + + def get_info( self ): + #Use info from ldfa if versioned info is not set + if self._info: + return self._info + return self.library_folder_dataset_association.info + def set_info( self, info ): + self._info = info + info = property( get_info, set_info ) + + def get_name( self ): + #Use info from ldfa if versioned info is not set + if self._name: + return self._name + return self.library_folder_dataset_association.name + def set_name( self, name ): + self._name = name + name = property( get_name, set_name ) + + def display_name( self ): + #use name from ldfa is versioned info is not set + if self._name: + return self.datatype.display_name( self ) + self.library_folder_dataset_association.display_name() + + def __getattr__( self, name ): + return getattr( self.library_folder_dataset_association, name ) #Any nonexistant attributes will be pulled from the lfda + + def get_library_item_info_templates( self, template_list = [] ): + if self.library_item_info_template_associations: + template_list.extend( [ library_item_info_template_association.library_item_info_template for library_item_info_template_association in self.library_item_info_template_associations if library_item_info_template_association.library_item_info_template not in template_list ] ) + self.folder.get_library_item_info_templates( template_list ) + return template_list + +class LibraryFolderDatasetAssociation( DatasetInstance ): + def __init__( self, + #folder = None, + #order_id = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + library_dataset = None, + **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.library_dataset = library_dataset + #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 + def get_library_item_info_templates( self, template_list = [] ): + if self.library_item_info_template_associations: + template_list.extend( [ library_item_info_template_association.library_item_info_template for library_item_info_template_association in self.library_item_info_template_associations if library_item_info_template_association.library_item_info_template not in template_list ] ) + self.library_dataset.get_library_item_info_templates( template_list ) + return template_list + +class LibraryItemInfoTemplateAssociation( object ): pass +class LibraryItemInfoTemplate( object ): + def add_element( self, element = None, name = None, description = None ): + if element: + raise "undefined" + else: + new_elem = LibraryItemInfoTemplateElement() + new_elem.name = name + new_elem.description = description + new_elem.order_id = self.item_count + self.item_count += 1 + self.flush() + new_elem.library_item_info_template_id = self.id + new_elem.flush() + return new_elem + + """class InfoField( object ): + def __init__( self, name, description, type ): + self.name = name + self.description = description + self.type = type + class StringInfoField( InfoField ): + def __init__( self, name, description, type = 'string' ): + InfoField.__init__( self, name, description, type ) + def __init__( self, name='unnamed', contents=None ): + self.contents = contents + """ +class LibraryItemInfoTemplateElement( object ): + pass +class LibraryItemInfoAssociation( object ): + def set_library_item( self, library_item ): + if isinstance( library_item, Library ): + self.library = library_item + elif isinstance( library_item, LibraryDataset ): + self.library_dataset = library_item + elif isinstance( library_item, LibraryFolder ): + self.folder = library_item + elif isinstance( library_item, LibraryFolderDatasetAssociation ): + self.library_folder_dataset_association = library_item + else: + raise 'unimplemented' + +class LibraryItemInfo( object ): + def get_element_by_template_element( self, template_element ): + for element in self.elements: + if element.library_item_info_template_element == template_element: + return element + raise 'element not found' +class LibraryItemInfoElement( object ): + pass + +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 ): @@ -541,7 +969,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 @@ -611,7 +1048,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 ): @@ -641,3 +1081,5 @@ def directory_hash_id( id ): padded = padded[:-3] # Break into chunks of three return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ] + + diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 75391720639..2c5a04ecfc9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -13,6 +13,7 @@ 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, LibraryRBACAgent metadata = MetaData() context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) @@ -42,7 +43,9 @@ User.table = Table( "galaxy_user", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "email", TrimmedString( 255 ), nullable=False ), Column( "password", TrimmedString( 40 ), nullable=False ), - Column( "external", Boolean, default=False ) ) + Column( "external", Boolean, default=False ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "purged", Boolean, index=True, default=False ) ) History.table = Table( "history", metadata, Column( "id", Integer, primary_key=True), @@ -62,7 +65,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 ), @@ -70,6 +72,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 ) ), @@ -111,6 +114,216 @@ 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", String( 255 ), 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", String( 255 ), index=True, unique=True ), + Column( "description", TEXT ), + Column( "type", String( 40 ), 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 ) ) + +ActionLibraryItemRoleAssociation.table = Table( "action_library_item_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "action", TEXT ), + Column( "library_folder_id", Integer, ForeignKey( "library_folder.id" ), nullable=True, index=True ), + Column( "library_id", Integer, ForeignKey( "library.id" ), nullable=True, index=True ), + Column( "library_dataset_id", Integer, ForeignKey( "library_dataset.id" ), nullable=True, index=True ), + Column( "library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True, index=True ), + Column( "library_item_info_template_id", Integer, ForeignKey( "library_item_info_template.id" ), nullable=True, index=True ), + Column( "library_item_info_id", Integer, ForeignKey( "library_item_info.id" ), nullable=True, 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 ) ) + +LibraryDataset.table = Table( "library_dataset", metadata, + Column( "id", Integer, primary_key=True ), + Column( "library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id", use_alter=True, name="library_folder_dataset_association_id_fk" ), nullable=True, index=True ),#current version of dataset, if null, there is not a current version selected + 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( "name", TrimmedString( 255 ), key="_name" ), #when not None/null this will supercede display in library (but not when imported into user's history?) + Column( "info", TrimmedString( 255 ), key="_info" ), #when not None/null this will supercede display in library (but not when imported into user's history?) + Column( "deleted", Boolean, index=True, default=False ), + ) + +#this should be renamed, no longer an association between dataset and folder +LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "library_dataset_id", Integer, ForeignKey( "library_dataset.id" ), index=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + 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", String( 255 ), index=True ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "purged", 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( "purged", Boolean, index=True, default=False ), + Column( "genome_build", TrimmedString( 40 ) ) ) + +LibraryItemInfoTemplateElement.table = Table( "library_item_info_template_element", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "optional", Boolean, index=True, default=True ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "name", TEXT ), + Column( "description", TEXT ), + Column( "type", TEXT, default='string' ), + Column( "order_id", Integer ), + Column( "options", JSONType() ), + Column( "library_item_info_template_id", Integer, ForeignKey( "library_item_info_template.id" ), index=True ) ) + +LibraryItemInfoTemplate.table = Table( "library_item_info_template", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "optional", Boolean, index=True, default=True ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "name", TEXT ), + Column( "description", TEXT ), + Column( "item_count", Integer, default=0 ) ) + +LibraryItemInfoTemplateAssociation.table = Table( "library_item_info_template_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "library_folder_id", Integer, ForeignKey( "library_folder.id" ), nullable=True, index=True ), + Column( "library_id", Integer, ForeignKey( "library.id" ), nullable=True, index=True ), + Column( "library_dataset_id", Integer, ForeignKey( "library_dataset.id" ), nullable=True, index=True ), + Column( "library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True, index=True ), + Column( "library_item_info_template_id", Integer, ForeignKey( "library_item_info_template.id" ), index=True ) ) + +LibraryItemInfoElement.table = Table( "library_item_info_element", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "contents", TEXT ), + Column( "library_item_info_id", Integer, ForeignKey( "library_item_info.id" ), index=True ), + Column( "library_item_info_template_element_id", Integer, ForeignKey( "library_item_info_template_element.id" ), index=True ) ) + +LibraryItemInfo.table = Table( "library_item_info", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), nullable=True, index=True ), + Column( "library_item_info_template_id", Integer, ForeignKey( "library_item_info_template.id" ), nullable=True, index=True ) + ) + +LibraryItemInfoAssociation.table = Table( "library_item_info_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "library_folder_id", Integer, ForeignKey( "library_folder.id" ), nullable=True, index=True ), + Column( "library_id", Integer, ForeignKey( "library.id" ), nullable=True, index=True ), + Column( "library_dataset_id", Integer, ForeignKey( "library_dataset.id" ), nullable=True, index=True ), + Column( "library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True, index=True ), + Column( "library_item_info_id", Integer, ForeignKey( "library_item_info.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), nullable=True, index=True ) ) + + +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 ), @@ -239,6 +452,7 @@ MetadataFile.table = Table( "metadata_file", metadata, Column( "id", Integer, primary_key=True ), Column( "name", TEXT ), 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 ), @@ -258,21 +472,41 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab 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 ) ), + #copied_from_library_folder_dataset_associations=relation( + # LibraryFolderDatasetAssociation, + # primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + # backref=backref( "copied_to_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.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 ) ), + visible_children=relation( + HistoryDatasetAssociation, + primaryjoin=( ( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ) & ( HistoryDatasetAssociation.table.c.visible == True ) ) ) ) ) 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 ) ), + active_history_associations=relation( + HistoryDatasetAssociation, + primaryjoin=( ( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) & ( HistoryDatasetAssociation.table.c.deleted == False ) ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ), + active_library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( ( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) & ( LibraryFolderDatasetAssociation.table.c.deleted == False ) ) ) ) ) @@ -297,14 +531,210 @@ 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 ) ), galaxy_sessions=relation( GalaxySession, order_by=desc( GalaxySession.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, ActionLibraryItemRoleAssociation, ActionLibraryItemRoleAssociation.table, + properties=dict( + folder=relation( LibraryFolder, backref="actions" ), + library=relation( Library, backref="actions" ), + library_dataset=relation( LibraryDataset, backref="actions" ), + library_folder_dataset_association = relation( LibraryFolderDatasetAssociation, backref="actions" ), + library_item_info = relation( LibraryItemInfo, backref="actions" ), + library_item_info_template = relation( LibraryItemInfoTemplate, backref="actions" ), + role=relation( Role, backref="library_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 ), + datasets=relation( LibraryDataset, + primaryjoin=( ( LibraryDataset.table.c.folder_id == LibraryFolder.table.c.id ) ), + order_by=asc( LibraryDataset.table.c.order_id ), + lazy=False, + viewonly=True ), + active_datasets=relation( LibraryDataset, + primaryjoin=( ( LibraryDataset.table.c.folder_id == LibraryFolder.table.c.id ) & ( not_( LibraryDataset.table.c.deleted ) ) ), + order_by=asc( LibraryDataset.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, LibraryDataset, LibraryDataset.table, + properties=dict( + #dataset=relation( Dataset ), + folder=relation( LibraryFolder ), + library_folder_dataset_association=relation( LibraryFolderDatasetAssociation, primaryjoin=( LibraryDataset.table.c.library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ) ), + expired_datasets = relation( LibraryFolderDatasetAssociation, foreign_keys=[LibraryDataset.table.c.id,LibraryDataset.table.c.library_folder_dataset_association_id ], primaryjoin=( ( LibraryDataset.table.c.id == LibraryFolderDatasetAssociation.table.c.library_dataset_id ) & ( not_( LibraryDataset.table.c.library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ) ) ), viewonly=True, uselist=True ) + #expired_datasets = relation( LibraryFolderDatasetAssociation, secondary=LibraryFolderDatasetAssociation.table, primaryjoin=( LibraryDataset.table.c.id == LibraryFolderDatasetAssociation.table.c.library_dataset_id), secondaryjoin = ( LibraryDataset.table.c.library_folder_dataset_association_id != LibraryFolderDatasetAssociation.table.c.id ), viewonly=True ) + + #expired_datasets = relation( LibraryFolderDatasetAssociation, primaryjoin=( and_( LibraryDataset.table.c.id == LibraryFolderDatasetAssociation.table.c.library_dataset_id, LibraryDataset.table.c.library_folder_dataset_association_id != LibraryFolderDatasetAssociation.table.c.id ) ) ) + + #expired_datasets = relation( LibraryFolderDatasetAssociation, primaryjoin=( ( LibraryDataset.table.c.id == LibraryFolderDatasetAssociation.table.c.library_dataset_id ) & ( not_( LibraryDataset.table.c.library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ) ) ) ) + #expired_datasets = relation( LibraryFolderDatasetAssociation, primaryjoin=( ( LibraryDataset.table.c.id == LibraryFolderDatasetAssociation.table.c.library_dataset_id ) & ( not_( LibraryDataset.table.c.library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ) ) ), + # order_by=asc( LibraryDataset.table.c.order_id ), + # lazy=False, + # viewonly=True ), + + + #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] ) ), + #visible_children=relation( + # LibraryFolderDatasetAssociation, + # primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ) & ( LibraryFolderDatasetAssociation.table.c.visible == True ) ) ), + #tags=relation( + # LibraryTagDatasetAssociation, + # primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + # backref=backref( "datasets" ) ) + ) ) + +assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAssociation.table, + properties=dict( + dataset=relation( Dataset ), + library_dataset = relation( LibraryDataset, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.library_dataset_id == LibraryDataset.table.c.id ) ), + #folder=relation( LibraryFolder ), + 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] ) ), + copied_to_history_dataset_associations=relation( + HistoryDatasetAssociation, + primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_library_folder_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 ) ), + 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] ) ), + visible_children=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ) & ( LibraryFolderDatasetAssociation.table.c.visible == True ) ) ), + tags=relation( + LibraryTagDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + backref=backref( "datasets" ) ) + ) ) + + +assign_mapper( context, LibraryItemInfoTemplateElement, LibraryItemInfoTemplateElement.table, + properties=dict( library_item_info_template=relation( LibraryItemInfoTemplate, backref="elements" ), + ) ) + +assign_mapper( context, LibraryItemInfoTemplate, LibraryItemInfoTemplate.table ) + +assign_mapper( context, LibraryItemInfoTemplateAssociation, LibraryItemInfoTemplateAssociation.table, + properties=dict( folder=relation( LibraryFolder, backref="library_item_info_template_associations" ), + library=relation( Library, backref="library_item_info_template_associations" ), + library_dataset=relation( LibraryDataset, backref="library_item_info_template_associations" ), + library_folder_dataset_association = relation( LibraryFolderDatasetAssociation, backref="library_item_info_template_associations" ), + library_item_info_template = relation( LibraryItemInfoTemplate, backref="library_item_info_template_associations" ), + ) ) + + +assign_mapper( context, LibraryItemInfoElement, LibraryItemInfoElement.table, + properties=dict( library_item_info=relation( LibraryItemInfo, backref="elements" ), + library_item_info_template_element=relation( LibraryItemInfoTemplateElement ) + ) ) + +assign_mapper( context, LibraryItemInfo, LibraryItemInfo.table, + properties=dict( library_item_info_template=relation( LibraryItemInfoTemplate, backref="library_item_infos" ), + ) ) + +assign_mapper( context, LibraryItemInfoAssociation, LibraryItemInfoAssociation.table, + properties=dict( folder=relation( LibraryFolder, backref="library_item_info_associations" ), + library=relation( Library, backref="library_item_info_associations" ), + library_dataset=relation( LibraryDataset, backref="library_item_info_associations" ), + library_folder_dataset_association = relation( LibraryFolderDatasetAssociation, backref="library_item_info_associations" ), + library_item_info = relation( LibraryItemInfo, backref="library_item_info_associations" ), + ) ) + +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, lazy=False ) ) ) @@ -370,7 +800,7 @@ 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 ): """ @@ -390,13 +820,13 @@ def db_next_hid( self ): raise History._next_hid = db_next_hid - -def init( file_path, url, engine_options={}, create_tables=False ): - """Connect mappings to the database""" - # Connect dataset to the file path - Dataset.file_path = file_path + +def guess_dialect_for_url( url ): + return (url.split(':', 1))[0] + +def load_egg_for_url( url ): # Load the appropriate db module - dialect = (url.split(':', 1))[0] + dialect = guess_dialect_for_url( url ) try: egg = dialect_to_egg[dialect] try: @@ -408,6 +838,13 @@ def init( file_path, url, engine_options={}, create_tables=False ): except KeyError: # Let this go, it could possibly work with db's we don't support log.error( "database_connection contains an unknown SQLAlchemy database dialect: %s" % dialect ) + +def init( file_path, url, engine_options={}, create_tables=False ): + """Connect mappings to the database""" + # Connect dataset to the file path + Dataset.file_path = file_path + # Load the appropriate db module + load_egg_for_url( url ) # Create the database engine engine = create_engine( url, **engine_options ) # Connect the metadata to the database. @@ -427,9 +864,13 @@ def init( file_path, url, engine_options={}, create_tables=False ): # 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 ) + result.library_security_agent = LibraryRBACAgent( result ) return result def get_suite(): """Get unittest suite for this module""" import unittest, mapping_tests return unittest.makeSuite( mapping_tests.MappingTests ) + diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..f1ff64a7b2d --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,535 @@ +""" +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_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" ) + ) + #Actions that can be performed on Library Items + library_actions = Bunch( + LIBRARY_ADD = Action( + "add library item", "Role members can add library items to this folder", "grant" ), + LIBRARY_MODIFY = Action( + "modify library item", "Role members can modify this library item", "grant" ), + LIBRARY_MANAGE = Action( + "manage library permissions", "Role members can manage roles associated with this library item", "grant" ) + ) + + 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 of Action objects + """ + 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={}, history=False, dataset=False ): + raise "Unimplemented Method" + def history_set_default_permissions( self, history, permissions=None, dataset=False, bypass_manage_permission=False ): + raise "Unimplemented Method" + def set_all_dataset_permissions( self, dataset, permissions ): + raise "Unimplemented Method" + def set_dataset_permission( self, dataset, permission ): + raise "Unimplemented Method" + def make_dataset_public( self, dataset ): + 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, bypass_manage_permission=False ): + # bypass_manage_permission is used to change permissions of datasets in a userless history when logging in + 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 current default permissions for the user + for dup in user.default_permissions: + dup.delete() + dup.flush() + # Add the new default permissions for the user + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for dup in [ self.model.DefaultUserPermissions( user, action, role ) for role in roles ]: + dup.flush() + if history: + for history in user.active_histories: + """ + TODO: instead of user.active_histories, a faster approach would be the following, but we need the + history.activatable_datasets mapper corrected so that eagerload can be used. + histories = app.model.History.filter( and_( app.model.History.table.c.user_id==user.id, + app.model.History.table.c.purged==False ) ) \ + .options( eagerload( 'activatable_datasets' ) ).all() + """ + self.history_set_default_permissions( history, permissions=permissions, dataset=dataset, bypass_manage_permission=bypass_manage_permission ) + def user_get_default_permissions( self, user ): + permissions = {} + for dup in user.default_permissions: + action = self.get_action( dup.action ) + if action in permissions: + permissions[ action ].append( dup.role ) + else: + permissions[ action ] = [ dup.role ] + return permissions + def history_set_default_permissions( self, history, permissions={}, dataset=False, bypass_manage_permission=False ): + # bypass_manage_permission is used to change permissions of datasets in a user-less history when logging in + user = history.user + if not user: + # default permissions on a user-less history are None + return None + if not permissions: + permissions = self.user_get_default_permissions( user ) + # Delete all of the current default permission for the history + for dhp in history.default_permissions: + dhp.delete() + dhp.flush() + # Add the new default permissions for the history + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for dhp in [ self.model.DefaultHistoryPermissions( history, action, role ) for role in roles ]: + dhp.flush() + if dataset: + # Only deal with datasets that are not purged + for hda in history.activatable_datasets: + dataset = hda.dataset + if dataset.library_associations: + # Don't change permissions on a dataset associated with a library + continue + if [ assoc for assoc in dataset.history_associations if assoc.history not in user.histories ]: + # Don't change permissions on a dataset associated with a history not owned by the user + continue + if bypass_manage_permission or self.allow_action( user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=dataset ): + self.set_all_dataset_permissions( dataset, permissions ) + def history_get_default_permissions( self, history ): + permissions = {} + for dhp in history.default_permissions: + action = self.get_action( dhp.action ) + if action in permissions: + permissions[ action ].append( dhp.role ) + else: + permissions[ action ] = [ dhp.role ] + return permissions + def set_all_dataset_permissions( self, dataset, permissions={} ): + # Set new permissions on a dataset, eliminating all current permissions + # Delete all of the current permissions on the dataset + for adra in dataset.actions: + adra.delete() + adra.flush() + # Add the new permissions on the dataset + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for adra in [ self.model.ActionDatasetRoleAssociation( action, dataset, role ) for role in roles ]: + adra.flush() + def set_dataset_permission( self, dataset, permission={} ): + # Set a specific permission on a dataset, leaving all other current permissions on the dataset alone + for action, roles in permission.items(): + if isinstance( action, Action ): + action = action.action + # Delete the current specific permission on the dataset if one exists + for adra in dataset.actions: + if adra.action == action: + adra.delete() + adra.flush() + # Add the new specific permission on the dataset + for adra in [ self.model.ActionDatasetRoleAssociation( action, dataset, role ) for role in roles ]: + adra.flush() + def make_dataset_public( self, dataset ): + # A dataset is considered public if there are no "access" actions associated with it. Any + # other actions ( 'manage permissions', 'edit metadata' ) are irrelevant. + for adra in dataset.actions: + if adra.action == self.permitted_actions.DATASET_ACCESS.action: + adra.delete() + adra.flush() + def get_dataset_permissions( self, dataset ): + if not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + permissions = {} + for adra in dataset.actions: + action = self.get_action( adra.action ) + if action in permissions: + permissions[ action ].append( adra.role ) + else: + permissions[ action ] = [ adra.role ] + return permissions + 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_all_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_permission( dataset, { self.permitted_actions.DATASET_ACCESS : [ sharing_role ] } ) + def set_entity_user_associations( self, users=[], roles=[], groups=[], delete_existing_assocs=True ): + for user in users: + if delete_existing_assocs: + for a in user.non_private_roles + user.groups: + a.delete() + a.flush() + for role in roles: + self.associate_components( user=user, role=role ) + for group in groups: + self.associate_components( user=user, group=group ) + def set_entity_group_associations( self, groups=[], users=[], roles=[], delete_existing_assocs=True ): + for group in groups: + if delete_existing_assocs: + for a in group.roles + group.users: + a.delete() + a.flush() + for role in roles: + self.associate_components( group=group, role=role ) + for user in users: + self.associate_components( group=group, user=user ) + 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 ) + +class LibraryRBACAgent( RBACAgent ): + """Class that handles galaxy library security""" + #Actions that can be performed on Library Items + permitted_actions = Bunch( + LIBRARY_ADD = Action( + "add library item", "Role members can add library items", "grant" ), + LIBRARY_MODIFY = Action( + "modify library item", "Role members can modify and delete this library item", "grant" ), + LIBRARY_MANAGE = Action( + "manage library permissions", "Role members can manage roles associated with this library item", "grant" ) + ) + def __init__( self, model, permitted_actions=None ): + self.model = model + self.library_types = ( ( 'library', self.model.Library ) , ( 'library_folder', self.model.LibraryFolder ) , ( 'library_dataset', self.model.LibraryDataset ) ) + if permitted_actions: + self.permitted_actions = permitted_actions + 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 of Action objects + """ + return self.permitted_actions.__dict__.values() + def allow_action( self, user, action, library_item, **kwd ): + #action = self.get_action( action ) + #assert action is not None, 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) + if user is None: return False #all permissions are granted -> non-users cannot have permissions + if action.model == 'grant': + library_items = {} + for item_name, item_class in self.library_types: + if isinstance( library_item, item_class ): + library_items[ "%s_id" % item_name ] = library_item.id + #else: + # library_items[ "%s_id" % item_name ] = None + user_role_ids = [ r.id for r in user.all_roles() ] + #check to see if user has access to any of the roles + allowed_role_assocs = self.model.ActionLibraryItemRoleAssociation.filter_by( action = action.action, **library_items ) + if allowed_role_assocs: + if not hasattr( allowed_role_assocs, '__iter__' ): + allowed_role_assocs = [allowed_role_assocs] + for allowed_role_assoc in allowed_role_assocs: + if allowed_role_assoc.role_id in user_role_ids: + return True + return False + else: + raise 'Unimplemented model (%s) specified for action (%s)' % ( action.model, action.action ) + + def set_all_permissions( self, library_item, permissions={} ): + # Set new permissions on a library item, eliminating all current permissions + # Delete all of the current permissions on the item + for role_assoc in library_item.actions: + role_assoc.delete() + role_assoc.flush() + # Add the new permissions on the dataset + for action, roles in permissions.items(): + if isinstance( action, Action ): + action = action.action + for role_assoc in [ self.model.ActionLibraryItemRoleAssociation( action, library_item, role ) for role in roles ]: + role_assoc.flush() + + def copy_permissions( self, source_library_item, target_library_item, user=None ): + #copy all permissions from source, if user is provided ensure that user's private role is included + permissions = {} + for role_assoc in source_library_item.actions: + if role_assoc.action in permissions: + permissions[role_assoc.action].append( role_assoc.role ) + else: + permissions[role_assoc.action] = [ role_assoc.role ] + self.set_all_permissions( target_library_item, permissions ) + + #make sure user's private group is included + if user: + private_role = self.model.security_agent.get_private_user_role( user ) + for name, action in self.permitted_actions.items(): + library_items = {} #move to a _guess_library_items method + for item_name, item_class in self.library_types: + if isinstance( target_library_item, item_class ): + library_items[ "%s_id" % item_name ] = target_library_item.id + if not self.model.ActionLibraryItemRoleAssociation.filter_by( role_id=private_role.id, action = action.action, **library_items ).first(): + alira = self.model.ActionLibraryItemRoleAssociation( action.action, target_library_item, private_role ) + alira.flush() + + def show_library_item( self, user, library_item ): + if self.allow_action( user, self.permitted_actions.LIBRARY_MODIFY, library_item ) or \ + self.allow_action( user, self.permitted_actions.LIBRARY_MANAGE, library_item ) or \ + self.allow_action( user, self.permitted_actions.LIBRARY_ADD, library_item ): + return True + if isinstance( library_item, self.model.Library ): + return self.show_library_item( user, library_item.root_folder ) + elif isinstance( library_item, self.model.LibraryFolder ): + for folder in library_item.folders: + if self.show_library_item( user, folder ): + return True + return False + + + 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={}, history=False, dataset=False ): + raise "Unimplemented Method" + def history_set_default_permissions( self, history, permissions=None, dataset=False, bypass_manage_permission=False ): + raise "Unimplemented Method" + def set_all_dataset_permissions( self, dataset, permissions ): + raise "Unimplemented Method" + def set_dataset_permission( self, dataset, permission ): + raise "Unimplemented Method" + def make_dataset_public( self, dataset ): + 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 ] ) + + + +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 4b3d65fabac..0885c094f62 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1190,7 +1190,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 @@ -1209,9 +1209,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 @@ -1324,6 +1321,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() @@ -1360,6 +1358,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 b9230b2fbac..6b8e42bd24b 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -47,6 +47,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 ): @@ -116,6 +119,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] @@ -169,6 +180,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_all_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 @@ -233,6 +245,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 ) @@ -244,7 +259,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 c093fa25794..f0a05835bf7 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -72,10 +72,22 @@ class UploadToolAction( object ): return self.upload_empty( trans, job, "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: temp_name = sniff.stream_to_file( urllib.urlopen( line ), prefix='url_paste' ) except Exception, e: @@ -83,7 +95,7 @@ class UploadToolAction( object ): self.remove_tempfile( temp_name ) return self.upload_empty( trans, job, "Error:", str( e ) ) try: - data_list.append( self.add_file( trans, temp_name, line, file_type, dbkey, info="uploaded url", space_to_tab=space_to_tab ) ) + data_list.append( self.add_file( trans, temp_name, NAME, file_type, dbkey, info="uploaded url", space_to_tab=space_to_tab ) ) except Exception, e: log.exception( 'exception in add_file using url_paste temp_name %s: %s' % ( str( temp_name ), str( e ) ) ) self.remove_tempfile( temp_name ) @@ -124,9 +136,10 @@ class UploadToolAction( object ): trans.log_event( 'job id %d ended ok, file size: %s' % ( job.id, file_size_str ), tool_id=tool.id ) return dict( output=hda ) - def upload_empty( self, trans, job, err_code, err_msg ): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.name = err_code + def upload_empty(self, trans, job, err_code, err_msg): + data = trans.app.model.HistoryDatasetAssociation( create_dataset=True ) + trans.app.security_agent.set_all_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 @@ -151,12 +164,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 ) @@ -226,6 +239,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_all_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 7f340f42fec..1246c509aeb 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -3,17 +3,14 @@ Basic tool parameters. """ import logging, string, sys, os, os.path - from elementtree.ElementTree import XML, Element - from galaxy import config, datatypes, util from galaxy.web import form_builder from galaxy.util.bunch import Bunch - import validation, dynamic_options - # For BaseURLToolParameter from galaxy.web import url_for +import galaxy.model log = logging.getLogger(__name__) @@ -998,30 +995,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 ): @@ -1068,28 +1051,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 and 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/util/__init__.py b/lib/galaxy/util/__init__.py index cd0d1483270..17e0cae11bf 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -85,6 +85,7 @@ mapped_chars = { '>' :'__gt__', ']' :'__cb__', '{' :'__oc__', '}' :'__cc__', + '@' : '__at__' } def restore_text(text): @@ -123,7 +124,7 @@ class Params: Operates on string or list values only (HTTP parameters). - >>> values = { 'status':'on', 'symbols':[ 'alpha', '<>', '$rm&#@!' ] } + >>> values = { 'status':'on', 'symbols':[ 'alpha', '<>', '$rm&#!' ] } >>> par = Params(values) >>> par.status 'on' @@ -132,9 +133,9 @@ class Params: >>> par.get('price', 0) 0 >>> par.symbols # replaces unknown symbols with X - ['alpha', '__lt____gt__', 'XrmXXX!'] + ['alpha', '__lt____gt__', 'XrmXX!'] >>> par.flatten() # flattening to a list - [('status', 'on'), ('symbols', 'alpha'), ('symbols', '__lt____gt__'), ('symbols', 'XrmXXX!')] + [('status', 'on'), ('symbols', 'alpha'), ('symbols', '__lt____gt__'), ('symbols', 'XrmXX!')] """ # HACK: Need top prevent sanitizing certain parameter types. The @@ -264,6 +265,20 @@ def string_as_bool( string ): else: return False +def listify( item ): + """ + Make a single item a single item list, or return a list if passed a + list. Passing a None returns an empty list. + """ + if item is None: + return [] + elif isinstance( item, list ): + return item + elif isinstance( item, str ) and item.count( ',' ): + return item.split( ',' ) + else: + return [ item ] + def commaify(amount): orig = amount new = re.sub("^(-?\d+)(\d{3})", '\g<1>,\g<2>', amount) diff --git a/lib/galaxy/web/__init__.py b/lib/galaxy/web/__init__.py index 508e2b35037..15bb7888368 100644 --- a/lib/galaxy/web/__init__.py +++ b/lib/galaxy/web/__init__.py @@ -2,5 +2,5 @@ The Galaxy web application. """ -from framework import expose, json, require_login, url_for, error, form, FormBuilder +from framework import expose, json, require_login, require_admin, url_for, error, form, FormBuilder diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 534124e2131..f2e95dfbcbd 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -28,4 +28,7 @@ class BaseController( object ): Root = BaseController """ Deprecated: `BaseController` used to be available under the name `Root` -""" \ No newline at end of file +""" + +class ControllerUnavailable( Exception ): + pass \ No newline at end of file diff --git a/lib/galaxy/web/buildapp.py b/lib/galaxy/web/buildapp.py index a9ab866cdc2..65a5d315ea8 100644 --- a/lib/galaxy/web/buildapp.py +++ b/lib/galaxy/web/buildapp.py @@ -28,13 +28,18 @@ def add_controllers( webapp, app ): them to the webapp. """ from galaxy.web.base.controller import BaseController + from galaxy.web.base.controller import ControllerUnavailable import galaxy.web.controllers controller_dir = galaxy.web.controllers.__path__[0] for fname in os.listdir( controller_dir ): if not( fname.startswith( "_" ) ) and fname.endswith( ".py" ): name = fname[:-3] module_name = "galaxy.web.controllers." + name - module = __import__( module_name ) + try: + module = __import__( module_name ) + except ControllerUnavailable, exc: + log.debug("%s could not be loaded: %s" % (module_name, str(exc))) + continue for comp in module_name.split( "." )[1:]: module = getattr( module, comp ) # Look for a controller inside the modules @@ -57,6 +62,8 @@ def app_factory( global_conf, **kwargs ): # Create the universe WSGI application webapp = galaxy.web.framework.WebApplication( app, session_cookie='galaxysession' ) add_controllers( webapp, app ) + # Force /history to go to /root/history -- needed since the tests assume this + webapp.add_route( '/history', controller='root', action='history' ) # These two routes handle our simple needs at the moment webapp.add_route( '/async/:tool_id/:data_id/:data_secret', controller='async', action='index', tool_id=None, data_id=None, data_secret=None ) webapp.add_route( '/:controller/:action', action='index' ) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 81315558db1..755f2024181 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,28 +1,1625 @@ - +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 +from galaxy.model.orm import * +import logging log = logging.getLogger( __name__ ) class Admin( BaseController ): @web.expose + @web.require_admin 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 ): 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 - else: - msg = 'Invalid password' - return msg + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + return trans.fill_template( '/admin/index.mako', msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin + def center( self, trans, **kwd ): + return trans.fill_template( '/admin/center.mako' ) + @web.expose + @web.require_admin + def reload_tool( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg, messagetype=messagetype ) + @web.expose + @web.require_admin + def tool_reload( self, trans, tool_version=None, **kwd ): + 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, messagetype='done' ) + + # Galaxy Role Stuff + @web.expose + @web.require_admin + def roles( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + roles = trans.app.model.Role.filter( and_( trans.app.model.Role.table.c.deleted==False, + trans.app.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/roles.mako', + roles=roles, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def create_role( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + if params.get( 'create_role_button', False ): + name = util.restore_text( params.name ) + description = util.restore_text( params.description ) + in_users = util.listify( params.get( 'in_users', [] ) ) + in_groups = util.listify( params.get( 'in_groups', [] ) ) + if not name or not description: + msg = "Enter a valid name and a description" + elif trans.app.model.Role.filter( trans.app.model.Role.table.c.name==name ).first(): + msg = "A role with that name already exists" + else: + # Create the role + role = trans.app.model.Role( name=name, description=description, type=trans.app.model.Role.types.ADMIN ) + role.flush() + # Create the UserRoleAssociations + for user in [ trans.app.model.User.get( x ) for x in in_users ]: + ura = trans.app.model.UserRoleAssociation( user, role ) + ura.flush() + # Create the GroupRoleAssociations + for group in [ trans.app.model.Group.get( x ) for x in in_groups ]: + gra = trans.app.model.GroupRoleAssociation( group, role ) + gra.flush() + msg = "Role '%s' has been created with %d associated users and %d associated groups" % ( role.name, len( in_users ), len( in_groups ) ) + trans.response.send_redirect( web.url_for( controller='admin', action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + trans.response.send_redirect( web.url_for( controller='admin', action='create_role', msg=util.sanitize_text( msg ), messagetype='error' ) ) + out_users = [] + for user in trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all(): + out_users.append( ( user.id, user.email ) ) + out_groups = [] + for group in trans.app.model.Group.filter( trans.app.model.Group.table.c.deleted==False ).order_by( trans.app.model.Group.table.c.name ).all(): + out_groups.append( ( group.id, group.name ) ) + return trans.fill_template( '/admin/dataset_security/role_create.mako', + in_users=[], + out_users=out_users, + in_groups=[], + out_groups=out_groups, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def role( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + role = trans.app.model.Role.get( int( params.role_id ) ) + if params.get( 'role_members_edit_button', False ): + in_users = [ trans.app.model.User.get( x ) for x in util.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 util.listify( params.in_groups ) ] + trans.app.security_agent.set_entity_role_associations( roles=[ role ], users=in_users, groups=in_groups ) + role.refresh() + msg = "Role '%s' has been updated with %d associated users and %d associated groups" % ( role.name, len( in_users ), len( in_groups ) ) + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + elif params.get( 'rename', False ): + if params.rename == 'submitted': + old_name = role.name + new_name = util.restore_text( params.name ) + new_description = util.restore_text( params.description ) + if not new_name: + msg = 'Enter a valid name' + return trans.fill_template( '/admin/dataset_security/role_rename.mako', role=role, msg=msg, messagetype='error' ) + elif trans.app.model.Role.filter( trans.app.model.Role.table.c.name==new_name ).first(): + msg = 'A role with that name already exists' + return trans.fill_template( '/admin/dataset_security/role_rename.mako', role=role, msg=msg, messagetype='error' ) + else: + role.name = new_name + role.description = new_description + role.flush() + msg = "Role '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/dataset_security/role_rename.mako', role=role, msg=msg, messagetype=messagetype ) + in_users = [] + out_users = [] + in_groups = [] + out_groups = [] + for user in trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).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.filter( trans.app.model.Group.table.c.deleted==False ).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, + messagetype=messagetype ) + @web.expose + @web.require_admin + def mark_role_deleted( self, trans, **kwd ): + params = util.Params( kwd ) + role = trans.app.model.Role.get( int( params.role_id ) ) + role.deleted = True + role.flush() + msg = "Role '%s' has been marked as deleted." % role.name + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def deleted_roles( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + # 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 = trans.app.model.Role.query() \ + .filter( trans.app.model.Role.table.c.deleted==True ) \ + .order_by( trans.app.model.Role.table.c.name ) \ + .all() + for role in roles: + groups = [] + for gra in role.groups: + groups.append( trans.app.model.Group.get( gra.group_id ) ) + users = [] + for ura in role.users: + users.append( trans.app.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, + messagetype=messagetype ) + @web.expose + @web.require_admin + def undelete_role( self, trans, **kwd ): + params = util.Params( kwd ) + role = trans.app.model.Role.get( int( params.role_id ) ) + role.deleted = False + role.flush() + msg = "Role '%s' has been marked as not deleted." % role.name + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def purge_role( self, trans, **kwd ): + # This method should only be called for a Role that has previously been deleted. + # Purging a deleted Role deletes all of the following from the database: + # - UserRoleAssociations where role_id == Role.id + # - DefaultUserPermissions where role_id == Role.id + # - DefaultHistoryPermissions where role_id == Role.id + # - GroupRoleAssociations where role_id == Role.id + # - ActionDatasetRoleAssociations where role_id == Role.id + params = util.Params( kwd ) + role = trans.app.model.Role.get( int( params.role_id ) ) + if not role.deleted: + # We should never reach here, but just in case there is a bug somewhere... + msg = "Role '%s' has not been deleted, so it cannot be purged." % role.name + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='error' ) ) + # 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 ActionDatasetRoleAssociations + for adra in role.actions: + adra.delete() + adra.flush() + msg = "The following have been purged from the database for role '%s': " % role.name + msg += "DefaultUserPermissions, DefaultHistoryPermissions, UserRoleAssociations, GroupRoleAssociations, ActionDatasetRoleAssociations." + trans.response.send_redirect( web.url_for( action='deleted_roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + + # Galaxy Group Stuff + @web.expose + @web.require_admin + def groups( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + # 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 = trans.app.model.Group.query() \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ) \ + .all() + for group in groups: + members = [] + for uga in group.members: + members.append( trans.app.model.User.get( uga.user_id ) ) + roles = [] + for gra in group.roles: + roles.append( trans.app.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, + messagetype=messagetype ) + @web.expose + @web.require_admin + def group( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + group = trans.app.model.Group.get( int( params.group_id ) ) + if params.get( 'group_roles_users_edit_button', False ): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( params.in_roles ) ] + in_users = [ trans.app.model.User.get( x ) for x in util.listify( params.in_users ) ] + trans.app.security_agent.set_entity_group_associations( groups=[ group ], roles=in_roles, users=in_users ) + group.refresh() + msg += "Group '%s' has been updated with %d associated roles and %d associated users" % ( group.name, len( in_roles ), len( in_users ) ) + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + if params.get( 'rename', False ): + if params.rename == 'submitted': + old_name = group.name + new_name = util.restore_text( params.name ) + if not new_name: + msg = 'Enter a valid name' + return trans.fill_template( '/admin/dataset_security/group_rename.mako', group=group, msg=msg, messagetype='error' ) + elif trans.app.model.Group.filter( trans.app.model.Group.table.c.name==new_name ).first(): + msg = 'A group with that name already exists' + return trans.fill_template( '/admin/dataset_security/group_rename.mako', group=group, msg=msg, messagetype='error' ) + else: + group.name = new_name + group.flush() + msg = "Group '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/dataset_security/group_rename.mako', group=group, msg=msg, messagetype=messagetype ) + in_roles = [] + out_roles = [] + in_users = [] + out_users = [] + for role in trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all(): + if role in [ x.role for x in group.roles ]: + in_roles.append( ( role.id, role.name ) ) + else: + out_roles.append( ( role.id, role.name ) ) + for user in trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all(): + if user in [ x.user for x in group.users ]: + in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + msg += 'Group %s is currently associated with %d roles and %d users' % ( group.name, len( in_roles ), len( in_users ) ) + return trans.fill_template( '/admin/dataset_security/group.mako', + group=group, + in_roles=in_roles, + out_roles=out_roles, + in_users=in_users, + out_users=out_users, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def create_group( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + if params.get( 'create_group_button', False ): + name = util.restore_text( params.name ) + in_users = util.listify( params.get( 'in_users', [] ) ) + in_roles = util.listify( params.get( 'in_roles', [] ) ) + if not name: + msg = "Enter a valid name" + elif trans.app.model.Group.filter( trans.app.model.Group.table.c.name==name ).first(): + msg = "A group with that name already exists" + else: + # Create the group + group = trans.app.model.Group( name=name ) + group.flush() + # Create the UserRoleAssociations + for user in [ trans.app.model.User.get( x ) for x in in_users ]: + uga = trans.app.model.UserGroupAssociation( user, group ) + uga.flush() + # Create the GroupRoleAssociations + for role in [ trans.app.model.Role.get( x ) for x in in_roles ]: + gra = trans.app.model.GroupRoleAssociation( group, role ) + gra.flush() + msg = "Group '%s' has been created with %d associated users and %d associated roles" % ( name, len( in_users ), len( in_roles ) ) + trans.response.send_redirect( web.url_for( controller='admin', action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + trans.response.send_redirect( web.url_for( controller='admin', action='create_group', msg=util.sanitize_text( msg ), messagetype='error' ) ) + out_users = [] + for user in trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all(): + out_users.append( ( user.id, user.email ) ) + out_roles = [] + for role in trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all(): + out_roles.append( ( role.id, role.name ) ) + return trans.fill_template( '/admin/dataset_security/group_create.mako', + in_users=[], + out_users=out_users, + in_roles=[], + out_roles=out_roles, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def mark_group_deleted( self, trans, **kwd ): + params = util.Params( kwd ) + group = trans.app.model.Group.get( int( params.group_id ) ) + group.deleted = True + group.flush() + msg = "Group '%s' has been marked as deleted." % group.name + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def deleted_groups( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + # 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 = trans.app.model.Group.query() \ + .filter( trans.app.model.Group.table.c.deleted==True ) \ + .order_by( trans.app.model.Group.table.c.name ) \ + .all() + for group in groups: + members = [] + for uga in group.members: + members.append( trans.app.model.User.get( uga.user_id ) ) + roles = [] + for gra in group.roles: + roles.append( trans.app.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, + messagetype=messagetype ) + @web.expose + @web.require_admin + def undelete_group( self, trans, **kwd ): + params = util.Params( kwd ) + group = trans.app.model.Group.get( int( params.group_id ) ) + group.deleted = False + group.flush() + msg = "Group '%s' has been marked as not deleted." % group.name + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def purge_group( self, trans, **kwd ): + # This method should only be called for a Group that has previously been deleted. + # Purging a deleted Group simply deletes all UserGroupAssociations and GroupRoleAssociations. + params = util.Params( kwd ) + group = trans.app.model.Group.get( int( params.group_id ) ) + if not group.deleted: + # We should never reach here, but just in case there is a bug somewhere... + msg = "Group '%s' has not been deleted, so it cannot be purged." % group.name + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='error' ) ) + # 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 + msg = "The following have been purged from the database for group '%s': UserGroupAssociations, GroupRoleAssociations." % group.name + trans.response.send_redirect( web.url_for( action='deleted_groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + + # Galaxy User Stuff + @web.expose + @web.require_admin + def create_new_user( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + email = '' + password = '' + confirm = '' + subscribe = False + messagetype = params.get( 'messagetype', 'done' ) + if 'user_create_button' in kwd: + if 'email' in kwd: + email = kwd[ 'email' ] + if 'password' in kwd: + password = kwd[ 'password' ] + if 'confirm' in kwd: + confirm = kwd[ 'confirm' ] + if 'subscribe' in kwd: + subscribe = kwd[ 'subscribe' ] + messagetype = 'error' + if len( email ) == 0 or "@" not in email or "." not in email: + msg = "Please enter a real email address" + elif len( email) > 255: + msg = "Email address exceeds maximum allowable length" + elif trans.app.model.User.filter( trans.app.model.User.table.c.email==email ).first(): + msg = "User with that email already exists" + elif len( password ) < 6: + msg = "Please use a password of at least 6 characters" + elif password != confirm: + msg = "Passwords do not match" + else: + user = trans.app.model.User( email=email ) + user.set_password_cleartext( password ) + user.flush() + trans.app.security_agent.create_private_user_role( user ) + trans.app.security_agent.user_set_default_permissions( user, history=False, dataset=False ) + trans.log_event( "Admin created a new account for user %s" % email ) + msg = 'Created new user account' + messagetype = 'done' + #subscribe user to email list + if subscribe: + mail = os.popen( "%s -t" % trans.app.config.sendmail_path, 'w' ) + mail.write( "To: %s\nFrom: %s\nSubject: Join Mailing List\n\nJoin Mailing list." % ( trans.app.config.mailing_join_addr, email ) ) + if mail.close(): + msg + ". However, subscribing to the mailing list has failed." + messagetype = 'error' + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + return trans.fill_template( '/admin/user/create.mako', + msg=msg, + messagetype=messagetype, + email=email, + password=password, + confirm=confirm, + subscribe=subscribe ) + @web.expose + @web.require_admin + def reset_user_password( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + user_id = int( params.user_id ) + user = trans.app.model.User.filter( trans.app.model.User.table.c.id==user_id ).first() + password = '' + confirm = '' + messagetype = params.get( 'messagetype', 'done' ) + if 'reset_user_password_button' in kwd: + if 'password' in kwd: + password = kwd[ 'password' ] + if 'confirm' in kwd: + confirm = kwd[ 'confirm' ] + messagetype = 'error' + if len( password ) < 6: + msg = "Please use a password of at least 6 characters" + elif password != confirm: + msg = "Passwords do not match" + else: + user.set_password_cleartext( password ) + user.flush() + trans.log_event( "Admin reset password for user %s" % user.email ) + msg = 'Password reset' + messagetype = 'done' + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + return trans.fill_template( '/admin/user/reset_password.mako', + msg=msg, + messagetype=messagetype, + user=user, + password=password, + confirm=confirm ) + @web.expose + @web.require_admin + def mark_user_deleted( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + user = trans.app.model.User.get( int( params.user_id ) ) + user.deleted = True + user.flush() + msg = "User '%s' has been marked as deleted." % user.email + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def undelete_user( self, trans, **kwd ): + params = util.Params( kwd ) + user = trans.app.model.User.get( int( params.user_id ) ) + user.deleted = False + user.flush() + msg = "User '%s' has been marked as not deleted." % user.email + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def purge_user( self, trans, **kwd ): + # This method should only be called for a User that has previously been deleted. + # We keep the User in the database ( marked as purged ), and stuff associated + # with the user's private role in case we want the ability to unpurge the user + # some time in the future. + # Purging a deleted User deletes all of the following: + # - DefaultUserPermissions where user_id == User.id EXCEPT FOR THE PRIVATE ROLE + # - History where user_id = User.id + # - DefaultHistoryPermissions where history_id == History.id EXCEPT FOR THE PRIVATE ROLE + # - HistoryDatasetAssociation where history_id = History.id + # - Dataset where HistoryDatasetAssociation.dataset_id = Dataset.id + # - UserGroupAssociation where user_id == User.id + # - UserRoleAssociation where user_id == User.id EXCEPT FOR THE PRIVATE ROLE + # Purging Histories and Datasets must be handled via the cleanup_datasets.py script + params = util.Params( kwd ) + user = trans.app.model.User.get( int( params.user_id ) ) + if not user.deleted: + # We should never reach here, but just in case there is a bug somewhere... + msg = "User '%s' has not been deleted, so it cannot be purged." % user.email + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype='error' ) ) + private_role = trans.app.security_agent.get_private_user_role( user ) + # Delete DefaultUserPermissions EXCEPT FOR THE PRIVATE ROLE + for dup in user.default_permissions: + if dup.role_id != private_role.id: + dup.delete() + dup.flush() + # Delete History + for h in user.active_histories: + h.refresh() + # Delete DefaultHistoryPermissions EXCEPT FOR THE PRIVATE ROLE + for dp in h.default_permissions: + if dp.role_id != private_role.id: + dp.delete() + dp.flush() + for hda in h.active_datasets: + # Delete HistoryDatasetAssociation + d = trans.app.model.Dataset.get( hda.dataset_id ) + # Delete Dataset + if not d.deleted: + d.deleted = True + d.flush() + hda.deleted = True + hda.flush() + h.deleted = True + h.flush() + # Delete UserGroupAssociations + for uga in user.groups: + uga.delete() + uga.flush() + # Delete UserRoleAssociations EXCEPT FOR THE PRIVATE ROLE + for ura in user.roles: + if ura.role_id != private_role.id: + ura.delete() + ura.flush() + # Purge the user + user.purged = True + user.flush() + msg = "User '%s' has been marked as purged." % user.email + trans.response.send_redirect( web.url_for( action='deleted_users', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def deleted_users( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + users = trans.app.model.User.filter( and_( trans.app.model.User.table.c.deleted==True, trans.app.model.User.table.c.purged==False ) ) \ + .order_by( trans.app.model.User.table.c.email ) \ + .all() + return trans.fill_template( '/admin/user/deleted_users.mako', users=users, msg=msg, messagetype=messagetype ) + @web.expose + @web.require_admin + def users( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + # 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.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all() + for user in users: + groups = [] + for uga in user.groups: + groups.append( trans.app.model.Group.get( uga.group_id ) ) + roles = [] + for ura in user.non_private_roles: + roles.append( trans.app.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, + allow_user_deletion=trans.app.config.allow_user_deletion, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def user( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + user = trans.app.model.User.get( int( params.user_id ) ) + if params.get( 'user_roles_groups_edit_button', False ): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( params.in_roles ) ] + in_groups = [ trans.app.model.Group.get( x ) for x in util.listify( params.in_groups ) ] + trans.app.security_agent.set_entity_user_associations( users=[ user ], roles=in_roles, groups=in_groups ) + user.refresh() + msg += "User '%s' has been updated with %d associated roles and %d associated groups (private roles are not displayed)" % \ + ( user.email, len( in_roles ), len( in_groups ) ) + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + in_roles = [] + out_roles = [] + in_groups = [] + out_groups = [] + for role in trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all(): + if role in [ x.role for x in user.roles ]: + in_roles.append( ( role.id, role.name ) ) + else: + out_roles.append( ( role.id, role.name ) ) + for group in trans.app.model.Group.filter( trans.app.model.Group.table.c.deleted==False ).order_by( trans.app.model.Group.table.c.name ).all(): + if group in [ x.group for x in user.groups ]: + in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + msg += "User '%s' is currently associated with %d roles and is a member of %d groups" % ( user.email, len( in_roles ), len( in_groups ) ) + return trans.fill_template( '/admin/dataset_security/user.mako', + user=user, + in_roles=in_roles, + out_roles=out_roles, + in_groups=in_groups, + out_groups=out_groups, + msg=msg, + messagetype=messagetype ) + # Utility methods to enable removal of associations - redirects are key + @web.expose + @web.require_admin + def remove_group_from_role( self, trans, **kwd ): + params = util.Params( kwd ) + group_id = int( params.group_id ) + group = trans.app.model.Group.get( group_id ) + role_id = int( params.role_id ) + role = trans.app.model.Role.get( role_id ) + gra = trans.app.model.GroupRoleAssociation.filter( and_( trans.app.model.GroupRoleAssociation.table.c.group_id==group_id, + trans.app.model.GroupRoleAssociation.table.c.role_id==role_id ) ).first() + gra.delete() + gra.flush() + msg = "Group '%s' removed from role '%s'" % ( group.name, role.name ) + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def remove_group_from_user( self, trans, **kwd ): + params = util.Params( kwd ) + group_id = int( params.group_id ) + group = trans.app.model.Group.get( group_id ) + user_id = int( params.user_id ) + user = trans.app.model.User.get( user_id ) + uga = trans.app.model.UserGroupAssociation.filter( and_( trans.app.model.UserGroupAssociation.table.c.group_id==group_id, + trans.app.model.UserGroupAssociation.table.c.user_id==user_id ) ).first() + uga.delete() + uga.flush() + msg = "Group '%s' removed from user '%s'" % ( group.name, user.email ) + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def remove_role_from_group( self, trans, **kwd ): + params = util.Params( kwd ) + role_id = int( params.role_id ) + role = trans.app.model.Role.get( role_id ) + group_id = int( params.group_id ) + group = trans.app.model.Group.get( group_id ) + gra = trans.app.model.GroupRoleAssociation.filter( and_( trans.app.model.GroupRoleAssociation.table.c.role_id==role_id, + trans.app.model.GroupRoleAssociation.table.c.group_id==group_id ) ).first() + gra.delete() + gra.flush() + msg = "Role '%s' removed from group '%s'" % ( role.name, group.name ) + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def remove_role_from_user( self, trans, **kwd ): + params = util.Params( kwd ) + user_id = int( params.user_id ) + user = trans.app.model.User.get( user_id ) + role_id = int( params.role_id ) + role = trans.app.model.Role.get( role_id ) + ura = trans.app.model.UserRoleAssociation.filter( and_( trans.app.model.UserRoleAssociation.table.c.user_id==user_id, + trans.app.model.UserRoleAssociation.table.c.role_id==role_id ) ).first() + ura.delete() + ura.flush() + msg = "Role '%s' removed from user '%s'" % ( role.name, user.email ) + trans.response.send_redirect( web.url_for( action='users', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def remove_user_from_group( self, trans, **kwd ): + params = util.Params( kwd ) + user_id = int( params.user_id ) + user = trans.app.model.User.get( user_id ) + group_id = int( params.group_id ) + group = trans.app.model.Group.get( group_id ) + uga = trans.app.model.UserGroupAssociation.filter( and_( trans.app.model.UserGroupAssociation.table.c.user_id==user_id, + trans.app.model.UserGroupAssociation.table.c.group_id==group_id ) ).first() + uga.delete() + uga.flush() + msg = "User '%s' removed from group '%s'" % ( user.email, group.name ) + trans.response.send_redirect( web.url_for( action='groups', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def remove_user_from_role( self, trans, **kwd ): + params = util.Params( kwd ) + user_id = int( params.user_id ) + user = trans.app.model.User.get( user_id ) + role_id = int( params.role_id ) + role = trans.app.model.Role.get( role_id ) + ura = trans.app.model.UserRoleAssociation.filter( and_( trans.app.model.UserRoleAssociation.table.c.user_id==user_id, + trans.app.model.UserRoleAssociation.table.c.role_id==role_id ) ).first() + ura.delete() + ura.flush() + msg = "User '%s' removed from role '%s'" % ( user.email, role.name ) + trans.response.send_redirect( web.url_for( action='roles', msg=util.sanitize_text( msg ), messagetype='done' ) ) + + # Galaxy Library Stuff + @web.expose + @web.require_admin + def library_browser( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + created_lfda_ids = params.get( 'created_lfda_ids', '' ) + return trans.fill_template( '/admin/library/browser.mako', + libraries=trans.app.model.Library.filter( trans.app.model.Library.table.c.deleted==False ) \ + .order_by( trans.app.model.Library.name ).all(), + created_lfda_ids=created_lfda_ids, + deleted=False, + msg=msg, + messagetype=messagetype ) + libraries = library_browser + @web.expose + @web.require_admin + def library( self, trans, id=None, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + if params.get( 'new', False ): + action = 'new' + elif params.get( 'rename', False ): + action = 'rename' + elif params.get( 'delete', False ): + action = 'delete' + elif params.get( 'update_roles', False ): + action = 'update_roles' + else: + msg = 'Invalid action attempted on library' + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if not id and not action == 'new': + msg = "You must specify a library to %s." % action + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if not action == 'new': + library = trans.app.model.Library.get( int( id ) ) + if action == 'new': + 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() + msg = 'The new library named %s has been created' % library.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/library/new_library.mako', msg=msg, messagetype=messagetype ) + elif action == 'rename': + if params.rename == 'submitted': + old_name = library.name + new_name = util.restore_text( params.name ) + new_description = util.restore_text( params.description ) + if not new_name: + msg = 'Enter a valid name' + return trans.fill_template( '/admin/library/rename_library.mako', library=library, msg=msg, messagetype='error' ) + else: + if params.get( 'root_folder', False ): + root_folder = library.root_folder + root_folder.name = new_name + root_folder.flush() + library.name = new_name + library.description = new_description + library.flush() + msg = "Library '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/library/rename_library.mako', library=library, msg=msg, messagetype=messagetype ) + elif action == 'delete': + def delete_folder( library_folder ): + for folder in library_folder.active_folders: + delete_folder( folder ) + for lfda in library_folder.active_datasets: + # We don't set lfda.dataset.deleted to True here because the cleanup_dataset script + # will eventually remove it from disk. The purge_library method below sets the dataset + # to deleted. This allows for the library to be undeleted ( before it is purged ), + # restoring all of its contents. + lfda.deleted = True + lfda.flush() + library_folder.deleted = True + library_folder.flush() + delete_folder( library.root_folder ) + library.deleted = True + library.flush() + msg = "Library '%s' and all of its contents have been marked deleted" % library.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + elif action == 'update_roles': + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( library, permissions ) + library.refresh() + msg = "Permissions updated for library '%s'" % library.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + + @web.expose + @web.require_admin + def deleted_libraries( self, trans, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + libraries=trans.app.model.Library.filter( and_( trans.app.model.Library.table.c.deleted==True, + trans.app.model.Library.table.c.purged==False ) ) \ + .order_by( trans.app.model.Library.table.c.name ).all() + return trans.fill_template( '/admin/library/browser.mako', + libraries=libraries, + deleted=True, + msg=msg, + messagetype=messagetype ) + @web.expose + @web.require_admin + def undelete_library( self, trans, **kwd ): + params = util.Params( kwd ) + library = trans.app.model.Library.get( int( params.id ) ) + def undelete_folder( library_folder ): + for folder in library_folder.folders: + undelete_folder( folder ) + for lfda in library_folder.datasets: + lfda.deleted = False + lfda.flush() + library_folder.deleted = False + library_folder.flush() + undelete_folder( library.root_folder ) + library.deleted = False + library.flush() + msg = "Library '%s' and all of its contents have been marked not deleted" % library.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def purge_library( self, trans, **kwd ): + params = util.Params( kwd ) + library = trans.app.model.Library.get( int( params.id ) ) + def purge_folder( library_folder ): + for lf in library_folder.folders: + purge_folder( lf ) + for lfda in library_folder.datasets: + lfda.refresh() + dataset = lfda.dataset + dataset.refresh() + # If the dataset is not associated with any additional undeleted folders, then we can delete it. + # We don't set dataset.purged to True here because the cleanup_datasets script will do that for + # us, as well as removing the file from disk. + if not dataset.deleted and len( dataset.active_library_associations ) <= 1: # This is our current lfda + dataset.deleted = True + dataset.flush() + lfda.deleted = True + lfda.flush() + library_folder.purged = True + library_folder.flush() + purge_folder( library.root_folder ) + library.purged = True + library.flush() + msg = "Library '%s' and all of its contents have been purged, datasets will be removed from disk via the cleanup_datasets script" % library.name + return trans.response.send_redirect( web.url_for( action='deleted_libraries', msg=util.sanitize_text( msg ), messagetype='done' ) ) + @web.expose + @web.require_admin + def folder( self, trans, id, **kwd ): + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + if params.get( 'new', False ): + action = 'new' + elif params.get( 'rename', False ): + action = 'rename' + elif params.get( 'delete', False ): + action = 'delete' + elif params.get( 'update_roles', False ): + action = 'update_roles' + else: + msg = "Invalid action attempted on folder." + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + folder = trans.app.model.LibraryFolder.get( id ) + if not folder: + msg = "Invalid folder specified, id: %s" % str( id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if action == 'new': + 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() + msg = "New folder named '%s' has been added to this library" % new_folder.name + return trans.response.send_redirect( web.url_for( action='folder', id=new_folder.id, msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/library/new_folder.mako', folder=folder, msg=msg, messagetype=messagetype ) + elif action == 'rename': + if params.rename == 'submitted': + old_name = folder.name + new_name = util.restore_text( params.name ) + new_description = util.restore_text( params.description ) + if not new_name: + msg = 'Enter a valid name' + return trans.fill_template( '/admin/library/rename_folder.mako', folder=folder, msg=msg, messagetype='error' ) + else: + folder.name = new_name + folder.description = new_description + folder.flush() + msg = "Folder '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + return trans.fill_template( '/admin/library/rename_folder.mako', folder=folder, msg=msg, messagetype=messagetype ) + elif action == 'delete': + def delete_folder( folder ): + folder.refresh() + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for lfda in folder.active_datasets: + lfda.deleted = True + lfda.flush() + folder.deleted = True + folder.flush() + delete_folder( folder ) + msg = "Folder '%s' and all of its contents have been marked deleted" % folder.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + elif action =='update_roles': + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( folder, permissions ) + folder.refresh() + msg = "Permissions updated for folder '%s'" % folder.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + + + @web.expose + @web.require_admin + def library_dataset( self, trans, id=None, name=None, info=None, **kwd ): + dataset = trans.app.model.LibraryDataset.get( id ) + msg = "" + messagetype="done" + + if not id or not dataset: + msg = "Invalid library dataset specified, id: %s" %str( library_dataset_id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if 'save' in kwd: + dataset.name = name + dataset.info = info + target_lda = trans.app.model.LibraryFolderDatasetAssociation.get( kwd.get( 'set_lda_id' ) ) + dataset.library_folder_dataset_association = target_lda + trans.app.model.flush() + msg = 'Attributes updated for library dataset %s' % dataset.name + elif 'update_roles' in kwd: + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( dataset, permissions ) + dataset.refresh() + + + return trans.fill_template( "/admin/library/library_dataset.mako", + dataset=dataset, + err=None, + msg=msg, + messagetype=messagetype ) + + + + @web.expose + @web.require_admin + def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): + 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 = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + + # add_file method + def add_file( file_obj, name, extension, dbkey, last_used_build, roles, info='no info', space_to_tab=False, replace_dataset=None ): + 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 uncompressing 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 ) + elif os.stat( temp_name ).st_size < 262144000: # 250MB + line_count = sniff.convert_newlines( temp_name ) + else: + if sniff.check_newlines( temp_name ): + line_count = sniff.convert_newlines( temp_name ) + else: + line_count = None + + if extension == 'auto': + data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) + else: + data_type = extension + if replace_dataset: + library_dataset = replace_dataset + else: + library_dataset = trans.app.model.LibraryDataset( name=name, info=info, extension=data_type, dbkey=dbkey ) + library_dataset.flush() + + dataset = trans.app.model.LibraryFolderDatasetAssociation( name=name, + info=info, + extension=data_type, + dbkey=dbkey, + library_dataset = library_dataset, + create_dataset=True ) + dataset.flush() + #library_item.set_library_folder_dataset_association( dataset ) + if not replace_dataset: + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( library_dataset, genome_build=last_used_build ) + library_dataset.library_folder_dataset_association_id = dataset.id + #library_dataset.library_folder_dataset_association = dataset + library_dataset.flush() + if roles: + for role in roles: + adra = trans.app.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 + + replace_id = params.get( 'replace_id', None ) + try: + replace_dataset = trans.app.model.LibraryDataset.get( replace_id ) + except: + replace_dataset = None + + # Dataset upload + if params.get( 'new_dataset_button', False ): + # Copied from upload tool action + data_file = params.get( 'file_data', '' ) + url_paste = params.get( 'url_paste', '' ) + server_dir = params.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, replace_id=replace_id, msg=util.sanitize_text( msg ), messagetype='done' ) ) + space_to_tab = params.get( 'space_to_tab', False ) + if space_to_tab and space_to_tab not in [ "None", None ]: + space_to_tab = True + roles = [] + role_ids = params.get( 'roles', [] ) + for role_id in util.listify( role_ids ): + roles.append( trans.app.model.Role.get( role_id ) ) + temp_name = "" + data_list = [] + created_lfda_ids = '' + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + created_lfda = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, + roles, + info="uploaded file", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = str( created_lfda.id ) + 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: + created_lfda = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + last_used_build, + roles, + info="uploaded url", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + created_lfda = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + last_used_build, + roles, + info="pasted entry", + space_to_tab=space_to_tab ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + 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 + created_lfda = add_file( open( full_file, 'rb' ), + file, + extension, + dbkey, + last_used_build, + roles, + info="imported file", + space_to_tab=space_to_tab ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + if created_lfda_ids: + created_lfda_ids = created_lfda_ids.lstrip( ',' ) + total_added = len( created_lfda_ids.split( ',' ) ) + msg = "%i new datasets added to the library ( each is selected below ). " % total_added + msg += "Click the Go button at the bottom of this page to edit the permissions on these datasets if necessary." + trans.response.send_redirect( web.url_for( action='library_browser', + created_lfda_ids=created_lfda_ids, + msg=util.sanitize_text( msg ), + messagetype='done' ) ) + else: + msg = "Upload failed" + trans.response.send_redirect( web.url_for( action='library_browser', + created_lfda_ids=created_lfda_ids, + msg=util.sanitize_text( msg ), + messagetype='error' ) ) + + # No dataset(s) specified, display upload form + elif not id or replace_dataset: + # 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.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.c.description ).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, + messagetype=messagetype, + replace_dataset=replace_dataset ) + 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: + msg = "Invalid dataset specified, id: %s" %str( id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + # 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 util.listify( p.get( k + '_in', [] ) ) ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + trans.app.security_agent.set_all_dataset_permissions( lda.dataset, permissions ) + #need to set/display library security info + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( lda, 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() + msg = 'Attributes updated for dataset %s' % lda.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + 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() + msg = 'Attributes updated for dataset %s' % lda.name + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + elif p.delete: + # The user selected the "Remove this dataset from the library" pop-up menu option + lda.deleted = True + lda.flush() + msg = 'Dataset %s has been removed from this library' % lda.name + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + 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, + messagetype=messagetype ) + # multiple ids specfied, display permission form, permissions will be updated for all simultaneously. + elif ids: + lfdas = [] + for id in [ int( id ) for id in ids ]: + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if lfda is None: + msg = 'You specified an invalid dataset id: %s' %str( id ) + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + lfdas.append( lfda ) + if len( lfdas ) < 2: + msg = 'You must specify at least two datasets on which to modify permissions, ids you sent: %s' % str( ids ) + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + 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 util.listify( params.get( k + '_in', [] ) ) ] + permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles + for lfda in lfdas: + trans.app.security_agent.set_all_dataset_permissions( lfda.dataset, permissions ) + lfda.dataset.refresh() + msg = 'Permissions and roles have been updated on %d datasets' % len( lfdas ) + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + # Ensure that the permissions across all datasets are identical. Otherwise, we can't update together. + tmp = [] + for lfda in lfdas: + perms = trans.app.security_agent.get_dataset_permissions( lfda.dataset ) + if perms not in tmp: + tmp.append( perms ) + if len( tmp ) != 1: + msg = 'The datasets you selected do not have identical permissions, so they can not be updated together' + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + else: + return trans.fill_template( "/admin/library/dataset.mako", dataset=lfdas ) + @web.expose + @web.require_admin + def add_dataset_to_folder_from_history( self, trans, ids="", folder_id=None, **kwd ): + try: + folder = trans.app.model.LibraryFolder.get( folder_id ) + except: + msg = "Invalid folder id: %s" % str( folder_id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + # See if the current history is empty + history = trans.get_history() + history.refresh() + if not history.active_datasets: + msg = 'Your current history is empty' + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if params.get( 'add_dataset_from_history_button', False ): + if not isinstance( ids, list ): + if ids: + ids = ids.split( "," ) + else: + ids = [] + 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: + msg = "The requested dataset id %s is invalid" % str( data_id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if dataset_names: + msg = "Added the following datasets to the library folder: %s" % ( ", ".join( dataset_names ) ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + else: + msg = 'Select at least one dataset from the list' + messagetype = 'error' + return trans.fill_template( "/admin/library/add_dataset_from_history.mako", history=history, folder=folder, msg=msg, messagetype=messagetype ) + + @web.expose + @web.require_admin + def library_item_info_template( self, trans, id=None, new_element_count=0, library_id=None, folder_id=None, library_dataset_id=None, library_folder_dataset_association_id=None, **kwd ): + new_element_count = int( new_element_count ) + library_item_info_template = None + #library_item_info_template = library = folder = library_dataset = library_folder_dataset_association = None + try: + library_item_info_template = trans.app.model.LibraryItemInfoTemplate.get( id ) + except: + library_item_info_template = None + msg = "" + messagetype="done" + + if id and not library_item_info_template: + msg = "Invalid library info template specified, id: %s" %str( id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if 'library_item_info_template_create_button' in kwd: + #create template then display edit screen + library_item_info_template = trans.app.model.LibraryItemInfoTemplate() + library_item_info_template.name = kwd.get( 'name', 'unnamed' ) + library_item_info_template.description = kwd.get( 'description', '' ) + library_item_info_template.flush() + + #create template association + library_item_info_template_assoc = trans.app.model.LibraryItemInfoTemplateAssociation() + library_item_info_template_assoc.library_item_info_template = library_item_info_template + if folder_id: + library_item_info_template_assoc.folder = trans.app.model.LibraryFolder.get( folder_id ) + elif library_id: + library_item_info_template_assoc.library = trans.app.model.Library.get( library_id ) + elif library_dataset_id: + library_item_info_template_assoc.library_dataset = trans.app.model.LibraryDataset.get( library_dataset_id ) + elif library_folder_dataset_association_id: + library_item_info_template_assoc.library_folder_dataset_association = trans.app.model.LibraryFolderDatasetAssociation.get( library_folder_dataset_association_id ) + library_item_info_template_assoc.flush() + + #now create and add elements + for i in range( int( kwd.get( 'set_element_count', 0 ) ) ): + elem_name = kwd.get( 'new_element_name_%i' % i, None ) + elem_description = kwd.get( 'new_element_description_%i' % i, None ) + #skip any elements that have a missing name and description + if not elem_name: + elem_name = elem_description #if we have a description but no name, the description will be both; a name cannot be empty, but a description can + if elem_name: + library_item_info_template.add_element( name = elem_name, description = elem_description ) + + elif 'library_item_info_template_edit_button' in kwd: + #save changes to existing attributes + #only set name if nonempty/nonNone is passed, but always set description + name = kwd.get( 'name', None ) + if name: + library_item_info_template.name = name + library_item_info_template.description = kwd.get( 'description', '' ) + library_item_info_template.flush() + + #save changes to exisiting elements + for elem_id in kwd.get( 'element_ids', [] ): + library_item_info_template_element = trans.app.model.LibraryItemInfoTemplateElement.get( elem_id ) + name = kwd.get( 'element_name_%s' % elem_id, None ) + if name: + library_item_info_template_element.name = name + library_item_info_template_element.description = kwd.get( 'element_description_%s' % elem_id, None ) + library_item_info_template_element.flush() + + #add new elements + for i in range( int( kwd.get( 'set_element_count', 0 ) ) ): + elem_name = kwd.get( 'new_element_name_%i' % i, None ) + elem_description = kwd.get( 'new_element_description_%i' % i, None ) + #skip any elements that have a missing name and description + if not elem_name: + elem_name = elem_description #if we have a description but no name, the description will be both; a name cannot be empty, but a description can + if elem_name: + library_item_info_template.add_element( name = elem_name, description = elem_description ) + library_item_info_template.refresh() + + return trans.fill_template( "/admin/library/item_info_template.mako", library_item_info_template = library_item_info_template, new_element_count=new_element_count, library_id=library_id, library_dataset_id=library_dataset_id, library_folder_dataset_association_id=library_folder_dataset_association_id, folder_id=folder_id, msg=msg, messagetype=messagetype ) + + + @web.expose + @web.require_admin + def download_dataset_from_folder(self, trans, id, **kwd): + """Catches the dataset id and displays file contents as directed""" + # id refers to a LibraryFolderDatasetAssociation object + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + dataset = trans.app.model.Dataset.get( lfda.dataset_id ) + if not dataset: + msg = 'Invalid id %s received for file downlaod' % str( id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + mime = trans.app.datatypes_registry.get_mimetype_by_extension( lfda.extension.lower() ) + trans.response.set_content_type( mime ) + fStat = os.stat( lfda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = lfda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = "attachment; filename=GalaxyLibraryDataset-%s-[%s]" % ( str( id ), fname ) + try: + return open( lfda.file_name ) + except: + msg = 'This dataset contains no content' + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + 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 + @web.require_admin + def datasets( self, trans, **kwd ): + # This method is used by the select list labeled "Perform action on selected datasets" on the admin library browser. + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + messagetype = params.get( 'messagetype', 'done' ) + if params.get( 'action_on_datasets_button', False ): + if not params.dataset_ids: + msg = "At least one dataset must be selected for %s" % params.action + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + dataset_ids = util.listify( params.dataset_ids ) + if params.action == 'edit': + trans.response.send_redirect( web.url_for( action='dataset', + id=",".join( dataset_ids ), + msg=util.sanitize_text( msg ), + messagetype=messagetype ) ) + elif params.action == 'delete': + for id in dataset_ids: + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + lfda.deleted = True + lfda.flush() + msg = "The selected datasets have been removed from this library" + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='done' ) ) + else: + msg = "Action %s is not yet implemented" % str( params.action ) + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + else: + trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype=messagetype ) ) + @web.expose + @web.require_admin + def delete_dataset( self, trans, id=None, **kwd): + if id: + # id is a LibraryFolderDatasetAssociation.id + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + lfda.deleted = True + lfda.flush() + msg = "Dataset %s was deleted from library folder %s" % ( lfda.name, lfda.folder.name ) + trans.response.send_redirect( web.url_for( action='folder', + id=str( lfda.folder.id ), + msg=util.sanitize_text( msg ), + messagetype='done' ) ) + msg = "You did not specify a dataset to delete." + return trans.response.send_redirect( web.url_for( action='folder', + id=str( lfda.folder.id ), + msg=util.sanitize_text( msg ), + messagetype='error' ) ) + + @web.expose + @web.require_admin def memdump( self, trans, ids = 'None', sorts = 'None', pages = 'None', new_id = None, new_sort = None, **kwd ): if self.app.memdump is None: return trans.show_error_message( "Memdump is not enabled (set use_memdump = True in universe_wsgi.ini)" ) diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index e8b0542ee19..c3709d2f913 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_all_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 78c020d18dd..f526a5fec3a 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -104,31 +104,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 id: %s." % str( dataset_id ) ) + 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 allowed to access this dataset" ) def _undelete( self, trans, id ): history = trans.get_history() @@ -142,7 +141,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 @@ -156,7 +155,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/genetrack.py b/lib/galaxy/web/controllers/genetrack.py new file mode 100644 index 00000000000..019239d0da3 --- /dev/null +++ b/lib/galaxy/web/controllers/genetrack.py @@ -0,0 +1,283 @@ +import time, glob, os +from itertools import cycle +import hashlib + +from mako import exceptions +from mako.template import Template +from mako.lookup import TemplateLookup +from galaxy.web.base.controller import * + +try: + import pkg_resources + pkg_resources.require("GeneTrack") + import atlas + from atlas import sql + from atlas import hdf + from atlas import util as atlas_utils + from atlas.web import formlib, feature_query, feature_filter + from atlas.web import label_cache as atlas_label_cache + from atlas.plotting.const import * + from atlas.plotting.tracks import prefab + from atlas.plotting.tracks import chart + from atlas.plotting import tracks +except Exception, exc: + raise ControllerUnavailable("GeneTrack could not import a required dependency: %s" % str(exc)) + +pkg_resources.require( "Paste" ) +import paste.httpexceptions + +# Database helpers +SHOW_LABEL_LIMIT = 10000 + +def list_labels(session): + """ + Returns a list of labels that will be plotted in order. + """ + labels = sql.Label + query = session.query(labels).order_by("-id") + return query + +def open_databases( conf ): + """ + A helper function that returns handles to the hdf and sql databases + """ + db = hdf.hdf_open( conf.HDF_DATABASE, mode='r' ) + session = sql.get_session( conf.SQL_URI ) + return db, session + +def hdf_query(db, name, param, autosize=False ): + """ + Schema specific hdf query. + Note that returns data as columns not rows. + """ + if not hdf.has_node(db=db, name=name): + atlas.warn( 'missing label %s' % name ) + return [], [], [], [] + data = hdf.GroupData( db=db, name=name) + istart, iend = data.get_indices(label=param.chrom, start=param.start, stop=param.end) + table = data.get_table(label=param.chrom) + if autosize: + # attempts to reduce the number of points + size = len( table.cols.ix[istart:iend] ) + step = max( [1, size/1200] ) + else: + step = 1 + + ix = table.cols.ix[istart:iend:step].tolist() + wx = table.cols.wx[istart:iend:step].tolist() + cx = table.cols.cx[istart:iend:step].tolist() + ax = table.cols.ax[istart:iend:step].tolist() + return ix, wx, cx, ax + +# Chart helpers +def build_tracks( param, conf, data_label, fit_label, pred_label, strand, show=False ): + """ + Builds tracks + """ + # gets all the labels for a fast lookup + label_cache = atlas_label_cache( conf ) + + # get database handles for hdf and sql + db, session = open_databases( conf ) + + # fetching x and y coordinates for bar and fit (line) for + # each strand plus (p), minus (m), all (a) + bix, bpy, bmy, bay = hdf_query( db=db, name=data_label, param=param ) + fix, fpy, fmy, fay = hdf_query( db=db, name=fit_label, param=param ) + + # close the hdf database + db.close() + + # get all features within the range + all = feature_query( session=session, param=param ) + + # draws the barchart and the nucleosome chart below it + if strand == 'composite': + bar = prefab.composite_bartrack( fix=fix, fay=fay, bix=bix, bay=bay, param=param) + else: + bar = prefab.twostrand_bartrack( fix=fix, fmy=fmy, fpy=fpy, bix=bix, bmy=bmy, bpy=bpy, param=param) + + charts = list() + charts.append( bar ) + + return charts + +def feature_chart(param=None, session=None, label=None, label_dict={}, color=cycle( [LIGHT, WHITE] ) ): + # draw the ORF tracks + all = feature_filter(feature_query(session=session, param=param), name=label, kdict=label_dict) + opts = track_options( + xscale=param.xscale, w=param.width, fgColor=PURPLE, + show_labels=param.show_labels, ylabel=str(label), + bgColor=color.next() + ) + return [ + tracks.split_tracks(features=all, options=opts, split=param.show_labels, track_type='vector') + ] + +def consolidate_charts( charts, param ): + # create the multiplot + opt = chart_options( w=param.width ) + multi = chart.MultiChart(options=opt, charts=charts) + return multi + +# SETUP Track Builders +import functools +def twostrand_tracks( param=None, conf=None ): + return build_tracks( data_label=conf.LABEL, fit_label=conf.FIT_LABEL, pred_label=conf.PRED_LABEL, param=param, conf=conf, strand='twostrand') +def composite_tracks( param=None, conf=None ): + return build_tracks( data_label=conf.LABEL, fit_label=conf.FIT_LABEL, pred_label=conf.PRED_LABEL, param=param, conf=conf, strand='composite') + +class BaseConf( object ): + """ + Fake web_conf for atlas. + """ + IMAGE_DIR = "static/genetrack/plots/" + LEVELS = [str(x) for x in [ 50, 100, 250, 500, 1000, 2500, 5000, 10000, 20000, 50000, 100000, 200000 ]] + ZOOM_LEVELS = zip(LEVELS, LEVELS) + PLOT_SETUP = [ + ('comp-id', 'Composite' , 'genetrack/index.html', composite_tracks ), + ('two-id' , 'Two Strand', 'genetrack/index.html', twostrand_tracks ), + ] + PLOT_CHOICES = [ (id, name) for (id, name, page, func) in PLOT_SETUP ] + PLOT_MAPPER = dict( [ (id, (page, func)) for (id, name, page, func) in PLOT_SETUP ] ) + + def __init__(self, **kwds): + for key,value in kwds.items(): + setattr( self, key, value) + +class WebRoot(BaseController): + @web.expose + def search(self, trans, word='', dataset_id=None, submit=''): + """ + Default search page + """ + data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) + if not data: + raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset id: %s." % str( dataset_id ) ) + # the main configuration file + conf = BaseConf( + TITLE = "%s: %s" % (data.metadata.dbkey, data.metadata.label), + HDF_DATABASE = os.path.join( data.extra_files_path, data.metadata.hdf ), + SQL_URI = "sqlite:///%s" % os.path.join( data.extra_files_path, data.metadata.sqlite ), + LABEL = data.metadata.label, + FIT_LABEL = "%s-SIGMA-%d" % (data.metadata.label, 20), + PRED_LABEL = "PRED-%s-SIGMA-%d" % (data.metadata.label, 20), + ) + + param = atlas.Param( word=word ) + + # search for a given + session = sql.get_session( conf.SQL_URI ) + + if param.word: + def search_query( word, text ): + query = session.query(sql.Feature).filter( "name LIKE :word or freetext LIKE :text" ).params(word=word, text=text) + query = list(query[:20]) + return query + + # a little heuristics to match most likely target + targets = [ + (param.word+'%', 'No match'), # match beginning + ('%'+param.word+'%', 'No match'), # match name anywhere + ('%'+param.word+'%', '%'+param.word+'%'), # match json anywhere + ] + for word, text in targets: + query = search_query( word=word, text=text) + if query: + break + else: + query = [] + + return trans.fill_template_mako('genetrack/search.html', param=param, query=query, dataset_id=dataset_id) + + @web.expose + def index(self, trans, dataset_id=None, **kwds): + """ + Main request handler + """ + color = cycle( [LIGHT, WHITE] ) + data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) + if not data: + raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset id: %s." % str( dataset_id ) ) + # the main configuration file + conf = BaseConf( + TITLE = "%s: %s" % (data.metadata.dbkey, data.metadata.label), + HDF_DATABASE = os.path.join( data.extra_files_path, data.metadata.hdf ), + SQL_URI = "sqlite:///%s" % os.path.join( data.extra_files_path, data.metadata.sqlite ), + LABEL = data.metadata.label, + FIT_LABEL = "%s-SIGMA-%d" % (data.metadata.label, 20), + PRED_LABEL = "PRED-%s-SIGMA-%d" % (data.metadata.label, 20), + ) + session = sql.get_session( conf.SQL_URI ) + + if os.path.exists( conf.HDF_DATABASE ): + db = hdf.hdf_open( conf.HDF_DATABASE, mode='r' ) + conf.CHROM_FIELDS = [(x,x) for x in hdf.GroupData(db=db, name=conf.LABEL).labels] + db.close() + else: + query = session.execute(sql.select([sql.feature_table.c.chrom]).distinct()) + conf.CHROM_FIELDS = [(x.chrom,x.chrom) for x in query] + + # generate a new form based on the configuration + form = formlib.main_form( conf ) + + # clear the tempdir every once in a while + atlas_utils.clear_tempdir( dir=conf.IMAGE_DIR, days=1, chance=10) + + incoming = form.defaults() + incoming.update( kwds ) + + # manage the zoom and pan requests + incoming = formlib.zoom_change( kdict=incoming, levels=conf.LEVELS) + incoming = formlib.pan_view( kdict=incoming ) + + # process the form + param = atlas.Param( **incoming ) + form.process( incoming ) + + if kwds and form.isSuccessful(): + # adds the sucessfull parameters + param.update( form.values() ) + + # if it was a search word not a number go to search page + try: + center = int( param.feature ) + except ValueError: + # go and search for these + return trans.response.send_redirect( web.url_for( controller='genetrack', action='search', word=param.feature, dataset_id=dataset_id ) ) + + param.width = min( [2000, int(param.img_size)] ) + param.xscale = [ param.start, param.end ] + param.show_labels = ( param.end - param.start ) <= SHOW_LABEL_LIMIT + + # get the template and the function used to generate the tracks + tmpl_name, track_maker = conf.PLOT_MAPPER[param.plot] + + # check against a hash, display an image that already exists if it was previously created. + hash = hashlib.sha1() + hash.update(str(dataset_id)) + for key in sorted(kwds.keys()): + hash.update(str(kwds[key])) + fname = "%s.png" % hash.hexdigest() + fpath = os.path.join(conf.IMAGE_DIR, fname) + + charts = [] + param.fname = fname + + # The SHA1 hash should uniquely identify the qs that created the plot... + if os.path.exists(fpath): + os.utime(fpath, (time.time(), time.time())) + return trans.fill_template_mako(tmpl_name, conf=conf, form=form, param=param, dataset_id=dataset_id) + + # If the hashed filename doesn't exist, create it. + if track_maker is not None and os.path.exists( conf.HDF_DATABASE ): + # generate the fit track + charts = track_maker( param=param, conf=conf ) + + for label in list_labels( session ): + charts.extend( feature_chart(param=param, session=session, label=label.name, label_dict={label.name:label.id}, color=color)) + + track_chart = consolidate_charts( charts, param ) + track_chart.save(fname=fpath) + + return trans.fill_template_mako(tmpl_name, conf=conf, form=form, param=param, dataset_id=dataset_id) diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py new file mode 100644 index 00000000000..5224fab844f --- /dev/null +++ b/lib/galaxy/web/controllers/history.py @@ -0,0 +1,278 @@ +from galaxy.web.base.controller import * +from cgi import escape + +log = logging.getLogger( __name__ ) + +# States for passing messages +SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" + +class HistoryController( BaseController ): + + @web.expose + def index( self, trans ): + return "" + + @web.expose + def list_as_xml( self, trans ): + """ + XML history list for functional tests + """ + return trans.fill_template( "/history/list_as_xml.mako" ) + + @web.expose + @web.require_login( "work with multiple histories" ) + def list( self, trans, id=[], operation=None, show_deleted = False, **kwd ): + """ + List all available histories + """ + # TODO: these two operations need to be updates still + if operation: + operation = operation.lower() + if operation == "share": + return self.share( trans, id, **kwd ) + elif operation == "rename": + return self.rename( trans, id, **kwd ) + # Coerce ids to list + if not isinstance( id, list ): + id = id.split( "," ) + # Ensure ids are integers + try: + history_ids = map( int, id ) + except: + return trans.show_error_message( "Invalid history id" ) + # Note that this page was loaded + trans.log_event( "History id %s available" % str( id ) ) + # Display no message by default + status, message = None, None + refresh_history = False + # If an operation was provided, load the histories and ensure they all + # belong to the current user + if history_ids and operation: + histories = [] + for hid in history_ids: + history = self.app.model.History.get( hid ) + if history: + # Ensure history is owned by current user + if history.user_id != None and trans.user: + assert trans.user.id == history.user_id, "History does not belong to current user" + histories.append( history ) + else: + log.warn( "Invalid history id '%r' passed to list", hid ) + operation = operation.lower() + if operation == "switch": + status, message = self._list_switch( trans, histories ) + refresh_history = True + elif operation == "share": + ## Caught above for now + pass + elif operation == "rename": + ## Caught above for now + pass + elif operation == "delete": + status, message = self._list_delete( trans, histories ) + elif operation == "undelete": + status, message = self._list_undelete( trans, histories ) + trans.app.model.flush() + # Render the list view + return trans.fill_template( "/history/list.mako", + ids = id, + user = trans.user, + current_history = trans.history, + show_deleted = util.string_as_bool( show_deleted ), + refresh_history = refresh_history, + message_type = status, + message = message + ) + + def _list_delete( self, trans, histories ): + """Delete histories""" + n_deleted = 0 + deleted_current = False + for history in histories: + if not history.deleted: + # Delete DefaultHistoryPermissions + for dhp in history.default_permissions: + dhp.delete() + dhp.flush() + # Mark history as deleted in db + history.deleted = True + # If deleting the current history, make a new current. + if history == trans.history: + deleted_current = True + trans.new_history() + trans.log_event( "History id %d marked as deleted" % history.id ) + n_deleted += 1 + status = SUCCESS + message_parts = [] + if n_deleted: + message_parts.append( "Deleted %d histories." % n_deleted ) + if deleted_current: + message_parts.append( "Your active history was deleted, a new empty history is now active.") + status = INFO + return ( status, " ".join( message_parts ) ) + + def _list_undelete( self, trans, histories ): + """Undelete histories""" + n_undeleted = 0 + n_already_purged = 0 + for history in histories: + if history.purged: + n_already_purged += 1 + if history.deleted: + history.deleted = False + n_undeleted += 1 + trans.log_event( "History id %d marked as undeleted" % history.id ) + status = SUCCESS + message_parts = [] + if n_undeleted: + message_parts.append( "Undeleted %d histories." % n_undeleted ) + if n_already_purged: + message_parts.append( "%d have already been purged and cannot be undeleted." % n_already_purged ) + status = WARNING + return status, "".join( message_parts ) + + def _list_switch( self, trans, histories ): + """Switch to a new different history""" + new_history = histories[0] + galaxy_session = trans.get_galaxy_session() + try: + 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 ) + new_history.flush() + trans.set_history( new_history ) + trans.log_event( "History switched to id: %s, name: '%s'" % (str(new_history.id), new_history.name ) ) + # No message + return None, None + + ## These have been moved from 'root' but not cleaned up + + @web.expose + @web.require_login( "share histories with other users" ) + def share( self, trans, id=None, email="", **kwd ): + send_to_err = "" + if not id: + id = trans.get_history().id + if not isinstance( id, list ): + id = [ id ] + histories = [] + history_names = [] + for hid in id: + histories.append( trans.app.model.History.get( hid ) ) + history_names.append(histories[-1].name) + 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.filter( trans.app.model.User.table.c.email==email ).first() + params = util.Params( kwd ) + action = params.get( 'action', None ) + if 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 'history_share_btn' in kwd or action != 'share': + # The user is attempting to share a history whose datasets cannot all be accessed by the other user. In this case, + # the user sharing the history can chose to make the datasets public ( action == 'public' ) if he has the authority + # to do so, or automatically create a new "sharing role" that allows the user to share his private datasets only with the + # desired user ( action == 'private' ). + can_change = {} + cannot_change = {} + for history in histories: + for hda in history.activatable_datasets: + # Only deal with datasets that have not been purged + if not trans.app.security_agent.allow_action( send_to_user, + trans.app.security_agent.permitted_actions.DATASET_ACCESS, + dataset=hda ): + # The user with which we are sharing the history does not have access permission on the current dataset + 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: + # The current user has authority to change permissions on the current dataset because + # they have permission to manage permissions on the dataset and the dataset is not associated + # with a library. + if action == "private": + trans.app.security_agent.privately_share_dataset( hda.dataset, users=[ user, send_to_user ] ) + elif action == "public": + trans.app.security_agent.make_dataset_public( hda.dataset ) + elif history not in can_change: + # Build the set of histories / datasets on which the current user has authority + # to "manage permissions". This is used in /history/share.mako + can_change[ history ] = [ hda ] + else: + can_change[ history ].append( hda ) + else: + if action in [ "private", "public" ]: + # Don't change stuff that the user doesn't have permission to change + continue + elif history not in cannot_change: + # Build the set of histories / datasets on which the current user does + # not have authority to "manage permissions". This is used in /history/share.mako + 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( 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 ) ) ) + self.app.model.flush() + return trans.show_message( "History (%s) has been shared with: %s" % ( ",".join( history_names ),email ) ) + return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) + + @web.expose + @web.require_login( "rename histories" ) + def rename( self, trans, id=None, name=None, **kwd ): + if trans.app.memory_usage: + # Keep track of memory usage + m0 = self.app.memory_usage.memory() + user = trans.get_user() + + if not isinstance( id, list ): + if id != None: + id = [ id ] + if not isinstance( name, list ): + if name != None: + name = [ name ] + histories = [] + cur_names = [] + if not id: + if not trans.get_history().user: + return trans.show_error_message( "You must save your history before renaming it." ) + id = [trans.get_history().id] + for history_id in id: + history = trans.app.model.History.get( history_id ) + if history and history.user_id == user.id: + histories.append(history) + cur_names.append(history.name) + if not name or len(histories)!=len(name): + return trans.fill_template( "/history/rename.mako",histories=histories ) + change_msg = "" + for i in range(len(histories)): + if histories[i].user_id == user.id: + if name[i] == histories[i].name: + change_msg = change_msg + "

History: "+cur_names[i]+" is already named: "+name[i]+"

" + elif name[i] not in [None,'',' ']: + name[i] = escape(name[i]) + histories[i].name = name[i] + histories[i].flush() + change_msg = change_msg + "

History: "+cur_names[i]+" renamed to: "+name[i]+"

" + trans.log_event( "History renamed: id: %s, renamed to: '%s'" % (str(histories[i].id), name[i] ) ) + else: + change_msg = change_msg + "

You must specify a valid name for History: "+cur_names[i]+"

" + else: + change_msg = change_msg + "

History: "+cur_names[i]+" does not appear to belong to you.

" + if self.app.memory_usage: + m1 = trans.app.memory_usage.memory( m0, pretty=True ) + log.info( "End of root/history_rename, memory used increased by %s" % m1 ) + return trans.show_message( "

%s" % change_msg, refresh_frames=['history'] ) \ No newline at end of file diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py new file mode 100644 index 00000000000..30cb797bc71 --- /dev/null +++ b/lib/galaxy/web/controllers/library.py @@ -0,0 +1,668 @@ +from galaxy.web.base.controller import * +from galaxy.model.orm import * +from galaxy.datatypes import sniff +import logging, tempfile, zipfile, tarfile, os, sys, StringIO, shutil, urllib + +if sys.version_info[:2] < ( 2, 6 ): + zipfile.BadZipFile = zipfile.error +if sys.version_info[:2] < ( 2, 5 ): + zipfile.LargeZipFile = zipfile.error + +log = logging.getLogger( __name__ ) + +class Library( BaseController ): + @web.expose + def browse( self, trans, msg=None, messagetype=None, **kwd ): + libraries = trans.app.model.Library.filter( trans.app.model.Library.table.c.deleted==False ) \ + .order_by( trans.app.model.Library.table.c.name ).all() + return trans.fill_template( '/library/browser.mako', + libraries=libraries, + default_action=kwd.get( 'default_action', None ), + msg=msg, + messagetype=messagetype ) + 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" ) + if not isinstance( import_ids, list ): + import_ids = [ import_ids ] + p = util.Params( kwd ) + if not p.do_action: + return trans.show_error_message( "You must select an action to perform on selected datasets" ) + if p.do_action == 'add': + 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 into your history" % len( import_ids ), refresh_frames=['history'] ) + else: + # Can't use mkstemp - the file must not exist first + try: + tmpd = tempfile.mkdtemp() + tmpf = os.path.join( tmpd, 'library_download.' + p.do_action ) + if p.do_action == 'zip': + try: + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED, True ) + except RuntimeError: + log.exception( "Compression error when opening zipfile for library download" ) + return trans.show_error_message( "ZIP compression is not available in this Python, please notify an administrator" ) + except (TypeError, zipfile.LargeZipFile): + # ZIP64 is only in Python2.5+. Remove TypeError when 2.4 support is dropped + log.warning( 'Max zip file size is 2GB, ZIP64 not supported' ) + archive = zipfile.ZipFile( tmpf, 'w', zipfile.ZIP_DEFLATED ) + archive.add = lambda x, y: archive.write( x, y.encode('CP437') ) + elif p.do_action == 'tgz': + try: + archive = tarfile.open( tmpf, 'w:gz' ) + except tarfile.CompressionError: + log.exception( "Compression error when opening tarfile for library download" ) + return trans.show_error_message( "gzip compression is not available in this Python, please notify an administrator" ) + elif p.do_action == 'tbz': + try: + archive = tarfile.open( tmpf, 'w:bz2' ) + except tarfile.CompressionError: + log.exception( "Compression error when opening tarfile for library download" ) + return trans.show_error_message( "bzip2 compression is not available in this Python, please notify an administrator" ) + except (OSError, zipfile.BadZipFile, tarfile.ReadError): + log.exception( "Unable to create archive for download" ) + return trans.show_error_message( "Unable to create archive for download, please report this error" ) + seen = [] + for id in import_ids: + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if not lfda or not trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset = lfda.dataset ): + continue + path = "" + parent_folder = lfda.folder + while parent_folder is not None: + path = os.path.join( parent_folder.name, path ) + if parent_folder.parent is None: + path = os.path.join( parent_folder.library_root[0].name, path ) + parent_folder = parent_folder.parent + path += lfda.name + while path in seen: + path += '_' + seen.append( path ) + try: + archive.add( lfda.dataset.file_name, path ) + except IOError: + log.exception( "Unable to write to temporary library download archive" ) + return trans.show_error_message( "Unable to create archive for download, please report this error" ) + archive.close() + tmpfh = open( tmpf ) + # clean up now + try: + os.unlink( tmpf ) + os.rmdir( tmpd ) + except OSError: + log.exception( "Unable to remove temporary library download archive and directory" ) + return trans.show_error_message( "Unable to create archive for download, please report this error" ) + trans.response.headers[ "Content-Disposition" ] = "attachment; filename=GalaxyLibraryFiles.%s" % kwd['action'] + return tmpfh + @web.expose + def download_dataset_from_folder(self, trans, id, **kwd): + """Catches the dataset id and displays file contents as directed""" + # id refers to a LibraryFolderDatasetAssociation object + lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + dataset = trans.app.model.Dataset.get( lfda.dataset_id ) + if not dataset: + msg = 'Invalid id %s received for file downlaod' % str( id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=msg, messagetype='error' ) ) + mime = trans.app.datatypes_registry.get_mimetype_by_extension( lfda.extension.lower() ) + trans.response.set_content_type( mime ) + fStat = os.stat( lfda.file_name ) + trans.response.headers[ 'Content-Length' ] = int( fStat.st_size ) + valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + fname = lfda.name + fname = ''.join( c in valid_chars and c or '_' for c in fname )[ 0:150 ] + trans.response.headers[ "Content-Disposition" ] = "attachment; filename=GalaxyLibraryDataset-%s-[%s]" % ( str( id ), fname ) + try: + return open( lfda.file_name ) + except: + msg = 'This dataset contains no content' + return trans.response.send_redirect( web.url_for( action='library_browser', msg=msg, messagetype='error' ) ) + + @web.expose + def add_dataset( self, trans, folder_id=None, replace_id = None, name = None, info = None, refer_id = None, **kwd ): + folder = replace_dataset = None + if folder_id: + folder = trans.app.model.LibraryFolder.get( folder_id ) + permission_source = folder + else: + replace_dataset = trans.app.model.LibraryDataset.get( replace_id ) + permission_source = replace_dataset + msg = "" + messagetype="done" + + + + #### Copied/modified from admin controller this should be modular + # add_file method + def add_file( file_obj, name, extension, dbkey, last_used_build, roles, info='no info', space_to_tab=False, replace_dataset=None ): + 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 uncompressing 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 + if replace_dataset: + library_dataset = replace_dataset + else: + library_dataset = trans.app.model.LibraryDataset( name=name, info=info, extension=data_type, dbkey=dbkey ) + library_dataset.flush() + trans.app.model.library_security_agent.copy_permissions( permission_source, library_dataset, user = trans.get_user() ) + + dataset = trans.app.model.LibraryFolderDatasetAssociation( name=name, + info=info, + extension=data_type, + dbkey=dbkey, + library_dataset = library_dataset, + create_dataset=True ) + dataset.flush() + trans.app.model.library_security_agent.copy_permissions( permission_source, dataset, user = trans.get_user() ) + #library_item.set_library_folder_dataset_association( dataset ) + if not replace_dataset: + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( library_dataset, genome_build=last_used_build ) + library_dataset.library_folder_dataset_association_id = dataset.id + #library_dataset.library_folder_dataset_association = dataset + library_dataset.flush() + if roles: + for role in roles: + adra = trans.app.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 + + + + + if not folder and not replace_dataset: + msg = "Invalid library target specifed (folder id: %s, Library Dataset: %s)" %( str( folder_id ), replace_id ) + return trans.response.send_redirect( web.url_for( controller='library', action='browse', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if ( folder and trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_ADD, folder ) ) or ( replace_dataset and trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_ADD, replace_dataset ) ): + if 'new_dataset_button' in kwd: + params = util.Params( kwd ) + + #todo: populate these vars better + dbkey = params.get( 'dbkey', '?' ) + last_used_build = dbkey + extension = params.get( 'extension', 'auto' ) + + #### Copied from admin controller this should be unified + # Copied from upload tool action + data_file = params.get( 'file_data', '' ) + url_paste = params.get( 'url_paste', '' ) + server_dir = params.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='add_dataset', folder_id=folder_id, replace_id=replace_id, msg=util.sanitize_text( msg ), messagetype='done' ) ) + space_to_tab = params.get( 'space_to_tab', False ) + if space_to_tab and space_to_tab not in [ "None", None ]: + space_to_tab = True + roles = [] + role_ids = params.get( 'roles', [] ) + for role_id in util.listify( role_ids ): + roles.append( trans.app.model.Role.get( role_id ) ) + temp_name = "" + data_list = [] + created_lfda_ids = '' + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + created_lfda = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, + roles, + info="uploaded file", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = str( created_lfda.id ) + 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: + created_lfda = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + last_used_build, + roles, + info="uploaded url", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + created_lfda = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + last_used_build, + roles, + info="pasted entry", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + 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 + created_lfda = add_file( open( full_file, 'rb' ), + file, + extension, + dbkey, + last_used_build, + roles, + info="imported file", + space_to_tab=space_to_tab, + replace_dataset=replace_dataset ) + created_lfda_ids = '%s,%s' % ( created_lfda_ids, str( created_lfda.id ) ) + if created_lfda_ids: + created_lfda_ids = created_lfda_ids.lstrip( ',' ) + created_lfda_ids = created_lfda_ids.split(',') + msg = "%i new datasets added to the library. " % len( created_lfda_ids ) + return trans.fill_template( "/library/dataset_manage_list.mako", + lfdas=[ trans.app.model.LibraryFolderDatasetAssociation.get( lfda_id ) for lfda_id in created_lfda_ids ], + err=None, + msg=msg, + messagetype=messagetype ) + + #total_added = len( created_lfda_ids.split( ',' ) ) + #msg = "%i new datasets added to the library ( each is selected below ). " % total_added + #msg += "Click the Go button at the bottom of this page to edit the permissions on these datasets if necessary." + #trans.response.send_redirect( web.url_for( action='browse', + # created_lfda_ids=created_lfda_ids, + # msg=util.sanitize_text( msg ), + # messagetype='done' ) ) + else: + msg = "Upload failed" + trans.response.send_redirect( web.url_for( action='browse', + created_lfda_ids=created_lfda_ids, + msg=util.sanitize_text( msg ), + messagetype='error' ) ) + elif "add_dataset_from_history_button" in kwd: + + # See if the current history is empty + history = trans.get_history() + history.refresh() + if not history.active_datasets: + msg = 'Your current history is empty' + return trans.response.send_redirect( web.url_for( action='browse', msg=util.sanitize_text( msg ), messagetype='error' ) ) + hids = kwd.get( 'hids', [] ) + if not isinstance( hids, list ): + if hids: + hids = hids.split( "," ) + else: + hids = [] + dataset_names = [] + if hids: + for data_id in hids: + data = trans.app.model.HistoryDatasetAssociation.get( data_id ) + if data: + data = data.to_library_dataset_folder_association() + dataset_names.append( data.name ) + if folder: + folder.add_dataset( data ) + elif replace_dataset: + #if we are replacing versions and we recieve a list, we add all the datasets, and set the last one in the list as current + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, replace_dataset ): + replace_dataset.set_library_folder_dataset_association( data ) + else: + data.library_dataset = replace_dataset + data.flush() + else: + msg = "The requested dataset id %s is invalid" % str( data_id ) + return trans.response.send_redirect( web.url_for( action='library_browser', msg=util.sanitize_text( msg ), messagetype='error' ) ) + if dataset_names: + if folder: + msg = "Added the following datasets to the library folder: %s" % ( ", ".join( dataset_names ) ) + else: + msg = "Added the following datasets to the versioned library dataset: %s" % ( ", ".join( dataset_names ) ) + return trans.response.send_redirect( web.url_for( action='browse', msg=util.sanitize_text( msg ), messagetype='done' ) ) + else: + msg = 'Select at least one dataset from the list' + messagetype = 'error' + return trans.fill_template( "/library/new_dataset.mako", history=history, folder=folder, msg=msg, messagetype=messagetype ) + + #copied... + def get_dbkey_options( last_used_build ): + for dbkey, build_name in util.dbnames: + yield build_name, dbkey, ( dbkey==last_used_build ) + + return trans.fill_template( "/library/new_dataset.mako", + folder=folder, + replace_dataset = replace_dataset, + file_formats=trans.app.datatypes_registry.upload_file_formats, + dbkeys = get_dbkey_options( '?' ), + err=None, + msg=msg, + messagetype=messagetype ) + + + @web.expose + def library_dataset( self, trans, id, name = None, info = None, refer_id = None, **kwd ): + dataset = trans.app.model.LibraryDataset.get( id ) + if refer_id: + refered_lda = trans.app.model.LibraryFolderDatasetAssociation.get( refer_id ) + else: + refered_lda=None + msg = "" + messagetype="done" + + if not id or not dataset: + msg = "Invalid library dataset specified, id: %s" %str( library_dataset_id ) + return trans.response.send_redirect( web.url_for( controller='library', action='browse', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if 'save' in kwd: + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, dataset ): + dataset.name = name + dataset.info = info + target_lda = trans.app.model.LibraryFolderDatasetAssociation.get( kwd.get( 'set_lda_id' ) ) + dataset.library_folder_dataset_association = target_lda + trans.app.model.flush() + msg = 'Attributes updated for library dataset %s' % dataset.name + else: + msg = "Permission Denied" + messagetype = "error" + elif 'update_roles' in kwd: + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MANAGE, dataset ): + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( dataset, permissions ) + dataset.refresh() + msg = 'Permissions updated for library dataset %s' % dataset.name + else: + msg = "Permission Denied" + messagetype = "error" + + return trans.fill_template( "/library/library_dataset.mako", + dataset=dataset, + refered_lda = refered_lda, + err=None, + msg=msg, + messagetype=messagetype ) + + @web.expose + def folder( self, trans, id, name = None, description = None, **kwd ): + folder = trans.app.model.LibraryFolder.get( id ) + msg = "" + messagetype="done" + + if not id or not folder: + msg = "Invalid library folder specified, id: %s" %str( id ) + return trans.response.send_redirect( web.url_for( controller='library', action='browse', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if 'save' in kwd: + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, folder ): + folder.name = name + folder.description = description + trans.app.model.flush() + msg = 'Attributes updated for library folder %s' % folder.name + else: + msg = "Permission Denied" + messagetype = "error" + elif 'update_roles' in kwd: + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MANAGE, folder ): + # The user clicked the Save button on the 'Associate With Roles' form + permissions = {} + for k, v in trans.app.model.library_security_agent.permitted_actions.items(): + in_roles = [ trans.app.model.Role.get( x ) for x in util.listify( kwd.get( k + '_in', [] ) ) ] + permissions[ trans.app.model.library_security_agent.get_action( v.action ) ] = in_roles + trans.app.model.library_security_agent.set_all_permissions( folder, permissions ) + folder.refresh() + msg = 'Permissions updated for library folder %s' % folder.name + else: + msg = "Permission Denied" + messagetype = "error" + elif 'create_new' in kwd: + #create folder then return manage_folder template for new folder + #new folders default to having the same permissions as their parent folder + new_folder = trans.app.model.LibraryFolder( name = name, description = description ) + new_folder.flush() + folder.add_folder( new_folder ) + new_folder.flush() + trans.app.model.library_security_agent.copy_permissions( folder, new_folder, user = trans.get_user() ) + folder = new_folder + msg = "New folder (%s) created." % ( new_folder.name ) + return trans.fill_template( "/library/manage_folder.mako", + folder=folder, + err=None, + msg=msg, + messagetype=messagetype ) + + @web.expose + def edit_library( self, trans, id=None, **kwd ): + raise Exception( 'Not yet implemented' ) + + + @web.expose + def library_item_info( self, trans, do_action='display', id=None, library_item_id=None, library_item_type=None, **kwd ): + #dataset = trans.app.model.LibraryDataset.get( id ) + if id: + item_info = trans.app.model.LibraryItemInfo.get( id ) + else: + item_info = None + + if library_item_type == 'library': + library_item = trans.app.model.Library.get( library_item_id ) + elif library_item_type == 'library_dataset': + library_item = trans.app.model.LibraryDataset.get( library_item_id ) + elif library_item_type == 'folder': + library_item = trans.app.model.LibraryFolder.get( library_item_id ) + elif library_item_type == 'library_folder_dataset_association': + library_item = trans.app.model.LibraryFolderDatasetAssociation.get( library_item_id ) + else: + library_item_type == None + library_item = None + + msg = "" + messagetype="done" + + if not item_info and not library_item_type: + msg = "Unable to perform requested action (%s)." % do_action + return trans.response.send_redirect( web.url_for( controller='library', action='browse', msg=util.sanitize_text( msg ), messagetype='error' ) ) + + if do_action == 'display': + return trans.fill_template( "/library/display_info.mako", + item_info=item_info, + err=None, + msg=msg, + messagetype=messagetype ) + elif do_action == 'new_info': + if library_item: + if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_ADD, library_item ): + if 'create_new_info_button' in kwd: + user = trans.get_user() + #create new info then send back to make more + library_item_info_template_id = kwd.get( 'library_item_info_template_id', None ) + library_item_info_template = trans.app.model.LibraryItemInfoTemplate.get( library_item_info_template_id ) + library_item_info = trans.app.model.LibraryItemInfo() + library_item_info.library_item_info_template = library_item_info_template + library_item_info.user = user + library_item_info.flush() + + trans.app.model.library_security_agent.copy_permissions( library_item_info_template, library_item_info, user = user ) + + for template_element in library_item_info_template.elements: + info_element_value = kwd.get( "info_element_%s_%s" % ( library_item_info_template.id, template_element.id), None ) + info_element = trans.app.model.LibraryItemInfoElement() + info_element.contents = info_element_value + info_element.library_item_info_template_element = template_element + info_element.library_item_info = library_item_info + info_element.flush() + library_item_info_association = trans.app.model.LibraryItemInfoAssociation() + library_item_info_association.set_library_item( library_item ) + library_item_info_association.library_item_info = library_item_info + library_item_info_association.user = user + library_item_info_association.flush() + #don't need to set permissions on the association object? + + msg = 'Library Item Info has been save, you can now fill out more templates.' + return trans.fill_template( "/library/new_info.mako", + library_item=library_item, + library_item_type=library_item_type, + err=None, + msg=msg, + messagetype=messagetype ) + else: + return trans.show_error_message( "You do not have permission to add info to this library item." ) + #add more functionality -> user's should be able to edit/delete, etc, and create/delete and edit templates + + return trans.fill_template( "/library/display_info.mako", + item_info=item_info, + err=None, + msg="Unable to perform requested action (%s)." % do_action, + messagetype=messagetype ) + + + +#methods used when adding files, copied from upload... + def check_gzip( self, temp_name ): + 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 ) + + def check_zip( self, temp_name ): + if not zipfile.is_zipfile( temp_name ): + return ( False, False, None ) + zip_file = zipfile.ZipFile( temp_name, "r" ) + # Make sure the archive consists of valid files. The current rules are: + # 1. Archives can only include .ab1, .scf or .txt files + # 2. All file extensions within an archive must be the same + name = zip_file.namelist()[0] + test_ext = name.split( "." )[1].strip().lower() + if not ( test_ext == 'scf' or test_ext == 'ab1' or test_ext == 'txt' ): + return ( True, False, test_ext ) + for name in zip_file.namelist(): + ext = name.split( "." )[1].strip().lower() + if ext != test_ext: + return ( True, False, test_ext ) + return ( True, True, test_ext ) + + def check_html( self, temp_name, chunk=None ): + if chunk is None: + temp = open(temp_name, "U") + else: + temp = chunk + regexp1 = re.compile( "]*HREF[^>]+>", re.I ) + regexp2 = re.compile( "]*>", re.I ) + regexp3 = re.compile( "]*>", re.I ) + regexp4 = re.compile( "]*>", re.I ) + lineno = 0 + for line in temp: + lineno += 1 + matches = regexp1.search( line ) or regexp2.search( line ) or regexp3.search( line ) or regexp4.search( line ) + if matches: + if chunk is None: + temp.close() + return True + if lineno > 100: + break + if chunk is None: + temp.close() + return False + + def check_binary( self, temp_name, chunk=None ): + if chunk is None: + temp = open( temp_name, "U" ) + else: + temp = chunk + lineno = 0 + for line in temp: + lineno += 1 + line = line.strip() + if line: + for char in line: + if ord( char ) > 128: + if chunk is None: + temp.close() + return True + if lineno > 10: + break + if chunk is None: + temp.close() + return False + +class BadFileException( Exception ): + pass diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 5feec8dbcc5..0f7ba0e9f46 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__ ) @@ -20,16 +16,20 @@ class RootController( BaseController ): return 'This link may not be followed from within Galaxy.' @web.expose - def index(self, trans, id=None, tool_id=None, mode=None, m_c=None, m_a=None, **kwd): + def index(self, trans, id=None, tool_id=None, mode=None, workflow_id=None, m_c=None, m_a=None, **kwd): return trans.fill_template( "root/index.mako", tool_id=tool_id, + workflow_id=workflow_id, m_c=m_c, m_a=m_a ) ## ---- Tool related ----------------------------------------------------- @web.expose def tool_menu( self, trans ): - return trans.fill_template('/root/tool_menu.mako', toolbox=self.get_toolbox() ) + if trans.app.config.require_login and not trans.user: + return trans.fill_template( '/no_access.mako', message = 'Please log in to access Galaxy tools.' ) + else: + return trans.fill_template('/root/tool_menu.mako', toolbox=self.get_toolbox() ) @web.expose def tool_help( self, trans, id ): @@ -55,6 +55,8 @@ class RootController( BaseController ): NOTE: No longer accepts "id" or "template" options for security reasons. """ history = trans.get_history() + if trans.app.config.require_login and not trans.user: + return trans.fill_template( '/no_access.mako', message = 'Please log in to access Galaxy histories.' ) if as_xml: trans.response.set_content_type('text/xml') return trans.fill_template_mako( "root/history_as_xml.mako", history=history ) @@ -88,7 +90,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 @@ -134,22 +136,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 allowed to access this dataset" else: return "No dataset with id '%s'" % str( id ) @@ -161,9 +166,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 ) @@ -173,9 +181,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 @@ -191,77 +202,114 @@ 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 ): + params = 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) ) ) + if lid is None or trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, data ): + #trans.app.model.library_security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset=data ): + edit_allowed = True + else: + edit_allowed = False + if params.change: + # The user clicked the Save button on the 'Change data type' form + if not edit_allowed: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + trans.app.datatypes_registry.change_datatype( data, params.datatype ) + trans.app.model.flush() + elif params.save: + # The user clicked the Save button on the 'Edit Attributes' form + if not edit_allowed: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) + data.name = params.name + data.info = params.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 = params.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( params.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 params.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + if not edit_allowed: + 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 params.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 edit_allowed: + 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 params.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 = params.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_all_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, edit_allowed = edit_allowed ) + else: + return trans.show_error_message( "You do not have permission to edit this dataset's (%s) attributes." % id ) def __delete_dataset( self, trans, id ): data = self.app.model.HistoryDatasetAssociation.get( id ) @@ -305,7 +353,7 @@ class RootController( BaseController ): def delete_async( self, trans, id = None, **kwd): if id: try: - int( id ) + id = int( id ) except: return "Dataset id '%s' is invalid" %str( id ) self.__delete_dataset( trans, id ) @@ -318,75 +366,13 @@ class RootController( BaseController ): """Displays a list of history related actions""" return trans.fill_template( "/history/options.mako", user = trans.get_user(), history = trans.get_history() ) - + @web.expose - def history_delete( self, trans, id=None, **kwd): - """Deletes a list of histories, ensures that histories are owned by current user""" - history_names = [] - if id: - if isinstance( id, list ): - history_ids = id - else: - history_ids = [ id ] - user = trans.get_user() - for hid in history_ids: - try: - int( hid ) - except: - return trans.show_message( "Invalid history: %s" % str( hid ) ) - history = self.app.model.History.get( hid ) - 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) - history.deleted = True - # If deleting the current history, make a new current. - if history == trans.get_history(): - trans.new_history() - else: - return trans.show_message( "Not able to find history %s" % str( hid ) ) - self.app.model.flush() - trans.log_event( "History id %s marked as deleted" % str(hid) ) - else: - return trans.show_message( "You must select at least one history to delete." ) - return trans.show_message( "History deleted: %s" % ",".join(history_names), - refresh_frames=['history']) - - @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 ) ) + def history_delete( self, trans, id ): + """ + Backward compatibility with check_galaxy script. + """ + return trans.webapp.controllers['history'].list( trans, id, operation='delete' ) @web.expose def clear_history( self, trans ): @@ -398,60 +384,6 @@ class RootController( BaseController ): self.app.model.flush() trans.log_event( "History id %s cleared" % (str(history.id)) ) trans.response.send_redirect( url_for("/index" ) ) - - @web.expose - @web.require_login( "share histories with other users" ) - def history_share( self, trans, id=None, email="", **kwd ): - send_to_err = "" - if not id: - id = trans.get_history().id - if not isinstance( id, list ): - id = [ id ] - histories = [] - history_names = [] - for hid in id: - histories.append( trans.app.model.History.get( hid ) ) - history_names.append(histories[-1].name) - 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.filter_by( email=email ).first() - 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: - for history in histories: - new_history = history.copy() - 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)) ) - self.app.model.flush() - return trans.show_message( "History (%s) has been shared with: %s" % (",".join(history_names),email) ) - return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) - - @web.expose - @web.require_login( "work with multiple histories" ) - def history_available( self, trans, id=[], do_operation = "view", show_deleted = False, ok_msg = "", error_msg="", as_xml=False, **kwd ): - """ - List all available histories - """ - if as_xml: - trans.response.set_content_type('text/xml') - return trans.fill_template( "/history/list_as_xml.mako" ) - if not isinstance( id, list ): - id = id.split( "," ) - trans.log_event( "History id %s available" % str( id ) ) - history_operations = dict( share=self.history_share, rename=self.history_rename, delete=self.history_delete, undelete=self.history_undelete ) - if do_operation in history_operations: - return history_operations[do_operation]( trans, id=id, show_deleted=show_deleted, ok_msg=ok_msg, error_msg=error_msg, **kwd ) - return trans.fill_template( "/history/list.mako", ids=id, - user=trans.get_user(), - current_history=trans.get_history(), - show_deleted=util.string_as_bool( show_deleted ), - ok_msg=ok_msg, error_msg=error_msg ) - - @web.expose def history_import( self, trans, id=None, confirm=False, **kwd ): @@ -466,7 +398,7 @@ 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() @@ -502,28 +434,6 @@ class RootController( BaseController ): Warning! If you import this history, you will lose your current history. Click here to confirm. """ % web.url_for( id=id, confirm=True ) ) - - @web.expose - @web.require_login( "switch histories" ) - def history_switch( self, trans, id=None ): - if not id: - return trans.response.send_redirect( web.url_for( action='history_available' ) ) - else: - new_history = trans.app.model.History.get( id ) - if new_history: - galaxy_session = trans.get_galaxy_session() - try: - 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 ) - new_history.flush() - trans.set_history( new_history ) - trans.log_event( "History switched to id: %s, name: '%s'" % (str(new_history.id), new_history.name ) ) - return trans.show_message( "History switched to: %s" % new_history.name, - refresh_frames=['history']) - else: - return trans.show_error_message( "History not found" ) @web.expose def history_new( self, trans ): @@ -532,52 +442,17 @@ class RootController( BaseController ): return trans.show_message( "New history created", refresh_frames = ['history'] ) @web.expose - @web.require_login( "renames histories" ) - def history_rename( self, trans, id=None, name=None, **kwd ): - user = trans.get_user() - - if not isinstance( id, list ): - if id != None: - id = [ id ] - if not isinstance( name, list ): - if name != None: - name = [ name ] - histories = [] - cur_names = [] - if not id: - if not trans.get_history().user: - return trans.show_error_message( "You must save your history before renaming it." ) - id = [trans.get_history().id] - for history_id in id: - history = trans.app.model.History.get( history_id ) - if history and history.user_id == user.id: - histories.append(history) - cur_names.append(history.name) - if not name or len(histories)!=len(name): - return trans.fill_template( "/history/rename.mako",histories=histories ) - change_msg = "" - for i in range(len(histories)): - if histories[i].user_id == user.id: - if name[i] == histories[i].name: - change_msg = change_msg + "

History: "+cur_names[i]+" is already named: "+name[i]+"

" - elif name[i] not in [None,'',' ']: - name[i] = escape(name[i]) - histories[i].name = name[i] - histories[i].flush() - change_msg = change_msg + "

History: "+cur_names[i]+" renamed to: "+name[i]+"

" - trans.log_event( "History renamed: id: %s, renamed to: '%s'" % (str(histories[i].id), name[i] ) ) - else: - change_msg = change_msg + "

You must specify a valid name for History: "+cur_names[i]+"

" - else: - change_msg = change_msg + "

History: "+cur_names[i]+" does not appear to belong to you.

" - 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_all_dataset_permissions( data.dataset, permissions ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -594,9 +469,33 @@ 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 + dataset = 'dataset' in kwd + bypass_manage_permission = 'bypass_manage_permission' in kwd + trans.app.security_agent.history_set_default_permissions( history, permissions, dataset=dataset, bypass_manage_permission=bypass_manage_permission ) + 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""" @@ -613,7 +512,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 @@ -621,8 +520,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 ): @@ -665,3 +571,7 @@ 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 70b032f27cd..6fde80cd2dc 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -1,14 +1,25 @@ """ 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 log = logging.getLogger( __name__ ) +require_login_template = """ +

Welcome to Galaxy

+ +

+ This installation of Galaxy has been configured such that only users who are logged in may use it.%s +

+

+""" +require_login_nocreation_template = require_login_template % "" +require_login_creation_template = require_login_template % " If you don't already have an account, you may create one." + class User( BaseController ): @web.expose @@ -72,40 +83,69 @@ class User( BaseController ): def login( self, trans, email='', password='' ): email_error = password_error = None # Attempt login + if trans.app.config.require_login: + refresh_frames = [ 'masthead', 'history', 'tools' ] + else: + refresh_frames = [ 'masthead', 'history' ] if email or password: - user = trans.app.model.User.filter_by( email=email ).first() + user = trans.app.model.User.filter( trans.app.model.User.table.c.email==email ).first() if not user: email_error = "No such user" + elif user.deleted: + email_error = "This account has been marked deleted, contact your Galaxy administrator to restore the account." elif user.external: - return trans.show_error_message( "This account was created for use with an external authentication " - + "method. Please contact your local Galaxy administrator to activate it." ) + email_error = "This account was created for use with an external authentication method, contact your local Galaxy administrator to activate it." elif not user.check_password( password ): password_error = "Invalid password" else: trans.handle_user_login( user ) trans.log_event( "User logged in" ) - return trans.show_ok_message( "Now logged in as " + user.email, refresh_frames=['masthead', 'history'] ) - return trans.show_form( - web.FormBuilder( web.url_for(), "Login", submit_text="Login" ) - .add_text( "email", "Email address", value=email, error=email_error ) + msg = "Now logged in as " + user.email + "." + if trans.app.config.require_login: + msg += ' Click here to continue to the front page.' % web.url_for( '/static/welcome.html' ) + return trans.show_ok_message( msg, refresh_frames=refresh_frames ) + form = web.FormBuilder( web.url_for(), "Login", submit_text="Login" ) \ + .add_text( "email", "Email address", value=email, error=email_error ) \ .add_password( "password", "Password", value='', error=password_error, - help="Forgot password? Reset here" % web.url_for( action='reset_password' ) ) ) + help="Forgot password? Reset here" % web.url_for( action='reset_password' ) ) + if trans.app.config.require_login: + if trans.app.config.allow_user_creation: + return trans.show_form( form, header = require_login_creation_template % web.url_for( action = 'create' ) ) + else: + return trans.show_form( form, header = require_login_nocreation_template ) + else: + return trans.show_form( form ) + @web.expose def logout( self, trans ): + if trans.app.config.require_login: + refresh_frames = [ 'masthead', 'history', 'tools' ] + else: + refresh_frames = [ 'masthead', 'history' ] # Since logging an event requires a session, we'll log prior to ending the session trans.log_event( "User logged out" ) trans.handle_user_logout() - return trans.show_ok_message( "You are no longer logged in", refresh_frames=['masthead', 'history'] ) - + msg = "You are no longer logged in." + if trans.app.config.require_login: + msg += ' Click here to return to the login page.' % web.url_for( controller='user', action='login' ) + return trans.show_ok_message( msg, refresh_frames=refresh_frames ) + @web.expose - def create( self, trans, email='', password='', confirm='',subscribe=False ): + def create( self, trans, email='', password='', confirm='', subscribe=False ): + if trans.app.config.require_login: + refresh_frames = [ 'masthead', 'history', 'tools' ] + else: + refresh_frames = [ 'masthead', 'history' ] + if not trans.app.config.allow_user_creation and not trans.user_is_admin(): + return trans.show_error_message( 'User registration is disabled. Please contact your Galaxy administrator for an account.' ) email_error = password_error = confirm_error = None if email: if len( email ) == 0 or "@" not in email or "." not in email: email_error = "Please enter a real email address" - elif len( email) > 255: + elif len( email ) > 255: email_error = "Email address exceeds maximum allowable length" - elif trans.app.model.User.filter_by( email=email ).first(): + elif trans.app.model.User.filter( and_( trans.app.model.User.table.c.email==email, + trans.app.model.User.table.c.deleted==False ) ).first(): email_error = "User with that email already exists" elif len( password ) < 6: password_error = "Please use a password of at least 6 characters" @@ -115,6 +155,11 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() + trans.app.security_agent.create_private_user_role( user ) + # We set default user permissions, before we log in and set the default history permissions + trans.app.security_agent.user_set_default_permissions( user ) + # The handle_user_login() method has a call to the history_set_default_permissions() method + # (needed when logging in with a history), user needs to have default permissions set before logging in trans.handle_user_login( user ) trans.log_event( "User created a new account" ) trans.log_event( "User logged in" ) @@ -133,13 +178,13 @@ class User( BaseController ): .add_input( "checkbox","Subscribe To Mailing List","subscribe", value='subscribe' ) ) @web.expose - def reset_password(self, trans, email=None, **kwd): + def reset_password( self, trans, email=None, **kwd ): error = '' - reset_user = trans.app.model.User.filter_by( email=email ).first() + reset_user = trans.app.model.User.filter( trans.app.model.User.table.c.email==email ).first() user = trans.get_user() if reset_user: if user and user.id != reset_user.id: - error = "You may only reset your own password" + error = "You may only reset your own password" else: chars = string.letters + string.digits new_pass = "" @@ -148,13 +193,34 @@ class User( BaseController ): mail = os.popen("%s -t" % trans.app.config.sendmail_path, 'w') mail.write("To: %s\nFrom: no-reply@%s\nSubject: Galaxy Password Reset\n\nYour password has been reset to \"%s\" (no quotes)." % (email, trans.request.remote_addr, new_pass) ) if mail.close(): - return trans.show_ok_message( "Failed to reset password! If this problem persist, submit a bug report.") + return trans.show_error_message( 'Failed to reset password. If this problem persists, please submit a bug report.' ) reset_user.set_password_cleartext( new_pass ) reset_user.flush() trans.log_event( "User reset password: %s" % email ) - return trans.show_ok_message( "Password has been reset and emailed to: %s." % email) + return trans.show_ok_message( "Password has been reset and emailed to: %s. Click here to return to the login form." % ( email, web.url_for( action='login' ) ) ) elif email != None: error = "The specified user does not exist" 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 ] + action = trans.app.security_agent.get_action( v.action ).action + permissions[ 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 94b573ef9c5..c4584960baa 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -13,12 +13,17 @@ from galaxy.util.bunch import Bunch from galaxy.util.topsort import topsort, topsort_levels, CycleError from galaxy.workflow.modules import * from galaxy.model.mapping import desc +from galaxy.model.orm import * 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 +38,29 @@ 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 ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def list_for_run( self, trans ): + """ + Render workflow list for analysis view (just allows running workflow + or switching to management view) + """ + user = trans.get_user() + workflows = trans.sa_session.query( model.StoredWorkflow ) \ + .filter_by( user=user, deleted=False ) \ + .order_by( desc( model.StoredWorkflow.c.update_time ) ) \ + .all() + shared_by_others = trans.sa_session \ + .query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=user ) \ + .filter( model.StoredWorkflow.c.deleted == False ) \ + .order_by( desc( model.StoredWorkflow.c.update_time ) ) \ + .all() + return trans.fill_template( "workflow/list_for_run.mako", workflows = workflows, shared_by_others = shared_by_others ) @@ -44,7 +71,8 @@ class WorkflowController( BaseController ): # Load workflow from database stored = get_stored_workflow( trans, id ) if email: - other = model.User.filter_by( email=email ).first() + other = model.User.filter( and_( model.User.table.c.email==email, + model.User.table.c.deleted==False ) ).first() if not other: mtype = "error" msg = ( "User '%s' does not exist" % email ) @@ -61,10 +89,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 +107,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 +132,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 +157,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 +178,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" ) @@ -430,11 +458,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) ) ) ) @@ -507,8 +534,8 @@ class WorkflowController( BaseController ): elif isinstance( input, Conditional ): values = input_values[ input.name ] current = values["__current_case__"] - prefix = prefix + "|" + input.name - visitor( input.cases[current].inputs, values, prefix ) + new_prefix = prefix + input.name + "|" + visitor( input.cases[current].inputs, values, new_prefix ) else: if isinstance( input, DataToolParameter ): prefixed_name = prefix + input.name diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 7abd179d478..93226dcd98e 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -4,7 +4,7 @@ Galaxy web application framework import pkg_resources -import os, sys, time, random, string +import os, sys, time, socket, random, string pkg_resources.require( "Cheetah" ) from Cheetah.Template import Template import base @@ -32,6 +32,17 @@ log = logging.getLogger( __name__ ) url_for = base.routes.url_for +UCSC_SERVERS = ( + 'hgw1.cse.ucsc.edu', + 'hgw2.cse.ucsc.edu', + 'hgw3.cse.ucsc.edu', + 'hgw4.cse.ucsc.edu', + 'hgw5.cse.ucsc.edu', + 'hgw6.cse.ucsc.edu', + 'hgw7.cse.ucsc.edu', + 'hgw8.cse.ucsc.edu', +) + def expose( func ): """ Decorator: mark a function as 'exposed' and thus web accessible @@ -60,6 +71,19 @@ def require_login( verb="perform this action" ): return decorator return argcatcher +def require_admin( func ): + def decorator( self, trans, *args, **kwargs ): + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + if not admin_users: + return trans.show_error_message( "You must be logged in as an administrator to access this feature, but no administrators are set in the Galaxy configuration." ) + user = trans.get_user() + if not user: + return trans.show_error_message( "You must be logged in as an administrator to access this feature." ) + if not user.email in admin_users: + return trans.show_error_message( "You must be an administrator to access this feature." ) + return func( self, trans, *args, **kwargs ) + return decorator + NOT_SET = object() class MessageException( Exception ): @@ -118,6 +142,8 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): self.workflow_building_mode = False # Always have a valid galaxy session self.__ensure_valid_session( session_cookie ) + if self.app.config.require_login: + self.__ensure_logged_in_user( environ ) @property def sa_session( self ): """ @@ -223,8 +249,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.user = self.__get_or_create_remote_user( remote_user_email ) galaxy_session_requires_flush = True elif galaxy_session.user.email != remote_user_email: - # Session exists but is not associated with the correct - # remote user + # Session exists but is not associated with the correct remote user invalidate_existing_session = True user_for_new_session = self.__get_or_create_remote_user( remote_user_email ) log.warning( "User logged in as '%s' externally, but has a cookie as '%s' invalidating session", @@ -260,6 +285,28 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if prev_galaxy_session: objects_to_flush.append( prev_galaxy_session ) sa_session.flush( objects_to_flush ) + def __ensure_logged_in_user( self, environ ): + allowed_paths = ( + url_for( controller='root', action='index' ), + url_for( controller='root', action='tool_menu' ), + url_for( controller='root', action='masthead' ), + url_for( controller='root', action='history' ), + url_for( controller='user', action='login' ), + url_for( controller='user', action='create' ), + url_for( controller='user', action='reset_password' ), + url_for( controller='library', action='browse' ) + ) + display_as = url_for( controller='root', action='display_as' ) + if self.galaxy_session.user is None: + if self.app.config.ucsc_display_sites and self.request.path == display_as: + try: + host = socket.gethostbyaddr( self.environ[ 'REMOTE_ADDR' ] )[0] + except( socket.error, socket.herror, socket.gaierror, socket.timeout ): + host = None + if host in UCSC_SERVERS: + return + if self.request.path not in allowed_paths: + self.response.send_redirect( url_for( controller='root', action='index' ) ) def __create_new_session( self, prev_galaxy_session=None, user_for_new_session=None ): """ Create a new GalaxySession for this request, possibly with a connection @@ -287,7 +334,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): Return the user in $HTTP_REMOTE_USER and create if necessary """ # remote_user middleware ensures HTTP_REMOTE_USER exists - user = self.app.model.User.filter_by( email=remote_user_email ).first() + user = self.app.model.User.filter( self.app.model.User.table.c.email==remote_user_email ).first() if user is None: random.seed() user = self.app.model.User( email=remote_user_email ) @@ -295,6 +342,8 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): user.external = True user.flush() #self.log_event( "Automatically created account '%s'", user.email ) + elif user.deleted: + return self.show_error_message( "Your account is no longer valid, contact your Galaxy administrator to activate your account." ) return user def __update_session_cookie( self, name='galaxysession' ): """ @@ -307,20 +356,25 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): Login a new user (possibly newly created) - create a new session - associate new session with user - - if old session had a history and it was not associated with a user, associate it with the new session. + - if old session had a history and it was not associated with a user, associate it with the new session, + otherwise associate the current session's history with the user """ prev_galaxy_session = self.galaxy_session prev_galaxy_session.is_valid = False self.galaxy_session = self.__create_new_session( prev_galaxy_session, user ) if prev_galaxy_session.current_history: history = prev_galaxy_session.current_history - if history.user is None: - self.galaxy_session.add_history( history ) - self.galaxy_session.current_history = history - history.user = user - self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session, history ] ) + elif self.galaxy_session.current_history: + history = self.galaxy_session.current_history else: - self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session ] ) + history = self.history + if history not in self.galaxy_session.histories: + self.galaxy_session.add_history( history ) + if history.user is None: + history.user = user + self.galaxy_session.current_history = history + self.app.security_agent.history_set_default_permissions( history, dataset=True, bypass_manage_permission=True ) + self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session, history ] ) # This method is not called from the Galaxy reports, so the cookie will always be galaxysession self.__update_session_cookie( name='galaxysession' ) def handle_user_logout( self ): @@ -376,6 +430,8 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): history.user = self.galaxy_session.user # Track genome_build with history history.genome_build = util.dbnames.default_value + # Set the user's default history permissions + self.app.security_agent.history_set_default_permissions( history ) # Save self.sa_session.flush( [ self.galaxy_session, history ] ) return history @@ -388,7 +444,13 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): self.galaxy_session.user = user self.sa_session.flush( [ self.galaxy_session ] ) user = property( get_user, set_user ) - + + def user_is_admin( self ): + admin_users = self.app.config.get( "admin_users", "" ).split( "," ) + if self.user and admin_users and self.user.email in admin_users: + return True + return False + def get_toolbox(self): """Returns the application toolbox""" return self.app.toolbox @@ -434,12 +496,12 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): Convenience method for displaying an warn message. See `show_message`. """ return self.show_message( message, 'warning', refresh_frames ) - def show_form( self, form ): + def show_form( self, form, header=None ): """ Convenience method for displaying a simple page with a single HTML form. """ - return self.fill_template( "form.mako", form=form ) + return self.fill_template( "form.mako", form=form, header=header ) def fill_template(self, filename, **kwargs): """ Fill in a template, putting any keyword arguments on the context. @@ -476,8 +538,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 ) @@ -488,13 +550,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 ): """ @@ -514,3 +577,4 @@ class Bunch( dict ): return self[key] def __setattr__( self, key, value ): self[key] = value + diff --git a/lib/galaxy/web/framework/base.py b/lib/galaxy/web/framework/base.py index 6dff0719d2a..5bef6415223 100644 --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -84,15 +84,19 @@ class WebApplication( object ): friendly objects, finds the appropriate method to handle the request and calls it. """ - # Setup the transaction - trans = self.transaction_factory( environ ) # Map url using routes - path_info = trans.request.path_info + path_info = environ.get( 'PATH_INFO', '' ) map = self.mapper.match( path_info ) if map == None: raise httpexceptions.HTTPNotFound( "No route for " + path_info ) - # Save the complete mapper dict, we pop things off so they don't get passed down - raw_map = dict( map ) + # Setup routes + rc = routes.request_config() + rc.mapper = self.mapper + rc.mapper_dict = map + rc.environ = environ + # Setup the transaction + trans = self.transaction_factory( environ ) + rc.redirect = trans.response.send_redirect # Get the controller class controller_name = map.pop( 'controller', None ) controller = self.controllers.get( controller_name, None ) @@ -111,12 +115,6 @@ class WebApplication( object ): # Is the method callable if not callable( method ): raise httpexceptions.HTTPNotFound( "Action not callable for " + path_info ) - # Setup routes - rc = routes.request_config() - rc.mapper = self.mapper - rc.mapper_dict = raw_map - rc.environ = environ - rc.redirect = trans.response.send_redirect # Combine mapper args and query string / form args and call kwargs = trans.request.params.mixed() kwargs.update( map ) @@ -336,4 +334,4 @@ def flatten( seq ): for y in flatten( x, encoding ): yield y else: - yield x \ No newline at end of file + yield x diff --git a/lib/galaxy/webapps/reports/config.py b/lib/galaxy/webapps/reports/config.py index 5fde91813a3..aba37aca676 100644 --- a/lib/galaxy/webapps/reports/config.py +++ b/lib/galaxy/webapps/reports/config.py @@ -27,6 +27,7 @@ class Configuration( object ): self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root ) self.id_secret = kwargs.get( "id_secret", "USING THE DEFAULT IS NOT SECURE!" ) self.use_remote_user = string_as_bool( kwargs.get( "use_remote_user", "False" ) ) + self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root ) self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/reports/compiled_templates" ), self.root ) self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index ab3d5c5a95b..e35c1811e98 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -104,7 +104,7 @@ class Users( BaseController ): cutoff_time = datetime.utcnow() - timedelta( days=int( not_logged_in_for_days ) ) now = strftime( "%Y-%m-%d %H:%M:%S" ) users = [] - for user in galaxy.model.User.query().order_by( galaxy.model.User.table.c.email ).all(): + for user in galaxy.model.User.filter( galaxy.model.User.table.c.deleted==False ).order_by( galaxy.model.User.table.c.email ).all(): if user.galaxy_sessions: last_galaxy_session = user.galaxy_sessions[ 0 ] if last_galaxy_session.update_time < cutoff_time: diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index f9561bf8783..07c3084f61f 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -208,8 +208,8 @@ class ToolModule( object ): values = input_values[ input.name ] current = values["__current_case__"] label_prefix = label_prefix - name_prefix = name_prefix + "|" + input.name - visitor( input.cases[current].inputs, values, name_prefix, label_prefix ) + new_name_prefix = name_prefix + input.name + "|" + visitor( input.cases[current].inputs, values, new_name_prefix, label_prefix ) else: if isinstance( input, DataToolParameter ): data_inputs.append( dict( name=name_prefix+input.name, label=label_prefix+input.label, extensions=input.extensions ) ) diff --git a/reports_wsgi.ini.sample b/reports_wsgi.ini.sample index b87d4dc6cff..1eceb9c6819 100644 --- a/reports_wsgi.ini.sample +++ b/reports_wsgi.ini.sample @@ -46,6 +46,9 @@ use_lint = false # NEVER enable this on a public site (even test or QA) # use_interactive = true +# Force everyone to log in (disable anonymous access) +require_login = False + # path to sendmail sendmail_path = /usr/sbin/sendmail diff --git a/scripts/cleanup_datasets/cleanup_datasets.py b/scripts/cleanup_datasets/cleanup_datasets.py index 33f16e32752..79ea673c4d4 100644 --- a/scripts/cleanup_datasets/cleanup_datasets.py +++ b/scripts/cleanup_datasets/cleanup_datasets.py @@ -207,6 +207,12 @@ def purge_histories( h, d, m, cutoff_time, remove_from_disk ): metadata_file.purged = True metadata_file.flush() print "%s" % metadata_file.file_name() + for lda in dataset.library_associations: + for metadata_file in m.filter( m.table.c.lda_id==lda.id ).all(): + metadata_file.deleted = True + metadata_file.purged = True + metadata_file.flush() + print "%s" % metadata_file.file_name() dataset_count += 1 try: disk_space += file_size @@ -275,6 +281,12 @@ def purge_datasets( d, m, cutoff_time, remove_from_disk ): metadata_file.purged = True metadata_file.flush() print "%s" % metadata_file.file_name() + for lda in dataset.library_associations: + for metadata_file in m.filter( m.table.c.lda_id==lda.id ).all(): + metadata_file.deleted = True + metadata_file.purged = True + metadata_file.flush() + print "%s" % metadata_file.file_name() dataset_count += 1 try: disk_space += file_size @@ -310,6 +322,12 @@ def purge_dataset( dataset, d, m ): if not shared_data.deleted: purgable = False break + if purgable: + # This check handles the library_folder_dataset_association approach to sharing. + for shared_data in dataset.library_associations: + if not shared_data.deleted: + purgable = False + break if purgable: dataset.purged = True dataset.file_size = 0 @@ -326,6 +344,12 @@ def purge_dataset( dataset, d, m ): metadata_file.purged = True metadata_file.flush() print "%s" % metadata_file.file_name() + for lda in dataset.library_associations: + for metadata_file in m.filter( m.table.c.lda_id==lda.id ).all(): + metadata_file.deleted = True + metadata_file.purged = True + metadata_file.flush() + print "%s" % metadata_file.file_name() try: # Remove associated extra files from disk if they exist os.unlink( dataset.extra_files_path ) diff --git a/scripts/get_platforms.py b/scripts/get_platforms.py index be75ca87698..3bccb74992b 100755 --- a/scripts/get_platforms.py +++ b/scripts/get_platforms.py @@ -1,9 +1,12 @@ #!/usr/bin/env python -import sys +import sys, os assert sys.version_info[:2] >= ( 2, 4 ) -from eggs import get_full_platform, get_noplatform +lib = os.path.abspath( os.path.join( os.path.dirname( __file__ ), "..", "lib" ) ) +sys.path.append( lib ) + +from galaxy.eggs import get_platform, get_noplatform print get_noplatform() -print get_full_platform() +print get_platform( platform=True ) diff --git a/scripts/update_database/update_database_with_security_libraries.py b/scripts/update_database/update_database_with_security_libraries.py new file mode 100644 index 00000000000..bf8e2aed68b --- /dev/null +++ b/scripts/update_database/update_database_with_security_libraries.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +#Dan Blankenberg +""" +BACKUP YOUR DATABASE BEFORE RUNNING THIS SCRIPT! + +This script has been tested with Postgres, SQLite, and MySQL databases. + +This script updates a pre-security/library database with necessary schema changes and sets default roles/permissions +for users and their histories and datasets. It will reset permissions on histories and datasets if they are already set. +Due to limitations of SQLite this script is unable to add foreign keys to existing SQLite tables (foreign keys are +ignored by SQLite anyway). + +REMEMBER TO BACKUP YOUR DATABASE BEFORE RUNNING THIS SCRIPT! +""" + +import sys, os, ConfigParser, tempfile +import galaxy.app, galaxy.model +import sqlalchemy +from galaxy.model.orm import * + +assert sys.version_info[:2] >= ( 2, 4 ) + +def main(): + def print_warning( warning_text ): + print "\nWarning: %s\n" % ( warning_text ) + + print + print "The purpose of this script is to update an existing Galaxy database that does not have Library or Security settings to a version that does." + print + print "This script will do the following:" + print "1) Create new tables that do not exist in a pre-security/library database" + print "2) Alter existing tables as necessary ( add new columns, indexes, etc )" + print "3) Create a private role for each user" + print "4) Set default permissions for each user ( set as 'manage permissions' and associated with the user's private role )" + print "5) Set default permissions for each user's history that has not been purged ( set as 'manage permissions' and associated with the user's private role )" + print "6) Set permissions on all appropriate datasets ( in each user's history ) that have not been purged ( set as 'manage permissions' and associated with the user's private role )" + print + print "*** It is critically important to backup your database before you continue. ***" + print + print "If you have backed up your database and would like to run this script, enter 'yes'." + print + should_continue = raw_input("enter 'yes' to continue>") + if should_continue.lower() != "yes": + print "Script aborted by user." + sys.exit(0) + + # Load Configuration from file + ini_file = sys.argv.pop(1) + conf_parser = ConfigParser.ConfigParser({'here':os.getcwd()}) + conf_parser.read(ini_file) + configuration = {} + for key, value in conf_parser.items("app:main"): configuration[key] = value + + # If we don't load the tools, the app will startup much faster + empty_xml = tempfile.NamedTemporaryFile() + empty_xml.write( "" ) + empty_xml.flush() + configuration['tool_config_file'] = empty_xml.name + # No need to load job runners + configuration['enable_job_running'] = False + + print + print "Loading app, with database_create_tables=False, to add Columns to existing tables" + print + # Set database_create_tables to False, then load the app + configuration['database_create_tables'] = False + app = galaxy.app.UniverseApplication( global_conf = ini_file, **configuration ) + + # Try to guess the database type that we have, in order to execute the proper raw SQL commands + if app.config.database_connection: + dialect = galaxy.model.mapping.guess_dialect_for_url( app.config.database_connection ) + else: + # default dialect is sqlite + dialect = "sqlite" + + # Now we alter existing tables, unfortunately SQLAlchemy does not support this. + # SQLite is very lacking in its implementation of the ALTER command, we'll do what we can... + # We cannot check with SA to see if new columns exist, so we will try and except (trying to + # access the table with our current SA Metadata which references missing columns will throw + # exceptions ) + + # galaxy_user table - must be altered differently depending on if we are using sqlite or not + if dialect == "sqlite": + try: + # 'true' and 'false' doesn't work properly in SQLite --> both are always True (is sqlite actually + # storing a string here, which is always true when not empty?) + app.model.session.execute( "ALTER TABLE 'galaxy_user' ADD COLUMN 'deleted' BOOLEAN default 0" ) + except sqlalchemy.exceptions.OperationalError, e: + print_warning( "adding column 'deleted' failed: %s" % ( e ) ) + try: + app.model.session.execute( "ALTER TABLE 'galaxy_user' ADD COLUMN 'purged' BOOLEAN default 0" ) + except sqlalchemy.exceptions.OperationalError, e: + print_warning( "adding column 'purged' failed: %s" % ( e ) ) + else: + try: + app.model.session.execute( "ALTER TABLE galaxy_user ADD COLUMN deleted BOOLEAN default false" ) + except ( sqlalchemy.exceptions.ProgrammingError, sqlalchemy.exceptions.OperationalError ), e: + # Postgres and MySQL raise different Exceptions for this same failure. + print_warning( "adding column 'deleted' failed: %s" % ( e ) ) + try: + app.model.session.execute( "ALTER TABLE galaxy_user ADD COLUMN purged BOOLEAN default false" ) + except ( sqlalchemy.exceptions.ProgrammingError, sqlalchemy.exceptions.OperationalError ), e: + print_warning( "adding column 'purged' failed: %s" % ( e ) ) + + # history_dataset_association table - these alters are the same, regardless if we are using sqlite + try: + app.model.session.execute( "ALTER TABLE history_dataset_association ADD COLUMN copied_from_library_folder_dataset_association_id INTEGER" ) + except ( sqlalchemy.exceptions.ProgrammingError, sqlalchemy.exceptions.OperationalError ), e: + print_warning( "adding column 'copied_from_library_folder_dataset_association_id' failed: %s" % ( e ) ) + + # metadata_file table + try: + app.model.session.execute( "ALTER TABLE metadata_file ADD COLUMN lda_id INTEGER" ) + except ( sqlalchemy.exceptions.ProgrammingError, sqlalchemy.exceptions.OperationalError ), e: + print_warning( "adding column 'lda_id' failed: %s" % ( e ) ) + + + # Create indexes for new columns in the galaxy_user and metadata_file tables + try: + i = sqlalchemy.Index( 'ix_galaxy_user_deleted', app.model.User.table.c.deleted ) + i.create() + except Exception, e: + print_warning( "Adding index failed: %s" % ( e ) ) + try: + i = sqlalchemy.Index( 'ix_galaxy_user_purged', app.model.User.table.c.purged ) + i.create() + except Exception, e: + print_warning( "Adding index failed: %s" % ( e ) ) + try: + i = sqlalchemy.Index( 'ix_metadata_file_lda_id', app.model.MetadataFile.table.c.lda_id ) + i.create() + except Exception, e: + print_warning( "Adding index failed: %s" % ( e ) ) + + + # Shutdown the app + app.shutdown() + del app + print + print "Columns added to tables, restarting app, with database_create_tables=True" + + # Restart the app, this time with create_tables == True + configuration['database_create_tables'] = True + app = galaxy.app.UniverseApplication( global_conf = ini_file, **configuration ) + + # Add foreign key constraints as necessary for new columns added above + print "Adding foreign key constraints" + if dialect != "sqlite": + try: + app.model.session.execute( "ALTER TABLE history_dataset_association ADD FOREIGN KEY (copied_from_library_folder_dataset_association_id) REFERENCES library_folder_dataset_association(id)" ) + except Exception, e: + print_warning( "Adding foreign key constraint to table has failed for an unknown reason: %s" % ( e ) ) + try: + app.model.session.execute( "ALTER TABLE metadata_file ADD FOREIGN KEY (lda_id) REFERENCES library_folder_dataset_association(id)" ) + except Exception, e: + print_warning( "Adding foreign key constraint to table has failed for an unknown reason: %s" % ( e ) ) + + else: + # SQLite ignores ( but parses on initial table creation ) foreign key constraints anyway + # See: http://www.sqlite.org/omitted.html (there is some way to set up behavior using triggers) + print_warning( "Adding foreign key constraints to table is not supported in SQLite." ) + + print "creating private roles and setting defaults for existing users and their histories and datasets" + security_agent = app.security_agent + # For each user: + # 1. make sure they have a private role + # 2. set DefaultUserPermissions + # 3. set DefaultHisstoryPermissions on existing histories + # 4. set ActionDatasetRoleAssociations on each history's activatable_datasets + default_user_action = security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS.action + + for user in app.model.User.query().all(): + print + print "################" + print "Setting up user %s." % user.email + private_role = security_agent.get_private_user_role( user ) + if not private_role: + security_agent.create_private_user_role( user ) + print "Created private role for %s" % user.email + else: + print_warning( "%s already has a private role, re-setting defaults anyway" % user.email ) + print "Setting DefaultUserPermissions for user %s" % user.email + # Delete all of the current default permissions for the user + for dup in user.default_permissions: + dup.delete() + dup.flush() + # Add the new default permissions for the user + dup = app.model.DefaultUserPermissions( user, default_user_action, private_role ) + dup.flush() + # Set DefaultHistoryPermissions on all of the user's active histories and associated datasets + # TODO: fix mapping for history.activatable_datasets so it doesn't throw an exception on the + # following query when eagerloading activatable_datasets. Fix all of the queries below that + # call history.active_datasets to be history.activatable_datasets when this works. + histories = app.model.History.filter( and_( app.model.History.table.c.user_id==user.id, + app.model.History.table.c.purged==False ) ) \ + .options( eagerload( 'active_datasets' ) ).all() + print "Setting DefaultHistoryPermissions for %d un-purged histories associated with %s" % ( len( histories ), user.email ) + for history in histories: + # Delete all of the current default permissions for the history + for dhp in history.default_permissions: + dhp.delete() + dhp.flush() + # Add the new default permissions for the history + dhp = app.model.DefaultHistoryPermissions( history, default_user_action, private_role ) + dhp.flush() + print "Setting ActionDatasetRoleAssociations for %d un-purged datasets in history %d" % ( len( history.active_datasets ), history.id ) + # Set the permissions on the current history's datasets that are not purged + for hda in history.active_datasets: + dataset = hda.dataset + if dataset.library_associations: + # Don't change permissions on a dataset associated with a library + continue + if [ assoc for assoc in dataset.history_associations if assoc.history not in user.histories ]: + # Don't change permissions on a dataset associated with a history not owned by the user + continue + # Delete all of the current permissions on the dataset + for adra in dataset.actions: + adra.delete() + adra.flush() + # Add the new permissions on the dataset + adra = app.model.ActionDatasetRoleAssociation( default_user_action, dataset, private_role ) + adra.flush() + app.shutdown() + print + print "Update finished, please review output for warnings and errors." + empty_xml.close() #close tempfile, it will automatically be deleted off system + + +if __name__ == "__main__": + main() diff --git a/scripts/update_database/update_database_with_security_libraries.sh b/scripts/update_database/update_database_with_security_libraries.sh new file mode 100644 index 00000000000..65013437651 --- /dev/null +++ b/scripts/update_database/update_database_with_security_libraries.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# This script must be executed from the $UNIVERSE_HOME directory +# e.g., sh ./scripts/update_database/update_database_with_security_libraries.sh + +. ./scripts/get_python.sh +. ./setup_paths.sh + +$GALAXY_PYTHON ./scripts/update_database/update_database_with_security_libraries.py ./universe_wsgi.ini $@ diff --git a/setup.sh b/setup.sh index 1c6d6944be5..b0f504ccac9 100644 --- a/setup.sh +++ b/setup.sh @@ -27,6 +27,7 @@ database/compiled_templates database/job_working_directory database/import database/pbs +static/genetrack/plots " for sample in $SAMPLES; do diff --git a/static/genetrack/genetrack.css b/static/genetrack/genetrack.css new file mode 100644 index 00000000000..668300be06d --- /dev/null +++ b/static/genetrack/genetrack.css @@ -0,0 +1,78 @@ + +body { + font-family: "Trebuchet MS", Arial, tahoma, sans-serif; + font-size: 14px; + line-height: 1.6em; + margin: 0; + padding: 0; + border-top: 9px solid #CCD9FF; +} + +/* Error message style */ +.error{ + background: #FFFF66; +} + +/* Error message style */ +.message{ + background: #33FF66; +} + +/* Odd data row in the table */ +.selected { + background-color: #FFFFCC; +} + +.nav_button{ + background-color:#EEEEEE; + border:1px solid; + color: #000000; +} + +.nav_button:hover{ + background-color:#000000; + border:1px solid; + color: #FFFFFF; +} + +.grey { + background-color: #EFEFEF; +} + +.odd { + background-color: #ECECEC; +} + +.even { + background-color: #FFFFFF; +} + +/* Text table style */ +.data_table { + border: 1px solid #CCCCCC; + background-color: white; +} + +/* Footer is added to every page */ +#footer { + background: #EFEFEF; + text-align:center; + padding:.2em; + border-top: 1px solid #CCD9FF; + border-bottom: 1px solid #CCD9FF; + clear: both; +} + +#footer p { + font-size:.94em; line-height:2em; color:#cccccc; margin: 0; + } + +#tag { + font-size:.80em; margin: 4px; padding: 2px; + } + + +#footer img { + vertical-align: middle; margin-left: 3px; padding-bottom: 2px; +} + diff --git a/static/genetrack/genetrack.js b/static/genetrack/genetrack.js new file mode 100644 index 00000000000..be88d107ab8 --- /dev/null +++ b/static/genetrack/genetrack.js @@ -0,0 +1,79 @@ +var cookie_name = "genetrack_ui" +var now = new Date(); +now.setTime(now.getTime() + 365 * 24 * 60 * 60 * 1000); + +// this toggles between none and block +function toggle(name){ + var elem = get(name) + if (elem) { + if (elem.style.display=="none"){ + elem.style.display="block" + setCookie(cookie_name, name, now) + } else { + elem.style.display="none" + setCookie(cookie_name, '', now) + } + + } +} + +function main(){ + //executed upon main body load + var value = getCookie(cookie_name); + toggle( value ) +} + +// this toggles between visible and hidden +function show(name){ + var elem = get(name) + if (elem.style.visibility=="hidden"){ + elem.style.visibility="visible"; + } else { + elem.style.visibility="hidden"; + } +} + +// utility function to get the length of on object +function len(obj){ + return obj.length; +} + +// utility function to get an element by id +function get(name){ + return document.getElementById(name); +} + +// pops up a window +function pop_up(url) { + day = new Date(); + id = day.getTime(); + eval("page" + id + " = window.open(url, '" + id + "', 'toolbar=0,scrollbars=1,location=0,statusbar=1,menubar=0,resizable=1,width=500,height=300');"); +} + +// +// cookie management off the web +// http://www.webreference.com/js/column8/property.html +// +function setCookie(name, value, expires, path, domain, secure) { + var curCookie = name + "=" + escape(value) + + ((expires) ? "; expires=" + expires.toGMTString() : "") + + ((path) ? "; path=" + path : "") + + ((domain) ? "; domain=" + domain : "") + + ((secure) ? "; secure" : ""); + document.cookie = curCookie; +} + +function getCookie(name) { + var dc = document.cookie; + var prefix = name + "="; + var begin = dc.indexOf("; " + prefix); + if (begin == -1) { + begin = dc.indexOf(prefix); + if (begin != 0) return null; + } else + begin += 2; + var end = document.cookie.indexOf(";", begin); + if (end == -1) + end = dc.length; + return unescape(dc.substring(begin + prefix.length, end)); +} 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..fc06c2008ff 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -142,6 +142,7 @@ div.form-row label select, input, textarea { + font: inherit; font-size: 115%; } @@ -152,7 +153,7 @@ select, textarea, input[type="text"], input[type="file"] /* Messages */ -.errormessage, .warningmessage, .donemessage, .infomessage, .welcomeBlue, .welcomeRed +.errormessage, .warningmessage, .donemessage, .infomessage, .welcomeBlue, .welcomeRed , .screencastBox, .yellowbox, .redbox, .bluebox, .greenbox { padding: 10px; padding-left: 52px; @@ -201,6 +202,85 @@ select, textarea, input[type="text"], input[type="file"] background-image: none; } +.screencastBox +{ + padding-left: 10px; + border-color: #AAAA66; + background-color: #FFFFCC; + background-image: none; +} + +.redbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #FF6666; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; + +} + +.yellowbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #FFCC00; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; +} + +.bluebox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #6666FF; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + color: #FFFFFF; + min-height: 32px; +} + +.greenbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #00CC00; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; +} + +.redbox li, .yellowbox li, .bluebox li, .greenbox li { + list-style: disc; + text-transform: none; + list-style-position: inside; + list-style-image: none; + margin: 3px; +} + + .errormessagesmall, .warningmessagesmall, .donemessagesmall, .infomessagesmall { padding: 5px; @@ -283,6 +363,11 @@ table.colored tr background: $table_row_bg; } +table.colored tr.odd_row +{ + background: $odd_row_bg; +} + div.debug { margin: 10px; @@ -350,6 +435,40 @@ 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; +} + +## State colors + +.state-color-queued { + border-color: $history_queued_border; + background: $history_queued_bg; +} + +.state-color-ok { + border-color: $history_ok_border; + background: $history_ok_bg; +} + +.state-color-error { + border-color: $history_error_border; + background: $history_error_bg; +} + +.state-color-running { + border-color: $history_running_border; + background: $history_running_bg; +} + +## Button styles + .action-button { background: #eeeeee; color: #333; @@ -365,6 +484,9 @@ ul.toolParameterExpandableCollapsable -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border-radius: 0.5em; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } .action-button > * { @@ -373,13 +495,15 @@ ul.toolParameterExpandableCollapsable .action-button:hover { color: black; - background: #aaaaaa; + background: #dddddd; } .action-button:active { color: white; background: #aaaaaa; } +## Popup menu styles + div.popupmenu { display: none; background: #eeeeee; @@ -394,6 +518,9 @@ div.popupmenu { -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border-radius: 0.5em; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } div.popupmenu-item { @@ -416,4 +543,40 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; +} + +div.permissionContainer { + padding-left: 20px; +} + +## Data grid style + +.grid { + padding-top: 1em; + border-collapse: collapse; + width: 100%; +} +.grid tbody td { + border-top: solid #DDDDDD 1px; + border-bottom: solid #DDDDDD 1px; + padding: 0.5em 1em; +} +.grid thead th { + background: $table_header_bg; + background-image: url(form_title_bg.png); + background-repeat: repeat-x; + background-position: top; + border-top: solid $table_border 1px; + border-bottom: solid $table_border 1px; + padding: 0.5em 1em; + text-align: left; +} +.grid tfoot td { + background-color: #F8F8F8; + border-top: solid #DDDDDD 1px; + border-bottom: solid #DDDDDD 1px; + padding: 0.5em 1em; +} +.grid .current { + background-color: #EEEEFF; } \ No newline at end of file diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 4de4530b076..2962cfb40b2 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; @@ -141,6 +142,7 @@ div.form-row label select, input, textarea { + font: inherit; font-size: 115%; } @@ -151,7 +153,7 @@ select, textarea, input[type="text"], input[type="file"] /* Messages */ -.errormessage, .warningmessage, .donemessage, .infomessage, .welcomeBlue, .welcomeRed +.errormessage, .warningmessage, .donemessage, .infomessage, .welcomeBlue, .welcomeRed , .screencastBox, .yellowbox, .redbox, .bluebox, .greenbox { padding: 10px; padding-left: 52px; @@ -200,6 +202,85 @@ select, textarea, input[type="text"], input[type="file"] background-image: none; } +.screencastBox +{ + padding-left: 10px; + border-color: #AAAA66; + background-color: #FFFFCC; + background-image: none; +} + +.redbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #FF6666; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; + +} + +.yellowbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #FFCC00; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; +} + +.bluebox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #6666FF; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + color: #FFFFFF; + min-height: 32px; +} + +.greenbox +{ + border: none; + padding: 10px; + border-color: #000000; + background-color: #00CC00; + background-image: none; + border-right-width: 1px; + border-right-style: dotted; + border-bottom-width: 1px; + border-bottom-style: dotted; + margin-top: 5px; + min-height: 32px; +} + +.redbox li, .yellowbox li, .bluebox li, .greenbox li { + list-style: disc; + text-transform: none; + list-style-position: inside; + list-style-image: none; + margin: 3px; +} + + .errormessagesmall, .warningmessagesmall, .donemessagesmall, .infomessagesmall { padding: 5px; @@ -282,6 +363,11 @@ table.colored tr background: white; } +table.colored tr.odd_row +{ + background: #DADFEF; +} + div.debug { margin: 10px; @@ -293,7 +379,7 @@ div.debug div.odd_row { - background: #FFFF99; + background: #DADFEF; } #footer { @@ -348,6 +434,38 @@ 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; +} + + +.state-color-queued { + border-color: #888888; + background: #EEEEEE; +} + +.state-color-ok { + border-color: #66AA66; + background: #CCFFCC; +} + +.state-color-error { + border-color: #AA6666; + background: #FFCCCC; +} + +.state-color-running { + border-color: #AAAA66; + background: #FFFFCC; +} + + .action-button { background: #eeeeee; color: #333; @@ -363,6 +481,9 @@ ul.toolParameterExpandableCollapsable -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border-radius: 0.5em; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } .action-button > * { @@ -371,13 +492,14 @@ ul.toolParameterExpandableCollapsable .action-button:hover { color: black; - background: #aaaaaa; + background: #dddddd; } .action-button:active { color: white; background: #aaaaaa; } + div.popupmenu { display: none; background: #eeeeee; @@ -392,6 +514,9 @@ div.popupmenu { -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border-radius: 0.5em; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } div.popupmenu-item { @@ -414,4 +539,39 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; +} + +div.permissionContainer { + padding-left: 20px; +} + + +.grid { + padding-top: 1em; + border-collapse: collapse; + width: 100%; +} +.grid tbody td { + border-top: solid #DDDDDD 1px; + border-bottom: solid #DDDDDD 1px; + padding: 0.5em 1em; +} +.grid thead th { + background: #ebd9b2; + background-image: url(form_title_bg.png); + background-repeat: repeat-x; + background-position: top; + border-top: solid #d8b365 1px; + border-bottom: solid #d8b365 1px; + padding: 0.5em 1em; + text-align: left; +} +.grid tfoot td { + background-color: #F8F8F8; + border-top: solid #DDDDDD 1px; + border-bottom: solid #DDDDDD 1px; + padding: 0.5em 1em; +} +.grid .current { + background-color: #EEEEFF; } \ No newline at end of file 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..6a3004ad548 --- /dev/null +++ b/static/june_2007_style/blue/library.css @@ -0,0 +1,71 @@ +.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; +} + +span.expandLink { + width: 100%; + height: 100%; + display: block; +} 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..e48f52009af 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; @@ -118,35 +131,9 @@ body right: 0px; z-index: 200; } -td.panel-header -{ - background: #f5f5f5; - background-image: url(panel_header_bg.png); - background-position: top center; - background-repeat: repeat-x; - padding: 0; - border-bottom: solid #999 1px; -} -div.panel-header -{ - margin: 0; - padding: 4px; - padding-right: 10px; - padding-left: 10px; - color: #333; - font-weight: bold; -} - -table.column-layout -{ - position: absolute; - width: 100%; - height: 100%; -} .unified-panel-header { height: 2em; - overflow: hidden; z-index: 1000; background: #cccccc; background-image: url(panel_header_bg.png); @@ -159,19 +146,21 @@ table.column-layout padding-left: 10px; color: #333; font-weight: bold; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } - + .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,50 +186,14 @@ div.unified-panel-body { color: black; background: #aaaaaa; } + .panel-header-button:active { color: white; background: #aaaaaa; } -div.popupmenu { - display: none; - background: #eee; - position: fixed; - z-index: 20000; - background: #cccccc; - background-image: url(panel_header_bg.png); - background-position: top center; - background-repeat: repeat-x; - border: solid #999 1px; - padding-top: 3px; - padding-bottom: 3px; -} - -div.popupmenu-top { - padding-bottom: 3px; -} -div.popupmenu-top-inner { - height: 10px; - width: 20px; - margin-top: -13px; - margin-left: auto; - margin-right: 20px; - background: url(popupmenu_callout_top.png); -} - -div.popupmenu-item { - padding: 3px; - padding-left: 10px; - padding-right: 10px; - cursor: pointer; -} - -div.popupmenu-item:hover { - background: #AAAAEE; -} - #overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 20000; } @@ -271,6 +224,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/tool_menu.css b/static/june_2007_style/blue/tool_menu.css index b0da725086e..b18e01624e5 100644 --- a/static/june_2007_style/blue/tool_menu.css +++ b/static/june_2007_style/blue/tool_menu.css @@ -31,15 +31,16 @@ div.toolSectionDetailsInner div.toolSectionTitle { - padding-bottom: 0px; font-weight: bold; } div.toolPanelLabel { - padding-top: 5px; + padding-top: 10px; padding-bottom: 5px; font-weight: bold; + color: gray; + text-transform: uppercase; } div.toolTitle @@ -52,9 +53,17 @@ div.toolTitle list-style: square outside; } +div.toolSectionBody div.toolPanelLabel +{ + padding-top: 5px; + padding-bottom: 5px; + margin-left: 16px; + margin-right: 10px; + display: list-item; + list-style: none outside; +} div.toolTitleNoSection { padding-bottom: 0px; - font-weight: bold; } 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..75f8678a05c --- /dev/null +++ b/static/june_2007_style/library.css.tmpl @@ -0,0 +1,71 @@ +.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; +} + +span.expandLink { + width: 100%; + height: 100%; + display: block; +} diff --git a/static/june_2007_style/make_style.py b/static/june_2007_style/make_style.py index f3d0c8a804d..5d62c7f1f56 100755 --- a/static/june_2007_style/make_style.py +++ b/static/june_2007_style/make_style.py @@ -1,7 +1,11 @@ #!/usr/bin/env python +#from galaxy import eggs +#import pkg_resources +#pkg_resources.require("Cheetah") + import sys, string, os.path -from galaxy import eggs +#from galaxy import eggs import pkg_resources pkg_resources.require( "Cheetah" ) @@ -18,8 +22,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" ) ] @@ -69,7 +73,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 @@ -79,3 +84,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..016b228a0e0 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; @@ -118,31 +131,6 @@ body right: 0px; z-index: 200; } -td.panel-header -{ - background: ${panel_header_bg_top}; - background-image: url(panel_header_bg.png); - background-position: top center; - background-repeat: repeat-x; - padding: 0; - border-bottom: solid ${layout_border} 1px; -} -div.panel-header -{ - margin: 0; - padding: 4px; - padding-right: 10px; - padding-left: 10px; - color: #333; - font-weight: bold; -} - -table.column-layout -{ - position: absolute; - width: 100%; - height: 100%; -} .unified-panel-header { height: 2em; @@ -158,6 +146,9 @@ table.column-layout padding-left: 10px; color: #333; font-weight: bold; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; } .unified-panel-header-inner { @@ -201,43 +192,6 @@ div.unified-panel-body { background: #aaaaaa; } -div.popupmenu { - display: none; - background: #eee; - position: fixed; - z-index: 20000; - background: ${panel_header_bg_bottom}; - background-image: url(panel_header_bg.png); - background-position: top center; - background-repeat: repeat-x; - border: solid ${layout_border} 1px; - padding-top: 3px; - padding-bottom: 3px; -} - -div.popupmenu-top { - padding-bottom: 3px; -} -div.popupmenu-top-inner { - height: 10px; - width: 20px; - margin-top: -13px; - margin-left: auto; - margin-right: 20px; - background: url(popupmenu_callout_top.png); -} - -div.popupmenu-item { - padding: 3px; - padding-left: 10px; - padding-right: 10px; - cursor: pointer; -} - -div.popupmenu-item:hover { - background: ${layout_hover}; -} - #overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; @@ -268,4 +222,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/june_2007_style/tool_menu.css.tmpl b/static/june_2007_style/tool_menu.css.tmpl index aa1178971a2..ef4b4ca7b28 100644 --- a/static/june_2007_style/tool_menu.css.tmpl +++ b/static/june_2007_style/tool_menu.css.tmpl @@ -31,15 +31,16 @@ div.toolSectionDetailsInner div.toolSectionTitle { - padding-bottom: 0px; font-weight: bold; } div.toolPanelLabel { - padding-top: 5px; + padding-top: 10px; padding-bottom: 5px; font-weight: bold; + color: gray; + text-transform: uppercase; } div.toolTitle @@ -52,9 +53,17 @@ div.toolTitle list-style: square outside; } +div.toolSectionBody div.toolPanelLabel +{ + padding-top: 5px; + padding-bottom: 5px; + margin-left: 16px; + margin-right: 10px; + display: list-item; + list-style: none outside; +} div.toolTitleNoSection { padding-bottom: 0px; - font-weight: bold; } diff --git a/static/scripts/galaxy.base.js b/static/scripts/galaxy.base.js index a14244dcac8..f343c31269a 100644 --- a/static/scripts/galaxy.base.js +++ b/static/scripts/galaxy.base.js @@ -1,3 +1,19 @@ +$.fn.makeAbsolute = function(rebase) { + return this.each(function() { + var el = $(this); + var pos = el.position(); + el.css({ + position: "absolute", + marginLeft: 0, marginTop: 0, + top: pos.top, left: pos.left, + right: $(window).width() - ( pos.left + el.width() ) + }); + if (rebase) { + el.remove().appendTo("body"); + } + }); +} + jQuery(document).ready( function() { // Links with confirmation jQuery( "a[@confirm]" ).click( function() { diff --git a/static/scripts/galaxy.panels.js b/static/scripts/galaxy.panels.js index 7463991fbf9..efe2ccb0a2a 100644 --- a/static/scripts/galaxy.panels.js +++ b/static/scripts/galaxy.panels.js @@ -79,7 +79,12 @@ function make_left_panel( panel_el, center_el, border_el ) { } } ).find( "div" ).show();; - + var force_panel = function( op ) { + if ( ( hidden && op == 'show' ) || ( ! hidden && op == 'hide' ) ) { + toggle(); + } + } + return { force_panel: force_panel }; }; function make_right_panel( panel_el, center_el, border_el ) { @@ -173,7 +178,12 @@ function make_right_panel( panel_el, center_el, border_el ) { } } ).find( "div" ).show(); - return { handle_minwidth_hint: handle_minwidth_hint }; + var force_panel = function( op ) { + if ( ( hidden && op == 'show' ) || ( ! hidden && op == 'hide' ) ) { + toggle(); + } + } + return { handle_minwidth_hint: handle_minwidth_hint, force_panel: force_panel }; }; // Modal dialog boxes @@ -234,4 +244,4 @@ function make_popupmenu( button_element, options ) { } ); }; $( button_element ).click( click ); -}; \ No newline at end of file +}; diff --git a/static/scripts/packed/galaxy.panels.js b/static/scripts/packed/galaxy.panels.js index 241d997a112..fd71a3f43d6 100644 --- a/static/scripts/packed/galaxy.panels.js +++ b/static/scripts/packed/galaxy.panels.js @@ -1 +1 @@ -var hidden_width=7;var border_tweak=9;var jq=jQuery;function ensure_dd_helper(){if(jq("#DD-helper").length==0){$("
").css({background:"white",opacity:0,zIndex:9000,position:"absolute",top:0,left:0,width:"100%",height:"100%"}).appendTo("body").hide()}}function make_left_panel(E,A,B){var D=false;var C=null;resize=function(F){var G=F;if(F<0){F=0}jq(E).css("width",F);jq(B).css("left",G);jq(A).css("left",F+7);if(document.recalc){document.recalc()}};toggle=function(){if(D){jq(B).removeClass("hover");jq(B).animate({left:C},"fast");jq(E).css("left",-C).show().animate({left:0},"fast",function(){resize(C);jq(B).removeClass("hidden")});D=false}else{C=jq(B).position().left;jq(A).css("left",hidden_width);if(document.recalc){document.recalc()}jq(B).removeClass("hover");jq(E).animate({left:-C},"fast");jq(B).animate({left:-1},"fast",function(){jq(this).addClass("hidden")});D=true}};jq(B).hover(function(){jq(this).addClass("hover")},function(){jq(this).removeClass("hover")}).draggable({start:function(F,G){jq("#DD-helper").show()},stop:function(F,G){jq("#DD-helper").hide();return false},drag:function(F,G){x=G.position.left;x=Math.min(400,Math.max(100,x));if(D){jq(E).css("left",0);jq(B).removeClass("hidden");D=false}resize(x);G.position.left=x;G.position.top=$(this).data("draggable").originalPosition.top},click:function(){toggle()}}).find("div").show()}function make_right_panel(A,E,G){var I=false;var F=false;var C=null;var D=function(J){jq(A).css("width",J);jq(E).css("right",J+9);jq(G).css("right",J).css("left","");if(document.recalc){document.recalc()}};var H=function(){if(I){jq(G).removeClass("hover");jq(G).animate({right:C},"fast");jq(A).css("right",-C).show().animate({right:0},"fast",function(){D(C);jq(G).removeClass("hidden")});I=false}else{C=jq(document).width()-jq(G).position().left-border_tweak;jq(E).css("right",hidden_width+1);if(document.recalc){document.recalc()}jq(G).removeClass("hover");jq(A).animate({right:-C},"fast");jq(G).animate({right:-1},"fast",function(){jq(this).addClass("hidden")});I=true}F=false};var B=function(J){var K=jq(E).width()-(I?C:0);if(K").text(F).click(G));A.append(" ")});A.show()}else{A.hide()}var A=$(".dialog-box").find(".extra_buttons").html("");if(C){$.each(C,function(F,G){A.append($("
@@ -41,7 +41,7 @@ if hist == trans.get_history(): cur_history_text = " (current history)" %> -
${i + 1}${cur_history_text}: ${hist.name}
+
${i + 1}${cur_history_text}: ${hist.name}
%endfor %if trans.get_user(): <% @@ -50,12 +50,12 @@ checked = " checked" %>
-
New history named:
+
New history named:
%endif
-
+

diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 39936d24f4c..9545dd8d0ef 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -1,146 +1,209 @@ <%inherit file="/base.mako"/> -<%def name="title()">History Item Attributes +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/library/common.mako" import="render_existing_library_item_info" /> + +<%def name="title()">Edit Dataset Attributes <%def name="datatype( dataset, datatypes )"> - + -
-
Edit Attributes
-
-
- -
- -
- -
-
-
-
- -
- -
-
-
- %for name, spec in data.metadata.spec.items(): - %if spec.visible: +<% + if isinstance( data, trans.app.model.HistoryDatasetAssociation ): + id_name = 'id' + elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + id_name = 'lid' + lda_source, library_source = data.source_library_dataset +%> + +%if library_source: + ${render_msg( 'You are currently viewing a Dataset from a library, you can go here to view versions.' % ( h.url_for( controller='library', action='library_dataset', id=library_source.id, refer_id=lda_source.id ) ), 'info' )} +%endif + +%if edit_allowed: +
+
Edit Attributes
+
+ +
- -
- ${data.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. -
-
- %if data.missing_meta(): -
Required metadata values are missing. Some of these values may not be editable by the user. Selecting "Auto-detect" will attempt to fix these values.
- %endif -
-
- -

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

-
Convert to new format
-
-
- -
- +
+ +
+ +
+
+
+ %for name, spec in data.metadata.spec.items(): + %if spec.visible: +
+ +
+ ${data.metadata.get_html_by_name( name )} +
+
+
+ %endif + %endfor +
+ +
+ +
+
- +
-
- This will create a new dataset with the contents of this - dataset converted to a new format. + This will inspect the dataset and attempt to correct the above column values if they are not accurate.
+
+ %if data.missing_meta(): +
Required metadata values are missing. Some of these values may not be editable by the user. Selecting "Auto-detect" will attempt to fix these values.
+ %endif +
+
+

+ %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. +
+
+
+
+ +
+
+
+
+

+ %endif + %endif +

+
Change data type
+
+
+ +
+ +
+ ${datatype( data, 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. +
+
+
+
+ +
+
+
+
+

+%else: +

+
View Attributes
+
+
+ Name: ${data.name}
-
-
- -
- -
-
- -

- %endif + Info: ${data.info} +

+ Data Format: ${data.ext} +
+ %for name, spec in data.metadata.spec.items(): + %if spec.visible: + ${spec.desc}: + %if spec.unwrap( spec.get( name ) ): + ${spec.unwrap( spec.get( name ) )} + %else: + ${spec.no_value} + %endif +
+ %endif + %endfor +
+
+
+

+%endif - -

-
Change data type
-
-
- -
- -
- ${datatype( data, datatypes )} +%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, data.name, 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
- -
- 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. -
-
-
-
- -
- -
-
- -

-

-
Copy History Item
-
- Click here to make a copy of this history item. -
-
-

+
+
+%endif +

+${render_existing_library_item_info( data )} +

+%if isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + ${render_msg( 'This is a Library Dataset, you can click here to import it into your history.' % ( h.url_for( controller='library', action='import_datasets', import_ids=data.id, do_action='add' ) ), 'info' )} +%endif diff --git a/templates/dataset/security_common.mako b/templates/dataset/security_common.mako new file mode 100644 index 00000000000..a57b658df5d --- /dev/null +++ b/templates/dataset/security_common.mako @@ -0,0 +1,107 @@ +<%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:
+
+ +
+
+ + +## Any permission ( e.g., 'DATASET_ACCESS' ) included in the do_not_render param will not be rendered on the page. +<%def name="render_permission_form( obj, obj_name, form_url, id_name, id, all_roles, do_not_render=[] )"> + <% + permitted_actions = trans.app.model.Dataset.permitted_actions.items() + if isinstance( obj, trans.app.model.User ): + current_actions = obj.default_permissions + obj_str = 'user %s' % obj_name + elif isinstance( obj, trans.app.model.History ): + current_actions = obj.default_permissions + obj_str = 'history %s' % obj_name + elif isinstance( obj, trans.app.model.Dataset ): + current_actions = obj.actions + obj_str = obj_name + elif isinstance( obj, trans.app.model.LibraryFolderDatasetAssociation ): + current_actions = obj.actions + obj.dataset.actions + obj_str = obj_name + permitted_actions = permitted_actions + trans.model.library_security_agent.permitted_actions.items() + elif isinstance( obj, trans.app.model.Library ): + current_actions = obj.actions + obj_str = 'library %s' % obj_name + permitted_actions = trans.model.library_security_agent.permitted_actions.items() + elif isinstance( obj, trans.app.model.LibraryDataset ): + current_actions = obj.actions + obj_str = 'library dataset %s' % obj_name + permitted_actions = trans.model.library_security_agent.permitted_actions.items() + elif isinstance( obj, trans.app.model.LibraryFolder ): + current_actions = obj.actions + obj_str = 'library folder %s' % obj_name + permitted_actions = trans.model.library_security_agent.permitted_actions.items() + else: + current_actions = obj.dataset.actions + obj_str = 'unknown object %s' %obj_name + %> + +
+
Manage permissions and role associations for ${obj_str}
+
+
+ +
+ %for k, v in permitted_actions: + %if k not in do_not_render: +
+ ${render_select( current_actions, k, v, all_roles )} +
+ %endif + %endfor +
+ +
+
+
+
+

+ diff --git a/templates/form.mako b/templates/form.mako index c003de02585..9fe2a413884 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -10,6 +10,10 @@ $(function(){ +%if header: + ${header} +%endif +

${form.title}
@@ -21,9 +25,11 @@ $(function(){ cls += " form-row-error" %>
+ %if input.use_label: + %endif
diff --git a/templates/genetrack/base.html b/templates/genetrack/base.html new file mode 100644 index 00000000000..cd381d54b93 --- /dev/null +++ b/templates/genetrack/base.html @@ -0,0 +1,29 @@ + + + + +${self.title()} + + + + +<%def name="title()"> + Title + + +<%def name="footer()"> + + + + + + ${self.body()} + ${self.footer()} + + + diff --git a/templates/genetrack/index.html b/templates/genetrack/index.html new file mode 100644 index 00000000000..23dee5cea67 --- /dev/null +++ b/templates/genetrack/index.html @@ -0,0 +1,75 @@ +## index.html +<%inherit file="base.html"/> +<%def name="title()"> + Index + + +

${conf.TITLE}

+ +
+ + + + + % if form.errors(): + + % endif + + + + + + + + + + + + + + + + + +
+ % for ekey, evalue in form.errors().items(): + ERROR:    ${ekey}:    ${evalue}
+ % endfor +
+ More + + Chromosome: ${form.chrom.tag()}   + Feature: ${form.feature.tag()}   + Width: ${form.zoom.tag()}   + Plot: ${form.plot.tag()}   + + + +
+ +     + +     + +     + +
+ +
+ +
+ + +
diff --git a/templates/genetrack/search.html b/templates/genetrack/search.html new file mode 100644 index 00000000000..d707a6da479 --- /dev/null +++ b/templates/genetrack/search.html @@ -0,0 +1,55 @@ +## search.html +<%! +from itertools import cycle +colors = cycle( [ 'even', 'odd' ] ) +%> + +<%inherit file="base.html"/> +<%def name="title()"> + Search + + +

Search

+ +
+
+ Search terms + + +
+
+ +% if param.word: + + % if len(query)>0: +

Showing the best ${len(query)} matches

+ + + + + % for color, row in zip(colors, query): + ${makerow(color, row)} + % endfor +
Name + Chromosome + Start:End + Type +
+ + % else: +

No results found

+ % endif + +%endif + +
+<%def name="makerow(color, row)"> + + ${row.name} + ${row.chrom} + ${row.start}:${row.end} + ${row.label.name} + + + + diff --git a/templates/history/list.mako b/templates/history/list.mako index a30e5c5f7c2..a5de3273c68 100644 --- a/templates/history/list.mako +++ b/templates/history/list.mako @@ -1,65 +1,201 @@ <%inherit file="/base.mako"/> <%def name="title()">Your saved histories -%if error_msg: -

-

${error_msg}
-
-

-%endif -%if ok_msg: -

-

${ok_msg}
-
-

+%if message: +

+

${message}
+
+

%endif -%if user.histories: -

Stored Histories

- %if show_deleted: - - %else: - - %endif -
- - - %for history in user.histories: - %if ( show_deleted and not history.purged ) or not( history.deleted ): - - - - - - +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<%def name="stylesheets()"> + + + + + +

Stored Histories

+ + + + + %if show_deleted: + + %endif + +
NameSizeLast modifiedActions
- ${history.name} - %if history == trans.get_history(): - (current history) - %endif - ${len(history.active_datasets)}${str(history.update_time)[:19]} - %if not history.deleted: - rename
- switch to
- delete
- %else: - undelete
- %endif -
+ + + + + + + + + + + + + + %for i, history in enumerate( user.histories ): + + %if ( show_deleted and not history.purged ) or not( history.deleted ): + + + + + + + + + + + + + + + + + + %endif + + %endfor + + + + + + + + + + +
Name (click to activate)Datasets (by state)StatusLast update
+ %if not history.deleted: + ${history.name} + + %else: + ${history.name} + %endif + + + <% + total_ok = sum( 1 for d in history.active_datasets if d.state == 'ok' ) + total_running = sum( 1 for d in history.active_datasets if d.state == 'running' ) + total_queued = sum( 1 for d in history.active_datasets if d.state == 'queued' ) + total_error = sum( 1 for d in history.active_datasets if d.state == 'error' ) + %> + + %if total_ok: +
${total_ok}
+ %else: +
+ %endif + + %if total_error: +
${total_error}
+ %endif + + %if total_running: +
${total_running}
+ %else: +
+ %endif + + %if total_queued: +
${total_queued}
+ %else: +
+ %endif + +
+ %if history == trans.get_history(): + active + %endif + %if history.deleted: + deleted + %endif + ${h.date.distance_of_time_in_words( history.update_time, h.date.datetime.utcnow() )} +
+ + %if not history.deleted: + rename
+ switch to
+ delete
+ %else: + undelete
+ %endif + +
+
+ For selected histories: + + + + %if show_deleted: + + %endif +
+
-%else: - You have no stored histories -%endif + + + diff --git a/templates/history/options.mako b/templates/history/options.mako index dfe3ffd9d09..bb807cb2e97 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -11,15 +11,16 @@
    %if user: -
  • Rename current history (stored as "${history.name}")
  • -
  • List previously stored histories
  • +
  • Rename current history (stored as "${history.name}")
  • +
  • List previously stored histories
  • %if len( history.active_datasets ) > 0:
  • Create a new empty history
  • %endif
  • Construct workflow from the current history
  • -
  • Share current history
+
  • Share current history
  • +
  • Change default permissions for the current history
  • %endif
  • Show deleted datasets in history
  • -
  • Delete current history
  • +
  • Delete current history
  • diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..a189523440d --- /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, trans.history.name, h.url_for(), 'id', None, trans.user.all_roles() )} +%endif diff --git a/templates/history/rename.mako b/templates/history/rename.mako index 07b9a4a9ee2..02b5f01c7c3 100644 --- a/templates/history/rename.mako +++ b/templates/history/rename.mako @@ -4,7 +4,7 @@
    Rename History
    -
    + %for history in histories: diff --git a/templates/history/share.mako b/templates/history/share.mako index 5e64e66756a..3ec8a41581e 100644 --- a/templates/history/share.mako +++ b/templates/history/share.mako @@ -1,30 +1,117 @@ <%inherit file="/base.mako"/> <%def name="title()">Share histories -
    -
    Share Histories
    -
    Current NameNew Name
    - - - %for history in histories: - - - - %endfor - - %if send_to_err: - - %endif - - -
    History Name:Number of Datasets:Share Link
    ${history.name} - - %if len(history.datasets) < 1: -
    - This history contains no data. +%if not can_change and not cannot_change: +
    +
    Share Histories
    + + + + %for history in histories: + + + + + + %endfor + + %if send_to_err: + + %endif + + +
    History Name:Number of Datasets:Share Link
    ${history.name} + %if len( history.datasets ) < 1: +
    This history contains no data.
    + %else: + ${len(history.datasets)} + %endif +
    copy link to share
    Email of User to share with:
    ${send_to_err}
    - %else: - ${len(history.datasets)} - %endif -
    copy link to share
    Email of User to share with:
    ${send_to_err}
    -
    \ 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 with which you're sharing does not have permission to access. + These datasets are shown below. Datasets that the user 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 are not authorized 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..08ae3fdb9d3 --- /dev/null +++ b/templates/library/browser.mako @@ -0,0 +1,228 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + +<% +def name_sorted( l ): + return sorted( l, lambda a, b: cmp( a.name.lower(), b.name.lower() ) ) +%> + + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + def show_folder(): + if trans.app.security_agent.check_folder_contents( trans.user, parent ) or trans.app.model.library_security_agent.show_library_item( trans.user, parent ): + return True + if not show_folder: + 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 trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, parent ) or trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_ADD, parent ) or trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MANAGE, parent ): + + %endif + + +
    + + + +
  • + %if subfolder: +
    + + + \ No newline at end of file diff --git a/templates/library/dataset_manage_list.mako b/templates/library/dataset_manage_list.mako new file mode 100644 index 00000000000..488e699d64f --- /dev/null +++ b/templates/library/dataset_manage_list.mako @@ -0,0 +1,28 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +<%def name="title()">List Datasets + +
    +
    Edit Attributes
    +
    + %for lfda in lfdas: +
    + +
    + Add info +
    + + + +
    +
    + %endfor +
    +
    diff --git a/templates/library/display_info.mako b/templates/library/display_info.mako new file mode 100644 index 00000000000..2ba781c6284 --- /dev/null +++ b/templates/library/display_info.mako @@ -0,0 +1,28 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +

    + +

    +
    Available Library Item Info
    +
    + %for template_info_element in item_info.library_item_info_template.elements: +
    + +
    + ${item_info.get_element_by_template_element( template_info_element ).contents} +
    +
    + ${template_info_element.description} +
    +
    +
    + %endfor +
    +
    diff --git a/templates/library/library_dataset.mako b/templates/library/library_dataset.mako new file mode 100644 index 00000000000..0790386930d --- /dev/null +++ b/templates/library/library_dataset.mako @@ -0,0 +1,99 @@ +<%inherit file="/base.mako"/> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/library/common.mako" import="render_existing_library_item_info" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +<%def name="title()">Edit Library Dataset Attributes + +%if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MANAGE, dataset ): + <%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + ${render_permission_form( dataset, dataset.name, h.url_for( action='library_dataset' ), 'id', dataset.id, trans.user.all_roles() )} +%endif + +%if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, dataset ): +
    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + ${dataset.library_folder_dataset_association.name} (current) + %if refered_lda == dataset.library_folder_dataset_association: + (your version) + %endif + %for expired_dataset in dataset.expired_datasets: +
    + ${expired_dataset.name} + %if refered_lda == expired_dataset: + (your version) + %endif + %endfor +
    +
    + Click here to replace this dataset with a new version. +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +%else: +
    +
    View Attributes
    +
    +
    + Name: ${dataset.name} +
    + Info: ${dataset.info} +
    + Dataset Versions: +
    + ${dataset.library_folder_dataset_association.name} (current) + %if refered_lda == dataset.library_folder_dataset_association: + (your version) + %endif + %for expired_dataset in dataset.expired_datasets: +
    + ${expired_dataset.name} + %if refered_lda == expired_dataset: + (your version) + %endif + %endfor +
    +
    +
    +
    + +

    + +%endif +

    + +${render_existing_library_item_info( dataset )} diff --git a/templates/library/manage_folder.mako b/templates/library/manage_folder.mako new file mode 100644 index 00000000000..96e4ed309ee --- /dev/null +++ b/templates/library/manage_folder.mako @@ -0,0 +1,85 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MODIFY, folder ): +

    +
    Edit folder name and description
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +

    +%endif + +%if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_MANAGE, folder ): + ${render_permission_form( folder, folder.name, h.url_for( action='folder' ), 'id', folder.id, trans.user.all_roles() )} +%endif + +%if trans.app.model.library_security_agent.allow_action( trans.user, trans.app.model.library_security_agent.permitted_actions.LIBRARY_ADD, folder ): +

    +
    Add a subfolder
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    +Click here to add a dataset. +

    +%endif + diff --git a/templates/library/new_dataset.mako b/templates/library/new_dataset.mako new file mode 100644 index 00000000000..5b7451ce182 --- /dev/null +++ b/templates/library/new_dataset.mako @@ -0,0 +1,150 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/library/common.mako" import="render_available_templates" /> + + +<% import os %> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +<% + roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all() + history = trans.get_history() +%> + +

    +
    Create a new library dataset
    +
    +
    + %if replace_dataset is not None: + +
    + You are currently selecting a new file to replace '${replace_dataset.name}'. +
    +
    + %else: + + %endif +
    + +
    + +
    +
    + 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. +
    +
    +
    +
    + +
    +
    +
    +
    +

    +%if history: +

    +
    Active datasets in your current history (${history.name})
    +
    +
    + %if replace_dataset is not None: + +
    + You are currently selecting a new file to replace '${replace_dataset.name}'. +
    +
    + %else: + + %endif + %for dataset in history.active_datasets: +
    + ${dataset.hid}: ${dataset.name} +
    + %endfor + +
    +
    +
    +%endif \ No newline at end of file diff --git a/templates/library/new_info.mako b/templates/library/new_info.mako new file mode 100644 index 00000000000..6f8006f71ee --- /dev/null +++ b/templates/library/new_info.mako @@ -0,0 +1,20 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/library/common.mako" import="render_available_templates" /> + + +<% import os %> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +<% + #should be able to associate roles with infos + #roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all() + #history = trans.get_history() +%> +

    +${render_available_templates( library_item )} + +

    diff --git a/templates/message.mako b/templates/message.mako index 551b60c4735..3e6783245e5 100644 --- a/templates/message.mako +++ b/templates/message.mako @@ -1,32 +1,44 @@ <%inherit file="/base.mako"/> <%def name="javascripts()"> -${parent.javascripts()} - + if ( parent.handle_minwidth_hint ) + { + parent.handle_minwidth_hint( -1 ); + } +

    ${message}
    + +## Render a message +<%def name="render_msg( msg, messagetype='done' )"> +
    ${msg}
    +
    + diff --git a/templates/no_access.mako b/templates/no_access.mako new file mode 100644 index 00000000000..031cafbaf5f --- /dev/null +++ b/templates/no_access.mako @@ -0,0 +1,15 @@ +<%inherit file="/base.mako"/> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +
    ${message}
    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..69c3ab49ff8 100644 --- a/templates/root/index.mako +++ b/templates/root/index.mako @@ -1,11 +1,26 @@ <%inherit file="/base_panels.mako"/> +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=True + self.active_view="analysis" +%> +%if trans.app.config.require_login and not trans.user: + +%endif + + <%def name="left_panel()">
    Tools
    - +
    @@ -13,8 +28,12 @@ ## If a specific tool id was specified, load it in the middle frame <% - if tool_id is not None: + if trans.app.config.require_login and not trans.user: + center_url = h.url_for( controller='user', action='login' ) + elif tool_id is not None: center_url = h.url_for( 'tool_runner', tool_id=tool_id, from_noframe=True ) + elif workflow_id is not None: + center_url = h.url_for( controller='workflow', action='run', id=workflow_id ) elif m_c is not None: center_url = h.url_for( controller=m_c, action=m_a ) else: @@ -29,12 +48,12 @@
    History
    - +
    - \ No newline at end of file + diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 58d8f1776db..270d9f59d3c 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -9,32 +9,65 @@ - + +
    Galaxy${brand}
    - - Info: report bugs - | wiki - | screencasts - | blog + %if not app.config.require_login or ( app.config.require_login and trans.user): + View: + analysis + | workflow + %if admin_user == "true": + | admin + %endif + + +     + %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 + %if app.config.require_login: + | logout + %else: + | logout + %endif %else: - Account: create - | login + Account: + %if app.config.allow_user_creation: + create | + %endif + login %endif %endif   +
    diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index 2bf5c2c4fb3..7fb4017a999 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -38,11 +38,9 @@ ## Render a label <%def name="render_label( label )"> -
    ${label.text}
    -
    @@ -91,7 +89,6 @@ %for key, val in toolbox.tool_panel.items(): %if key.startswith( 'tool' ): ${render_tool( val, False )} -
    %elif key.startswith( 'workflow' ): ${render_workflow( key, val, False )} %elif key.startswith( 'section' ): @@ -112,10 +109,10 @@ %endfor
    -
    %elif key.startswith( 'label' ): ${render_label( val )} %endif +
    %endfor ## Link to workflow management. The location of this may change, but eventually @@ -128,21 +125,19 @@ Workflow (beta)
    -
    -
    - Manage workflows -
    - %if t.user: - %for m in t.user.stored_workflow_menu_entries: - - %endfor - %endif +
    + Manage workflows
    + %if t.user: + %for m in t.user.stored_workflow_menu_entries: + + %endfor + %endif
    -
    + 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..45b44795f38 --- /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, trans.user.email, h.url_for(), 'id', None, trans.user.all_roles() )} +%endif diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 825c5fc401e..fe1afd7b4ee 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()} +