From fddd3b572fbfdfb3d4d4b6c9f159f7687364a730 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:44:10 -0400 Subject: [PATCH 001/267] RECOMMIT: First pass with adding role based access controls. Added roles and groups. Users can be associated with groups. Roles can be associated with roles, datasets, groups, and users. Currently the creator of a dataset can mark the dataset as private, preventing other users from viewing or utilizing this dataset, even if it's containing history is shared. This is done via the edit attributes page for a particular dataset. This requires enable_beta_features to be set. --- lib/galaxy/datatypes/images.py | 4 +- lib/galaxy/model/__init__.py | 574 ++++++++++++++++++++----- lib/galaxy/model/mapping.py | 170 ++++++++ lib/galaxy/tools/__init__.py | 4 +- lib/galaxy/tools/actions/__init__.py | 16 +- lib/galaxy/tools/actions/upload.py | 4 +- lib/galaxy/tools/parameters/basic.py | 24 +- lib/galaxy/web/controllers/async.py | 2 +- lib/galaxy/web/controllers/dataset.py | 47 +- lib/galaxy/web/controllers/root.py | 220 ++++++---- lib/galaxy/web/framework/__init__.py | 5 +- templates/dataset/edit_attributes.mako | 35 +- templates/root/history_common.mako | 4 +- 13 files changed, 875 insertions(+), 234 deletions(-) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 90d44ab47f4..7c58d38f632 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -110,7 +110,7 @@ class Gmaj( data.Data ): "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 } ) + "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, 'copy_access_from': dataset.id } ) } class_name = "edu.psu.bx.gmaj.MajApplet.class" archive = "/static/gmaj/gmaj.jar" @@ -180,7 +180,7 @@ class Laj( data.Text ): "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 } ), + "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), "noseq": "true" } class_name = "edu.psu.cse.bio.laj.LajApplet.class" diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7d08ccb2e46..0bed6bb7d19 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -27,19 +27,99 @@ def set_datatypes_registry( d_registry ): datatypes_registry = d_registry class User( object ): - def __init__( self, email=None, password=None ): + def __init__( self, email=None, password=None, groups = [], roles = [], default_groups = [], default_roles = [] ): self.email = email self.password = password self.external = False # Relationships self.histories = [] + if not groups: + groups.append( GalaxyGroup.get( GalaxyGroup.public_id ) ) + default_groups.append( groups[-1] ) + default_groups.append( self.create_private_group() ) + group_id_added = [] + for group in groups: + if group.id not in group_id_added: + group.add_user( self ) + group_id_added.append( group.id ) + group_id_added = [] + for group in default_groups: + if group.id not in group_id_added: + user_group_assoc = DefaultUserGroupAssociation( self, group ) + user_group_assoc.flush() + group_id_added.append( group.id ) + role_id_added = [] + for role in roles: + if role.id not in role_id_added: + role.add_user( self ) + role_id_added.append( role.id ) + role_id_added = [] + for role in default_roles: + if role.id not in role_id_added: + role_group_assoc = DefaultUserRoleAssociation( self, role ) + role_group_assoc.flush() + role_id_added.append( role.id ) 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 create_private_group( self ): + #create private group + group = GalaxyGroup( self.email, priority = 10 ) + group.flush() + #create private dataset access role + role = AccessRole( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ), priority = 1 ) + role.flush() + #add role to group + group.add_role( role ) + #create roles for user modification of role + user_role = AccessRole( "%s role modification" % self.email, list( AccessRole.access_actions.__dict__.values() ) ) + user_role.flush() + #add role to user + user_role.add_user( self ) + #add role to role + role.add_role( user_role ) + + #create roles for user modification of group + group_role = AccessRole( "%s group modification" % self.email, list( GalaxyGroup.access_actions.__dict__.values() ) ) + group_role.flush() + #add role to group + group.add_access_role( group_role ) + #associate role and user + group_role.add_user( self ) + + #add user to group + group.add_user( self ) + group.flush() + return group + def add_group( self, group ): + return group.add_user( self ) + def has_group( self, check_group ): + return bool( UserGroupAssociation.get_by( group_id = check_group.id, user_id = self.id ) ) + def has_role( self, check_role ): + return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) + def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): + if groups is not None: + for assoc in self.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = DefaultUserGroupAssociation( self, group ) + assoc.flush() + if roles is not None: + for assoc in self.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = DefaultUserRoleAssociation( self, role ) + assoc.flush() + if history: + for history in self.histories: + history.set_default_access( groups = groups, roles = roles, dataset = dataset ) + class Job( object ): """ A job represents a request to run a tool given input datasets, tool @@ -101,10 +181,338 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset +class AccessRole( object ): + dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading + USE = 'dataset_use', #use in jobs + ADD_ROLE = 'dataset_add_role', #dataset can be added to roles + REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles + ADD_GROUP = 'dataset_add_group', #dataset can be added to groups + REMOVE_GROUP = 'dataset_remove_group' ) #dataset can be removed from groups + role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset + REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset + DELETE = 'role_delete', #delete a role + MODIFY = 'role_modify', #change a role's actions, + ADD_GROUP = 'role_add_group', #add role to a group + REMOVE_GROUP = 'role_remove_group' ) #remove role from a group + group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset + REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group + DELETE = 'group_delete', #delete a group + ADD_ROLE = 'group_add_role', #add role to group + REMOVE_ROLE = 'group_remove_role', #remove role from group + ADD_USER = 'group_add_user' ) #add users to group + + access_actions = role_actions + + def __init__( self, name, actions, priority = 0 ): + self.name = name + if not isinstance( actions, list ): + actions = [ actions ] + self.actions = actions + self.priority = priority + def add_user( self, user ): + assoc = UserRoleAssociation( user, self ) + assoc.flush() + return assoc + def add_group( self, group ): + assoc = GroupRoleAssociation( group, self ) + assoc.flush() + return assoc + def add_role( self, role ): + assoc = RoleRoleAssociation( role, self ) + assoc.flush() + return assoc + def add_dataset( self, dataset ): + assoc = RoleDatasetAssociation( self, dataset ) + assoc.flush() + return assoc + +class GalaxyGroup( object ): + public_id = None + access_actions = AccessRole.group_actions + def __init__( self, name, priority = 0 ): + self.name = name + self.priority = priority + def add_user( self, user ): + assoc = UserGroupAssociation( user, self ) + assoc.flush() + return assoc + def add_role( self, role ): + return role.add_group( self ) + def add_access_role( self, role ): + assoc = GroupRoleAccessAssociation( self, role ) + assoc.flush() + return assoc + def add_dataset( self, dataset ): + assoc = GroupDatasetAssociation( self, dataset ) + assoc.flush() + return assoc + +class UserGroupAssociation( object ): + def __init__( self, user, group ): + self.user = user + self.group = group + +class RoleRoleAssociation( object ): + def __init__( self, role, target_role ): + self.role = role + self.target_role = target_role + +class GroupRoleAccessAssociation( object ): + def __init__( self, group, role ): + self.group = group + self.role = role + +class GroupRoleAssociation( object ): + def __init__( self, group, role ): + self.group = group + self.role = role + +class UserRoleAssociation( object ): + def __init__( self, user, role ): + self.user = user + self.role = role + +class GroupDatasetAssociation( object ): + def __init__( self, group, dataset ): + if isinstance( group, GroupDatasetAssociation ) or isinstance( group, DefaultUserGroupAssociation ) or isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.group = group + + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset + self.dataset = dataset + +class RoleDatasetAssociation( object ): + def __init__( self, role, dataset ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.role = role + + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset + self.dataset = dataset + +class DefaultUserRoleAssociation( object ): + def __init__( self, user, role ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.user = user + self.role = role + +class DefaultUserGroupAssociation( object ): + def __init__( self, user, group ): + if isinstance( group, GroupDatasetAssociation ) or isinstance( group, DefaultUserGroupAssociation ) or isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.user = user + self.group = group + +class DefaultHistoryRoleAssociation( object ): + def __init__( self, history, role ): + if isinstance( role, RoleDatasetAssociation ) or isinstance( role, DefaultUserRoleAssociation ) or isinstance( role, DefaultHistoryRoleAssociation ): + role = role.role + self.history = history + self.role = role + +class DefaultHistoryGroupAssociation( object ): + def __init__( self, history, group ): + if isinstance( group, GroupDatasetAssociation ) or isinstance( group, DefaultUserGroupAssociation ) or isinstance( group, DefaultHistoryGroupAssociation ): + group = group.group + self.history = history + self.group = group + +class Dataset( object ): + states = Bunch( NEW = 'new', + QUEUED = 'queued', + RUNNING = 'running', + OK = 'ok', + EMPTY = 'empty', + ERROR = 'error', + DISCARDED = 'discarded' ) + access_actions = AccessRole.dataset_actions + file_path = "/tmp/" + engine = None + def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True, access_groups=[], access_roles=[] ): + self.id = id + self.state = state + self.deleted = False + self.purged = False + self.purgable = purgable + self.external_filename = external_filename + self._extra_files_path = extra_files_path + self.file_size = file_size + if access_groups or access_roles: + #self.flush() + for group in access_groups: + group.add_dataset( self ) + group.flush() + for role in access_roles: + role.add_dataset( self ) + role.flush() + def get_file_name( self ): + if not self.external_filename: + assert self.id is not None, "ID must be set before filename used (commit the object)" + # First try filename directly under file_path + filename = os.path.join( self.file_path, "dataset_%d.dat" % self.id ) + # Only use that filename if it already exists (backward compatibility), + # otherwise construct hashed path + if not os.path.exists( filename ): + dir = os.path.join( self.file_path, *directory_hash_id( self.id ) ) + # Create directory if it does not exist + try: + os.makedirs( dir ) + except OSError, e: + # File Exists is okay, otherwise reraise + if e.errno != errno.EEXIST: + raise + # Return filename inside hashed directory + return os.path.abspath( os.path.join( dir, "dataset_%d.dat" % self.id ) ) + else: + filename = self.external_filename + # Make filename absolute + return os.path.abspath( filename ) + + def set_file_name ( self, filename ): + if not filename: + self.external_filename = None + else: + self.external_filename = filename + + file_name = property( get_file_name, set_file_name ) + + @property + def extra_files_path( self ): + if self._extra_files_path: + path = self._extra_files_path + else: + path = os.path.join( self.file_path, "dataset_%d_files" % self.id ) + #only use path directly under self.file_path if it exists + if not os.path.exists( path ): + path = os.path.join( os.path.join( self.file_path, *directory_hash_id( self.id ) ), "dataset_%d_files" % self.id ) + # Make path absolute + return os.path.abspath( path ) + + def get_size( self ): + """Returns the size of the data on disk""" + if self.file_size: + return self.file_size + else: + try: + return os.path.getsize( self.file_name ) + except OSError: + return 0 + def set_size( self ): + """Returns the size of the data on disk""" + try: + self.file_size = os.path.getsize( self.file_name ) + except OSError: + self.file_size = 0 + def has_data( self ): + """Detects whether there is any data""" + return self.get_size() > 0 + def mark_deleted( self, include_children=True ): + self.deleted = True + def allow_action( self, user, action ): + """Returns true when user has permission to perform an action""" + + #if dataset is in public group, we always return true for viewing and using + #this may need to change when the ability to alter groups and roles is allowed + if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = GalaxyGroup.public_id, dataset_id = self.id ): + return True + elif user is not None: + #loop through permissions and if allowed return true: + #check roles associated directly with dataset first + for role_dataset_assoc in self.roles: + if action in role_dataset_assoc.role.actions and user.has_role( role_dataset_assoc.role ): + return True + #check roles associated with dataset through groups + for group_dataset_assoc in self.groups: + if user.has_group( group_dataset_assoc.group ): + for group_role_assoc in group_dataset_assoc.group.roles: + if action in group_role_assoc.role.actions: + return True + return False #no user and dataset not in public group, or user lacks permission + def guess_derived_groups_roles( self, other_datasets = [] ): + """Returns a list of output roles and groups based upon itself and provided datasets""" + if not other_datasets: + return [ data_group_assoc.group for data_group_assoc in self.groups ], [ data_role_assoc.role for data_role_assoc in self.roles ] + access_roles = None + priority_access_role = None + access_groups = None + priority_access_group = None + for dataset in [ self ] + other_datasets: + #determine access roles and groups for output datasets + #roles and groups for output dataset is the intersection across all inputs + #if we end up with no intersection between inputs, then we rely on priorities + if isinstance( dataset, HistoryDatasetAssociation ): + dataset = dataset.dataset + roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] + for role in roles: + if priority_access_role is None or priority_access_role.priority < role.priority: + priority_access_role = role + groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] + for group in groups: + if priority_access_group is None or priority_access_group.priority < group.priority: + priority_access_group = group + if access_roles is None: + access_roles = set( roles ) + access_groups = set( groups ) + else: + access_roles.intersection_update( set( roles ) ) + access_groups.intersection_update( set( groups ) ) + + #complete lists for output dataset access + if access_roles: + access_roles = list( access_roles ) + else: + access_roles = [] + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + #if we have no roles or groups left after intersection, + #take the highest priority group or role + if not access_roles and not access_groups: + if priority_access_role and priority_access_group: + if priority_access_group.priority == priority_access_role.priority: + access_groups = [ priority_access_group ] + access_roles = [ priority_access_role ] + elif priority_access_group.priority > priority_access_role.priority: + access_groups = [ priority_access_group ] + else: + access_roles = [ priority_access_role ] + elif priority_access_role: + access_roles = [ priority_access_role ] + elif priority_access_group: + access_groups = [ priority_access_group ] + + return access_groups, access_roles + def add_group( self, group ): + return group.add_dataset( self ) + def add_role( self, role ): + return role.add_dataset( self ) + + def has_group( self, group ): + return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) + def has_role( self, role ): + return bool( RoleDatasetAssociation.get_by( role_id = role.id, dataset_id = self.id ) ) + + # FIXME: sqlalchemy will replace this + def _delete(self): + """Remove the file that corresponds to this data""" + try: + os.remove(self.data.file_name) + except OSError, e: + log.critical('%s delete error %s' % (self.__class__.__name__, e)) + + + class HistoryDatasetAssociation( object ): + states = Dataset.states + access_actions = Dataset.access_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, copied_from_history_dataset_association = None, validation_errors=None, visible=True, create_dataset = False ): + parent_id=None, copied_from_history_dataset_association = None, validation_errors=None, + visible=True, create_dataset = False, access_groups = [], access_roles = [] ): self.name = name or "Unnamed dataset" self.id = id self.hid = hid @@ -120,7 +528,7 @@ class HistoryDatasetAssociation( object ): # Relationships self.history = history if not dataset and create_dataset: - dataset = Dataset() + dataset = Dataset( access_groups = access_groups, access_roles = access_roles ) dataset.flush() self.dataset = dataset self.parent_id = parent_id @@ -131,10 +539,6 @@ class HistoryDatasetAssociation( object ): 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 ): @@ -252,7 +656,8 @@ class HistoryDatasetAssociation( object ): def get_converter_types(self): return self.datatype.get_converter_types( self, datatypes_registry) - def copy( self, copy_children = False, parent_id = None ): + def copy( self, copy_children = False, parent_id = None, target_user = None ): + if target_user is None: target_user = self.user des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) des.flush() if copy_children: @@ -274,10 +679,12 @@ class HistoryDatasetAssociation( object ): for child in self.children: child.mark_deleted() + def allow_action( self, user, action ): + return self.dataset.allow_action( user, action ) class History( object ): - def __init__( self, id=None, name=None, user=None ): + def __init__( self, id=None, name=None, user=None, default_roles = [], default_groups = [] ): self.id = id self.name = name or "Unnamed history" self.deleted = False @@ -288,6 +695,18 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] + if not default_roles: + if user: + default_roles = user.default_roles + if not default_groups: + if user: + default_groups = user.default_groups + else: + default_groups = [ GalaxyGroup.get( GalaxyGroup.public_id ) ] + + + self.set_default_access( roles = default_roles, groups = default_groups ) + def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -326,18 +745,55 @@ class History( object ): self.genome_build = genome_build self.datasets.append( dataset ) - def copy(self): - des = History() + 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 - des.user_id = self.user_id for data in self.datasets: - new_data = data.copy( copy_children = True ) + new_data = data.copy( copy_children = True, target_user = target_user ) des.add_dataset( new_data ) new_data.flush() des.hid_counter = self.hid_counter des.flush() return des + + def set_default_access( self, groups = None, roles = None, dataset = False ): + if groups is not None: + for assoc in self.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = DefaultHistoryGroupAssociation( self, group ) + assoc.flush() + if roles is not None: + for assoc in self.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = DefaultHistoryRoleAssociation( self, role ) + assoc.flush() + if dataset: + for data in self.datasets: + for hda in data.dataset.history_associations: + if self.user and hda.history not in self.user.histories: + break + else: + if groups is not None: + for assoc in data.dataset.groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + group.add_dataset( data ) + if roles is not None: + for assoc in data.dataset.roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + role.add_dataset( data ) + + # class Query( object ): # def __init__( self, name=None, state=None, tool_parameters=None, history=None ): @@ -348,98 +804,6 @@ class History( object ): # self.history = history # self.datasets = [] -class Dataset( object ): - states = Bunch( NEW = 'new', - QUEUED = 'queued', - RUNNING = 'running', - OK = 'ok', - EMPTY = 'empty', - ERROR = 'error', - DISCARDED = 'discarded' ) - file_path = "/tmp/" - engine = None - def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): - self.id = id - self.state = state - self.deleted = False - self.purged = False - self.purgable = purgable - self.external_filename = external_filename - self._extra_files_path = extra_files_path - self.file_size = file_size - - def get_file_name( self ): - if not self.external_filename: - assert self.id is not None, "ID must be set before filename used (commit the object)" - # First try filename directly under file_path - filename = os.path.join( self.file_path, "dataset_%d.dat" % self.id ) - # Only use that filename if it already exists (backward compatibility), - # otherwise construct hashed path - if not os.path.exists( filename ): - dir = os.path.join( self.file_path, *directory_hash_id( self.id ) ) - # Create directory if it does not exist - try: - os.makedirs( dir ) - except OSError, e: - # File Exists is okay, otherwise reraise - if e.errno != errno.EEXIST: - raise - # Return filename inside hashed directory - return os.path.abspath( os.path.join( dir, "dataset_%d.dat" % self.id ) ) - else: - filename = self.external_filename - # Make filename absolute - return os.path.abspath( filename ) - - def set_file_name ( self, filename ): - if not filename: - self.external_filename = None - else: - self.external_filename = filename - - file_name = property( get_file_name, set_file_name ) - - @property - def extra_files_path( self ): - if self._extra_files_path: - path = self._extra_files_path - else: - path = os.path.join( self.file_path, "dataset_%d_files" % self.id ) - #only use path directly under self.file_path if it exists - if not os.path.exists( path ): - path = os.path.join( os.path.join( self.file_path, *directory_hash_id( self.id ) ), "dataset_%d_files" % self.id ) - # Make path absolute - return os.path.abspath( path ) - - def get_size( self ): - """Returns the size of the data on disk""" - if self.file_size: - return self.file_size - else: - try: - return os.path.getsize( self.file_name ) - except OSError: - return 0 - def set_size( self ): - """Returns the size of the data on disk""" - try: - self.file_size = os.path.getsize( self.file_name ) - except OSError: - self.file_size = 0 - def has_data( self ): - """Detects whether there is any data""" - return self.get_size() > 0 - def mark_deleted( self, include_children=True ): - self.deleted = True - - # FIXME: sqlalchemy will replace this - def _delete(self): - """Remove the file that corresponds to this data""" - try: - os.remove(self.data.file_name) - except OSError, e: - log.critical('%s delete error %s' % (self.__class__.__name__, e)) - class Old_Dataset( Dataset ): pass diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cdc8419c439..335ab183dd5 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -114,6 +114,99 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) +GalaxyGroup.table = Table( "galaxy_group", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "priority", Integer ) ) + +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 ) ) + +AccessRole.table = Table( "access_role", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "actions", JSONType(), default=[] ), + Column( "priority", Integer ) ) + +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( "access_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( "access_role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.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 ) ) + +RoleDatasetAssociation.table = Table( "role_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + +RoleRoleAssociation.table = Table( "role_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "target_role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupRoleAccessAssociation.table = Table( "group_role_access_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.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 ) ) + + +DefaultUserRoleAssociation.table = Table( "default_user_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +DefaultUserGroupAssociation.table = Table( "default_user_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +DefaultHistoryRoleAssociation.table = Table( "default_history_role_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +DefaultHistoryGroupAssociation.table = Table( "default_history_group_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "history_id", Integer, ForeignKey( "history.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 ), @@ -298,6 +391,56 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) +assign_mapper( context, GalaxyGroup, GalaxyGroup.table, + properties=dict( users=relation( UserGroupAssociation ), + datasets=relation( GroupDatasetAssociation ) ) ) + +assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, + properties=dict( user=relation( User, backref = "groups" ), + group=relation( GalaxyGroup, backref = "users" ) ) ) + +assign_mapper( context, UserRoleAssociation, UserRoleAssociation.table, + properties=dict( role=relation( AccessRole, backref = "users" ), + user=relation( User, backref = "roles" ) ) ) + +assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, + properties=dict( role=relation( AccessRole, backref = "groups" ), + group=relation( GalaxyGroup, backref = "roles" ) ) ) + +assign_mapper( context, AccessRole, AccessRole.table ) + +assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "groups" ), + group=relation( GalaxyGroup, backref = "datasets" ) ) ) + +assign_mapper( context, RoleDatasetAssociation, RoleDatasetAssociation.table, + properties=dict( dataset=relation( Dataset, backref = "roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, RoleRoleAssociation, RoleRoleAssociation.table, + properties=dict( role=relation( AccessRole, primaryjoin=( ( RoleRoleAssociation.table.c.role_id == AccessRole.table.c.id ) ) ), + target_role=relation( AccessRole, primaryjoin=( RoleRoleAssociation.table.c.target_role_id == AccessRole.table.c.id ), backref="roles" ) ) ) + +assign_mapper( context, GroupRoleAccessAssociation, GroupRoleAccessAssociation.table, + properties=dict( role=relation( AccessRole, backref="access_groups" ), + group=relation( GalaxyGroup, backref="access_roles" ) ) ) + +assign_mapper( context, DefaultUserRoleAssociation, DefaultUserRoleAssociation.table, + properties=dict( user=relation( User, backref = "default_roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, + properties=dict( user=relation( User, backref = "default_groups" ), + group=relation( GalaxyGroup ) ) ) + +assign_mapper( context, DefaultHistoryRoleAssociation, DefaultHistoryRoleAssociation.table, + properties=dict( history=relation( History, backref = "default_roles" ), + role=relation( AccessRole ) ) ) + +assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, + properties=dict( history=relation( History, backref = "default_groups" ), + group=relation( GalaxyGroup ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -411,6 +554,33 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) result.context = context result.create_tables = create_tables + #set up default table entries here, currently only exist for access controls + if result.AccessRole.count() == 0: + log.warning( "There were no access roles located, setting up default (public) access roles." ) + #create public group + public_group = result.GalaxyGroup( 'public' ) + public_group.flush() + #create public_all role + public_role = result.AccessRole( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.GalaxyGroup.access_actions.ADD_DATASET, result.GalaxyGroup.access_actions.REMOVE_DATASET ] ) + public_role.flush() + public_group.add_role( public_role ) + + #store public group id + GalaxyGroup.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions + #add all datasets to public group + for dataset in result.Dataset.select(): + public_group.add_dataset( dataset ) + + #loop through all current users and associate with the public group + #and create and associate with user's own group + for user in result.User.select(): + public_group.add_user( user ) + private_group = user.create_private_group() + user.set_default_access( groups = [ public_group, private_group ], roles = [], history = True, dataset = True ) + else: + #retrieve from database and store public group id, assume first created group is public + GalaxyGroup.public_id = result.GalaxyGroup.select( order_by = asc( result.GalaxyGroup.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions + log.debug( "Public Group identified as id = %s." % ( GalaxyGroup.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index a56e6a829b4..9d21d33f803 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1084,7 +1084,7 @@ class Tool: if visible == "visible": visible = True 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 ) + child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1120,7 +1120,7 @@ class Tool: else: visible = False 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 ) + primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) 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 f2bb0cd8185..058caf07203 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,6 +43,8 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break + if data and not data.allow_action( trans.user, data.access_actions.USE ): + 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 ): @@ -79,6 +81,16 @@ class DefaultToolAction( object ): data = NoneDataset( datatypes_registry = trans.app.datatypes_registry ) if data.dbkey not in [None, '?']: input_dbkey = data.dbkey + + #determine output dataset access list + existing_datasets = [ inp for inp in inp_data.values() if inp ] + if existing_datasets: + output_access_groups, output_access_roles = existing_datasets[0].dataset.guess_derived_groups_roles( existing_datasets[1:] ) + else: + #no valid inputs, we will use history defaults + output_access_roles = [ role.role for role in trans.history.default_roles ] + output_access_groups = [ group.group for group in trans.history.default_groups ] + # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -117,7 +129,7 @@ class DefaultToolAction( object ): ext = output.format if ext == "input": ext = input_ext - data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) + data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True, access_groups=output_access_groups, access_roles=output_access_roles ) # Commit the dataset immediately so it gets database assigned unique id data.flush() # Create an empty file immediately @@ -183,6 +195,8 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: + if not dataset.allow_action( trans.user, dataset.access_actions.USE ): + 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 ) diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index b3904436a63..5c68786b6b1 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,7 +65,7 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) + data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -158,7 +158,7 @@ class UploadToolAction( object ): if info is None: info = 'uploaded %s file' %data_type - data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) 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 b10b51bdb5e..212a691d6aa 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -979,19 +979,25 @@ class DataToolParameter( ToolParameter ): 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.model import History, HistoryDatasetAssociation, User, AccessRole, GalaxyGroup, GroupRoleAssociation >>> 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 ) ) + >>> role = AccessRole( 'test', list( AccessRole.dataset_actions.__dict__.values() ) ) + >>> role.flush() + >>> group = GalaxyGroup( 'test' ) + >>> group.flush() + >>> GalaxyGroup.public_id = group.id + >>> GroupRoleAssociation( group, role ).flush() + >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True, access_groups=[ group ] ) ) + >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True, access_groups=[ group ] ) ) >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah - >>> print p.get_html( trans=Bunch( history=hist ) ) + >>> print p.get_html( trans=Bunch( history=hist, user=None ) ) +
+ + <% checked = "" %> + %if not data.dataset.has_group( trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id ) ): + <% checked = " checked" %> + %endif +
+ +
+
+
+ This will prevent other users from viewing or utilizing this dataset, even if you share your history with them. +
+
+
+
+ +
+ + + +%endif diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 877705614ed..d7093121868 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,9 @@ ## Body for history items, extra info and actions, data "peek"
- %if data_state == "queued": + %if not data.allow_action( trans.user, data.access_actions.VIEW ): +
You do not have permision to view this dataset.
+ %elif data_state == "queued":
Job is waiting to run
%elif data_state == "running":
Job is currently running
From 8fd301c2e66af7f5e95da7559c93b28961ded7a5 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:45:11 -0400 Subject: [PATCH 002/267] RECOMMIT: Allow users to change the default permissions assigned to new datasets (not relying on input) for their current history. --- lib/galaxy/web/controllers/history.py | 44 +++++++++++++++++++++++++++ templates/history/options.mako | 1 + templates/history/permissions.mako | 42 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 lib/galaxy/web/controllers/history.py create mode 100644 templates/history/permissions.mako diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py new file mode 100644 index 00000000000..6483b94850c --- /dev/null +++ b/lib/galaxy/web/controllers/history.py @@ -0,0 +1,44 @@ +from galaxy.web.base.controller import * +import logging + +log = logging.getLogger( __name__ ) + +class HistoryController( BaseController ): + @web.expose + def index( self, trans, **kwd ): + raise 'Unimplemented' + + @web.expose + def set_default_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the current history""" + #TODO: allow changing of default roles associated with history + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions form""" + history = trans.get_history() + group_in = [] + group_out = [] + #collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in history.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + history.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) + 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." ) diff --git a/templates/history/options.mako b/templates/history/options.mako index 4faa967742d..81cf68f30d6 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,6 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • +
  • Change default permissions for the current history
  • %endif
  • Share current history
  • %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako new file mode 100644 index 00000000000..315609189cd --- /dev/null +++ b/templates/history/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permissions + +%if trans.user: +
    +
    Change Default History Permissions
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permissions assigned to new datasets for your current history. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file From 250c5ae7595566fad5c5ecab3bc01465292a5352 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:47:06 -0400 Subject: [PATCH 003/267] RECOMMIT: Allow users to specify the default permissions assigned to new histories. --- lib/galaxy/web/controllers/user.py | 34 ++++++++++++++++++++++++ templates/user/index.mako | 3 +++ templates/user/permissions.mako | 42 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 templates/user/permissions.mako diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 41890611abc..bc3801137d2 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -166,3 +166,37 @@ class User( BaseController ): return trans.show_form( web.FormBuilder( web.url_for(), "Reset Password", submit_text="Submit" ) .add_text( "email", "Email", value=email, error=error ) ) + + @web.expose + def set_default_permissions( self, trans, **kwd ): + """Sets the user's default permissions for the new histories""" + #TODO: allow changing of default roles + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions form""" + group_in = [] + group_out = [] + #collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in trans.user.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + trans.user.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default new history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to new history's default permissions." ) + 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 permissions." ) diff --git a/templates/user/index.mako b/templates/user/index.mako index 2b11262cece..64d23dd2410 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -8,6 +8,9 @@ %else: diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako new file mode 100644 index 00000000000..1b0803d44df --- /dev/null +++ b/templates/user/permissions.mako @@ -0,0 +1,42 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Change Default History Permissions + +%if trans.user: +
    +
    Change Default Permissions for new Histories
    +
    +
    +
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> +
    + + + %for group in user_groups: + + %endfor +
    GroupInOut
    ${group.name}
    +
    + +
    + +
    + This will change the default permissions assigned to new datasets for new histories. +
    +
    +
    +
    + +
    +
    +
    +
    +%endif \ No newline at end of file From 80e56db29da96f851dcff768494987d4ac0b47ef Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 1 Aug 2008 15:47:57 -0400 Subject: [PATCH 004/267] RECOMMIT: Removing newly created history controller, and moving history_set_permissions into the root controller with the rest of the history methods. --- lib/galaxy/web/controllers/history.py | 44 --------------------------- lib/galaxy/web/controllers/root.py | 36 ++++++++++++++++++++++ templates/history/options.mako | 2 +- 3 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 lib/galaxy/web/controllers/history.py diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py deleted file mode 100644 index 6483b94850c..00000000000 --- a/lib/galaxy/web/controllers/history.py +++ /dev/null @@ -1,44 +0,0 @@ -from galaxy.web.base.controller import * -import logging - -log = logging.getLogger( __name__ ) - -class HistoryController( BaseController ): - @web.expose - def index( self, trans, **kwd ): - raise 'Unimplemented' - - @web.expose - def set_default_permissions( self, trans, **kwd ): - """Sets the user's default permissions for the current history""" - #TODO: allow changing of default roles associated with history - if trans.user: - if 'set_permissions' in kwd: - """The user clicked the set_permissions button on the set_permissions form""" - history = trans.get_history() - group_in = [] - group_out = [] - #collect groups as entered by user - for name, value in kwd.items(): - if name.startswith( "group_" ): - group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) - if not group: - return trans.show_error_message( 'You have specified an invalid group.' ) - if value == 'in': - group_in.append( group ) - else: - group_out.append( group ) - if not group_in: - return trans.show_error_message( "You must specify at least one default group." ) - cur_groups = [ assoc.group for assoc in history.default_groups ] - group_in.sort() - cur_groups.sort() - if cur_groups != group_in: - history.set_default_access( groups = group_in ) - return trans.show_ok_message( 'Default history permissions have been changed.' ) - else: - return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) - 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." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 0812ce714cf..bb6328b0a5d 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -620,6 +620,42 @@ class RootController( BaseController ): 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""" + #TODO: allow changing of default roles associated with history + if trans.user: + if 'set_permissions' in kwd: + """The user clicked the set_permissions button on the set_permissions form""" + history = trans.get_history() + group_in = [] + group_out = [] + #collect groups as entered by user + for name, value in kwd.items(): + if name.startswith( "group_" ): + group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + if value == 'in': + group_in.append( group ) + else: + group_out.append( group ) + if not group_in: + return trans.show_error_message( "You must specify at least one default group." ) + cur_groups = [ assoc.group for assoc in history.default_groups ] + group_in.sort() + cur_groups.sort() + if cur_groups != group_in: + history.set_default_access( groups = group_in ) + return trans.show_ok_message( 'Default history permissions have been changed.' ) + else: + return trans.show_error_message( "You did not specify any changes to this history's default permissions." ) + 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""" diff --git a/templates/history/options.mako b/templates/history/options.mako index 81cf68f30d6..1bb2794a32b 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,7 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • -
  • Change default permissions for the current history
  • +
  • Change default permissions for the current history
  • %endif
  • Share current history %endif From 4a2efcffd15c2a34d21b3cd952da8e838526f3e3 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Tue, 5 Aug 2008 16:35:14 -0400 Subject: [PATCH 005/267] Changes to access controls, mostly cosmetic. --- lib/galaxy/model/__init__.py | 212 +++++++++++++------------ lib/galaxy/model/mapping.py | 122 ++++++++------ lib/galaxy/tools/__init__.py | 8 +- lib/galaxy/tools/actions/__init__.py | 4 +- lib/galaxy/tools/actions/upload.py | 8 +- lib/galaxy/tools/parameters/basic.py | 31 ++-- lib/galaxy/web/controllers/async.py | 4 +- lib/galaxy/web/controllers/root.py | 8 +- lib/galaxy/web/controllers/user.py | 2 +- lib/galaxy/web/framework/__init__.py | 2 +- templates/dataset/edit_attributes.mako | 2 +- 11 files changed, 238 insertions(+), 165 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 0bed6bb7d19..d74ae8a0420 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -27,38 +27,16 @@ def set_datatypes_registry( d_registry ): datatypes_registry = d_registry class User( object ): - def __init__( self, email=None, password=None, groups = [], roles = [], default_groups = [], default_roles = [] ): + def __init__( self, email=None, password=None ): self.email = email self.password = password self.external = False # Relationships self.histories = [] - if not groups: - groups.append( GalaxyGroup.get( GalaxyGroup.public_id ) ) - default_groups.append( groups[-1] ) - default_groups.append( self.create_private_group() ) - group_id_added = [] - for group in groups: - if group.id not in group_id_added: - group.add_user( self ) - group_id_added.append( group.id ) - group_id_added = [] - for group in default_groups: - if group.id not in group_id_added: - user_group_assoc = DefaultUserGroupAssociation( self, group ) - user_group_assoc.flush() - group_id_added.append( group.id ) - role_id_added = [] - for role in roles: - if role.id not in role_id_added: - role.add_user( self ) - role_id_added.append( role.id ) - role_id_added = [] - for role in default_roles: - if role.id not in role_id_added: - role_group_assoc = DefaultUserRoleAssociation( self, role ) - role_group_assoc.flush() - role_id_added.append( role.id ) + + self.set_default_access() + self.add_group( Group.get_public_group() ) + def set_password_cleartext( self, cleartext ): """Set 'self.password' to the digest of 'cleartext'.""" self.password = sha.new( cleartext ).hexdigest() @@ -66,28 +44,42 @@ class User( object ): """Check if 'cleartext' matches 'self.password' when hashed.""" return self.password == sha.new( cleartext ).hexdigest() def create_private_group( self ): + #create roles for user modification of role + user_permission = Permission( "%s role modification" % self.email, list( Role.access_actions.__dict__.values() ) ) + user_permission.flush() + user_role = Role( "%s role modification" % self.email ) + user_role.flush() + user_role.add_permission( user_permission ) + #add role to user + user_role.add_user( self ) + user_role.add_control_role( user_role ) + + #create private group - group = GalaxyGroup( self.email, priority = 10 ) + group = Group( self.email, priority = 10 ) group.flush() + #create dataset permissions + dataset_permission = Permission( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ) ) + dataset_permission.flush() #create private dataset access role - role = AccessRole( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ), priority = 1 ) + role = Role( "%s dataset access" % self.email, priority = 10 ) + role.add_permission( dataset_permission ) role.flush() + #add control role to role + role.add_control_role( user_role ) #add role to group group.add_role( role ) - #create roles for user modification of role - user_role = AccessRole( "%s role modification" % self.email, list( AccessRole.access_actions.__dict__.values() ) ) - user_role.flush() - #add role to user - user_role.add_user( self ) - #add role to role - role.add_role( user_role ) - #create roles for user modification of group - group_role = AccessRole( "%s group modification" % self.email, list( GalaxyGroup.access_actions.__dict__.values() ) ) + group_permission = Permission( "%s group modification" % self.email, list( Group.access_actions.__dict__.values() ) ) + group_permission.flush() + group_role = Role( "%s group modification" % self.email ) group_role.flush() + #add control role to role + group_role.add_control_role( user_role ) + group_role.add_permission( group_permission ) #add role to group - group.add_access_role( group_role ) + group.add_control_role( group_role ) #associate role and user group_role.add_user( self ) @@ -102,6 +94,9 @@ class User( object ): def has_role( self, check_role ): return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): + if groups is None and roles is None: + groups = [ Group.get_public_group(), self.create_private_group() ] + roles = [] if groups is not None: for assoc in self.default_groups: #this is the association not the actual group assoc.delete() @@ -181,7 +176,7 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -class AccessRole( object ): +class Permission( object ): dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading USE = 'dataset_use', #use in jobs ADD_ROLE = 'dataset_add_role', #dataset can be added to roles @@ -200,14 +195,21 @@ class AccessRole( object ): ADD_ROLE = 'group_add_role', #add role to group REMOVE_ROLE = 'group_remove_role', #remove role from group ADD_USER = 'group_add_user' ) #add users to group - - access_actions = role_actions - - def __init__( self, name, actions, priority = 0 ): + def __init__( self, name = None, actions = [] ): self.name = name - if not isinstance( actions, list ): - actions = [ actions ] self.actions = actions + def add_action( self, action ): + if action not in self.actions: + return self.actions.append( action ) + raise 'action (%s) already exists in permissions list (%s: %s).' % ( action, self.id, self.actions ) + def remove_action( self, action ): + return self.actions.remove( action ) + +class Role( object ): + access_actions = Permission.role_actions + + def __init__( self, name, priority = 0 ): + self.name = name self.priority = priority def add_user( self, user ): assoc = UserRoleAssociation( user, self ) @@ -217,18 +219,27 @@ class AccessRole( object ): assoc = GroupRoleAssociation( group, self ) assoc.flush() return assoc - def add_role( self, role ): - assoc = RoleRoleAssociation( role, self ) + def add_permission( self, permission ): + assoc = RolePermissionAssociation( self, permission ) assoc.flush() return assoc def add_dataset( self, dataset ): assoc = RoleDatasetAssociation( self, dataset ) assoc.flush() return assoc + def add_control_role( self, role ): + assoc = RoleControlRoleAssociation( role, self ) + assoc.flush() + return assoc -class GalaxyGroup( object ): +class Group( object ): public_id = None - access_actions = AccessRole.group_actions + access_actions = Permission.group_actions + + @classmethod + def get_public_group( cls ): + return Group.get( cls.public_id ) + def __init__( self, name, priority = 0 ): self.name = name self.priority = priority @@ -238,26 +249,31 @@ class GalaxyGroup( object ): return assoc def add_role( self, role ): return role.add_group( self ) - def add_access_role( self, role ): - assoc = GroupRoleAccessAssociation( self, role ) - assoc.flush() - return assoc def add_dataset( self, dataset ): assoc = GroupDatasetAssociation( self, dataset ) assoc.flush() return assoc + def add_control_role( self, role ): + assoc = GroupControlRoleAssociation( self, role ) + assoc.flush() + return assoc + +class RolePermissionAssociation( object ): + def __init__( self, role, permission ): + self.role = role + self.permission = permission class UserGroupAssociation( object ): def __init__( self, user, group ): self.user = user self.group = group -class RoleRoleAssociation( object ): +class RoleControlRoleAssociation( object ): def __init__( self, role, target_role ): self.role = role self.target_role = target_role -class GroupRoleAccessAssociation( object ): +class GroupControlRoleAssociation( object ): def __init__( self, group, role ): self.group = group self.role = role @@ -328,10 +344,10 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) - access_actions = AccessRole.dataset_actions + access_actions = Permission.dataset_actions file_path = "/tmp/" engine = None - def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True, access_groups=[], access_roles=[] ): + def __init__( self, id=None, state=None, external_filename=None, extra_files_path=None, file_size=None, purgable=True ): self.id = id self.state = state self.deleted = False @@ -340,14 +356,7 @@ class Dataset( object ): self.external_filename = external_filename self._extra_files_path = extra_files_path self.file_size = file_size - if access_groups or access_roles: - #self.flush() - for group in access_groups: - group.add_dataset( self ) - group.flush() - for role in access_roles: - role.add_dataset( self ) - role.flush() + def get_file_name( self ): if not self.external_filename: assert self.id is not None, "ID must be set before filename used (commit the object)" @@ -416,20 +425,23 @@ class Dataset( object ): #if dataset is in public group, we always return true for viewing and using #this may need to change when the ability to alter groups and roles is allowed - if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = GalaxyGroup.public_id, dataset_id = self.id ): + if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = Group.public_id, dataset_id = self.id ): return True elif user is not None: #loop through permissions and if allowed return true: #check roles associated directly with dataset first for role_dataset_assoc in self.roles: - if action in role_dataset_assoc.role.actions and user.has_role( role_dataset_assoc.role ): - return True + if user.has_role( role_dataset_assoc.role ): + for permission in role_dataset_assoc.role.permissions: + if action in permission.permission.actions: + return True #check roles associated with dataset through groups for group_dataset_assoc in self.groups: if user.has_group( group_dataset_assoc.group ): for group_role_assoc in group_dataset_assoc.group.roles: - if action in group_role_assoc.role.actions: - return True + for permission in group_role_assoc.role.permissions: + if action in permission.permission.actions: + return True return False #no user and dataset not in public group, or user lacks permission def guess_derived_groups_roles( self, other_datasets = [] ): """Returns a list of output roles and groups based upon itself and provided datasets""" @@ -490,7 +502,22 @@ class Dataset( object ): return group.add_dataset( self ) def add_role( self, role ): return role.add_dataset( self ) - + def set_groups( self, groups ): + for assoc in self.groups: + assoc.delete() + assoc.flush() + for group in groups: + if not isinstance( group, Group ): + group = group.group + self.add_group( group ) + def set_roles( self, roles ): + for assoc in self.roles: + assoc.delete() + assoc.flush() + for role in roles: + if not isinstance( role, Role ): + role = role.role + self.add_role( role ) def has_group( self, group ): return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) def has_role( self, role ): @@ -512,7 +539,7 @@ 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, access_groups = [], access_roles = [] ): + visible=True, create_dataset = False ): self.name = name or "Unnamed dataset" self.id = id self.hid = hid @@ -528,7 +555,7 @@ class HistoryDatasetAssociation( object ): # Relationships self.history = history if not dataset and create_dataset: - dataset = Dataset( access_groups = access_groups, access_roles = access_roles ) + dataset = Dataset() dataset.flush() self.dataset = dataset self.parent_id = parent_id @@ -684,7 +711,7 @@ class HistoryDatasetAssociation( object ): class History( object ): - def __init__( self, id=None, name=None, user=None, default_roles = [], default_groups = [] ): + def __init__( self, id=None, name=None, user=None ): self.id = id self.name = name or "Unnamed history" self.deleted = False @@ -695,17 +722,7 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] - if not default_roles: - if user: - default_roles = user.default_roles - if not default_groups: - if user: - default_groups = user.default_groups - else: - default_groups = [ GalaxyGroup.get( GalaxyGroup.public_id ) ] - - - self.set_default_access( roles = default_roles, groups = default_groups ) + self.set_default_access() def _next_hid( self ): # TODO: override this with something in the database that ensures @@ -760,6 +777,13 @@ class History( object ): return des def set_default_access( self, groups = None, roles = None, dataset = False ): + if groups is None and roles is None: + if self.user: + groups = self.user.default_groups + roles = self.user.default_roles + else: + groups = [ Group.get_public_group() ] + roles = [] if groups is not None: for assoc in self.default_groups: #this is the association not the actual group assoc.delete() @@ -778,20 +802,12 @@ class History( object ): for data in self.datasets: for hda in data.dataset.history_associations: if self.user and hda.history not in self.user.histories: + data.dataset.set_groups( [ Group.get_public_group() ] ) + data.dataset.set_roles( [] ) break else: - if groups is not None: - for assoc in data.dataset.groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - group.add_dataset( data ) - if roles is not None: - for assoc in data.dataset.roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - role.add_dataset( data ) + data.dataset.set_groups( groups ) + data.dataset.set_roles( roles ) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 335ab183dd5..c3fe6561ffc 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -114,7 +114,7 @@ ValidationError.table = Table( "validation_error", metadata, Column( "err_type", TrimmedString( 64 ) ), Column( "attributes", TEXT ) ) -GalaxyGroup.table = Table( "galaxy_group", metadata, +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 ), @@ -128,25 +128,38 @@ UserGroupAssociation.table = Table( "user_group_association", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) -AccessRole.table = Table( "access_role", metadata, +Permission.table = Table( "permission", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + Column( "actions", JSONType(), default=[] ) ) + +Role.table = Table( "role", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "name", TEXT ), - Column( "actions", JSONType(), default=[] ), Column( "priority", Integer ) ) +RolePermissionAssociation.table = Table( "role_permission_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "permission_id", Integer, ForeignKey( "permission.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( "access_role.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( "access_role.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 ) ) @@ -159,21 +172,21 @@ GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, RoleDatasetAssociation.table = Table( "role_dataset_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) -RoleRoleAssociation.table = Table( "role_role_association", metadata, +RoleControlRoleAssociation.table = Table( "role_control_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), - Column( "target_role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), + Column( "target_role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) -GroupRoleAccessAssociation.table = Table( "group_role_access_association", metadata, +GroupControlRoleAssociation.table = Table( "group_control_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.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 ) ) @@ -181,7 +194,7 @@ GroupRoleAccessAssociation.table = Table( "group_role_access_association", metad DefaultUserRoleAssociation.table = Table( "default_user_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) @@ -195,7 +208,7 @@ DefaultUserGroupAssociation.table = Table( "default_user_group_association", met DefaultHistoryRoleAssociation.table = Table( "default_history_role_association", metadata, Column( "id", Integer, primary_key=True ), - Column( "role_id", Integer, ForeignKey( "access_role.id" ), index=True ), + Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ) ) @@ -391,55 +404,62 @@ assign_mapper( context, User, User.table, collection_class=ordering_list( 'order_index' ) ) ) ) -assign_mapper( context, GalaxyGroup, GalaxyGroup.table, +assign_mapper( context, Group, Group.table, properties=dict( users=relation( UserGroupAssociation ), datasets=relation( GroupDatasetAssociation ) ) ) assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, properties=dict( user=relation( User, backref = "groups" ), - group=relation( GalaxyGroup, backref = "users" ) ) ) + group=relation( Group, backref = "users" ) ) ) assign_mapper( context, UserRoleAssociation, UserRoleAssociation.table, - properties=dict( role=relation( AccessRole, backref = "users" ), + properties=dict( role=relation( Role, backref = "users" ), user=relation( User, backref = "roles" ) ) ) assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, - properties=dict( role=relation( AccessRole, backref = "groups" ), - group=relation( GalaxyGroup, backref = "roles" ) ) ) + properties=dict( role=relation( Role, backref = "groups" ), + group=relation( Group, backref = "roles" ) ) ) + +assign_mapper( context, Permission, Permission.table ) + +assign_mapper( context, Role, Role.table ) + +assign_mapper( context, RolePermissionAssociation, RolePermissionAssociation.table, + properties=dict( role=relation( Role, backref = "permissions" ), + permission=relation( Permission, backref = "roles" ) ) ) -assign_mapper( context, AccessRole, AccessRole.table ) assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), - group=relation( GalaxyGroup, backref = "datasets" ) ) ) + group=relation( Group, backref = "datasets" ) ) ) assign_mapper( context, RoleDatasetAssociation, RoleDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) -assign_mapper( context, RoleRoleAssociation, RoleRoleAssociation.table, - properties=dict( role=relation( AccessRole, primaryjoin=( ( RoleRoleAssociation.table.c.role_id == AccessRole.table.c.id ) ) ), - target_role=relation( AccessRole, primaryjoin=( RoleRoleAssociation.table.c.target_role_id == AccessRole.table.c.id ), backref="roles" ) ) ) +assign_mapper( context, RoleControlRoleAssociation, RoleControlRoleAssociation.table, + properties=dict( role=relation( Role, primaryjoin=( ( RoleControlRoleAssociation.table.c.role_id == Role.table.c.id ) ) ), + target_role=relation( Role, primaryjoin=( RoleControlRoleAssociation.table.c.target_role_id == Role.table.c.id ), backref="roles" ) ) ) -assign_mapper( context, GroupRoleAccessAssociation, GroupRoleAccessAssociation.table, - properties=dict( role=relation( AccessRole, backref="access_groups" ), - group=relation( GalaxyGroup, backref="access_roles" ) ) ) +assign_mapper( context, GroupControlRoleAssociation, GroupControlRoleAssociation.table, + properties=dict( role=relation( Role, backref="access_groups" ), + group=relation( Group, backref="access_roles" ) ) ) assign_mapper( context, DefaultUserRoleAssociation, DefaultUserRoleAssociation.table, properties=dict( user=relation( User, backref = "default_roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, properties=dict( user=relation( User, backref = "default_groups" ), - group=relation( GalaxyGroup ) ) ) + group=relation( Group ) ) ) assign_mapper( context, DefaultHistoryRoleAssociation, DefaultHistoryRoleAssociation.table, properties=dict( history=relation( History, backref = "default_roles" ), - role=relation( AccessRole ) ) ) + role=relation( Role ) ) ) assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssociation.table, properties=dict( history=relation( History, backref = "default_groups" ), - group=relation( GalaxyGroup ) ) ) + group=relation( Group ) ) ) assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) @@ -555,32 +575,42 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.context = context result.create_tables = create_tables #set up default table entries here, currently only exist for access controls - if result.AccessRole.count() == 0: + if result.Role.count() == 0: log.warning( "There were no access roles located, setting up default (public) access roles." ) #create public group - public_group = result.GalaxyGroup( 'public' ) + public_group = result.Group( 'public' ) public_group.flush() #create public_all role - public_role = result.AccessRole( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.GalaxyGroup.access_actions.ADD_DATASET, result.GalaxyGroup.access_actions.REMOVE_DATASET ] ) + public_role = result.Role( 'public' ) public_role.flush() public_group.add_role( public_role ) + permission = result.Permission( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.Group.access_actions.ADD_DATASET, result.Group.access_actions.REMOVE_DATASET ] ) + permission.flush() + public_role.add_permission( permission ) #store public group id - GalaxyGroup.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions - #add all datasets to public group - for dataset in result.Dataset.select(): - public_group.add_dataset( dataset ) + Group.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions - #loop through all current users and associate with the public group - #and create and associate with user's own group - for user in result.User.select(): - public_group.add_user( user ) - private_group = user.create_private_group() - user.set_default_access( groups = [ public_group, private_group ], roles = [], history = True, dataset = True ) + #loop through all histories and set up rbac on users, histories and datasets + for history in result.History.select(): + if history.user: + if not history.user.default_groups: + history.user.set_default_access( history = True, dataset = True ) + history.user.add_group( public_group ) + history.user.flush() + else: + history.set_default_access( dataset = True ) + history.flush() + #add all datasets which aren't in a history to the public group + orphans = result.Dataset.get_by( history_id = None ) + if orphans: + for dataset in orphans: + dataset.set_groups( [ public_group ] ) + dataset.set_roles( [] ) else: #retrieve from database and store public group id, assume first created group is public - GalaxyGroup.public_id = result.GalaxyGroup.select( order_by = asc( result.GalaxyGroup.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions - log.debug( "Public Group identified as id = %s." % ( GalaxyGroup.public_id ) ) + Group.public_id = result.Group.select( order_by = asc( result.Group.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions + log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) return result def get_suite(): diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 9d21d33f803..af94a3a24ce 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1084,7 +1084,9 @@ class Tool: if visible == "visible": visible = True 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, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) + child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext, parent_id=outdata.id, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + child_dataset.dataset.set_groups( outdata.dataset.groups ) + child_dataset.dataset.set_roles( outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1120,7 +1122,9 @@ class Tool: else: visible = False 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, access_groups=outdata.dataset.groups, access_roles=outdata.dataset.roles ) + primary_data = self.app.model.HistoryDatasetAssociation( extension=ext, designation=designation, visible=visible, dbkey=outdata.dbkey, create_dataset=True ) + primary_data.dataset.set_groups( outdata.dataset.groups ) + primary_data.dataset.set_roles( outdata.dataset.roles ) 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 058caf07203..25393b29b7f 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -129,7 +129,9 @@ class DefaultToolAction( object ): ext = output.format if ext == "input": ext = input_ext - data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True, access_groups=output_access_groups, access_roles=output_access_roles ) + data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) + data.dataset.set_groups( output_access_groups ) + data.dataset.set_roles( output_access_roles ) # Commit the dataset immediately so it gets database assigned unique id data.flush() # Create an empty file immediately diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 5c68786b6b1..6d7675a9456 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -65,7 +65,9 @@ class UploadToolAction( object ): return dict( output=data_list[0] ) def upload_empty(self, trans, err_code, err_msg): - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -158,7 +160,9 @@ class UploadToolAction( object ): if info is None: info = 'uploaded %s file' %data_type - data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) 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 212a691d6aa..379529b8cab 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -979,21 +979,34 @@ class DataToolParameter( ToolParameter ): 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, User, AccessRole, GalaxyGroup, GroupRoleAssociation + >>> from galaxy.model import History, HistoryDatasetAssociation, User, Role, Permission, Group, GroupRoleAssociation >>> from galaxy.util.bunch import Bunch >>> hist = History() >>> hist.flush() - >>> role = AccessRole( 'test', list( AccessRole.dataset_actions.__dict__.values() ) ) + >>> permission = Permission( 'test', list( Permission.dataset_actions.__dict__.values() ) ) + >>> permission.flush() + >>> role = Role( 'test' ) >>> role.flush() - >>> group = GalaxyGroup( 'test' ) + >>> assoc = role.add_permission( permission ) + >>> group = Group( 'test' ) >>> group.flush() - >>> GalaxyGroup.public_id = group.id + >>> Group.public_id = group.id >>> GroupRoleAssociation( group, role ).flush() - >>> hist.add_dataset( HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=4, extension='png', create_dataset=True, access_groups=[ group ] ) ) - >>> hist.add_dataset( HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True, access_groups=[ group ] ) ) + >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) + >>> dataset1.dataset.set_groups( [ group ] ) + >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) + >>> dataset2.dataset.set_groups( [ group ] ) + >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) + >>> dataset3.dataset.set_groups( [ group ] ) + >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) + >>> dataset4.dataset.set_groups( [ group ] ) + >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) + >>> dataset5.dataset.set_groups( [ group ] ) + >>> hist.add_dataset( dataset1 ) + >>> hist.add_dataset( dataset2 ) + >>> hist.add_dataset( dataset3 ) + >>> hist.add_dataset( dataset4 ) + >>> hist.add_dataset( dataset5 ) >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index 4d68fb8eb0c..9278e5f3cf6 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -103,7 +103,9 @@ class ASync( BaseController ): #data.state = jobs.JOB_OK #history.datasets.add_dataset( data ) - data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE, access_groups = [ group.group for group in trans.history.default_groups ], access_roles = [ role.role for role in trans.history.default_roles ] ) + data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE ) + data.dataset.set_groups( trans.history.default_groups ) + data.dataset.set_roles( trans.history.default_roles ) data.name = GALAXY_NAME data.dbkey = GALAXY_BUILD data.info = GALAXY_INFO diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index bb6328b0a5d..3a9f9471ef5 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -265,7 +265,7 @@ class RootController( BaseController ): if not trans.user: return trans.show_error_message( "You must be logged in if you want to change dataset permissions." ) private_dataset = 'private_dataset' - public_group = trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id ) + public_group = trans.app.model.Group.get_public_group() if private_dataset in kwd and data.dataset.has_group( public_group ): #check user has permision and then remove public group if data.dataset.allow_action( trans.user, data.dataset.access_actions.REMOVE_GROUP ): @@ -599,7 +599,9 @@ class RootController( BaseController ): copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) roles = copy_access_from.dataset.roles groups = copy_access_from.dataset.groups - data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True, access_groups = groups, access_roles = roles ) + data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) + data.dataset.set_groups( groups ) + data.dataset.set_roles( roles ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -633,7 +635,7 @@ class RootController( BaseController ): #collect groups as entered by user for name, value in kwd.items(): if name.startswith( "group_" ): - group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + group = trans.app.model.Group.get( name.replace( "group_", "", 1 ) ) if not group: return trans.show_error_message( 'You have specified an invalid group.' ) if value == 'in': diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index bc3801137d2..8de8eff2da0 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -179,7 +179,7 @@ class User( BaseController ): #collect groups as entered by user for name, value in kwd.items(): if name.startswith( "group_" ): - group = trans.app.model.GalaxyGroup.get( name.replace( "group_", "", 1 ) ) + group = trans.app.model.Group.get( name.replace( "group_", "", 1 ) ) if not group: return trans.show_error_message( 'You have specified an invalid group.' ) if value == 'in': diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index e3874e63248..111ac1a4f43 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -433,7 +433,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if history is not None and user is not None: if not history.user: #This user will now aquire previously unowned history, let set permissions to user's default - history.set_default_access( roles = [ role.role for role in user.default_roles ], groups = [ group.group for group in user.default_groups ], dataset = True ) + history.set_default_access( roles = user.default_roles, groups = user.default_groups, dataset = True ) history.user_id = user.id history.flush() self.__history = history diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index e159c89b3a6..aa5e0d23370 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -144,7 +144,7 @@ Private Dataset: <% checked = "" %> - %if not data.dataset.has_group( trans.app.model.GalaxyGroup.get( trans.app.model.GalaxyGroup.public_id ) ): + %if not data.dataset.has_group( trans.app.model.Group.get_public_group() ): <% checked = " checked" %> %endif
    From 32cb3c53414eda4cf69f7c0d520fa29427d0e447 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Wed, 6 Aug 2008 15:22:02 -0400 Subject: [PATCH 006/267] Moved security code out of model and into its own directory. --- lib/galaxy/app.py | 3 + lib/galaxy/model/__init__.py | 271 +----------------- lib/galaxy/model/mapping.py | 16 +- lib/galaxy/security/__init__.py | 362 +++++++++++++++++++++++++ lib/galaxy/tools/__init__.py | 8 +- lib/galaxy/tools/actions/__init__.py | 10 +- lib/galaxy/tools/actions/upload.py | 10 +- lib/galaxy/tools/parameters/basic.py | 23 +- lib/galaxy/web/controllers/async.py | 4 +- lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/root.py | 34 +-- lib/galaxy/web/controllers/user.py | 3 +- lib/galaxy/web/framework/__init__.py | 3 +- templates/dataset/edit_attributes.mako | 2 +- templates/root/history_common.mako | 2 +- 15 files changed, 433 insertions(+), 320 deletions(-) create mode 100644 lib/galaxy/security/__init__.py diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 0c4ea728395..1964945012e 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -4,6 +4,7 @@ from galaxy import config, jobs, util, tools, web import galaxy.model import galaxy.model.mapping import galaxy.datatypes.registry +import galaxy.security class UniverseApplication( object ): """Encapsulates the state of a Universe application""" @@ -30,6 +31,8 @@ class UniverseApplication( object ): self.toolbox = tools.ToolBox( self.config.tool_config, self.config.tool_path, self ) #Load datatype converters self.datatypes_registry.load_datatype_converters( self.toolbox ) + #Load security policy + self.security_agent = self.model.security_agent # Start the job queue job_dispatcher = jobs.DefaultJobDispatcher( self ) self.job_queue = jobs.JobQueue( self, job_dispatcher ) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index d74ae8a0420..6e0d0922ea6 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__ ) @@ -34,86 +35,12 @@ class User( object ): # Relationships self.histories = [] - self.set_default_access() - self.add_group( Group.get_public_group() ) - 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 create_private_group( self ): - #create roles for user modification of role - user_permission = Permission( "%s role modification" % self.email, list( Role.access_actions.__dict__.values() ) ) - user_permission.flush() - user_role = Role( "%s role modification" % self.email ) - user_role.flush() - user_role.add_permission( user_permission ) - #add role to user - user_role.add_user( self ) - user_role.add_control_role( user_role ) - - - #create private group - group = Group( self.email, priority = 10 ) - group.flush() - #create dataset permissions - dataset_permission = Permission( "%s dataset access" % self.email, list( Dataset.access_actions.__dict__.values() ) ) - dataset_permission.flush() - #create private dataset access role - role = Role( "%s dataset access" % self.email, priority = 10 ) - role.add_permission( dataset_permission ) - role.flush() - #add control role to role - role.add_control_role( user_role ) - #add role to group - group.add_role( role ) - - #create roles for user modification of group - group_permission = Permission( "%s group modification" % self.email, list( Group.access_actions.__dict__.values() ) ) - group_permission.flush() - group_role = Role( "%s group modification" % self.email ) - group_role.flush() - #add control role to role - group_role.add_control_role( user_role ) - group_role.add_permission( group_permission ) - #add role to group - group.add_control_role( group_role ) - #associate role and user - group_role.add_user( self ) - - #add user to group - group.add_user( self ) - group.flush() - return group - def add_group( self, group ): - return group.add_user( self ) - def has_group( self, check_group ): - return bool( UserGroupAssociation.get_by( group_id = check_group.id, user_id = self.id ) ) - def has_role( self, check_role ): - return bool( UserRoleAssociation.get_by( role_id = check_role.id, user_id = self.id ) ) - def set_default_access( self, groups = None, roles = None, history = False, dataset = False ): - if groups is None and roles is None: - groups = [ Group.get_public_group(), self.create_private_group() ] - roles = [] - if groups is not None: - for assoc in self.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - assoc = DefaultUserGroupAssociation( self, group ) - assoc.flush() - if roles is not None: - for assoc in self.default_roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - assoc = DefaultUserRoleAssociation( self, role ) - assoc.flush() - if history: - for history in self.histories: - history.set_default_access( groups = groups, roles = roles, dataset = dataset ) class Job( object ): """ @@ -177,24 +104,10 @@ class JobToOutputDatasetAssociation( object ): self.dataset = dataset class Permission( object ): - dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading - USE = 'dataset_use', #use in jobs - ADD_ROLE = 'dataset_add_role', #dataset can be added to roles - REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles - ADD_GROUP = 'dataset_add_group', #dataset can be added to groups - REMOVE_GROUP = 'dataset_remove_group' ) #dataset can be removed from groups - role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset - REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset - DELETE = 'role_delete', #delete a role - MODIFY = 'role_modify', #change a role's actions, - ADD_GROUP = 'role_add_group', #add role to a group - REMOVE_GROUP = 'role_remove_group' ) #remove role from a group - group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset - REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group - DELETE = 'group_delete', #delete a group - ADD_ROLE = 'group_add_role', #add role to group - REMOVE_ROLE = 'group_remove_role', #remove role from group - ADD_USER = 'group_add_user' ) #add users to group + dataset_actions = RBACAgent.actions.dataset_actions + role_actions = RBACAgent.actions.role_actions + group_actions = RBACAgent.actions.group_actions + def __init__( self, name = None, actions = [] ): self.name = name self.actions = actions @@ -211,26 +124,6 @@ class Role( object ): def __init__( self, name, priority = 0 ): self.name = name self.priority = priority - def add_user( self, user ): - assoc = UserRoleAssociation( user, self ) - assoc.flush() - return assoc - def add_group( self, group ): - assoc = GroupRoleAssociation( group, self ) - assoc.flush() - return assoc - def add_permission( self, permission ): - assoc = RolePermissionAssociation( self, permission ) - assoc.flush() - return assoc - def add_dataset( self, dataset ): - assoc = RoleDatasetAssociation( self, dataset ) - assoc.flush() - return assoc - def add_control_role( self, role ): - assoc = RoleControlRoleAssociation( role, self ) - assoc.flush() - return assoc class Group( object ): public_id = None @@ -243,20 +136,6 @@ class Group( object ): def __init__( self, name, priority = 0 ): self.name = name self.priority = priority - def add_user( self, user ): - assoc = UserGroupAssociation( user, self ) - assoc.flush() - return assoc - def add_role( self, role ): - return role.add_group( self ) - def add_dataset( self, dataset ): - assoc = GroupDatasetAssociation( self, dataset ) - assoc.flush() - return assoc - def add_control_role( self, role ): - assoc = GroupControlRoleAssociation( self, role ) - assoc.flush() - return assoc class RolePermissionAssociation( object ): def __init__( self, role, permission ): @@ -420,108 +299,6 @@ class Dataset( object ): return self.get_size() > 0 def mark_deleted( self, include_children=True ): self.deleted = True - def allow_action( self, user, action ): - """Returns true when user has permission to perform an action""" - - #if dataset is in public group, we always return true for viewing and using - #this may need to change when the ability to alter groups and roles is allowed - if action in [ self.access_actions.USE, self.access_actions.VIEW ] and GroupDatasetAssociation.get_by( group_id = Group.public_id, dataset_id = self.id ): - return True - elif user is not None: - #loop through permissions and if allowed return true: - #check roles associated directly with dataset first - for role_dataset_assoc in self.roles: - if user.has_role( role_dataset_assoc.role ): - for permission in role_dataset_assoc.role.permissions: - if action in permission.permission.actions: - return True - #check roles associated with dataset through groups - for group_dataset_assoc in self.groups: - if user.has_group( group_dataset_assoc.group ): - for group_role_assoc in group_dataset_assoc.group.roles: - for permission in group_role_assoc.role.permissions: - if action in permission.permission.actions: - return True - return False #no user and dataset not in public group, or user lacks permission - def guess_derived_groups_roles( self, other_datasets = [] ): - """Returns a list of output roles and groups based upon itself and provided datasets""" - if not other_datasets: - return [ data_group_assoc.group for data_group_assoc in self.groups ], [ data_role_assoc.role for data_role_assoc in self.roles ] - access_roles = None - priority_access_role = None - access_groups = None - priority_access_group = None - for dataset in [ self ] + other_datasets: - #determine access roles and groups for output datasets - #roles and groups for output dataset is the intersection across all inputs - #if we end up with no intersection between inputs, then we rely on priorities - if isinstance( dataset, HistoryDatasetAssociation ): - dataset = dataset.dataset - roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] - for role in roles: - if priority_access_role is None or priority_access_role.priority < role.priority: - priority_access_role = role - groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] - for group in groups: - if priority_access_group is None or priority_access_group.priority < group.priority: - priority_access_group = group - if access_roles is None: - access_roles = set( roles ) - access_groups = set( groups ) - else: - access_roles.intersection_update( set( roles ) ) - access_groups.intersection_update( set( groups ) ) - - #complete lists for output dataset access - if access_roles: - access_roles = list( access_roles ) - else: - access_roles = [] - if access_groups: - access_groups = list( access_groups) - else: - access_groups = [] - #if we have no roles or groups left after intersection, - #take the highest priority group or role - if not access_roles and not access_groups: - if priority_access_role and priority_access_group: - if priority_access_group.priority == priority_access_role.priority: - access_groups = [ priority_access_group ] - access_roles = [ priority_access_role ] - elif priority_access_group.priority > priority_access_role.priority: - access_groups = [ priority_access_group ] - else: - access_roles = [ priority_access_role ] - elif priority_access_role: - access_roles = [ priority_access_role ] - elif priority_access_group: - access_groups = [ priority_access_group ] - - return access_groups, access_roles - def add_group( self, group ): - return group.add_dataset( self ) - def add_role( self, role ): - return role.add_dataset( self ) - def set_groups( self, groups ): - for assoc in self.groups: - assoc.delete() - assoc.flush() - for group in groups: - if not isinstance( group, Group ): - group = group.group - self.add_group( group ) - def set_roles( self, roles ): - for assoc in self.roles: - assoc.delete() - assoc.flush() - for role in roles: - if not isinstance( role, Role ): - role = role.role - self.add_role( role ) - def has_group( self, group ): - return bool( GroupDatasetAssociation.get_by( group_id = group.id, dataset_id = self.id ) ) - def has_role( self, role ): - return bool( RoleDatasetAssociation.get_by( role_id = role.id, dataset_id = self.id ) ) # FIXME: sqlalchemy will replace this def _delete(self): @@ -706,9 +483,6 @@ class HistoryDatasetAssociation( object ): for child in self.children: child.mark_deleted() - def allow_action( self, user, action ): - return self.dataset.allow_action( user, action ) - class History( object ): def __init__( self, id=None, name=None, user=None ): @@ -722,8 +496,6 @@ class History( object ): self.datasets = [] self.galaxy_sessions = [] - self.set_default_access() - def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -776,39 +548,6 @@ class History( object ): des.flush() return des - def set_default_access( self, groups = None, roles = None, dataset = False ): - if groups is None and roles is None: - if self.user: - groups = self.user.default_groups - roles = self.user.default_roles - else: - groups = [ Group.get_public_group() ] - roles = [] - if groups is not None: - for assoc in self.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() - for group in groups: - assoc = DefaultHistoryGroupAssociation( self, group ) - assoc.flush() - if roles is not None: - for assoc in self.default_roles: #this is the association not the actual group - assoc.delete() - assoc.flush() - for role in roles: - assoc = DefaultHistoryRoleAssociation( self, role ) - assoc.flush() - if dataset: - for data in self.datasets: - for hda in data.dataset.history_associations: - if self.user and hda.history not in self.user.histories: - data.dataset.set_groups( [ Group.get_public_group() ] ) - data.dataset.set_roles( [] ) - break - else: - data.dataset.set_groups( groups ) - data.dataset.set_roles( roles ) - # class Query( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index c3fe6561ffc..746ec2f622d 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -19,6 +19,7 @@ from sqlalchemy import * from galaxy.model import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch +from galaxy.security import GalaxyRBACAgent metadata = DynamicMetaData( threadlocal=False ) context = SessionContext( create_session ) @@ -574,6 +575,8 @@ def init( file_path, url, engine_options={}, create_tables=False ): result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) result.context = context result.create_tables = create_tables + #load local galaxy security policy + result.security_agent = GalaxyRBACAgent( result ) #set up default table entries here, currently only exist for access controls if result.Role.count() == 0: log.warning( "There were no access roles located, setting up default (public) access roles." ) @@ -583,10 +586,10 @@ def init( file_path, url, engine_options={}, create_tables=False ): #create public_all role public_role = result.Role( 'public' ) public_role.flush() - public_group.add_role( public_role ) + result.security_agent.associate_components( group = public_group, role = public_role ) permission = result.Permission( 'public', [ result.Dataset.access_actions.USE, result.Dataset.access_actions.VIEW, result.Group.access_actions.ADD_DATASET, result.Group.access_actions.REMOVE_DATASET ] ) permission.flush() - public_role.add_permission( permission ) + result.security_agent.associate_components( permission = permission, role = public_role ) #store public group id Group.public_id = public_group.id #we use the id instead of the object, because of alchemy sessions @@ -595,18 +598,17 @@ def init( file_path, url, engine_options={}, create_tables=False ): for history in result.History.select(): if history.user: if not history.user.default_groups: - history.user.set_default_access( history = True, dataset = True ) - history.user.add_group( public_group ) + results.security_agent.setup_new_user( history.user ) history.user.flush() else: - history.set_default_access( dataset = True ) + result.security_agent.history_set_default_access( history, dataset = True ) history.flush() #add all datasets which aren't in a history to the public group orphans = result.Dataset.get_by( history_id = None ) if orphans: for dataset in orphans: - dataset.set_groups( [ public_group ] ) - dataset.set_roles( [] ) + result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + result.security_agent.set_dataset_roles( dataset, [] ) else: #retrieve from database and store public group id, assume first created group is public Group.public_id = result.Group.select( order_by = asc( result.Group.table.c.create_time ) )[0].id #we use the id instead of the object, because of alchemy sessions diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py new file mode 100644 index 00000000000..fa56d3a4310 --- /dev/null +++ b/lib/galaxy/security/__init__.py @@ -0,0 +1,362 @@ +""" +Utility functions used systemwide. + +""" +import logging +from galaxy.util.bunch import Bunch + +log = logging.getLogger(__name__) + +class RBACAgent: + """Class that handles galaxy security""" + + actions = Bunch( + dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading + USE = 'dataset_use', #use in jobs + ADD_ROLE = 'dataset_add_role', #dataset can be added to roles + REMOVE_ROLE = 'dataset_remove_role', #dataset can be removed from roles + ADD_GROUP = 'dataset_add_group', #dataset can be added to groups + REMOVE_GROUP = 'dataset_remove_group' ), #dataset can be removed from groups + role_actions = Bunch( ADD_DATASET = 'role_add_dataset', #add role to dataset + REMOVE_DATASET = 'role_remove_dataset', #remove role from dataset + DELETE = 'role_delete', #delete a role + MODIFY = 'role_modify', #change a role's actions, + ADD_GROUP = 'role_add_group', #add role to a group + REMOVE_GROUP = 'role_remove_group' ), #remove role from a group + group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add group to dataset + REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group + DELETE = 'group_delete', #delete a group + ADD_ROLE = 'group_add_role', #add role to group + REMOVE_ROLE = 'group_remove_role', #remove role from group + ADD_USER = 'group_add_user' ) #add users to group + ) + + 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_groups_roles_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_group( self, user ): + raise "Unimplemented Method" + def user_set_default_access( self, user, groups = None, roles = None, history = False, dataset = False ): + raise "Unimplemented Method" + def setup_new_user( self, user ): + self.user_set_default_access( user, history = True, dataset = True ) + self.associate_components( user = user, group = self.get_public_group() ) + def history_set_default_access( self, history, groups = None, roles = None, dataset = False ): + raise "Unimplemented Method" + def get_public_group( self ): + raise "Unimplemented Method" + def set_dataset_groups( self, dataset, groups ): + raise "Unimplemented Method" + def set_dataset_roles( self, dataset, roles ): + 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 ) ) + +class GalaxyRBACAgent( RBACAgent ): + + def __init__( self, model, actions = None ): + self.model = model + if actions: + actions = 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""" + + while not isinstance( dataset, self.model.Dataset ): + dataset = dataset.dataset + #if dataset is in public group, we always return true for viewing and using + #this may need to change when the ability to alter groups and roles is allowed + if action in [ self.actions.dataset_actions.USE, self.actions.dataset_actions.VIEW ] and self.components_are_associated( group = self.get_public_group(), dataset = dataset ): + return True + elif user is not None: + #loop through permissions and if allowed return true: + #check roles associated directly with dataset first + for role_dataset_assoc in dataset.roles: + if self.components_are_associated( user = user, role = role_dataset_assoc.role ): + for permission in role_dataset_assoc.role.permissions: + if action in permission.permission.actions: + return True + #check roles associated with dataset through groups + for group_dataset_assoc in dataset.groups: + if self.components_are_associated( user = user, group = group_dataset_assoc.group ): + for group_role_assoc in group_dataset_assoc.group.roles: + for permission in group_role_assoc.role.permissions: + if action in permission.permission.actions: + return True + return False #no user and dataset not in public group, or user lacks permission + def guess_derived_groups_roles_for_datasets( self, datasets = [] ): + """Returns a list of output roles and groups based upon itself and provided datasets""" + access_roles = None + priority_access_role = None + access_groups = None + priority_access_group = None + for dataset in datasets: + #determine access roles and groups for output datasets + #roles and groups for output dataset is the intersection across all inputs + #if we end up with no intersection between inputs, then we rely on priorities + if isinstance( dataset, self.model.HistoryDatasetAssociation ): + dataset = dataset.dataset + roles = [ data_role_assoc.role for data_role_assoc in dataset.roles ] + for role in roles: + if priority_access_role is None or priority_access_role.priority < role.priority: + priority_access_role = role + groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] + for group in groups: + if priority_access_group is None or priority_access_group.priority < group.priority: + priority_access_group = group + if access_roles is None: + access_roles = set( roles ) + access_groups = set( groups ) + else: + access_roles.intersection_update( set( roles ) ) + access_groups.intersection_update( set( groups ) ) + + #complete lists for output dataset access + if access_roles: + access_roles = list( access_roles ) + else: + access_roles = [] + if access_groups: + access_groups = list( access_groups) + else: + access_groups = [] + #if we have no roles or groups left after intersection, + #take the highest priority group or role + if not access_roles and not access_groups: + if priority_access_role and priority_access_group: + if priority_access_group.priority == priority_access_role.priority: + access_groups = [ priority_access_group ] + access_roles = [ priority_access_role ] + elif priority_access_group.priority > priority_access_role.priority: + access_groups = [ priority_access_group ] + else: + access_roles = [ priority_access_role ] + elif priority_access_role: + access_roles = [ priority_access_role ] + elif priority_access_group: + access_groups = [ priority_access_group ] + + return access_groups, access_roles + + def associate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to associate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.associate_group_dataset( kwd['group'], kwd['dataset'] ) + elif 'role' in kwd: + return self.associate_role_dataset( kwd['role'], kwd['dataset'] ) + elif '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'] ) + elif 'control_role' in kwd: + return self.associate_role_control_role( kwd['control_role'], kwd['role'] ) + elif 'target_role' in kwd: + return self.associate_role_control_role( kwd['role'], kwd['target_role'] ) + elif 'permission' in kwd: + return self.associate_role_permission( kwd['role'], kwd['permission'] ) + elif 'group' in kwd: + if 'control_role' in kwd: + return self.associate_group_control_role( kwd['group'], kwd['control_role'] ) + raise 'No valid method of associating provided components: %s' % kwd + def associate_group_dataset( self, group, dataset ): + assoc = self.model.GroupDatasetAssociation( group, dataset ) + assoc.flush() + return assoc + def associate_role_dataset( self, role, dataset ): + assoc = self.model.RoleDatasetAssociation( role, dataset ) + assoc.flush() + return assoc + 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_role_control_role( self, control_role, role ): + assoc = self.model.RoleControlRoleAssociation( control_role, role ) + assoc.flush() + return assoc + def associate_group_control_role( self, group, role ): + assoc = self.model.GroupControlRoleAssociation( group, role ) + assoc.flush() + return assoc + def associate_role_permission( self, role, permission ): + assoc = self.model.RolePermissionAssociation( role, permission ) + assoc.flush() + return assoc + + def create_private_user_group( self, user ): + #create roles for user modification of role + user_permission = self.model.Permission( "%s role modification" % user.email, list( self.model.Role.access_actions.__dict__.values() ) ) + user_permission.flush() + user_role = self.model.Role( "%s role modification" % user.email ) + user_role.flush() + self.associate_components( role = user_role, permission = user_permission ) + self.associate_components( user = user, role = user_role ) + self.associate_components( control_role = user_role, role = user_role ) + + + #create private group + group = self.model.Group( user.email, priority = 10 ) + group.flush() + #create dataset permissions + dataset_permission = self.model.Permission( "%s dataset access" % user.email, list( self.model.Dataset.access_actions.__dict__.values() ) ) + dataset_permission.flush() + #create private dataset access role + role = self.model.Role( "%s dataset access" % user.email, priority = 10 ) + self.associate_components( role = role, permission = dataset_permission ) + role.flush() + #add control role to role + self.associate_components( control_role = user_role, role = role ) + #add role to group + self.associate_components( group = group, role = user_role ) + + #create roles for user modification of group + group_permission = self.model.Permission( "%s group modification" % user.email, list( self.model.Group.access_actions.__dict__.values() ) ) + group_permission.flush() + group_role = self.model.Role( "%s group modification" % user.email ) + group_role.flush() + #add control role to role + self.associate_components( control_role = user_role, role = group_role ) + self.associate_components( permission = group_permission, role = group_role ) + #add role to group + self.associate_components( control_role = group_role, group = group ) + #associate role and user + self.associate_components( role = group_role, user = user ) + + #add user to group + self.associate_components( group = group, user = user ) + group.flush() + return group + + + + def user_set_default_access( self, user, groups = None, roles = None, history = False, dataset = False ): + if groups is None and roles is None: + groups = [ self.get_public_group(), self.create_private_user_group( user ) ] + roles = [] + if groups is not None: + for assoc in user.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = self.model.DefaultUserGroupAssociation( user, group ) + assoc.flush() + if roles is not None: + for assoc in user.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = self.model.DefaultUserRoleAssociation( user, role ) + assoc.flush() + if history: + for history in user.histories: + self.history_set_default_access( history, groups = groups, roles = roles, dataset = dataset ) + + def history_set_default_access( self, history, groups = None, roles = None, dataset = False ): + if groups is None and roles is None: + if history.user: + groups = history.user.default_groups + roles = history.user.default_roles + else: + groups = [ self.get_public_group() ] + roles = [] + if groups is not None: + for assoc in history.default_groups: #this is the association not the actual group + assoc.delete() + assoc.flush() + for group in groups: + assoc = self.model.DefaultHistoryGroupAssociation( history, group ) + assoc.flush() + if roles is not None: + for assoc in history.default_roles: #this is the association not the actual group + assoc.delete() + assoc.flush() + for role in roles: + assoc = self.model.DefaultHistoryRoleAssociation( history, role ) + assoc.flush() + if dataset: + for data in history.datasets: + for hda in data.dataset.history_associations: + if history.user and hda.history not in history.user.histories: + self.set_dataset_groups( data.dataset, [ self.get_public_group() ] ) + self.set_dataset_roles( data.dataset, [] ) + break + else: + self.set_dataset_groups( data.dataset, groups ) + self.set_dataset_roles( data.dataset, roles ) + + def get_public_group( self ): + return self.model.Group.get_public_group() + + def set_dataset_groups( self, dataset, groups ): + if isinstance( dataset, self.model.HistoryDatasetAssociation): + dataset = dataset.dataset + for assoc in dataset.groups: + assoc.delete() + assoc.flush() + for group in groups: + if not isinstance( group, self.model.Group ): + group = group.group + self.associate_components( dataset = dataset, group = group ) + def set_dataset_roles( self, dataset, roles ): + if isinstance( dataset, self.model.HistoryDatasetAssociation): + dataset = dataset.dataset + for assoc in dataset.roles: + assoc.delete() + assoc.flush() + for role in roles: + if not isinstance( role, self.model.Role ): + role = role.role + self.associate_components( dataset = dataset, 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 'group' in kwd: + return self.model.GroupDatasetAssociation.get_by( group_id = kwd['group'].id, dataset_id = kwd['dataset'].id ) + elif 'role' in kwd: + return self.model.RoleDatasetAssociation.get_by( role_id = kwd['role'].id, dataset_id = kwd['dataset'].id ) + elif 'user' in kwd: + if 'group' in kwd: + return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) + elif 'role' in kwd: + return self.model.UserRoleAssociation.get_by( user_id = kwd['user'].id, role_id = kwd['role'].id ) + elif 'role' in kwd: + if 'group' in kwd: + return self.model.GroupRoleAssociation.get_by( group_id = kwd['group'].id, role_id = kwd['role'].id ) + elif 'control_role' in kwd: + return self.model.RoleControlRoleAssociation.get_by( target_role_id = kwd['role'].id, role_id = kwd['control_role'].id ) + elif 'target_role' in kwd: + return self.model.RoleControlRoleAssociation.get_by( role_id = kwd['role'].id, target_role_id = kwd['target_role'].id ) + elif 'group' in kwd: + if 'control_role' in kwd: + return self.model.GroupControlRoleAssociation.get_by( group_id = kwd['group'].id, role_id = kwd['control_role'].id ) + raise 'No valid method of associating provided components: %s' % kwd + + + + + + diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index af94a3a24ce..4f646b7b6d3 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,8 +1085,8 @@ 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 ) - child_dataset.dataset.set_groups( outdata.dataset.groups ) - child_dataset.dataset.set_roles( outdata.dataset.roles ) + self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_roles( child_dataset.dataset, outdata.dataset.roles ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1123,8 +1123,8 @@ 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 ) - primary_data.dataset.set_groups( outdata.dataset.groups ) - primary_data.dataset.set_roles( outdata.dataset.roles ) + self.app.security_agent.set_dataset_groups( primary_data.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_roles( primary_data.dataset, outdata.dataset.roles ) 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 25393b29b7f..e3e042fd71c 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -43,7 +43,7 @@ class DefaultToolAction( object ): assoc.flush() data = new_data break - if data and not data.allow_action( trans.user, data.access_actions.USE ): + if data and not trans.app.security_agent.allow_action( trans.user, data.access_actions.USE, dataset = data ): raise "User does not have permission to use a dataset (%s) provided for input." % data.id return data if isinstance( input, DataToolParameter ): @@ -85,7 +85,7 @@ class DefaultToolAction( object ): #determine output dataset access list existing_datasets = [ inp for inp in inp_data.values() if inp ] if existing_datasets: - output_access_groups, output_access_roles = existing_datasets[0].dataset.guess_derived_groups_roles( existing_datasets[1:] ) + output_access_groups, output_access_roles = trans.app.security_agent.guess_derived_groups_roles_for_datasets( existing_datasets ) else: #no valid inputs, we will use history defaults output_access_roles = [ role.role for role in trans.history.default_roles ] @@ -130,10 +130,10 @@ class DefaultToolAction( object ): if ext == "input": ext = input_ext data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) - data.dataset.set_groups( output_access_groups ) - data.dataset.set_roles( output_access_roles ) # Commit the dataset immediately so it gets database assigned unique id data.flush() + trans.app.security_agent.set_dataset_groups( data.dataset, output_access_groups ) + trans.app.security_agent.set_dataset_roles( data.dataset, output_access_roles ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations @@ -197,7 +197,7 @@ class DefaultToolAction( object ): job.add_parameter( name, value ) for name, dataset in inp_data.iteritems(): if dataset: - if not dataset.allow_action( trans.user, dataset.access_actions.USE ): + if not trans.app.security_agent.allow_action( trans.user, dataset.access_actions.USE, 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: diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 6d7675a9456..195fb3ef884 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -66,9 +66,9 @@ class UploadToolAction( object ): def upload_empty(self, trans, err_code, err_msg): data = trans.app.model.HistoryDatasetAssociation( create_dataset = True ) - data.dataset.set_groups( trans.history.default_groups ) - data.dataset.set_roles( trans.history.default_roles ) - data.name = err_code + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_roles( data.dataset, trans.history.default_roles ) + data.name = err_code data.extension = "txt" data.dbkey = "?" data.info = err_msg @@ -161,8 +161,8 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) - data.dataset.set_groups( trans.history.default_groups ) - data.dataset.set_roles( trans.history.default_roles ) + trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_roles( data.dataset, trans.history.default_roles ) 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 379529b8cab..5c598e65406 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -981,27 +981,30 @@ class DataToolParameter( ToolParameter ): >>> # Mock up a history (not connected to database) >>> from galaxy.model import History, HistoryDatasetAssociation, User, Role, Permission, Group, GroupRoleAssociation >>> from galaxy.util.bunch import Bunch + >>> from galaxy.security import GalaxyRBACAgent + >>> import galaxy.model + >>> security_agent = GalaxyRBACAgent( galaxy.model ) >>> hist = History() >>> hist.flush() >>> permission = Permission( 'test', list( Permission.dataset_actions.__dict__.values() ) ) >>> permission.flush() >>> role = Role( 'test' ) >>> role.flush() - >>> assoc = role.add_permission( permission ) + >>> assoc = security_agent.associate_components( role = role, permission = permission ) >>> group = Group( 'test' ) >>> group.flush() >>> Group.public_id = group.id - >>> GroupRoleAssociation( group, role ).flush() + >>> assoc = security_agent.associate_components( group = group, role = role ) >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> dataset1.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset1, [ group ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) - >>> dataset2.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset2, [ group ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> dataset3.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset3, [ group ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> dataset4.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset4, [ group ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) - >>> dataset5.dataset.set_groups( [ group ] ) + >>> security_agent.set_dataset_groups( dataset5, [ group ] ) >>> hist.add_dataset( dataset1 ) >>> hist.add_dataset( dataset2 ) >>> hist.add_dataset( dataset3 ) @@ -1010,7 +1013,7 @@ class DataToolParameter( ToolParameter ): >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah - >>> print p.get_html( trans=Bunch( history=hist, user=None ) ) + >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) ) diff --git a/templates/history/options.mako b/templates/history/options.mako index 1bb2794a32b..4bebc932dca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -18,7 +18,7 @@ %endif %if app.config.enable_beta_features:
  • Construct workflow from the current history
  • -
  • Change default permissions for the current history
  • +
  • Change default permitted actions for the current history
  • %endif
  • Share current history %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako index 315609189cd..6607f002c41 100644 --- a/templates/history/permissions.mako +++ b/templates/history/permissions.mako @@ -1,11 +1,11 @@ <%inherit file="/base.mako"/> -<%def name="title()">Change Default History Permissions +<%def name="title()">Change Default History Permitted Actions %if trans.user:
    -
    Change Default History Permissions
    +
    Change Default History Permitted Actions
    -
    +
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> @@ -29,12 +29,12 @@
    - This will change the default permissions assigned to new datasets for your current history. + This will change the default permitted actions assigned to new datasets for your current history.
    - +
    diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index d59e8cdcf17..607745ab20b 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,7 @@ ## Body for history items, extra info and actions, data "peek"
    - %if not trans.app.security_agent.allow_action( trans.user, data.access_actions.VIEW, dataset = data.dataset ): + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data.dataset ):
    You do not have permision to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    diff --git a/templates/user/index.mako b/templates/user/index.mako index 64d23dd2410..1047466aad0 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -9,7 +9,7 @@
  • Change your password
  • Update your email address
  • %if app.config.enable_beta_features: -
  • Change default permissions for new histories
  • +
  • Change default permitted actions for new histories
  • %endif
  • Logout
  • diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako index 1b0803d44df..7b6b90f976d 100644 --- a/templates/user/permissions.mako +++ b/templates/user/permissions.mako @@ -1,11 +1,11 @@ <%inherit file="/base.mako"/> -<%def name="title()">Change Default History Permissions +<%def name="title()">Change Default History Permitted Actions %if trans.user:
    -
    Change Default Permissions for new Histories
    +
    Change Default Permitted Actions for new Histories
    -
    +
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> @@ -29,12 +29,12 @@
    - This will change the default permissions assigned to new datasets for new histories. + This will change the default permitted actions assigned to new datasets for new histories.
    - +
    diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index 6a706b5ab04..ddfbb0241e0 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -38,8 +38,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + description + ")" history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) - app.security_agent.set_dataset_roles( newdata.dataset, base_dataset.dataset.roles ) app.model.flush() try: copyfile(filepath,newdata.file_name) diff --git a/tools/data_source/microbial_import_code.py b/tools/data_source/microbial_import_code.py index ad6a877e353..e8816f093ff 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -129,8 +129,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + microbe_info[kingdom][org]['chrs'][chr]['data'][description]['feature'] +" for "+microbe_info[kingdom][org]['name']+":"+chr + ")" newdata.flush() + #TODO, Nate: Make sure the following is functionally correct app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) - app.security_agent.set_dataset_roles( newdata.dataset, base_dataset.dataset.roles ) history.add_dataset( newdata ) app.model.flush() try: diff --git a/tools/maf/maf_to_bed_code.py b/tools/maf/maf_to_bed_code.py index d3784661188..428486b3e37 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -32,8 +32,8 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.name = basic_name + " (" + dbkey + ")" newdata.flush() history.add_dataset( newdata ) + #TODO, Nate: Make sure the following is functionally correct app.security_agent.set_dataset_groups( newdata.dataset, output_data.dataset.groups ) - app.security_agent.set_dataset_roles( newdata.dataset, output_data.dataset.roles ) newdata.flush() history.flush() app.model.flush() From dc9ffe68605089a3fe1e0684859c7d689a20036c Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Mon, 11 Aug 2008 16:58:48 -0400 Subject: [PATCH 010/267] Actions have been reduced to 3, and the public group is now the one named 'public' (names, for now, will be unique). --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/security/__init__.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 58a4f596104..cadd7a78ef0 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -144,7 +144,7 @@ class Group( object ): def guess_public_group( cls ): # TODO, Nate: Make sure this method is functionally correct. #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) + cls.set_public_group( Group.select_by( name = 'public' ) ) class UserGroupAssociation( object ): def __init__( self, user, group ): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index ebccb02e005..6582da8c368 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -14,17 +14,9 @@ log = logging.getLogger(__name__) class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( - dataset_actions = Bunch( VIEW = 'dataset_view', #viewing/downloading - USE = 'dataset_use', #use in jobs - ADD_GROUP = 'dataset_add_group', #dataset can be added to groups - REMOVE_GROUP = 'dataset_remove_group' #dataset can be removed from groups - ), - group_actions = Bunch( ADD_DATASET = 'group_add_dataset', #add dataset to group - REMOVE_DATASET = 'group_remove_dataset', #remove dataset from group - DELETE = 'group_delete', #delete a group - ADD_USER = 'group_add_user', #add users to group - REMOVE_USER = 'group_remove_user' #remove user from group - ) + EDIT_METADATA = 'edit_metadata', + MANAGE_PERMISSIONS = 'manage_permissions', + ACCESS = 'access' ) def allow_action( self, user, action, **kwd ): raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) From df6ab911b103db183ba3d270e83a58f3b1d504d8 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 12 Aug 2008 14:19:45 -0400 Subject: [PATCH 011/267] Fix references to actions removed in a previous commit. --- lib/galaxy/model/__init__.py | 6 ++---- lib/galaxy/security/__init__.py | 19 +++++++++++++++---- lib/galaxy/tools/actions/__init__.py | 4 ++-- lib/galaxy/tools/parameters/basic.py | 4 ++-- lib/galaxy/web/controllers/dataset.py | 4 ++-- lib/galaxy/web/controllers/root.py | 11 +++++++---- templates/dataset/edit_attributes.mako | 2 ++ templates/root/history_common.mako | 4 ++-- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index cadd7a78ef0..c47235036a0 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -104,8 +104,6 @@ class JobToOutputDatasetAssociation( object ): self.dataset = dataset class GroupDatasetAssociation( object ): - dataset_actions = RBACAgent.permitted_actions.dataset_actions - group_actions = RBACAgent.permitted_actions.group_actions def __init__( self, group, dataset, permitted_actions=[] ): if isinstance( group, GroupDatasetAssociation ) or \ isinstance( group, DefaultUserGroupAssociation ) or \ @@ -125,7 +123,7 @@ class GroupDatasetAssociation( object ): class Group( object ): public_id = None - permitted_actions = GroupDatasetAssociation.group_actions + permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) def __init__( self, name = None, priority = 0 ): self.name = name self.priority = priority @@ -179,7 +177,7 @@ class Dataset( object ): EMPTY = 'empty', ERROR = 'error', DISCARDED = 'discarded' ) - permitted_actions = GroupDatasetAssociation.dataset_actions + 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 ): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 6582da8c368..94315714c4f 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -14,9 +14,9 @@ log = logging.getLogger(__name__) class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( - EDIT_METADATA = 'edit_metadata', - MANAGE_PERMISSIONS = 'manage_permissions', - ACCESS = 'access' + DATASET_EDIT_METADATA = 'dataset_edit_metadata', + DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', + DATASET_ACCESS = 'dataset_access' ) def allow_action( self, user, action, **kwd ): raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) @@ -68,7 +68,7 @@ class GalaxyRBACAgent( RBACAgent ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using # This may need to change when the ability to alter groups and permitted_actions is allowed - if action in [ self.permitted_actions.dataset_actions.USE, self.permitted_actions.dataset_actions.VIEW ] and \ + if action == self.permitted_actions.DATASET_ACCESS and \ self.components_are_associated( group = self.get_public_group(), dataset = dataset ): return True elif user is not None: @@ -229,3 +229,14 @@ class GalaxyRBACAgent( RBACAgent ): if 'group' in kwd: return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) raise 'No valid method of associating provided components: %s' % kwd + +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/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index deb54fe6a0a..bd74d188ee6 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -44,7 +44,7 @@ class DefaultToolAction( object ): data = new_data break # 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.USE, dataset=data ): + 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 ): @@ -197,7 +197,7 @@ class DefaultToolAction( object ): 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.USE, dataset=dataset ): + 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: diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index d889d9dd79d..b626e665472 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -1065,7 +1065,7 @@ class DataToolParameter( ToolParameter ): hid = "%s.%d" % ( parent_hid, i + 1 ) else: hid = str( data.hid ) - if not data.deleted and data.state not in [data.states.ERROR] and data.visible and trans.app.security_agent.allow_action( trans.user, data.permitted_actions.USE, dataset = data ): + if not data.deleted and data.state not in [data.states.ERROR] and data.visible and trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): if self.options and data.get_dbkey() != filter_value: continue if isinstance( data.datatype, self.formats): @@ -1079,7 +1079,7 @@ class DataToolParameter( ToolParameter ): data = datasets[0] elif not self.converter_safe( other_values, trans ): continue - if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.USE, dataset = data ): + if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): continue selected = ( value and ( data in value ) ) field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected ) diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index 0f2f52dcc01..d38537509cd 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -107,7 +107,7 @@ class DatasetInterface( BaseController ): if not data: raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) # TODO, Nate: Make sure the following is functionally correct. - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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) @@ -127,4 +127,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) \ No newline at end of file + raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index e6328ba3c8a..8841c46eedd 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -139,7 +139,7 @@ class RootController( BaseController ): except: return "Dataset id '%s' is invalid" %str( id ) if data: - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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: @@ -171,7 +171,7 @@ class RootController( BaseController ): if data: child = data.get_child_by_designation( designation ) if child: - if trans.app.security_agent.allow_action( trans.user, child.permitted_actions.VIEW, dataset = child ): + 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." @@ -184,7 +184,7 @@ 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: - if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data ): + 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 ) @@ -219,7 +219,7 @@ class RootController( BaseController ): return trans.show_error_message( "Problem retrieving dataset id %s with history id %s." % ( str( id ), str( hid ) ) ) if 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.USE, dataset = data ): + if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): p = util.Params(kwd, safe=False) if p.change: @@ -262,6 +262,8 @@ class RootController( BaseController ): if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) return trans.show_ok_message( msg, refresh_frames=['history'] ) + ''' + # Users can't currently change permissions or groups elif p.change_permision: """The user clicked the change_permision button on the 'Change permissions' form""" if not trans.user: @@ -283,6 +285,7 @@ class RootController( BaseController ): else: return trans.show_error_message( "You have not specified a valid change of permitted actions." ) return trans.show_ok_message( 'Permitted actions have been changed.', refresh_frames=['history'] ) + ''' data.datatype.before_edit( data ) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 314b7960e78..1479b15c20a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,6 +133,7 @@

    +<%doc> %if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.REMOVE_GROUP, dataset = data ) or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.ADD_GROUP, dataset = data ) ):

    Change Permitted Actions
    @@ -163,3 +164,4 @@
    %endif + diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 607745ab20b..aa9a5c238a5 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -32,7 +32,7 @@ ## Body for history items, extra info and actions, data "peek"
    - %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.VIEW, dataset = data.dataset ): + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ):
    You do not have permision to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    @@ -102,4 +102,4 @@
    - \ No newline at end of file + From d4474cd22fb0184c83f1d3e6180f3eed4fd68f6e Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 12 Aug 2008 14:53:22 -0400 Subject: [PATCH 012/267] Change back the way we guess the public group (as per Greg) and add a deleted column to Group so groups can be undeleted. ALTER TABLE galaxy_group ADD COLUMN deleted BOOLEAN; --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index c47235036a0..7dda26375bb 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -142,7 +142,7 @@ class Group( object ): def guess_public_group( cls ): # TODO, Nate: Make sure this method is functionally correct. #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select_by( name = 'public' ) ) + cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) class UserGroupAssociation( object ): def __init__( self, user, group ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index c99899d4e5a..8c11ec41a2c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -120,7 +120,8 @@ Group.table = Table( "galaxy_group", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "name", TEXT ), - Column( "priority", Integer ) ) + Column( "priority", Integer ), + Column( "deleted", Boolean, index=True, default=False ) ) UserGroupAssociation.table = Table( "user_group_association", metadata, Column( "id", Integer, primary_key=True ), From da0b09039cdb013e0ad15c325bc500abf003f7e3 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Tue, 12 Aug 2008 16:56:26 -0400 Subject: [PATCH 013/267] Initial check in of library functionality. The ui is Very rough, at best. Libraries can be accessed by users through a new tool under datasources. Libraries can be created and accessed for modification via a link on the admin page. Currently, all library datasets are currently associated with the public group. Tags have been defined (tables and mappings) and can be associated with a dataset or a folder, but there is currenty no interface for displaying or creating these. Creation and editing of a library should be an admin only function, however this check is not currently present. The creation/editing page for a dataset is currently hard coded, it should be dynamic (i.e. datatypes and dbkeys) Brief description of a library: A library is composed of a name, description and a root folder. A folder is composed of a name, description, and a parent folder (none when is the root), where a folder can have many or none child folders and datasets. A library dataset is composed of most of the same attributes as a history dataset, but it is linked via a folder_id to a folder (instead of a history_id to a history). Ordering of the library is maintained by the 'order_id' attribute of each folder and dataset object (which is populated by the item_count attribute of a folder) --- lib/galaxy/model/__init__.py | 123 ++- lib/galaxy/model/mapping.py | 118 ++- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/controllers/library.py | 218 +++++ lib/galaxy/web/framework/__init__.py | 7 +- templates/admin_main.mako | 6 + templates/form.mako | 2 + templates/library/admin_list_libraries.mako | 16 + templates/library/manage_dataset.mako | 100 ++ templates/library/manage_folder.mako | 191 ++++ templates/library/manage_library.mako | 46 + templates/library/new_dataset.mako | 997 ++++++++++++++++++++ templates/library/user_list_libraries.mako | 14 + templates/library/user_view_library.mako | 58 ++ tool_conf.xml.sample | 1 + tools/data_source/access_libraries.xml | 11 + 16 files changed, 1883 insertions(+), 27 deletions(-) create mode 100644 lib/galaxy/web/controllers/library.py create mode 100644 templates/library/admin_list_libraries.mako create mode 100644 templates/library/manage_dataset.mako create mode 100644 templates/library/manage_folder.mako create mode 100644 templates/library/manage_library.mako create mode 100644 templates/library/new_dataset.mako create mode 100644 templates/library/user_list_libraries.mako create mode 100644 templates/library/user_view_library.mako create mode 100644 tools/data_source/access_libraries.xml diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7dda26375bb..e6637b2ad7f 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -262,16 +262,16 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) -class HistoryDatasetAssociation( object ): + +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, copied_from_history_dataset_association = None, validation_errors=None, - visible=True, create_dataset = False ): + parent_id=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 @@ -282,14 +282,12 @@ class HistoryDatasetAssociation( object ): 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 ): @@ -399,10 +397,7 @@ class HistoryDatasetAssociation( object ): 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 ) + raise 'Unimplemented' def get_child_by_designation(self, designation): for child in self.children: if child.designation == designation: @@ -411,17 +406,6 @@ class HistoryDatasetAssociation( object ): def get_converter_types(self): return self.datatype.get_converter_types( self, datatypes_registry) - - def copy( self, copy_children = False, parent_id = None, target_user = None ): - if target_user is None: target_user = self.user - des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) - des.flush() - if copy_children: - for child in self.children: - child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs - des.flush() - return des def add_validation_error( self, validation_error ): self.validation_errors.append( validation_error ) @@ -436,6 +420,35 @@ class HistoryDatasetAssociation( object ): child.mark_deleted() + +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 ): + print "self.dataset", self.dataset + + des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) + print "des data", des.dataset + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + + def 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 @@ -499,9 +512,75 @@ class History( object ): des.hid_counter = self.hid_counter des.flush() return des - +class Library( object ): + def __init__( self, name = None, description = None, root_folder = None ): + self.name = name or "Unnamed library" + self.description = description + self.root_folder = root_folder + +class LibraryFolder( object ): + def __init__( self, name = None, description = None, order_id = None ): + self.name = name or "Unnamed folder" + self.description = description + self.item_count = item_count + self.order_id = order_id + def add_dataset( self, dataset ): + dataset.folder_id = self.id + dataset.order_id = self.item_count + self.item_count += 1 + def add_folder( self, folder ): + folder.parent_id = self.id + folder.order_id = self.item_count + self.item_count += 1 + +class LibraryFolderDatasetAssociation( DatasetInstance ): + def __init__( self, folder = None, order_id = None, copied_from_history_dataset_association = None, copied_from_library_folder_dataset_association = None, **kwd ): + DatasetInstance.__init__( self, **kwd ) + self.folder = folder + self.order_id = order_id + self.copied_from_history_dataset_association = copied_from_history_dataset_association + self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association + + def to_history_dataset_association( self, parent_id = None ): + des = HistoryDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_library_folder_dataset_association = self ) + des.flush() + for child in self.children: + child_copy = child.to_history_dataset_association( parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + + + def copy( self, copy_children = False, parent_id = None ): + des = LibraryFolderDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_library_folder_dataset_association = self ) + des.flush() + if copy_children: + for child in self.children: + child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) + des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + des.flush() + return des + + def clear_associated_files( self, metadata_safe = False, purge = False ): + return + + +class LibraryTag( object ): + def __init__( self, tag ): + self.tag = tag + +class LibraryTagFolderAssociation( object ): + def __init__( self, tag, folder ): + self.tag = tag + self.folder = folder + +class LibraryTagDatasetAssociation( object ): + def __init__( self, tag, dataset ): + self.tag = tag + self.dataset = dataset + # class Query( object ): # def __init__( self, name=None, state=None, tool_parameters=None, history=None ): # self.name = name or "Unnamed query" diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 8c11ec41a2c..6a8f982fc81 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -74,6 +74,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 ) ), @@ -157,6 +158,66 @@ DefaultHistoryGroupAssociation.table = Table( "default_history_group_association Column( "update_time", DateTime, default=now, onupdate=now ), Column( "permitted_actions", JSONType(), default=[] ) ) +LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=True ), + Column( "folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "order_id", Integer ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id", use_alter=True, name='history_dataset_association_dataset_id_fkey' ), nullable=True ), + Column( "copied_from_library_folder_dataset_association_id", Integer, ForeignKey( "library_folder_dataset_association.id", use_alter=True, name='library_folder_dataset_association_id_fkey' ), nullable=True ), + Column( "name", TrimmedString( 255 ) ), + Column( "info", TrimmedString( 255 ) ), + Column( "blurb", TrimmedString( 255 ) ), + Column( "peek" , TEXT ), + Column( "extension", TrimmedString( 64 ) ), + Column( "metadata", MetadataType(), key="_metadata" ), + Column( "parent_id", Integer, ForeignKey( "library_folder_dataset_association.id" ), nullable=True ), + Column( "designation", TrimmedString( 255 ) ), + Column( "deleted", Boolean, index=True, default=False ), + Column( "visible", Boolean ) ) + +Library.table = Table( "library", metadata, + Column( "id", Integer, primary_key=True ), + Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", TEXT ), + 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 ) ) + +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 ), @@ -298,6 +359,10 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab 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] ) ), + 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] ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), @@ -311,7 +376,10 @@ assign_mapper( context, Dataset, Dataset.table, properties=dict( history_associations=relation( HistoryDatasetAssociation, - primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ) + primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ) ), + library_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( Dataset.table.c.id == LibraryFolderDatasetAssociation.table.c.dataset_id ) ) ) ) @@ -365,6 +433,54 @@ assign_mapper( context, DefaultHistoryGroupAssociation, DefaultHistoryGroupAssoc properties=dict( history=relation( History, backref = "default_groups" ), group=relation( Group ) ) ) +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] ) ), + tags=relation( + LibraryTagFolderAssociation, + primaryjoin=( LibraryFolder.table.c.id == LibraryTagFolderAssociation.table.c.folder_id ), + backref=backref( "folders" ) ) + ) ) + +assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAssociation.table, + properties=dict( + dataset=relation( Dataset ), + folder=relation( + LibraryFolder, + backref=backref( "datasets" ) ), + copied_to_library_folder_dataset_associations=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "copied_from_library_folder_dataset_association", primaryjoin=( LibraryFolderDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + children=relation( + LibraryFolderDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), + backref=backref( "parent", primaryjoin=( LibraryFolderDatasetAssociation.table.c.parent_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id] ) ), + tags=relation( + LibraryTagDatasetAssociation, + primaryjoin=( LibraryFolderDatasetAssociation.table.c.id == LibraryTagDatasetAssociation.table.c.dataset_id ), + backref=backref( "datasets" ) ) + ) ) + +assign_mapper( context, LibraryTag, LibraryTag.table ) + +assign_mapper( context, LibraryTagFolderAssociation, LibraryTagFolderAssociation.table, + properties=dict( tag=relation( LibraryTag ), + folder=relation( LibraryFolder ) ) ) + +assign_mapper( context, LibraryTagDatasetAssociation, LibraryTagDatasetAssociation.table, + properties=dict( tag=relation( LibraryTag ), + dataset=relation( LibraryFolderDatasetAssociation ) ) ) + assign_mapper( context, JobToInputDatasetAssociation, JobToInputDatasetAssociation.table, properties=dict( job=relation( Job ), dataset=relation( HistoryDatasetAssociation ) ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 94315714c4f..0ba0bf6b621 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -1,5 +1,5 @@ """ -Utility functions used systemwide. +Galaxy Security """ import logging diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py new file mode 100644 index 00000000000..c8b1f6f0573 --- /dev/null +++ b/lib/galaxy/web/controllers/library.py @@ -0,0 +1,218 @@ + +from galaxy.web.base.controller import * +from galaxy.datatypes import sniff +import logging, shutil, StringIO + +log = logging.getLogger( __name__ ) + +class Library( BaseController ): + + @web.expose + def index( self, trans, library_id = None, import_ids = [], **kwd ): + #use for importing an entry into your history + if import_ids: + if not isinstance( import_ids, list ): + import_ids = [import_ids] + history = trans.get_history() + for id in import_ids: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() + history.add_dataset( dataset ) + dataset.flush() + history.flush() + return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) + elif library_id: + return trans.fill_template( 'library/user_view_library.mako', library = trans.app.model.Library.get( library_id ) ) + return trans.fill_template( 'library/user_list_libraries.mako', libraries = trans.app.model.Library.select() ) + + #make admin only + @web.expose + def manage_libraries( self, trans, **kwd ): + return trans.fill_template( 'library/admin_list_libraries.mako', libraries = trans.app.model.Library.select() ) + + #make admin only + @web.expose + def manage_library( self, trans, id=None, name="Unnamed", description=None, **kwd ): + if 'create_library' in kwd: + library = trans.app.model.Library( name = name, description = description ) + root_folder = trans.app.model.LibraryFolder( name = name, description = description ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + trans.response.send_redirect( web.url_for( action='manage_folder', id = root_folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) + library = trans.app.model.Library.get( id ) + if library: + return trans.fill_template( 'library/manage_library.mako', library = library ) + else: + return trans.show_error_message( "Invalid library specified" ) + + #make admin only + @web.expose + def manage_folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + if 'create_folder' in kwd: + folder = trans.app.model.LibraryFolder( name = name, description = description ) + if parent_id: + parent_folder = trans.app.model.LibraryFolder.get( parent_id ) + parent_folder.add_folder( folder ) + folder.flush() + trans.response.send_redirect( web.url_for( action='manage_folder', id = folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', None, 'parent_id', value = parent_id, use_label = False ) + .add_input( 'hidden', "Create Folder", 'create_folder', use_label = False ) ) + folder = trans.app.model.LibraryFolder.get( id ) + if folder: + msg = '' + if 'rename_folder' in kwd: + folder.name = name + folder.description = description + folder.flush() + msg = 'Folder has been renamed.' + return trans.fill_template( 'library/manage_folder.mako', folder = folder, msg = msg ) + else: + return trans.show_error_message( "Invalid folder specified" ) + + + #make admin only + @web.expose + def manage_dataset( self, trans, id=None, name="Unnamed", info = 'no info', extension = None, folder_id = None, dbkey = None, **kwd ): + data_files = [] + def add_file( file_obj, name, extension, dbkey, info = 'no info', space_to_tab = False ): + data_type = None + temp_name = sniff.stream_to_file( file_obj ) + if space_to_tab: + line_count = sniff.convert_newlines_sep2tabs( temp_name ) + else: + line_count = sniff.convert_newlines( temp_name ) + if extension == 'auto': + data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) + else: + data_type = extension + dataset = trans.app.model.LibraryFolderDatasetAssociation( name = name, info = info, extension = data_type, dbkey = dbkey, create_dataset = True ) + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( dataset ) + dataset.flush() + # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public + trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + 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 + if 'create_dataset' in kwd: + #copied from upload tool action + last_dataset_created = None + data_file = kwd['file_data'] + url_paste = kwd['url_paste'] + space_to_tab = False + if 'space_to_tab' in kwd: + if kwd['space_to_tab'] not in ["None", None]: + space_to_tab = True + temp_name = "" + data_list = [] + + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + last_dataset_created = add_file( data_file.file, file_name, extension, dbkey, info="uploaded file", space_to_tab = space_to_tab ) + elif url_paste not in [ None, "" ]: + if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + url_paste = url_paste.replace( '\r', '' ).split( '\n' ) + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + last_dataset_created = add_file( urllib.urlopen( line ), line, extension, dbkey, info="uploaded url", space_to_tab=space_to_tab ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + last_dataset_created = add_file( StringIO.StringIO( url_paste ), 'Pasted Entry', extension, dbkey, info="pasted entry", space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='manage_dataset', id = last_dataset_created.id ) ) + #return self.manage_dataset( trans, id = last_dataset_created.id ) + elif id is None: + return trans.fill_template( 'library/new_dataset.mako', folder_id = folder_id ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if dataset: + #copied from edit attributes for 'regular' datasets + 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( dataset, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + dataset.name = name + dataset.info = info + + # The following for loop will save all metadata_spec items + for name, spec in dataset.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(dataset.metadata,name,None) + else: + setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) + + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + for name, spec in dataset.datatype.metadata_spec.items(): + # We need to be careful about the attributes we are resetting + if name != 'name' and name != 'info' and name != 'dbkey': + if spec.get( 'default' ): + setattr( dataset.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) + dataset.datatype.set_meta( dataset ) + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + + dataset.datatype.before_edit( dataset ) + + if "dbkey" in dataset.datatype.metadata_spec and not dataset.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 + dataset.metadata.dbkey = dataset.dbkey + metadata = list() + # a list of MetadataParemeters + for name, spec in dataset.datatype.metadata_spec.items(): + if spec.visible: + metadata.append( spec.wrap( dataset.metadata.get(name), dataset ) ) + # let's not overwrite the imported datatypes module with the variable datatypes? + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + return trans.fill_template( "/library/manage_dataset.mako", dataset=dataset, metadata=metadata, + datatypes=ldatatypes, err=None ) + else: + return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 529cb6156b0..124d1549453 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -527,8 +527,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 ) @@ -539,13 +539,14 @@ class FormInput( object ): """ Simple class describing a form input element """ - def __init__( self, type, name, label, value=None, error=None, help=None ): + def __init__( self, type, name, label, value=None, error=None, help=None, use_label=True ): self.type = type self.name = name self.label = label self.value = value self.error = error self.help = help + self.use_label = use_label class FormData( object ): """ diff --git a/templates/admin_main.mako b/templates/admin_main.mako index f9c26121124..02e39576685 100644 --- a/templates/admin_main.mako +++ b/templates/admin_main.mako @@ -29,5 +29,11 @@ + + + Manage Libraries + + + diff --git a/templates/form.mako b/templates/form.mako index c003de02585..42fdb745ba3 100644 --- a/templates/form.mako +++ b/templates/form.mako @@ -21,9 +21,11 @@ $(function(){ cls += " form-row-error" %>
    + %if input.use_label: + %endif
    diff --git a/templates/library/admin_list_libraries.mako b/templates/library/admin_list_libraries.mako new file mode 100644 index 00000000000..ffe82787da2 --- /dev/null +++ b/templates/library/admin_list_libraries.mako @@ -0,0 +1,16 @@ +<%inherit file="/base.mako"/> +<%def name="title()">View Libraries + +
    +
    Manage Libraries
    +
    + %for library in libraries: + + %endfor + +
    +
    diff --git a/templates/library/manage_dataset.mako b/templates/library/manage_dataset.mako new file mode 100644 index 00000000000..f04615def51 --- /dev/null +++ b/templates/library/manage_dataset.mako @@ -0,0 +1,100 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Edit Dataset Attributes + + +<%def name="datatype( dataset, datatypes )"> + + + +
    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + %for element in metadata: +
    + +
    + ${element.get_html()} +
    +
    +
    + %endfor +
    + +
    +
    +
    + +
    + +
    +
    + This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
    +
    +
    +
    + +

    + + +

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

    diff --git a/templates/library/manage_folder.mako b/templates/library/manage_folder.mako new file mode 100644 index 00000000000..19e12555625 --- /dev/null +++ b/templates/library/manage_folder.mako @@ -0,0 +1,191 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> + + + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + ##

    +
    +
    ${data.display_name()}
    +
    +
    + + ## Header row for history items (name, state, action buttons) + +
    + %if data_state != 'ok': +
    + %endif +
    +
    + ##display data + edit attributes + ##delete +
    + ##${data.display_name()} +
    + + ## Body for history items, extra info and actions, data "peek" + +
    + %if data_state == "queued": +
    Job is waiting to run
    + %elif data_state == "running": +
    Job is currently running
    + %elif data_state == "error": +
    + An error occurred running this job: ${data.display_info().strip()}, + report this error +
    + %elif data_state == "empty": +
    No data: ${data.display_info()}
    + %elif data_state == "ok": +
    + ${data.blurb}, + format: ${data.ext}, + database: + %if data.dbkey == '?': + ${data.dbkey} + %else: + ${data.dbkey} + %endif +
    +
    Info: ${data.display_info()}
    + %if data.peek != "no peek": +
    ${data.display_peek()}
    + %endif + %else: +
    Error: unknown dataset state "${data_state}".
    + %endif + + ## Recurse for child datasets + + +
    +
    +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> + +
    +
    Contents of Folder: ${this_folder.name}
    +
    +
    +
    + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + %for component in components: + ${render_component( component )} + %endfor + +
    +
    +
    + +
    + +
    +
    + + + + + + +<%def name="title()">Manage Folder: ${folder.name} + +
    +
    Change Folder Attributes
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    Manage Folder Contents: ${folder.name}
    +
    +
    + %if folder.parent: + up a level + %elif folder.library_root: + manage library + %endif +
    +
    +
    + ${render_folder( folder )} +
    + +
    + +
    + +
    +
    + + diff --git a/templates/library/manage_library.mako b/templates/library/manage_library.mako new file mode 100644 index 00000000000..5d94dfb6b78 --- /dev/null +++ b/templates/library/manage_library.mako @@ -0,0 +1,46 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Manage Library + + +
    +
    Edit a Library: ${library.name}
    +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + diff --git a/templates/library/new_dataset.mako b/templates/library/new_dataset.mako new file mode 100644 index 00000000000..1c517cdfede --- /dev/null +++ b/templates/library/new_dataset.mako @@ -0,0 +1,997 @@ +<%inherit file="/base.mako"/> +<%def name="title()">Create New Library Dataset + +
    +
    Create a new Library Dataset
    +
    +
    + + + +
    + + +
    + + +
    + + +
    + +
    + + +
    + +
    + Here you may specify a list of URLs (one per line) or paste the contents of a file. +
    + + + +
    + +
    + +
    + + +
    Yes
    + +
    + + Use this option if you are entering intervals by hand. +
    + + +
    + +
    + +
    + + +
    + +
    + Which format? See help below +
    + + +
    + +
    + +
    + + +##this should be generated dynamically +
    + + + +
    + +
    + + +
    + +
    + + +
    +
    +
    + diff --git a/templates/library/user_list_libraries.mako b/templates/library/user_list_libraries.mako new file mode 100644 index 00000000000..23a2b3b9ad1 --- /dev/null +++ b/templates/library/user_list_libraries.mako @@ -0,0 +1,14 @@ +<%inherit file="/base.mako"/> +<%def name="title()">View Libraries + +
    +
    View Library
    +
    + %for library in libraries: + + %endfor + +
    +
    diff --git a/templates/library/user_view_library.mako b/templates/library/user_view_library.mako new file mode 100644 index 00000000000..3b948558f28 --- /dev/null +++ b/templates/library/user_view_library.mako @@ -0,0 +1,58 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> + + + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> +
    + ${data.name} +
    + + +## Render a folder +<%def name="render_folder( this_folder )"> + +
    + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> +
    + %for component in components: + ${render_component( component )} + %endfor +
    +
    + + + + + + + +<%def name="title()">View Library: ${library.name} + +
    +
    Import from Library: ${library.name}
    +
    +
    + ${render_folder( library.root_folder )} + +
    + +
    + +
    +
    + diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index e46f0c54ad1..69ccfe583ee 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -12,6 +12,7 @@ +
    diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml new file mode 100644 index 00000000000..ffd383c481a --- /dev/null +++ b/tools/data_source/access_libraries.xml @@ -0,0 +1,11 @@ + + + + stored locally + + + + + + + From f0396dbc188aa381e9fa2d953900583e08e010ad Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 14 Aug 2008 16:18:06 -0400 Subject: [PATCH 014/267] First pass at administration components that incorporate dataset security and libraries. --- lib/galaxy/config.py | 2 +- lib/galaxy/model/__init__.py | 15 +- lib/galaxy/model/mapping.py | 12 +- lib/galaxy/security/__init__.py | 7 +- lib/galaxy/web/controllers/admin.py | 680 +++++++++++- lib/galaxy/web/controllers/library.py | 201 +--- .../admin/dataset_security/group_create.mako | 96 ++ .../group_dataset_permitted_actions_edit.mako | 71 ++ .../admin/dataset_security/group_members.mako | 47 + .../dataset_security/group_members_edit.mako | 114 ++ templates/admin/dataset_security/groups.mako | 67 ++ templates/admin/dataset_security/index.mako | 13 + .../specified_users_groups.mako | 56 + templates/admin/dataset_security/users.mako | 80 ++ templates/admin/index.mako | 13 + templates/admin/library/dataset.mako | 85 ++ templates/admin/library/folder.mako | 158 +++ templates/admin/library/libraries.mako | 19 + templates/admin/library/library.mako | 35 + templates/admin/library/new_dataset.mako | 953 +++++++++++++++++ templates/admin/reload_tool.mako | 28 + templates/admin_main.mako | 39 - templates/library/admin_list_libraries.mako | 16 - templates/library/libraries.mako | 13 + .../{user_view_library.mako => library.mako} | 52 +- templates/library/manage_dataset.mako | 100 -- templates/library/manage_folder.mako | 191 ---- templates/library/manage_library.mako | 46 - templates/library/new_dataset.mako | 997 ------------------ templates/library/user_list_libraries.mako | 14 - templates/root/masthead.mako | 3 + universe_wsgi.ini.sample | 4 +- 32 files changed, 2557 insertions(+), 1670 deletions(-) create mode 100644 templates/admin/dataset_security/group_create.mako create mode 100644 templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako create mode 100644 templates/admin/dataset_security/group_members.mako create mode 100644 templates/admin/dataset_security/group_members_edit.mako create mode 100644 templates/admin/dataset_security/groups.mako create mode 100644 templates/admin/dataset_security/index.mako create mode 100644 templates/admin/dataset_security/specified_users_groups.mako create mode 100644 templates/admin/dataset_security/users.mako create mode 100644 templates/admin/index.mako create mode 100644 templates/admin/library/dataset.mako create mode 100644 templates/admin/library/folder.mako create mode 100644 templates/admin/library/libraries.mako create mode 100644 templates/admin/library/library.mako create mode 100644 templates/admin/library/new_dataset.mako create mode 100644 templates/admin/reload_tool.mako delete mode 100644 templates/admin_main.mako delete mode 100644 templates/library/admin_list_libraries.mako create mode 100644 templates/library/libraries.mako rename templates/library/{user_view_library.mako => library.mako} (58%) delete mode 100644 templates/library/manage_dataset.mako delete mode 100644 templates/library/manage_folder.mako delete mode 100644 templates/library/manage_library.mako delete mode 100644 templates/library/new_dataset.mako delete mode 100644 templates/library/user_list_libraries.mako diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 432499bef7a..ac54a889b7a 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -45,7 +45,7 @@ class Configuration( object ): self.job_scheduler_policy = kwargs.get("job_scheduler_policy", "FIFO") self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") ) self.job_working_directory = resolve_path( kwargs.get( "job_working_directory", "database/job_working_directory" ), self.root ) - self.admin_pass = kwargs.get('admin_pass',"galaxy") + self.admin_users = kwargs.get( "admin_users", "" ) self.sendmail_path = kwargs.get('sendmail_path',"/usr/sbin/sendmail") self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-user-join@bx.psu.edu") self.error_email_to = kwargs.get( 'error_email_to', None ) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index e6637b2ad7f..460b6433b1d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -129,20 +129,18 @@ class Group( object ): self.priority = priority @classmethod def get_public_group( cls ): - # TODO, Nate: Make sure this method is functionally correct. return Group.get( cls.public_id ) @classmethod def set_public_group( cls, group ): - # TODO, Nate: Make sure this method is functionally correct. - #we store the id instead of the object, because of alchemy sessions + # We store the id instead of the object, because of alchemy sessions if isinstance( group, Group ): group = group.id cls.public_id = group @classmethod def guess_public_group( cls ): - # TODO, Nate: Make sure this method is functionally correct. - #retrieve from database and store public group id, assume first created group is public - cls.set_public_group( Group.select( order_by = Group.table.c.create_time )[0] ) + # Retrieve from database and store public group id + group = Group.select_by( name='public' )[0] + cls.set_public_group( group ) class UserGroupAssociation( object ): def __init__( self, user, group ): @@ -262,7 +260,6 @@ class Dataset( object ): except OSError, e: log.critical('%s delete error %s' % (self.__class__.__name__, e)) - class DatasetInstance( object ): """A base class for all 'dataset instances', HDAs, LDAs, etc""" states = Dataset.states @@ -419,8 +416,6 @@ class DatasetInstance( object ): for child in self.children: child.mark_deleted() - - 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 ) @@ -521,7 +516,7 @@ class Library( object ): self.root_folder = root_folder class LibraryFolder( object ): - def __init__( self, name = None, description = None, order_id = None ): + 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 diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 6a8f982fc81..e6f132693da 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -139,9 +139,7 @@ GroupDatasetAssociation.table = Table( "group_dataset_association", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "permitted_actions", JSONType(), default=[] ) ) -# TODO, Nate: Need to better understand what these Default tables are for and add appropriate -# comments here to clarify them. Need to ensure that they should include the permitted_actions -# columns, and if so, that they are correctly populated. +# The following table stores the permissions that are considered the defaults for new histories when they are created by a user DefaultUserGroupAssociation.table = Table( "default_user_group_association", metadata, Column( "id", Integer, primary_key=True ), Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), @@ -150,6 +148,8 @@ DefaultUserGroupAssociation.table = Table( "default_user_group_association", met Column( "update_time", DateTime, default=now, onupdate=now ), Column( "permitted_actions", JSONType(), default=[] ) ) +# The following table stores the default permissions assigned to histories for datasets +# that need permissions ( dataset permissions that cannot be determined based on ancestor ) DefaultHistoryGroupAssociation.table = Table( "default_history_group_association", metadata, Column( "id", Integer, primary_key=True ), Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), @@ -417,10 +417,6 @@ assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, properties=dict( user=relation( User, backref = "groups" ), group=relation( Group, backref = "users" ) ) ) - -# TODO, Nate: Need to make sure we have optimal performance - may need more mappers... -# if we have a user and a list of datasets, what is the fastest -# way to ask whether the user has a certain action on all of them. assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), group=relation( Group, backref = "datasets" ) ) ) @@ -609,7 +605,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): if result.Group.count() == 0: log.warning( "There were no groups located, setting up default (public) group." ) # Create public group - public_group = result.security_agent.create_group( name = 'public' ) + public_group = result.security_agent.create_group( name='public' ) # Store public group id result.security_agent.set_public_group( public_group ) # Loop through all histories and set up rbac on users, histories and datasets diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 0ba0bf6b621..ed73ec2364e 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -13,9 +13,14 @@ log = logging.getLogger(__name__) # are correct when an authenticated user creates things inside their "private" environment. class RBACAgent: """Class that handles galaxy security""" - permitted_actions = Bunch( + permitted_actions = Bunch( + # The ability to edit the metadata of the associated dataset DATASET_EDIT_METADATA = 'dataset_edit_metadata', + # The ability to change the permissions of a dataset (so specifically, to add and modify + # group_dataset_association rows where the dataset is the dataset for which the permission is set). DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', + # The ability to perform any read only operation on the dataset (view, display at external site, + # use in a job, etc). DATASET_ACCESS = 'dataset_access' ) def allow_action( self, user, action, **kwd ): diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fd9b4be90d8..3367ce6bc36 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,24 +1,676 @@ +import shutil, StringIO from galaxy.web.base.controller import * -import logging, sets, time +from galaxy.datatypes import sniff +from galaxy.security import RBACAgent +import galaxy.model +from xml.sax.saxutils import escape, unescape +import pkg_resources +pkg_resources.require( "sqlalchemy>=0.3" ) +import sqlalchemy as sa +import logging log = logging.getLogger( __name__ ) +entities = { '@': 'FuNkYaT' } +unentities = { 'FuNkYaT' : '@' } +no_privilege_msg = "You must have Galaxy administrator privileges to use this feature." + class Admin( BaseController ): + def user_is_admin( self, trans ): + admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) + if not admin_users: + return False + user = trans.get_user() + if not user: + return False + if not user.email in admin_users: + return False + return True @web.expose def index( self, trans, **kwd ): - msg = '' - if 'action' in kwd: - if kwd['action'] == "tool_reload": - msg = self.tool_reload( **kwd ) - return trans.fill_template( 'admin_main.mako', toolbox=self.app.toolbox, msg=msg ) - - def tool_reload( self, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) - if params.passwd==self.app.config.admin_pass: - tool_id = params.tool_id - self.app.toolbox.reload( tool_id ) - msg = 'Reloaded tool: ' + tool_id + msg = params.msg + return trans.fill_template( '/admin/index.mako', msg=msg ) + @web.expose + def reload_tool( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + @web.expose + def tool_reload( self, trans, tool_version=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + tool_id = params.tool_id + self.app.toolbox.reload( tool_id ) + msg = 'Reloaded tool: ' + tool_id + return trans.fill_template( '/admin/reload_tool.mako', toolbox=self.app.toolbox, msg=msg ) + @web.expose + def dataset_security( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/dataset_security/index.mako', msg=msg ) + + # Galaxy Group Stuff + @web.expose + def groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # This query retrieves groups that are not deleted and members of each group + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + sa.func.count( galaxy.model.User.table.c.id ).label( 'total_members' ) ), + whereclause = galaxy.model.Group.table.c.deleted == False, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.UserGroupAssociation.table + ).outerjoin( galaxy.model.User.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.Group.table.c.name, + galaxy.model.Group.table.c.priority ], + order_by = [ galaxy.model.Group.table.c.name ] ) + groups = [] + for row in q.execute(): + # This 2nd query retrieves the number of datasets and dataset permitted_actions associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row.total_members, + total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/groups.mako', + groups=groups, + msg=msg ) + @web.expose + def create_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email') ), + from_obj = [ galaxy.model.User.table ], + order_by = [ galaxy.model.User.table.c.email ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_create.mako', users=users, msg=msg ) + @web.expose + def new_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + name = unescape( params.name, unentities ) + if not name: + msg = "Please enter a name" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: - msg = 'Invalid password' - return msg + try: + priority = int( params.priority ) + except: + priority = 0 + # Create the group + group = galaxy.model.Group( name, priority ) + group.flush() + # Add the members + members = params.members + for user_id in members: + user = galaxy.model.User.get( user_id ) + # Create the UserGroupAssociation + user_group_association = galaxy.model.UserGroupAssociation( user, group ) + user_group_association.flush() + msg = "The new group has been created with priority %s and %s members" % ( str( priority ), str( len( members ) ) ) + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def group_members( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group_name = unescape( params.group_name, unentities ) + # This query retrieves all members of the group + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + whereclause = galaxy.model.UserGroupAssociation.table.c.group_id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.UserGroupAssociation.table, + galaxy.model.User.table ) ], + order_by = [ 'user_email' ] ) + members = [] + for row in q.execute(): + members.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_members.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + members=members, + msg=msg ) + @web.expose + def group_members_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group_name = unescape( params.group_name, unentities ) + members = params.members + # First get all users + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + order_by = [ 'user_email' ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + # Then get members of the group + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email' ) ), + whereclause = galaxy.model.UserGroupAssociation.table.c.group_id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.UserGroupAssociation.table, + galaxy.model.User.table ) ], + order_by = [ 'user_email' ] ) + members = [] + for row in q.execute(): + members.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/group_members_edit.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + users=users, + members=members, + msg=msg ) + @web.expose + def update_group_members( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + members = params.members + if members and not isinstance( members, list ): + # mako passes singleton lists as strings for some reason + members = [ members ] + # Handle case where admin removed all members from group + elif members is None: + members = [] + group = galaxy.model.Group.get( group_id ) + # This is tricky since we have default association tables with + # records referring to members of this group. Because of this, + # we'll need to handle changes to the member list rather than the + # simpler approach of deleting all existing members and creating + # new records for user_ids in the received members param. + # First remove existing members that are not in the received members param + for user_group_assoc in group.users: + if user_group_assoc.user_id not in members: + user = galaxy.model.User.get( user_group_assoc.user_id ) + # Delete DefaultUserGroupAssociations + for default_user_group_association in user.default_groups: + if default_user_group_association.group_id == group_id: + default_user_group_association.delete() + default_user_group_association.flush() + break # Should only be 1 record + # Delete DefaultHistoryGroupAssociations + for history in user.histories: + for default_history_group_association in history.default_groups: + if default_history_group_association.group_id == group_id: + default_history_group_association.delete() + default_history_group_association.flush() + # Delete the UserGroupAssociation + user_group_assoc.delete() + user_group_assoc.flush() + # Then add all new members to the group + for user_id in members: + user = galaxy.model.User.get( user_id ) + if user not in group.users: + user_group_association = galaxy.model.UserGroupAssociation( user, group ) + user_group_association.flush() + msg = "Group membership has been updated with a total of %s members" % len( members ) + trans.response.send_redirect( '/admin/group_members?group_id=%s&group_name=%s&msg=%s' % ( str( group_id ), params.group_name, msg ) ) + @web.expose + def group_dataset_permitted_actions( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = int( params.group_id ) + group_name = unescape( params.group_name, unentities ) + # Need to get all actions to send to the form + dataset_actions = [] + dpas = RBACAgent.permitted_actions + for dpa in dpas.items(): + if dpa[0].startswith( 'DATASET' ): + dataset_actions.append( dpa[1] ) + dataset_actions.sort() + q = sa.select( ( ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ) ), + whereclause = galaxy.model.GroupDatasetAssociation.table.c.id == group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table ) ] ) + gdas = [] + for row in q.execute(): + permitted_actions = [] + # Although there may be GroupDatasetAssociations, there may not be any permitted_actions on them + if row.permitted_actions: + for action in row.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + gdas.append( ( row.group_priority, + permitted_actions ) ) + break # Just need 1 row + return trans.fill_template( '/admin/dataset_security/group_dataset_permitted_actions_edit.mako', + group_id=group_id, + group_name=escape( group_name, entities ), + gdas=gdas, + dataset_actions=dataset_actions, + msg=msg ) + @web.expose + def group_dataset_permitted_actions_edit( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + group_id = int( params.group_id ) + actions = params.actions + if actions and not isinstance( actions, list ): + actions = [ actions ] + # Update the permitted_actions for every GroupDatasetAssociation of the Group + q = sa.update( galaxy.model.GroupDatasetAssociation.table, + whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, + values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) + result = q.execute() + msg = "The dataset permitted actions for the group have been updated, affecting %d rows in the group_dataset_association table" % result.rowcount + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def mark_group_deleted( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + group.deleted = True + group.flush() + msg = "The group has been marked as deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def deleted_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + # This query retrieves groups that are not deleted and members of each group + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), + sa.func.count( galaxy.model.User.table.c.id ).label( 'total_members' ) ), + whereclause = galaxy.model.Group.table.c.deleted == True, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.UserGroupAssociation.table + ).outerjoin( galaxy.model.User.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.Group.table.c.name, + galaxy.model.Group.table.c.priority ], + order_by = [ galaxy.model.Group.table.c.name ] ) + groups = [] + for row in q.execute(): + # This 2nd query retrieves the number of datasets and dataset permitted_actions associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row.total_members, + total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/deleted_groups.mako', + groups=groups, + msg=msg ) + @web.expose + def undelete_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + group.deleted = False + group.flush() + msg = "The group has been marked as not deleted." + trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) + @web.expose + def purge_group( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + group_id = params.group_id + group = galaxy.model.Group.get( group_id ) + # Remove members and all associations + for user_group_assoc in group.users: + user = galaxy.model.User.get( user_group_assoc.user_id ) + # Delete DefaultUserGroupAssociations + for default_user_group_association in user.default_groups: + if default_user_group_association.group_id == group_id: + default_user_group_association.delete() + default_user_group_association.flush() + break # Should only be 1 record + # Delete DefaultHistoryGroupAssociations + for history in user.histories: + for default_history_group_association in history.default_groups: + if default_history_group_association.group_id == group_id: + default_history_group_association.delete() + default_history_group_association.flush() + # Delete the UserGroupAssociation + user_group_assoc.delete() + user_group_assoc.flush() + # Delete the Group + group.delete() + group.flush() + msg = "The group has been purged from the database." + trans.response.send_redirect( '/admin/deleted_groups?msg=%s' % msg ) + + # Galaxy User Stuff + @web.expose + def users( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + q = sa.select( ( ( galaxy.model.User.table.c.id ).label( 'user_id' ), + ( galaxy.model.User.table.c.email ).label( 'user_email') ), + from_obj = [ galaxy.model.User.table ], + order_by = [ galaxy.model.User.table.c.email ] ) + users = [] + for row in q.execute(): + users.append( ( row.user_id, + escape( row.user_email, entities ) ) ) + return trans.fill_template( '/admin/dataset_security/users.mako', + users=users, + msg=msg ) + @web.expose + def specified_users_groups( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + user_id = int( params.user_id ) + user_email = unescape( params.user_email, unentities ) + # Get the groups to which the user belongs + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ), + ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ) ), + whereclause = galaxy.model.User.table.c.id == user_id, + from_obj = [ sa.outerjoin( galaxy.model.User.table, + galaxy.model.UserGroupAssociation.table ).outerjoin( galaxy.model.Group.table ) ], + order_by = [ 'group_name' ] ) + groups = [] + for row in q.execute(): + # Perform a 2nd query to get datasets associated with each group + q2 = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ), + sa.func.count( galaxy.model.Dataset.table.c.id ).label( 'total_datasets' ) ), + whereclause = galaxy.model.Group.table.c.id == row.group_id, + from_obj = [ sa.outerjoin( galaxy.model.Group.table, + galaxy.model.GroupDatasetAssociation.table + ).outerjoin( galaxy.model.Dataset.table ) ], + group_by = [ galaxy.model.Group.table.c.id, + galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) + for row2 in q2.execute(): + total_datasets = row2.total_datasets + permitted_actions = [] + # There may not yet be any GroupDatasetAssociations, in which case no + # actions will be found + if row2.permitted_actions: + for action in row2.permitted_actions: + permitted_actions.append( action.encode( 'ascii' ) ) + permitted_actions.sort() + groups.append( ( row.group_id, + escape( row.group_name, entities ), + row.group_priority, + row2.total_datasets, + permitted_actions ) ) + return trans.fill_template( '/admin/dataset_security/specified_users_groups.mako', + user_id=user_id, + user_email=escape( user_email, entities ), + groups=groups, + msg=msg ) + + # Galaxy Library Stuff + @web.expose + def libraries( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select() ) + @web.expose + def library( self, trans, id=None, name="Unnamed", description=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if 'create_library' in kwd: + library = trans.app.model.Library( name=name, description=description ) + root_folder = trans.app.model.LibraryFolder( name=name, description=description ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + trans.response.send_redirect( web.url_for( action='folder', id = root_folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) + library = trans.app.model.Library.get( id ) + if library: + return trans.fill_template( '/admin/library/library.mako', library = library ) + else: + return trans.show_error_message( "Invalid library specified" ) + @web.expose + def folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if 'create_folder' in kwd: + folder = trans.app.model.LibraryFolder( name = name, description = description ) + if parent_id: + parent_folder = trans.app.model.LibraryFolder.get( parent_id ) + parent_folder.add_folder( folder ) + folder.flush() + trans.response.send_redirect( web.url_for( action='folder', id = folder.id ) ) + elif id is None: + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) + .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) + .add_text( name = "description", label = "Description", value = None, error = None, help = None ) + .add_input( 'hidden', None, 'parent_id', value = parent_id, use_label = False ) + .add_input( 'hidden', "Create Folder", 'create_folder', use_label = False ) ) + folder = trans.app.model.LibraryFolder.get( id ) + if folder: + msg = '' + if 'rename_folder' in kwd: + folder.name = name + folder.description = description + folder.flush() + msg = 'Folder has been renamed.' + return trans.fill_template( '/admin/library/folder.mako', folder=folder, msg=msg ) + else: + return trans.show_error_message( "Invalid folder specified" ) + @web.expose + def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + data_files = [] + def add_file( file_obj, name, extension, dbkey, info = 'no info', space_to_tab = False ): + data_type = None + temp_name = sniff.stream_to_file( file_obj ) + if space_to_tab: + line_count = sniff.convert_newlines_sep2tabs( temp_name ) + else: + line_count = sniff.convert_newlines( temp_name ) + if extension == 'auto': + data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) + else: + data_type = extension + dataset = trans.app.model.LibraryFolderDatasetAssociation( name = name, info = info, extension = data_type, dbkey = dbkey, create_dataset = True ) + folder = trans.app.model.LibraryFolder.get( folder_id ) + folder.add_dataset( dataset ) + dataset.flush() + # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public + trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + 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 + if 'create_dataset' in kwd: + #copied from upload tool action + last_dataset_created = None + data_file = kwd['file_data'] + url_paste = kwd['url_paste'] + space_to_tab = False + if 'space_to_tab' in kwd: + if kwd['space_to_tab'] not in ["None", None]: + space_to_tab = True + temp_name = "" + data_list = [] + + if 'filename' in dir( data_file ): + file_name = data_file.filename + file_name = file_name.split( '\\' )[-1] + file_name = file_name.split( '/' )[-1] + last_dataset_created = add_file( data_file.file, file_name, extension, dbkey, info="uploaded file", space_to_tab = space_to_tab ) + elif url_paste not in [ None, "" ]: + if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + url_paste = url_paste.replace( '\r', '' ).split( '\n' ) + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + last_dataset_created = add_file( urllib.urlopen( line ), line, extension, dbkey, info="uploaded url", space_to_tab=space_to_tab ) + else: + is_valid = False + for line in url_paste: + line = line.rstrip( '\r\n' ) + if line: + is_valid = True + break + if is_valid: + last_dataset_created = add_file( StringIO.StringIO( url_paste ), 'Pasted Entry', extension, dbkey, info="pasted entry", space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='dataset', id = last_dataset_created.id ) ) + #return self.dataset( trans, id = last_dataset_created.id ) + elif id is None: + return trans.fill_template( '/admin/library/new_dataset.mako', folder_id = folder_id ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if dataset: + #copied from edit attributes for 'regular' datasets + 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( dataset, p.datatype ) + trans.app.model.flush() + elif p.save: + # The user clicked the Save button on the 'Edit Attributes' form + dataset.name = name + dataset.info = info + + # The following for loop will save all metadata_spec items + for name, spec in dataset.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(dataset.metadata,name,None) + else: + setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) + + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + elif p.detect: + # The user clicked the Auto-detect button on the 'Edit Attributes' form + for name, spec in dataset.datatype.metadata_spec.items(): + # We need to be careful about the attributes we are resetting + if name != 'name' and name != 'info' and name != 'dbkey': + if spec.get( 'default' ): + setattr( dataset.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) + dataset.datatype.set_meta( dataset ) + dataset.datatype.after_edit( dataset ) + trans.app.model.flush() + return trans.show_ok_message( "Attributes updated" ) + + dataset.datatype.before_edit( dataset ) + + if "dbkey" in dataset.datatype.metadata_spec and not dataset.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 + dataset.metadata.dbkey = dataset.dbkey + metadata = list() + # a list of MetadataParemeters + for name, spec in dataset.datatype.metadata_spec.items(): + if spec.visible: + metadata.append( spec.wrap( dataset.metadata.get(name), dataset ) ) + # let's not overwrite the imported datatypes module with the variable datatypes? + ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] + ldatatypes.sort() + return trans.fill_template( "/admin/library/dataset.mako", + dataset=dataset, + metadata=metadata, + datatypes=ldatatypes, + err=None ) + else: + return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index c8b1f6f0573..94e7b5731e0 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,12 +1,10 @@ from galaxy.web.base.controller import * -from galaxy.datatypes import sniff -import logging, shutil, StringIO +import logging log = logging.getLogger( __name__ ) class Library( BaseController ): - @web.expose def index( self, trans, library_id = None, import_ids = [], **kwd ): #use for importing an entry into your history @@ -21,198 +19,5 @@ class Library( BaseController ): history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) elif library_id: - return trans.fill_template( 'library/user_view_library.mako', library = trans.app.model.Library.get( library_id ) ) - return trans.fill_template( 'library/user_list_libraries.mako', libraries = trans.app.model.Library.select() ) - - #make admin only - @web.expose - def manage_libraries( self, trans, **kwd ): - return trans.fill_template( 'library/admin_list_libraries.mako', libraries = trans.app.model.Library.select() ) - - #make admin only - @web.expose - def manage_library( self, trans, id=None, name="Unnamed", description=None, **kwd ): - if 'create_library' in kwd: - library = trans.app.model.Library( name = name, description = description ) - root_folder = trans.app.model.LibraryFolder( name = name, description = description ) - root_folder.flush() - library.root_folder = root_folder - library.flush() - trans.response.send_redirect( web.url_for( action='manage_folder', id = root_folder.id ) ) - elif id is None: - return trans.show_form( - web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) - .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) - .add_text( name = "description", label = "Description", value = None, error = None, help = None ) - .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) - library = trans.app.model.Library.get( id ) - if library: - return trans.fill_template( 'library/manage_library.mako', library = library ) - else: - return trans.show_error_message( "Invalid library specified" ) - - #make admin only - @web.expose - def manage_folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): - if 'create_folder' in kwd: - folder = trans.app.model.LibraryFolder( name = name, description = description ) - if parent_id: - parent_folder = trans.app.model.LibraryFolder.get( parent_id ) - parent_folder.add_folder( folder ) - folder.flush() - trans.response.send_redirect( web.url_for( action='manage_folder', id = folder.id ) ) - elif id is None: - return trans.show_form( - web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) - .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) - .add_text( name = "description", label = "Description", value = None, error = None, help = None ) - .add_input( 'hidden', None, 'parent_id', value = parent_id, use_label = False ) - .add_input( 'hidden', "Create Folder", 'create_folder', use_label = False ) ) - folder = trans.app.model.LibraryFolder.get( id ) - if folder: - msg = '' - if 'rename_folder' in kwd: - folder.name = name - folder.description = description - folder.flush() - msg = 'Folder has been renamed.' - return trans.fill_template( 'library/manage_folder.mako', folder = folder, msg = msg ) - else: - return trans.show_error_message( "Invalid folder specified" ) - - - #make admin only - @web.expose - def manage_dataset( self, trans, id=None, name="Unnamed", info = 'no info', extension = None, folder_id = None, dbkey = None, **kwd ): - data_files = [] - def add_file( file_obj, name, extension, dbkey, info = 'no info', space_to_tab = False ): - data_type = None - temp_name = sniff.stream_to_file( file_obj ) - if space_to_tab: - line_count = sniff.convert_newlines_sep2tabs( temp_name ) - else: - line_count = sniff.convert_newlines( temp_name ) - if extension == 'auto': - data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) - else: - data_type = extension - dataset = trans.app.model.LibraryFolderDatasetAssociation( name = name, info = info, extension = data_type, dbkey = dbkey, create_dataset = True ) - folder = trans.app.model.LibraryFolder.get( folder_id ) - folder.add_dataset( dataset ) - dataset.flush() - # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public - trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) - 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 - if 'create_dataset' in kwd: - #copied from upload tool action - last_dataset_created = None - data_file = kwd['file_data'] - url_paste = kwd['url_paste'] - space_to_tab = False - if 'space_to_tab' in kwd: - if kwd['space_to_tab'] not in ["None", None]: - space_to_tab = True - temp_name = "" - data_list = [] - - if 'filename' in dir( data_file ): - file_name = data_file.filename - file_name = file_name.split( '\\' )[-1] - file_name = file_name.split( '/' )[-1] - last_dataset_created = add_file( data_file.file, file_name, extension, dbkey, info="uploaded file", space_to_tab = space_to_tab ) - elif url_paste not in [ None, "" ]: - if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: - url_paste = url_paste.replace( '\r', '' ).split( '\n' ) - for line in url_paste: - line = line.rstrip( '\r\n' ) - if line: - last_dataset_created = add_file( urllib.urlopen( line ), line, extension, dbkey, info="uploaded url", space_to_tab=space_to_tab ) - else: - is_valid = False - for line in url_paste: - line = line.rstrip( '\r\n' ) - if line: - is_valid = True - break - if is_valid: - last_dataset_created = add_file( StringIO.StringIO( url_paste ), 'Pasted Entry', extension, dbkey, info="pasted entry", space_to_tab=space_to_tab ) - trans.response.send_redirect( web.url_for( action='manage_dataset', id = last_dataset_created.id ) ) - #return self.manage_dataset( trans, id = last_dataset_created.id ) - elif id is None: - return trans.fill_template( 'library/new_dataset.mako', folder_id = folder_id ) - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if dataset: - #copied from edit attributes for 'regular' datasets - 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( dataset, p.datatype ) - trans.app.model.flush() - elif p.save: - # The user clicked the Save button on the 'Edit Attributes' form - dataset.name = name - dataset.info = info - - # The following for loop will save all metadata_spec items - for name, spec in dataset.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(dataset.metadata,name,None) - else: - setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) - - dataset.datatype.after_edit( dataset ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated" ) - elif p.detect: - # The user clicked the Auto-detect button on the 'Edit Attributes' form - for name, spec in dataset.datatype.metadata_spec.items(): - # We need to be careful about the attributes we are resetting - if name != 'name' and name != 'info' and name != 'dbkey': - if spec.get( 'default' ): - setattr( dataset.metadata,name,spec.unwrap( spec.get( 'default' ), spec )) - dataset.datatype.set_meta( dataset ) - dataset.datatype.after_edit( dataset ) - trans.app.model.flush() - return trans.show_ok_message( "Attributes updated" ) - - dataset.datatype.before_edit( dataset ) - - if "dbkey" in dataset.datatype.metadata_spec and not dataset.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 - dataset.metadata.dbkey = dataset.dbkey - metadata = list() - # a list of MetadataParemeters - for name, spec in dataset.datatype.metadata_spec.items(): - if spec.visible: - metadata.append( spec.wrap( dataset.metadata.get(name), dataset ) ) - # let's not overwrite the imported datatypes module with the variable datatypes? - ldatatypes = [x for x in trans.app.datatypes_registry.datatypes_by_extension.iterkeys()] - ldatatypes.sort() - return trans.fill_template( "/library/manage_dataset.mako", dataset=dataset, metadata=metadata, - datatypes=ldatatypes, err=None ) - else: - return trans.show_error_message( "Invalid dataset specified" ) + return trans.fill_template( '/library/library.mako', library=trans.app.model.Library.get( library_id ) ) + return trans.fill_template( '/library/libraries.mako', libraries=trans.app.model.Library.select() ) diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako new file mode 100644 index 00000000000..28596e03fb3 --- /dev/null +++ b/templates/admin/dataset_security/group_create.mako @@ -0,0 +1,96 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Create Group +
    +
    + Libraries  |   + Groups  |   + Users +
    +

    Create Group

    + + %if msg: + + %endif + + + +

    ${msg}

    + + + + + + %if len( users ) == 0: + + %else: + + + + + + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + + %endif + + +
    Name:   Priority:
    There are no Galaxy users
    Add Members to Group - Quick Find
    + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z +
    + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: +
    + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

    + <% anchored = True %> + %endif + ${email} + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: +

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    +
    diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako new file mode 100644 index 00000000000..58406d7edbf --- /dev/null +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -0,0 +1,71 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Permitted Actions on Datasets +
    +
    + Libraries  |   + Groups  |   + Users +
    +

    Manage Permitted Actions on Datasets for Group '${gn}'

    + + %if msg: + + %endif + + %if len( gdas ) == 0: + + %else: + + + + + + <% ctr = 0 %> + + %for gda in gdas: + %if ctr % 2 == 1: + + %else: + + %endif + + + + + <% ctr += 1 %> + %endfor + + + %endif +

    ${msg}

     
    There is no Galaxy group named '${gn}'
    GroupPriorityPermitted Actions on Datasets
    ${gn}${gda[0]} + %for da in dataset_actions: + <% check = False %> + %for action in gda[1]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
    + %endfor +
    +
    +
    diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako new file mode 100644 index 00000000000..36e407959f6 --- /dev/null +++ b/templates/admin/dataset_security/group_members.mako @@ -0,0 +1,47 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% gn = unescape( group_name, unentities ) %> + +<%def name="title()">Create Group +
    +
    + Libraries  |   + Groups  |   + Users +
    + +
    Members of Group '${gn}'
    + + %if msg: + + %endif + + %if len( members ) == 0: + + %else: + <% ctr = 0 %> + %for member in members: + <% email = unescape( member[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor + %endif +

    ${msg}

     
    Group '${gn}' contains no members
    ${email}
    +
    diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako new file mode 100644 index 00000000000..c7defaa7697 --- /dev/null +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -0,0 +1,114 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Manage Group Membership +
    +
    + Libraries  |   + Groups  |   + Users +
    + + %if msg: + + %endif + <% gn = unescape( group_name, unentities ) %> + + + + +

    ${msg}

     
    + + + %if len( users ) == 0: + + %else: + + + + + + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + + %endif + + +
    There are no Galaxy users
    Members of '${gn}' - Quick Find
    + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z +
    + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% + email = unescape( user[1], unentities ) + check = False + %> + %for member in members: + <% member_email = unescape( member[1], unentities ) %> + %if email == member_email: + <% + check = True + break + %> + %endif + %endfor + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: +
    + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

    + <% anchored = True %> + %endif + %if check: + ${email} + %else: + ${email} + %endif + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: +

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + %if check: + ${email} + %else: + ${email} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    +
    diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako new file mode 100644 index 00000000000..0687802932e --- /dev/null +++ b/templates/admin/dataset_security/groups.mako @@ -0,0 +1,67 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Groups +
    +
    + Libraries  |   + Users +
    + +

    Groups

    + + %if msg: + + %endif + %if len( groups ) == 0: + + %else: + + + + + + + + + <% ctr = 0 %> + %for group in groups: + <% group_name = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + + %if group[4] > 0: + + %else: + + %endif + + + + <% ctr += 1 %> + %endfor + %endif +

    ${msg}

    There are no Galaxy groups
    GroupPriorityMembersDatasetsGroup Permitted Actions on Datasets 
    ${group_name}${group[2]}${group[3]}${group[4]}${group[4]} + %if len( group[5] ) == 1: + ${group[5][0]} + %elif len( group[5] ) > 1: + %for da in group[5]: + ${da}
    + %endfor + %endif +
    Mark group deleted
    +
    diff --git a/templates/admin/dataset_security/index.mako b/templates/admin/dataset_security/index.mako new file mode 100644 index 00000000000..1a013ec1fc6 --- /dev/null +++ b/templates/admin/dataset_security/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Dataset Security +
    +
    Dataset Security
    + + %if msg: + + %endif + + +

    ${msg}

    Groups
    Users
    +
    diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako new file mode 100644 index 00000000000..8a392a67e31 --- /dev/null +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -0,0 +1,56 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<% email = unescape( user_email, unentities ) %> + +<%def name="title()">Create Group +
    +
    + Libraries  |   + Groups  |   + Users +
    +

    Groups of which '${email}' is a member

    + + %if msg: + + %endif + %if len( groups ) == 0: + + %else: + + + + + + + <% ctr = 0 %> + %for group in groups: + <% gn = unescape( group[1], unentities ) %> + %if ctr % 2 == 1: + + %else: + + %endif + + + %if group[3] > 0: + + %else: + + %endif + + + <% ctr += 1 %> + %endfor + %endif +

    ${msg}

    User '${email}' belongs to no groups
    GroupPriorityDatasetsPermitted Actions on Datasets
    ${gn}${group[2]}${group[3]}${group[3]} + %for da in group[4]: + ${da}
    + %endfor +
    +
    diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako new file mode 100644 index 00000000000..22abae62d54 --- /dev/null +++ b/templates/admin/dataset_security/users.mako @@ -0,0 +1,80 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Users +
    +
    + Groups  |   + Libraries +
    + + %if msg: + + %endif + + %if len( users ) == 0: + + %else: + + + + + <% + ctr = 0 + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + %for user in users: + <% email = unescape( user[1], unentities ) %> + %if not email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if ctr % 2 == 1: + + %else: + + %endif + + <% ctr += 1 %> + %endfor + + %endif +

    ${msg}

     
    There are no Galaxy users
    Galaxy Users - Quick Find
    + |A|B|C|D|E|F + |G|H|I|J|K|L + |M|N|O|P|Q|R + |S|T|U|V|W|X + |Y|Z +
    + %if email.upper().startswith( curr_anchor ): + %if not anchored: +

    + <% anchored = True %> + %endif + ${email} + %else: + %for anchor in anchors[ anchor_loc: ]: + %if email.upper().startswith( anchor ): + %if not anchored: +

    + <% + curr_anchor = anchor + anchored = True + %> + %endif + ${email} + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %endif +
    +
    diff --git a/templates/admin/index.mako b/templates/admin/index.mako new file mode 100644 index 00000000000..84e90c84b55 --- /dev/null +++ b/templates/admin/index.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +
    +

    Galaxy Administration

    + + %if msg: + + %endif + + + +

    ${msg}

    Dataset Security
    Libraries
    Reload a tool while the Galaxy server is running
    +
    diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako new file mode 100644 index 00000000000..52a35099045 --- /dev/null +++ b/templates/admin/library/dataset.mako @@ -0,0 +1,85 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Edit Dataset Attributes +<%def name="datatype( dataset, datatypes )"> + + +
    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + %for element in metadata: +
    + +
    + ${element.get_html()} +
    +
    +
    + %endfor +
    + +
    +
    +
    + +
    + +
    +
    + This will inspect the dataset and attempt to correct the above column values + if they are not accurate. +
    +
    +
    +
    +

    +

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

    diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako new file mode 100644 index 00000000000..b4b11c74b36 --- /dev/null +++ b/templates/admin/library/folder.mako @@ -0,0 +1,158 @@ +<%inherit file="/base.mako"/> + +<%def name="render_component( component )"> + <% + if isinstance( component, trans.app.model.LibraryFolder ): + return render_folder( component ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + return render_dataset( component ) + %> + +## Render the dataset `data` as history item, using `hid` as the displayed id +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> +

    +
    ${data.display_name()}
    +
    +
    + ## Header row for history items (name, state, action buttons) +
    + %if data_state != 'ok': +
    + %endif +
    +
    + edit attributes +
    + ##${data.display_name()} +
    + ## Body for history items, extra info and actions, data "peek" +
    + %if data_state == "queued": +
    Job is waiting to run
    + %elif data_state == "running": +
    Job is currently running
    + %elif data_state == "error": +
    + An error occurred running this job: ${data.display_info().strip()}, + report this error +
    + %elif data_state == "empty": +
    No data: ${data.display_info()}
    + %elif data_state == "ok": +
    + ${data.blurb}, + format: ${data.ext}, + database: + %if data.dbkey == '?': + ${data.dbkey} + %else: + ${data.dbkey} + %endif +
    +
    Info: ${data.display_info()}
    + %if data.peek != "no peek": +
    ${data.display_peek()}
    + %endif + %else: +
    Error: unknown dataset state "${data_state}".
    + %endif + ## Recurse for child datasets +
    +
    +
    + +## Render a folder +<%def name="render_folder( this_folder )"> +
    +
    Contents of Folder: ${this_folder.name}
    +
    +
    + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> + %for component in components: + ${render_component( component )} + %endfor +
    +
    + +
    +
    +
    + +<%def name="title()">Manage Folder: ${folder.name} +
    +
    + Libraries  |   + Groups  |   + Users +
    +
    Change Folder Attributes
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    Manage Folder Contents: ${folder.name}
    +
    +
    + %if folder.parent: + Up a Level + %elif folder.library_root: + Manage Library + %endif +
    +
    +
    + ${render_folder( folder )} +
    +
    +
    +
    diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako new file mode 100644 index 00000000000..7e44442e1aa --- /dev/null +++ b/templates/admin/library/libraries.mako @@ -0,0 +1,19 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Libraries +
    +
    + Groups  |   + Users +
    + +
    +
    Galaxy Libraries
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako new file mode 100644 index 00000000000..5d979af8abb --- /dev/null +++ b/templates/admin/library/library.mako @@ -0,0 +1,35 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Library +
    +
    + Libraries  |   + Groups  |   + Users +
    +
    Manage Library '${library.name}'
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + + +
     
    +
    +
    + diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako new file mode 100644 index 00000000000..241e121ebeb --- /dev/null +++ b/templates/admin/library/new_dataset.mako @@ -0,0 +1,953 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">Create New Library Dataset +
    +
    + Libraries  |   + Groups  |   + Users +
    +
    Create a new Library Dataset
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + Here you may specify a list of URLs (one per line) or paste the contents of a file. +
    +
    +
    +
    + +
    Yes
    +
    + Use this option if you are entering intervals by hand. +
    +
    +
    +
    + +
    + +
    +
    + Which format? See help below +
    +
    +
    +
    + + ##this should be generated dynamically +
    +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/templates/admin/reload_tool.mako b/templates/admin/reload_tool.mako new file mode 100644 index 00000000000..1d050ea4764 --- /dev/null +++ b/templates/admin/reload_tool.mako @@ -0,0 +1,28 @@ +<%inherit file="/base.mako"/> + +
    +

    Reload a Tool

    + + %if msg: + + %endif + + + +

    ${msg}

    +
    +

    + Reload tool: + + +

    +
    +
    +
    diff --git a/templates/admin_main.mako b/templates/admin_main.mako deleted file mode 100644 index 02e39576685..00000000000 --- a/templates/admin_main.mako +++ /dev/null @@ -1,39 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Galaxy Administration - - - - - - - - - - - - -
    -

    Galaxy Administration

    - %if msg: -

    ${msg}

    - %endif -
    -
    -

    Admin password:

    -

    - Reload tool: - - -

    -
    -
    - Manage Libraries -
    - diff --git a/templates/library/admin_list_libraries.mako b/templates/library/admin_list_libraries.mako deleted file mode 100644 index ffe82787da2..00000000000 --- a/templates/library/admin_list_libraries.mako +++ /dev/null @@ -1,16 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">View Libraries - -
    -
    Manage Libraries
    -
    - %for library in libraries: - - %endfor - -
    -
    diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako new file mode 100644 index 00000000000..247333f709c --- /dev/null +++ b/templates/library/libraries.mako @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="title()">View Libraries +
    +
    View Library
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/library/user_view_library.mako b/templates/library/library.mako similarity index 58% rename from templates/library/user_view_library.mako rename to templates/library/library.mako index 3b948558f28..2706efcde25 100644 --- a/templates/library/user_view_library.mako +++ b/templates/library/library.mako @@ -1,58 +1,44 @@ <%inherit file="/base.mako"/> <%def name="render_component( component )"> - <% + <% if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) + return render_folder( component ) elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - return render_dataset( component ) - %> + return render_dataset( component ) + %> - - ## Render the dataset `data` as history item, using `hid` as the displayed id <%def name="render_dataset( data )"> -
    +
    ${data.name} -
    +
    - ## Render a folder <%def name="render_folder( this_folder )"> -
    - Folder: ${this_folder.name} - <% - components = list( this_folder.folders ) + list( this_folder.datasets ) - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> -
    - %for component in components: - ${render_component( component )} - %endfor -
    + Folder: ${this_folder.name} + <% + components = list( this_folder.folders ) + list( this_folder.datasets ) + components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] + components.sort() + components = [ tup[-1] for tup in components ] + %> +
    + %for component in components: + ${render_component( component )} + %endfor +
    - - - - - - <%def name="title()">View Library: ${library.name} - -
    +
    Import from Library: ${library.name}
    ${render_folder( library.root_folder )} -
    -
    - diff --git a/templates/library/manage_dataset.mako b/templates/library/manage_dataset.mako deleted file mode 100644 index f04615def51..00000000000 --- a/templates/library/manage_dataset.mako +++ /dev/null @@ -1,100 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Edit Dataset Attributes - - -<%def name="datatype( dataset, datatypes )"> - - - -
    -
    Edit Attributes
    -
    -
    - -
    - -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - %for element in metadata: -
    - -
    - ${element.get_html()} -
    -
    -
    - %endfor -
    - -
    -
    -
    - -
    - -
    -
    - This will inspect the dataset and attempt to correct the above column values - if they are not accurate. -
    -
    -
    -
    - -

    - - -

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

    diff --git a/templates/library/manage_folder.mako b/templates/library/manage_folder.mako deleted file mode 100644 index 19e12555625..00000000000 --- a/templates/library/manage_folder.mako +++ /dev/null @@ -1,191 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="render_component( component )"> - <% - if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - return render_dataset( component ) - %> - - - -## Render the dataset `data` as history item, using `hid` as the displayed id -<%def name="render_dataset( data )"> - <% - if data.state in ['no state','',None]: - data_state = "queued" - else: - data_state = data.state - %> - ##

    -
    -
    ${data.display_name()}
    -
    -
    - - ## Header row for history items (name, state, action buttons) - -
    - %if data_state != 'ok': -
    - %endif -
    -
    - ##display data - edit attributes - ##delete -
    - ##${data.display_name()} -
    - - ## Body for history items, extra info and actions, data "peek" - -
    - %if data_state == "queued": -
    Job is waiting to run
    - %elif data_state == "running": -
    Job is currently running
    - %elif data_state == "error": -
    - An error occurred running this job: ${data.display_info().strip()}, - report this error -
    - %elif data_state == "empty": -
    No data: ${data.display_info()}
    - %elif data_state == "ok": -
    - ${data.blurb}, - format: ${data.ext}, - database: - %if data.dbkey == '?': - ${data.dbkey} - %else: - ${data.dbkey} - %endif -
    -
    Info: ${data.display_info()}
    - %if data.peek != "no peek": -
    ${data.display_peek()}
    - %endif - %else: -
    Error: unknown dataset state "${data_state}".
    - %endif - - ## Recurse for child datasets - - -
    -
    -
    - - -## Render a folder -<%def name="render_folder( this_folder )"> - -
    -
    Contents of Folder: ${this_folder.name}
    -
    -
    -
    - <% - components = list( this_folder.folders ) + list( this_folder.datasets ) - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> - %for component in components: - ${render_component( component )} - %endfor - -
    -
    -
    - -
    - -
    -
    - - - - - - -<%def name="title()">Manage Folder: ${folder.name} - -
    -
    Change Folder Attributes
    -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - - - -
    - -
    -
    Manage Folder Contents: ${folder.name}
    -
    -
    - %if folder.parent: - up a level - %elif folder.library_root: - manage library - %endif -
    -
    -
    - ${render_folder( folder )} -
    - -
    - -
    - -
    -
    -
    - diff --git a/templates/library/manage_library.mako b/templates/library/manage_library.mako deleted file mode 100644 index 5d94dfb6b78..00000000000 --- a/templates/library/manage_library.mako +++ /dev/null @@ -1,46 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Manage Library - - -
    -
    Edit a Library: ${library.name}
    -
    -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - - -
    - -
    - - -
    - -
    - -
    - diff --git a/templates/library/new_dataset.mako b/templates/library/new_dataset.mako deleted file mode 100644 index 1c517cdfede..00000000000 --- a/templates/library/new_dataset.mako +++ /dev/null @@ -1,997 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Create New Library Dataset - -
    -
    Create a new Library Dataset
    -
    -
    - - - -
    - - -
    - - -
    - - -
    - -
    - - -
    - -
    - Here you may specify a list of URLs (one per line) or paste the contents of a file. -
    - - - -
    - -
    - -
    - - -
    Yes
    - -
    - - Use this option if you are entering intervals by hand. -
    - - -
    - -
    - -
    - - -
    - -
    - Which format? See help below -
    - - -
    - -
    - -
    - - -##this should be generated dynamically -
    - - - -
    - -
    - - -
    - -
    - - -
    -
    -
    - diff --git a/templates/library/user_list_libraries.mako b/templates/library/user_list_libraries.mako deleted file mode 100644 index 23a2b3b9ad1..00000000000 --- a/templates/library/user_list_libraries.mako +++ /dev/null @@ -1,14 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">View Libraries - -
    -
    View Library
    -
    - %for library in libraries: - - %endfor - -
    -
    diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 58d8f1776db..097d7707acc 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -20,6 +20,9 @@ | wiki | screencasts | blog + %if admin_user == "true": + | admin + %endif     diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 6dcdf061f94..58ca578eb15 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -77,8 +77,8 @@ use_lint = false # NEVER enable this on a public site (even test or QA) use_interactive = true -# Admin Password -admin_pass = galaxy +# Admin Users - this should be a comma-separated list of valid Galaxy users +#admin_users = user1@bx.psu.edu,user2@bx.psu.edu # path to sendmail sendmail_path = /usr/sbin/sendmail From d392b887ec32dd4ba4e76fc2d8b57e780e8b5e43 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 14 Aug 2008 17:44:06 -0400 Subject: [PATCH 015/267] Fix a permitted_actions bug, and re-enable the ability for a user to make a single dataset private or public. And fix a few typos. --- lib/galaxy/security/__init__.py | 20 ++++++++++++++++---- lib/galaxy/web/controllers/async.py | 2 +- lib/galaxy/web/controllers/root.py | 17 +++++++---------- templates/dataset/edit_attributes.mako | 10 ++++------ templates/root/history_common.mako | 2 +- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index ed73ec2364e..7709c5a1916 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -81,9 +81,8 @@ class GalaxyRBACAgent( RBACAgent ): # Check permitted_actions associated with dataset through groups for group_dataset_assoc in dataset.groups: if self.components_are_associated( user = user, group = group_dataset_assoc.group ): - for pa in group_dataset_assoc.permitted_actions: - if action in pa.permitted_actions.actions: - return True + if action in group_dataset_assoc.permitted_actions: + return True return False # No user and dataset not in public group, or user lacks permission def guess_derived_groups_for_datasets( self, datasets=[] ): # TODO, Nate: Make sure this method is functionally correct. @@ -132,6 +131,12 @@ class GalaxyRBACAgent( RBACAgent ): if 'group' in kwd: return self.associate_user_group( kwd['user'], kwd['group'] ) raise 'No valid method of associating provided components: %s' % kwd + def disassociate_components( self, **kwd ): + assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to disassociate.' + if 'dataset' in kwd: + if 'group' in kwd: + return self.disassociate_group_dataset( kwd['group'], kwd['dataset'] ) + raise 'No valid method of associating provided components: %s' % kwd def associate_group_dataset( self, group, dataset, permitted_actions=[] ): # TODO, Nate: Make sure this method is functionally correct. # TODO: For now, just take the dataset's permitted_actions, but we need to make sure @@ -145,6 +150,11 @@ class GalaxyRBACAgent( RBACAgent ): assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) assoc.flush() return assoc + def disassociate_group_dataset( self, group, dataset ): + log.debug("In disassociate_group_dataset, removing %s -> %s" % (group.id, dataset.id)) + assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) + assoc.delete() + assoc.flush() def associate_user_group( self, user, group ): assoc = self.model.UserGroupAssociation( user, group ) assoc.flush() @@ -182,7 +192,7 @@ class GalaxyRBACAgent( RBACAgent ): # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. if groups is None: if history.user: - groups = history.user.default_groups + groups = [ assoc.group for assoc in history.user.default_groups ] else: groups = [ self.get_public_group() ] if groups is not None: @@ -234,6 +244,8 @@ class GalaxyRBACAgent( RBACAgent ): if 'group' in kwd: return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) raise 'No valid method of associating provided components: %s' % kwd + def dataset_has_group( self, dataset_id, group_id ): + return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index 004868434d8..dcb6a0c1be5 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) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 8841c46eedd..dc58f02f2ee 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -262,30 +262,27 @@ class RootController( BaseController ): if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) return trans.show_ok_message( msg, refresh_frames=['history'] ) - ''' - # Users can't currently change permissions or groups - elif p.change_permision: - """The user clicked the change_permision button on the 'Change permissions' form""" + elif p.change_permission: + """The user clicked the change_permission button on the 'Change permissions' form""" if not trans.user: return trans.show_error_message( "You must be logged in if you want to change dataset permitted actions." ) private_dataset = 'private_dataset' public_group = trans.app.security_agent.get_public_group() - if private_dataset in kwd and data.dataset.has_group( public_group ): + if private_dataset in kwd and trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): #check user has permission and then remove public group - if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.REMOVE_GROUP, dataset = data.dataset ): - trans.app.security_agent.remove_component_association( dataset = data, group = public_group ) + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + trans.app.security_agent.disassociate_components( dataset = data, group = public_group ) else: return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) - elif private_dataset not in kwd and not data.dataset.has_group( public_group ): + elif private_dataset not in kwd and not trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): #check user has permission and then add public group - if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.ADD_GROUP, dataset = data.dataset ): + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): trans.app.security_agent.associate_components( dataset = data, group = public_group) else: return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) else: return trans.show_error_message( "You have not specified a valid change of permitted actions." ) return trans.show_ok_message( 'Permitted actions have been changed.', refresh_frames=['history'] ) - ''' data.datatype.before_edit( data ) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 1479b15c20a..c54f6eb0d1a 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,19 +133,18 @@

    -<%doc> -%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.REMOVE_GROUP, dataset = data ) or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.ADD_GROUP, dataset = data ) ): +%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ):

    Change Permitted Actions
    -
    +
    <% checked = "" %> - %if not data.dataset.has_group( trans.app.model.Group.get_public_group() ): + %if not trans.app.security_agent.dataset_has_group( data.id, trans.app.model.Group.get_public_group().id ): <% checked = " checked" %> %endif
    @@ -158,10 +157,9 @@
    - +
    %endif - diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index aa9a5c238a5..cd3225e7994 100644 --- a/templates/root/history_common.mako +++ b/templates/root/history_common.mako @@ -33,7 +33,7 @@
    %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): -
    You do not have permision to view this dataset.
    +
    You do not have permission to view this dataset.
    %elif data_state == "queued":
    Job is waiting to run
    %elif data_state == "running": From a22b2ba86d82f1c7f16f57506d9e8bbe6c2f7e34 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 14 Aug 2008 18:19:38 -0400 Subject: [PATCH 016/267] Default to only adding datasets to a user's private group, and ensure externally authenticated users get their defaults set up. --- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/framework/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 7709c5a1916..81db90994b6 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -171,7 +171,7 @@ class GalaxyRBACAgent( RBACAgent ): def user_set_default_access( self, user, groups = None, history = False, dataset = False ): # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. if groups is None: - groups = [ self.get_public_group(), self.create_private_user_group( user ) ] + groups = [ self.create_private_user_group( user ) ] if groups is not None: for assoc in user.default_groups: #this is the association not the actual group assoc.delete() diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 124d1549453..87f099a681d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -272,6 +272,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): user.set_password_cleartext( 'external' ) user.external = True user.flush() + self.app.security_agent.setup_new_user( user ) self.log_event( "Automatically created account '%s'" % user.email ) return user def get_cookie_user( self ): From 849a462b5c6c3631e3e3c12c09521a4cf50dd722 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 15 Aug 2008 09:07:02 -0400 Subject: [PATCH 017/267] Fix for sharing a history with another user, still need to apply security checks. --- lib/galaxy/model/__init__.py | 71 ++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 460b6433b1d..599ab72a78b 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -417,7 +417,12 @@ class DatasetInstance( object ): child.mark_deleted() class HistoryDatasetAssociation( DatasetInstance ): - def __init__( self, hid = None, history = None, copied_from_history_dataset_association = None, copied_from_library_folder_dataset_association = None, **kwd ): + 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 @@ -426,10 +431,19 @@ class HistoryDatasetAssociation( DatasetInstance ): self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association def copy( self, copy_children = False, parent_id = None ): - print "self.dataset", self.dataset - - des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self ) - print "des data", des.dataset + des = HistoryDatasetAssociation( hid=self.hid, + name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_history_dataset_association=self ) des.flush() if copy_children: for child in self.children: @@ -501,14 +515,13 @@ class History( object ): des.flush() des.name = self.name for data in self.datasets: - new_data = data.copy( copy_children = True, target_user = target_user ) + new_data = data.copy( copy_children = True ) des.add_dataset( new_data ) 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" @@ -531,7 +544,12 @@ class LibraryFolder( object ): self.item_count += 1 class LibraryFolderDatasetAssociation( DatasetInstance ): - def __init__( self, folder = None, order_id = None, copied_from_history_dataset_association = None, copied_from_library_folder_dataset_association = None, **kwd ): + def __init__( self, + folder = None, + order_id = None, + copied_from_history_dataset_association = None, + copied_from_library_folder_dataset_association = None, + **kwd ): DatasetInstance.__init__( self, **kwd ) self.folder = folder self.order_id = order_id @@ -539,7 +557,18 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): self.copied_from_library_folder_dataset_association = copied_from_library_folder_dataset_association def to_history_dataset_association( self, parent_id = None ): - des = HistoryDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_library_folder_dataset_association = self ) + des = HistoryDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self ) des.flush() for child in self.children: child_copy = child.to_history_dataset_association( parent_id = des.id ) @@ -549,7 +578,18 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): def copy( self, copy_children = False, parent_id = None ): - des = LibraryFolderDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, peek=self.peek, extension=self.extension, dbkey=self.dbkey, metadata=self._metadata, dataset = self.dataset, visible=self.visible, deleted=self.deleted, parent_id=parent_id, copied_from_library_folder_dataset_association = self ) + des = LibraryFolderDatasetAssociation( name=self.name, + info=self.info, + blurb=self.blurb, + peek=self.peek, + extension=self.extension, + dbkey=self.dbkey, + metadata=self._metadata, + dataset = self.dataset, + visible=self.visible, + deleted=self.deleted, + parent_id=parent_id, + copied_from_library_folder_dataset_association = self ) des.flush() if copy_children: for child in self.children: @@ -626,7 +666,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 From 199e3327df08b8ebf497936fdcd29c4fc07d01ce Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 15 Aug 2008 16:40:13 -0400 Subject: [PATCH 018/267] Feature enhancements and cleanup for dataset security and libraries. 1) DefaultHistoryGroupAssociations are now deleted when a history is deleted. 2) New user's private group name now includes 'private group' 3) When an admin adds a new dataset to a library folder, they can associate it with 1 or more groups and then edit the permitted_actions on the edit page for the dataset. 4) Other miscellaneous cleanup --- lib/galaxy/model/mapping.py | 2 - lib/galaxy/security/__init__.py | 23 +- lib/galaxy/web/controllers/admin.py | 128 +++- lib/galaxy/web/controllers/dataset.py | 5 +- lib/galaxy/web/controllers/root.py | 7 +- templates/admin/library/dataset.mako | 35 + templates/admin/library/folder.mako | 4 +- templates/admin/library/new_dataset.mako | 919 +---------------------- 8 files changed, 184 insertions(+), 939 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index e6f132693da..2303c9eb338 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -186,7 +186,6 @@ Library.table = Table( "library", metadata, Column( "name", TEXT ), 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 ), @@ -217,7 +216,6 @@ LibraryTagDatasetAssociation.table = Table( "library_tag_dataset_association", m 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 ), diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 81db90994b6..c6790b2d7e3 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -21,7 +21,7 @@ class RBACAgent: DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', # The ability to perform any read only operation on the dataset (view, display at external site, # use in a job, etc). - DATASET_ACCESS = 'dataset_access' + DATASET_ACCESSS = 'dataset_access' ) def allow_action( self, user, action, **kwd ): raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) @@ -67,9 +67,8 @@ class GalaxyRBACAgent( RBACAgent ): 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 ): - # TODO, Nate: Make sure this method is functionally correct. """Returns true when user has permission to perform an action""" - while not isinstance( dataset, self.model.Dataset ): + if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using # This may need to change when the ability to alter groups and permitted_actions is allowed @@ -122,7 +121,6 @@ class GalaxyRBACAgent( RBACAgent ): return rval raise 'No valid method of creating group with %s' % ( kwd ) def associate_components( self, **kwd ): - # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to associate.' if 'dataset' in kwd: if 'group' in kwd: @@ -138,15 +136,11 @@ class GalaxyRBACAgent( RBACAgent ): return self.disassociate_group_dataset( kwd['group'], kwd['dataset'] ) raise 'No valid method of associating provided components: %s' % kwd def associate_group_dataset( self, group, dataset, permitted_actions=[] ): - # TODO, Nate: Make sure this method is functionally correct. - # TODO: For now, just take the dataset's permitted_actions, but we need to make sure - # we can associate group permitted_actions if necessary - need to look into this... if not permitted_actions: if isinstance( dataset.permitted_actions, Bunch ): permitted_actions = dataset.permitted_actions.__dict__.values() else: permitted_actions = dataset.permitted_actions - log.debug("In associate_group_dataset, permitted_actions: %s" %str(permitted_actions) ) assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) assoc.flush() return assoc @@ -160,9 +154,9 @@ class GalaxyRBACAgent( RBACAgent ): assoc.flush() return assoc def create_private_user_group( self, user ): - # TODO, Nate: Make sure this method is functionally correct. # Create private group - group = self.model.Group( user.email, priority = 10 ) + group_name = "%s private group" % user.email + group = self.model.Group( name=group_name, priority=10 ) group.flush() # Add user to group self.associate_components( group=group, user=user ) @@ -177,12 +171,10 @@ class GalaxyRBACAgent( RBACAgent ): assoc.delete() assoc.flush() for group in groups: - log.debug("In user_set_default_access, group: %s" %str(group)) if isinstance( group, self.model.Group ): permitted_actions = group.permitted_actions.__dict__.values() else: permitted_actions = group.permitted_actions - log.debug("In user_set_default_access, permitted_actions: %s" % str( permitted_actions)) assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) assoc.flush() if history: @@ -200,12 +192,10 @@ class GalaxyRBACAgent( RBACAgent ): assoc.delete() assoc.flush() for group in groups: - log.debug("In history_set_default_access, group: %s" %str(group)) if isinstance( group, self.model.Group ): permitted_actions = group.permitted_actions.__dict__.values() else: permitted_actions = group.permitted_actions - log.debug("In history_set_default_access, permitted_actions: %s" % str( permitted_actions)) assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) assoc.flush() if dataset: @@ -223,7 +213,6 @@ class GalaxyRBACAgent( RBACAgent ): def guess_public_group( self ): return self.model.Group.guess_public_group() def set_dataset_groups( self, dataset, groups ): - # TODO, Nate: Make sure this method is functionally correct. if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset for group_dataset_assoc in dataset.groups: @@ -232,7 +221,6 @@ class GalaxyRBACAgent( RBACAgent ): for group in groups: if not isinstance( group, self.model.Group ): group = group.group - log.debug("In set_dataset_groups, before elf.associate_components, dataset: %s, group: %s" % ( str(dataset), str(group))) self.associate_components( dataset=dataset, group=group ) def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. @@ -254,6 +242,5 @@ def get_permitted_actions( self, filter=None ): 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)] + [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/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 3367ce6bc36..55f4f850646 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,5 +1,6 @@ -import shutil, StringIO +import shutil, StringIO, operator +from galaxy import util from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent @@ -540,7 +541,7 @@ class Admin( BaseController ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) data_files = [] - def add_file( file_obj, name, extension, dbkey, info = 'no info', space_to_tab = False ): + def add_file( file_obj, name, extension, dbkey, groups, info='no info', space_to_tab=False ): data_type = None temp_name = sniff.stream_to_file( file_obj ) if space_to_tab: @@ -555,8 +556,14 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( folder_id ) folder.add_dataset( dataset ) dataset.flush() - # TODO, SET SECURTY INTERACTIVELY ON DATASET, right now everything is public - trans.app.security_agent.set_dataset_groups( dataset.dataset, [trans.app.security_agent.get_public_group()] ) + # GroupDatasetAssociations will enable security on the dataset based on the permitted_actions + # associated with the GroupDatasetAssociation. The default permitted_actions at this point + # will be DATASET_ACCESS, but the user can change this after the file is uploaded. + permitted_actions = [ RBACAgent.permitted_actions.DATASET_ACCESS ] + for group_id in groups: + group = galaxy.model.Group.get( group_id ) + group_dataset_assoc = galaxy.model.GroupDatasetAssociation( group, dataset.dataset, permitted_actions ) + group_dataset_assoc.flush() shutil.move( temp_name, dataset.dataset.file_name ) dataset.dataset.state = dataset.dataset.states.OK dataset.init_meta() @@ -575,7 +582,7 @@ class Admin( BaseController ): return dataset if 'create_dataset' in kwd: - #copied from upload tool action + # Copied from upload tool action last_dataset_created = None data_file = kwd['file_data'] url_paste = kwd['url_paste'] @@ -583,6 +590,12 @@ class Admin( BaseController ): if 'space_to_tab' in kwd: if kwd['space_to_tab'] not in ["None", None]: space_to_tab = True + groups = kwd['groups'] + if groups and not isinstance( groups, list ): + # mako sends singleton lists as a string + groups = [ groups ] + if groups is None: + groups = [] temp_name = "" data_list = [] @@ -590,14 +603,26 @@ class Admin( BaseController ): file_name = data_file.filename file_name = file_name.split( '\\' )[-1] file_name = file_name.split( '/' )[-1] - last_dataset_created = add_file( data_file.file, file_name, extension, dbkey, info="uploaded file", space_to_tab = space_to_tab ) + last_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + groups, + info="uploaded file", + space_to_tab=space_to_tab ) elif url_paste not in [ None, "" ]: if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: url_paste = url_paste.replace( '\r', '' ).split( '\n' ) for line in url_paste: line = line.rstrip( '\r\n' ) if line: - last_dataset_created = add_file( urllib.urlopen( line ), line, extension, dbkey, info="uploaded url", space_to_tab=space_to_tab ) + last_dataset_created = add_file( urllib.urlopen( line ), + line, + extension, + dbkey, + groups, + info="uploaded url", + space_to_tab=space_to_tab ) else: is_valid = False for line in url_paste: @@ -606,16 +631,73 @@ class Admin( BaseController ): is_valid = True break if is_valid: - last_dataset_created = add_file( StringIO.StringIO( url_paste ), 'Pasted Entry', extension, dbkey, info="pasted entry", space_to_tab=space_to_tab ) - trans.response.send_redirect( web.url_for( action='dataset', id = last_dataset_created.id ) ) - #return self.dataset( trans, id = last_dataset_created.id ) + last_dataset_created = add_file( StringIO.StringIO( url_paste ), + 'Pasted Entry', + extension, + dbkey, + groups, + info="pasted entry", + space_to_tab=space_to_tab ) + trans.response.send_redirect( web.url_for( action='dataset', id=last_dataset_created.id ) ) elif id is None: - return trans.fill_template( '/admin/library/new_dataset.mako', folder_id = folder_id ) + # Send list of data formats to the form so the "extension" select list can be populated dynamically + file_formats = trans.app.datatypes_registry.upload_file_formats + # Send list of genome builds to the form so the "dbkey" select list can be populated dynamically + def get_dbkey_options(): + last_used_build = trans.history.genome_build + for dbkey, build_name in util.dbnames: + yield build_name, dbkey, ( dbkey==last_used_build ) + dbkeys = get_dbkey_options() + # Send list of groups to the form so the dataset can be associated with 1 or more of them. + groups = [] + q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), + ( galaxy.model.Group.table.c.name ).label( 'group_name' ) ), + order_by = [ galaxy.model.Group.table.c.name ] ) + for row in q.execute(): + groups.append( ( row.group_id, row.group_name ) ) + groups = sorted( groups, key=operator.itemgetter(1) ) + return trans.fill_template( '/admin/library/new_dataset.mako', + folder_id=folder_id, + file_formats=file_formats, + dbkeys=dbkeys, + groups=groups ) dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) if dataset: - #copied from edit attributes for 'regular' datasets + # Copied from edit attributes for 'regular' datasets with some additions p = util.Params(kwd, safe=False) - if p.change: + if p.change_permitted_actions: + # The user clicked the Save button on the 'Group Associations' form + actions = p.actions + if actions and not isinstance( actions, list ): + actions = [ actions ] + if actions is None: + actions = [] + # actions is a list of comma-separated strings consisting of group_id and permitted_action, + # something like: ['6,dataset_access', '6,dataset_edit_metadata']. We'll parse them and + # create a dict whose keys are groups_id and values are permitted_actions + gdpa_dict = {} + for action in actions: + group_id, dpa = action.split( ',' ) + group_id = int( group_id ) + if group_id in gdpa_dict.keys(): + gdpa_dict[ group_id ].append( dpa ) + else: + gdpa_dict[ group_id ] = [ dpa ] + # Check to see if we need to delete any GroupDatasetAssociations. This occurs if + # the user unchecked all boxes for a group + for group_dataset_assoc in dataset.dataset.groups: + if group_dataset_assoc.group_id not in gdpa_dict.keys(): + group_dataset_assoc.delete() + group_dataset_assoc.flush() + # Use the dict to update the permitted actions for each GroupDatasetAssociaton + for group_id in gdpa_dict: + actions = gdpa_dict[ group_id ] + # Update the permitted_actions for every GroupDatasetAssociation of the Group + q = sa.update( galaxy.model.GroupDatasetAssociation.table, + whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, + values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) + result = q.execute() + elif p.change: # The user clicked the Save button on the 'Change data type' form trans.app.datatypes_registry.change_datatype( dataset, p.datatype ) trans.app.model.flush() @@ -623,7 +705,6 @@ class Admin( BaseController ): # The user clicked the Save button on the 'Edit Attributes' form dataset.name = name dataset.info = info - # The following for loop will save all metadata_spec items for name, spec in dataset.datatype.metadata_spec.items(): if spec.get("readonly"): @@ -651,7 +732,20 @@ class Admin( BaseController ): return trans.show_ok_message( "Attributes updated" ) dataset.datatype.before_edit( dataset ) - + # Get all actions to send to the form + dataset_actions = [] + dpas = RBACAgent.permitted_actions + for dpa in dpas.items(): + if dpa[0].startswith( 'DATASET' ): + dataset_actions.append( dpa[1] ) + dataset_actions.sort() + # Get the permitted_actions of each GroupDatasetAssociation to send to the form + gdas = [] + # Refresh the dataset to ensure we have a valid set of DatasetGroupAssociations + dataset.dataset.refresh() + for group_dataset_assoc in dataset.dataset.groups: + group = galaxy.model.Group.get( group_dataset_assoc.group_id ) + gdas.append( ( group.id, group.name, group_dataset_assoc.permitted_actions ) ) if "dbkey" in dataset.datatype.metadata_spec and not dataset.metadata.dbkey: # Copy dbkey into metadata, for backwards compatability # This looks like it does nothing, but getting the dbkey @@ -670,7 +764,9 @@ class Admin( BaseController ): return trans.fill_template( "/admin/library/dataset.mako", dataset=dataset, metadata=metadata, - datatypes=ldatatypes, + datatypes=ldatatypes, + dataset_actions=dataset_actions, + gdas=gdas, err=None ) else: return trans.show_error_message( "Invalid dataset specified" ) diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index d38537509cd..3d43b5bef5e 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -105,8 +105,7 @@ class DatasetInterface( BaseController ): """Catches the dataset id and displays file contents as directed""" data = trans.app.model.HistoryDatasetAssociation.get( dataset_id ) if not data: - raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) - # TODO, Nate: Make sure the following is functionally correct. + raise paste.httpexceptions.HTTPRequestRangeNotSatisfiable( "Invalid reference dataset." ) if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): if filename is None or filename.lower() == "index": mime = trans.app.datatypes_registry.get_mimetype_by_extension( data.extension.lower() ) @@ -127,4 +126,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not privileged to access this dataset." ) + raise paste.httpexceptions.HTTPForbidden( "You are not permitted to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index dc58f02f2ee..6850b069cb7 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -395,8 +395,13 @@ class RootController( BaseController ): if history: if history.user_id != None and user: assert user.id == history.user_id, "History does not belong to current user" - history_names.append(history.name) + # Delete DefaultHistoryGroupAssociations + for default_history_group_association in history.default_groups: + default_history_group_association.delete() + default_history_group_association.flush() + # Mark history as deleted in db history.deleted = True + history_names.append(history.name) # If deleting the current history, make a new current. if history == trans.get_history(): trans.new_history() diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index 52a35099045..b522a05256d 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -13,6 +13,41 @@ %endfor +
    +
    Group Associations
    +
    +
    + + %for gda in gdas: +
    + ${gda[1]} +
    +
    +
    + %for da in dataset_actions: + <% check = False %> + %for action in gda[2]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
    + %endfor +
    +
    + %endfor +
    +
    +
    +
    Edit Attributes
    diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako index b4b11c74b36..e93b64e7989 100644 --- a/templates/admin/library/folder.mako +++ b/templates/admin/library/folder.mako @@ -94,9 +94,9 @@
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 241e121ebeb..35a8106fd81 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -37,24 +37,9 @@
    @@ -64,889 +49,29 @@
    - ##this should be generated dynamically
    -
    + +
    - + + Multi-select list - hold the appropriate key while clicking to select multiple columns +
    + +
    +
    +
    +
    +
    From 140d8b5f7d5aaadf3f0ffbceced65041b0652456 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 18 Aug 2008 09:00:53 -0400 Subject: [PATCH 019/267] Fix a typo in the security agent ( my last commit ) that broke a bunch of stuff. --- lib/galaxy/security/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index c6790b2d7e3..b36881505e0 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -21,7 +21,7 @@ class RBACAgent: DATASET_MANAGE_PERMISSIONS = 'dataset_manage_permissions', # The ability to perform any read only operation on the dataset (view, display at external site, # use in a job, etc). - DATASET_ACCESSS = 'dataset_access' + DATASET_ACCESS = 'dataset_access' ) def allow_action( self, user, action, **kwd ): raise 'No valid method of checking action (%s) on %s for user %s.' % ( action, kwd, user ) @@ -68,6 +68,7 @@ class GalaxyRBACAgent( RBACAgent ): 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""" + log.debug("In allow_dataset_action, user: %s, action: %s, dataset: %s" % ( str(user), str(action), str(dataset))) if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using @@ -238,9 +239,11 @@ class GalaxyRBACAgent( RBACAgent ): def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' if filter is None: + log.debug("In get_permitted_actions, returning RBACAgent.permitted_actions: %s" % str( RBACAgent.permitted_actions)) 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)] + log.debug("In get_permitted_actions, returning tmp_bunch: %s" % str( tmp_bunch)) return tmp_bunch From 751cca164bb68cf24325f89be9f877ac4814527d Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 18 Aug 2008 11:21:31 -0400 Subject: [PATCH 020/267] LibraryFolders now track last used dbkey ( silimar to histories ), so when a new dataset is added to a folder, the last used dbkey is selected. --- lib/galaxy/model/__init__.py | 36 +++----------- lib/galaxy/model/mapping.py | 4 +- lib/galaxy/security/__init__.py | 4 -- lib/galaxy/web/controllers/admin.py | 60 ++++++++++++++++-------- templates/admin/library/new_dataset.mako | 6 ++- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 599ab72a78b..4ef5a34744e 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -285,34 +285,26 @@ class DatasetInstance( object ): 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 self._metadata: self._metadata = dict() @@ -321,11 +313,8 @@ class DatasetInstance( object ): # Needs to accept a MetadataCollection, a bunch, or a dict self._metadata = dict( bunch.items() ) 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). - """ + # 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] @@ -343,7 +332,6 @@ class DatasetInstance( object ): #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 ) @@ -400,16 +388,12 @@ class DatasetInstance( object ): if child.designation == designation: return child return None - def get_converter_types(self): return self.datatype.get_converter_types( self, datatypes_registry) - 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: @@ -429,7 +413,6 @@ class HistoryDatasetAssociation( DatasetInstance ): 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 ): des = HistoryDatasetAssociation( hid=self.hid, name=self.name, @@ -451,7 +434,6 @@ class HistoryDatasetAssociation( DatasetInstance ): 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: @@ -469,7 +451,6 @@ class History( object ): self.user = user self.datasets = [] self.galaxy_sessions = [] - def _next_hid( self ): # TODO: override this with something in the database that ensures # better integrity @@ -481,13 +462,11 @@ class History( object ): 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 ) @@ -507,7 +486,6 @@ class History( object ): 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 @@ -534,10 +512,13 @@ class LibraryFolder( object ): self.description = description self.item_count = item_count self.order_id = order_id - def add_dataset( self, dataset ): + self.genome_build = None + def add_dataset( self, dataset, genome_build=None ): dataset.folder_id = self.id dataset.order_id = self.item_count self.item_count += 1 + if genome_build not in [None, '?']: + self.genome_build = genome_build def add_folder( self, folder ): folder.parent_id = self.id folder.order_id = self.item_count @@ -555,7 +536,6 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 ): des = HistoryDatasetAssociation( name=self.name, info=self.info, @@ -575,8 +555,6 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 ): des = LibraryFolderDatasetAssociation( name=self.name, info=self.info, @@ -597,11 +575,9 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs des.flush() return des - def clear_associated_files( self, metadata_safe = False, purge = False ): return - class LibraryTag( object ): def __init__( self, tag ): self.tag = tag diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 2303c9eb338..cd4aa9c3ee9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -66,7 +66,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 ), @@ -194,7 +193,8 @@ LibraryFolder.table = Table( "library_folder", metadata, Column( "name", TEXT ), Column( "description", TEXT ), Column( "order_id", Integer ), - Column( "item_count", Integer ) ) + Column( "item_count", Integer ), + Column( "genome_build", TrimmedString( 40 ) ) ) LibraryTag.table = Table( "library_tag", metadata, Column( "id", Integer, primary_key=True ), diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index b36881505e0..0eaba298d10 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -68,7 +68,6 @@ class GalaxyRBACAgent( RBACAgent ): 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""" - log.debug("In allow_dataset_action, user: %s, action: %s, dataset: %s" % ( str(user), str(action), str(dataset))) if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset # If dataset is in public group, we always return true for viewing and using @@ -146,7 +145,6 @@ class GalaxyRBACAgent( RBACAgent ): assoc.flush() return assoc def disassociate_group_dataset( self, group, dataset ): - log.debug("In disassociate_group_dataset, removing %s -> %s" % (group.id, dataset.id)) assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) assoc.delete() assoc.flush() @@ -239,11 +237,9 @@ class GalaxyRBACAgent( RBACAgent ): def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' if filter is None: - log.debug("In get_permitted_actions, returning RBACAgent.permitted_actions: %s" % str( RBACAgent.permitted_actions)) 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)] - log.debug("In get_permitted_actions, returning tmp_bunch: %s" % str( tmp_bunch)) return tmp_bunch diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 55f4f850646..cb8aad42c78 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -513,6 +513,10 @@ class Admin( BaseController ): return trans.show_error_message( no_privilege_msg ) if 'create_folder' in kwd: folder = trans.app.model.LibraryFolder( name = name, description = 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 (?) + folder.genome_build = util.dbnames.default_value if parent_id: parent_folder = trans.app.model.LibraryFolder.get( parent_id ) parent_folder.add_folder( folder ) @@ -540,8 +544,17 @@ class Admin( BaseController ): def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) + if isinstance( dbkey, list ): + last_used_build = dbkey[0] + else: + last_used_build = dbkey + if folder_id and not last_used_build: + folder = trans.app.model.LibraryFolder.get( folder_id ) + last_used_build = folder.genome_build data_files = [] - def add_file( file_obj, name, extension, dbkey, groups, info='no info', space_to_tab=False ): + + # add_file method + def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): data_type = None temp_name = sniff.stream_to_file( file_obj ) if space_to_tab: @@ -552,9 +565,13 @@ class Admin( BaseController ): data_type = sniff.guess_ext( temp_name, sniff_order=trans.app.datatypes_registry.sniff_order ) else: data_type = extension - dataset = trans.app.model.LibraryFolderDatasetAssociation( name = name, info = info, extension = data_type, dbkey = dbkey, create_dataset = True ) + dataset = trans.app.model.LibraryFolderDatasetAssociation( name=name, + info=info, + extension=data_type, + dbkey=dbkey, + create_dataset=True ) folder = trans.app.model.LibraryFolder.get( folder_id ) - folder.add_dataset( dataset ) + folder.add_dataset( dataset, genome_build=last_used_build ) dataset.flush() # GroupDatasetAssociations will enable security on the dataset based on the permitted_actions # associated with the GroupDatasetAssociation. The default permitted_actions at this point @@ -575,12 +592,12 @@ class Admin( BaseController ): 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 'create_dataset' in kwd: # Copied from upload tool action last_dataset_created = None @@ -603,12 +620,13 @@ class Admin( BaseController ): file_name = data_file.filename file_name = file_name.split( '\\' )[-1] file_name = file_name.split( '/' )[-1] - last_dataset_created = add_file( data_file.file, - file_name, - extension, - dbkey, + last_dataset_created = add_file( data_file.file, + file_name, + extension, + dbkey, + last_used_build, groups, - info="uploaded file", + info="uploaded file", space_to_tab=space_to_tab ) elif url_paste not in [ None, "" ]: if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: @@ -618,10 +636,11 @@ class Admin( BaseController ): if line: last_dataset_created = add_file( urllib.urlopen( line ), line, - extension, - dbkey, + extension, + dbkey, + last_used_build, groups, - info="uploaded url", + info="uploaded url", space_to_tab=space_to_tab ) else: is_valid = False @@ -632,22 +651,22 @@ class Admin( BaseController ): break if is_valid: last_dataset_created = add_file( StringIO.StringIO( url_paste ), - 'Pasted Entry', - extension, - dbkey, + 'Pasted Entry', + extension, + dbkey, + last_used_build, groups, - info="pasted entry", + info="pasted entry", space_to_tab=space_to_tab ) trans.response.send_redirect( web.url_for( action='dataset', id=last_dataset_created.id ) ) elif id is None: # 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 = trans.history.genome_build + 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() + dbkeys = get_dbkey_options( last_used_build ) # Send list of groups to the form so the dataset can be associated with 1 or more of them. groups = [] q = sa.select( ( ( galaxy.model.Group.table.c.id ).label( 'group_id' ), @@ -660,6 +679,7 @@ class Admin( BaseController ): folder_id=folder_id, file_formats=file_formats, dbkeys=dbkeys, + last_used_build=last_used_build, groups=groups ) dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) if dataset: diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 35a8106fd81..3c641288b74 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -52,7 +52,11 @@
    From f50ee0fff3fcf803778f3a38f76852ac2c987365 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 19 Aug 2008 10:04:30 -0400 Subject: [PATCH 021/267] Added security to libraries. Non-admin users can now only see libraries that contain datasets associated with the user's groups, and each library will only display those datasets that are associated with the user's groups. --- lib/galaxy/web/controllers/admin.py | 6 +- lib/galaxy/web/controllers/library.py | 81 ++++++++++++++++++++++++-- templates/admin/library/dataset.mako | 45 +++++++------- templates/library/libraries.mako | 6 +- templates/library/library.mako | 31 +++++++++- tools/data_source/access_libraries.xml | 14 ++--- 6 files changed, 142 insertions(+), 41 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index cb8aad42c78..f0ccc2f4f07 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -703,6 +703,8 @@ class Admin( BaseController ): gdpa_dict[ group_id ].append( dpa ) else: gdpa_dict[ group_id ] = [ dpa ] + # Refresh the Dataset to ensure we have a valid set of DatasetGroupAssociations + dataset.dataset.refresh() # Check to see if we need to delete any GroupDatasetAssociations. This occurs if # the user unchecked all boxes for a group for group_dataset_assoc in dataset.dataset.groups: @@ -761,9 +763,11 @@ class Admin( BaseController ): dataset_actions.sort() # Get the permitted_actions of each GroupDatasetAssociation to send to the form gdas = [] - # Refresh the dataset to ensure we have a valid set of DatasetGroupAssociations + # Refresh the Dataset to ensure we have a valid set of GroupDatasetAssociations dataset.dataset.refresh() for group_dataset_assoc in dataset.dataset.groups: + # Refresh the GroupDatasetAssociation to ensure we have a valid set of permitted_actions + group_dataset_assoc.refresh() group = galaxy.model.Group.get( group_dataset_assoc.group_id ) gdas.append( ( group.id, group.name, group_dataset_assoc.permitted_actions ) ) if "dbkey" in dataset.datatype.metadata_spec and not dataset.metadata.dbkey: diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 94e7b5731e0..43c45a58402 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -6,9 +6,12 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose - def index( self, trans, library_id = None, import_ids = [], **kwd ): - #use for importing an entry into your history + def index( self, trans, library_id=None, import_ids=[], **kwd ): + # Need user to get associated Groups and Datasets + user = trans.get_user() + libraries = [] if import_ids: + # Used for importing a dataset into a user's history if not isinstance( import_ids, list ): import_ids = [import_ids] history = trans.get_history() @@ -19,5 +22,75 @@ class Library( BaseController ): history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) elif library_id: - return trans.fill_template( '/library/library.mako', library=trans.app.model.Library.get( library_id ) ) - return trans.fill_template( '/library/libraries.mako', libraries=trans.app.model.Library.select() ) + # Since permitted_actions are kept with the GroupDatasetAssociation, each accessible Library will only + # display the subset of [ it's complete set of ] datasets that the user has permission to access. We + # pass group_ids so this can be handled in the template. + if not user: + group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] + else: + group_ids = [] + for user_group_assoc in user.groups: + group_ids.append( user_group_assoc.group_id ) + library = trans.app.model.Library.get( library_id ) + return trans.fill_template( '/library/library.mako', library=library, group_ids=group_ids ) + if user: + # Only display libraries that contain datasets associated with the user's groups + group_ids = [] + for user_group_assoc in user.groups: + group = trans.app.model.Group.get( user_group_assoc.group_id ) + group_ids.append( group.id ) + libs = trans.app.model.Library.select() + for library in libs: + user_can_access = False + # Check for public datasets in the Library's root folder + for library_folder_dataset_assoc in library.root_folder.datasets: + if user_can_access: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + libraries.append( library ) + user_can_access = True + break + for folder in library.root_folder.folders: + if user_can_access: + break + for library_folder_dataset_assoc in folder.datasets: + if user_can_access: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + libraries.append( library ) + user_can_access = True + break + else: + # Only display libraries that contain datasets associated with the public group + group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] + libs = trans.app.model.Library.select() + for library in libs: + public_library = False + # Check for public datasets in the Library's root folder + for library_folder_dataset_assoc in library.root_folder.datasets: + if public_library: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + libraries.append( library ) + public_library = True + break + # Check for public datasets in the root folder's sub-folders + for folder in library.root_folder.folders: + if public_library: + break + for library_folder_dataset_assoc in folder.datasets: + if public_library: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + libraries.append( library ) + public_library = True + break + return trans.fill_template( '/library/libraries.mako', group_ids=group_ids, libraries=libraries ) diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index b522a05256d..a7877e54479 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%def name="title()">Edit Dataset Attributes + <%def name="datatype( dataset, datatypes )"> + +<%def name="group_dataset_permitted_actions( dataset_actions, gda )"> + %for da in dataset_actions: + <% check = False %> + %for action in gda[2]: + %if action == da: + <% + check = True + break + %> + %endif + %endfor + %if check: + + %else: + + %endif + ${da}
    + %endfor + +
    Group Associations
    %for gda in gdas: -
    - ${gda[1]} -
    +
    ${gda[1]}
    - %for da in dataset_actions: - <% check = False %> - %for action in gda[2]: - %if action == da: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - - %endif - ${da}
    - %endfor -
    + ${group_dataset_permitted_actions( dataset_actions, gda )}
    %endfor
    diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako index 247333f709c..1176bd2162a 100644 --- a/templates/library/libraries.mako +++ b/templates/library/libraries.mako @@ -1,12 +1,12 @@ <%inherit file="/base.mako"/> -<%def name="title()">View Libraries +<%def name="title()">Libraries You Can Access
    -
    View Library
    +
    Libraries You Can Access
    %for library in libraries: %endfor
    diff --git a/templates/library/library.mako b/templates/library/library.mako index 2706efcde25..373ff9c4118 100644 --- a/templates/library/library.mako +++ b/templates/library/library.mako @@ -3,17 +3,41 @@ <%def name="render_component( component )"> <% if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) + render = False + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + # TODO: Do we need to upgrade sqlalchemy? The following shouldn't be necessary if the mappers work correctly. + # Check the folder's sub-folders to see what can be rendered + for library_folder in component.folders: + render_component( library_folder ) + if render: + return render_folder( component ) elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - return render_dataset( component ) + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + if render: + return render_dataset( component ) %> + ## Render the dataset `data` as history item, using `hid` as the displayed id <%def name="render_dataset( data )">
    ${data.name}
    + ## Render a folder <%def name="render_folder( this_folder )">
    @@ -31,11 +55,12 @@
    + <%def name="title()">View Library: ${library.name}
    Import from Library: ${library.name}
    - + ${render_folder( library.root_folder )}
    diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml index ffd383c481a..e6dabfbf1b1 100644 --- a/tools/data_source/access_libraries.xml +++ b/tools/data_source/access_libraries.xml @@ -1,11 +1,7 @@ - - stored locally - - - - - - - + stored locally + + + + \ No newline at end of file From 64b785cdc7e1ff1e7022d2ca92d6dd5f10643075 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 19 Aug 2008 16:46:38 -0400 Subject: [PATCH 022/267] Enhancement to the Admin GUI: added link to Libraries that a specific user can access because they contain datasets associated with 1 of the user's groups. Also optimized some code in the library controller. --- lib/galaxy/web/controllers/admin.py | 56 ++++++++++- lib/galaxy/web/controllers/library.py | 96 ++++++++----------- .../specified_users_groups.mako | 22 ++++- .../specified_users_group_libraries.mako | 27 ++++++ templates/library/library.mako | 1 - 5 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 templates/admin/library/specified_users_group_libraries.mako diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f0ccc2f4f07..f3ff2773608 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -461,6 +461,7 @@ class Admin( BaseController ): galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ] ) for row2 in q2.execute(): total_datasets = row2.total_datasets + libraries = [] permitted_actions = [] # There may not yet be any GroupDatasetAssociations, in which case no # actions will be found @@ -468,16 +469,42 @@ class Admin( BaseController ): for action in row2.permitted_actions: permitted_actions.append( action.encode( 'ascii' ) ) permitted_actions.sort() + # If we have permitted actions, then we have at least 1 GroupDatasetAssociation, in + # which case, we can see if we have any Libraries that the user can access + libs = trans.app.model.Library.select() + for library in libs: + folder = library.root_folder + components = list( folder.folders ) + list( folder.datasets ) + for component in components: + if self.renderable( trans, component, row2.group_id ): + libraries.append( library.id ) + break groups.append( ( row.group_id, escape( row.group_name, entities ), row.group_priority, row2.total_datasets, - permitted_actions ) ) + permitted_actions, + libraries ) ) return trans.fill_template( '/admin/dataset_security/specified_users_groups.mako', user_id=user_id, user_email=escape( user_email, entities ), - groups=groups, + groups=groups, msg=msg ) + @web.expose + def specified_users_group_libraries( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg + library_ids = params.library_ids.split( ',' ) + libraries = [] + for id in library_ids: + library = trans.app.model.Library.get( id ) + libraries.append( library ) + return trans.fill_template( '/admin/library/specified_users_group_libraries.mako', + user_email=params.user_email, + group_name=params.group_name, + libraries=libraries ) # Galaxy Library Stuff @web.expose @@ -794,3 +821,28 @@ class Admin( BaseController ): err=None ) else: return trans.show_error_message( "Invalid dataset specified" ) + def renderable( self, trans, component, group_id ): + render = False + if isinstance( component, trans.app.model.LibraryFolder ): + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id == group_id: + render = True + break + # Check the folder's sub-folders to see what can be rendered + if not render: + for library_folder in component.folders: + self.renderable( trans, library_folder, group_id ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id == group_id: + render = True + break + return render + diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 43c45a58402..5514fe113eb 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -33,64 +33,46 @@ class Library( BaseController ): group_ids.append( user_group_assoc.group_id ) library = trans.app.model.Library.get( library_id ) return trans.fill_template( '/library/library.mako', library=library, group_ids=group_ids ) - if user: - # Only display libraries that contain datasets associated with the user's groups - group_ids = [] - for user_group_assoc in user.groups: - group = trans.app.model.Group.get( user_group_assoc.group_id ) - group_ids.append( group.id ) + else: + if user: + # Only display libraries that contain datasets associated with the user's groups + group_ids = [] + for user_group_assoc in user.groups: + group = trans.app.model.Group.get( user_group_assoc.group_id ) + group_ids.append( group.id ) + else: + # Only display libraries that contain datasets associated with the public group + group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] libs = trans.app.model.Library.select() for library in libs: - user_can_access = False - # Check for public datasets in the Library's root folder - for library_folder_dataset_assoc in library.root_folder.datasets: - if user_can_access: + folder = library.root_folder + components = list( folder.folders ) + list( folder.datasets ) + for component in components: + if self.renderable( trans, component, group_ids ): + libraries.append( library ) break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - libraries.append( library ) - user_can_access = True - break - for folder in library.root_folder.folders: - if user_can_access: - break - for library_folder_dataset_assoc in folder.datasets: - if user_can_access: - break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - libraries.append( library ) - user_can_access = True - break - else: - # Only display libraries that contain datasets associated with the public group - group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] - libs = trans.app.model.Library.select() - for library in libs: - public_library = False - # Check for public datasets in the Library's root folder - for library_folder_dataset_assoc in library.root_folder.datasets: - if public_library: - break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - libraries.append( library ) - public_library = True - break - # Check for public datasets in the root folder's sub-folders - for folder in library.root_folder.folders: - if public_library: - break - for library_folder_dataset_assoc in folder.datasets: - if public_library: - break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - libraries.append( library ) - public_library = True - break return trans.fill_template( '/library/libraries.mako', group_ids=group_ids, libraries=libraries ) + def renderable( self, trans, component, group_ids ): + render = False + if isinstance( component, trans.app.model.LibraryFolder ): + # Check the folder's datasets to see what can be rendered + for library_folder_dataset_assoc in component.datasets: + if render: + break + dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + # Check the folder's sub-folders to see what can be rendered + if not render: + for library_folder in component.folders: + self.renderable( trans, library_folder, group_ids ) + elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): + render = False + dataset = trans.app.model.Dataset.get( component.dataset_id ) + for group_dataset_assoc in dataset.groups: + if group_dataset_assoc.group_id in group_ids: + render = True + break + return render diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako index 8a392a67e31..e5678635c96 100644 --- a/templates/admin/dataset_security/specified_users_groups.mako +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -17,20 +17,27 @@

    Groups of which '${email}' is a member

    %if msg: - + %endif %if len( groups ) == 0: - + %else: + <% ctr = 0 %> %for group in groups: - <% gn = unescape( group[1], unentities ) %> + <% + gn = unescape( group[1], unentities ) + library_ids = '' + for library_id in group[5]: + library_ids += "%s," % library_id + library_ids = library_ids.rstrip( ',' ) + %> %if ctr % 2 == 1: %else: @@ -48,9 +55,16 @@ ${da}
    %endfor + <% ctr += 1 %> - %endfor + %endfor %endif

    ${msg}

    ${msg}

    User '${email}' belongs to no groups
    User '${email}' belongs to no groups
    Group Priority Datasets Permitted Actions on DatasetsContaining Libraries
    + %if len( group[5] ) > 0: + ${len( group[5] )} + %else: + ${len( group[5] )} + %endif +
    diff --git a/templates/admin/library/specified_users_group_libraries.mako b/templates/admin/library/specified_users_group_libraries.mako new file mode 100644 index 00000000000..37241869cf3 --- /dev/null +++ b/templates/admin/library/specified_users_group_libraries.mako @@ -0,0 +1,27 @@ +<%inherit file="/base.mako"/> + +<% + from galaxy.web.controllers.admin import entities, unentities + from xml.sax.saxutils import escape, unescape +%> + +<%def name="title()">Specified Users Group Libraries{ +<% + gn = unescape( group_name, unentities ) + email = unescape( user_email, unentities ) +%> +
    +
    + Libraries  |   + Groups  |   + Users +
    +
    Libraries containing datasets associated with group '${gn}' that user '${email}' can access
    +
    + %for library in libraries: + + %endfor +
    +
    diff --git a/templates/library/library.mako b/templates/library/library.mako index 373ff9c4118..3c3002caa5d 100644 --- a/templates/library/library.mako +++ b/templates/library/library.mako @@ -13,7 +13,6 @@ if group_dataset_assoc.group_id in group_ids: render = True break - # TODO: Do we need to upgrade sqlalchemy? The following shouldn't be necessary if the mappers work correctly. # Check the folder's sub-folders to see what can be rendered for library_folder in component.folders: render_component( library_folder ) From 3802816b69b2bca3341e3386b3dd38d47986fed7 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 20 Aug 2008 18:11:04 -0400 Subject: [PATCH 023/267] User interface to permissions, and a bunch of changes to ensure that both permitted_actions and groups are set appropriately on datasets. Somewhat tested and mostly working, but needs more testing. Greg: If a userless session has a history, if that user logs in, the DefaultHistoryGroupAssociation for the current history as well as the datasets in that history are supposed to be updated with the permissions in DefaultUserGroupAssociation, but this isn't happening. Possibly because the default permissions on userless histories create datasets in the public group with only the DATASET_ACCESS permission (or possibly for another reason). What are the implications of giving public users DATASET_MANAGE_PERMISSIONS? --- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/security/__init__.py | 165 ++++++++++++--------- lib/galaxy/tools/__init__.py | 6 +- lib/galaxy/tools/actions/__init__.py | 7 +- lib/galaxy/tools/actions/upload.py | 6 +- lib/galaxy/tools/parameters/basic.py | 10 +- lib/galaxy/web/controllers/async.py | 3 +- lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/root.py | 69 ++++----- lib/galaxy/web/controllers/user.py | 36 ++--- lib/galaxy/web/framework/__init__.py | 3 +- static/june_2007_style/blue/base.css | 6 +- templates/dataset/edit_attributes.mako | 85 ++++++++--- templates/history/permissions.mako | 56 +++++-- templates/user/permissions.mako | 55 +++++-- tools/data_source/encode_import_code.py | 3 +- tools/data_source/microbial_import_code.py | 3 +- tools/maf/maf_to_bed_code.py | 3 +- 18 files changed, 299 insertions(+), 221 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index cd4aa9c3ee9..4922b4fd7d5 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -619,7 +619,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): orphans = result.Dataset.get_by( history_id = None ) if orphans: for dataset in orphans: - result.security_agent.set_dataset_groups( dataset, [ public_group ] ) + result.security_agent.set_dataset_permissions( dataset, [ ( public_group, result.security_agent.permitted_actions.DATASET_ACCESS ) ] ) else: result.security_agent.guess_public_group() log.debug( "Public Group identified as id = %s." % ( Group.public_id ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 0eaba298d10..9c497437120 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -7,10 +7,6 @@ from galaxy.util.bunch import Bunch log = logging.getLogger(__name__) -# TODO, Nate: Think about whether the following permitted actions are appropriate for the dataset and -# group objects. What should be the default "public" permitted actions? Make sure that the public group -# and public datasets are set with the correct permitted actions. Also make sure that "private" settings -# are correct when an authenticated user creates things inside their "private" environment. class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( @@ -23,9 +19,14 @@ class RBACAgent: # use in a job, etc). DATASET_ACCESS = 'dataset_access' ) + permitted_action_descriptions = Bunch( + DATASET_EDIT_METADATA = "Edit this dataset's metadata in the library", + DATASET_MANAGE_PERMISSIONS = "Manage the groups associated with this dataset (and those groups' permissions on the dataset)", + DATASET_ACCESS = "View, import, and perform analyses on this dataset" + ) 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_groups_permitted_actions_for_datasets( self, datasets = [] ): + 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 @@ -35,12 +36,12 @@ class RBACAgent: raise 'No valid method of creating group with %s' % ( kwd ) def create_private_user_group( self, user ): raise "Unimplemented Method" - def user_set_default_access( self, user, groups = None, history = False, dataset = False ): + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): raise "Unimplemented Method" def setup_new_user( self, user ): self.user_set_default_access( user, history = True, dataset = True ) self.associate_components( user=user, group=self.get_public_group() ) - def history_set_default_access( self, history, groups=None, dataset=False ): + def history_set_default_access( self, history, permissions=None, dataset=False ): raise "Unimplemented Method" def set_public_group( self, group ): raise "Unimplemented Method" @@ -48,7 +49,7 @@ class RBACAgent: raise "Unimplemented Method" def guess_public_group( self ): raise "Unimplemented Method" - def set_dataset_groups( self, dataset, groups ): + def set_dataset_permissions( self, dataset, permissions ): raise "Unimplemented Method" def set_dataset_permitted_actions( self, dataset ): raise "Unimplemented Method" @@ -56,6 +57,24 @@ class RBACAgent: 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_action_description( self, permitted_action ): + """ + Return the description of a permitted_action, regardless of + whether permitted_action is a key or value. + """ + if self.permitted_action_descriptions.get( permitted_action ) is not None: + return self.permitted_action_descriptions.get( permitted_action ) + else: + for k, v in self.permitted_actions.items(): + if v == permitted_action: + return self.permitted_action_descriptions.get( k ) + return permitted_action # so at least something useful is printable class GalaxyRBACAgent( RBACAgent ): def __init__( self, model, permitted_actions=None ): @@ -83,35 +102,41 @@ class GalaxyRBACAgent( RBACAgent ): if action in group_dataset_assoc.permitted_actions: return True return False # No user and dataset not in public group, or user lacks permission - def guess_derived_groups_for_datasets( self, datasets=[] ): - # TODO, Nate: Make sure this method is functionally correct. - """Returns a list of groups for the output dataset based upon itself and provided datasets""" - access_groups = None - priority_access_group = None + def guess_derived_permissions_for_datasets( self, datasets=[] ): + """Returns a list of group/action tuples for the output dataset based upon provided datasets""" + intersect = None + priority_access_perms = None for dataset in datasets: # Determine access groups for output datasets - these groups are the # intersection across all inputs. If we end up with no intersection # between inputs, then we rely on priorities if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset - groups = [ data_group_assoc.group for data_group_assoc in dataset.groups ] - for group in groups: - if priority_access_group is None or priority_access_group.priority < group.priority: - priority_access_group = group - if access_groups is None: - access_groups = set( groups ) + assocs = [ assoc for assoc in dataset.groups ] + for assoc in assocs: + if priority_access_perms is None or priority_access_perms[0].priority < assoc.group.priority: + priority_access_perms = ( assoc.group, assoc.permitted_actions ) + if intersect is None: + intersect = [ (a.group, a.permitted_actions) for a in assocs ] else: - access_groups.intersection_update( set( groups ) ) - # Complete lists for output dataset access - if access_groups: - access_groups = list( access_groups) - else: - access_groups = [] + # Intersect existing perms with new perms + #access_assocs = filter( lambda x: x.group in [ a.group for a in access_assocs ], assocs ) + new_intersect = [] + for group, actions in intersect: + matches = filter( lambda x: x.group == group, assocs ) + # Could a dataset ever have more than one GDA with the same group id? + if len( matches ) > 1: + log.error( "Unable to derive permissions, duplicate group_dataset_association rows exist for group %d, dataset %d" % (group.id, dataset.id) ) + elif len( matches ) == 1: + # compare permitted_actions + pa_intersect = filter( lambda x: x in actions, matches[0].permitted_actions ) + new_intersect.append( ( group, pa_intersect ) ) + intersect = new_intersect # If we have no groups left after intersection, take the highest priority group - if not access_groups: - if priority_access_group: - access_groups = [ priority_access_group ] - return access_groups + if not intersect: + if priority_access_perms: + intersect = [ priority_access_perms ] + return intersect def get_group( self, id ): return self.model.Group.get( id ) raise 'No valid method of retrieving requested group %s' % ( id ) @@ -125,29 +150,18 @@ class GalaxyRBACAgent( RBACAgent ): if 'dataset' in kwd: if 'group' in kwd: return self.associate_group_dataset( kwd['group'], kwd['dataset'] ) + elif 'permissions' in kwd: + return self.associate_group_dataset( kwd['permissions'][0], kwd['dataset'], kwd['permissions'][1] ) elif 'user' in kwd: if 'group' in kwd: return self.associate_user_group( kwd['user'], kwd['group'] ) raise 'No valid method of associating provided components: %s' % kwd - def disassociate_components( self, **kwd ): - assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to disassociate.' - if 'dataset' in kwd: - if 'group' in kwd: - return self.disassociate_group_dataset( kwd['group'], kwd['dataset'] ) - raise 'No valid method of associating provided components: %s' % kwd - def associate_group_dataset( self, group, dataset, permitted_actions=[] ): - if not permitted_actions: - if isinstance( dataset.permitted_actions, Bunch ): - permitted_actions = dataset.permitted_actions.__dict__.values() - else: - permitted_actions = dataset.permitted_actions + def associate_group_dataset( self, group, dataset, permitted_actions=[ RBACAgent.permitted_actions.DATASET_ACCESS ] ): + # HACK: The default permitted_actions should really not be used... need to find cases where this is done and correct it + log.debug("In associate_permissions_dataset, dataset: %d, group: %d, permitted_actions: %s" % ( dataset.id, group.id, permitted_actions ) ) assoc = self.model.GroupDatasetAssociation( group, dataset, permitted_actions ) assoc.flush() return assoc - def disassociate_group_dataset( self, group, dataset ): - assoc = self.model.GroupDatasetAssociation.selectone_by( group_id = group.id, dataset_id = dataset.id ) - assoc.delete() - assoc.flush() def associate_user_group( self, user, group ): assoc = self.model.UserGroupAssociation( user, group ) assoc.flush() @@ -161,66 +175,69 @@ class GalaxyRBACAgent( RBACAgent ): self.associate_components( group=group, user=user ) group.flush() return group - def user_set_default_access( self, user, groups = None, history = False, dataset = False ): - # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. - if groups is None: - groups = [ self.create_private_user_group( user ) ] - if groups is not None: + def user_set_default_access( self, user, permissions = None, history = False, dataset = False ): + if permissions is None: + permissions = [ ( self.create_private_user_group( user ), self.permitted_actions.__dict__.values() ) ] + if permissions is not None: for assoc in user.default_groups: #this is the association not the actual group assoc.delete() assoc.flush() - for group in groups: - if isinstance( group, self.model.Group ): - permitted_actions = group.permitted_actions.__dict__.values() - else: - permitted_actions = group.permitted_actions + for group, permitted_actions in permissions: + log.debug("In user_set_default_access, user: %s, group: %s, permitted_actions: %s" % (user.email, group.name, str( permitted_actions))) assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) assoc.flush() if history: for history in user.histories: - self.history_set_default_access( history, groups=groups, dataset=dataset ) - def history_set_default_access( self, history, groups=None, dataset=False ): - # TODO, Nate: Make sure this method is functionally correct with permitted actions set appropriately. - if groups is None: + self.history_set_default_access( history, permissions=permissions, dataset=dataset ) + def user_get_default_access( self, user ): + return [ ( duga.group, duga.permitted_actions ) for duga in user.default_groups ] + def history_set_default_access( self, history, permissions=None, dataset=False ): + if permissions is None: if history.user: - groups = [ assoc.group for assoc in history.user.default_groups ] + permissions = self.user_get_default_access( history.user ) else: - groups = [ self.get_public_group() ] - if groups is not None: + # FIXME: should this be all permissions? + permissions = [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] + if permissions is not None: for assoc in history.default_groups: #this is the association not the actual group assoc.delete() assoc.flush() - for group in groups: - if isinstance( group, self.model.Group ): - permitted_actions = group.permitted_actions.__dict__.values() - else: - permitted_actions = group.permitted_actions + for group, permitted_actions in permissions: + log.debug("In history_set_default_access, history: %s, group: %s, permitted_actions: %s" % (history.id, group.name, str( permitted_actions))) assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) assoc.flush() if dataset: for data in history.datasets: for hda in data.dataset.history_associations: if history.user and hda.history not in history.user.histories: - self.set_dataset_groups( data.dataset, [ self.get_public_group() ] ) + self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) break else: - self.set_dataset_groups( data.dataset, groups ) + self.set_dataset_permissions( data.dataset, permissions ) + def history_get_default_access( self, history ): + return [ ( dhga.group, dhga.permitted_actions ) for dhga in history.default_groups ] def get_public_group( self ): return self.model.Group.get_public_group() def set_public_group( self, group ): return self.model.Group.set_public_group( group ) def guess_public_group( self ): return self.model.Group.guess_public_group() - def set_dataset_groups( self, dataset, groups ): + def set_dataset_permissions( self, dataset, permissions ): + """ + Apply permissions (a list of (group, permitted_action) tuples) + to a dataset, removing any existing permissions. permissions can + also be a list of GroupDatasetAssociations (for simplicity). + """ if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset for group_dataset_assoc in dataset.groups: group_dataset_assoc.delete() group_dataset_assoc.flush() - for group in groups: - if not isinstance( group, self.model.Group ): - group = group.group - self.associate_components( dataset=dataset, group=group ) + if isinstance( permissions[0], self.model.GroupDatasetAssociation ): + permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] + for ptuple in permissions: + log.debug("In set_dataset_permissions, before elf.associate_components, dataset: %s, group: %s, permitted_actions: %s" % ( str(dataset.id), str(ptuple[0].id), str(ptuple[1]))) + self.associate_components( dataset=dataset, permissions=ptuple ) def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 7b5c4b0d305..4030cadd47e 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1085,8 +1085,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 ) - # TODO, Nate: Make sure the following is functionally correct. - self.app.security_agent.set_dataset_groups( child_dataset.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_permissions( child_dataset.dataset, outdata.dataset.groups ) # Move data from temp location to dataset location shutil.move( filename, child_dataset.file_name ) child_dataset.flush() @@ -1123,8 +1122,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 ) - # TODO, Nate: Make sure the following is functionally correct. - self.app.security_agent.set_dataset_groups( primary_data.dataset, outdata.dataset.groups ) + self.app.security_agent.set_dataset_permissions( primary_data.dataset, outdata.dataset.groups ) 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 bd74d188ee6..efc0ce99893 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -86,11 +86,10 @@ class DefaultToolAction( object ): # Determine output dataset permitted_actions list existing_datasets = [ inp for inp in inp_data.values() if inp ] if existing_datasets: - output_access_groups = trans.app.security_agent.guess_derived_groups_for_datasets( 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_access_groups = [ group.group for group in trans.history.default_groups ] - + output_permissions = trans.app.security_agent.history_get_default_access( trans.history ) # Build name for output datasets based on tool name and input names if len( input_names ) == 1: on_text = input_names[0] @@ -132,7 +131,7 @@ class DefaultToolAction( object ): data = trans.app.model.HistoryDatasetAssociation( extension=ext, create_dataset=True ) # Commit the dataset immediately so it gets database assigned unique id data.flush() - trans.app.security_agent.set_dataset_groups( data.dataset, output_access_groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, output_permissions ) # Create an empty file immediately open( data.file_name, "w" ).close() # This may not be neccesary with the new parent/child associations diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index a9623ae40e0..9fba4fe467d 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -66,8 +66,7 @@ class UploadToolAction( object ): def upload_empty(self, trans, err_code, err_msg): data = trans.app.model.HistoryDatasetAssociation( create_dataset=True ) - # TODO, Nate: Make sure the following is appropriate. - trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( trans.history ) ) data.name = err_code data.extension = "txt" data.dbkey = "?" @@ -161,8 +160,7 @@ class UploadToolAction( object ): info = 'uploaded %s file' %data_type data = trans.app.model.HistoryDatasetAssociation( history = trans.history, extension = ext, create_dataset = True ) - # TODO, Nate: Make sure the following is appropriate. - trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( 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 b626e665472..4167629927b 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -992,15 +992,15 @@ class DataToolParameter( ToolParameter ): >>> group.flush() >>> Group.public_id = group.id >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset1, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset1, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset2, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset2, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset3, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset3, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset4, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset4, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) - >>> security_agent.set_dataset_groups( dataset5, [ group ] ) + >>> security_agent.set_dataset_permissions( dataset5, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> hist.add_dataset( dataset1 ) >>> hist.add_dataset( dataset2 ) >>> hist.add_dataset( dataset3 ) diff --git a/lib/galaxy/web/controllers/async.py b/lib/galaxy/web/controllers/async.py index dcb6a0c1be5..f0e4ec01e7f 100644 --- a/lib/galaxy/web/controllers/async.py +++ b/lib/galaxy/web/controllers/async.py @@ -104,8 +104,7 @@ class ASync( BaseController ): #history.datasets.add_dataset( data ) data = trans.app.model.HistoryDatasetAssociation( create_dataset = True, extension = GALAXY_TYPE ) - # TODO, Nate: Make sure the following is functionally correct. - trans.app.security_agent.set_dataset_groups( data.dataset, trans.history.default_groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, trans.app.security_agent.history_get_default_access( 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 3d43b5bef5e..8e7a09bfc9d 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -126,4 +126,4 @@ class DatasetInterface( BaseController ): except: raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: - raise paste.httpexceptions.HTTPForbidden( "You are not permitted to access this dataset." ) + return trans.show_error_message( "You are not privileged to access this dataset." ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 6850b069cb7..83ab538e87f 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -14,8 +14,6 @@ import urllib log = logging.getLogger( __name__ ) class RootController( BaseController ): - # TODO, Nate: This is where a lot of the new dataset security stuff is managed. - # Make sure it is is functionally correct. @web.expose def default(self, trans, target1=None, target2=None, **kwd): @@ -266,23 +264,18 @@ class RootController( BaseController ): """The user clicked the change_permission button on the 'Change permissions' form""" if not trans.user: return trans.show_error_message( "You must be logged in if you want to change dataset permitted actions." ) - private_dataset = 'private_dataset' - public_group = trans.app.security_agent.get_public_group() - if private_dataset in kwd and trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): - #check user has permission and then remove public group - if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): - trans.app.security_agent.disassociate_components( dataset = data, group = public_group ) - else: - return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) - elif private_dataset not in kwd and not trans.app.security_agent.dataset_has_group( data.dataset.id, public_group.id ): - #check user has permission and then add public group - if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): - trans.app.security_agent.associate_components( dataset = data, group = public_group) - else: - return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) + if trans.app.security_agent.allow_action( trans.user, data.dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data.dataset ): + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + permissions = [] + for group_id in group_ids_checked: + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + actions = trans.app.security_agent.convert_permitted_action_strings( action_strings ) + permissions.append( ( trans.app.security_agent.get_group( group_id ), actions ) ) + trans.app.security_agent.set_dataset_permissions( data.dataset, permissions ) + return trans.show_ok_message( "Dataset permissions have been set.", refresh_frames=['history'] ) else: - return trans.show_error_message( "You have not specified a valid change of permitted actions." ) - return trans.show_ok_message( 'Permitted actions have been changed.', refresh_frames=['history'] ) + return trans.show_error_message( "You are not authorized to change this dataset's permitted actions." ) data.datatype.before_edit( data ) @@ -596,12 +589,12 @@ class RootController( BaseController ): """Adds a POSTed file to a History""" try: history = trans.app.model.History.get( history_id ) - groups = history.default_groups + groups = trans.app.security_agent.history_get_default_access( history ) if copy_access_from: copy_access_from = trans.app.model.HistoryDatasetAssociation.get( copy_access_from ) - groups = copy_access_from.dataset.groups + group_dataset_associations = copy_access_from.dataset.groups data = trans.app.model.HistoryDatasetAssociation( name = name, info = info, extension = ext, dbkey = dbkey, create_dataset = True ) - trans.app.security_agent.set_dataset_groups( data.dataset, groups ) + trans.app.security_agent.set_dataset_permissions( data.dataset, group_dataset_associations ) data.flush() data_file = open( data.file_name, "wb" ) file_data.file.seek( 0 ) @@ -629,34 +622,24 @@ class RootController( BaseController ): if 'set_permitted_actions' in kwd: """The user clicked the set_permitted_actions button on the set_permitted_actions form""" history = trans.get_history() - group_in = [] - group_out = [] - # Collect groups as entered by user - for name, value in kwd.items(): - if name.startswith( "group_" ): - group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) - if not group: - return trans.show_error_message( 'You have specified an invalid group.' ) - if value == 'in': - group_in.append( group ) - else: - group_out.append( group ) - if not group_in: + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + if not group_ids_checked: return trans.show_error_message( "You must specify at least one default group." ) - cur_groups = [ assoc.group for assoc in history.default_groups ] - group_in.sort() - cur_groups.sort() - if cur_groups != group_in: - trans.app.security_agent.history_set_default_access( history, groups=group_in ) - return trans.show_ok_message( 'Default history permitted actions have been changed.' ) - else: - return trans.show_error_message( "You did not specify any changes to this history's default permitted actions." ) + permissions = [] + for group_id in group_ids_checked: + group = trans.app.security_agent.get_group( group_id ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + permissions.append( ( group, trans.app.security_agent.convert_permitted_action_strings( action_strings ) ) ) + trans.app.security_agent.history_set_default_access( history, permissions = permissions ) + return trans.show_ok_message( 'Default history permitted actions 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 permitted actions." ) - @web.expose def dataset_make_primary( self, trans, id=None): """Copies a dataset and makes primary""" diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index d46abb922c5..5b00894d1f9 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -170,33 +170,23 @@ class User( BaseController ): @web.expose def set_default_permitted_actions( self, trans, **kwd ): - # TODO, Nate: Make sure this method is functionally correct. """Sets the user's default permitted actions for the new histories""" if trans.user: if 'set_permitted_actions' in kwd: """The user clicked the set_permitted_actions button on the set_permitted_actions form""" - group_in = [] - group_out = [] - # Collect groups as entered by user - for name, value in kwd.items(): - if name.startswith( "group_" ): - group = trans.app.security_agent.get_group( name.replace( "group_", "", 1 ) ) - if not group: - return trans.show_error_message( 'You have specified an invalid group.' ) - if value == 'in': - group_in.append( group ) - else: - group_out.append( group ) - if not group_in: - return trans.show_error_message( "You must specify at least one default group." ) - cur_groups = [ assoc.group for assoc in trans.user.default_groups ] - group_in.sort() - cur_groups.sort() - if cur_groups != group_in: - trans.app.security_agent.user_set_default_access( trans.user, groups = group_in ) - return trans.show_ok_message( 'Default new history permitted actions have been changed.' ) - else: - return trans.show_error_message( "You did not specify any changes to new history's default permitted actions." ) + group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] + group_ids_checked = filter( lambda x: not x.count('_'), group_args ) + if not group_ids_checked: + return trans.show_error_message( "You must specify at least one default group." ) + permissions = [] + for group_id in group_ids_checked: + group = trans.app.security_agent.get_group( group_id ) + if not group: + return trans.show_error_message( 'You have specified an invalid group.' ) + action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] + permissions.append( ( group, trans.app.security_agent.convert_permitted_action_strings( action_strings ) ) ) + trans.app.security_agent.user_set_default_access( trans.user, permissions ) + return trans.show_ok_message( 'Default new history permitted actions have been changed.' ) return trans.fill_template( 'user/permissions.mako' ) else: # User not logged in, history group must be only public diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 87f099a681d..bd3741a939d 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -433,10 +433,9 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.flush() self.__galaxy_session = galaxy_session if history is not None and user is not None: - # TODO, Nate: Make sure the following is functionally correct if not history.user: # This user will now acquire previously non-owned history, so set permitted actions to user's default - self.app.security_agent.history_set_default_access( history, groups=user.default_groups, dataset=True ) + self.app.security_agent.history_set_default_access( history, dataset=True ) history.user_id = user.id history.flush() self.__history = history diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index e0ac97ea56b..2cf80e80722 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -414,4 +414,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index c54f6eb0d1a..712b2eccfc5 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -133,33 +133,80 @@

    -%if trans.app.config.enable_beta_features and trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): +%if trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): +

    -
    Change Permitted Actions
    +
    Change Dataset Access Permissions
    - - <% checked = "" %> - %if not trans.app.security_agent.dataset_has_group( data.id, trans.app.model.Group.get_public_group().id ): - <% checked = " checked" %> - %endif -
    - -
    -
    -
    - This will prevent other users from viewing or utilizing this dataset, even if you share your history with them. -
    -
    -
    -
    + <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> + <% dataset_group_ids = [ assoc.group.id for assoc in data.dataset.groups ] %> +
    + Check each group which should have access to this dataset. +
    + %for group in user_groups: + %if group.id in dataset_group_ids: + <% assoc = filter( lambda x: x.group_id == group.id, data.dataset.groups )[0] %> + %else: + <% assoc = None %> + %endif + ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor
    +%elif trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ) ): +
    +
    Dataset Access Permissions
    +
    +
    + +
    + %for assoc in data.dataset.groups: + ${assoc.group.name} +
      + %for action in assoc.permitted_actions: +
    • ${trans.app.security_agent.get_permitted_action_description(action)}
    • + %endfor +
    + %endfor +
    + You do not have permission to edit this dataset's permissions. +
    +
    +
    +
    %endif diff --git a/templates/history/permissions.mako b/templates/history/permissions.mako index 6607f002c41..013481d9351 100644 --- a/templates/history/permissions.mako +++ b/templates/history/permissions.mako @@ -2,6 +2,23 @@ <%def name="title()">Change Default History Permitted Actions %if trans.user: +
    Change Default History Permitted Actions
    @@ -9,27 +26,34 @@
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.get_history().default_groups ] %> -
    - - %for group in user_groups: - + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    %endfor -
    GroupInOut
    ${group.name}
    -
    - This will change the default permitted actions assigned to new datasets for your current history. + This will change the default permitted actions assigned + to new datasets in your current history. You may also + specify the defaults for new histories via the + user options. +
    @@ -39,4 +63,4 @@
    -%endif \ No newline at end of file +%endif diff --git a/templates/user/permissions.mako b/templates/user/permissions.mako index 7b6b90f976d..e451bc71c29 100644 --- a/templates/user/permissions.mako +++ b/templates/user/permissions.mako @@ -2,6 +2,23 @@ <%def name="title()">Change Default History Permitted Actions %if trans.user: +
    Change Default Permitted Actions for new Histories
    @@ -9,27 +26,33 @@
    <% user_groups = [ assoc.group for assoc in trans.user.groups ] %> <% cur_groups = [ assoc.group for assoc in trans.user.default_groups ] %> -
    - - %for group in user_groups: - + + checked + %else: + <% assoc = None %> + %endif + /> ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    %endfor -
    GroupInOut
    ${group.name}
    -
    - This will change the default permitted actions assigned to new datasets for new histories. + This will change the default permitted actions assigned + to new datasets in new histories. You may also specify + per-history defaults via the + history options.
    @@ -39,4 +62,4 @@
    -%endif \ No newline at end of file +%endif diff --git a/tools/data_source/encode_import_code.py b/tools/data_source/encode_import_code.py index ddfbb0241e0..19fb17fef95 100644 --- a/tools/data_source/encode_import_code.py +++ b/tools/data_source/encode_import_code.py @@ -38,8 +38,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + description + ")" history.add_dataset( newdata ) - #TODO, Nate: Make sure the following is functionally correct - app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) + app.security_agent.set_dataset_permissions( newdata.dataset, base_dataset.dataset.groups ) app.model.flush() try: copyfile(filepath,newdata.file_name) diff --git a/tools/data_source/microbial_import_code.py b/tools/data_source/microbial_import_code.py index e8816f093ff..b5ccbf11c3c 100644 --- a/tools/data_source/microbial_import_code.py +++ b/tools/data_source/microbial_import_code.py @@ -129,8 +129,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.extension = file_type newdata.name = basic_name + " (" + microbe_info[kingdom][org]['chrs'][chr]['data'][description]['feature'] +" for "+microbe_info[kingdom][org]['name']+":"+chr + ")" newdata.flush() - #TODO, Nate: Make sure the following is functionally correct - app.security_agent.set_dataset_groups( newdata.dataset, base_dataset.dataset.groups ) + app.security_agent.set_dataset_permissions( newdata.dataset, base_dataset.dataset.groups ) history.add_dataset( newdata ) app.model.flush() try: diff --git a/tools/maf/maf_to_bed_code.py b/tools/maf/maf_to_bed_code.py index 428486b3e37..d28c10b73ca 100644 --- a/tools/maf/maf_to_bed_code.py +++ b/tools/maf/maf_to_bed_code.py @@ -32,8 +32,7 @@ def exec_after_process(app, inp_data, out_data, param_dict, tool, stdout, stderr newdata.name = basic_name + " (" + dbkey + ")" newdata.flush() history.add_dataset( newdata ) - #TODO, Nate: Make sure the following is functionally correct - app.security_agent.set_dataset_groups( newdata.dataset, output_data.dataset.groups ) + app.security_agent.set_dataset_permissions( newdata.dataset, output_data.dataset.groups ) newdata.flush() history.flush() app.model.flush() From 9387c752445dcadbe68c8996fd7a694dab4fd2e3 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 21 Aug 2008 09:17:09 -0400 Subject: [PATCH 024/267] Added unique indexes to Library.name and Group.name, fixed some bugs in the admin controller. --- lib/galaxy/model/mapping.py | 4 +- lib/galaxy/web/controllers/admin.py | 37 +++++++++++++++---- .../admin/dataset_security/group_create.mako | 2 +- .../group_dataset_permitted_actions_edit.mako | 2 +- .../dataset_security/group_members_edit.mako | 2 +- templates/admin/library/libraries.mako | 3 ++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 4922b4fd7d5..d66c3ce391c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -119,7 +119,7 @@ Group.table = Table( "galaxy_group", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), - Column( "name", TEXT ), + Column( "name", TEXT, index=True, unique=True ), Column( "priority", Integer ), Column( "deleted", Boolean, index=True, default=False ) ) @@ -182,7 +182,7 @@ Library.table = Table( "library", metadata, Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), - Column( "name", TEXT ), + Column( "name", TEXT, index=True, unique=True ), Column( "description", TEXT ) ) LibraryFolder.table = Table( "library_folder", metadata, diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f3ff2773608..02bd8183e2f 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -134,6 +134,9 @@ class Admin( BaseController ): if not name: msg = "Please enter a name" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) + elif len( trans.app.model.Group.select_by( name=name ) ) > 0: + msg = "A group with that name already exists" + trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: try: priority = int( params.priority ) @@ -144,6 +147,12 @@ class Admin( BaseController ): group.flush() # Add the members members = params.members + if members and not isinstance( members, list ): + # mako passes singleton lists as strings for some reason + members = [ members ] + # Handle case where admin removed all members from group + elif members is None: + members = [] for user_id in members: user = galaxy.model.User.get( user_id ) # Create the UserGroupAssociation @@ -504,25 +513,33 @@ class Admin( BaseController ): return trans.fill_template( '/admin/library/specified_users_group_libraries.mako', user_email=params.user_email, group_name=params.group_name, - libraries=libraries ) + libraries=libraries, + msg=msg ) # Galaxy Library Stuff @web.expose def libraries( self, trans, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) - return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select() ) + params = util.Params( kwd ) + msg = params.msg + return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select(), msg=msg ) @web.expose def library( self, trans, id=None, name="Unnamed", description=None, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg if 'create_library' in kwd: + if len( trans.app.model.Library.select_by( name=name ) ) > 0: + msg = "A library with that name already exists" + trans.response.send_redirect( web.url_for( action='libraries', msg=msg ) ) library = trans.app.model.Library( name=name, description=description ) root_folder = trans.app.model.LibraryFolder( name=name, description=description ) root_folder.flush() library.root_folder = root_folder library.flush() - trans.response.send_redirect( web.url_for( action='folder', id = root_folder.id ) ) + trans.response.send_redirect( web.url_for( action='folder', id=root_folder.id, msg=msg ) ) elif id is None: return trans.show_form( web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) @@ -531,13 +548,15 @@ class Admin( BaseController ): .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) library = trans.app.model.Library.get( id ) if library: - return trans.fill_template( '/admin/library/library.mako', library = library ) + return trans.fill_template( '/admin/library/library.mako', library=library, msg=msg ) else: return trans.show_error_message( "Invalid library specified" ) @web.expose def folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + msg = params.msg if 'create_folder' in kwd: folder = trans.app.model.LibraryFolder( name = name, description = description ) # We are associating the last used genome_build with folders, so we will always @@ -548,7 +567,7 @@ class Admin( BaseController ): parent_folder = trans.app.model.LibraryFolder.get( parent_id ) parent_folder.add_folder( folder ) folder.flush() - trans.response.send_redirect( web.url_for( action='folder', id = folder.id ) ) + trans.response.send_redirect( web.url_for( action='folder', id=folder.id, msg=msg ) ) elif id is None: return trans.show_form( web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) @@ -579,6 +598,8 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( folder_id ) last_used_build = folder.genome_build data_files = [] + params = util.Params( kwd ) + msg = params.msg # add_file method def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): @@ -707,7 +728,8 @@ class Admin( BaseController ): file_formats=file_formats, dbkeys=dbkeys, last_used_build=last_used_build, - groups=groups ) + groups=groups, + msg=msg ) dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) if dataset: # Copied from edit attributes for 'regular' datasets with some additions @@ -818,7 +840,8 @@ class Admin( BaseController ): datatypes=ldatatypes, dataset_actions=dataset_actions, gdas=gdas, - err=None ) + err=None, + msg=msg ) else: return trans.show_error_message( "Invalid dataset specified" ) def renderable( self, trans, component, group_id ): diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako index 28596e03fb3..d4d1a204d88 100644 --- a/templates/admin/dataset_security/group_create.mako +++ b/templates/admin/dataset_security/group_create.mako @@ -87,7 +87,7 @@ %endif -
    +
    diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako index 58406d7edbf..55c47da7f5a 100644 --- a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -64,7 +64,7 @@ <% ctr += 1 %> %endfor -
    +
    %endif diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako index c7defaa7697..48cc138474a 100644 --- a/templates/admin/dataset_security/group_members_edit.mako +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -105,7 +105,7 @@ %endif -
    +
    diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako index 7e44442e1aa..d4d2d1fccfd 100644 --- a/templates/admin/library/libraries.mako +++ b/templates/admin/library/libraries.mako @@ -8,6 +8,9 @@

    + %if msg: +

    ${msg}

    + %endif
    Galaxy Libraries
    %for library in libraries: From 8f19013837bb525995693837586fc0881b03f3e2 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 21 Aug 2008 13:20:48 -0400 Subject: [PATCH 025/267] Add a 'pencil' icon to datasets in the library import tool, allows users with appropriate permissions to edit metadata on datasets in the library. --- lib/galaxy/web/controllers/root.py | 27 +++++-- templates/dataset/edit_attributes.mako | 101 ++++++++++++++++--------- templates/library/library.mako | 13 +++- 3 files changed, 99 insertions(+), 42 deletions(-) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 83ab538e87f..1324499974b 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -203,29 +203,38 @@ 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 ) ) ) - if data.history.user is not None and data.history.user != trans.user: + trans.log_event( "Problem retrieving dataset id %s with history id %s and library id %s." % ( str( id ), str( hid ), str( lid ) ) ) + return trans.show_error_message( "Problem retrieving dataset." ) + if id is not None and data.history.user is not None and data.history.user != trans.user: return trans.show_error_message( "This instance of a dataset (%s) in a history does not belong to you." % ( data.id ) ) if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): p = util.Params(kwd, safe=False) + can_edit_metadata = lid is None or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) if p.change: # The user clicked the Save button on the 'Change data type' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) trans.app.datatypes_registry.change_datatype( data, p.datatype ) trans.app.model.flush() elif p.save: # The user clicked the Save button on the 'Edit Attributes' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) data.name = p.name data.info = p.info @@ -245,6 +254,8 @@ class RootController( BaseController ): return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) elif p.detect: # The user clicked the Auto-detect button on the 'Edit Attributes' form + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) for name, spec in data.datatype.metadata_spec.items(): # We need to be careful about the attributes we are resetting if name != 'name' and name != 'info' and name != 'dbkey': @@ -255,7 +266,11 @@ class RootController( BaseController ): trans.app.model.flush() return trans.show_ok_message( "Attributes updated", refresh_frames=['history'] ) elif p.convert_data: + if lid is not None: + return trans.show_error_message( "Data in the library cannot be converted. Please import it to a history and covert it." ) """The user clicked the Convert button on the 'Convert to new format' form""" + if not can_edit_metadata: + return trans.show_error_message( "You are not authorized to change this dataset's metadata." ) target_type = kwd.get("target_type", None) if target_type: msg = data.datatype.convert_dataset(trans, data, target_type) diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 712b2eccfc5..1ed13711513 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -15,11 +15,19 @@ +<% +if isinstance( data, trans.app.model.HistoryDatasetAssociation ): + id_name = 'id' +elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): + id_name = 'lid' +%> + +%if ( id_name == 'id' or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) ):
    Edit Attributes
    - +
    - +
    @@ -68,39 +76,41 @@

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

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

    + +

    +
    + +

    + %endif %endif @@ -108,7 +118,7 @@

    Change data type
    - +

    +%else: +

    +
    View Attributes
    +
    +
    + Name: ${data.name} +
    + Info: ${data.info} +
    + Data Format: ${data.ext} +
    + %for element in metadata: + ${element.spec.desc}: ${element.value[0]} +
    + %endfor +
    +
    +
    + +

    +%endif %if trans.user and ( trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset = data ) ): +

    +
    Change Dataset Access Permissions
    +
    + + +
    + <% groups = trans.app.model.Group.select() %> + <% active_group_ids = [ assoc.group.id for assoc in datasets[0].dataset.groups ] %> +
    + Check each group which should have access to this dataset. +
    + %for group in groups: + %if group.id in active_group_ids: + <% assoc = filter( lambda x: x.group_id == group.id, datasets[0].dataset.groups )[0] %> + %else: + <% assoc = None %> + %endif + ${group.name}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor + +
    + +
    +
    diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index a7877e54479..aae1e1c1a2f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -51,6 +51,7 @@
    +

    Edit Attributes
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 1c335678ffb..0d5a87966bd 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -1,5 +1,7 @@ <%inherit file="/base.mako"/> +<% import os %> + <%def name="title()">Create New Library Dataset
    @@ -27,6 +29,23 @@
    + %if trans.app.config.library_import_dir is not None: +
    + +
    + +
    +
    + You may also choose to upload all files in a subdirectory of ${trans.app.config.library_import_dir} on the Galaxy server. +
    +
    +
    + %endif
    Yes
    diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 58ca578eb15..4930c93ec6e 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -80,6 +80,10 @@ use_interactive = true # Admin Users - this should be a comma-separated list of valid Galaxy users #admin_users = user1@bx.psu.edu,user2@bx.psu.edu +# Files in directories under this directory can be directly imported through +# the library admin's "add dataset" tool +## library_import_dir = /var/opt/galaxy/import + # path to sendmail sendmail_path = /usr/sbin/sendmail From a0f38f5595e7d5cfef0eada2de9b284445f0baf5 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 28 Aug 2008 14:01:03 -0400 Subject: [PATCH 039/267] Copy gzip handling from the upload tool to the admin library import tool. Speaking of, we should probably find a way to combine the two, since they basically do the same thing. There is a lot of duplicated code here. --- lib/galaxy/web/controllers/admin.py | 48 +++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index b74734921ca..9ca06a42948 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1,6 +1,5 @@ - -import shutil, StringIO, operator, urllib -from galaxy import util +import shutil, StringIO, operator, urllib, gzip, tempfile +from galaxy import util, datatypes from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent @@ -648,6 +647,33 @@ class Admin( BaseController ): def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): data_type = None temp_name = sniff.stream_to_file( file_obj ) + + # See if we have a gzipped file, which, if it passes our restrictions, we'll uncompress on the fly. + is_gzipped, is_valid = self.check_gzip( temp_name ) + if is_gzipped and not is_valid: + raise BadFileException( "you attempted to upload an inappropriate file." ) + elif is_gzipped and is_valid: + # We need to uncompress the temp_name file + CHUNK_SIZE = 2**20 # 1Mb + fd, uncompressed = tempfile.mkstemp() + gzipped_file = gzip.GzipFile( temp_name ) + while 1: + try: + chunk = gzipped_file.read( CHUNK_SIZE ) + except IOError: + os.close( fd ) + os.remove( uncompressed ) + raise BadFileException( 'problem decompressing gzipped data.' ) + if not chunk: + break + os.write( fd, chunk ) + os.close( fd ) + gzipped_file.close() + # Replace the gzipped file with the decompressed file + shutil.move( uncompressed, temp_name ) + name = name.rstrip( '.gz' ) + data_type = 'gzip' + if space_to_tab: line_count = sniff.convert_newlines_sep2tabs( temp_name ) else: @@ -926,6 +952,22 @@ class Admin( BaseController ): msg=msg ) else: return trans.show_error_message( "Invalid dataset specified" ) + 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 != datatypes.data.gzip_magic: + return ( False, False ) + CHUNK_SIZE = 2**15 # 32Kb + gzipped_file = gzip.GzipFile( temp_name ) + chunk = gzipped_file.read( CHUNK_SIZE ) + gzipped_file.close() + #if self.check_html( temp_name, chunk=chunk ) or self.check_binary( temp_name, chunk=chunk ): + # return( True, False ) + return ( True, True ) @web.expose def change_permissions( self, trans, ids=[], **kwd ): if not self.user_is_admin( trans ): From ff321b14691979e8f7ba99a7429120cf11d5e29c Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 29 Aug 2008 13:50:58 -0400 Subject: [PATCH 040/267] When uploading to the library, make permissions options a bit clearer. --- lib/galaxy/web/controllers/admin.py | 29 ++++++++------- templates/admin/library/dataset.mako | 47 ++++++++++-------------- templates/admin/library/new_dataset.mako | 26 ++++++++++--- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 9ca06a42948..fce461c71ca 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -643,6 +643,12 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg + def listify( item ): + if isinstance( item, list ): + return item + else: + return [ item ] + # add_file method def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): data_type = None @@ -731,22 +737,19 @@ class Admin( BaseController ): if 'space_to_tab' in kwd: if kwd['space_to_tab'] not in ["None", None]: space_to_tab = True - if 'groups' not in kwd: - msg = 'The dataset must be associated with at least 1 group.' + if 'groups' not in kwd and 'users' not in kwd and 'public' not in kwd: + msg = 'The dataset must be associated with at least 1 user or group, or be set public.' trans.response.send_redirect( web.url_for( action='dataset', folder_id=folder_id, msg=msg ) ) - groups = kwd['groups'] - if groups and not isinstance( groups, list ): - # mako sends singleton lists as a string - groups = [ groups ] - elif groups is None: - groups = [] - # Greg: what's this for? it kills the ability to select multiple groups - #else: - # groups = [] + groups = [] + if 'groups' in kwd: + groups.extend( listify(kwd['groups']) ) + if 'users' in kwd: + groups.extend( listify(kwd['users']) ) + if 'public' in kwd: + groups.extend( [ trans.app.security_agent.get_public_group().id ] ) temp_name = "" data_list = [] created_datasets = [] - if 'filename' in dir( data_file ): file_name = data_file.filename file_name = file_name.split( '\\' )[-1] @@ -843,7 +846,7 @@ class Admin( BaseController ): # Copied from edit attributes for 'regular' datasets with some additions p = util.Params(kwd, safe=False) if p.change_permitted_actions: - # The user clicked the Save button on the 'Group Associations' form + # The user clicked the Save button on the 'Dataset Permissions' form actions = p.actions if actions and not isinstance( actions, list ): actions = [ actions ] diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index aae1e1c1a2f..ea17090632f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -15,38 +15,29 @@ -<%def name="group_dataset_permitted_actions( dataset_actions, gda )"> - %for da in dataset_actions: - <% check = False %> - %for action in gda[2]: - %if action == da: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - - %endif - ${da}
    - %endfor - -
    -
    Group Associations
    +
    Dataset Permissions
    - %for gda in gdas: -
    ${gda[1]}
    -
    -
    - ${group_dataset_permitted_actions( dataset_actions, gda )} -
    - %endfor +
    + <% dataset_gdas = [ assoc for assoc in dataset.dataset.groups ] %> +
    + Choose the permissions each user or group should have on this dataset. +
    + %for gda in dataset_gdas: + ${gda.group.name.replace( ' private group', '' )}
    +
    + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
    + %endfor +
    + %endfor +
    diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 0d5a87966bd..1154e3af8bf 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -85,14 +85,30 @@
    - - Multi-select list - hold the appropriate key while clicking to select multiple columns + + This dataset can be accessed by anyone (make it public).
    +

    + <% user_groups = [ g for g in groups if g[1].endswith( ' private group' ) ] %> + <% real_groups = [ g for g in groups if not g[1].endswith( ' private group' ) and g[1] != 'public' ] %>

    - + %for group in user_groups: + %endfor + %if len( real_groups ): +

    + + + %endif +

    +
    + To select multiple users or groups, hold ctrl or command while clicking.
    From 9be266362bf1458f73a1f88d08f7c69b598bf25f Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 3 Sep 2008 12:14:26 -0400 Subject: [PATCH 041/267] You can now update individual dataset permissions from a group's dataset list in the admin interface. --- lib/galaxy/security/__init__.py | 7 +- lib/galaxy/web/controllers/admin.py | 70 +++++++------------ .../group_dataset_permitted_actions_edit.mako | 65 ++++++++--------- 3 files changed, 64 insertions(+), 78 deletions(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index daf85db4318..d248c37d990 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -235,10 +235,13 @@ class GalaxyRBACAgent( RBACAgent ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) - def get_dataset_permissions( self, dataset ): + def get_dataset_permissions( self, dataset, group_id=None ): if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset - return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups ] + if group_id is not None: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups if gda.group_id == int(group_id) ][0] + else: + return [ ( gda.group, gda.permitted_actions ) for gda in dataset.groups ] def get_component_associations( self, **kwd ): # TODO, Nate: Make sure this method is functionally correct. assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index fce461c71ca..63703b57ca7 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -284,44 +284,12 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg group_id = int( params.group_id ) - group_name = unescape( params.group_name, unentities ) gdas = [] permitted_actions = [] - # Need to get all actions to send to the form - dataset_actions = [] - dpas = RBACAgent.permitted_actions - dpa_descriptions = RBACAgent.permitted_action_descriptions - for dpa in dpas.items(): - pa = pa_description = '' - pa = dpa[0] - if dpa[0].startswith( 'DATASET' ): - for dpa_description in dpa_descriptions.items(): - if pa == dpa_description[0]: - pa = dpa[1] - pa_description = dpa_description[1] - break - dataset_actions.append( ( pa, pa_description ) ) - dataset_actions = sorted( dataset_actions, key=operator.itemgetter(0) ) - q = sa.select( ( ( galaxy.model.Group.table.c.priority ).label( 'group_priority' ), - ( galaxy.model.GroupDatasetAssociation.table.c.permitted_actions ).label( 'permitted_actions' ) ), - whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, - from_obj = [ sa.outerjoin( galaxy.model.Group.table, - galaxy.model.GroupDatasetAssociation.table ) ] ) - for row in q.execute(): - permitted_actions = [] - # Although there may be GroupDatasetAssociations, there may not be any permitted_actions on them - if row.permitted_actions: - for action in row.permitted_actions: - permitted_actions.append( action.encode( 'ascii' ) ) - permitted_actions.sort() - gdas.append( ( row.group_priority, - permitted_actions ) ) - break # Just need 1 row + group = galaxy.model.Group.get( group_id ) return trans.fill_template( '/admin/dataset_security/group_dataset_permitted_actions_edit.mako', - group_id=group_id, - group_name=escape( group_name, entities ), - gdas=gdas, - dataset_actions=dataset_actions, + group=group, + gdas=group.datasets, msg=msg ) @web.expose def group_dataset_permitted_actions_edit( self, trans, **kwd ): @@ -329,15 +297,29 @@ class Admin( BaseController ): return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group_id = int( params.group_id ) - actions = params.actions - if actions and not isinstance( actions, list ): - actions = [ actions ] - # Update the permitted_actions for every GroupDatasetAssociation of the Group - q = sa.update( galaxy.model.GroupDatasetAssociation.table, - whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, - values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) - result = q.execute() - msg = "The dataset permitted actions for the group have been updated, affecting %d rows in the group_dataset_association table" % result.rowcount + permissions = {} + for gda_id in params.gdas.split(','): + permissions[gda_id] = params.get('gda_actions_%s' % gda_id, None) + if isinstance( permissions[gda_id], str ): + permissions[gda_id] = [ permissions[gda_id] ] + for gda_id, permitted_actions in permissions.items(): + if permitted_actions is None: + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + if gda.permitted_actions is not []: + log.debug( "gda %s: permitted_actions emptied (set to none)" % gda_id ) + gda.permitted_actions = [] + gda.flush() + else: + valid_pas = trans.app.model.security_agent.convert_permitted_action_strings( permitted_actions ) + if not valid_pas: + continue # this shouldn't happen, but might as well check + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + if sorted(gda.permitted_actions) != sorted(valid_pas): + log.debug( "gda %s: permitted_actions changed to %s" % ( gda_id, valid_pas ) ) + gda = trans.app.model.GroupDatasetAssociation.get( int( gda_id ) ) + gda.permitted_actions = valid_pas + gda.flush() + msg = "The dataset permitted actions have been updated" trans.response.send_redirect( '/admin/groups?msg=%s' % msg ) @web.expose def mark_group_deleted( self, trans, **kwd ): diff --git a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako index 6648a72446f..5afe2ae809f 100644 --- a/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako +++ b/templates/admin/dataset_security/group_dataset_permitted_actions_edit.mako @@ -1,12 +1,5 @@ <%inherit file="/base.mako"/> -<% - from galaxy.web.controllers.admin import entities, unentities - from xml.sax.saxutils import escape, unescape -%> - -<% gn = unescape( group_name, unentities ) %> - <%def name="title()">Permitted Actions on Datasets
    @@ -14,50 +7,58 @@ Groups  |   Users
    -

    Manage Permitted Actions on Datasets for Group '${gn}'

    +

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

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

    ${msg}

     
    There is no Galaxy group named '${gn}'
    The group you selected has no associated datasets.
    GroupPriorityPermitted Actions on DatasetsAssociation Names/InfoPermitted Actions
    ${gn}${gda[0]} - %for da in dataset_actions: - <% check = False %> - %for action in gda[1]: - %if action == da[0]: - <% - check = True - break - %> - %endif - %endfor - %if check: - - %else: - + %if gda.dataset.library_associations: + Library name(s):
    + %endif +
      + %for da in gda.dataset.library_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    + %if gda.dataset.history_associations: + History name(s):
    + %endif +
      + %for da in gda.dataset.history_associations: +
    • ${da.name} (${da.info})
    • + %endfor +
    +
    + %for pa in trans.app.model.Dataset.permitted_actions: + <% pa_val = trans.app.security_agent.permitted_actions.__dict__[pa] %> + ${da[1]}
    + /> + ${pa_val}
    ${trans.app.security_agent.get_permitted_action_description(pa)}

    %endfor
    From 85d1203a5cc99a4a1eac188b9103c7f6bccbeb70 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 3 Sep 2008 12:27:41 -0400 Subject: [PATCH 042/267] Tab-like top level navigation. This makes the "workflow" view separate from the "interactive analysis" view, which makes sense since tools and history are irrelevant when managing workflows. The "workflow" entry in the tool menu now only shows up if a user has workflows added to it, otherwise you get to the workflow management screen from the top. Adds infrastructure for a few things (including pages with 0, 1, or both panels, a compact message box). --- lib/galaxy/web/controllers/root.py | 4 +- lib/galaxy/web/controllers/workflow.py | 8 +- static/june_2007_style/blue/base.css | 1 + static/june_2007_style/blue/masthead.css | 36 ++++++ static/june_2007_style/blue/panel_layout.css | 54 +++++++-- static/june_2007_style/blue_colors.ini | 1 + static/june_2007_style/make_style.py | 5 +- static/june_2007_style/masthead.css.tmpl | 36 ++++++ static/june_2007_style/panel_layout.css.tmpl | 46 ++++++++ templates/base_panels.mako | 56 +++++++++- templates/root/index.mako | 8 ++ templates/root/masthead.mako | 38 +++++-- templates/root/tool_menu.mako | 25 ++--- templates/workflow/editor.mako | 28 +++-- templates/workflow/index.mako | 112 +++---------------- templates/workflow/list.mako | 99 ++++++++++++++++ 16 files changed, 408 insertions(+), 149 deletions(-) create mode 100644 templates/workflow/list.mako diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 1324499974b..aff793e79cf 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -671,7 +671,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 @@ -687,7 +687,7 @@ class RootController( BaseController ): 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, admin_user=admin_user ) + 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 ): diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index b33a051f350..502b9cdc8e7 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -16,8 +16,12 @@ class WorkflowController( BaseController ): beta = True @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) """ @@ -32,7 +36,7 @@ class WorkflowController( BaseController ): .filter( model.StoredWorkflow.c.deleted == False ) \ .order_by( desc( model.StoredWorkflow.c.update_time ) ) \ .all() - return trans.fill_template( "workflow/index.mako", + return trans.fill_template( "workflow/list.mako", workflows = workflows, shared_by_others = shared_by_others ) diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 418362d4610..6e2b8ee1b20 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; diff --git a/static/june_2007_style/blue/masthead.css b/static/june_2007_style/blue/masthead.css index f206e7f1b44..b89faddb0a8 100644 --- a/static/june_2007_style/blue/masthead.css +++ b/static/june_2007_style/blue/masthead.css @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: #eeeeee; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: #222532; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: #222532; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/blue/panel_layout.css b/static/june_2007_style/blue/panel_layout.css index b7ec445bc43..7dda79706a8 100644 --- a/static/june_2007_style/blue/panel_layout.css +++ b/static/june_2007_style/blue/panel_layout.css @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -146,7 +159,6 @@ table.column-layout .unified-panel-header { height: 2em; - overflow: hidden; z-index: 1000; background: #cccccc; background-image: url(panel_header_bg.png); @@ -160,18 +172,17 @@ table.column-layout color: #333; font-weight: bold; } - + .unified-panel-header-inner { padding-top: 0.45em; } - + .menu-bg { background: #C1C9E5 url(menu_bg.png) top repeat-x; } div.unified-panel-body { position: absolute; - z-index: 1; top: 2em; bottom: 0; width: 100%; @@ -197,6 +208,7 @@ div.unified-panel-body { color: black; background: #aaaaaa; } + .panel-header-button:active { color: white; background: #aaaaaa; @@ -240,7 +252,7 @@ div.popupmenu-item:hover { } #overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 20000; } @@ -271,6 +283,34 @@ div.popupmenu-item:hover { padding: 5px; } -.dialog-box body { - overflow: scroll; + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: #303030; + padding: 0px; + padding-left: 26px; + background-color: #FFCCCC; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: #FFFFCC; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: #CCFFCC; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: #CCCCFF; } \ No newline at end of file diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 447ca3f98bc..89c61a73016 100644 --- a/static/june_2007_style/blue_colors.ini +++ b/static/june_2007_style/blue_colors.ini @@ -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/make_style.py b/static/june_2007_style/make_style.py index 4ba96651c52..74a52166332 100755 --- a/static/june_2007_style/make_style.py +++ b/static/june_2007_style/make_style.py @@ -13,7 +13,6 @@ 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"), ( "history.css.tmpl", "history.css" ), ( "tool_menu.css.tmpl", "tool_menu.css" ), @@ -64,7 +63,8 @@ for line in open( vars ): for input, output in templates: print input ,"->", output open( os.path.join( out_dir, output ), "w" ).write( str( Template( file=input, searchList=[context] ) ) ) - + +""" for rule, output in images: t = string.Template( rule ).substitute( context ) print t, "->", output @@ -74,3 +74,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() ) +""" \ No newline at end of file diff --git a/static/june_2007_style/masthead.css.tmpl b/static/june_2007_style/masthead.css.tmpl index 0c759f2aba6..a6d3b7ea5c7 100644 --- a/static/june_2007_style/masthead.css.tmpl +++ b/static/june_2007_style/masthead.css.tmpl @@ -7,6 +7,7 @@ body margin: 3px; margin-right: 5px; margin-left: 5px; + overflow: hidden; } div.pageTitle @@ -24,3 +25,38 @@ a:link, a:visited, a:active { color: $masthead_link; } + +#tab-bar-bottom +{ + z-index: -1; + position:absolute; + top:27px; left: 0; + width: 100%; + height: 100%; + background: $masthead_active_tab_bg; +} + +span.link-group +{ + margin: 0; + padding: 0; + display: inline; + padding-bottom: 10px; + margin-bottom: -10px; +} + +span.link-group span +{ + margin: 0; + padding: 0; + display: inline; +} + +span.link-group span.active-link +{ + background: $masthead_active_tab_bg; + padding-left: 3px; padding-right: 3px; + margin-left: -3px; margin-right: -3px; + padding-bottom: 10px; + margin-bottom: -10px; +} \ No newline at end of file diff --git a/static/june_2007_style/panel_layout.css.tmpl b/static/june_2007_style/panel_layout.css.tmpl index 0479767977b..4df44dba18a 100644 --- a/static/june_2007_style/panel_layout.css.tmpl +++ b/static/june_2007_style/panel_layout.css.tmpl @@ -45,6 +45,19 @@ body font-weight: bold; } +#messagebox +{ + position:absolute; + top:33px; + left:0; + width:100%; + height:24px !important; + overflow: hidden; + border-bottom: solid #999 1px; + font-size: 90%; +} + + #left, #left-border, #center, #right-border, #right { position: absolute; @@ -268,4 +281,37 @@ div.popupmenu-item:hover { .dialog-box .body, .dialog-box .buttons { padding: 5px; +} + +## Messages for message box, slightly different style + +.panel-error-message, .panel-warning-message, .panel-done-message, .panel-info-message +{ + height: 24px; + line-height: 24px; + color: $base_text; + padding: 0px; + padding-left: 26px; + background-color: $error_message_bg; + background-image: url(error_small.png); + background-repeat: no-repeat; + background-position: 6px 50%; +} + +.panel-warning-message +{ + background-image: url(warn_small.png); + background-color: $warn_message_bg; +} + +.panel-done-message +{ + background-image: url(done_small.png); + background-color: $done_message_bg; +} + +.panel-info-message +{ + background-image: url(info_small.png); + background-color: $info_message_bg; } \ No newline at end of file diff --git a/templates/base_panels.mako b/templates/base_panels.mako index 29a2dee470e..0d275ed44c2 100644 --- a/templates/base_panels.mako +++ b/templates/base_panels.mako @@ -1,6 +1,18 @@ ## This needs to be on the first line, otherwise IE6 goes into quirks mode +<% + self.has_left_panel=True + self.has_right_panel=True + self.message_box_visible=False + self.message_box_class="" + self.active_view=None +%> + +<%def name="init()"> +## Override + + ## Default title <%def name="title()">Galaxy @@ -8,6 +20,22 @@ <%def name="stylesheets()"> + ## Default javascripts @@ -27,19 +55,30 @@ ## Masthead <%def name="masthead()"> - + + + +## Messagebox +<%def name="message_box_content()"> ## Document + + ${self.init()} + ${self.title()} ${self.javascripts()} @@ -53,17 +92,26 @@
    ${self.masthead()}
    +
    + %if self.message_box_visible: + ${self.message_box_content()} + %endif +
    + %if self.has_left_panel:
    ${self.left_panel()}
    + %endif
    ${self.center_panel()}
    + %if self.has_right_panel:
    + %endif ## Allow other body level elements ${next.body()} diff --git a/templates/root/index.mako b/templates/root/index.mako index f573571162b..bcc6b3d8ee7 100644 --- a/templates/root/index.mako +++ b/templates/root/index.mako @@ -1,5 +1,13 @@ <%inherit file="/base_panels.mako"/> +<%def name="init()"> +<% + self.has_left_panel=True + self.has_right_panel=True + self.active_view="analysis" +%> + + <%def name="left_panel()">
    Tools
    diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index 097d7707acc..b308158c941 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -9,35 +9,51 @@ - + +
    Galaxy${brand}
    - - Info: report bugs - | wiki - | screencasts - | blog + View: + analysis + | workflow + +     + + Info: report bugs + | wiki + | screencasts %if admin_user == "true": - | admin + | admin %endif +     + %if app.config.use_remote_user: Logged in as ${t.user.email} %else: %if t.user: - Logged in as ${t.user.email}: manage - | logout + Logged in as ${t.user.email}: manage + | logout %else: - Account: create - | login + Account: create + | login %endif %endif   +
    diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index 431b2fffc43..2ae31c6f3fd 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -83,25 +83,22 @@ ## configure which of their stored workflows appear in the tools menu). %if app.config.enable_beta_features: -
    -
    -
    - Workflow (beta) -
    -
    -
    -
    - Manage workflows -
    - %if t.user: + %if t.user and t.user.stored_workflow_menu_entries: +
    +
    +
    + Your workflows +
    +
    +
    %for m in t.user.stored_workflow_menu_entries: %endfor - %endif -
    -
    +
    +
    + %endif %endif
    diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 75b6c0c92e3..b7c523d6205 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()"> @@ -217,7 +229,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; @@ -289,11 +301,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()} + + + +<%def name="left_panel()"> +
    +
    Administration
    +
    +
    +
    +
    +
    + Security +
    + +
    +
    + Data +
    + +
    +
    + Tools +
    + +
    +
    +
    + + +<%def name="center_panel()"> + + + + \ No newline at end of file diff --git a/templates/root/masthead.mako b/templates/root/masthead.mako index b308158c941..a28931773ad 100644 --- a/templates/root/masthead.mako +++ b/templates/root/masthead.mako @@ -27,15 +27,20 @@ class="active-link" %endif >workflow + %if admin_user == "true": + | admin + %endif +     Info: report bugs | wiki | screencasts - %if admin_user == "true": - | admin - %endif diff --git a/templates/workflow/list.mako b/templates/workflow/list.mako index 00ee3c3992c..a3d7affb50e 100644 --- a/templates/workflow/list.mako +++ b/templates/workflow/list.mako @@ -17,15 +17,17 @@

    Your workflows

    - + %if workflows: - +
    From dc049016d7684dc3894a3dd457ecbe70141603f9 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 3 Sep 2008 13:25:27 -0400 Subject: [PATCH 044/267] Add permission style to CSS template and regenerate. --- static/june_2007_style/base.css.tmpl | 6 +++++- static/june_2007_style/blue/base.css | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index 9dbb402d5b1..a26546f242c 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -426,4 +426,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 1b637ba530d..af6ec775ecd 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -425,4 +425,8 @@ div.popupmenu-item:hover { .popup-arrow:hover { color: black; -} \ No newline at end of file +} + +div.permissionContainer { + padding-left: 20px; +} From fabaf9e645f04d481432af7eead85b034f630634 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 4 Sep 2008 10:32:23 -0400 Subject: [PATCH 045/267] Eliminate unique restriction on library name - requires db schema change: drop index ix_library_name; create index ix_library_name on library(name); --- lib/galaxy/model/mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 20c9f567bbf..5beab407c4c 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -182,7 +182,7 @@ Library.table = Table( "library", metadata, Column( "root_folder_id", Integer, ForeignKey( "library_folder.id" ), index=True ), Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), - Column( "name", TEXT, index=True, unique=True ), + Column( "name", TEXT, index=True ), Column( "deleted", Boolean, index=True, default=False ), Column( "description", TEXT ) ) From f05de947a151a27e05f810fa189f4e94c8a45726 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Thu, 4 Sep 2008 13:22:37 -0400 Subject: [PATCH 046/267] Add a flag to datatypes 'copy_safe_peek', which when False will cause the peek to be rengenerated when a dataset is copied. Peeks such as for GMAJ need to be generated uniquely for each dataset occurance (dataset_id is needed for viewing, history_id is needed for exporting from gmaj). --- lib/galaxy/datatypes/data.py | 3 ++ lib/galaxy/datatypes/images.py | 50 ++++++++++++++++----------- lib/galaxy/model/__init__.py | 33 ++++++++++++------ lib/galaxy/web/controllers/admin.py | 2 +- lib/galaxy/web/controllers/library.py | 4 +-- 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 071e7ce53a7..53a8bca7ba0 100644 --- a/lib/galaxy/datatypes/data.py +++ b/lib/galaxy/datatypes/data.py @@ -47,6 +47,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) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 7c58d38f632..1db5291a5a2 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -103,18 +103,22 @@ def create_applet_tag_peek( class_name, archive, params ): class Gmaj( data.Data ): """Class describing a GMAJ Applet""" file_ext = "gmaj.zip" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, - "buttonlabel": "Launch GMAJ", - "nobutton": "false", - "urlpause" :"100", - "debug": "false", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'maf', 'name': 'GMAJ Output on data %s' % dataset.hid, 'info': 'Added by GMAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ) - } - class_name = "edu.psu.bx.gmaj.MajApplet.class" - archive = "/static/gmaj/gmaj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "bundle":"display?id=%s&tofile=yes&toext=.zip" % dataset.id, + "buttonlabel": "Launch GMAJ", + "nobutton": "false", + "urlpause" :"100", + "debug": "false", + "posturl": "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, 'copy_access_from': dataset.id } ) + } + class_name = "edu.psu.bx.gmaj.MajApplet.class" + archive = "/static/gmaj/gmaj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the GMAJ applet." dataset.blurb = 'GMAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: @@ -175,17 +179,21 @@ class Html( data.Text ): class Laj( data.Text ): """Class describing a LAJ Applet""" file_ext = "laj" + copy_safe_peek = False def set_peek( self, dataset ): - params = { - "alignfile1": "display?id=%s" % dataset.id, - "buttonlabel": "Launch LAJ", - "title": "LAJ in Galaxy", - "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), - "noseq": "true" - } - class_name = "edu.psu.cse.bio.laj.LajApplet.class" - archive = "/static/laj/laj.jar" - dataset.peek = create_applet_tag_peek( class_name, archive, params ) + if hasattr( dataset, 'history_id' ): + params = { + "alignfile1": "display?id=%s" % dataset.id, + "buttonlabel": "Launch LAJ", + "title": "LAJ in Galaxy", + "posturl": "history_add_to?%s" % urlencode( { 'history_id': dataset.history_id, 'ext': 'lav', 'name': 'LAJ Output', 'info': 'Added by LAJ', 'dbkey': dataset.dbkey, 'copy_access_from': dataset.id } ), + "noseq": "true" + } + class_name = "edu.psu.cse.bio.laj.LajApplet.class" + archive = "/static/laj/laj.jar" + dataset.peek = create_applet_tag_peek( class_name, archive, params ) + else: + dataset.peek = "After you add this item to your history, you will be able to launch the LAJ applet." dataset.blurb = 'LAJ Multiple Alignment Viewer' def display_peek(self, dataset): try: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index d9ed17b1aa5..0116b26ec2c 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -413,7 +413,7 @@ class HistoryDatasetAssociation( DatasetInstance ): 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 ): + def copy( self, copy_children = False, parent_id = None, target_history = None ): des = HistoryDatasetAssociation( hid=self.hid, name=self.name, info=self.info, @@ -426,12 +426,14 @@ class HistoryDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_history_dataset_association=self ) + copied_from_history_dataset_association=self, + history = target_history ) des.flush() if copy_children: for child in self.children: child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + 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 ): @@ -493,8 +495,8 @@ class History( object ): des.flush() des.name = self.name for data in self.datasets: - new_data = data.copy( copy_children = True ) - des.add_dataset( new_data ) + 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() @@ -540,7 +542,11 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 ): + 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, @@ -552,14 +558,17 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_library_folder_dataset_association = self ) + copied_from_library_folder_dataset_association = self, + history = target_history, + hid = hid ) des.flush() for child in self.children: child_copy = child.to_history_dataset_association( parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + 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 ): + def copy( self, copy_children = False, parent_id = None, target_folder = None ): des = LibraryFolderDatasetAssociation( name=self.name, info=self.info, blurb=self.blurb, @@ -571,12 +580,14 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): visible=self.visible, deleted=self.deleted, parent_id=parent_id, - copied_from_library_folder_dataset_association = self ) + copied_from_library_folder_dataset_association = self, + folder = target_folder ) des.flush() if copy_children: for child in self.children: child_copy = child.copy( copy_children = copy_children, parent_id = des.id ) - des.set_peek() #in some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs + 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 ): diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 63703b57ca7..f6608cfc879 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -708,7 +708,7 @@ class Admin( BaseController ): last_dataset_created = None data_file = kwd['file_data'] url_paste = kwd['url_paste'] - server_dir = kwd['server_dir'] + server_dir = kwd.get( 'server_dir', 'None' ) if data_file == '' and url_paste == '' and server_dir in [ 'None', '' ]: if trans.app.config.library_import_dir is not None: msg = 'Select a file, enter a URL or Text, or select a server directory.' diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 841830035ec..19820ebba10 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -32,8 +32,8 @@ class Library( BaseController ): import_ids = [import_ids] history = trans.get_history() for id in import_ids: - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() - history.add_dataset( dataset ) + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association( target_history = history ) + history.add_dataset( dataset, set_hid = not dataset.hid ) dataset.flush() history.flush() return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) From bd1a4e01ccc494e3357afb14c5195fa0afcf6f54 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 8 Sep 2008 13:41:55 -0400 Subject: [PATCH 047/267] More cleanup for admin UI. --- static/june_2007_style/base.css.tmpl | 5 ++ static/june_2007_style/blue/base.css | 7 ++- static/june_2007_style/blue_colors.ini | 2 +- .../admin/dataset_security/group_members.mako | 57 ++++++++++--------- .../dataset_security/group_members_edit.mako | 22 ++++--- templates/admin/dataset_security/groups.mako | 15 ++--- .../specified_users_groups.mako | 36 ++++++------ templates/admin/dataset_security/users.mako | 40 +++++-------- templates/admin/library/libraries.mako | 50 +++++++++------- templates/admin/library/library.mako | 17 +++--- templates/admin/reload_tool.mako | 33 +++++------ 11 files changed, 146 insertions(+), 138 deletions(-) diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index a26546f242c..28a0f87f546 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -283,6 +283,11 @@ table.colored tr background: $table_row_bg; } +table.colored tr.odd_row +{ + background: $odd_row_bg; +} + div.debug { margin: 10px; diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index af6ec775ecd..74a7b7c68ec 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -283,6 +283,11 @@ table.colored tr background: white; } +table.colored tr.odd_row +{ + background: #DADFEF; +} + div.debug { margin: 10px; @@ -294,7 +299,7 @@ div.debug div.odd_row { - background: #FFFF99; + background: #DADFEF; } #footer { diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 89c61a73016..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 diff --git a/templates/admin/dataset_security/group_members.mako b/templates/admin/dataset_security/group_members.mako index 79c44284459..9acd9faafe2 100644 --- a/templates/admin/dataset_security/group_members.mako +++ b/templates/admin/dataset_security/group_members.mako @@ -7,32 +7,33 @@ <% gn = unescape( group_name, unentities ) %> -<%def name="title()">Create Group -
    -
    - Libraries  |   - %if deleted: - Deleted Groups  |   - %else: - Groups  |   - %endif -
    - - %if not deleted: - - %endif -
    Members of Group '${gn}'
    -
    Name # of Steps
    Users
    - %if msg: - - %endif - - %if len( members ) == 0: - - %else: +<%def name="title()">Group Members + +%if msg: +
    ${msg}
    +%endif + +

    Members of Group '${gn}'

    + +%if not deleted: + + +%endif + +%if len( members ) == 0: + + Group '${gn}' contains no members + +%else: + +

    ${msg}

     
    Group '${gn}' contains no members
    + <% ctr = 0 %> %for member in members: <% email = unescape( member[1], unentities ) %> @@ -48,6 +49,6 @@ <% ctr += 1 %> %endfor - %endif
    Username
    - + +%endif diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako index 48cc138474a..03ce26ded29 100644 --- a/templates/admin/dataset_security/group_members_edit.mako +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -6,17 +6,16 @@ %> <%def name="title()">Manage Group Membership -
    -
    - Libraries  |   - Groups  |   -
    Users
    - %if msg: - - %endif - <% gn = unescape( group_name, unentities ) %> + +%if msg: +
    ${msg}
    +%endif + +<% gn = unescape( group_name, unentities ) %> + +

    Members of group '${gn}'

    + +

    ${msg}

    %else: - - + %endif diff --git a/templates/admin/dataset_security/specified_users_groups.mako b/templates/admin/dataset_security/specified_users_groups.mako index e5678635c96..546842b59f4 100644 --- a/templates/admin/dataset_security/specified_users_groups.mako +++ b/templates/admin/dataset_security/specified_users_groups.mako @@ -7,21 +7,22 @@ <% email = unescape( user_email, unentities ) %> -<%def name="title()">Create Group -
    -
    - Libraries  |   - Groups  |   -
    - -

    Groups of which '${email}' is a member

    -
     
    @@ -25,7 +24,6 @@ %if len( users ) == 0:
    There are no Galaxy users
    Members of '${gn}' - Quick Find
    |A|B|C|D|E|F diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako index 17ebbd1d524..63a3920684a 100644 --- a/templates/admin/dataset_security/groups.mako +++ b/templates/admin/dataset_security/groups.mako @@ -64,15 +64,12 @@ anchored = False curr_anchor = 'A' %> -
    -
    - |A|B|C|D|E|F - |G|H|I|J|K|L - |M|N|O|P|Q|R - |S|T|U|V|W|X - |Y|Z -
    +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor
    Users
    - %if msg: - - %endif - %if len( groups ) == 0: - - %else: +<%def name="title()">Group Membership + +%if msg: +
    ${msg}
    +%endif + +

    Groups of which '${email}' is a member

    + +%if len( groups ) == 0: + + User '${email}' belongs to no groups + +%else: + +

    ${msg}

    User '${email}' belongs to no groups
    + @@ -65,6 +66,7 @@ <% ctr += 1 %> %endfor - %endif +
    Group Priority
    -
    + +%endif diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako index 05c4d8e8455..12e1b4ca550 100644 --- a/templates/admin/dataset_security/users.mako +++ b/templates/admin/dataset_security/users.mako @@ -19,7 +19,6 @@ <% render_quick_find = len( users ) > 50 - ctr = 0 %> %if render_quick_find: <% @@ -28,34 +27,31 @@ anchored = False curr_anchor = 'A' %> - - + - %else: - %endif - %for user in users: + + %for ctr, user in enumerate( users ): <% email = unescape( user[1], unentities ) %> %if render_quick_find and not email.upper().startswith( curr_anchor ): <% anchored = False %> %endif - - %if ctr % 2 == 1: -
    - %endif +
    - %if ctr % 2 == 1: - - %endif - <% ctr += 1 %> %endfor
    - |A|B|C|D|E|F - |G|H|I|J|K|L - |M|N|O|P|Q|R - |S|T|U|V|W|X - |Y|Z +
    + Jump to letter: + %for a in anchors: + | ${a} + %endfor
    Email address
    Email address
    %if render_quick_find and email.upper().startswith( curr_anchor ): %if not anchored: -
    - -
    + <% anchored = True %> %endif ${email} @@ -64,9 +60,7 @@ %if email.upper().startswith( anchor ): %if not anchored: -
    - -
    + <% curr_anchor = anchor anchored = True @@ -83,10 +77,6 @@ ${email} %endif
    diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako index d4d2d1fccfd..3511b32a923 100644 --- a/templates/admin/library/libraries.mako +++ b/templates/admin/library/libraries.mako @@ -1,22 +1,34 @@ <%inherit file="/base.mako"/> <%def name="title()">Libraries -
    -
    - Groups  |   - Users -
    - -
    - %if msg: -

    ${msg}

    - %endif -
    Galaxy Libraries
    -
    - %for library in libraries: - - %endfor -
    -
    + +%if msg: +
    ${msg}
    +%endif + +

    Libraries

    + + + +%if len(libraries) == 0: + + There are no libraries + +%else: + + + + + + + %for library in libraries: + + + + + %endfor +
    NameDescription
    ${library.name}${library.description}
    + +%endif \ No newline at end of file diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako index 67db5c1d1f6..781a8b584ae 100644 --- a/templates/admin/library/library.mako +++ b/templates/admin/library/library.mako @@ -1,16 +1,13 @@ <%inherit file="/base.mako"/> <%def name="title()">Library + +%if msg: +
    ${msg}
    +%endif +
    -
    - Libraries  |   - Groups  |   - Users -
    - %if msg: -

    ${msg}

    - %endif -
    Manage Library '${library.name}'
    +
    Library '${library.name}'
    @@ -29,7 +26,7 @@
    - +
     
     
    diff --git a/templates/admin/reload_tool.mako b/templates/admin/reload_tool.mako index 1d050ea4764..3b8e568cfd4 100644 --- a/templates/admin/reload_tool.mako +++ b/templates/admin/reload_tool.mako @@ -1,16 +1,17 @@ <%inherit file="/base.mako"/> -
    -

    Reload a Tool

    - - %if msg: - - %endif - - - -

    ${msg}

    -
    -

    - Reload tool: +%if msg: +

    ${msg}
    +%endif + +
    +
    Reload Tool
    +
    + +
    + +
    +
    -

    - -
    +
    + +
    From 8266801ea01bb3a582a46b14f06a91e5607e60a1 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 8 Sep 2008 14:27:56 -0400 Subject: [PATCH 048/267] Fix for admin interface functional test. --- test/functional/test_security_and_libraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 30b460a049e..748684e696f 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -41,7 +41,7 @@ class TestHistory( TwillTestCase ): """Testing logging in as an admin user""" self.login( email='test@bx.psu.edu' ) #This is configured as our admin user self.visit_page( "admin" ) - self.check_page_for_string( 'Galaxy Administration' ) + self.check_page_for_string( 'Administration' ) self.logout() # Need to ensure that we have 2 users self.login( email='test2@bx.psu.edu' ) # This will not be an admin user From 4edc6d66263aeff227985a9f98165c59843a543a Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 10 Sep 2008 10:10:13 -0400 Subject: [PATCH 049/267] Spiffy new library browser interface. Also, dim datasets in your history on which you don't have permissions. --- LICENSE.txt | 7 +- lib/galaxy/security/__init__.py | 36 ++++++-- lib/galaxy/web/controllers/library.py | 65 ++++---------- static/images/expander_closed.png | Bin 0 -> 395 bytes static/images/expander_open.png | Bin 0 -> 446 bytes static/images/folder_closed.png | Bin 0 -> 537 bytes static/images/folder_open.png | Bin 0 -> 688 bytes static/images/library_closed.png | Bin 0 -> 593 bytes static/images/library_open.png | Bin 0 -> 622 bytes static/june_2007_style/blue/history.css | 9 +- static/june_2007_style/blue/library.css | 65 ++++++++++++++ static/june_2007_style/history.css.tmpl | 9 +- static/june_2007_style/library.css.tmpl | 65 ++++++++++++++ static/june_2007_style/make_style.py | 7 +- templates/library/browser.mako | 104 ++++++++++++++++++++++ templates/library/common.mako | 109 ++++++++++++++++++++++++ templates/library/libraries.mako | 13 --- templates/library/library.mako | 79 ----------------- templates/root/history_common.mako | 6 +- tool_conf.xml.sample | 2 +- tools/data_source/access_libraries.xml | 4 +- 21 files changed, 425 insertions(+), 155 deletions(-) create mode 100644 static/images/expander_closed.png create mode 100644 static/images/expander_open.png create mode 100644 static/images/folder_closed.png create mode 100644 static/images/folder_open.png create mode 100644 static/images/library_closed.png create mode 100644 static/images/library_open.png create mode 100644 static/june_2007_style/blue/library.css create mode 100644 static/june_2007_style/library.css.tmpl create mode 100644 templates/library/browser.mako create mode 100644 templates/library/common.mako delete mode 100644 templates/library/libraries.mako delete mode 100644 templates/library/library.mako 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/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index d248c37d990..6782bbe3089 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -89,11 +89,14 @@ class GalaxyRBACAgent( RBACAgent ): """Returns true when user has permission to perform an action""" if not isinstance( dataset, self.model.Dataset ): dataset = dataset.dataset - # If dataset is in public group, we always return true for viewing and using - # This may need to change when the ability to alter groups and permitted_actions is allowed - if action == self.permitted_actions.DATASET_ACCESS and \ - self.components_are_associated( group = self.get_public_group(), dataset = dataset ): - return True + if user is None: + try: + public_assoc = [ gda for gda in dataset.groups if gda.group == self.get_public_group() ][0] + except: + # There'll be an IndexError if the dataset doesn't have a gda with public + return False + if action in public_assoc.permitted_actions: + return True elif user is not None: # Loop through permitted_actions and if allowed return true: # Check permitted_actions associated with dataset through groups @@ -231,7 +234,7 @@ class GalaxyRBACAgent( RBACAgent ): for group_dataset_assoc in dataset.groups: group_dataset_assoc.delete() group_dataset_assoc.flush() - if isinstance( permissions[0], self.model.GroupDatasetAssociation ): + if len( permissions ) and isinstance( permissions[0], self.model.GroupDatasetAssociation ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) @@ -254,6 +257,27 @@ class GalaxyRBACAgent( RBACAgent ): raise 'No valid method of associating provided components: %s' % kwd def dataset_has_group( self, dataset_id, group_id ): return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) + def check_folder_contents( self, user, entry ): + """ + Return true if there are any datasets under 'folder' that the + user has access permission on. We do this a lot and it's a + pretty inefficient method, optimizations are welcomed. + """ + if isinstance( entry, self.model.Library ): + return self.check_folder_contents( user, entry.root_folder ) + elif isinstance( entry, self.model.LibraryFolderDatasetAssociation ): + return self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=entry ) + elif isinstance( entry, self.model.LibraryFolder ): + for dataset in entry.active_datasets: + if self.allow_action( user, self.permitted_actions.DATASET_ACCESS, dataset=dataset ): + return True + for folder in entry.active_folders: + if self.check_folder_contents( user, folder ): + return True + return False + else: + raise 'Passed an illegal object to check_folder_contents: %s' % type( entry ) + def get_permitted_actions( self, filter=None ): '''Utility method to return a subset of RBACAgent's permitted actions''' diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 19820ebba10..1d42735603d 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -6,52 +6,19 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose - def index( self, trans, library_id=None, import_ids=[], **kwd ): - def renderable( component, group_ids ): - #return True if component or at least one of components contents is - #associated with a group that is in group_ids - if isinstance( component, trans.app.model.LibraryFolder ): - # Check the folder's datasets to see what can be rendered - for library_folder_dataset_assoc in component.active_datasets: - if renderable( library_folder_dataset_assoc, group_ids ): - return True - # Check the folder's sub-folders to see what can be rendered - for library_folder in component.active_folders: - if renderable( library_folder, group_ids ): - return True - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - dataset = trans.app.model.Dataset.get( component.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - return True - return False - - if import_ids: - # Used for importing a dataset into a user's history - if not isinstance( import_ids, list ): - import_ids = [import_ids] - history = trans.get_history() - for id in import_ids: - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association( target_history = history ) - history.add_dataset( dataset, set_hid = not dataset.hid ) - dataset.flush() - history.flush() - return trans.show_ok_message( "%i datasets have been imported into your history" % len( import_ids ), refresh_frames=['history'] ) - else: - # Need user to get associated Groups and Datasets - user = trans.get_user() - if user: - group_ids = [ user_group_assoc.group_id for user_group_assoc in user.groups ] - else: - group_ids = [ trans.app.model.Group.select_by( name='public' )[0].id ] - - if library_id: - # Since permitted_actions are kept with the GroupDatasetAssociation, each accessible Library will only - # display the subset of [ it's complete set of ] datasets that the user has permission to access. We - # pass group_ids so this can be handled in the template. - library = trans.app.model.Library.get( library_id ) - return trans.fill_template( '/library/library.mako', library=library, group_ids=group_ids ) - - #render available libraries - libraries = [ library for library in trans.app.model.Library.select_by( deleted = False ) if renderable( library.root_folder, group_ids ) ] - return trans.fill_template( '/library/libraries.mako', group_ids=group_ids, libraries=libraries ) + def browse( self, trans, **kwd ): + return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ) ) + index = browse + @web.expose + def import_datasets( self, trans, import_ids=[], **kwd ): + if not import_ids: + return trans.show_error_message( "You must select at least one dataset to import" ) + if not isinstance( import_ids, list ): + import_ids = [ import_ids ] + history = trans.get_history() + for id in import_ids: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() + history.add_dataset( dataset ) + dataset.flush() + history.flush() + return trans.show_ok_message( "%i dataset(s) have been imported in to your history" % len( import_ids ), refresh_frames=['history'] ) diff --git a/static/images/expander_closed.png b/static/images/expander_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..e252606d3e68c6da135a9b165996d9da968ef7fc GIT binary patch literal 395 zcmV;60d)R}P)IO8de(|Ml<%@O-40!dwX61{2C5s-llVw2V@@N0oo_PPieZ!0Y2~+R( zk!(QTf=B;X9DnzJ@u9c>OP4(U@7{849!UlyO@H`*;lVfmCvAW6f9CF&{}ZR*{jXDW zb_vl21ozzrYJBy-Vb$aRjjJF3@7nm}zjw#A|58cE9uZ}LbIY~=6ShA8U$XeY|MDdd zfQCH!?_7WRzhvaG%|sbsT7Kz&`}!yUix%Do#>T_{_Ei`DO9UTSBkH=Hg(w4*^UnUS zTk-IJ<+2C=ZObqG7Z2FGlB7VCN;>(!bn*TFHYMl(i+Sx`L~=ArL>~EXU3lidsO!!J pWF;gqzXSh89JkLNxXeT<1_12n>%V}Y6R`jQ002ovPDHLkV1iLCz99er literal 0 HcmV?d00001 diff --git a/static/images/expander_open.png b/static/images/expander_open.png new file mode 100644 index 0000000000000000000000000000000000000000..22848b0061c86538731e46a46fbd2553c3958d45 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvPEJt`@wP+3?}4V+dAc};Se$-3 zX}|a30Fl=7+G?sCTwr5XIC$5E)iv6chvdEs;KS86xga?O!)UU}+46=TbU4lS1SUCfIU-bU^0 z+!?^Yuw=WOS=H{VtrP1+k8bMUCGHT&;aAFjZ0cOY{>huy)(Smy%zB%rF4w)x{c&JG z{DUWpT7E}Y@2{9K=ie>W`<*7+R=nr^nrX}{_2AhpyNYva%MMjNQ5R-^c3xi9wQ+x# m+S-_||56|PL-M3Q@P*%9xbv!ydkQdc89ZJ6T-G@yGywqawXp^O literal 0 HcmV?d00001 diff --git a/static/images/folder_closed.png b/static/images/folder_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..784e8fa48234f4f64b6922a6758f254ee0ca08ec GIT binary patch literal 537 zcmV+!0_OdRP)x(K@^6+>g^d@v4;gkbWsEoXE%32*i1tcpTNXd5CcIl)ECgqz|2rE6EW}s7R?kl za1q`0GCkMruC6-2LANtwVlsgzsp4?{@7$`KBv!G66>Vie3h?3OmEEkjwdLG0PgLVi z`!N((f$A@n17Ldj#`};0I3@iHJ5M{#IZz|UIYRm4(!uV7eYIYIwQf&}_2J~}>pQ^n z6o8--^T(=hkBNQ_k{-_GWE;FMW7!p}f{NG3nHZ{D5<3d8&tLh%a4AqqnjMkr3m&fkMdECD3N5}Unig5wy40;>lo4j~k+e}v)` zR6)J8Mk*u=SpB`p6o)7j?S0T@9?bz#m@l>gc*zk__|*!FMcHwP!gwLJvS~9c0px8E zWCVGc zN?Hxg{(SJp>2>GN9JetoZ(aZH;Ije%0FdZ{S!LRVX%}YG;=d8L^N!mJkCd*SBx($jgG)S}+Le)f;q=GIn30YFNh3ZW|nh}2rL)`=3ju9K*d z<&~Gte>sT=5|RjR+}A(|O&3gS5t&*G5h0Um$c5IgEgxJl_8nzY#B+YU^|Fhvzo-^U z6fNn3(lBMcl9{Silx)4RpURdFw}1BZ?>}8S&h8fk5`hjKW@sP$LJL*otPOMf+jp$? zcC^*PiL>vkTOZln=wuc^%A^$j`TU&aa3ETVYE{(r=bgN835`stIePGw{uwD`7K9Y) z=4)VDHnkl3YL%JeLce6>Uubzae&kQ4(l6k>CGhc0FIuigU9U^L+D`6bSr zIEp(+L4eIgaZT(|{B!*DbrTYc1t0J9*MLJm+n zOEVloE20S^g6s|1rvjjuW1W$TV;&TbK2|slm=91)Q{X{Kg;c!Saglq7xo8`(>{A}G zw@`8gScq(adr(4@;>^%e<~7+uNMv&?8L0#%0yj~@DW&fPImKeyFHkRKuNERO3(SOB z?S)niZbFEN`0<|w+LjBKR#DU7F3d&rQGGY&Y)Fv0w002i=Jm_>x9jBX>wg3XCKK`E f>xbvJ_5i;DeEQXvyE?}U00000NkvXXu0mjfKqCEw literal 0 HcmV?d00001 diff --git a/static/images/library_open.png b/static/images/library_open.png new file mode 100644 index 0000000000000000000000000000000000000000..7d863f949741ff83fd8373a77c0d95a3d95e441f GIT binary patch literal 622 zcmV-!0+IcRP)YeaZ-G+53gSTz{SPWVdFiPaPX+$~@n)fi9>qgJ zh4fN-QcEhq^wI<|vImjml9)pF*W2zl+qWbTPaSyKyqWKt`DS)j3xa?yVgf%ewhl|m zA$_0x^Rx4E@d&=>uRoSN*Cm&aL#`7&zr0;L(pKMn1G#$ZszFkMC^?B8f`0w&-UD$u zd-(?iK6#!;xZ`>J^A9DGiTfguewDOKwDEKoL}`ZE_M}0=xvsgf$usmJeV$oo28d0#yu!pdgpag$giC zhlKP!CMX3wsPMGgZ5n>Xg+hV--ENofAcH61#0B7Hvl-7Il}g2A6;lfG`FxbX8A>p$ z0P;GW4il9Mr9jO9)xB^Rgy%*CSTdQ6D;kDDnM{U{5Q21FA4#ZM71$_Dba8z<-Y_Na zS@|v#c0PZNDux9AoXD-ZoWP~q)7QrC`G +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + return "" + pad = parent_pad + 20 + %> + %if parent_pad == 0: +
  • ${parent.name}
  • +
      + %else: +
    • ${parent.name}
    • +
        + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + %if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
      • ${render_dataset( dataset )}
      • + %endif + %endfor +
      + + +

      Libraries

      +
      +
        +%for library in libraries: + %if trans.app.security_agent.check_folder_contents( trans.user, library ): +
      • + + + + +
        ${library.name}FormatDbInfo
      • +
          + ${render_folder( library.root_folder, 0 )} +
        +
        + %endif +%endfor +
      + +
      diff --git a/templates/library/common.mako b/templates/library/common.mako new file mode 100644 index 00000000000..7abf13adca5 --- /dev/null +++ b/templates/library/common.mako @@ -0,0 +1,109 @@ +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
      + %else: +
      + %endif + + ## Header row for history items (name, state, action buttons) + +
      +
      + + %if data_state == 'running': +
      + %elif data_state != 'ok': +
      + %endif +
      + <%doc> +
      + display data + edit attributes + delete +
      + + + + + + +
      ${data.display_name()}${data.ext}${data.dbkey}${data.info}
      +
      + + ## Body for history items, extra info and actions, data "peek" + +
      + %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
      + %elif data_state == "error": +
      + An error occurred running this job: ${data.display_info().strip()}, + report this error +
      + %elif data_state == "empty": +
      No data: ${data.display_info()}
      + %elif data_state == "ok": +
      + ${data.blurb} +
      +
      + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
      + %if data.peek != "no peek": +
      ${data.display_peek()}
      + %endif + %else: +
      Error: unknown dataset state "${data_state}".
      + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
      + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
      + %endif + %endif +
      +
      + diff --git a/templates/library/libraries.mako b/templates/library/libraries.mako deleted file mode 100644 index 1176bd2162a..00000000000 --- a/templates/library/libraries.mako +++ /dev/null @@ -1,13 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Libraries You Can Access -
      -
      Libraries You Can Access
      -
      - %for library in libraries: - - %endfor -
      -
      diff --git a/templates/library/library.mako b/templates/library/library.mako deleted file mode 100644 index 5f5440c56a0..00000000000 --- a/templates/library/library.mako +++ /dev/null @@ -1,79 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="render_component( component )"> - <% - if isinstance( component, trans.app.model.LibraryFolder ): - render = False - # Check the folder's datasets to see what can be rendered - for library_folder_dataset_assoc in component.datasets: - if render: - break - dataset = trans.app.model.Dataset.get( library_folder_dataset_assoc.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - render = True - break - # Check the folder's sub-folders to see what can be rendered - for library_folder in component.folders: - render_component( library_folder ) - if render: - return render_folder( component ) - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - render = False - dataset = trans.app.model.Dataset.get( component.dataset_id ) - for group_dataset_assoc in dataset.groups: - if group_dataset_assoc.group_id in group_ids: - render = True - break - if render: - return render_dataset( component ) - %> - - -## Render the dataset `data` as history item, using `hid` as the displayed id -<%def name="render_dataset( data )"> -
      - ${data.name} - view or edit attributes -
      - - -## Render a folder -<%def name="render_folder( this_folder )"> -
      - Folder: ${this_folder.name} - <% - components = this_folder.active_components - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> -
      - %for component in components: - ${render_component( component )} - %endfor -
      -
      - - -<%def name="title()">View Library: ${library.name} -
      -
      Import from Library: ${library.name}
      -
      -
      - ${render_folder( library.root_folder )} -
      - -
      -
      -
      diff --git a/templates/root/history_common.mako b/templates/root/history_common.mako index 2336f5d43c7..e91445921d8 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 ## Header row for history items (name, state, action buttons) diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index 69ccfe583ee..03ac818620b 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -2,6 +2,7 @@
      + @@ -12,7 +13,6 @@ -
      diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml index e6dabfbf1b1..1281386f7ce 100644 --- a/tools/data_source/access_libraries.xml +++ b/tools/data_source/access_libraries.xml @@ -1,7 +1,7 @@ stored locally - + - \ No newline at end of file + From 9030c528b0b90c7dafebcc8d79517082b16e3c81 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 10 Sep 2008 11:21:57 -0400 Subject: [PATCH 050/267] imported patch workflow-index-bug --- lib/galaxy/web/controllers/workflow.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 502b9cdc8e7..ac1a7f10ab6 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -67,7 +67,7 @@ class WorkflowController( BaseController ): session.save( 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, @@ -82,7 +82,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 ) @@ -111,7 +111,7 @@ class WorkflowController( BaseController ): 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" ) @@ -136,7 +136,7 @@ class WorkflowController( BaseController ): 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" ) @@ -153,7 +153,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" ) @@ -437,8 +437,7 @@ class WorkflowController( BaseController ): trans.sa_session.save( 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) ) ) ) From b1caa7431aed31b601497e2bb0eacd98fe0e4821 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 10 Sep 2008 23:22:06 -0400 Subject: [PATCH 051/267] Admin side of the new file browser. It's late, I've been working all night, and I'm not sure what my name is... so there are probably bugs. Please play with this stuff and try to break it. This commit also includes permissions editing that actually (finally!) lets you create new group->dataset associations after the dataset already exists. Currently the only entry point is from the Library, but the "display a group's datasets" page can now be extended to contain useful information about associated datasets (like whose history a dataset is part of and what folder a dataset is in) to make it actually, like, useful and stuff. --- lib/galaxy/security/__init__.py | 2 +- lib/galaxy/web/controllers/admin.py | 377 ++++++++++-------- .../{library_closed.png => silk/book.png} | Bin .../{library_open.png => silk/book_open.png} | Bin .../{folder_closed.png => silk/folder.png} | Bin .../{folder_open.png => silk/folder_page.png} | Bin .../resultset_bottom.png} | Bin .../resultset_next.png} | Bin templates/admin/library/browser.mako | 162 ++++++++ .../admin/library/change_permissions.mako | 57 --- templates/admin/library/common.mako | 253 ++++++++++++ templates/admin/library/dataset.mako | 35 +- templates/admin/library/folder.mako | 170 -------- templates/admin/library/libraries.mako | 34 -- templates/admin/library/library.mako | 38 -- templates/admin/library/new_dataset.mako | 11 +- templates/library/browser.mako | 18 +- 17 files changed, 651 insertions(+), 506 deletions(-) rename static/images/{library_closed.png => silk/book.png} (100%) rename static/images/{library_open.png => silk/book_open.png} (100%) rename static/images/{folder_closed.png => silk/folder.png} (100%) rename static/images/{folder_open.png => silk/folder_page.png} (100%) rename static/images/{expander_open.png => silk/resultset_bottom.png} (100%) rename static/images/{expander_closed.png => silk/resultset_next.png} (100%) create mode 100644 templates/admin/library/browser.mako delete mode 100644 templates/admin/library/change_permissions.mako create mode 100644 templates/admin/library/common.mako delete mode 100644 templates/admin/library/folder.mako delete mode 100644 templates/admin/library/libraries.mako delete mode 100644 templates/admin/library/library.mako diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 6782bbe3089..4bd8cf2fe44 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -209,7 +209,7 @@ class GalaxyRBACAgent( RBACAgent ): for data in history.datasets: for hda in data.dataset.history_associations: if history.user and hda.history not in history.user.histories: - # When would this occur? + # This will occur when a user logs in and has datasets in their previously-public history. self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) break else: diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f6608cfc879..08673f53ed6 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -542,74 +542,119 @@ class Admin( BaseController ): # Galaxy Library Stuff @web.expose - def libraries( self, trans, **kwd ): + def library_browser( self, trans, **kwd ): + ## TODO: "show deleted libraries" toggle? if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) - params = util.Params( kwd ) - msg = params.msg - return trans.fill_template( '/admin/library/libraries.mako', libraries=trans.app.model.Library.select_by( deleted = False ), msg=msg ) - @web.expose - def library( self, trans, id=None, name="Unnamed", description=None, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) - params = util.Params( kwd ) - msg = params.msg - if 'create_library' in kwd: - if len( trans.app.model.Library.select_by( name=name ) ) > 0: - msg = "A library with that name already exists" - trans.response.send_redirect( web.url_for( action='libraries', msg=msg ) ) - library = trans.app.model.Library( name=name, description=description ) - root_folder = trans.app.model.LibraryFolder( name=name, description=description ) - root_folder.flush() - library.root_folder = root_folder - library.flush() - trans.response.send_redirect( web.url_for( action='folder', id=root_folder.id, msg=msg ) ) - elif id is None: - return trans.show_form( - web.FormBuilder( action = web.url_for(), title = "Create a new Library", name = "create_library", submit_text = "Submit" ) - .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) - .add_text( name = "description", label = "Description", value = None, error = None, help = None ) - .add_input( 'hidden', "Create Library", 'create_library', use_label = False ) ) - library = trans.app.model.Library.get( id ) - if library: - return trans.fill_template( '/admin/library/library.mako', library=library, msg=msg ) + if 'message' in kwd: + message = kwd['message'] else: - return trans.show_error_message( "Invalid library specified" ) + message = None + return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ), message = message ) + libraries = library_browser @web.expose - def folder( self, trans, id=None, name="Unnamed", description=None, parent_id = None, **kwd ): + def library( self, trans, id=None, **kwd ): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) + if not id and 'new' not in kwd: + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify a library" ) + if 'new' not in kwd: + library = trans.app.model.Library.get( id ) params = util.Params( kwd ) - msg = params.msg - if 'create_folder' in kwd: - folder = trans.app.model.LibraryFolder( name = name, description = 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 (?) - folder.genome_build = util.dbnames.default_value - if parent_id: - parent_folder = trans.app.model.LibraryFolder.get( parent_id ) - parent_folder.add_folder( folder ) - folder.flush() - trans.response.send_redirect( web.url_for( action='folder', id=folder.id, msg=msg ) ) - elif id is None: + if 'new' in kwd: + if params.new == 'submitted': + library = trans.app.model.Library( name = params.name, description = params.description ) + root_folder = trans.app.model.LibraryFolder( name = params.name, description = "" ) + root_folder.flush() + library.root_folder = root_folder + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) return trans.show_form( - web.FormBuilder( action = web.url_for(), title = "Create a new Folder", name = "create_folder", submit_text = "Submit" ) - .add_text( name = "name", label = "Name", value = "Unnamed", error = None, help = None ) - .add_text( name = "description", label = "Description", value = None, error = None, help = None ) - .add_input( 'hidden', None, 'parent_id', value = parent_id, use_label = False ) - .add_input( 'hidden', "Create Folder", 'create_folder', use_label = False ) ) - folder = trans.app.model.LibraryFolder.get( id ) - if folder: - msg = '' - if 'rename_folder' in kwd: - folder.name = name - folder.description = description + web.FormBuilder( action = web.url_for(), title = "Create a new Library", name="library", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Library" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + if 'root_folder' in kwd: + root_folder = library.root_folder + root_folder.name = params.name + root_folder.flush() + library.name = params.name + library.description = params.description + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit library name and description", name = "library", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = library.name ) + .add_text( name = "description", label = "Description", value = library.description ) + .add_input( 'checkbox', 'Also change the root folder\'s name', 'root_folder' ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True folder.flush() - msg = 'Folder has been renamed.' - return trans.fill_template( '/admin/library/folder.mako', folder=folder, msg=msg ) + delete_folder( library.root_folder ) + library.deleted = True + library.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) else: - return trans.show_error_message( "Invalid folder specified" ) + return trans.show_error_message( "Galaxy can't perform a library action if you don't specify an action" ) + @web.expose + def folder( self, trans, id=None, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + if not id: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify a folder" ) + params = util.Params( kwd ) + folder = trans.app.model.LibraryFolder.get( id ) + if 'new' in kwd: + if params.new == 'submitted': + new_folder = trans.app.model.LibraryFolder( name = params.name, description = params.description ) + # We are associating the last used genome_build with folders, so we will always + # initialize a new folder with the first dbkey in util.dbnames which is currently + # ? unspecified (?) + new_folder.genome_build = util.dbnames.default_value + folder.add_folder( new_folder ) + new_folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Create a new folder", name="folder", submit_text = "Create" ) + .add_text( name = "name", label = "Name", value = "New Folder" ) + .add_text( name = "description", label = "Description", value = "" ) + .add_input( 'hidden', '', 'new', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'rename' in kwd: + if params.rename == 'submitted': + folder.name = params.name + folder.description = params.description + folder.flush() + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + return trans.show_form( + web.FormBuilder( action = web.url_for(), title = "Edit folder name and description", name = "folder", submit_text = "Save" ) + .add_text( name = "name", label = "Name", value = folder.name ) + .add_text( name = "description", label = "Description", value = folder.description ) + .add_input( 'hidden', '', 'rename', 'submitted', use_label = False ) + .add_input( 'hidden', '', 'id', id, use_label = False ) ) + elif 'delete' in kwd: + def delete_folder( folder ): + for subfolder in folder.active_folders: + delete_folder( subfolder ) + for dataset in folder.active_datasets: + dataset.deleted = True + dataset.flush() + folder.deleted = True + folder.flush() + delete_folder( folder ) + return trans.response.send_redirect( web.url_for( action='library_browser' ) ) + else: + return trans.show_error_message( "Galaxy can't perform a folder action if you don't specify an action" ) @web.expose def dataset( self, trans, id=None, name="Unnamed", info='no info', extension=None, folder_id=None, dbkey=None, **kwd ): if not self.user_is_admin( trans ): @@ -625,12 +670,6 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg - def listify( item ): - if isinstance( item, list ): - return item - else: - return [ item ] - # add_file method def add_file( file_obj, name, extension, dbkey, last_used_build, groups, info='no info', space_to_tab=False ): data_type = None @@ -703,6 +742,7 @@ class Admin( BaseController ): return dataset # END add_file method + # Dataset upload if 'create_dataset' in kwd: # Copied from upload tool action last_dataset_created = None @@ -794,13 +834,25 @@ class Admin( BaseController ): info="imported file", space_to_tab=space_to_tab ) created_datasets.append( last_dataset_created ) - if len( created_datasets ): - trans.response.send_redirect( web.url_for( action='change_permissions', ids=",".join( [ str(d.id) for d in created_datasets ] ) ) ) + if len( created_datasets ) > 1: + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "%i new datasets added to the library. Click here if you'd like to edit the permissions on these datasets." % ( + len( created_datasets ), + web.url_for( action='dataset', id=",".join( [ str(d.id) for d in created_datasets ] ) ) + ) + ) ) elif last_dataset_created is not None: - trans.response.send_redirect( web.url_for( action='dataset', id=last_dataset_created.id ) ) + trans.response.send_redirect( web.url_for( + action = 'library_browser', + message = "New dataset added to the library. Click here if you'd like to edit the permissions or attributes on this dataset." % + web.url_for( action='dataset', id=last_dataset_created.id ) + ) ) else: return trans.show_error_message( 'Upload failed' ) - elif id is None: + + # No dataset(s) specified, display upload form + elif not id: # Send list of data formats to the form so the "extension" select list can be populated dynamically file_formats = trans.app.datatypes_registry.upload_file_formats # Send list of genome builds to the form so the "dbkey" select list can be populated dynamically @@ -823,45 +875,21 @@ class Admin( BaseController ): last_used_build=last_used_build, groups=groups, msg=msg ) - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if dataset: + else: + if id.count( ',' ): + ids = id.split(',') + id = None + else: + ids = None + # id specified, display attributes form + if id: + dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if not dataset: + return trans.show_error_message( "Invalid dataset specified" ) + # Copied from edit attributes for 'regular' datasets with some additions p = util.Params(kwd, safe=False) - if p.change_permitted_actions: - # The user clicked the Save button on the 'Dataset Permissions' form - actions = p.actions - if actions and not isinstance( actions, list ): - actions = [ actions ] - if actions is None: - actions = [] - # actions is a list of comma-separated strings consisting of group_id and permitted_action, - # something like: ['6,dataset_access', '6,dataset_edit_metadata']. We'll parse them and - # create a dict whose keys are groups_id and values are permitted_actions - gdpa_dict = {} - for action in actions: - group_id, dpa = action.split( ',' ) - group_id = int( group_id ) - if group_id in gdpa_dict.keys(): - gdpa_dict[ group_id ].append( dpa ) - else: - gdpa_dict[ group_id ] = [ dpa ] - # Refresh the Dataset to ensure we have a valid set of DatasetGroupAssociations - dataset.dataset.refresh() - # Check to see if we need to delete any GroupDatasetAssociations. This occurs if - # the user unchecked all boxes for a group - for group_dataset_assoc in dataset.dataset.groups: - if group_dataset_assoc.group_id not in gdpa_dict.keys(): - group_dataset_assoc.delete() - group_dataset_assoc.flush() - # Use the dict to update the permitted actions for each GroupDatasetAssociaton - for group_id in gdpa_dict: - actions = gdpa_dict[ group_id ] - # Update the permitted_actions for every GroupDatasetAssociation of the Group - q = sa.update( galaxy.model.GroupDatasetAssociation.table, - whereclause = galaxy.model.GroupDatasetAssociation.table.c.group_id == group_id, - values = { galaxy.model.GroupDatasetAssociation.table.c.permitted_actions : actions } ) - result = q.execute() - elif p.change: + if p.change: # The user clicked the Save button on the 'Change data type' form trans.app.datatypes_registry.change_datatype( dataset, p.datatype ) trans.app.model.flush() @@ -879,7 +907,7 @@ class Admin( BaseController ): setattr(dataset.metadata,name,None) else: setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) - + dataset.datatype.after_edit( dataset ) trans.app.model.flush() return trans.show_ok_message( "Attributes updated" ) @@ -894,24 +922,11 @@ class Admin( BaseController ): dataset.datatype.after_edit( dataset ) trans.app.model.flush() return trans.show_ok_message( "Attributes updated" ) - + elif p.delete: + dataset.deleted = True + dataset.flush() + trans.response.send_redirect( web.url_for( action='library_browser' ) ) dataset.datatype.before_edit( dataset ) - # Get all actions to send to the form - dataset_actions = [] - dpas = RBACAgent.permitted_actions - for dpa in dpas.items(): - if dpa[0].startswith( 'DATASET' ): - dataset_actions.append( dpa[1] ) - dataset_actions.sort() - # Get the permitted_actions of each GroupDatasetAssociation to send to the form - gdas = [] - # Refresh the Dataset to ensure we have a valid set of GroupDatasetAssociations - dataset.dataset.refresh() - for group_dataset_assoc in dataset.dataset.groups: - # Refresh the GroupDatasetAssociation to ensure we have a valid set of permitted_actions - group_dataset_assoc.refresh() - group = galaxy.model.Group.get( group_dataset_assoc.group_id ) - gdas.append( ( group.id, group.name, group_dataset_assoc.permitted_actions ) ) if "dbkey" in dataset.datatype.metadata_spec and not dataset.metadata.dbkey: # Copy dbkey into metadata, for backwards compatability # This looks like it does nothing, but getting the dbkey @@ -931,12 +946,28 @@ class Admin( BaseController ): dataset=dataset, metadata=metadata, datatypes=ldatatypes, - dataset_actions=dataset_actions, - gdas=gdas, err=None, msg=msg ) - else: - return trans.show_error_message( "Invalid dataset specified" ) + # multiple ids specfied, display multi permission form + elif ids: + datasets = [] + for id in [ int( id ) for id in ids ]: + d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + if d is None: + return trans.show_error_message( 'You specified an invalid dataset' ) + datasets.append( d ) + if len( datasets ) < 2: + return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) + # If the permissions on the first dataset don't match the intersection + # of permissions, the permissions across all datasets are not + # identical. Although, should we care, or should we just let the admin + # overwrite permissions regardless? + if trans.app.security_agent.get_dataset_permissions( datasets[0] ) != \ + trans.app.security_agent.guess_derived_permissions_for_datasets( [ d.dataset for d in datasets ] ): + return trans.show_error_message( "The datasets you selected do not have identical permissions, so they can not be updated together" ) + else: + return trans.fill_template( "/admin/library/dataset.mako", + dataset=datasets ) def check_gzip( self, temp_name ): """ Utility method to check gzipped uploads @@ -954,42 +985,59 @@ class Admin( BaseController ): # return( True, False ) return ( True, True ) @web.expose - def change_permissions( self, trans, ids=[], **kwd ): + def dataset_permissions( self, trans, id=None, **kwd ): + ''' + In this method, id is an actual Dataset object, not an association. + ''' if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) - if not ids: - return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) - datasets = [] - for id in [ int( id ) for id in ids.split(',') ]: - d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) - if d is None: - return trans.show_error_message( 'You specified an invalid dataset' ) - datasets.append( d ) - if len( datasets ) == 0: - return trans.show_error_message( 'You must specify at least two datasets to modify permissions on' ) - elif len( datasets ) == 1: - trans.response.send_redirect( web.url_for( action='dataset', id=datasets[0].id ) ) - # If the permissions on the first dataset don't match the intersection - # of permissions, the permissions across all datasets are not - # identical. Although, should we care, or should we just let the admin - # overwrite permissions regardless? - if trans.app.security_agent.get_dataset_permissions( datasets[0] ) != \ - trans.app.security_agent.guess_derived_permissions_for_datasets( [ d.dataset for d in datasets ] ): - return trans.show_error_message( "The datasets you selected do not have identical permissions, so they can not be updated together" ) - if 'change_permissions' in kwd: - group_args = [ k.replace('group_', '', 1) for k in kwd if k.startswith('group_') ] - group_ids_checked = filter( lambda x: not x.count('_'), group_args ) - permissions = [] - for group_id in group_ids_checked: - action_strings = [ action.replace(group_id + '_', '', 1) for action in group_args if action.startswith(group_id + '_') ] - actions = trans.app.security_agent.convert_permitted_action_strings( action_strings ) - permissions.append( ( trans.app.security_agent.get_group( group_id ), actions ) ) - for d in datasets: - trans.app.security_agent.set_dataset_permissions( d.dataset, permissions ) - return trans.show_ok_message( "Dataset permissions have been set." ) + if not id: + return trans.show_error_message( 'You must specify at least one dataset to modify permissions on' ) + params = util.Params( kwd ) + # id can be a list of comma separated datasets, too. + if id.count( ',' ): + ids = id.split( ',' ) else: - return trans.fill_template( "/admin/library/change_permissions.mako", - datasets=datasets ) + ids = [ id ] + datasets = [] + for d_id in ids: + d = trans.app.model.Dataset.get( d_id ) + if not d: + return trans.show_error_message( 'You specified an invalid dataset' ) + datasets.append( d ) + if 'change_permitted_actions' in kwd: + users = [] + groups = [] + if params.users: + users = listify( params.users ) + if params.groups: + groups = listify( params.groups ) + permissions = [] + for group_id in users + groups: + permitted_actions = [ pa.replace( group_id + ',', '' ) for pa in params.actions if pa.startswith( group_id + ',' ) ] + permitted_actions = trans.app.security_agent.convert_permitted_action_strings( permitted_actions ) + permissions.append( ( trans.app.model.Group.get( int( group_id ) ), permitted_actions ) ) + if params.public: + permissions.append( ( trans.app.security_agent.get_public_group(), trans.app.security_agent.permitted_actions.DATASET_ACCESS ) ) + for dataset in datasets: + trans.app.security_agent.set_dataset_permissions( dataset, permissions ) + elif 'create_group_associations' in kwd: + users = [] + groups = [] + if params.users: + users = listify( params.users ) + if params.groups: + groups = listify( params.groups ) + if params.public: + for dataset in datasets: + trans.app.security_agent.associate_components( group=trans.app.security_agent.get_public_group(), dataset=dataset ) + for group_id in users + groups: + for dataset in datasets: + trans.app.security_agent.associate_components( group=trans.app.model.Group.get( int( group_id ) ), dataset=dataset ) + if params.lid: + trans.response.send_redirect( web.url_for( action='dataset', id=params.lid ) ) + else: + trans.response.send_redirect( web.url_for( action='library_browser' ) ) @web.expose def delete_dataset( self, trans, id=None, **kwd): if not self.user_is_admin( trans ): @@ -1072,3 +1120,12 @@ class Admin( BaseController ): trans.log_event( "Library id %s deleted." % id ) trans.response.send_redirect( web.url_for( action = 'libraries', msg = 'You have deleted the library %s.' % library.id ) ) return trans.show_error_message( "You did not specify a library to delete." ) + +def listify( item ): + """ + Since single params are not a single item list + """ + if isinstance( item, list ): + return item + else: + return [ item ] diff --git a/static/images/library_closed.png b/static/images/silk/book.png similarity index 100% rename from static/images/library_closed.png rename to static/images/silk/book.png diff --git a/static/images/library_open.png b/static/images/silk/book_open.png similarity index 100% rename from static/images/library_open.png rename to static/images/silk/book_open.png diff --git a/static/images/folder_closed.png b/static/images/silk/folder.png similarity index 100% rename from static/images/folder_closed.png rename to static/images/silk/folder.png diff --git a/static/images/folder_open.png b/static/images/silk/folder_page.png similarity index 100% rename from static/images/folder_open.png rename to static/images/silk/folder_page.png diff --git a/static/images/expander_open.png b/static/images/silk/resultset_bottom.png similarity index 100% rename from static/images/expander_open.png rename to static/images/silk/resultset_bottom.png diff --git a/static/images/expander_closed.png b/static/images/silk/resultset_next.png similarity index 100% rename from static/images/expander_closed.png rename to static/images/silk/resultset_next.png diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako new file mode 100644 index 00000000000..e520ac4723f --- /dev/null +++ b/templates/admin/library/browser.mako @@ -0,0 +1,162 @@ +<%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_dataset" /> + +<%def name="title()">Import from Library +<%def name="stylesheets()"> + + + + + + +<%def name="render_folder( parent, parent_pad )"> + <% + ##if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + ## return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
    • +
      + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif + +
      + +
    • + %if subfolder: +
        + %else: +
          + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + ##%if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
        • ${render_dataset( dataset )}
        • + ##%endif + %endfor +
        + + +

        Libraries

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

        +

        + ${message} +
        +

        +%endif + +

        +
        +
          +%for library in libraries: + ##%if trans.app.security_agent.check_folder_contents( trans.user, library ): +
        • + + + + +
          + + ${library.name} + %if library.description: + - ${library.description} + %endif + + + FormatDbInfo
        • +
            + ${render_folder( library.root_folder, 0 )} +
          +
          + ##%endif +%endfor +
        +## +
        diff --git a/templates/admin/library/change_permissions.mako b/templates/admin/library/change_permissions.mako deleted file mode 100644 index a3be1b647d0..00000000000 --- a/templates/admin/library/change_permissions.mako +++ /dev/null @@ -1,57 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">Change Dataset Permissions - - -
        -
        Change Dataset Access Permissions
        -
        -
        - -
        - <% groups = trans.app.model.Group.select() %> - <% active_group_ids = [ assoc.group.id for assoc in datasets[0].dataset.groups ] %> -
        - Check each group which should have access to this dataset. -
        - %for group in groups: - %if group.id in active_group_ids: - <% assoc = filter( lambda x: x.group_id == group.id, datasets[0].dataset.groups )[0] %> - %else: - <% assoc = None %> - %endif - ${group.name}
        -
        - %for k, v in trans.app.security_agent.permitted_actions.items(): - ${trans.app.security_agent.get_permitted_action_description(k)}
        - %endfor -
        - %endfor - -
        -
        -
        -
        diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako new file mode 100644 index 00000000000..cdaf5492f90 --- /dev/null +++ b/templates/admin/library/common.mako @@ -0,0 +1,253 @@ +<%def name="render_permissions_forms( data_obj )"> + + +<% + redirect = None + id = None + if isinstance( data_obj, trans.app.model.LibraryFolderDatasetAssociation ): + dataset = data_obj.dataset + redirect = ("lid", data_obj.id) + elif isinstance( data_obj, list ): + dataset = data_obj[0].dataset + id = ",".join( [ str(d.dataset.id) for d in data_obj ] ) + if isinstance( data_obj[0], trans.app.model.LibraryFolderDatasetAssociation ): + redirect = ("lid", ",".join( [ str(d.id) for d in data_obj ] ) ) + else: + trans.show_error_message( "Unknown object passed to render_permissions_forms" ) + if id is None: + id = dataset.id +%> + +
        +
        Change Existing Permissions
        +
        +
        + + %if redirect: + + %endif +
        + <% + user_permissions = [ p for p in trans.app.security_agent.get_dataset_permissions( dataset ) if p[0].name.endswith( ' private group' ) ] + real_permissions = [ p for p in trans.app.security_agent.get_dataset_permissions( dataset ) if not p[0].name.endswith( ' private group' ) and p[0].name != 'public' ] + %> +
        + Update the permissions for the users and groups currently associated with this dataset. +
        +
        + %if trans.app.security_agent.dataset_has_group( dataset.id, trans.app.security_agent.get_public_group().id ): +
        + This dataset can be accessed by anyone (it is public).
        + %endif +
        +
        + %if not len( user_permissions ): +   None
        + %endif + %for p in user_permissions: + ${p[0].name.replace( ' private group', '' )}
        +
        + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
        + %endfor +
        + %endfor +
        +
        + %if not len( real_permissions ): +   None
        + %endif + %for p in real_permissions: + ${p[0].name}
        +
        + %for k, v in trans.app.security_agent.permitted_actions.items(): + ${trans.app.security_agent.get_permitted_action_description(k)}
        + %endfor +
        + %endfor +
        +
        +
        +
        +
        +
        +

        + +

        +
        Add New Users and Groups
        +
        +
        + + %if redirect: + + %endif +
        +
        + Select new users and groups to associate with this dataset (you can assign permissions after creating the association). +
        + ## Don't display the public access checkbox if it's already public (it's shown above instead) + %if not trans.app.security_agent.dataset_has_group( dataset.id, trans.app.security_agent.get_public_group().id ): +
        + This dataset can be accessed by anyone (make it public).
        + %endif +
        + <% + all_groups = trans.app.model.Group.select() + user_groups = [ g for g in all_groups if g.name.endswith( ' private group' ) ] + real_groups = [ g for g in all_groups if not g.name.endswith( ' private group' ) and g.name != 'public' ] + %> +
        + + +

        + + +

        +
        + To select multiple users or groups, hold ctrl or command while clicking. +
        +
        +
        +
        +
        +
        +

        + + + +<%doc> + Shamelessly stolen from history... this needs to be cleaned up to remove a + bunch of stuff that doesn't apply to library datasets (like state, etc). + + +## Render the dataset `data` +<%def name="render_dataset( data )"> + <% + if data.state in ['no state','',None]: + data_state = "queued" + else: + data_state = data.state + %> + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +

        + %else: +
        + %endif + + ## Header row for history items (name, state, action buttons) + +
        +
        + + %if data_state == 'running': +
        + %elif data_state != 'ok': +
        + %endif +
        + <%doc> +
        + display data + edit attributes + delete +
        + + + + + + +
        + + ${data.display_name()} + + + ${data.ext}${data.dbkey}${data.info}
        +
        + + ## Body for history items, extra info and actions, data "peek" + +
        +
        + ${data.blurb} +
        +
        + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
        + %if data.peek != "no peek": +
        ${data.display_peek()}
        + %endif + ## Recurse for child datasets + %if len( data.children ) > 0: + ## FIXME: This should not be in the template, there should + ## be a 'visible_children' method on dataset. + <% + children = [] + for child in data.children: + if child.visible: + children.append( child ) + %> + %if len( children ) > 0: +
        + There are ${len( children )} secondary datasets. + %for idx, child in enumerate(children): + ${render_dataset( child, idx + 1 )} + %endfor +
        + %endif + %endif +
        +
        + diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index ea17090632f..adb15e2cd4f 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -1,4 +1,5 @@ <%inherit file="/base.mako"/> +<%namespace file="common.mako" import="render_permissions_forms" /> <%def name="title()">Edit Dataset Attributes @@ -15,34 +16,9 @@ -
        -
        Dataset Permissions
        -
        -
        - -
        - <% dataset_gdas = [ assoc for assoc in dataset.dataset.groups ] %> -
        - Choose the permissions each user or group should have on this dataset. -
        - %for gda in dataset_gdas: - ${gda.group.name.replace( ' private group', '' )}
        -
        - %for k, v in trans.app.security_agent.permitted_actions.items(): - ${trans.app.security_agent.get_permitted_action_description(k)}
        - %endfor -
        - %endfor -
        -
        -
        -
        -
        -

        +${render_permissions_forms( dataset )} + +%if not isinstance( dataset, list ):

        Edit Attributes
        @@ -111,5 +87,6 @@
        -manage containing folder

        +%endif +Return to the library browser diff --git a/templates/admin/library/folder.mako b/templates/admin/library/folder.mako deleted file mode 100644 index aa73d81ee49..00000000000 --- a/templates/admin/library/folder.mako +++ /dev/null @@ -1,170 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="render_component( component )"> - <% - if isinstance( component, trans.app.model.LibraryFolder ): - return render_folder( component ) - elif isinstance( component, trans.app.model.LibraryFolderDatasetAssociation ): - return render_dataset( component ) - %> - - -## Render the dataset `data` as folder item, using `hid` as the displayed id -<%def name="render_dataset( data )"> - <% - if data.state in ['no state','',None]: - data_state = "queued" - else: - data_state = data.state - %> -

        -
        ${data.display_name()}
        -
        -
        - ## Header row for folder items (name, state, action buttons) -
        - %if data_state != 'ok': -
        - %endif -
        -
        - edit attributes - -
        - ##${data.display_name()} -
        - ## Body for folder items, extra info and actions, data "peek" -
        - %if data_state == "queued": -
        Job is waiting to run
        - %elif data_state == "running": -
        Job is currently running
        - %elif data_state == "error": -
        - An error occurred running this job: ${data.display_info().strip()}, - report this error -
        - %elif data_state == "empty": -
        No data: ${data.display_info()}
        - %elif data_state == "ok": -
        - ${data.blurb}, - format: ${data.ext}, - database: - %if data.dbkey == '?': - ${data.dbkey} - %else: - ${data.dbkey} - %endif -
        -
        Info: ${data.display_info()}
        - %if data.peek != "no peek": -
        ${data.display_peek()}
        - %endif - %else: -
        Error: unknown dataset state "${data_state}".
        - %endif - ## Recurse for child datasets -
        -
        -
        - - -## Render a folder -<%def name="render_folder( this_folder )"> -
        -
        Contents of Folder: ${this_folder.name}
        -
        -
        - <% - components = this_folder.active_components - components = [ ( getattr( components[i], "order_id" ), i, components [i] ) for i in xrange( len( components ) ) ] - components.sort() - components = [ tup[-1] for tup in components ] - %> - %for component in components: - ${render_component( component )} - %endfor -
        -
        - -
        -
        -
        - -<%def name="title()">Manage Folder: ${folder.name} -
        -
        - Libraries  |   - Groups  |   - Users -
        -
        Change Folder Attributes
        -
        -
        -
        - -
        - -
        -
        -
        -
        - -
        - -
        -
        -
        -
        -
        - -
        -
        - -
        -
        - -
        -
        -
        -
        -
        -
        Manage Folder Contents: ${folder.name}
        -
        -
        - %if folder.parent: - Up a Level | - %elif folder.library_root: - Manage Library | - %endif - delete folder -
        -
        -
        - ${render_folder( folder )} -
        -
        -
        -
        diff --git a/templates/admin/library/libraries.mako b/templates/admin/library/libraries.mako deleted file mode 100644 index 3511b32a923..00000000000 --- a/templates/admin/library/libraries.mako +++ /dev/null @@ -1,34 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Libraries - -%if msg: -
        ${msg}
        -%endif - -

        Libraries

        - - - -%if len(libraries) == 0: - - There are no libraries - -%else: - - - - - - - %for library in libraries: - - - - - %endfor -
        NameDescription
        ${library.name}${library.description}
        - -%endif \ No newline at end of file diff --git a/templates/admin/library/library.mako b/templates/admin/library/library.mako deleted file mode 100644 index 781a8b584ae..00000000000 --- a/templates/admin/library/library.mako +++ /dev/null @@ -1,38 +0,0 @@ -<%inherit file="/base.mako"/> - -<%def name="title()">Library - -%if msg: -
        ${msg}
        -%endif - -
        -
        Library '${library.name}'
        -
        -
        - -
        - -
        - -
        -
        -
        -
        - -
        - -
        -
        -
        - - -
         
        -
        -
        -
        -
        - diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 1154e3af8bf..0876d1a2c02 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -3,15 +3,10 @@ <% import os %> <%def name="title()">Create New Library Dataset +%if msg: +

        ${msg}

        +%endif
        -
        - Libraries  |   - Groups  |   - Users -
        - %if msg: -

        ${msg}

        - %endif
        Create a new Library Dataset
        diff --git a/templates/library/browser.mako b/templates/library/browser.mako index 5992519e3b2..5a8627ea3bb 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -16,19 +16,19 @@ q("li.libraryOrFolderRow").wrap( "" ).click( function() { var contents = q(this).parent().next("ul"); if ( this.id == "libraryRow" ) { - var icon_open = "${h.url_for( '/static/images/library_open.png' )}"; - var icon_closed = "${h.url_for( '/static/images/library_closed.png' )}"; + var icon_open = "${h.url_for( '/static/images/silk/book_open.png' )}"; + var icon_closed = "${h.url_for( '/static/images/silk/book.png' )}"; } else { - var icon_open = "${h.url_for( '/static/images/folder_open.png' )}"; - var icon_closed = "${h.url_for( '/static/images/folder_closed.png' )}"; + var icon_open = "${h.url_for( '/static/images/silk/folder_page.png' )}"; + var icon_closed = "${h.url_for( '/static/images/silk/folder.png' )}"; } if ( contents.is(":visible") ) { contents.slideUp("fast"); - q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/expander_closed.png' )}"; }); + q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/silk/resultset_next.png' )}"; }); q(this).children().find("img.rowIcon").each( function() { this.src = icon_closed; }); } else { contents.slideDown("fast"); - q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/expander_open.png' )}"; }); + q(this).children().find("img.expanderIcon").each( function() { this.src = "${h.url_for( '/static/images/silk/resultset_bottom.png' )}"; }); q(this).children().find("img.rowIcon").each( function() { this.src = icon_open; }); } }); @@ -65,10 +65,10 @@ pad = parent_pad + 20 %> %if parent_pad == 0: -
      • ${parent.name}
      • +
      • ${parent.name}
        • %else: -
        • ${parent.name}
        • +
        • ${parent.name}
          • %endif %for folder in parent.active_folders: @@ -88,7 +88,7 @@ %for library in libraries: %if trans.app.security_agent.check_folder_contents( trans.user, library ):
          • - + From e99e6164e2ca3ecc2592c12a9b6fabc3d5b82c92 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 12 Sep 2008 11:00:47 -0400 Subject: [PATCH 052/267] Fix link colors in admin. --- templates/admin/index.mako | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/admin/index.mako b/templates/admin/index.mako index 04661b69412..e3d642f55b3 100644 --- a/templates/admin/index.mako +++ b/templates/admin/index.mako @@ -57,6 +57,11 @@ display: list-item; list-style: square outside; } + a:link, a:visited, a:active + { + color: #303030; + } + @@ -104,4 +109,4 @@ - \ No newline at end of file + From f2f7550188bb45ca463172c5fe9d456210ba1474 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 12 Sep 2008 14:54:06 -0400 Subject: [PATCH 053/267] A couple bugfixes and a multi-update for datasets in the admin-side browser. --- lib/galaxy/web/controllers/admin.py | 38 +++++++++++++++----- templates/admin/library/browser.mako | 21 +++++++++-- templates/admin/library/common.mako | 2 +- templates/library/browser.mako | 52 ++++++++++++++++++++++++---- templates/library/common.mako | 28 +++++++-------- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 08673f53ed6..958e6234663 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -563,8 +563,8 @@ class Admin( BaseController ): params = util.Params( kwd ) if 'new' in kwd: if params.new == 'submitted': - library = trans.app.model.Library( name = params.name, description = params.description ) - root_folder = trans.app.model.LibraryFolder( name = params.name, description = "" ) + 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() @@ -578,10 +578,10 @@ class Admin( BaseController ): if params.rename == 'submitted': if 'root_folder' in kwd: root_folder = library.root_folder - root_folder.name = params.name + root_folder.name = util.restore_text( params.name ) root_folder.flush() - library.name = params.name - library.description = params.description + library.name = util.restore_text( params.name ) + library.description = util.restore_text( params.description ) library.flush() return trans.response.send_redirect( web.url_for( action='library_browser' ) ) return trans.show_form( @@ -616,7 +616,7 @@ class Admin( BaseController ): folder = trans.app.model.LibraryFolder.get( id ) if 'new' in kwd: if params.new == 'submitted': - new_folder = trans.app.model.LibraryFolder( name = params.name, description = params.description ) + 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 (?) @@ -632,8 +632,8 @@ class Admin( BaseController ): .add_input( 'hidden', '', 'id', id, use_label = False ) ) elif 'rename' in kwd: if params.rename == 'submitted': - folder.name = params.name - folder.description = params.description + folder.name = util.restore_text( params.name ) + folder.description = util.restore_text( params.description ) folder.flush() return trans.response.send_redirect( web.url_for( action='library_browser' ) ) return trans.show_form( @@ -908,6 +908,7 @@ class Admin( BaseController ): else: setattr(dataset.metadata,name,spec.unwrap(p.get(name, None), p)) + dataset.metadata.dbkey = dbkey dataset.datatype.after_edit( dataset ) trans.app.model.flush() return trans.show_ok_message( "Attributes updated" ) @@ -1039,6 +1040,27 @@ class Admin( BaseController ): else: trans.response.send_redirect( web.url_for( action='library_browser' ) ) @web.expose + def datasets( self, trans, **kwd ): + if not self.user_is_admin( trans ): + return trans.show_error_message( no_privilege_msg ) + params = util.Params( kwd ) + if 'with-selected' in kwd: + if not params.dataset_ids: + return trans.show_error_message( "At least one dataset must be selected." ) + dataset_ids = listify( params.dataset_ids ) + if params.action == 'edit': + trans.response.send_redirect( web.url_for( action = 'dataset', id = ",".join( dataset_ids ) ) ) + elif params.action == 'delete': + for id in dataset_ids: + d = trans.app.model.LibraryFolderDatasetAssociation.get( id ) + d.deleted = True + d.flush() + trans.response.send_redirect( web.url_for( action = 'library_browser' ) ) + else: + return trans.show_error_message( "Not implemented." ) + else: + return trans.show_error_message( "Galaxy can't operate on datasets without an operation." ) + @web.expose def delete_dataset( self, trans, id=None, **kwd): if not self.user_is_admin( trans ): return trans.show_error_message( no_privilege_msg ) diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako index e520ac4723f..31037536dbe 100644 --- a/templates/admin/library/browser.mako +++ b/templates/admin/library/browser.mako @@ -56,6 +56,15 @@ }); }); }); + function checkForm() { + if ( $("select#with-selected-select option:selected").text() == "delete" ) { + if ( confirm( "Are you sure you want to delete these datasets?" ) ) { + return true; + } else { + return false; + } + } + } <%def name="render_folder( parent, parent_pad )"> @@ -130,7 +139,7 @@ - +
              %for library in libraries: ##%if trans.app.security_agent.check_folder_contents( trans.user, library ): @@ -158,5 +167,13 @@ ##%endif %endfor
            -## +
            + With selected datasets: + + +
            diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako index cdaf5492f90..7e3423e0786 100644 --- a/templates/admin/library/common.mako +++ b/templates/admin/library/common.mako @@ -192,7 +192,7 @@
            ${library.name} ${library.name} Format Db Info
            %endif @@ -77,10 +77,10 @@ <% curr_anchor = 'A' %>
            - + ${data.display_name()}
            diff --git a/templates/library/browser.mako b/templates/library/browser.mako index 5a8627ea3bb..ef0f94a5cfe 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -58,18 +58,52 @@ }); + + + + <%def name="render_folder( parent, parent_pad )"> <% if not trans.app.security_agent.check_folder_contents( trans.user, parent ): return "" pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True %> - %if parent_pad == 0: -
          • ${parent.name}
          • -
              - %else: -
            • ${parent.name}
            • +
            • +
              + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif +
              +
            • + %if subfolder:
                + %else: +
                  %endif %for folder in parent.active_folders: ${render_folder( folder, pad )} @@ -88,7 +122,13 @@ %for library in libraries: %if trans.app.security_agent.check_folder_contents( trans.user, library ):
                • - + diff --git a/templates/library/common.mako b/templates/library/common.mako index 7abf13adca5..9861833113e 100644 --- a/templates/library/common.mako +++ b/templates/library/common.mako @@ -38,7 +38,18 @@
                  ${library.name} + + ${library.name} + %if library.description: + - ${library.description} + %endif + Format Db Info
                  - + @@ -50,18 +61,7 @@
                  %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
                  - %elif data_state == "error": -
                  - An error occurred running this job: ${data.display_info().strip()}, - report this error -
                  - %elif data_state == "empty": -
                  No data: ${data.display_info()}
                  - %elif data_state == "ok": + %else:
                  ${data.blurb}
                  @@ -82,8 +82,6 @@ %if data.peek != "no peek":
                  ${data.display_peek()}
                  %endif - %else: -
                  Error: unknown dataset state "${data_state}".
                  %endif ## Recurse for child datasets %if len( data.children ) > 0: From 4e87b28149a0a97cd54c3985767ae54fc27d5404 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 16 Sep 2008 13:53:07 -0400 Subject: [PATCH 054/267] Make the user aware of permissions issues when they share a history. --- lib/galaxy/web/controllers/root.py | 34 ++++++++++++ templates/history/share.mako | 84 +++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index aff793e79cf..2dab4c970bc 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -450,11 +450,45 @@ class RootController( BaseController ): return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() send_to_user = trans.app.model.User.get_by( email = email ) + p = util.Params( kwd ) + if p.action: + if p.action == "no_share": + trans.response.send_redirect( url_for( action='history_options' ) ) + try: + send_to_group = trans.app.model.Group.select_by( name = send_to_user.email + ' private group' )[0] + except: + send_to_group = None + if not send_to_group: + return trans.show_error_message( "Couldn't locate %s's private group, please report this error." % user.email ) if not send_to_user: send_to_err = "No such user" elif user.email == email: send_to_err = "You can't send histories to yourself" else: + # if we're not checking or changing permissions, skip this step + if not p.action or ( p.action and p.action != 'share' ): + # ugly + can_change = {} + cannot_change = {} + for history in histories: + for dataset in history.active_datasets: + if not trans.app.security_agent.allow_action( send_to_user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset ): + if trans.app.security_agent.allow_action( user, trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=dataset ): + if p.action and p.action == "update": + trans.app.security_agent.associate_components( dataset=dataset, permissions=( send_to_group, [ trans.app.security_agent.permitted_actions.DATASET_ACCESS ] ) ) + elif history not in can_change: + can_change[history] = [ dataset ] + else: + can_change[history].append( dataset ) + else: + if p.action and p.action == "update": + pass # don't change stuff that the user doesn't have permission to change + elif history not in cannot_change: + cannot_change[history] = [ dataset ] + else: + cannot_change[history].append( dataset ) + 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 diff --git a/templates/history/share.mako b/templates/history/share.mako index 5e64e66756a..b854f256cdf 100644 --- a/templates/history/share.mako +++ b/templates/history/share.mako @@ -1,6 +1,7 @@ <%inherit file="/base.mako"/> <%def name="title()">Share histories +%if not can_change and not cannot_change:
                  Share Histories
                  ${data.display_name()} +
                  + + view or edit attributes + +
                  + + ${data.display_name()} +
                  ${data.ext} ${data.dbkey} ${data.info}
                  @@ -27,4 +28,85 @@
                  -
                  \ No newline at end of file +
            +%else: + +
            + %for history in histories: + + %endfor + +
            + The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access. These datasets are shown below. Datasets which the user already has permission to access are not shown. +
            +

            + %if can_change: +

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

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

            +

            + %endif + %if cannot_change: +

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

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

            +

            + %endif +

            + How would you like to proceed? +

            + %if can_change: + Change permissions + %if cannot_change: + (where possible) + %endif +
            + %endif + Share anyway + %if can_change: + (don't change any permissions) + %endif +
            + Don't share
            +
            +
            +

            +
            +%endif From 8e17384a79d5bdf77daf82d0dbe68de1715f919c Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 18 Sep 2008 15:18:12 -0400 Subject: [PATCH 055/267] Upgrade to SQLAlchemy 0.4.7 and correct conflicts in ~/model/__init__.py. --- eggs.ini | 4 +- lib/galaxy/app.py | 2 +- lib/galaxy/model/__init__.py | 48 +++---------- lib/galaxy/model/custom_types.py | 17 +++-- lib/galaxy/model/mapping.py | 68 +++++++++---------- lib/galaxy/model/orm/__init__.py | 7 ++ lib/galaxy/model/orm/ext/__init__.py | 3 + lib/galaxy/model/orm/ext/assignmapper.py | 62 +++++++++++++++++ lib/galaxy/web/controllers/admin.py | 8 +-- lib/galaxy/web/framework/__init__.py | 2 +- .../webapps/reports/controllers/jobs.py | 2 +- .../webapps/reports/controllers/system.py | 5 +- .../webapps/reports/controllers/users.py | 2 +- scripts/cleanup_datasets/cleanup_datasets.py | 4 +- test/functional/test_history_functions.py | 5 +- .../functional/test_security_and_libraries.py | 2 +- tools/stats/grouping.py | 2 +- 17 files changed, 144 insertions(+), 99 deletions(-) create mode 100644 lib/galaxy/model/orm/__init__.py create mode 100644 lib/galaxy/model/orm/ext/__init__.py create mode 100644 lib/galaxy/model/orm/ext/assignmapper.py diff --git a/eggs.ini b/eggs.ini index 6b918a7f324..1950ce4aa05 100644 --- a/eggs.ini +++ b/eggs.ini @@ -39,7 +39,7 @@ PasteDeploy = 1.3.1 PasteScript = 1.3.6 Routes = 1.6.3 simplejson = 1.5 -SQLAlchemy = 0.3.11 +SQLAlchemy = 0.4.7p1 Tempita = 0.1 twill = 0.9 WebError = 0.8a @@ -84,7 +84,7 @@ PasteDeploy = http://cheeseshop.python.org/packages/source/P/PasteDeploy/PasteDe PasteScript = http://cheeseshop.python.org/packages/source/P/PasteScript/PasteScript-1.3.6.tar.gz Routes = http://pypi.python.org/packages/source/R/Routes/Routes-1.6.3.tar.gz simplejson = http://cheeseshop.python.org/packages/source/s/simplejson/simplejson-1.5.tar.gz -SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.3.11.tar.gz +SQLAlchemy = http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.4.7p1.tar.gz Tempita = http://pypi.python.org/packages/source/T/Tempita/Tempita-0.1.tar.gz twill = http://darcs.idyll.org/~t/projects/twill-0.9.tar.gz WebError = http://pypi.python.org/packages/source/W/WebError/WebError-0.8a.tar.gz diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 1964945012e..eb26d6a9acd 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -21,7 +21,7 @@ class UniverseApplication( object ): if self.config.database_connection: db_url = self.config.database_connection else: - db_url = "sqlite://%s?isolation_level=IMMEDIATE" % self.config.database + db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database # Setup the database engine and ORM self.model = galaxy.model.mapping.init( self.config.file_path, db_url, diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 342ee4636ef..4ab41942f4e 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -103,28 +103,9 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -<<<<<<< local -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() - self.dbkey = dbkey - self.deleted = deleted - self.visible = visible - # Relationships -======= class GroupDatasetAssociation( object ): def __init__( self, group, dataset, permitted_actions=[] ): - if isinstance( group, GroupDatasetAssociation ) or \ + if isinstance( group, GroupDatasetAssociation ) or \ isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group @@ -182,16 +163,9 @@ class DefaultHistoryGroupAssociation( object ): isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group ->>>>>>> other self.history = history -<<<<<<< local - 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 + self.group = group + self.permitted_actions = permitted_actions @property def ext( self ): @@ -342,8 +316,6 @@ class DefaultHistoryGroupAssociation( object ): for child in self.children: child.mark_deleted() - - class History( object ): def __init__( self, id=None, name=None, user=None ): self.id = id @@ -415,10 +387,6 @@ class History( object ): # # Relationships # self.history = history # self.datasets = [] -======= - self.group = group - self.permitted_actions = permitted_actions ->>>>>>> other class Dataset( object ): states = Bunch( NEW = 'new', @@ -528,7 +496,7 @@ class DatasetInstance( object ): self.extension = extension self.dbkey = dbkey self.designation = designation - self._metadata = metadata or dict() + self.metadata = metadata or dict() self.deleted = deleted self.visible = visible # Relationships @@ -559,9 +527,9 @@ class DatasetInstance( object ): def datatype( self ): return datatypes_registry.get_datatype_by_extension( self.extension ) def get_metadata( self ): - if not self._metadata: - self._metadata = dict() - return MetadataCollection( self, self.datatype.metadata_spec ) + if not hasattr( self, '_metadata_collection' ): + self._metadata_collection = MetadataCollection( self, self.datatype.metadata_spec ) + return self._metadata_collection def set_metadata( self, bunch ): # Needs to accept a MetadataCollection, a bunch, or a dict self._metadata = dict( bunch.items() ) @@ -587,6 +555,8 @@ class DatasetInstance( object ): dbkey = property( get_dbkey, set_dbkey ) def change_datatype( self, new_ext ): self.clear_associated_files() + if hasattr( self, '_metadata_collection' ): + del self._metadata_collection datatypes_registry.change_datatype( self, new_ext ) def get_size( self ): """Returns the size of the data on disk""" diff --git a/lib/galaxy/model/custom_types.py b/lib/galaxy/model/custom_types.py index f537a93f463..72d6b79075c 100644 --- a/lib/galaxy/model/custom_types.py +++ b/lib/galaxy/model/custom_types.py @@ -16,16 +16,15 @@ class JSONType( TypeDecorator ): self.mutable = mutable super( JSONType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_bind_param( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) - return self.jsonifyer.loads( str(buf) ) - - def convert_bind_param( self, value, dialect ): + return self.jsonifyer.dumps( value ) + + def process_result_value( self, value, dialect ): if value is None: return None - return self.impl.convert_bind_param( self.jsonifyer.dumps(value), dialect ) + return self.jsonifyer.loads( str( value ) ) def copy_value( self, value ): if self.mutable: @@ -58,10 +57,10 @@ class MetadataType( JSONType ): self.mutable = mutable super( MetadataType, self).__init__() - def convert_result_value( self, value, dialect ): + def process_result_value( self, value, dialect ): if value is None: return None - buf = self.impl.convert_result_value( value, dialect ) + buf = value ret = None try: ret = self.pickler.loads( str(buf) ) @@ -75,7 +74,7 @@ class MetadataType( JSONType ): class TrimmedString( TypeDecorator ): impl = String - def convert_bind_param( self, value, dialect ): + def process_bind_param( self, value, dialect ): """Automatically truncate string values""" if self.impl.length and value is not None: value = value[0:self.impl.length] diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 5beab407c4c..f0262731996 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -5,24 +5,21 @@ are encapsulated here. import logging log = logging.getLogger( __name__ ) -import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) - import sys import datetime -from sqlalchemy.ext.sessioncontext import SessionContext -from sqlalchemy.ext.assignmapper import assign_mapper -from sqlalchemy.ext.orderinglist import ordering_list - -from sqlalchemy import * from galaxy.model import * +from galaxy.model.orm import * +from galaxy.model.orm.ext.assignmapper import * from galaxy.model.custom_types import * from galaxy.util.bunch import Bunch from galaxy.security import GalaxyRBACAgent -metadata = DynamicMetaData( threadlocal=False ) -context = SessionContext( create_session ) +metadata = MetaData() +context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) + +# For backward compatibility with "context.current" +context.current = Session dialect_to_egg = { "sqlite" : "pysqlite>=2", @@ -224,15 +221,15 @@ Job.table = Table( "job", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), Column( "tool_id", String( 255 ) ), - Column( "tool_version", String, default="1.0.0" ), + Column( "tool_version", TEXT, default="1.0.0" ), Column( "state", String( 64 ) ), Column( "info", TrimmedString( 255 ) ), - Column( "command_line", String() ), + Column( "command_line", TEXT ), Column( "param_filename", String( 1024 ) ), Column( "runner_name", String( 255 ) ), - Column( "stdout", String() ), - Column( "stderr", String() ), - Column( "traceback", String() ), + Column( "stdout", TEXT ), + Column( "stderr", TEXT ), + Column( "traceback", TEXT ), Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True, nullable=True ), Column( "job_runner_name", String( 255 ) ), Column( "job_runner_external_id", String( 255 ) ) ) @@ -292,7 +289,7 @@ StoredWorkflow.table = Table( "stored_workflow", metadata, Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), Column( "latest_workflow_id", Integer, ForeignKey( "workflow.id", use_alter=True, name='stored_workflow_latest_workflow_id_fk' ), index=True ), - Column( "name", String ), + Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), ) @@ -301,7 +298,7 @@ Workflow.table = Table( "workflow", metadata, Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True, nullable=False ), - Column( "name", String ), + Column( "name", TEXT ), Column( "has_cycles", Boolean ), Column( "has_errors", Boolean ) ) @@ -312,8 +309,8 @@ WorkflowStep.table = Table( "workflow_step", metadata, Column( "update_time", DateTime, default=now, onupdate=now ), Column( "workflow_id", Integer, ForeignKey( "workflow.id" ), index=True, nullable=False ), Column( "type", String(64) ), - Column( "tool_id", String ), - Column( "tool_version", String ), # Reserved for future + Column( "tool_id", TEXT ), + Column( "tool_version", TEXT ), # Reserved for future Column( "tool_inputs", JSONType ), Column( "tool_errors", JSONType ), Column( "position", JSONType ), @@ -326,8 +323,8 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "id", Integer, primary_key=True ), Column( "output_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), Column( "input_step_id", Integer, ForeignKey( "workflow_step.id" ), index=True ), - Column( "output_name", String ), - Column( "input_name", String) + Column( "output_name", TEXT ), + Column( "input_name", TEXT) ) StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata, @@ -352,9 +349,7 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab dataset=relation( Dataset, primaryjoin=( Dataset.table.c.id == HistoryDatasetAssociation.table.c.dataset_id ), lazy=False ), - history=relation( - History, - primaryjoin=( History.table.c.id == HistoryDatasetAssociation.table.c.history_id ) ), + # .history defined in History mapper copied_to_history_dataset_associations=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_history_dataset_association_id == HistoryDatasetAssociation.table.c.id ), @@ -415,11 +410,11 @@ assign_mapper( context, Group, Group.table, assign_mapper( context, UserGroupAssociation, UserGroupAssociation.table, properties=dict( user=relation( User, backref = "groups" ), - group=relation( Group, backref = "users" ) ) ) + group=relation( Group, backref = "members" ) ) ) assign_mapper( context, GroupDatasetAssociation, GroupDatasetAssociation.table, properties=dict( dataset=relation( Dataset, backref = "groups" ), - group=relation( Group, backref = "datasets" ) ) ) + group=relation( Group, backref = "group_datasets" ) ) ) assign_mapper( context, DefaultUserGroupAssociation, DefaultUserGroupAssociation.table, properties=dict( user=relation( User, backref = "default_groups" ), @@ -555,11 +550,12 @@ def db_next_hid( self ): Override __next_hid to generate from the database in a concurrency safe way. """ - conn = self.table.engine.contextual_connect() + conn = object_session( self ).connection() + table = self.table trans = conn.begin() try: - next_hid = select( [self.c.hid_counter], self.c.id == self.id, for_update=True ).scalar() - self.table.update( self.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) + next_hid = select( [table.c.hid_counter], table.c.id == self.id, for_update=True ).scalar() + table.update( table.c.id == self.id ).execute( hid_counter = ( next_hid + 1 ) ) trans.commit() return next_hid except: @@ -588,17 +584,21 @@ def init( file_path, url, engine_options={}, create_tables=False ): # Create the database engine engine = create_engine( url, **engine_options ) # Connect the metadata to the database. - metadata.connect( engine ) - ## metadata.engine.echo = True + metadata.bind = engine + # Clear any existing contextual sessions and reconfigure + Session.remove() + Session.configure( bind=engine ) # Create tables if needed if create_tables: metadata.create_all() # metadata.engine.commit() # Pack everything into a bunch result = Bunch( **globals() ) - result.engine = metadata.engine - result.flush = lambda *args, **kwargs: context.current.flush( *args, **kwargs ) - result.context = context + result.engine = engine + result.flush = lambda *args, **kwargs: Session.flush( *args, **kwargs ) + result.session = Session + # For backward compatibility with "model.context.current" + result.context = Session result.create_tables = create_tables #load local galaxy security policy result.security_agent = GalaxyRBACAgent( result ) diff --git a/lib/galaxy/model/orm/__init__.py b/lib/galaxy/model/orm/__init__.py new file mode 100644 index 00000000000..433e1e26f45 --- /dev/null +++ b/lib/galaxy/model/orm/__init__.py @@ -0,0 +1,7 @@ +import pkg_resources +pkg_resources.require( "SQLAlchemy >= 0.4" ) + +from sqlalchemy import * +from sqlalchemy.orm import * + +from sqlalchemy.ext.orderinglist import ordering_list diff --git a/lib/galaxy/model/orm/ext/__init__.py b/lib/galaxy/model/orm/ext/__init__.py new file mode 100644 index 00000000000..230b8aec908 --- /dev/null +++ b/lib/galaxy/model/orm/ext/__init__.py @@ -0,0 +1,3 @@ +""" +Galaxy specific SQLAlchemy extensions. +""" \ No newline at end of file diff --git a/lib/galaxy/model/orm/ext/assignmapper.py b/lib/galaxy/model/orm/ext/assignmapper.py new file mode 100644 index 00000000000..d2b7e2ca7b4 --- /dev/null +++ b/lib/galaxy/model/orm/ext/assignmapper.py @@ -0,0 +1,62 @@ +""" +This is similar to the assignmapper extensions in SQLAclhemy 0.3 and 0.4 but +with some compatibility fixes. It assumes that the session is a ScopedSession, +and thus has the "mapper" method to attach contextual mappers to a class. It +adds additional query and session methods to the class to support the +SQLAlchemy 0.3 style of access. The following methods which would normally be +accessed through "Object.query().method()" are available directly through the +object: + + 'get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances' + +Additionally, the following Session methods, which normally accept an instance +or list of instances, are available directly through the objects, e.g. +"Session.flush( [instance] )" can be performed as "instance.flush()": + + 'refresh', 'expire', 'delete', 'expunge', 'update' +""" + +__all__ = [ 'assign_mapper' ] + +from sqlalchemy import util, exceptions +import types +from sqlalchemy.orm import mapper, Query + +def _monkeypatch_query_method( name, session, class_ ): + def do(self, *args, **kwargs): + ## util.warn_deprecated('Query methods on the class are deprecated; use %s.query.%s instead' % (class_.__name__, name)) + return getattr( class_.query, name)(*args, **kwargs) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, classmethod(do)) + +def _monkeypatch_session_method(name, session, class_, make_list=False): + def do(self, *args, **kwargs): + if make_list: + self = [ self ] + return getattr(session, name)( self, *args, **kwargs ) + try: + do.__name__ = name + except: + pass + if not hasattr(class_, name): + setattr(class_, name, do) + +def assign_mapper( session, class_, *args, **kwargs ): + m = class_.mapper = session.mapper( class_, *args, **kwargs ) + for name in ('get', 'filter', 'filter_by', 'select', 'select_by', + 'selectfirst', 'selectfirst_by', 'selectone', 'selectone_by', + 'get_by', 'join_to', 'join_via', 'count', 'count_by', + 'options', 'instances'): + _monkeypatch_query_method(name, session, class_) + for name in ('refresh', 'expire', 'delete', 'expunge', 'update'): + _monkeypatch_session_method(name, session, class_) + for name in ( 'flush', ): + _monkeypatch_session_method( name, session, class_, make_list=True ) + return m diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 958e6234663..c7035dd98eb 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -6,7 +6,7 @@ from galaxy.security import RBACAgent import galaxy.model from xml.sax.saxutils import escape, unescape import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging @@ -251,7 +251,7 @@ class Admin( BaseController ): # simpler approach of deleting all existing members and creating # new records for user_ids in the received members param. # First remove existing members that are not in the received members param - for user_group_assoc in group.users: + for user_group_assoc in group.members: if user_group_assoc.user_id not in members: user = galaxy.model.User.get( user_group_assoc.user_id ) # Delete DefaultUserGroupAssociations @@ -272,7 +272,7 @@ class Admin( BaseController ): # Then add all new members to the group for user_id in members: user = galaxy.model.User.get( user_id ) - if user not in group.users: + if user not in group.members: user_group_association = galaxy.model.UserGroupAssociation( user, group ) user_group_association.flush() msg = "Group membership has been updated with a total of %s members" % len( members ) @@ -405,7 +405,7 @@ class Admin( BaseController ): group_id = params.group_id group = galaxy.model.Group.get( group_id ) # Remove members and all associations - for user_group_assoc in group.users: + for user_group_assoc in group.members: user = galaxy.model.User.get( user_group_assoc.user_id ) # Delete DefaultUserGroupAssociations for default_user_group_association in user.default_groups: diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 2be8810b68e..20bda3c2d8e 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -24,7 +24,7 @@ import mako.lookup pkg_resources.require( "simplejson" ) import simplejson -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) from sqlalchemy import desc import logging diff --git a/lib/galaxy/webapps/reports/controllers/jobs.py b/lib/galaxy/webapps/reports/controllers/jobs.py index bf7935d9647..a502ca48532 100644 --- a/lib/galaxy/webapps/reports/controllers/jobs.py +++ b/lib/galaxy/webapps/reports/controllers/jobs.py @@ -5,7 +5,7 @@ import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/system.py b/lib/galaxy/webapps/reports/controllers/system.py index dc6d3b36b90..477b2084d03 100644 --- a/lib/galaxy/webapps/reports/controllers/system.py +++ b/lib/galaxy/webapps/reports/controllers/system.py @@ -2,8 +2,9 @@ import operator, os from datetime import datetime, timedelta from galaxy.webapps.reports.base.controller import * import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload, desc +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload +from sqlalchemy import desc import logging log = logging.getLogger( __name__ ) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index b99546f14dd..185a18107c3 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -3,7 +3,7 @@ import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) +pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) diff --git a/scripts/cleanup_datasets/cleanup_datasets.py b/scripts/cleanup_datasets/cleanup_datasets.py index 0d653981d00..97ca3f8e8f8 100644 --- a/scripts/cleanup_datasets/cleanup_datasets.py +++ b/scripts/cleanup_datasets/cleanup_datasets.py @@ -13,8 +13,8 @@ from galaxy import eggs import galaxy.model.mapping import pkg_resources -pkg_resources.require( "sqlalchemy>=0.3" ) -from sqlalchemy import eagerload +pkg_resources.require( "SQLAlchemy >= 0.4" ) +from sqlalchemy.orm import eagerload assert sys.version_info[:2] >= ( 2, 4 ) diff --git a/test/functional/test_history_functions.py b/test/functional/test_history_functions.py index 24663bf4e77..8b0b12819fa 100644 --- a/test/functional/test_history_functions.py +++ b/test/functional/test_history_functions.py @@ -48,7 +48,10 @@ class TestHistory( TwillTestCase ): """Testing sharing a history with another user""" self.upload_file('1.bed', dbkey='hg18') id, name, email = self.share_history() - self.check_page_for_string( 'History (%s) has been shared with: %s' %(name, email) ) + try: + self.check_page_for_string( 'History (%s) has been shared with: %s' %(name, email) ) + except TwillAssertionError: + self.check_page_for_string( "The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access." ) self.logout() self.login( email='test2@bx.psu.edu' ) self.view_stored_histories() diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 748684e696f..031eef67b61 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -85,7 +85,7 @@ class TestHistory( TwillTestCase ): def test_20_create_library( self ): """Testing creating new library""" - self.create_library( name='New Test Library', description='New test Library Description' ) + self.create_library( name='New Test Library', description='New Test Library Description' ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Library" ) diff --git a/tools/stats/grouping.py b/tools/stats/grouping.py index d21a6da6f4e..a26fb033f0b 100644 --- a/tools/stats/grouping.py +++ b/tools/stats/grouping.py @@ -90,7 +90,7 @@ def main(): for ii, line in enumerate( file( tmpfile.name )): if line and not line.startswith( '#' ): - line = line.strip() + line = line.rstrip( '\r\n' ) try: fields = line.split("\t") item = fields[group_col] From 0cfd2717f0e84b7bfa8dc350f5ef46bcda5016a1 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 19 Sep 2008 10:29:37 -0400 Subject: [PATCH 056/267] Need uselist=False for these backrefs in alchemy 0.4... are there others? --- lib/galaxy/model/mapping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index f0262731996..347010db27e 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -353,11 +353,11 @@ 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] ) ), + backref=backref( "copied_from_history_dataset_association", primaryjoin=( HistoryDatasetAssociation.table.c.copied_from_library_folder_dataset_association_id == LibraryFolderDatasetAssociation.table.c.id ), remote_side=[LibraryFolderDatasetAssociation.table.c.id], uselist=False ) ), implicitly_converted_datasets=relation( ImplicitlyConvertedDatasetAssociation, primaryjoin=( ImplicitlyConvertedDatasetAssociation.table.c.hda_parent_id == HistoryDatasetAssociation.table.c.id ) ), From 1e5d2759f832480c349b4fd4886429121c45e3a8 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 19 Sep 2008 15:37:36 -0400 Subject: [PATCH 057/267] =?UTF-8?q?Change=20the=20way=20GMAJ=20peeks=20are?= =?UTF-8?q?=20generated.=20A=20non-escaped=20©=5Faccess=5Ffrom=20was?= =?UTF-8?q?=20being=20modified=20to=20'=C2=A9=5Faccess=5Ffrom'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/galaxy/datatypes/images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/galaxy/datatypes/images.py b/lib/galaxy/datatypes/images.py index 1db5291a5a2..78bf6964e81 100644 --- a/lib/galaxy/datatypes/images.py +++ b/lib/galaxy/datatypes/images.py @@ -5,7 +5,7 @@ Image classes import data import logging from galaxy.datatypes.sniff import * -from urllib import urlencode +from urllib import urlencode, quote_plus import zipfile log = logging.getLogger(__name__) @@ -112,7 +112,7 @@ class Gmaj( data.Data ): "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, 'copy_access_from': dataset.id } ) + "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" @@ -186,7 +186,7 @@ class Laj( data.Text ): "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, 'copy_access_from': dataset.id } ), + "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" From bf540d2628ea06ac546c5754b794327fc0977fbe Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Fri, 19 Sep 2008 16:28:11 -0400 Subject: [PATCH 058/267] Unseq genomes for oct --- static/images/welcomePhoto.jpg | Bin 76711 -> 31819 bytes static/welcome.html | 23 +++++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/static/images/welcomePhoto.jpg b/static/images/welcomePhoto.jpg index 91abdf314960f806906f2312974db26934573a63..33fc6a470dfe04a4d42b2bd19754ba358f585ee6 100644 GIT binary patch delta 30643 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%0DmnRHzj>!E){9f=%`;rl@1az) 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%0D|rDM9ZWkDoB6vxZhlFV{3 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 Bp_Kpt diff --git a/static/welcome.html b/static/welcome.html index 7a5064c6af7..5bd2ab245cd 100644 --- a/static/welcome.html +++ b/static/welcome.html @@ -10,28 +10,27 @@
            -
            - Galaxy and 2008 Meeting Season +
            + Workflows are finally here!
            + Watch how you can (Click link to play)...
              -
            • Beyond Genome | San Francisco | June 9 - 11
            • -
            • ISMB 2008 | Toronto | July 19 - 23
            • -
            • Genome Informatics | Hinxton, UK | September 10 - 14
            • -
            • ASHG | Philadelphia | November 11 - 15
            • -
            +
          • Create Workflows by Example: Convert your Galaxy History into a Workflow.
          • +
          • Edit Workflows: I want to repeat an analysis...but...with different parameters.
          • +
          • Workflows from scratch: Drag, drag, drag...
          • +
            - Download new Galaxy brochure here. -
            - + For more screencasts click here. +

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


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

            From 830f545f1c3b1e31d22d9960d5acf01e8d235057 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Fri, 19 Sep 2008 16:29:55 -0400 Subject: [PATCH 059/267] James, please review. Is the use of save_or_update okay? Corrected another 0.4 backref assignment bug that Anton found in workflow. --- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/web/controllers/workflow.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 4ab41942f4e..cbeac083a1d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -936,7 +936,7 @@ class WorkflowStep( object ): self.tool_inputs = None self.tool_errors = None self.position = None - self.input_connections = None + self.input_connections = [] self.config = None class WorkflowStepConnection( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 347010db27e..222de251097 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -364,7 +364,7 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab children=relation( HistoryDatasetAssociation, primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), - backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id] ) ) + backref=backref( "parent", primaryjoin=( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ), remote_side=[HistoryDatasetAssociation.table.c.id], uselist=False ) ) ) ) assign_mapper( context, Dataset, Dataset.table, diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index ac1a7f10ab6..d50cb969c38 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -64,7 +64,7 @@ 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.list( trans ) @@ -107,7 +107,7 @@ 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 ) @@ -132,7 +132,7 @@ 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 ) @@ -309,7 +309,7 @@ class WorkflowController( BaseController ): workflow.stored_workflow = stored stored.latest_workflow = workflow # Persist - trans.sa_session.save( stored ) + trans.sa_session.save_or_update( stored ) trans.sa_session.flush() # Return something informative errors = [] @@ -434,7 +434,7 @@ 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 return trans.show_message( "Workflow '%s' created from current history." % workflow_name ) @@ -740,4 +740,4 @@ def cleanup_param_values( inputs, values ): prefix = "%s|" % ( key ) cleanup( prefix, input.cases[current_case].inputs, group_values ) cleanup( "", inputs, values ) - return associations \ No newline at end of file + return associations From b7e69706a12b6c42c7304f7a61ff2eafc3d56d3e Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 23 Sep 2008 17:23:29 -0400 Subject: [PATCH 060/267] 1) Add a new tag to DataToolParameters to filter out datasets from inclusion in the select list that do not pass the check. The tag set looks something like . 2) Fixed some bugs in how we associate datasets with groups and permissions, specifically when a user logs in after creating a history with datasets while not authenticated. 3) Added a new active_histories mapper to User. 4) The DataToolParameter's dataset_collecter is now sent history.active_datasets rather than history.datasets. --- lib/galaxy/model/mapping.py | 1 + lib/galaxy/security/__init__.py | 64 +++++++++++++---------- lib/galaxy/tools/__init__.py | 5 +- lib/galaxy/tools/parameters/basic.py | 77 +++++++++++++++++++--------- tools/data_destination/epigraph.xml | 10 +++- 5 files changed, 100 insertions(+), 57 deletions(-) diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 222de251097..afdaf27c4c3 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -399,6 +399,7 @@ assign_mapper( context, History, History.table, assign_mapper( context, User, User.table, properties=dict( histories=relation( History, backref="user", order_by=desc(History.table.c.update_time) ), + active_histories=relation( History, primaryjoin=( ( History.table.c.user_id == User.table.c.id ) & ( not_( History.table.c.deleted ) ) ), order_by=desc( History.table.c.update_time ) ), stored_workflow_menu_entries=relation( StoredWorkflowMenuEntry, backref="user", cascade="all, delete-orphan", collection_class=ordering_list( 'order_index' ) ) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 4bd8cf2fe44..425b1600598 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -142,7 +142,10 @@ class GalaxyRBACAgent( RBACAgent ): return intersect def get_group( self, id ): return self.model.Group.get( id ) - raise 'No valid method of retrieving requested group %s' % ( id ) + raise 'No valid method of retrieving requested group by id %s' % ( str( id ) ) + def get_group_by_name( self, name ): + return self.model.Group.get_by( name=name ) + raise 'No valid method of retrieving requested group by name %s' % ( str( name ) ) def create_group( self, **kwd ): rval = self.model.Group( **kwd ) rval.flush() @@ -181,14 +184,16 @@ class GalaxyRBACAgent( RBACAgent ): if permissions is None: permissions = [ ( self.create_private_user_group( user ), self.permitted_actions.__dict__.values() ) ] if permissions is not None: - for assoc in user.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() + # Delete all of the previous DefaultUserGroupAssociation + for duga in user.default_groups: #this is the association not the actual group + duga.delete() + duga.flush() + # Add the new DefaultUserGroupAssociation for group, permitted_actions in permissions: - assoc = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) - assoc.flush() + duga = self.model.DefaultUserGroupAssociation( user, group, permitted_actions ) + duga.flush() if history: - for history in user.histories: + for history in user.active_histories: self.history_set_default_access( history, permissions=permissions, dataset=dataset ) def user_get_default_access( self, user ): return [ ( duga.group, duga.permitted_actions ) for duga in user.default_groups ] @@ -199,22 +204,26 @@ class GalaxyRBACAgent( RBACAgent ): else: permissions = [ ( self.get_public_group(), self.permitted_actions.__dict__.values() ) ] if permissions is not None: - for assoc in history.default_groups: #this is the association not the actual group - assoc.delete() - assoc.flush() + # Delete all of the previous DefaultHistoryGroupAssociations + for dhga in history.default_groups: #this is the association not the actual group + dhga.delete() + dhga.flush() + # Add the new DefaultHistoryGroupAssociations for group, permitted_actions in permissions: - assoc = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) - assoc.flush() + dhga = self.model.DefaultHistoryGroupAssociation( history, group, permitted_actions ) + dhga.flush() if dataset: - for data in history.datasets: - for hda in data.dataset.history_associations: - if history.user and hda.history not in history.user.histories: + for hda in history.datasets: + for hda2 in hda.dataset.history_associations: + if history.user and hda2.history not in history.user.active_histories: # This will occur when a user logs in and has datasets in their previously-public history. - self.set_dataset_permissions( data.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) - break + self.set_dataset_permissions( hda.dataset, [ ( self.get_public_group(), [ self.permitted_actions.DATASET_ACCESS ] ) ] ) + #break else: - if self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=data.dataset ): - self.set_dataset_permissions( data.dataset, permissions ) + # At this point, we will have already created a gda for the public group and delete_assocs + # is set to False in order to preserve it. + if self.allow_action( history.user, self.permitted_actions.DATASET_MANAGE_PERMISSIONS, dataset=hda.dataset ): + self.set_dataset_permissions( hda.dataset, permissions, delete_existing_assocs=False ) def history_get_default_access( self, history ): return [ ( dhga.group, dhga.permitted_actions ) for dhga in history.default_groups ] def get_public_group( self ): @@ -223,7 +232,7 @@ class GalaxyRBACAgent( RBACAgent ): return self.model.Group.set_public_group( group ) def guess_public_group( self ): return self.model.Group.guess_public_group() - def set_dataset_permissions( self, dataset, permissions ): + def set_dataset_permissions( self, dataset, permissions, delete_existing_assocs=True ): """ Apply permissions (a list of (group, permitted_action) tuples) to a dataset, removing any existing permissions. permissions can @@ -231,10 +240,11 @@ class GalaxyRBACAgent( RBACAgent ): """ if isinstance( dataset, self.model.HistoryDatasetAssociation ): dataset = dataset.dataset - for group_dataset_assoc in dataset.groups: - group_dataset_assoc.delete() - group_dataset_assoc.flush() - if len( permissions ) and isinstance( permissions[0], self.model.GroupDatasetAssociation ): + if delete_existing_assocs: + for gda in dataset.groups: + gda.delete() + gda.flush() + if permissions and isinstance( permissions[0], self.model.GroupDatasetAssociation ): permissions = [ ( gda.group, gda.permitted_actions ) for gda in permissions ] for ptuple in permissions: self.associate_components( dataset=dataset, permissions=ptuple ) @@ -259,9 +269,9 @@ class GalaxyRBACAgent( RBACAgent ): return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) 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. + 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 ) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 8cb7c044719..23c6d8074df 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1067,7 +1067,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: @@ -1087,9 +1087,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 diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 6a2f278f47b..be656b53342 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -3,16 +3,13 @@ Basic tool parameters. """ import logging, string, sys, os - from elementtree.ElementTree import XML, Element - from galaxy import config, datatypes, util from galaxy.web import form_builder - import validation, dynamic_options - # For BaseURLToolParameter from galaxy.web import url_for +import galaxy.model log = logging.getLogger(__name__) @@ -985,6 +982,9 @@ class DataToolParameter( ToolParameter ): 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 + 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. + >>> # Mock up a history (not connected to database) >>> from galaxy.model import History, HistoryDatasetAssociation, User, Group >>> from galaxy.util.bunch import Bunch @@ -997,13 +997,10 @@ class DataToolParameter( ToolParameter ): >>> group.flush() >>> Group.public_id = group.id >>> dataset1 = HistoryDatasetAssociation( id=1, extension='txt', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset1, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset2 = HistoryDatasetAssociation( id=2, extension='bed', create_dataset=True ) >>> security_agent.set_dataset_permissions( dataset2, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset3 = HistoryDatasetAssociation( id=3, extension='fasta', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset3, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset4 = HistoryDatasetAssociation( id=4, extension='png', create_dataset=True ) - >>> security_agent.set_dataset_permissions( dataset4, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> dataset5 = HistoryDatasetAssociation( id=5, extension='interval', create_dataset=True ) >>> security_agent.set_dataset_permissions( dataset5, [ ( group, security_agent.permitted_actions.__dict__.values() ) ] ) >>> hist.add_dataset( dataset1 ) @@ -1011,13 +1008,13 @@ class DataToolParameter( ToolParameter ): >>> hist.add_dataset( dataset3 ) >>> hist.add_dataset( dataset4 ) >>> hist.add_dataset( dataset5 ) - >>> p = DataToolParameter( None, XML( '' ) ) + >>> p = DataToolParameter( None, XML( '' ) ) >>> print p.name blah + >>> print p.security_dict + {'test': ['access']} >>> print p.get_html( trans=Bunch( history=hist, user=None, app=Bunch( security_agent = security_agent ) ) ) """ @@ -1048,6 +1045,16 @@ class DataToolParameter( ToolParameter ): else: self.options = dynamic_options.DynamicOptions( options, self ) self.is_dynamic = self.options is not None + self.security_dict = {} + for security_check in elem.findall( "security_check" ): + group = security_check.get( 'group', None ) + assert group is not None, "security_check elements require a group attribute" + action = security_check.get( 'action', None ) + assert action is not None, "security_check elements require an action attribute" + try: + self.security_dict[ group ].append( action ) + except: + self.security_dict[ group ] = [ action ] def get_html_field( self, trans=None, value=None, other_values={} ): filter_value = None @@ -1064,34 +1071,54 @@ 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] and data.visible and trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): - if self.options and data.get_dbkey() != filter_value: + hid = str( hda.hid ) + if not hda.dataset.state == galaxy.model.Dataset.states.ERROR and hda.visible and trans.app.security_agent.allow_action( trans.user, hda.permitted_actions.DATASET_ACCESS, dataset=hda ): + if self.security_dict: + passed_security_check = True + for group_name, permitted_actions in self.security_dict.items(): + # Make sure the dataset belongs to the group + group = trans.app.security_agent.get_group_by_name( group_name ) + if not trans.app.security_agent.dataset_has_group( hda.dataset_id, group.id ): + passed_security_check = False + break + # Make sure the permitted_actions are available on the dataset for the group + group_dataset_permitted_actions = trans.app.security_agent.get_dataset_permissions( hda.dataset, group_id=group.id )[1] + actions_allowed = True + for action in permitted_actions: + if not action in group_dataset_permitted_actions: + actions_allowed = False + break + if not actions_allowed: + passed_security_check = False + break + if not passed_security_check: + 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: for target_ext in self.extensions: - if target_ext in data.get_converter_types(): - datasets = data.get_converted_files_by_type( target_ext ) + if target_ext in hda.get_converter_types(): + datasets = hda.get_converted_files_by_type( target_ext ) if datasets: data = datasets[0] elif not self.converter_safe( other_values, trans ): continue - if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data ): + 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 ( data in value ) ) - field.add_option( "%s: (as %s) %s" % ( hid, target_ext, data.name[:30] ), data.id, selected ) + selected = ( value and ( hda.dataset in value ) ) + field.add_option( "%s: (as %s) %s" % ( hid, target_ext, hda.name[:30] ), hda.dataset.id, selected ) break #we only report the first valid converter, assume self.extensions is a priority list # 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/tools/data_destination/epigraph.xml b/tools/data_destination/epigraph.xml index bb2bd9b1b5d..5f68e7bbc76 100644 --- a/tools/data_destination/epigraph.xml +++ b/tools/data_destination/epigraph.xml @@ -3,7 +3,8 @@ Genome analysis and prediction GENOME=${input1.dbkey} NAME=${input1.name} INFO=${input1.info} - + + @@ -12,6 +13,13 @@ + + .. class:: infomark + +**TIP:** Data sent to EpiGRAPH must be "publicly accessible" ( the **public** group must have permission to access the data ). Click the pencil icon in the history item to change the dataset's permissions if you are permitted to do so. + +----- + **What it does** This tool sends the selected dataset to EpiGRAPH for in-depth analysis and prediction. From 97dad70d8e992c3985fc59c198cfd2c501515238 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Wed, 24 Sep 2008 09:11:40 -0400 Subject: [PATCH 061/267] Fix for building redirect URL. --- lib/galaxy/tools/actions/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index c3c21af95ea..d5595db2af9 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -204,7 +204,10 @@ 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 ) # Job should not be queued, so set state to ok job.state = JOB_OK job.info = "Redirected to: %s" % redirect_url From 9bda606dbc4c89d870932f861ab3d7a0516e5ec2 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 24 Sep 2008 16:02:52 -0400 Subject: [PATCH 062/267] Resolve conflicts in model/__init__.py from central. --- lib/galaxy/model/__init__.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 6eebe6c9cdd..932d4a5be96 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -103,7 +103,6 @@ class JobToOutputDatasetAssociation( object ): self.name = name self.dataset = dataset -<<<<<<< local class GroupDatasetAssociation( object ): def __init__( self, group, dataset, permitted_actions=[] ): if isinstance( group, GroupDatasetAssociation ) or \ @@ -164,26 +163,6 @@ class DefaultHistoryGroupAssociation( object ): isinstance( group, DefaultUserGroupAssociation ) or \ isinstance( group, DefaultHistoryGroupAssociation ): group = group.group -======= -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 ->>>>>>> other self.history = history self.group = group self.permitted_actions = permitted_actions From bc7dc21cdbdd08a7e5fcb5c7ec3a740934ff9664 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 24 Sep 2008 16:12:13 -0400 Subject: [PATCH 063/267] Fixed conflicts in templates. --- templates/history/options.mako | 6 ------ templates/root/tool_menu.mako | 25 +------------------------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/templates/history/options.mako b/templates/history/options.mako index 49b63182e90..56dab0377ca 100644 --- a/templates/history/options.mako +++ b/templates/history/options.mako @@ -16,14 +16,8 @@ %if len( history.active_datasets ) > 0:
          • Create a new empty history
          • %endif -<<<<<<< local
          • Construct workflow from the current history
          • -======= - %if app.config.enable_beta_features: -
          • Construct workflow from the current history
          • - %endif
          • Change default permitted actions for the current history
          • ->>>>>>> other
          • Share current history
          • %endif
          • Delete current history diff --git a/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index 14e20f96280..04af8526c4c 100644 --- a/templates/root/tool_menu.mako +++ b/templates/root/tool_menu.mako @@ -82,7 +82,6 @@ ## at least some workflows will appear here (the user should be able to ## configure which of their stored workflows appear in the tools menu). -<<<<<<< local
            @@ -92,16 +91,7 @@
            Manage workflows -======= -%if app.config.enable_beta_features: - %if t.user and t.user.stored_workflow_menu_entries: -
            -
            -
            - Your workflows ->>>>>>> other
            -<<<<<<< local %if t.user: %for m in t.user.stored_workflow_menu_entries:
            @@ -111,19 +101,6 @@ %endif
            -======= -
            -
            - %for m in t.user.stored_workflow_menu_entries: - - %endfor -
            -
            - %endif -%endif ->>>>>>> other
            @@ -133,4 +110,4 @@ - \ No newline at end of file + From 5b75630caeaae8d46f5868e90e92688c41e21cb9 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 25 Sep 2008 16:14:26 -0400 Subject: [PATCH 064/267] Migrate sqlalchemy queries to 0.4 by eliminating deprecated queries ( select_by, selectone_by, get_by all replaced by filter_by ). --- lib/galaxy/jobs/__init__.py | 7 +++--- lib/galaxy/model/__init__.py | 2 +- lib/galaxy/model/mapping.py | 2 +- lib/galaxy/model/mapping_tests.py | 6 ++--- lib/galaxy/security/__init__.py | 8 +++---- lib/galaxy/web/controllers/admin.py | 5 +++-- lib/galaxy/web/controllers/library.py | 4 ++-- lib/galaxy/web/controllers/root.py | 22 ++++++++----------- lib/galaxy/web/controllers/user.py | 11 +++++----- lib/galaxy/web/controllers/workflow.py | 2 +- lib/galaxy/web/framework/__init__.py | 12 +++++----- .../webapps/reports/controllers/users.py | 11 ++-------- .../functional/test_security_and_libraries.py | 8 +++---- 13 files changed, 44 insertions(+), 56 deletions(-) diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 3cda6ce6c76..827d3db3d6d 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -97,11 +97,10 @@ class JobQueue( object ): model = self.app.model # Jobs in the NEW state won't be requeued unless we're tracking in the database if not self.track_jobs_in_database: - for job in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for job in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): log.debug( "no runner: %s is still in new state, adding to the jobs queue" %job.id ) self.queue.put( ( job.id, job.tool_id ) ) - for job in model.Job.select( (model.Job.c.state == model.Job.states.RUNNING) - | (model.Job.c.state == model.Job.states.QUEUED) ): + for job in model.Job.filter( (model.Job.c.state == model.Job.states.RUNNING) | (model.Job.c.state == model.Job.states.QUEUED) ).all(): if job.job_runner_name is not None: # why are we passing the queue to the wrapper? job_wrapper = JobWrapper( job.id, self.app.toolbox.tools_by_id[ job.tool_id ], self ) @@ -136,7 +135,7 @@ class JobQueue( object ): new_jobs = [] if self.track_jobs_in_database: model = self.app.model - for j in model.Job.select( model.Job.c.state == model.Job.states.NEW ): + for j in model.Job.filter( model.Job.c.state==model.Job.states.NEW ).all(): job = JobWrapper( j.id, self.app.toolbox.tools_by_id[ j.tool_id ], self ) new_jobs.append( job ) else: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 932d4a5be96..19279d5c319 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -139,7 +139,7 @@ class Group( object ): @classmethod def guess_public_group( cls ): # Retrieve from database and store public group id - group = Group.select_by( name='public' )[0] + group = Group.filter_by( name='public' ).first() cls.set_public_group( group ) class UserGroupAssociation( object ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index afdaf27c4c3..3acd38ec787 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -604,7 +604,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): #load local galaxy security policy result.security_agent = GalaxyRBACAgent( result ) # Ensure group named 'public' exists - public_group = result.Group.get_by( name='public' ) + public_group = result.Group.filter_by( name='public' ).first() if not public_group: public_group = result.security_agent.create_group( name = 'public' ) # Store public group id diff --git a/lib/galaxy/model/mapping_tests.py b/lib/galaxy/model/mapping_tests.py index e040c59e393..bfefcabddde 100644 --- a/lib/galaxy/model/mapping_tests.py +++ b/lib/galaxy/model/mapping_tests.py @@ -22,13 +22,13 @@ class MappingTests( unittest.TestCase ): model.context.current.flush() model.context.current.clear() # Check - users = model.User.select() + users = model.User.query().all() assert len( users ) == 1 assert users[0].email == "james@foo.bar.baz" assert users[0].password == "password" assert len( users[0].histories ) == 1 assert users[0].histories[0].name == "History 1" - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == ( "H" * 255 ) assert hists[0].user == users[0] @@ -40,7 +40,7 @@ class MappingTests( unittest.TestCase ): hists[1].name = "History 2b" model.context.current.flush() model.context.current.clear() - hists = model.History.select() + hists = model.History.query().all() assert hists[0].name == "History 1" assert hists[1].name == "History 2b" # gvk TODO need to ad test for GalaxySessions, but not yet sure what they should look like. diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 425b1600598..90aaa0ac3db 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -144,7 +144,7 @@ class GalaxyRBACAgent( RBACAgent ): return self.model.Group.get( id ) raise 'No valid method of retrieving requested group by id %s' % ( str( id ) ) def get_group_by_name( self, name ): - return self.model.Group.get_by( name=name ) + return self.model.Group.filter_by( name=name ).first() raise 'No valid method of retrieving requested group by name %s' % ( str( name ) ) def create_group( self, **kwd ): rval = self.model.Group( **kwd ) @@ -260,13 +260,13 @@ class GalaxyRBACAgent( RBACAgent ): assert len( kwd ) == 2, 'You must specify exactly 2 Galaxy security components to check for associations.' if 'dataset' in kwd: if 'group' in kwd: - return self.model.GroupDatasetAssociation.get_by( group_id = kwd['group'].id, dataset_id = kwd['dataset'].id ) + return self.model.GroupDatasetAssociation.filter_by( group_id=kwd['group'].id, dataset_id=kwd['dataset'].id ).first() elif 'user' in kwd: if 'group' in kwd: - return self.model.UserGroupAssociation.get_by( group_id = kwd['group'].id, user_id = kwd['user'].id ) + return self.model.UserGroupAssociation.filter_by( group_id=kwd['group'].id, user_id=kwd['user'].id ).first() raise 'No valid method of associating provided components: %s' % kwd def dataset_has_group( self, dataset_id, group_id ): - return bool( self.model.GroupDatasetAssociation.get_by( group_id = group_id, dataset_id = dataset_id ) ) + return bool( self.model.GroupDatasetAssociation.filter_by( group_id=group_id, dataset_id=dataset_id ).first() ) def check_folder_contents( self, user, entry ): """ Return true if there are any datasets under 'folder' that the diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index c7035dd98eb..0c71bebe4d8 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -4,6 +4,7 @@ from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent import galaxy.model +from galaxy.model.orm import * from xml.sax.saxutils import escape, unescape import pkg_resources pkg_resources.require( "SQLAlchemy >= 0.4" ) @@ -138,7 +139,7 @@ class Admin( BaseController ): if not name: msg = "Please enter a name" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) - elif len( trans.app.model.Group.select_by( name=name ) ) > 0: + elif trans.app.model.Group.filter_by( name=name ).first(): msg = "A group with that name already exists" trans.response.send_redirect( '/admin/create_group?msg=%s' % msg ) else: @@ -550,7 +551,7 @@ class Admin( BaseController ): message = kwd['message'] else: message = None - return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ), message = message ) + return trans.fill_template( '/admin/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all(), message = message ) libraries = library_browser @web.expose def library( self, trans, id=None, **kwd ): diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 1d42735603d..5db40832306 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,5 +1,5 @@ - from galaxy.web.base.controller import * +from galaxy.model.orm import * import logging log = logging.getLogger( __name__ ) @@ -7,7 +7,7 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose def browse( self, trans, **kwd ): - return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.select_by( deleted = False ) ) + return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all() ) index = browse @web.expose def import_datasets( self, trans, import_ids=[], **kwd ): diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 2dab4c970bc..63bfc1aab85 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__ ) @@ -449,13 +445,13 @@ class RootController( BaseController ): if not email: return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() - send_to_user = trans.app.model.User.get_by( email = email ) + send_to_user = trans.app.model.User.filter_by( email=email ).first() p = util.Params( kwd ) if p.action: if p.action == "no_share": trans.response.send_redirect( url_for( action='history_options' ) ) try: - send_to_group = trans.app.model.Group.select_by( name = send_to_user.email + ' private group' )[0] + send_to_group = trans.app.model.Group.filter_by( name=send_to_user.email+' private group' ).first() except: send_to_group = None if not send_to_group: @@ -532,7 +528,7 @@ class RootController( BaseController ): new_history.user_id = user.id galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -549,7 +545,7 @@ class RootController( BaseController ): new_history.user_id = None galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) @@ -574,7 +570,7 @@ class RootController( BaseController ): if new_history: galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.selectone_by( session_id=galaxy_session.id, history_id=new_history.id ) + association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 5b00894d1f9..c5c2de4a368 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -1,9 +1,8 @@ """ Contains the user interface in the Universe class """ - from galaxy.web.base.controller import * - +from galaxy.model.orm import * import logging, os, string from random import choice @@ -53,7 +52,7 @@ class User( BaseController ): email_err = "Please enter a real email address" elif len( email) > 255: email_err = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_err = "User with that email already exists" elif email != conf_email: conf_email_err = "Email addresses do not match." @@ -73,7 +72,7 @@ class User( BaseController ): email_error = password_error = None # Attempt login if email or password: - user = trans.app.model.User.get_by( email = email ) + user = trans.app.model.User.filter_by( email=email ).first() if not user: email_error = "No such user" elif user.external: @@ -108,7 +107,7 @@ class User( BaseController ): email_error = "Please enter a real email address" elif len( email) > 255: email_error = "Email address exceeds maximum allowable length" - elif len( trans.app.model.User.select_by( email=email ) ) > 0: + elif trans.app.model.User.filter_by( email=email ).first(): email_error = "User with that email already exists" elif len( password ) < 6: password_error = "Please use a password of at least 6 characters" @@ -144,7 +143,7 @@ class User( BaseController ): @web.expose def reset_password(self, trans, email=None, **kwd): error = '' - reset_user = trans.app.model.User.get_by( email = email ) + reset_user = trans.app.model.User.filter_by( email=email ).first() user = trans.get_user() if reset_user: if user and user.id != reset_user.id: diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 75b68bf3bf8..b0b17b92830 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -46,7 +46,7 @@ class WorkflowController( BaseController ): # Load workflow from database stored = get_stored_workflow( trans, id ) if email: - other = model.User.get_by( email=email ) + other = model.User.filter_by( email=email ).first() if not other: mtype = "error" msg = ( "User '%s' does not exist" % email ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 20bda3c2d8e..b3628b98d90 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -172,7 +172,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.current_history_id: history = self.app.model.History.get( galaxy_session.current_history_id ) if history and not history.deleted: @@ -217,7 +217,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): galaxy_session.user_id = self.user.id try: # See if we have already associated the history with the session - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=history.id ).first() except: association = None history.add_galaxy_session( galaxy_session, association=association ) @@ -266,7 +266,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): """Return the user in $HTTP_REMOTE_USER and create if necessary""" # remote_user middleware ensures HTTP_REMOTE_USER exists try: - user = self.app.model.User.selectone_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ) + user = self.app.model.User.filter_by( email=self.environ[ 'HTTP_REMOTE_USER' ] ).first() except: user = self.app.model.User( email=self.environ[ 'HTTP_REMOTE_USER' ] ) user.set_password_cleartext( 'external' ) @@ -283,7 +283,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if secure_id: session_key = self.security.decode_session_key( secure_id ) try: - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid and galaxy_session.user_id: user = self.app.model.User.get( galaxy_session.user_id ) if user: @@ -323,7 +323,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): session_key = self.security.decode_session_key( secure_id ) try: # Retrive the galaxy_session id via the unique session_key - galaxy_session = self.app.model.GalaxySession.selectone_by( session_key=session_key ) + galaxy_session = self.app.model.GalaxySession.filter_by( session_key=session_key ).first() if galaxy_session and galaxy_session.is_valid: self.__galaxy_session = galaxy_session except: @@ -384,7 +384,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): if self.history is not None: # See if we have already associated the session with the history try: - association = self.app.model.GalaxySessionToHistoryAssociation.select_by( session_id=galaxy_session.id, history_id=self.history.id )[0] + association = self.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=self.history.id ).first() except: association = None galaxy_session.add_history( self.history, association=association ) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index 185a18107c3..af9b0550b17 100644 --- a/lib/galaxy/webapps/reports/controllers/users.py +++ b/lib/galaxy/webapps/reports/controllers/users.py @@ -2,6 +2,7 @@ from datetime import * import calendar from galaxy.webapps.reports.base.controller import * import galaxy.model +from galaxy.model.orm import * import pkg_resources pkg_resources.require( "SQLAlchemy >= 0.4" ) import sqlalchemy as sa @@ -13,15 +14,7 @@ class Users( BaseController ): def registered_users( self, trans, **kwd ): params = util.Params( kwd ) msg = '' - engine = galaxy.model.mapping.metadata.engine - s = """ - SELECT - count(id) AS num_users - FROM - galaxy_user - """ - rows = engine.text( s ).execute().fetchall() - num_users = rows[0].num_users + num_users = galaxy.model.User.query().count() return trans.fill_template( 'registered_users.mako', num_users=num_users, msg=msg ) @web.expose def registered_users_per_month( self, trans, **kwd ): diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 031eef67b61..5b9063b6afa 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -59,7 +59,7 @@ class TestHistory( TwillTestCase ): # twill version 0.9 still does not allow for the following test #def test_15_add_group_member( self ): # """Testing adding a member to an existing group""" - # group = galaxy.model.Group.get_by( name='New Test Group' ) + # group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] # group_id = str( group.id ) # group_name = group.name.replace( ' ', '+' ) # self.add_group_member( group_id=group_id, group_name=group_name ) @@ -70,17 +70,17 @@ class TestHistory( TwillTestCase ): """Testing deleting a group""" self.visit_page( "admin/groups" ) self.check_page_for_string( "group_name=New+Test+Group" ) - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] group_id = str( group.id ) self.mark_group_deleted( group_id=group_id ) def test_25_undelete_group( self ): """Testing undeleting a deleted group""" - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] group_id = str( group.id ) self.undelete_group( group_id=group_id ) def test_30_purge_group( self ): """Testing purging a group""" - group = galaxy.model.Group.get_by( name='New Test Group' ) + group = galaxy.model.Group.filter_by( name='New Test Group' ).all()[0] self.purge_group( group=group ) def test_20_create_library( self ): From d41deccfe1ba4a339eea7b0a939d371592d7354e Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 30 Sep 2008 09:31:46 -0400 Subject: [PATCH 065/267] Add ability for external applications to send data to Galaxy. The /root/dataset_import() method makes use of the upload tool, taking advantage of all of the safety checks incorporated into the tool. --- lib/galaxy/tools/actions/upload.py | 14 +++++++++++- lib/galaxy/web/controllers/root.py | 34 ++++++++++++++++++++++++++++- tools/data_destination/epigraph.xml | 6 ++--- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index 9fba4fe467d..b33e1a1fbd8 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -36,12 +36,24 @@ class UploadToolAction( object ): return self.upload_empty( trans, "Error:", str( e ) ) if url_paste not in [ None, "" ]: if url_paste.lower().find( 'http://' ) >= 0 or url_paste.lower().find( 'ftp://' ) >= 0: + # If we were sent a DATA_URL from an external application in a post, NAME and INFO + # values should be in the request + if 'NAME' in incoming and incoming[ 'NAME' ] not in [ "None", None ]: + NAME = incoming[ 'NAME' ] + else: + NAME = '' + if 'INFO' in incoming and incoming[ 'INFO' ] not in [ "None", None ]: + INFO = incoming[ 'INFO' ] + else: + INFO = "uploaded url" url_paste = url_paste.replace( '\r', '' ).split( '\n' ) for line in url_paste: line = line.rstrip( '\r\n' ) if line: + if not NAME: + NAME = line try: - data_list.append( self.add_file( trans, urllib.urlopen( line ), line, file_type, dbkey, info="uploaded url", space_to_tab=space_to_tab ) ) + data_list.append( self.add_file( trans, urllib.urlopen( line ), NAME, file_type, dbkey, info=INFO, space_to_tab=space_to_tab ) ) except Exception, e: return self.upload_empty( trans, "Error:", str( e ) ) else: diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 63bfc1aab85..5f3a60874d2 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -87,7 +87,39 @@ 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.expose + def dataset_import( self, trans, **kwd ): + """ + External applications (e.g., EpiGRAPH) can import data to Galaxy by passing the following: + 1. DATA_URL - the url to which Galaxy should post a request to retrieve the data + 2. GENOME - the name of the UCSC genome assembly (e.g. hg18), dbkey in Galaxy + 3. NAME - data.name in Galaxy + 4. INFO - data.info in Galaxy + This method will create the tool parameters expected by the upload tool so that it + can be executed to retrieve the data from the external application. + """ + params_dict = {} + params = util.Params( kwd ) + DATA_URL = params.get( 'DATA_URL', None ) + assert DATA_URL is not None, "Required DATA_URL parameter missing from request" + params_dict[ 'url_paste' ] = DATA_URL + params_dict[ 'dbkey' ] = params.get( 'GENOME', '?' ) + NAME = params.get( 'NAME', None ) + assert NAME is not None, "Required NAME parameter missing from request" + params_dict[ 'NAME' ] = NAME + INFO = params.get( 'INFO', None ) + assert INFO is not None, "Required INFO parameter missing from request" + params_dict[ 'INFO' ] = INFO + params_dict[ 'runtool_btn' ] = 'Execute' + tool_id = 'upload1' + history = trans.get_history() + trans.ensure_valid_galaxy_session() + tool = trans.get_toolbox().tools_by_id.get( tool_id ) + template, vars = tool.handle_input( trans, params_dict ) + trans.log_event( "/root/dataset_import tool params: %s" % ( str( params_dict ) ), tool_id=tool_id ) + trans.response.send_redirect( url_for( "/index" ) ) + @web.json def history_item_updates( self, trans, ids=None, states=None ): # Avoid caching diff --git a/tools/data_destination/epigraph.xml b/tools/data_destination/epigraph.xml index 5f68e7bbc76..4ccd1378cc7 100644 --- a/tools/data_destination/epigraph.xml +++ b/tools/data_destination/epigraph.xml @@ -1,6 +1,6 @@ - - Genome analysis and prediction + + analysis and prediction with EpiGRAPH GENOME=${input1.dbkey} NAME=${input1.name} INFO=${input1.info} @@ -9,7 +9,7 @@ - + From 745cba211ab391da1c5d16c1e17460bf0ade4b7f Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 30 Sep 2008 10:39:46 -0400 Subject: [PATCH 066/267] Fixes for tool_conf.xml.sample, this file is not being properly kept current... --- tool_conf.xml.sample | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index 8b8cdca2964..addb40fa216 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -123,7 +123,6 @@ -
            @@ -156,8 +155,8 @@
            - - + +
            - \ No newline at end of file + diff --git a/templates/base_panels.mako b/templates/base_panels.mako index 0d275ed44c2..c04ffb3d0f4 100644 --- a/templates/base_panels.mako +++ b/templates/base_panels.mako @@ -10,7 +10,7 @@ %> <%def name="init()"> -## Override + ## Override ## Default title @@ -40,10 +40,10 @@ ## Default javascripts <%def name="javascripts()"> - + ## Default late-load javascripts @@ -64,7 +64,7 @@ %endif - + ## Masthead <%def name="masthead()"> @@ -80,40 +80,42 @@ ${self.init()} - ${self.title()} - ${self.javascripts()} - ${self.stylesheets()} + ${self.title()} + ${self.javascripts()} + ${self.stylesheets()} - ## Background displays first -
            - ## Layer iframes over backgrounds -
            - ${self.masthead()} -
            + ## Background displays first +
            + ## Layer iframes over backgrounds +
            + ${self.masthead()} +
            %if self.message_box_visible: ${self.message_box_content()} %endif
            %if self.has_left_panel: -
            - ${self.left_panel()} -
            -
            +
            + ${self.left_panel()} +
            +
            + +
            %endif -
            - ${self.center_panel()} -
            +
            + ${self.center_panel()} +
            %if self.has_right_panel: -
            - +
            + %endif ## Allow other body level elements - ${next.body()} + ${next.body()} ## Scripts can be loaded later since they progressively add features to ## the panels, but do not change layout diff --git a/templates/library/browser.mako b/templates/library/browser.mako index ef0f94a5cfe..e9a15c4b690 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -1,5 +1,6 @@ <%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()"> @@ -78,67 +79,88 @@ <%def name="render_folder( parent, parent_pad )"> - <% - if not trans.app.security_agent.check_folder_contents( trans.user, parent ): - return "" - pad = parent_pad + 20 - if parent_pad == 0: - expander = "/static/images/silk/resultset_bottom.png" - folder = "/static/images/silk/folder_page.png" - subfolder = False - else: - expander = "/static/images/silk/resultset_next.png" - folder = "/static/images/silk/folder.png" - subfolder = True - %> -
          • -
            - - ${parent.name} - %if parent.description: - - ${parent.description} - %endif -
            -
          • - %if subfolder: -
              - %else: -
                - %endif - %for folder in parent.active_folders: - ${render_folder( folder, pad )} - %endfor - %for dataset in parent.active_datasets: - %if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): -
              • ${render_dataset( dataset )}
              • - %endif - %endfor + <% + if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + return "" + pad = parent_pad + 20 + if parent_pad == 0: + expander = "/static/images/silk/resultset_bottom.png" + folder = "/static/images/silk/folder_page.png" + subfolder = False + else: + expander = "/static/images/silk/resultset_next.png" + folder = "/static/images/silk/folder.png" + subfolder = True + %> +
              • +
                + + ${parent.name} + %if parent.description: + - ${parent.description} + %endif +
                +
              • + %if subfolder: +
                  + %else: +
                    + %endif + %for folder in parent.active_folders: + ${render_folder( folder, pad )} + %endfor + %for dataset in parent.active_datasets: + %if trans.app.security_agent.allow_action( trans.user, trans.app.security_agent.permitted_actions.DATASET_ACCESS, dataset=dataset.dataset ): +
                  • ${render_dataset( dataset )}
                  • + %endif + %endfor

                  Libraries

                  -
                  -
                    -%for library in libraries: - %if trans.app.security_agent.check_folder_contents( trans.user, library ): -
                  • - - - - -
                    - - ${library.name} - %if library.description: - - ${library.description} + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%if not libraries: + No libraries contain datasets that you are allowed to access +%else: + <% can_access = False %> + +
                      + %for library in libraries: + %if trans.app.security_agent.check_folder_contents( trans.user, library ): + <% can_access = True %> +
                    • +
                      + + + + + + + +
                      + + ${library.name} + %if library.description: + - ${library.description} + %endif + FormatDbInfo
                      +
                      +
                    • +
                        + ${render_folder( library.root_folder, 0 )} +
                      +
                      + %endif + %endfor +
                    + %if can_access: + + %else: + No libraries contain datasets that you are allowed to access %endif -
                    FormatDbInfo
                  • -
                      - ${render_folder( library.root_folder, 0 )} -
                    -
                    - %endif -%endfor -
                  - -
                  + +%endif diff --git a/templates/library/common.mako b/templates/library/common.mako index 9861833113e..d1bf6a5b6d2 100644 --- a/templates/library/common.mako +++ b/templates/library/common.mako @@ -17,91 +17,75 @@
                  %endif - ## Header row for history items (name, state, action buttons) - + ## Header row for history items (name, state, action buttons)
                  - - %if data_state == 'running': -
                  - %elif data_state != 'ok': -
                  - %endif + + %if data_state == 'running': +
                  + %elif data_state != 'ok': +
                  + %endif
                  - <%doc> -
                  - display data - edit attributes - delete -
                  - - - - - - -
                  -
                  - - view or edit attributes - -
                  - - ${data.display_name()} -
                  ${data.ext}${data.dbkey}${data.info}
                  -
                  - - ## Body for history items, extra info and actions, data "peek" - -
                  - %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): -
                  You do not have permission to view this dataset.
                  - %else: -
                  - ${data.blurb} -
                  -
                  - %if data.has_data: - save - %for display_app in data.datatype.get_display_types(): - <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> - %if len( display_links ) > 0: - | ${data.datatype.get_display_label(display_app)} - %for display_name, display_link in display_links: - ${display_name} - %endfor - %endif - %endfor - %endif -
                  - %if data.peek != "no peek": -
                  ${data.display_peek()}
                  - %endif - %endif - ## Recurse for child datasets - %if len( data.children ) > 0: - ## FIXME: This should not be in the template, there should - ## be a 'visible_children' method on dataset. - <% - children = [] - for child in data.children: - if child.visible: - children.append( child ) - %> - %if len( children ) > 0: -
                  - There are ${len( children )} secondary datasets. - %for idx, child in enumerate(children): - ${render_dataset( child, idx + 1 )} - %endfor + <%doc> +
                  + display data + edit attributes + delete +
                  + + + + + + + + +
                  + + + ${data.display_name()} + ${data.ext}${data.dbkey}${data.info}
                  +
                  + + ## Body for history items, extra info and actions, data "peek" +
                  + %if not trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset = data.dataset ): +
                  You do not have permission to view this dataset.
                  + %else: +
                  ${data.blurb}
                  +
                  + %if data.has_data: + save + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + | ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor %endif +
                  + %if data.peek != "no peek": +
                  ${data.display_peek()}
                  %endif -
                  + %endif + ## Recurse for child datasets + %if len( data.visible_children ) > 0: +
                  + There are ${len( data.visible_children )} secondary datasets. + %for idx, child in enumerate( data.visible_children ): + ${render_dataset( child )} + %endfor +
                  + %endif
                  diff --git a/templates/message.mako b/templates/message.mako index 551b60c4735..19d11fd98f9 100644 --- a/templates/message.mako +++ b/templates/message.mako @@ -1,32 +1,37 @@ <%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}
                  + \ No newline at end of file diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index f0f820023ab..ab51a378605 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -674,7 +674,7 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create library: %s' % str( err ) ) self.home() - def rename_library( self, library_id, name='New Test Library Renamed', description='New Test Library Description Re-described' ): + def rename_library( self, library_id, name='New Test Library Renamed', description='New Test Library Description Re-described', root_folder='' ): """Rename a library""" try: self.visit_url( "%s/admin/library?rename=True&id=%s" % ( self.url, library_id ) ) @@ -682,6 +682,8 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( 'Edit library name and description' ) tc.fv( "1", "name", name ) # form field 1 is the field named name... tc.fv( "1", "description", description ) # form field 2 is the field named description... + if root_folder: + tc.fv( "1", "root_folder", root_folder ) tc.submit( "rename_library_button" ) except AssertionError, err: self.home() @@ -733,6 +735,25 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create add a dataset to a folder: %s' % str( err ) ) self.home() + def add_dataset_to_folder_from_history( self, folder_id ): + """Copy a dataset from the current history to a library folder""" + try: + # Create a new history + self.new_history() + self.upload_file( "1.bed" ) + self.verify_dataset_correctness( "1.bed" ) + self.visit_url( "%s/admin/add_dataset_to_folder_from_history?folder_id=%s" % ( self.url, folder_id ) ) + self.last_page() + self.check_page_for_string( 'Active datasets in your current history' ) + tc.fv( "1", "folder_id", folder_id ) + tc.fv( "1", "ids", "1" ) + tc.submit( "add_dataset_from_history_button" ) + self.last_page() + self.check_page_for_string( 'Added the following datasets to the library folder: 1.bed' ) + except AssertionError, err: + self.home() + raise AssertionError( 'Exception caught attempting to create add a dataset to a folder: %s' % str( err ) ) + self.home() def add_datasets_from_library_dir( self, folder_id, extension='auto', dbkey='hg18', roles=[] ): """Add a directory of datasets to a folder""" try: diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 4554d6ff93f..0c9e0ace5f6 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -129,11 +129,11 @@ class TestHistory( TwillTestCase ): galaxy.model.Library.table.c.deleted==False ) ).first() def test_21_rename_library( self ): """Testing renaming a library""" - self.rename_library( str( library.id ), name='New Test Library Renamed', description='New Test Library Description Re-described' ) + self.rename_library( str( library.id ), name='New Test Library Renamed', description='New Test Library Description Re-described', root_folder='on' ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Library Renamed" ) # Rename it back to what it was originally - self.rename_library( str( library.id ), name='New Test Library', description='New Test Library Description' ) + self.rename_library( str( library.id ), name='New Test Library', description='New Test Library Description', root_folder='on' ) def test_24_rename_root_folder( self ): """Testing renaming a library root folder""" folder = library.root_folder @@ -148,7 +148,11 @@ class TestHistory( TwillTestCase ): self.check_page_for_string( "1.bed" ) self.check_page_for_string( "bed" ) self.check_page_for_string( "hg18" ) - def test_30_add_new_folder( self ): + def test_30_copy_dataset_from_history_to_root_folder( self ): + """Testing copying a dataset from the current history to a library root folder""" + folder = library.root_folder + self.add_dataset_to_folder_from_history( str( folder.id ) ) + def test_33_add_new_folder( self ): """Testing adding a folder to a library root folder""" root_folder = library.root_folder name = 'New Test Folder' @@ -160,26 +164,26 @@ class TestHistory( TwillTestCase ): galaxy.model.LibraryFolder.table.c.description==description ) ).first() self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Folder" ) - def test_33_add_datasets_from_library_dir( self ): - """Testing adding dataset from library directory to sub-folder""" + def test_36_add_datasets_from_library_dir( self ): + """Testing adding several datasets from library directory to sub-folder""" self.add_datasets_from_library_dir( str( new_test_folder.id ), roles=[ str( new_test_role.id ) ] ) - def test_36_mark_group_deleted( self ): + def test_39_mark_group_deleted( self ): """Testing marking a group as deleted""" self.visit_page( "admin/groups" ) self.check_page_for_string( another_test_group.name ) self.mark_group_deleted( str( another_test_group.id ) ) - def test_39_undelete_group( self ): + def test_42_undelete_group( self ): """Testing undeleting a deleted group""" self.undelete_group( str( another_test_group.id ) ) - def test_42_mark_role_deleted( self ): + def test_45_mark_role_deleted( self ): """Testing marking a role as deleted""" self.visit_page( "admin/roles" ) self.check_page_for_string( another_test_role.name ) self.mark_role_deleted( str( another_test_role.id ) ) - def test_45_undelete_role( self ): + def test_48_undelete_role( self ): """Testing undeleting a deleted role""" self.undelete_role( str( another_test_role.id ) ) - def test_48_mark_library_deleted( self ): + def test_51_mark_library_deleted( self ): """Testing marking a library as deleted""" self.mark_library_deleted( str( library.id ) ) # Make sure the library was deleted @@ -203,7 +207,7 @@ class TestHistory( TwillTestCase ): if lfda.dataset.deleted: raise AssertionError( 'The dataset with id "%s" has been marked as deleted when it should not have been.' % lfda.dataset.id ) check_folder( library.root_folder ) - def test_51_mark_library_undeleted( self ): + def test_54_mark_library_undeleted( self ): """Testing marking a library as not deleted""" self.mark_library_undeleted( str( library.id ) ) # Make sure the library is undeleted @@ -232,7 +236,7 @@ class TestHistory( TwillTestCase ): library.refresh() if not library.deleted: raise AssertionError( 'The library id %s named "%s" has not been marked as deleted after it was undeleted.' % ( str( library.id ), library.name ) ) - def test_54_purge_group( self ): + def test_57_purge_group( self ): """Testing purging a group""" group_id = str( another_test_group.id ) self.purge_group( group_id ) @@ -244,7 +248,7 @@ class TestHistory( TwillTestCase ): gra = galaxy.model.GroupRoleAssociation.filter( galaxy.model.GroupRoleAssociation.table.c.group_id == group_id ).all() if gra: raise AssertionError( "Purging the group did not delete the GroupRoleAssociations for group_id '%s'" % group_id ) - def test_57_purge_role( self ): + def test_60_purge_role( self ): """Testing purging a role""" role_id = str( another_test_role.id ) self.purge_role( role_id ) @@ -256,7 +260,7 @@ class TestHistory( TwillTestCase ): adra = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.role_id == role_id ).all() if adra: raise AssertionError( "Purging the role did not delete the ActionDatasetRoleAssociations for role_id '%s'" % role_id ) - def test_60_purge_library( self ): + def test_63_purge_library( self ): """Testing purging a library""" self.purge_library( str( library.id ) ) # Make sure the library was purged From 1369b8d199a59509a725d3f10268825b060c4f00 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 20 Nov 2008 15:14:47 -0500 Subject: [PATCH 107/267] You can now require that users log in to use Galaxy. The sample config is way different but it's only because I cleaned it up a bit. The only new options are require_login/allow_user_creation. --- lib/galaxy/config.py | 4 +- lib/galaxy/util/__init__.py | 12 ++ lib/galaxy/web/__init__.py | 2 +- lib/galaxy/web/controllers/admin.py | 161 ++++++------------ lib/galaxy/web/controllers/root.py | 7 +- lib/galaxy/web/controllers/user.py | 75 ++++++-- lib/galaxy/web/framework/__init__.py | 39 ++++- lib/galaxy/web/framework/base.py | 22 ++- static/scripts/galaxy.panels.js | 16 +- static/scripts/packed/galaxy.panels.js | 2 +- .../scripts/packed/galaxy.ui.scrollPanel.js | 2 +- templates/admin/dataset_security/users.mako | 4 + templates/admin/index.mako | 3 +- templates/base_panels.mako | 2 + templates/form.mako | 4 + templates/message.mako | 8 +- templates/no_access.mako | 15 ++ templates/root/index.mako | 19 ++- templates/root/masthead.mako | 55 +++--- universe_wsgi.ini.sample | 156 +++++++++-------- 20 files changed, 357 insertions(+), 251 deletions(-) create mode 100644 templates/no_access.mako diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 560062ed8cd..a8df982d53c 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -38,10 +38,12 @@ class Configuration( object ): 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.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None ) + self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) + self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) ) self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root ) self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/compiled_templates" ), self.root ) self.local_job_queue_workers = int( kwargs.get( "local_job_queue_workers", "5" ) ) - self.cluster_job_queue_workers = int( kwargs.get( "cluster_job_queue_workers", "5" ) ) + self.cluster_job_queue_workers = int( kwargs.get( "cluster_job_queue_workers", "3" ) ) self.job_scheduler_policy = kwargs.get("job_scheduler_policy", "FIFO") self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") ) self.job_working_directory = resolve_path( kwargs.get( "job_working_directory", "database/job_working_directory" ), self.root ) diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 5c7d068f861..f82997f532c 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -246,6 +246,18 @@ 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 + 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/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index f3ae60abc5a..7ca8d20233f 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -13,44 +13,28 @@ import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) -no_privilege_msg = "You must have Galaxy administrator privileges to use this feature." - class Admin( BaseController ): - def user_is_admin( self, trans ): - admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) - if not admin_users: - return False - user = trans.get_user() - if not user: - return False - if not user.email in admin_users: - return False - return True @web.expose + @web.require_admin def index( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg 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 ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) return trans.fill_template( '/admin/center.mako' ) @web.expose + @web.require_admin def reload_tool( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg 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 ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) tool_id = params.tool_id self.app.toolbox.reload( tool_id ) @@ -59,9 +43,8 @@ class Admin( BaseController ): # Galaxy Role Stuff @web.expose + @web.require_admin def roles( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -72,9 +55,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def create_role( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -89,9 +71,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def new_role( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) name = params.name description = params.description @@ -108,14 +89,14 @@ class Admin( BaseController ): type=trans.app.model.Role.types.ADMIN ) role.flush() # Add the users - users = listify( params.users ) + users = util.listify( params.users ) for user_id in users: user = galaxy.model.User.get( user_id ) # Create the UserRoleAssociation ura = galaxy.model.UserRoleAssociation( user, role ) ura.flush() # Add the groups - groups = listify( params.groups ) + groups = util.listify( params.groups ) for group_id in groups: group = galaxy.model.Group.get( group_id ) # Create the GroupRoleAssociation @@ -124,9 +105,8 @@ class Admin( BaseController ): msg = "The new role has been created with %s associated users and %s associated groups" % ( str( len( users ) ), str( len( groups ) ) ) trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def role( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -180,12 +160,11 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def role_members_edit( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) role = galaxy.model.Role.get( int( params.role_id ) ) - in_users = [ trans.app.model.User.get( x ) for x in listify( params.in_users ) ] + 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: @@ -200,15 +179,14 @@ class Admin( BaseController ): if role == dhp.role: dhp.delete() dhp.flush() - in_groups = [ trans.app.model.Group.get( x ) for x in listify( params.in_groups ) ] + 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 = "The role has been updated with %s associated users and %s associated groups" % ( str( len( in_users ) ), str( len( in_groups ) ) ) trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def mark_role_deleted( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) role = galaxy.model.Role.get( int( params.role_id ) ) role.deleted = True @@ -216,9 +194,8 @@ class Admin( BaseController ): msg = "The role has been marked as deleted." trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def deleted_roles( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -242,9 +219,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def undelete_role( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) role = galaxy.model.Role.get( int( params.role_id ) ) role.deleted = False @@ -252,9 +228,8 @@ class Admin( BaseController ): msg = "The role has been marked as not deleted." trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def purge_role( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) role = galaxy.model.Role.get( int( params.role_id ) ) # Delete UserRoleAssociations @@ -285,9 +260,8 @@ class Admin( BaseController ): # Galaxy Group Stuff @web.expose + @web.require_admin def groups( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -311,9 +285,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def create_group( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -329,9 +302,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def new_group( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) name = params.name if not name: @@ -345,7 +317,7 @@ class Admin( BaseController ): group = galaxy.model.Group( name ) group.flush() # Add the members - members = listify( params.members ) + members = util.listify( params.members ) for user_id in members: user = galaxy.model.User.get( user_id ) # Create the UserGroupAssociation @@ -365,9 +337,8 @@ class Admin( BaseController ): msg = "The new group has been created with %s members and %s associated roles" % ( str( len( members ) ), str( len( roles ) ) ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def group_members_edit( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -382,12 +353,11 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def update_group_members( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group_id = int( params.group_id ) - members = listify( params.members ) + members = util.listify( params.members ) group = galaxy.model.Group.get( group_id ) # This is tricky since we have default association tables with # records referring to members of this group. Because of this, @@ -411,9 +381,8 @@ class Admin( BaseController ): # TODO: We probably don't want the following 2 methods since managing roles should be # restricted to the Role page due to private roles and rules governing them @web.expose + @web.require_admin def group_roles_edit( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -428,12 +397,11 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def update_group_roles( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group_id = int( params.group_id ) - roles = listify( params.roles ) + roles = util.listify( params.roles ) group = galaxy.model.Group.get( group_id ) # This is tricky since we have default association tables with # records referring to members of this group. Because of this, @@ -455,9 +423,8 @@ class Admin( BaseController ): msg = "Group updated with a total of %s associated roles" % len( roles ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def mark_group_deleted( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group = galaxy.model.Group.get( int( params.group_id ) ) group.deleted = True @@ -465,9 +432,8 @@ class Admin( BaseController ): msg = "The group has been marked as deleted." trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def deleted_groups( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -491,9 +457,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def undelete_group( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group = galaxy.model.Group.get( int( params.group_id ) ) group.deleted = False @@ -501,9 +466,8 @@ class Admin( BaseController ): msg = "The group has been marked as not deleted." trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def purge_group( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) group = galaxy.model.Group.get( int( params.group_id ) ) # Delete UserGroupAssociations @@ -522,9 +486,8 @@ class Admin( BaseController ): # Galaxy User Stuff @web.expose + @web.require_admin def users( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -545,9 +508,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def user( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) user_id = params.user_id msg = params.msg @@ -568,9 +530,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def user_groups_edit( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -589,12 +550,11 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def update_user_groups( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) user_id = int( params.user_id ) - groups = listify( params.groups ) + groups = util.listify( params.groups ) user = galaxy.model.User.get( user_id ) # First remove existing UserGroupAssociations that are not in the received groups param for uga in user.groups: @@ -613,9 +573,8 @@ class Admin( BaseController ): # Galaxy Library Stuff @web.expose + @web.require_admin def library_browser( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -629,9 +588,8 @@ class Admin( BaseController ): messagetype=messagetype ) libraries = library_browser @web.expose + @web.require_admin def library( self, trans, id=None, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -697,9 +655,8 @@ class Admin( BaseController ): msg = 'The library and all of its contents have been marked deleted' return trans.response.send_redirect( web.url_for( action='library_browser', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def deleted_libraries( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -712,9 +669,8 @@ class Admin( BaseController ): msg=msg, messagetype=messagetype ) @web.expose + @web.require_admin def undelete_library( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) library = galaxy.model.Library.get( int( params.id ) ) def undelete_folder( library_folder ): @@ -731,9 +687,8 @@ class Admin( BaseController ): msg = "The library and all of its contents have been marked not deleted" return trans.response.send_redirect( web.url_for( action='library_browser', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def purge_library( self, trans, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) library = galaxy.model.Library.get( int( params.id ) ) def purge_folder( library_folder ): @@ -759,9 +714,8 @@ class Admin( BaseController ): msg = "The library and all of its contents have been purged, datasets will be removed from disk via the cleanup_datasets script" return trans.response.send_redirect( web.url_for( action='deleted_libraries', msg=msg, messagetype='done' ) ) @web.expose + @web.require_admin def folder( self, trans, id, **kwd ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -819,9 +773,8 @@ class Admin( BaseController ): msg = 'The 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=msg, messagetype='done' ) ) @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 not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) if isinstance( dbkey, list ): last_used_build = dbkey[0] else: @@ -919,7 +872,7 @@ class Admin( BaseController ): space_to_tab = True roles = [] role_ids = params.get( 'roles', [] ) - for role_id in listify( role_ids ): + for role_id in util.listify( role_ids ): roles.append( galaxy.model.Role.get( role_id ) ) temp_name = "" data_list = [] @@ -1035,7 +988,7 @@ class Admin( BaseController ): # The user clicked the Save button on the 'Associate With Roles' form permissions = {} for k, v in trans.app.model.Dataset.permitted_actions.items(): - in_roles = [ trans.app.model.Role.get( x ) for x in listify( p.get( k + '_in', [] ) ) ] + 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_dataset_permissions( lda.dataset, permissions ) lda.dataset.refresh() @@ -1115,7 +1068,7 @@ class Admin( BaseController ): #p = util.Params( kwd ) permissions = {} for k, v in trans.app.model.Dataset.permitted_actions.items(): - in_roles = [ trans.app.model.Role.get( x ) for x in listify( params.get( k + '_in', [] ) ) ] + 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_dataset_permissions( lfda.dataset, permissions ) @@ -1191,10 +1144,9 @@ class Admin( BaseController ): # 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. - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) @@ -1202,7 +1154,7 @@ class Admin( BaseController ): 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=msg, messagetype='error' ) ) - dataset_ids = listify( params.dataset_ids ) + 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=msg, messagetype=messagetype ) ) elif params.action == 'delete': @@ -1218,9 +1170,8 @@ class Admin( BaseController ): else: trans.response.send_redirect( web.url_for( action='library_browser', msg=msg, messagetype=messagetype ) ) @web.expose + @web.require_admin def delete_dataset( self, trans, id=None, **kwd): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) if id: # id is a LibraryFolderDatasetAssociation.id lfda = trans.app.model.LibraryFolderDatasetAssociation.get( id ) @@ -1238,9 +1189,8 @@ class Admin( BaseController ): 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 not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) if self.app.memdump is None: return trans.show_error_message( "Memdump is not enabled (set use_memdump = True in universe_wsgi.ini)" ) heap = self.app.memdump.get() @@ -1278,14 +1228,3 @@ class Admin( BaseController ): breadcrumb += ".theone" heap = heap.theone return trans.fill_template( '/admin/memdump.mako', heap = heap, ids = ids, sorts = sorts, breadcrumb = breadcrumb, msg = msg ) - -def listify( item, return_none=False ): - """ - Since single params are not a single item list - """ - if item is None: - return [] - elif isinstance( item, list ): - return item - else: - return [ item ] diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index eb6ca6f4b90..5d7949ed347 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -25,7 +25,10 @@ class RootController( BaseController ): @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 ): @@ -51,6 +54,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 ) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 075474869b1..695bda82dd1 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -9,6 +9,17 @@ 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 @@ -75,6 +86,10 @@ class User( BaseController ): m0 = trans.app.memory_usage.memory() 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() if not user: @@ -87,33 +102,50 @@ class User( BaseController ): 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_ok_message( "Now logged in as " + user.email, refresh_frames=refresh_frames ) if trans.app.memory_usage: m1 = trans.app.memory_usage.memory( m0, pretty=True ) log.info( "End of user/login, memory used increased by %s" % m1 ) - return trans.show_form( - web.FormBuilder( web.url_for(), "Login", submit_text="Login" ) - .add_text( "email", "Email address", value=email, error=email_error ) + 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.memory_usage: # Keep track of memory usage m0 = trans.app.memory_usage.memory() + 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() if trans.app.memory_usage: m1 = trans.app.memory_usage.memory( m0, pretty=True ) log.info( "End of user/logout, memory used increased by %s" % m1 ) - return trans.show_ok_message( "You are no longer logged in", refresh_frames=['masthead', 'history'] ) + return trans.show_ok_message( "You are no longer logged in.", 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.memory_usage: # Keep track of memory usage m0 = trans.app.memory_usage.memory() + 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 local 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: @@ -130,29 +162,36 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() - trans.app.security_agent.setup_new_user( user ) - trans.handle_user_login( user ) - trans.log_event( "User created a new account" ) - trans.log_event( "User logged in" ) + if trans.user_is_admin(): + trans.app.security_agent.create_private_user_role( user ) + trans.app.security_agent.user_set_default_permissions( user ) + trans.log_event( "Admin created a new account" ) + msg = 'Created account ' + user.email + else: + trans.app.security_agent.setup_new_user( user ) + trans.handle_user_login( user ) + trans.log_event( "User created a new account" ) + trans.log_event( "User logged in" ) + msg = 'Now logged in as ' + user.email #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(): - return trans.show_warn_message( "Now logged in as " + user.email+". However, subscribing to the mailing list has failed.", refresh_frames=['masthead', 'history'] ) + return trans.show_warn_message( msg + ". However, subscribing to the mailing list has failed.", refresh_frames=refresh_frames ) if trans.app.memory_usage: m1 = trans.app.memory_usage.memory( m0, pretty=True ) log.info( "End of user/create, memory used increased by %s" % m1 ) - return trans.show_ok_message( "Now logged in as " + user.email, refresh_frames=['masthead', 'history'] ) + return trans.show_ok_message( msg, refresh_frames=refresh_frames ) return trans.show_form( web.FormBuilder( web.url_for(), "Create account", submit_text="Create" ) .add_text( "email", "Email address", value=email, error=email_error ) - .add_password( "password", "Password", value='', error=password_error ) - .add_password( "confirm", "Confirm password", value='', error=confirm_error ) + .add_password( "password", "Password", value='', error=password_error ) + .add_password( "confirm", "Confirm password", value='', error=confirm_error ) .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() user = trans.get_user() @@ -167,11 +206,11 @@ 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( diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index c415c9c023c..ff51ec616c1 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -60,6 +60,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 an administrator to access this feature, and no administrators are set in the Galaxy configuration." ) + user = trans.get_user() + if not user: + return trans.show_error_message( "You must be an administrator to access this feature, and currently you are not logged in." ) + 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 ): @@ -117,6 +130,8 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): self.workflow_building_mode = False # Always have a valid galaxy session self.__ensure_valid_session() + if self.app.config.require_login: + self.__ensure_logged_in_user( environ ) @property def sa_session( self ): """ @@ -240,6 +255,18 @@ 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' ), + ) + if self.galaxy_session.user is None and environ['PATH_INFO'] 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 @@ -369,7 +396,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 @@ -415,12 +448,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. 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/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($("
            Galaxy${brand}
            - View: - + analysis + | workflow + %if admin_user == "true": + | admin %endif - >analysis - | workflow - %if admin_user == "true": - | admin - %endif - - -     + + +     + %endif Info: report bugs | wiki @@ -51,10 +53,17 @@ %else: %if t.user: Logged in as ${t.user.email}: manage - | logout + %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/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 430d5e4a53a..90203df798c 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -14,37 +14,17 @@ threadpool_workers = 10 # Specifies the factory for the universe WSGI application paste.app_factory = galaxy.web.buildapp:app_factory -log_level = DEBUG -# Log memory usage -log_memory_usage = False - -# Log events -log_events = True - -# Should jobs be tracked through the database, rather than in memory -## track_jobs_in_database = true - -# Number of concurrent jobs to run (local runner) -local_job_queue_workers = 5 - -# Job scheduling policy to be used. -# module/package name and classname should be in "module:classname" format. -# Comment / uncomment the following policies depending upon which is to be used. -#job_scheduler_policy = FIFO -job_scheduler_policy = galaxy.jobs.schedulingpolicy.roundrobin:UserRoundRobin - -# Job queue cleanup interval in minutes. Currently only used by RoundRobin -job_queue_cleanup_interval = 30 - -# Database connection +# By default, Galaxy uses a SQLite database found here database_file = database/universe.sqlite -# You may use a SQLAlchemy connection string to specify an external database instead -## database_connection = postgres:///galaxy -## database_engine_option_echo = true -## database_engine_option_echo_pool = true -## database_engine_option_pool_size = 10 -## database_engine_option_max_overflow = 20 + +# You may use a SQLAlchemy connection string to specify an external database +# instead. PostgreSQL and MySQL are supported. +#database_connection = postgres:///galaxy +#database_engine_option_echo = true +#database_engine_option_echo_pool = true +#database_engine_option_pool_size = 10 +#database_engine_option_max_overflow = 20 # Where dataset files are saved file_path = database/files @@ -70,25 +50,9 @@ session_secret = changethisinproduction # Galaxy session security id_secret = changethisinproductiontoo -# Use user provided in an upstream server's $REMOTE_USER variable -## use_remote_user = False -# If use_remote_user is enabled and your external authentication -# method just returns bare usernames, set a default mail domain -## remote_user_maildomain = example.org - -# Configuration for debugging middleware -debug = true -use_lint = false - -# NEVER enable this on a public site (even test or QA) -use_interactive = true - -# Admin Users - this should be a comma-separated list of valid Galaxy users -#admin_users = user1@bx.psu.edu,user2@bx.psu.edu - # Files in directories under this directory can be directly imported through # the library admin's "add dataset" tool -## library_import_dir = /var/opt/galaxy/import +#library_import_dir = /var/opt/galaxy/import # path to sendmail sendmail_path = /usr/sbin/sendmail @@ -96,15 +60,6 @@ sendmail_path = /usr/sbin/sendmail # Address to join mailing list mailing_join_addr = galaxy-user-join@bx.psu.edu -# Write thread status periodically to 'heartbeat.log' (careful, uses disk space rapidly!) -## use_heartbeat = True - -# Enable the memory debugging interface (careful, negatively impacts server performance) -## use_memdump = True - -# Profiling middleware (cProfile based) -## use_profile = True - # For use by 'report this error' link on error-state datasets #smtp_server = smtp.example.org #error_email_to = galaxy-bugs@example.org @@ -136,7 +91,72 @@ static_style_dir = %(here)s/static/june_2007_style/blue #wiki_url = /path/to/my/local/wiki #bugs_email = mailto:galaxy-bugs@example.org -# ---- Job Runners ---------------------------------------------------------- +# ---- Logging and Debugging ------------------------------------------------ + +# Verbosity of log messages +log_level = DEBUG + +# Log memory usage +log_memory_usage = False + +# Log events +log_events = True + +# Configuration for debugging middleware +debug = True +use_lint = False + +# Interactive debugging - NEVER enable this on a public site +use_interactive = True + +# Write thread status periodically to 'heartbeat.log' (careful, uses disk space rapidly!) +#use_heartbeat = False + +# Enable the memory debugging interface (careful, negatively impacts server performance) +#use_memdump = False + +# Profiling middleware (cProfile based) +#use_profile = False + +# ---- Users and Security --------------------------------------------------- + +# User authentication can be delegated to an upstream proxy server (usually +# Apache). This is explained on the Galaxy wiki: +# +# http://g2.trac.bx.psu.edu/wiki/HowToInstall/ApacheProxy + +# Use user provided in an upstream server's $REMOTE_USER variable +#use_remote_user = False + +# If use_remote_user is enabled and your external authentication +# method just returns bare usernames, set a default mail domain +#remote_user_maildomain = example.org + +# this should be a comma-separated list of valid Galaxy users +#admin_users = user1@example.org,user2@example.org + +# Force everyone to log in (disable anonymous access) +#require_login = False + +# Can users register new accounts? +#allow_user_creation = True + +# ---- Job Execution -------------------------------------------------------- + +# Number of concurrent jobs to run (local job runner) +#local_job_queue_workers = 5 + +# Should jobs be tracked through the database, rather than in memory +#track_jobs_in_database = False + +# Job scheduling policy to be used. +# module/package name and classname should be in "module:classname" format. +# Comment / uncomment the following policies depending upon which is to be used. +#job_scheduler_policy = FIFO +job_scheduler_policy = galaxy.jobs.schedulingpolicy.roundrobin:UserRoundRobin + +# Job queue cleanup interval in minutes. Currently only used by RoundRobin +job_queue_cleanup_interval = 30 # Clustering Galaxy is not a straightforward process and requires a lot of # pre-configuration. See the ClusteringGalaxy Wiki before attempting to set @@ -147,23 +167,21 @@ static_style_dir = %(here)s/static/june_2007_style/blue # If running normally (without a cluster), do not change anything in this # section. -# start_job_runners: Comma-separated list of job runners to start. local is -# always started. If left commented, no jobs will be run on the cluster, even -# if a cluster URL is explicitly defined in the [galaxy:tool_runners] section -# below. The runners currently available are 'pbs' and 'sge'. +# Comma-separated list of job runners to start. local is always started. If +# left commented, no jobs will be run on the cluster, even if a cluster URL is +# explicitly defined in the [galaxy:tool_runners] section below. The runners +# currently available are 'pbs' and 'sge'. #start_job_runners = pbs -# default_cluster_job_runner: The URL for the default runner to use when a tool -# doesn't explicity define a runner below. For help on the cluster URL format, -# see the ClusteringGalaxy Wiki. Leave commented if not using a cluster job -# runner. +# The URL for the default runner to use when a tool doesn't explicity define a +# runner below. For help on the cluster URL format, see the ClusteringGalaxy +# Wiki. Leave commented if not using a cluster job runner. #default_cluster_job_runner = pbs:/// -# cluster_job_queue_workers: The cluster runners have their own thread pools -# used to prepare and finish jobs (so that these operations do not block normal -# queue operation). The value here is the number of worker threads available -# to each runner. -#cluster_job_queue_workers = 5 +# The cluster runners have their own thread pools used to prepare and finish +# jobs (so that these operations do not block normal queue operation). The +# value here is the number of worker threads available to each runner. +#cluster_job_queue_workers = 3 # The PBS options are described in detail in the Galaxy Configuration section of # the ClusteringGalaxy Wiki, and are only necessary when using file staging. From e3f07d5f4502fdc58a0465a9846218dc9967bc4d Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 20 Nov 2008 17:00:34 -0500 Subject: [PATCH 108/267] More bug fixes, functional tests for data security and libraries. --- lib/galaxy/web/controllers/admin.py | 7 +- .../dataset_security/deleted_groups.mako | 2 +- .../admin/dataset_security/deleted_roles.mako | 8 +- .../admin/dataset_security/group_create.mako | 10 +-- .../dataset_security/group_members_edit.mako | 7 +- .../dataset_security/group_roles_edit.mako | 10 +-- templates/admin/dataset_security/groups.mako | 10 +-- templates/admin/dataset_security/role.mako | 12 +-- templates/admin/dataset_security/roles.mako | 8 +- templates/admin/dataset_security/user.mako | 4 +- .../dataset_security/user_groups_edit.mako | 7 +- templates/admin/dataset_security/users.mako | 28 +++---- templates/admin/library/new_dataset.mako | 2 +- templates/dataset/edit_attributes.mako | 2 +- templates/dataset/security_common.mako | 4 +- test/base/twilltestcase.py | 82 ++++++++++++++----- .../functional/test_security_and_libraries.py | 76 +++++++++++------ 17 files changed, 177 insertions(+), 102 deletions(-) diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 7ca8d20233f..8593d0f65f5 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -77,7 +77,7 @@ class Admin( BaseController ): name = params.name description = params.description if not name or not description: - msg = "Please enter a name and a description" + msg = "Enter a valid name and a description" trans.response.send_redirect( web.url_for( action='create_role', msg=msg, messagetype='error' ) ) elif trans.app.model.Role.filter_by( name=name ).first(): msg = "A role with that name already exists" @@ -307,7 +307,7 @@ class Admin( BaseController ): params = util.Params( kwd ) name = params.name if not name: - msg = "Please enter a name" + msg = "Enter a valid name" trans.response.send_redirect( web.url_for( action='create_group', msg=msg, messagetype='error' ) ) elif trans.app.model.Group.filter_by( name=name ).first(): msg = "A group with that name already exists" @@ -1087,9 +1087,8 @@ class Admin( BaseController ): 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 ): - if not self.user_is_admin( trans ): - return trans.show_error_message( no_privilege_msg ) try: folder = trans.app.model.LibraryFolder.get( folder_id ) except: diff --git a/templates/admin/dataset_security/deleted_groups.mako b/templates/admin/dataset_security/deleted_groups.mako index 422c80073bd..1e0751e3dd2 100644 --- a/templates/admin/dataset_security/deleted_groups.mako +++ b/templates/admin/dataset_security/deleted_groups.mako @@ -26,7 +26,7 @@
              %for role in roles: -
            • ${role.name}
            • +
            • ${role.description}
            • %endfor
            %if not anchored: diff --git a/templates/admin/dataset_security/deleted_roles.mako b/templates/admin/dataset_security/deleted_roles.mako index 9975997d92e..64153e05e50 100644 --- a/templates/admin/dataset_security/deleted_roles.mako +++ b/templates/admin/dataset_security/deleted_roles.mako @@ -9,7 +9,7 @@
            - ${role.name} + ${role.description}
            Undelete @@ -78,10 +78,10 @@ groups = role_tuple[1] users = role_tuple[2] %> - %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + %if render_quick_find and not role.description.upper().startswith( curr_anchor ): <% anchored = False %> %endif - %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if render_quick_find and role.description.upper().startswith( curr_anchor ): %if not anchored: ${render_row( role, groups, users, ctr, anchored, curr_anchor )} <% anchored = True %> @@ -90,7 +90,7 @@ %endif %elif render_quick_find: %for anchor in anchors[ anchor_loc: ]: - %if role.name.upper().startswith( anchor ): + %if role.description.upper().startswith( anchor ): %if not anchored: <% curr_anchor = anchor %> ${render_row( role, groups, users, ctr, anchored, curr_anchor )} diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako index a137eb33ebb..ef9bbd407af 100644 --- a/templates/admin/dataset_security/group_create.mako +++ b/templates/admin/dataset_security/group_create.mako @@ -22,9 +22,9 @@
            %if not anchored: - ${role.name} + ${role.description} %else: - ${role.name} + ${role.description} %endif
            %for ctr, role in enumerate( roles ): - %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + %if render_quick_find and not role.description.upper().startswith( curr_anchor ): <% anchored = False %> %endif - %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if render_quick_find and role.description.upper().startswith( curr_anchor ): %if not anchored: ${render_role_row( role, ctr, anchored, curr_anchor )} <% anchored = True %> @@ -89,7 +89,7 @@ %endif %elif render_quick_find: %for anchor in anchors[ anchor_loc: ]: - %if role.name.upper().startswith( anchor ): + %if role.description.upper().startswith( anchor ): %if not anchored: <% curr_anchor = anchor %> ${render_role_row( role, ctr, anchored, curr_anchor )} diff --git a/templates/admin/dataset_security/group_members_edit.mako b/templates/admin/dataset_security/group_members_edit.mako index eb669d0c125..67575a415a2 100644 --- a/templates/admin/dataset_security/group_members_edit.mako +++ b/templates/admin/dataset_security/group_members_edit.mako @@ -14,6 +14,10 @@ %else: ${user.email} %endif + %if not anchored: + + + %endif @@ -41,7 +45,7 @@ curr_anchor = 'A' %> -
            + Jump to letter: %for a in anchors: | ${a} @@ -94,4 +98,3 @@
            %endif - diff --git a/templates/admin/dataset_security/group_roles_edit.mako b/templates/admin/dataset_security/group_roles_edit.mako index 69acf372fe3..f23bcc36a2c 100644 --- a/templates/admin/dataset_security/group_roles_edit.mako +++ b/templates/admin/dataset_security/group_roles_edit.mako @@ -10,9 +10,9 @@ %endif %if check: - ${role.name} + ${role.description} %else: - ${role.name} + ${role.description} %endif @@ -60,10 +60,10 @@ %> %endif %endfor - %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + %if render_quick_find and not role.description.upper().startswith( curr_anchor ): <% anchored = False %> %endif - %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if render_quick_find and role.description.upper().startswith( curr_anchor ): %if not anchored: ${render_row( role, ctr, anchored, curr_anchor, check )} <% anchored = True %> @@ -72,7 +72,7 @@ %endif %elif render_quick_find: %for anchor in anchors[ anchor_loc: ]: - %if role.name.upper().startswith( anchor ): + %if role.description.upper().startswith( anchor ): %if not anchored: <% curr_anchor = anchor %> ${render_row( role, ctr, anchored, curr_anchor, check )} diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako index 391aa727b1d..af8e7add914 100644 --- a/templates/admin/dataset_security/groups.mako +++ b/templates/admin/dataset_security/groups.mako @@ -23,9 +23,7 @@ @@ -34,9 +32,9 @@ %for role in roles:
          • %if not role.type == galaxy.model.Role.types.PRIVATE: - ${role.name} + ${role.description} %else: - ${role.name} + ${role.description} %endif
          • %endfor @@ -76,7 +74,7 @@ curr_anchor = 'A' %> - + Jump to letter: %for a in anchors: | ${a} diff --git a/templates/admin/dataset_security/role.mako b/templates/admin/dataset_security/role.mako index 63e9c450986..7d87f6fac90 100644 --- a/templates/admin/dataset_security/role.mako +++ b/templates/admin/dataset_security/role.mako @@ -48,29 +48,29 @@ $().ready(function() { %endif
            -
            Role '${role.name}'
            +
            Role '${role.description}'
            - Users associated with '${role.name}'
            + Users associated with '${role.description}'
            ${render_select( "in_users", in_users )}
            - Users not associated with '${role.name}'
            + Users not associated with '${role.description}'
            ${render_select( "out_users", out_users )}
            - Groups associated with '${role.name}'
            + Groups associated with '${role.description}'
            ${render_select( "in_groups", in_groups )}
            - Groups not associated with '${role.name}'
            + Groups not associated with '${role.description}'
            ${render_select( "out_groups", out_groups )}
            @@ -84,7 +84,7 @@ $().ready(function() {

            %if len( library_dataset_actions ) > 0: -

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

            +

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

            %endif %for ctr, role in enumerate( roles ): - %if render_quick_find and not role.name.upper().startswith( curr_anchor ): + %if render_quick_find and not role.description.upper().startswith( curr_anchor ): <% anchored = False %> %endif - %if render_quick_find and role.name.upper().startswith( curr_anchor ): + %if render_quick_find and role.description.upper().startswith( curr_anchor ): %if not anchored: ${render_row( role, ctr, anchored, curr_anchor )} <% anchored = True %> @@ -96,7 +96,7 @@ %endif %elif render_quick_find: %for anchor in anchors[ anchor_loc: ]: - %if role.name.upper().startswith( anchor ): + %if role.description.upper().startswith( anchor ): %if not anchored: <% curr_anchor = anchor %> ${render_row( role, ctr, anchored, curr_anchor )} diff --git a/templates/admin/dataset_security/user.mako b/templates/admin/dataset_security/user.mako index aff1479944a..90b1c2b61b3 100644 --- a/templates/admin/dataset_security/user.mako +++ b/templates/admin/dataset_security/user.mako @@ -9,9 +9,9 @@ <%def name="render_role( user, role )">
          • %if not role.type == galaxy.model.Role.types.PRIVATE: - ${role.name} + ${role.description} %else: - ${role.name} + ${role.description} %endif
            diff --git a/templates/admin/dataset_security/user_groups_edit.mako b/templates/admin/dataset_security/user_groups_edit.mako index fa27a1655d7..e10eb97e10a 100644 --- a/templates/admin/dataset_security/user_groups_edit.mako +++ b/templates/admin/dataset_security/user_groups_edit.mako @@ -14,6 +14,10 @@ %else: ${group.name} %endif + %if not anchored: + + + %endif @@ -41,7 +45,7 @@ curr_anchor = 'A' %>
          • -
            diff --git a/templates/admin/dataset_security/roles.mako b/templates/admin/dataset_security/roles.mako index 0e46ca82162..916bf0dd63a 100644 --- a/templates/admin/dataset_security/roles.mako +++ b/templates/admin/dataset_security/roles.mako @@ -13,7 +13,7 @@
            - ${role.name} + ${role.description} Groups
            + Jump to letter: %for a in anchors: | ${a} @@ -94,4 +98,3 @@
            %endif - diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako index 8cf1434b7b4..8dc7f58367a 100644 --- a/templates/admin/dataset_security/users.mako +++ b/templates/admin/dataset_security/users.mako @@ -29,7 +29,7 @@ %if not anchored: @@ -97,19 +97,19 @@ %endif %elif render_quick_find: %for anchor in anchors[ anchor_loc: ]: - %if user.email.upper().startswith( anchor ): - %if not anchored: - <% curr_anchor = anchor %> - ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} - <% anchored = True %> - %else: - ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} - %endif - <% - anchor_loc = anchors.index( anchor ) - break - %> - %endif + %if user.email.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( user, groups, roles, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif %endfor %else: ${render_row( user, groups, roles, ctr, True, '' )} diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 074d6e6f7ca..3888d629aee 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -93,7 +93,7 @@
            diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index fdee84f6a87..e5d44cbce6b 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -173,7 +173,7 @@
          • ${action.description}
            • %for role in roles: -
            • ${role.name}
            • +
            • ${role.description}
            • %endfor
            %endif diff --git a/templates/dataset/security_common.mako b/templates/dataset/security_common.mako index 2ebe99d157e..234e8b0a369 100644 --- a/templates/dataset/security_common.mako +++ b/templates/dataset/security_common.mako @@ -11,7 +11,7 @@ Roles associated:

            @@ -20,7 +20,7 @@ Roles not associated:

            diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index ab51a378605..08da005df79 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -368,19 +368,23 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( "User with that email already exists" ) except: self.check_page_for_string( "Now logged in as %s" %email ) - self.home() #Reset our URL for future tests + self.home() + # Make sure a new private role was created for the user + self.visit_page( "user/set_default_permissions" ) + self.check_page_for_string( email ) + self.home() def login( self, email='test@bx.psu.edu', password='testuser'): # test@bx.psu.edu is configured as an admin user self.create( email=email, password=password, confirm=password ) self.visit_page( "user/login?email=%s&password=%s" % (email, password) ) self.check_page_for_string( "Now logged in as %s" %email ) - self.home() #Reset our URL for future tests + self.home() def logout( self ): self.visit_page( "user/logout" ) self.check_page_for_string( "You are no longer logged in" ) - self.home() #Reset our URL for future tests + self.home() # Functions associated with browsers, cookies, HTML forms and page visits def check_page_for_string( self, patt ): @@ -548,26 +552,44 @@ class TwillTestCase( unittest.TestCase ): self.assertNotEqual(count, maxiter) # Dataset Security stuff - def create_role( self, name='New Test Role', description="Very cool new test role", user_ids=[], group_ids=[] ): + def create_role( self, name='New Test Role', description="Very cool new test role", user_ids=[], group_ids=[], private_role='' ): """Create a new role""" self.visit_url( "%s/admin/create_role" % self.url ) form = tc.show() self.check_page_for_string( "Create Role" ) - try: + try: + # Attempt to submit a blank form + tc.fv( "1", "name", "" ) + tc.fv( "1", "description", "" ) + tc.submit( "create_role_button" ) + self.last_page() + self.check_page_for_string( "Enter a valid name and a description" ) tc.fv( "1", "name", name ) tc.fv( "1", "description", description ) for user_id in user_ids: - tc.fv( "1", "3", user_id ) # form field 3 is the check box named 'users' + tc.fv( "1", "users", user_id ) for group_id in group_ids: - tc.fv( "1", "4", group_id ) # form field 4 is the check box named 'groups' + tc.fv( "1", "groups", group_id ) tc.submit( "create_role_button" ) + self.last_page() + check_str = 'The new role has been created with %d associated users and %d associated groups' % ( len( user_ids ), len( group_ids ) ) + self.check_page_for_string( check_str ) + if private_role: + # Make sure no private roles are displayed + try: + self.check_page_for_string( private_role ) + errmsg = 'Private role %s displayed on Non-private Roles page' % private_role + raise AssertionError( errmsg ) + except AssertionError: + # Reaching here is the behavior we want since no private roles should be displayed + pass except AssertionError, err: self.home() errmsg = 'Exception caught attempting to create role: %s' % str( err ) raise AssertionError( errmsg ) self.home() self.visit_page( "admin/roles" ) - self.check_page_for_string( name ) + self.check_page_for_string( description ) self.home() def mark_role_deleted( self, role_id ): """Mark a role as deleted""" @@ -594,12 +616,25 @@ class TwillTestCase( unittest.TestCase ): self.visit_url( "%s/admin/create_group" % self.url ) form = tc.show() self.check_page_for_string( "Create Group" ) - try: + # Make sure no private roles are displayed + try: + self.check_page_for_string( 'Private Role for' ) + errmsg = 'Private role displayed on Create Group page' + raise AssertionError( errmsg ) + except AssertionError: + # Reaching here is the behavior we want since no private roles should be displayed + pass + try: + # Attempt to submit a blank form + tc.fv( "1", "name", "" ) + tc.submit( "create_group_button" ) + self.last_page() + self.check_page_for_string( "Enter a valid name" ) tc.fv( "1", "name", name ) for user_id in user_ids: - tc.fv( "1", "2", user_id ) # form field 2 is the check box named 'members' + tc.fv( "1", "members", user_id ) for role_id in role_ids: - tc.fv( "1", "3", role_id ) # form field 3 is the check box named 'roles' + tc.fv( "1", "roles", role_id ) tc.submit( "create_group_button" ) except AssertionError, err: self.home() @@ -680,8 +715,14 @@ class TwillTestCase( unittest.TestCase ): self.visit_url( "%s/admin/library?rename=True&id=%s" % ( self.url, library_id ) ) self.last_page() self.check_page_for_string( 'Edit library name and description' ) - tc.fv( "1", "name", name ) # form field 1 is the field named name... - tc.fv( "1", "description", description ) # form field 2 is the field named description... + # Attempt to submit a blank form + tc.fv( "1", "name", "" ) + tc.fv( "1", "description", "" ) + tc.submit( "rename_library_button" ) + self.last_page() + self.check_page_for_string( 'Enter a valid name' ) + tc.fv( "1", "name", name ) + tc.fv( "1", "description", description ) if root_folder: tc.fv( "1", "root_folder", root_folder ) tc.submit( "rename_library_button" ) @@ -754,8 +795,9 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create add a dataset to a folder: %s' % str( err ) ) self.home() - def add_datasets_from_library_dir( self, folder_id, extension='auto', dbkey='hg18', roles=[] ): + def add_datasets_from_library_dir( self, folder_id, extension='auto', dbkey='hg18', roles_tuple=[] ): """Add a directory of datasets to a folder""" + # roles is a list of tuples: [ ( role_id, role_description ) ] try: self.visit_url( "%s/admin/dataset?folder_id=%s" % ( self.url, folder_id ) ) self.last_page() @@ -765,8 +807,8 @@ class TwillTestCase( unittest.TestCase ): tc.fv( "1", "dbkey", dbkey ) library_dir = "%s" % self.file_dir tc.fv( "1", "server_dir", "library" ) - for role_id in roles: - tc.fv( "1", "roles", role_id ) + for role_tuple in roles_tuple: + tc.fv( "1", "roles", role_tuple[0] ) tc.submit( "new_dataset_button" ) self.last_page() self.check_page_for_string( '3 new datasets added to the library ( each is selected below )' ) @@ -776,14 +818,14 @@ class TwillTestCase( unittest.TestCase ): tc.submit( "action_on_datasets_button" ) self.last_page() self.check_page_for_string( '( 3 of them )' ) - self.check_page_for_string( 'New Test Role' ) - self.check_page_for_string( 'Another Test Role' ) - tc.find( "update_roles" ) + for role_tuple in roles_tuple: + self.check_page_for_string( role_tuple[1] ) # NOTE: we cannot submit the form because of a bug in twill ( it cannot handle select lists # that include no option fields. Since the "manage permissions" and "edit metadata" select # lists have no options ( no roles associated ), submitting the form will throw a - # ParseError: exception. Uncomment the following 3 lines + # ParseError: exception. Uncomment the following 4 lines # when twill fixes this bug... + # tc.find( "update_roles" ) # tc.submit( "update_roles" ) # self.last_page() # self.check_page_for_string( 'Libraries' ) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 0c9e0ace5f6..ae154e79770 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -2,44 +2,45 @@ import galaxy.model from galaxy.model.orm import * from base.twilltestcase import * -security_msg = 'You must have Galaxy administrator privileges to use this feature.' +not_logged_in_security_msg = 'You must be an administrator to access this feature, and currently you are not logged in.' +logged_in_security_msg = 'You must be an administrator to access this feature.' class TestHistory( TwillTestCase ): def test_00_admin_features_when_not_logged_in( self ): """Testing admin_features when not logged in""" self.logout() self.visit_url( "%s/admin" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/reload_tool?tool_id=upload1" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/roles" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/create_role" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/new_role" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/role" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/groups" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/create_group" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/group_members_edit" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/update_group_members" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/users" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/library_browser" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/libraries" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/library" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/folder?id=1&new=True" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/dataset" % self.url ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( not_logged_in_security_msg ) def test_03_login_as_admin( self ): """Testing logging in as an admin user""" self.login( email='test@bx.psu.edu' ) #This is configured as our admin user @@ -55,6 +56,14 @@ class TestHistory( TwillTestCase ): break if not private_role_found: raise AssertionError( "Private role not found for user '%s'" % testuser1.email ) + # Make sure a DefaultUserPermission exists for the user + if not testuser1.default_permissions: + raise AssertionError( 'No DefaultUserPermissions were created for %s when their account was created' % testuser1.email ) + if len( testuser1.default_permissions ) > 1: + raise AssertionError( 'More than 1 DefaultUserPermissions were created for %s when their account was created' % testuser1.email ) + dup = galaxy.model.DefaultUserPermissions.filter( galaxy.model.DefaultUserPermissions.table.c.user_id==testuser1.id ).first() + if not dup.action == 'manage permissions': + raise AssertionError( 'The DefaultUserPermission.action for user "%s" is "%s", but it should be "manage permissions"' % ( testuser1.email, dup.action ) ) self.visit_url( "%s/admin/user?user_id=%s" % ( self.url, testuser1.id ) ) self.check_page_for_string( testuser1.email ) self.home() @@ -64,19 +73,23 @@ class TestHistory( TwillTestCase ): global testuser2 testuser2 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test2@bx.psu.edu' ).first() self.visit_page( "admin" ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( logged_in_security_msg ) + # NOTE: we cannot currently Set DefaultHistoryPermissions for this user + # because of a bug in twill where it is not able to handle select lists that + # include no options... self.logout() self.login( email='test3@bx.psu.edu' ) # This will not be an admin user global testuser3 testuser3 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test3@bx.psu.edu' ).first() self.visit_page( "admin" ) - self.check_page_for_string( security_msg ) + self.check_page_for_string( logged_in_security_msg ) self.logout() def test_06_create_role( self ): """Testing creating new non-private role with 2 members""" self.login( email=testuser1.email ) - self.create_role( user_ids=[ str( testuser1.id ), str( testuser2.id ) ] ) name = 'New Test Role' + description = 'Very cool new test role' + self.create_role( name=name, description=description, user_ids=[ str( testuser1.id ), str( testuser2.id ) ], private_role=testuser1.email ) # Get the role object for later tests global new_test_role new_test_role = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() @@ -107,7 +120,12 @@ class TestHistory( TwillTestCase ): # with the role, then add tests in test_55_purge_role to make sure the association records are deleted # when the role is purged. name = 'Another Test Role' - self.create_role( name=name, user_ids=[ str( testuser1.id ) ], group_ids=[ str( another_test_group.id ) ] ) + description = 'Another cool new test role' + self.create_role( name=name, + description=description, + user_ids=[ str( testuser1.id ) ], + group_ids=[ str( another_test_group.id ) ], + private_role=testuser1.email ) # Get the role object for later tests global another_test_role another_test_role = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() @@ -152,6 +170,17 @@ class TestHistory( TwillTestCase ): """Testing copying a dataset from the current history to a library root folder""" folder = library.root_folder self.add_dataset_to_folder_from_history( str( folder.id ) ) + # Now that we have a history and a dataset, we can test for ActionDatasetRoleAssociation - we're still logged in as testuser1. + # The default setting are "manage permissions" + last_dataset_created = galaxy.model.Dataset.query().order_by( desc( galaxy.model.Dataset.table.c.create_time ) ).first() + adras = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.dataset_id==last_dataset_created.id ).all() + if not adras: + raise AssertionError( 'No ActionDatasetRoleAssociations created for dataset id: %d' % last_dataset_created.id ) + if len( adras ) > 1: + raise AssertionError( 'More than 1 ActionDatasetRoleAssociations created for dataset id: %d' % last_dataset_created.id ) + for adra in adras: + if not adra.action == 'manage permissions': + raise AssertionError( 'ActionDatasetRoleAssociation.action "%s" is not the DefaultHistoryPermission setting, which is "manage permissions"' % str( adra.action ) ) def test_33_add_new_folder( self ): """Testing adding a folder to a library root folder""" root_folder = library.root_folder @@ -166,7 +195,8 @@ class TestHistory( TwillTestCase ): self.check_page_for_string( "New Test Folder" ) def test_36_add_datasets_from_library_dir( self ): """Testing adding several datasets from library directory to sub-folder""" - self.add_datasets_from_library_dir( str( new_test_folder.id ), roles=[ str( new_test_role.id ) ] ) + roles_tuple = [ ( str( new_test_role.id ), new_test_role.description ) ] + self.add_datasets_from_library_dir( str( new_test_folder.id ), roles_tuple=roles_tuple ) def test_39_mark_group_deleted( self ): """Testing marking a group as deleted""" self.visit_page( "admin/groups" ) @@ -178,7 +208,7 @@ class TestHistory( TwillTestCase ): def test_45_mark_role_deleted( self ): """Testing marking a role as deleted""" self.visit_page( "admin/roles" ) - self.check_page_for_string( another_test_role.name ) + self.check_page_for_string( another_test_role.description ) self.mark_role_deleted( str( another_test_role.id ) ) def test_48_undelete_role( self ): """Testing undeleting a deleted role""" From eeeab78b324a39d17b3c6d8835d9ca388c53eb02 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 21 Nov 2008 09:21:39 -0500 Subject: [PATCH 109/267] Fixes for accessing libraries from the analysis view. --- lib/galaxy/web/controllers/library.py | 3 ++- lib/galaxy/web/framework/__init__.py | 5 +++-- test/functional/test_security_and_libraries.py | 2 +- tools/data_source/access_libraries.xml | 8 ++++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 5db40832306..895e4bf1d25 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -7,7 +7,8 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose def browse( self, trans, **kwd ): - return trans.fill_template( '/library/browser.mako', libraries=trans.app.model.Library.filter_by( deleted=False ).all() ) + libraries=trans.app.model.Library.filter_by( deleted=False ).order_by( trans.app.model.Library.table.c.name ).all() + return trans.fill_template( '/library/browser.mako', libraries=libraries ) index = browse @web.expose def import_datasets( self, trans, import_ids=[], **kwd ): diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index ff51ec616c1..436c91d7668 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -64,10 +64,10 @@ 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 an administrator to access this feature, and no administrators are set in the Galaxy configuration." ) + 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 an administrator to access this feature, and currently you are not logged in." ) + 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 ) @@ -264,6 +264,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): 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' ) ) if self.galaxy_session.user is None and environ['PATH_INFO'] not in allowed_paths: self.response.send_redirect( url_for( controller='root', action='index' ) ) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index ae154e79770..df0af3dedf8 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -2,7 +2,7 @@ import galaxy.model from galaxy.model.orm import * from base.twilltestcase import * -not_logged_in_security_msg = 'You must be an administrator to access this feature, and currently you are not logged in.' +not_logged_in_security_msg = 'You must be logged in as an administrator to access this feature.' logged_in_security_msg = 'You must be an administrator to access this feature.' class TestHistory( TwillTestCase ): diff --git a/tools/data_source/access_libraries.xml b/tools/data_source/access_libraries.xml index 1281386f7ce..d843e9ae52e 100644 --- a/tools/data_source/access_libraries.xml +++ b/tools/data_source/access_libraries.xml @@ -1,7 +1,7 @@ - stored locally - - - + stored locally + + + From e27cf5804be09604e76ed67d3acb4762d3785b3f Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Mon, 24 Nov 2008 12:35:48 -0500 Subject: [PATCH 110/267] Change static paths in library templates and the method used to determine whether a user must log in --- lib/galaxy/web/framework/__init__.py | 2 +- templates/admin/library/new_folder.mako | 2 +- templates/admin/library/new_library.mako | 2 +- templates/admin/library/rename_folder.mako | 2 +- templates/admin/library/rename_library.mako | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 436c91d7668..c982464ad10 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -266,7 +266,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): url_for( controller='user', action='reset_password' ), url_for( controller='library', action='browse' ) ) - if self.galaxy_session.user is None and environ['PATH_INFO'] not in allowed_paths: + if self.galaxy_session.user is None and 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 ): """ diff --git a/templates/admin/library/new_folder.mako b/templates/admin/library/new_folder.mako index b8fca3b1ba6..8ce7207e96a 100644 --- a/templates/admin/library/new_folder.mako +++ b/templates/admin/library/new_folder.mako @@ -8,7 +8,7 @@
            Create a new folder
            -
            +
            diff --git a/templates/admin/library/new_library.mako b/templates/admin/library/new_library.mako index 8b33fad4c70..c514d2965b3 100644 --- a/templates/admin/library/new_library.mako +++ b/templates/admin/library/new_library.mako @@ -8,7 +8,7 @@
            Create a new library
            - +
            diff --git a/templates/admin/library/rename_folder.mako b/templates/admin/library/rename_folder.mako index 349fed2ecbe..8638edf60a1 100644 --- a/templates/admin/library/rename_folder.mako +++ b/templates/admin/library/rename_folder.mako @@ -8,7 +8,7 @@
            Edit folder name and description
            - +
            diff --git a/templates/admin/library/rename_library.mako b/templates/admin/library/rename_library.mako index 95f505e9046..9493ad1a7a3 100644 --- a/templates/admin/library/rename_library.mako +++ b/templates/admin/library/rename_library.mako @@ -8,7 +8,7 @@
            Edit library name and description
            - +
            From 7184560410210e34ffce4227754a15da1c1cf98d Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Mon, 24 Nov 2008 12:44:31 -0500 Subject: [PATCH 111/267] Add user-friendly redirect links when requiring login. --- lib/galaxy/web/controllers/user.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 695bda82dd1..f8251c3057e 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -102,7 +102,10 @@ class User( BaseController ): 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=refresh_frames ) + 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 ) if trans.app.memory_usage: m1 = trans.app.memory_usage.memory( m0, pretty=True ) log.info( "End of user/login, memory used increased by %s" % m1 ) @@ -133,7 +136,10 @@ class User( BaseController ): if trans.app.memory_usage: m1 = trans.app.memory_usage.memory( m0, pretty=True ) log.info( "End of user/logout, memory used increased by %s" % m1 ) - return trans.show_ok_message( "You are no longer logged in.", refresh_frames=refresh_frames ) + 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 ): From 8b3666a0a82cda02b46d3b58e5cc86f1ba92dce6 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 24 Nov 2008 14:44:39 -0500 Subject: [PATCH 112/267] Fixes and functional tests: - move admin method for creating new user account to admin controller - fixes and functional tests for initial settings of DefaultUserPermissions and DefaultHistoryPermissions when accounts and histories created - fixes and functional tests for changing DefaultHistoryPermissions for the current history - fixes and functional tests for Changing DefaultHistoryPermissions for new histories --- lib/galaxy/security/__init__.py | 10 +- lib/galaxy/web/controllers/admin.py | 43 +++- lib/galaxy/web/controllers/root.py | 4 +- lib/galaxy/web/controllers/user.py | 39 +--- lib/galaxy/web/framework/__init__.py | 19 +- templates/admin/dataset_security/users.mako | 8 +- templates/admin/library/dataset.mako | 2 +- test/base/twilltestcase.py | 35 ++- .../functional/test_security_and_libraries.py | 199 ++++++++++++++---- 9 files changed, 262 insertions(+), 97 deletions(-) diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index afdb673c090..e21aa24c759 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -47,12 +47,8 @@ class RBACAgent: raise "Unimplemented Method" def get_private_user_role( self, user ): raise "Unimplemented Method" - def user_set_default_permissions( self, user, permissions = None, history = False, dataset = False ): + def user_set_default_permissions( self, user, permissions={}, history=False, dataset=False ): raise "Unimplemented Method" - def setup_new_user( self, user ): - self.create_private_user_role( user ) - self.user_set_default_permissions( user, history = True, dataset = True ) - #self.associate_components( user=user, group=self.get_public_group() ) def history_set_default_permissions( self, history, permissions=None, dataset=False, bypass_manage_permission=False ): raise "Unimplemented Method" def set_dataset_permissions( self, dataset, permissions ): @@ -169,7 +165,7 @@ class GalaxyRBACAgent( RBACAgent ): else: return None return role - def user_set_default_permissions( self, user, permissions = {}, history = False, dataset = False ): + def user_set_default_permissions( self, user, permissions = {}, history=False, dataset=False ): if user is None: return None if not permissions: @@ -195,7 +191,7 @@ class GalaxyRBACAgent( RBACAgent ): for dup in user.default_permissions: perms[ self.get_action( dup.action ) ].append( dup.role ) return perms - def history_set_default_permissions( self, history, permissions = {}, dataset = False, bypass_manage_permission = False ): + def history_set_default_permissions( self, history, permissions={}, dataset=False, bypass_manage_permission=False ): if not history.user: return None # default permissions on a userless history are none if not permissions: diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 8593d0f65f5..dc72c913abe 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -485,6 +485,47 @@ class Admin( BaseController ): trans.response.send_redirect( web.url_for( action='deleted_groups', msg=msg, messagetype='done' ) ) # Galaxy User Stuff + @web.expose + @web.require_admin + def create_new_user( self, trans, email='', password='', confirm='', subscribe=False ): + 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: + email_error = "Email address exceeds maximum allowable length" + elif trans.app.model.User.filter_by( email=email ).first(): + email_error = "User with that email already exists" + elif len( password ) < 6: + password_error = "Please use a password of at least 6 characters" + elif password != confirm: + confirm_error = "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 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=msg, messagetype=messagetype ) ) + # TODO: make this a mako template + return trans.show_form( + web.FormBuilder( web.url_for(), "Create account", submit_text="Create" ) + .add_text( "email", "Email address", value=email, error=email_error ) + .add_password( "password", "Password", value='', error=password_error ) + .add_password( "confirm", "Confirm password", value='', error=confirm_error ) + .add_input( "checkbox","Subscribe To Mailing List","subscribe", value='subscribe' ) ) + + @web.expose @web.require_admin def users( self, trans, **kwd ): @@ -960,7 +1001,7 @@ class Admin( BaseController ): 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.c.type != trans.app.model.Role.types.PRIVATE ).order_by( trans.app.model.Role.c.name ).all() + roles = trans.app.model.Role.query().order_by( trans.app.model.Role.c.name ).all() return trans.fill_template( '/admin/library/new_dataset.mako', folder_id=folder_id, file_formats=file_formats, diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 5d7949ed347..beb32d121fa 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -800,7 +800,9 @@ class RootController( BaseController ): in_roles = [ in_roles ] in_roles = [ trans.app.model.Role.get( x ) for x in in_roles ] permissions[ trans.app.security_agent.get_action( v.action ) ] = in_roles - trans.app.security_agent.history_set_default_permissions( history, permissions ) + 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: diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index f8251c3057e..9e197c021dd 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -81,9 +81,6 @@ class User( BaseController ): @web.expose def login( self, trans, email='', password='' ): - if trans.app.memory_usage: - # Keep track of memory usage - m0 = trans.app.memory_usage.memory() email_error = password_error = None # Attempt login if trans.app.config.require_login: @@ -102,13 +99,11 @@ class User( BaseController ): 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=refresh_frames ) 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 ) - if trans.app.memory_usage: - m1 = trans.app.memory_usage.memory( m0, pretty=True ) - log.info( "End of user/login, memory used increased by %s" % m1 ) 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, @@ -143,15 +138,12 @@ class User( BaseController ): @web.expose def create( self, trans, email='', password='', confirm='', subscribe=False ): - if trans.app.memory_usage: - # Keep track of memory usage - m0 = trans.app.memory_usage.memory() 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 local Galaxy administrator for an account.' ) + 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: @@ -168,32 +160,23 @@ class User( BaseController ): user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) user.flush() - if trans.user_is_admin(): - trans.app.security_agent.create_private_user_role( user ) - trans.app.security_agent.user_set_default_permissions( user ) - trans.log_event( "Admin created a new account" ) - msg = 'Created account ' + user.email - else: - trans.app.security_agent.setup_new_user( user ) - trans.handle_user_login( user ) - trans.log_event( "User created a new account" ) - trans.log_event( "User logged in" ) - msg = 'Now logged in as ' + user.email + trans.app.security_agent.create_private_user_role( user ) + trans.handle_user_login( user ) + trans.app.security_agent.user_set_default_permissions( user, history=True, dataset=True ) + trans.log_event( "User created a new account" ) + trans.log_event( "User logged in" ) #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(): - return trans.show_warn_message( msg + ". However, subscribing to the mailing list has failed.", refresh_frames=refresh_frames ) - if trans.app.memory_usage: - m1 = trans.app.memory_usage.memory( m0, pretty=True ) - log.info( "End of user/create, memory used increased by %s" % m1 ) - return trans.show_ok_message( msg, refresh_frames=refresh_frames ) + return trans.show_warn_message( "Now logged in as " + user.email+". However, subscribing to the mailing list has failed.", refresh_frames=['masthead', 'history'] ) + return trans.show_ok_message( "Now logged in as " + user.email, refresh_frames=['masthead', 'history'] ) return trans.show_form( web.FormBuilder( web.url_for(), "Create account", submit_text="Create" ) .add_text( "email", "Email address", value=email, error=email_error ) - .add_password( "password", "Password", value='', error=password_error ) - .add_password( "confirm", "Confirm password", value='', error=confirm_error ) + .add_password( "password", "Password", value='', error=password_error ) + .add_password( "confirm", "Confirm password", value='', error=confirm_error ) .add_input( "checkbox","Subscribe To Mailing List","subscribe", value='subscribe' ) ) @web.expose diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index c982464ad10..af93111117f 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -316,20 +316,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 ) + self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session, history ] ) self.__update_session_cookie() def handle_user_logout( self ): """ diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako index 8dc7f58367a..39dad937182 100644 --- a/templates/admin/dataset_security/users.mako +++ b/templates/admin/dataset_security/users.mako @@ -42,14 +42,14 @@

            Users

            + + %if msg: ${render_msg( msg, messagetype )} %endif - - %if len( users_groups_roles ) == 0: There are no Galaxy users %else: diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index d910e01b362..01f33a65836 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -18,7 +18,7 @@ <% - roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE ).all() + roles = trans.app.model.Role.query().all() %> %if isinstance( dataset, list ): diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 08da005df79..dff00121fc0 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -373,19 +373,48 @@ class TwillTestCase( unittest.TestCase ): self.visit_page( "user/set_default_permissions" ) self.check_page_for_string( email ) self.home() - + def user_set_default_permissions( self, permissions_out=[], permissions_in=[], role_id=2 ): # role.id = 2 is Private Role for test2@bx.psu.edu + # NOTE: Twill has a bug that requires the ~/user/permissions page to contain at least 1 option value + # in each select list or twill throws an exception, which is: ParseError: OPTION outside of SELECT + # Due to this bug, we'll bypass visiting the page, and simply pass the permissions on to the + # /user/set_default_permissions method. + url = "user/set_default_permissions?update_roles=Save&id=None" + for po in permissions_out: + key = '%s_out' % po + url ="%s&%s=%s" % ( url, key, str( role_id ) ) + for pi in permissions_in: + key = '%s_in' % pi + url ="%s&%s=%s" % ( url, key, str( role_id ) ) + self.visit_url( "%s/%s" % ( self.url, url ) ) + self.last_page() + self.check_page_for_string( 'Default new history permissions have been changed.' ) + self.home() + def history_set_default_permissions( self, permissions_out=[], permissions_in=[], role_id=3 ): # role.id = 3 is Private Role for test3@bx.psu.edu + # NOTE: Twill has a bug that requires the ~/user/permissions page to contain at least 1 option value + # in each select list or twill throws an exception, which is: ParseError: OPTION outside of SELECT + # Due to this bug, we'll bypass visiting the page, and simply pass the permissions on to the + # /user/set_default_permissions method. + url = "root/history_set_default_permissions?update_roles=Save&id=None&dataset=True" + for po in permissions_out: + key = '%s_out' % po + url ="%s&%s=%s" % ( url, key, str( role_id ) ) + for pi in permissions_in: + key = '%s_in' % pi + url ="%s&%s=%s" % ( url, key, str( role_id ) ) + self.visit_url( "%s/%s" % ( self.url, url ) ) + self.last_page() + self.check_page_for_string( 'Default history permissions have been changed.' ) + self.home() def login( self, email='test@bx.psu.edu', password='testuser'): # test@bx.psu.edu is configured as an admin user self.create( email=email, password=password, confirm=password ) self.visit_page( "user/login?email=%s&password=%s" % (email, password) ) self.check_page_for_string( "Now logged in as %s" %email ) self.home() - def logout( self ): self.visit_page( "user/logout" ) self.check_page_for_string( "You are no longer logged in" ) self.home() - # Functions associated with browsers, cookies, HTML forms and page visits def check_page_for_string( self, patt ): """Looks for 'patt' in the current browser page""" diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index df0af3dedf8..8a95da8122e 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -1,3 +1,4 @@ +import sys import galaxy.model from galaxy.model.orm import * from base.twilltestcase import * @@ -41,50 +42,162 @@ class TestHistory( TwillTestCase ): self.check_page_for_string( not_logged_in_security_msg ) self.visit_url( "%s/admin/dataset" % self.url ) self.check_page_for_string( not_logged_in_security_msg ) - def test_03_login_as_admin( self ): - """Testing logging in as an admin user""" - self.login( email='test@bx.psu.edu' ) #This is configured as our admin user + def test_03_login_as_admin_user( self ): + """Testing logging in as an admin user - tests initial settings for DefaultUserPermissions and DefaultHistoryPermissions""" + self.login( email='test@bx.psu.edu' ) # test@bx.psu.edu is configured as our admin user self.visit_page( "admin" ) self.check_page_for_string( 'Administration' ) global testuser1 testuser1 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test@bx.psu.edu' ).first() - # Make sure a private role exists for the user - private_role_found = False - for role in testuser1.all_roles(): - if role.name == testuser1.email and role.description == 'Private Role for %s' % testuser1.email: - private_role_found = True - break - if not private_role_found: - raise AssertionError( "Private role not found for user '%s'" % testuser1.email ) - # Make sure a DefaultUserPermission exists for the user + # Make sure DefaultUserPermissions are correct if not testuser1.default_permissions: raise AssertionError( 'No DefaultUserPermissions were created for %s when their account was created' % testuser1.email ) if len( testuser1.default_permissions ) > 1: raise AssertionError( 'More than 1 DefaultUserPermissions were created for %s when their account was created' % testuser1.email ) dup = galaxy.model.DefaultUserPermissions.filter( galaxy.model.DefaultUserPermissions.table.c.user_id==testuser1.id ).first() - if not dup.action == 'manage permissions': - raise AssertionError( 'The DefaultUserPermission.action for user "%s" is "%s", but it should be "manage permissions"' % ( testuser1.email, dup.action ) ) + if not dup.action == galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action: + raise AssertionError( 'The DefaultUserPermission.action for user "%s" is "%s", but it should be "%s"' \ + % ( testuser1.email, dup.action, galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action ) ) + # Make sure DefaultHistoryPermissions are correct + latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + if not latest_history.default_permissions: + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when it was created' % latest_history.id ) + if len( latest_history.default_permissions ) > 1: + raise AssertionError( 'More than 1 DefaultHistoryPermissions were created for history id %d when it was created' % latest_history.id ) + dhp = galaxy.model.DefaultHistoryPermissions.filter( galaxy.model.DefaultHistoryPermissions.table.c.history_id==latest_history.id ).first() + if not dhp.action == galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action: + raise AssertionError( 'The DefaultHistoryPermission.action for history id %d is "%s", but it should be "%s"' \ + % ( latest_history.id, dhp.action, galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action ) ) self.visit_url( "%s/admin/user?user_id=%s" % ( self.url, testuser1.id ) ) self.check_page_for_string( testuser1.email ) self.home() self.logout() - # Make sure that we have 3 users - self.login( email='test2@bx.psu.edu' ) # This will not be an admin user + def test_06_login_as_non_admin_user1( self ): + """Testing logging in as non-admin user1 - tests private role creation, changing DefaultHistoryPermissions for new histories""" + self.login( email='test2@bx.psu.edu' ) # test2@bx.psu.edu is not an admin user global testuser2 testuser2 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test2@bx.psu.edu' ).first() self.visit_page( "admin" ) self.check_page_for_string( logged_in_security_msg ) - # NOTE: we cannot currently Set DefaultHistoryPermissions for this user - # because of a bug in twill where it is not able to handle select lists that - # include no options... + # Make sure a private role exists for testuser2 + private_role = None + for role in testuser2.all_roles(): + if role.name == testuser2.email and role.description == 'Private Role for %s' % testuser2.email: + private_role = role + break + if not private_role: + raise AssertionError( "Private role not found for user '%s'" % testuser2.email ) + # Add a dataset to the history + self.upload_file( '1.bed' ) + latest_dataset = galaxy.model.Dataset.query().order_by( desc( galaxy.model.Dataset.table.c.create_time ) ).first() + # Make sure ActionDatasetRoleAssociation is correct + if not latest_dataset.actions: + raise AssertionError( 'No ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) + if len( latest_dataset.actions ) > 1: + raise AssertionError( 'More than 1 ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) + adra = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.dataset_id==latest_dataset.id ).first() + if not adra.action == galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action: + raise AssertionError( 'The ActionDatasetRoleAssociation.action for dataset id %d is "%s", but it should be "%s"' \ + % ( latest_dataset.id, adra.action, galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action ) ) + # Change DefaultHistoryPermissions for testuser2 + permissions_in = [] + actions_in = [] + for key, value in galaxy.model.Dataset.permitted_actions.items(): + permissions_in.append( key ) + actions_in.append( value.action ) + # Sort actions for later comparison + actions_in.sort() + role_id = str( private_role.id ) + self.user_set_default_permissions( permissions_in=permissions_in, role_id=role_id ) + # Make sure the default permissions are changed for new histories + self.new_history() + latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + if not latest_history.default_permissions: + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % latest_history.id ) + if len( latest_history.default_permissions ) != len( galaxy.model.Dataset.permitted_actions.items() ): + raise AssertionError( '%d DefaultHistoryPermissions were created for history id %d, should have been %d' % ( len( latest_history.default_permissions ), latest_history.id, len( galaxy.model.Dataset.permitted_actions ) ) ) + dhps = [] + for dhp in latest_history.default_permissions: + dhps.append( dhp.action ) + # Sort actions for later comparison + dhps.sort() + for key, value in galaxy.model.Dataset.permitted_actions.items(): + if value.action not in dhps: + raise AssertionError( '%s not in history id %d default_permissions after they were changed' % ( value.action, latest_history.id ) ) + # Add a dataset to the history + self.upload_file( '1.bed' ) + latest_dataset = galaxy.model.Dataset.query().order_by( desc( galaxy.model.Dataset.table.c.create_time ) ).first() + # Make sure ActionDatasetRoleAssociations are correct + if not latest_dataset.actions: + raise AssertionError( 'No ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) + if len( latest_dataset.actions ) != len( latest_history.default_permissions ): + raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' % ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) + adras = [] + for adra in latest_dataset.actions: + adras.append( adra.action ) + # Sort actions for later comparison + adras.sort() + # Compare ActionDatasetRoleAssociations with permissions_in - shouuld be the same + if adras != actions_in: + raise AssertionError( 'ActionDatasetRoleAssociations "%s" for dataset id %d differ from changed default permissions "%s"' \ + % ( str( adras ), latest_dataset.id, str( actions_in ) ) ) + # Compare DefaultHistoryPermissions and ActionDatasetRoleAssociations - should be the same + if adras != dhps: + raise AssertionError( 'ActionDatasetRoleAssociations "%s" for dataset id %d differ from DefaultHistoryPermissions "%s" for history id %d' \ + % ( str( adras ), latest_dataset.id, str( dhps ), latest_history.id ) ) + self.home() self.logout() + def test_09_login_as_non_admin_user2( self ): + """Testing logging in as non-admin user2 - tests changing DefaultHistoryPermissions for the current history""" self.login( email='test3@bx.psu.edu' ) # This will not be an admin user global testuser3 testuser3 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test3@bx.psu.edu' ).first() - self.visit_page( "admin" ) - self.check_page_for_string( logged_in_security_msg ) + latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + self.upload_file( '1.bed' ) + latest_dataset = galaxy.model.Dataset.query().order_by( desc( galaxy.model.Dataset.table.c.create_time ) ).first() + permissions_in = [ 'DATASET_EDIT_METADATA', 'DATASET_MANAGE_PERMISSIONS' ] + # Make sure these are in sorted order for later comparison + actions_in = [ 'edit metadata', 'manage permissions' ] + permissions_out = [ 'DATASET_ACCESS' ] + actions_out = [ 'access' ] + private_role = None + for role in testuser3.all_roles(): + if role.name == testuser3.email and role.description == 'Private Role for %s' % testuser3.email: + private_role = role + break + if not private_role: + raise AssertionError( "Private role not found for user '%s'" % testuser3.email ) + role_id = str( private_role.id ) + # Change DefaultHistoryPermissions for the current history + self.history_set_default_permissions( permissions_out=permissions_out, permissions_in=permissions_in, role_id=role_id ) + if not latest_history.default_permissions: + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % latest_history.id ) + if len( latest_history.default_permissions ) != len( actions_in ): + raise AssertionError( '%d DefaultHistoryPermissions were created for history id %d, should have been %d' \ + % ( len( latest_history.default_permissions ), latest_history.id, len( permissions_in ) ) ) + # Make sure DefaultHistoryPermissions were correctly changed for the current history + dhps = [] + for dhp in latest_history.default_permissions: + dhps.append( dhp.action ) + # Sort actions for later comparison + dhps.sort() + # Compare DefaultHistoryPermissions and actions_in - should be the same + if dhps != actions_in: + raise AssertionError( 'DefaultHistoryPermissions "%s" for history id %d differ from actions "%s" passed for changing' \ + % ( str( dhps ), latest_history.id, str( actions_in ) ) ) + # Make sure ActionDatasetRoleAssociations are correct + if not latest_dataset.actions: + raise AssertionError( 'No ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) + if len( latest_dataset.actions ) != len( latest_history.default_permissions ): + raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' % ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) + adras = [] + for adra in latest_dataset.actions: + adras.append( adra.action ) + # Sort actions for later comparison + adras.sort() + self.home() self.logout() - def test_06_create_role( self ): + def test_12_create_role( self ): """Testing creating new non-private role with 2 members""" self.login( email=testuser1.email ) name = 'New Test Role' @@ -93,14 +206,14 @@ class TestHistory( TwillTestCase ): # Get the role object for later tests global new_test_role new_test_role = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() - def test_09_create_group( self ): + def test_15_create_group( self ): """Testing creating new group with 2 members and 1 associated role""" name = 'New Test Group' self.create_group( name=name, user_ids=[ str( testuser1.id ), str( testuser2.id ) ], role_ids=[ str( new_test_role.id ) ] ) # Get the group object for later tests global new_test_group new_test_group = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() - def test_12_add_group_member( self ): + def test_18_add_group_member( self ): """Testing editing membership of an existing group""" name = 'Another Test Group' self.create_group( name=name ) @@ -110,15 +223,11 @@ class TestHistory( TwillTestCase ): self.add_group_members( str( another_test_group.id ), [ str( testuser3.id ) ] ) self.visit_url( "%s/admin/group_members_edit?group_id=%s" % ( self.url, str( another_test_group.id ) ) ) self.check_page_for_string( testuser3.email ) - def test_15_associate_groups_with_role( self ): + def test_21_associate_groups_with_role( self ): """Testing adding existing groups to an existing role""" # NOTE: To get this to work with twill, all select lists on the ~/admin/role page must contain at least # 1 option value or twill throws an exception, which is: ParseError: OPTION outside of SELECT # Due to this bug in twill, we create the role, associating it with at least 1 user and 1 group... - # - # TODO: need to enhance this test to associate DefaultUserPermissions and DefaultHistoryPermissions - # with the role, then add tests in test_55_purge_role to make sure the association records are deleted - # when the role is purged. name = 'Another Test Role' description = 'Another cool new test role' self.create_role( name=name, @@ -133,7 +242,7 @@ class TestHistory( TwillTestCase ): self.associate_groups_with_role( str( another_test_role.id ), group_ids=[ str( new_test_group.id ) ] ) self.visit_page( 'admin/roles' ) self.check_page_for_string( new_test_group.name ) - def test_18_create_library( self ): + def test_24_create_library( self ): """Testing creating new library""" name = 'New Test Library' description = 'New Test Library Description' @@ -145,20 +254,20 @@ class TestHistory( TwillTestCase ): library = galaxy.model.Library.filter( and_( galaxy.model.Library.table.c.name==name, galaxy.model.Library.table.c.description==description, galaxy.model.Library.table.c.deleted==False ) ).first() - def test_21_rename_library( self ): + def test_27_rename_library( self ): """Testing renaming a library""" self.rename_library( str( library.id ), name='New Test Library Renamed', description='New Test Library Description Re-described', root_folder='on' ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Library Renamed" ) # Rename it back to what it was originally self.rename_library( str( library.id ), name='New Test Library', description='New Test Library Description', root_folder='on' ) - def test_24_rename_root_folder( self ): + def test_30_rename_root_folder( self ): """Testing renaming a library root folder""" folder = library.root_folder self.rename_folder( str( folder.id ), name='New Test Library Root Folder', description='New Test Library Root Folder Description' ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Library Root Folder" ) - def test_27_add_public_dataset_to_root_folder( self ): + def test_33_add_public_dataset_to_root_folder( self ): """Testing adding a public dataset to a library root folder""" folder = library.root_folder self.add_dataset( '1.bed', str( folder.id ), extension='bed', dbkey='hg18', roles=[] ) @@ -166,7 +275,7 @@ class TestHistory( TwillTestCase ): self.check_page_for_string( "1.bed" ) self.check_page_for_string( "bed" ) self.check_page_for_string( "hg18" ) - def test_30_copy_dataset_from_history_to_root_folder( self ): + def test_36_copy_dataset_from_history_to_root_folder( self ): """Testing copying a dataset from the current history to a library root folder""" folder = library.root_folder self.add_dataset_to_folder_from_history( str( folder.id ) ) @@ -181,7 +290,7 @@ class TestHistory( TwillTestCase ): for adra in adras: if not adra.action == 'manage permissions': raise AssertionError( 'ActionDatasetRoleAssociation.action "%s" is not the DefaultHistoryPermission setting, which is "manage permissions"' % str( adra.action ) ) - def test_33_add_new_folder( self ): + def test_39_add_new_folder( self ): """Testing adding a folder to a library root folder""" root_folder = library.root_folder name = 'New Test Folder' @@ -193,27 +302,27 @@ class TestHistory( TwillTestCase ): galaxy.model.LibraryFolder.table.c.description==description ) ).first() self.visit_page( 'admin/libraries' ) self.check_page_for_string( "New Test Folder" ) - def test_36_add_datasets_from_library_dir( self ): + def test_42_add_datasets_from_library_dir( self ): """Testing adding several datasets from library directory to sub-folder""" roles_tuple = [ ( str( new_test_role.id ), new_test_role.description ) ] self.add_datasets_from_library_dir( str( new_test_folder.id ), roles_tuple=roles_tuple ) - def test_39_mark_group_deleted( self ): + def test_45_mark_group_deleted( self ): """Testing marking a group as deleted""" self.visit_page( "admin/groups" ) self.check_page_for_string( another_test_group.name ) self.mark_group_deleted( str( another_test_group.id ) ) - def test_42_undelete_group( self ): + def test_48_undelete_group( self ): """Testing undeleting a deleted group""" self.undelete_group( str( another_test_group.id ) ) - def test_45_mark_role_deleted( self ): + def test_51_mark_role_deleted( self ): """Testing marking a role as deleted""" self.visit_page( "admin/roles" ) self.check_page_for_string( another_test_role.description ) self.mark_role_deleted( str( another_test_role.id ) ) - def test_48_undelete_role( self ): + def test_54_undelete_role( self ): """Testing undeleting a deleted role""" self.undelete_role( str( another_test_role.id ) ) - def test_51_mark_library_deleted( self ): + def test_57_mark_library_deleted( self ): """Testing marking a library as deleted""" self.mark_library_deleted( str( library.id ) ) # Make sure the library was deleted @@ -237,7 +346,7 @@ class TestHistory( TwillTestCase ): if lfda.dataset.deleted: raise AssertionError( 'The dataset with id "%s" has been marked as deleted when it should not have been.' % lfda.dataset.id ) check_folder( library.root_folder ) - def test_54_mark_library_undeleted( self ): + def test_60_mark_library_undeleted( self ): """Testing marking a library as not deleted""" self.mark_library_undeleted( str( library.id ) ) # Make sure the library is undeleted @@ -266,7 +375,7 @@ class TestHistory( TwillTestCase ): library.refresh() if not library.deleted: raise AssertionError( 'The library id %s named "%s" has not been marked as deleted after it was undeleted.' % ( str( library.id ), library.name ) ) - def test_57_purge_group( self ): + def test_63_purge_group( self ): """Testing purging a group""" group_id = str( another_test_group.id ) self.purge_group( group_id ) @@ -278,7 +387,7 @@ class TestHistory( TwillTestCase ): gra = galaxy.model.GroupRoleAssociation.filter( galaxy.model.GroupRoleAssociation.table.c.group_id == group_id ).all() if gra: raise AssertionError( "Purging the group did not delete the GroupRoleAssociations for group_id '%s'" % group_id ) - def test_60_purge_role( self ): + def test_66_purge_role( self ): """Testing purging a role""" role_id = str( another_test_role.id ) self.purge_role( role_id ) @@ -290,7 +399,7 @@ class TestHistory( TwillTestCase ): adra = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.role_id == role_id ).all() if adra: raise AssertionError( "Purging the role did not delete the ActionDatasetRoleAssociations for role_id '%s'" % role_id ) - def test_63_purge_library( self ): + def test_69_purge_library( self ): """Testing purging a library""" self.purge_library( str( library.id ) ) # Make sure the library was purged From ed90f0f68364e197722394ee134eb93c584bd7a2 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 28 Nov 2008 15:32:15 -0500 Subject: [PATCH 113/267] Fixes and new features for data security and libraries, requires db schema change and new config setting. - add ability for an admin to reset another user's password - add ability for an admin user to delete another user's account ( requires config setting to display feature ) - add ability for an admin user to manage deleted users ( undelete / purge ) - more code cleanup in the admin controller - several new functional tests and existing functional test improvements SQL commands to alter db schema: ALTER TABLE galaxy_user ADD COLUMN deleted BOOLEAN DEFAULT FALSE; CREATE INDEX ix_galaxy_user_deleted ON galaxy_user USING btree (deleted); ALTER TABLE galaxy_user ADD COLUMN purged BOOLEAN DEFAULT FALSE; CREATE INDEX ix_galaxy_user_purged ON galaxy_user USING btree (purged); New config setting: # Can an admin user delete user accounts? #allow_user_deletion = False --- lib/galaxy/config.py | 9 +- lib/galaxy/model/__init__.py | 3 + lib/galaxy/model/mapping.py | 4 +- lib/galaxy/web/controllers/admin.py | 356 +++++++++++----- lib/galaxy/web/controllers/root.py | 2 +- lib/galaxy/web/controllers/user.py | 22 +- lib/galaxy/web/controllers/workflow.py | 4 +- lib/galaxy/web/framework/__init__.py | 18 +- templates/admin/dataset_security/users.mako | 7 + templates/admin/library/browser.mako | 2 +- templates/admin/user/create.mako | 43 ++ templates/admin/user/deleted_users.mako | 91 +++++ templates/admin/user/reset_password.mako | 35 ++ test/base/twilltestcase.py | 115 ++++-- test/functional/__init__.py | 7 +- .../functional/test_security_and_libraries.py | 381 ++++++++++++++---- universe_wsgi.ini.sample | 3 + 17 files changed, 844 insertions(+), 258 deletions(-) create mode 100644 templates/admin/user/create.mako create mode 100644 templates/admin/user/deleted_users.mako create mode 100644 templates/admin/user/reset_password.mako diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index a8df982d53c..332c16a2491 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -40,6 +40,7 @@ class Configuration( object ): self.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None ) self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) ) self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) ) + self.allow_user_deletion = string_as_bool( kwargs.get( "allow_user_deletion", "False" ) ) self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root ) self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/compiled_templates" ), self.root ) self.local_job_queue_workers = int( kwargs.get( "local_job_queue_workers", "5" ) ) @@ -58,10 +59,10 @@ class Configuration( object ): self.pbs_dataset_server = kwargs.get('pbs_dataset_server', "" ) self.pbs_dataset_path = kwargs.get('pbs_dataset_path', "" ) self.pbs_stage_path = kwargs.get('pbs_stage_path', "" ) - self.use_heartbeat = string_as_bool( kwargs.get( 'use_heartbeat', False ) ) - self.use_memdump = string_as_bool( kwargs.get( 'use_memdump', False ) ) - self.log_memory_usage = string_as_bool( kwargs.get( 'log_memory_usage', False ) ) - self.log_events = string_as_bool( kwargs.get( 'log_events', False ) ) + self.use_heartbeat = string_as_bool( kwargs.get( 'use_heartbeat', 'False' ) ) + self.use_memdump = string_as_bool( kwargs.get( 'use_memdump', 'False' ) ) + self.log_memory_usage = string_as_bool( kwargs.get( 'log_memory_usage', 'False' ) ) + self.log_events = string_as_bool( kwargs.get( 'log_events', 'False' ) ) self.ucsc_display_sites = kwargs.get( 'ucsc_display_sites', "main,test,archaea" ).lower().split(",") self.gbrowse_display_sites = kwargs.get( 'gbrowse_display_sites', "wormbase,flybase,elegans" ).lower().split(",") self.brand = kwargs.get( 'brand', None ) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 5d6bf13d240..66fe0130b36 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -32,6 +32,8 @@ class User( object ): self.email = email self.password = password self.external = False + self.deleted = False + self.purged = False # Relationships self.histories = [] @@ -143,6 +145,7 @@ class Group( object ): permitted_actions = galaxy.security.get_permitted_actions( 'GROUP' ) def __init__( self, name = None ): self.name = name + self.deleted = False class UserGroupAssociation( object ): def __init__( self, user, group ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 4696c0ca1ad..86aa72579cd 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -43,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), diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index dc72c913abe..832a0d4f74f 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -3,12 +3,7 @@ from galaxy import util, datatypes from galaxy.web.base.controller import * from galaxy.datatypes import sniff from galaxy.security import RBACAgent -import galaxy.model from galaxy.model.orm import * -from xml.sax.saxutils import escape, unescape -import pkg_resources -pkg_resources.require( "SQLAlchemy >= 0.4" ) -import sqlalchemy as sa import logging log = logging.getLogger( __name__ ) @@ -60,9 +55,9 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) - users=trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + users = trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all() groups = trans.app.model.Group.query() \ - .filter( galaxy.model.Group.table.c.deleted==False ) \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ .order_by( trans.app.model.Group.table.c.name ) \ .all() return trans.fill_template( '/admin/dataset_security/role_create.mako', @@ -84,23 +79,21 @@ class Admin( BaseController ): trans.response.send_redirect( web.url_for( action='create_role', msg=msg, messagetype='error' ) ) else: # Create the role - role = galaxy.model.Role( name=name, - description=description, - type=trans.app.model.Role.types.ADMIN ) + role = trans.app.model.Role( name=name, description=description, type=trans.app.model.Role.types.ADMIN ) role.flush() # Add the users users = util.listify( params.users ) for user_id in users: - user = galaxy.model.User.get( user_id ) + user = trans.app.model.User.get( user_id ) # Create the UserRoleAssociation - ura = galaxy.model.UserRoleAssociation( user, role ) + ura = trans.app.model.UserRoleAssociation( user, role ) ura.flush() # Add the groups groups = util.listify( params.groups ) for group_id in groups: - group = galaxy.model.Group.get( group_id ) + group = trans.app.model.Group.get( group_id ) # Create the GroupRoleAssociation - gra = galaxy.model.GroupRoleAssociation( group, role ) + gra = trans.app.model.GroupRoleAssociation( group, role ) gra.flush() msg = "The new role has been created with %s associated users and %s associated groups" % ( str( len( users ) ), str( len( groups ) ) ) trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='done' ) ) @@ -115,7 +108,7 @@ class Admin( BaseController ): out_users = [] in_groups = [] out_groups = [] - for user in trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all(): + 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: @@ -163,7 +156,7 @@ class Admin( BaseController ): @web.require_admin def role_members_edit( self, trans, **kwd ): params = util.Params( kwd ) - role = galaxy.model.Role.get( int( params.role_id ) ) + role = trans.app.model.Role.get( int( params.role_id ) ) 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 ) @@ -188,7 +181,7 @@ class Admin( BaseController ): @web.require_admin def mark_role_deleted( self, trans, **kwd ): params = util.Params( kwd ) - role = galaxy.model.Role.get( int( params.role_id ) ) + role = trans.app.model.Role.get( int( params.role_id ) ) role.deleted = True role.flush() msg = "The role has been marked as deleted." @@ -202,17 +195,17 @@ class Admin( BaseController ): # Build a list of tuples which are roles followed by lists of groups and users # [ ( role, [ group, group, group ], [ user, user ] ), ( role, [ group, group ], [ user ] ) ] roles_groups_users = [] - roles = galaxy.model.Role.query() \ - .filter( galaxy.model.Role.table.c.deleted==True ) \ - .order_by( galaxy.model.Role.table.c.name ) \ + 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( galaxy.model.Group.get( gra.group_id ) ) + groups.append( trans.app.model.Group.get( gra.group_id ) ) users = [] for ura in role.users: - users.append( galaxy.model.User.get( ura.user_id ) ) + 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, @@ -222,7 +215,7 @@ class Admin( BaseController ): @web.require_admin def undelete_role( self, trans, **kwd ): params = util.Params( kwd ) - role = galaxy.model.Role.get( int( params.role_id ) ) + role = trans.app.model.Role.get( int( params.role_id ) ) role.deleted = False role.flush() msg = "The role has been marked as not deleted." @@ -230,8 +223,19 @@ class Admin( BaseController ): @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 = galaxy.model.Role.get( int( params.role_id ) ) + 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 = "The role has not been deleted, so it cannot be purged." + trans.response.send_redirect( web.url_for( action='roles', msg=msg, messagetype='error' ) ) # Delete UserRoleAssociations for ura in role.users: user = trans.app.model.User.get( ura.user_id ) @@ -252,10 +256,12 @@ class Admin( BaseController ): for gra in role.groups: gra.delete() gra.flush() - # Delete the Role - role.delete() - role.flush() - msg = "The role has been purged from the database." + # Delete ActionDatasetRoleAssociations + for adra in role.actions: + adra.delete() + adra.flush() + msg = "The following have been purged from the database for the role: " + msg += "DefaultUserPermissions, DefaultHistoryPermissions, UserRoleAssociations, GroupRoleAssociations, ActionDatasetRoleAssociations." trans.response.send_redirect( web.url_for( action='deleted_roles', msg=msg, messagetype='done' ) ) # Galaxy Group Stuff @@ -268,17 +274,17 @@ class Admin( BaseController ): # Build a list of tuples which are groups followed by lists of members and roles # [ ( group, [ member, member, member ], [ role, role ] ), ( group, [ member, member ], [ role ] ) ] groups_members_roles = [] - groups = galaxy.model.Group.query() \ - .filter( galaxy.model.Group.table.c.deleted==False ) \ - .order_by( galaxy.model.Group.table.c.name ) \ + 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( galaxy.model.User.get( uga.user_id ) ) + members.append( trans.app.model.User.get( uga.user_id ) ) roles = [] for gra in group.roles: - roles.append( galaxy.model.Role.get( gra.role_id ) ) + 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, @@ -290,10 +296,10 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) - users=trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + users = trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all() roles = trans.app.model.Role.query() \ - .filter( and_( galaxy.model.Role.table.c.deleted == False, - galaxy.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE ) ) \ + .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/group_create.mako', @@ -314,14 +320,14 @@ class Admin( BaseController ): trans.response.send_redirect( web.url_for( action='create_group', msg=msg, messagetype='error' ) ) else: # Create the group - group = galaxy.model.Group( name ) + group = trans.app.model.Group( name ) group.flush() # Add the members members = util.listify( params.members ) for user_id in members: - user = galaxy.model.User.get( user_id ) + user = trans.app.model.User.get( user_id ) # Create the UserGroupAssociation - uga = galaxy.model.UserGroupAssociation( user, group ) + uga = trans.app.model.UserGroupAssociation( user, group ) uga.flush() # Add the roles roles = params.roles @@ -330,9 +336,9 @@ class Admin( BaseController ): elif roles is None: roles = [] for role_id in roles: - role = galaxy.model.Role.get( role_id ) + role = trans.app.model.Role.get( role_id ) # Create the GroupRoleAssociation - gra = galaxy.model.GroupRoleAssociation( group, role ) + gra = trans.app.model.GroupRoleAssociation( group, role ) gra.flush() msg = "The new group has been created with %s members and %s associated roles" % ( str( len( members ) ), str( len( roles ) ) ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @@ -342,14 +348,15 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) - group = galaxy.model.Group.get( int( params.group_id ) ) + group = trans.app.model.Group.get( int( params.group_id ) ) members = [] for uga in group.members: - members.append ( galaxy.model.User.get( uga.user_id ) ) + members.append ( trans.app.model.User.get( uga.user_id ) ) + users = trans.app.model.User.filter( trans.app.model.User.table.c.deleted==False ).order_by( trans.app.model.User.table.c.email ).all() return trans.fill_template( '/admin/dataset_security/group_members_edit.mako', group=group, members=members, - users=galaxy.model.User.query().order_by( galaxy.model.User.table.c.email ).all(), + users=users, msg=msg, messagetype=messagetype ) @web.expose @@ -358,7 +365,7 @@ class Admin( BaseController ): params = util.Params( kwd ) group_id = int( params.group_id ) members = util.listify( params.members ) - group = galaxy.model.Group.get( group_id ) + group = trans.app.model.Group.get( group_id ) # This is tricky since we have default association tables with # records referring to members of this group. Because of this, # we'll need to handle changes to the member list rather than the @@ -372,9 +379,9 @@ class Admin( BaseController ): uga.flush() # Then add all new members to the group for user_id in members: - user = galaxy.model.User.get( user_id ) + user = trans.app.model.User.get( user_id ) if user not in group.members: - uga = galaxy.model.UserGroupAssociation( user, group ) + uga = trans.app.model.UserGroupAssociation( user, group ) uga.flush() msg = "Group membership has been updated with a total of %s members" % len( members ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @@ -386,14 +393,14 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) - group = galaxy.model.Group.get( int( params.group_id ) ) + group = trans.app.model.Group.get( int( params.group_id ) ) group_roles = [] for gra in group.roles: - group_roles.append ( galaxy.model.Role.get( gra.role_id ) ) + group_roles.append ( trans.app.model.Role.get( gra.role_id ) ) return trans.fill_template( '/admin/dataset_security/group_roles_edit.mako', group=group, group_roles=group_roles, - roles=galaxy.model.Role.query().order_by( galaxy.model.Role.table.c.name ).all(), + roles=trans.app.model.Role.query().order_by( trans.app.model.Role.table.c.name ).all(), msg=msg, messagetype=messagetype ) @web.expose @@ -402,7 +409,7 @@ class Admin( BaseController ): params = util.Params( kwd ) group_id = int( params.group_id ) roles = util.listify( params.roles ) - group = galaxy.model.Group.get( group_id ) + group = trans.app.model.Group.get( group_id ) # This is tricky since we have default association tables with # records referring to members of this group. Because of this, # we'll need to handle changes to the member list rather than the @@ -416,9 +423,9 @@ class Admin( BaseController ): gra.flush() # Then add all new roles to the group for role_id in roles: - role = galaxy.model.Role.get( role_id ) + role = trans.app.model.Role.get( role_id ) if role not in group.roles: - gra = galaxy.model.GroupRoleAssociation( group, role ) + gra = trans.app.model.GroupRoleAssociation( group, role ) gra.flush() msg = "Group updated with a total of %s associated roles" % len( roles ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) @@ -426,7 +433,7 @@ class Admin( BaseController ): @web.require_admin def mark_group_deleted( self, trans, **kwd ): params = util.Params( kwd ) - group = galaxy.model.Group.get( int( params.group_id ) ) + group = trans.app.model.Group.get( int( params.group_id ) ) group.deleted = True group.flush() msg = "The group has been marked as deleted." @@ -440,17 +447,17 @@ class Admin( BaseController ): # Build a list of tuples which are groups followed by lists of members and roles # [ ( group, [ member, member, member ], [ role, role ] ), ( group, [ member, member ], [ role ] ) ] groups_members_roles = [] - groups = galaxy.model.Group.query() \ - .filter( galaxy.model.Group.table.c.deleted==True ) \ - .order_by( galaxy.model.Group.table.c.name ) \ + 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( galaxy.model.User.get( uga.user_id ) ) + members.append( trans.app.model.User.get( uga.user_id ) ) roles = [] for gra in group.roles: - roles.append( galaxy.model.Role.get( gra.role_id ) ) + 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, @@ -460,7 +467,7 @@ class Admin( BaseController ): @web.require_admin def undelete_group( self, trans, **kwd ): params = util.Params( kwd ) - group = galaxy.model.Group.get( int( params.group_id ) ) + group = trans.app.model.Group.get( int( params.group_id ) ) group.deleted = False group.flush() msg = "The group has been marked as not deleted." @@ -468,8 +475,14 @@ class Admin( BaseController ): @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 = galaxy.model.Group.get( int( params.group_id ) ) + 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 = "The group has not been deleted, so it cannot be purged." + trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='error' ) ) # Delete UserGroupAssociations for uga in group.users: uga.delete() @@ -479,27 +492,40 @@ class Admin( BaseController ): gra.delete() gra.flush() # Delete the Group - group.delete() - group.flush() - msg = "The group has been purged from the database." + msg = "The following have been purged from the database for the group: UserGroupAssociations, GroupRoleAssociations." trans.response.send_redirect( web.url_for( action='deleted_groups', msg=msg, messagetype='done' ) ) # Galaxy User Stuff @web.expose @web.require_admin - def create_new_user( self, trans, email='', password='', confirm='', subscribe=False ): - email_error = password_error = confirm_error = None - if email: + def create_new_user( self, trans, **kwd ): + params = util.Params( kwd ) + msg = params.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: - email_error = "Please enter a real email address" + msg = "Please enter a real email address" elif len( email) > 255: - email_error = "Email address exceeds maximum allowable length" - elif trans.app.model.User.filter_by( email=email ).first(): - email_error = "User with that email already exists" + 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: - password_error = "Please use a password of at least 6 characters" + msg = "Please use a password of at least 6 characters" elif password != confirm: - confirm_error = "Passwords do not match" + msg = "Passwords do not match" else: user = trans.app.model.User( email=email ) user.set_password_cleartext( password ) @@ -507,25 +533,147 @@ class Admin( BaseController ): 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 account' + 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 ) ) + 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=msg, messagetype=messagetype ) ) - # TODO: make this a mako template - return trans.show_form( - web.FormBuilder( web.url_for(), "Create account", submit_text="Create" ) - .add_text( "email", "Email address", value=email, error=email_error ) - .add_password( "password", "Password", value='', error=password_error ) - .add_password( "confirm", "Confirm password", value='', error=confirm_error ) - .add_input( "checkbox","Subscribe To Mailing List","subscribe", value='subscribe' ) ) - - + trans.response.send_redirect( web.url_for( action='users', msg=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 = params.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=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 = params.msg + messagetype = params.get( 'messagetype', 'done' ) + user = trans.app.model.User.get( int( params.user_id ) ) + user.deleted = True + user.flush() + msg = "The user has been marked as deleted." + trans.response.send_redirect( web.url_for( action='users', msg=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 = "The user has been marked as not deleted." + trans.response.send_redirect( web.url_for( action='users', msg=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 = "The account has not been deleted, so it cannot be purged." + trans.response.send_redirect( web.url_for( action='users', msg=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 = "The user has been marked as purged." + trans.response.send_redirect( web.url_for( action='deleted_users', msg=msg, messagetype='done' ) ) + @web.expose + @web.require_admin + def deleted_users( self, trans, **kwd ): + params = util.Params( kwd ) + msg = params.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 ): @@ -535,24 +683,25 @@ class Admin( BaseController ): # Build a list of tuples which are users followed by lists of groups and roles # [ ( user, [ group, group, group ], [ role, role ] ), ( user, [ group, group ], [ role ] ) ] users_groups_roles = [] - users = trans.app.model.User.query().order_by( trans.app.model.User.table.c.email ).all() + 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( galaxy.model.Group.get( uga.group_id ) ) + groups.append( trans.app.model.Group.get( uga.group_id ) ) roles = [] for ura in user.non_private_roles: - roles.append( galaxy.model.Role.get( ura.role_id ) ) + 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 ) - user_id = params.user_id + user_id = int( params.user_id ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) user = trans.app.model.User.get( user_id ) @@ -560,7 +709,7 @@ class Admin( BaseController ): groups = trans.app.model.Group.query() \ .select_from( ( outerjoin( trans.app.model.Group, trans.app.model.UserGroupAssociation ) ) \ .outerjoin( trans.app.model.User ) ) \ - .filter( and_( trans.app.model.Group.deleted == False, trans.app.model.User.id == user_id ) ) \ + .filter( and_( trans.app.model.Group.deleted==False, trans.app.model.User.id==user_id ) ) \ .order_by( trans.app.model.Group.table.c.name ) \ .all() roles = user.all_roles() @@ -576,13 +725,13 @@ class Admin( BaseController ): params = util.Params( kwd ) msg = params.msg messagetype = params.get( 'messagetype', 'done' ) - user = galaxy.model.User.get( int( params.user_id ) ) + user = trans.app.model.User.get( int( params.user_id ) ) user_groups = [] for uga in user.groups: - user_groups.append ( galaxy.model.Group.get( uga.group_id ) ) - groups = galaxy.model.Group.query() \ - .filter( galaxy.model.Group.table.c.deleted==False ) \ - .order_by( galaxy.model.Group.table.c.name ) \ + user_groups.append ( trans.app.model.Group.get( uga.group_id ) ) + 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() return trans.fill_template( '/admin/dataset_security/user_groups_edit.mako', user=user, @@ -596,7 +745,7 @@ class Admin( BaseController ): params = util.Params( kwd ) user_id = int( params.user_id ) groups = util.listify( params.groups ) - user = galaxy.model.User.get( user_id ) + user = trans.app.model.User.get( user_id ) # First remove existing UserGroupAssociations that are not in the received groups param for uga in user.groups: if uga.group_id not in groups: @@ -605,9 +754,9 @@ class Admin( BaseController ): uga.flush() # Then add all new groups to the user for group_id in groups: - group = galaxy.model.Group.get( group_id ) + group = trans.app.model.Group.get( group_id ) if group not in user.groups: - uga = galaxy.model.UserGroupAssociation( user, group ) + uga = trans.app.model.UserGroupAssociation( user, group ) uga.flush() msg = "The user now belongs to a total of %s groups" % len( groups ) trans.response.send_redirect( web.url_for( action='users', msg=msg, messagetype='done' ) ) @@ -713,7 +862,7 @@ class Admin( BaseController ): @web.require_admin def undelete_library( self, trans, **kwd ): params = util.Params( kwd ) - library = galaxy.model.Library.get( int( params.id ) ) + library = trans.app.model.Library.get( int( params.id ) ) def undelete_folder( library_folder ): for folder in library_folder.folders: undelete_folder( folder ) @@ -731,7 +880,7 @@ class Admin( BaseController ): @web.require_admin def purge_library( self, trans, **kwd ): params = util.Params( kwd ) - library = galaxy.model.Library.get( int( params.id ) ) + library = trans.app.model.Library.get( int( params.id ) ) def purge_folder( library_folder ): for lf in library_folder.folders: purge_folder( lf ) @@ -877,7 +1026,7 @@ class Admin( BaseController ): dataset.flush() if roles: for role in roles: - adra = galaxy.model.ActionDatasetRoleAssociation( RBACAgent.permitted_actions.DATASET_ACCESS.action, dataset.dataset, role ) + 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 @@ -914,7 +1063,7 @@ class Admin( BaseController ): roles = [] role_ids = params.get( 'roles', [] ) for role_id in util.listify( role_ids ): - roles.append( galaxy.model.Role.get( role_id ) ) + roles.append( trans.app.model.Role.get( role_id ) ) temp_name = "" data_list = [] created_lfda_ids = '' @@ -1139,7 +1288,8 @@ class Admin( BaseController ): msg = params.get( 'msg', None ) messagetype = params.get( 'messagetype', 'done' ) # See if the current history is empty - history=trans.get_history() + 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=msg, messagetype='error' ) ) diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index beb32d121fa..5cb9c8d48df 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -527,7 +527,7 @@ class RootController( BaseController ): if not email: return trans.fill_template("/history/share.mako", histories=histories, email=email, send_to_err=send_to_err) user = trans.get_user() - send_to_user = trans.app.model.User.filter_by( email=email ).first() + send_to_user = trans.app.model.User.filter( trans.app.model.User.table.c.email==email ).first() p = util.Params( kwd ) if p.action and p.action == "no_share": trans.response.send_redirect( url_for( action='history_options' ) ) diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 9e197c021dd..7f65379f858 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -88,12 +88,13 @@ class User( BaseController ): 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: @@ -118,9 +119,6 @@ class User( BaseController ): @web.expose def logout( self, trans ): - if trans.app.memory_usage: - # Keep track of memory usage - m0 = trans.app.memory_usage.memory() if trans.app.config.require_login: refresh_frames = [ 'masthead', 'history', 'tools' ] else: @@ -128,9 +126,6 @@ class User( BaseController ): # 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() - if trans.app.memory_usage: - m1 = trans.app.memory_usage.memory( m0, pretty=True ) - log.info( "End of user/logout, memory used increased by %s" % m1 ) 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' ) @@ -148,9 +143,10 @@ class User( BaseController ): 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" @@ -182,11 +178,11 @@ class User( BaseController ): @web.expose 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 = "" diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 35c6eed0d0b..beaa684818a 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -13,6 +13,7 @@ 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 ): @@ -48,7 +49,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 ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index af93111117f..59324a80479 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -221,8 +221,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", @@ -293,16 +292,27 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): def __get_or_create_remote_user( self, remote_user_email ): """ Return the user in $HTTP_REMOTE_USER and create if necessary - Caller is responsible for flushing the returned user. """ # 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: user = self.app.model.User( email=remote_user_email ) user.set_password_cleartext( 'external' ) user.external = True self.log_event( "Automatically created account '%s'", user.email ) + # TODO: make sure this correctly handles deleted / purged users + elif user.deleted: + if user.purged: + # If the user has been purged, all associations have been deleted except for the private role + # and the DefaultUserPermissions and DefaultHistoryPermissions associated with it. We'll + # restore the user, but all of their previous histories and other associations will have been + # deleted. + user.purged = False + # If the user was not purged, the state of all of their associations at the time they were deleted + # will have been preserved. + user.deleted = False + user.flush() return user def __update_session_cookie( self ): """ diff --git a/templates/admin/dataset_security/users.mako b/templates/admin/dataset_security/users.mako index 39dad937182..e149d23cdb4 100644 --- a/templates/admin/dataset_security/users.mako +++ b/templates/admin/dataset_security/users.mako @@ -16,7 +16,11 @@ ${user.email}
            + Reset password Change associated groups + %if allow_user_deletion: + Mark user deleted + %endif
            @@ -44,6 +48,9 @@ %if msg: diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako index ed9da94ade4..f268a8a11dd 100644 --- a/templates/admin/library/browser.mako +++ b/templates/admin/library/browser.mako @@ -103,7 +103,7 @@ Create a new sub-folder in this folder Rename this folder %if subfolder: - Remove this folder and its contents from the library + Remove this folder and its contents from the library %endif
            %endif diff --git a/templates/admin/user/create.mako b/templates/admin/user/create.mako new file mode 100644 index 00000000000..e119e29e6c0 --- /dev/null +++ b/templates/admin/user/create.mako @@ -0,0 +1,43 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +
            +
            Create account
            +
            + +
            + +
            + +
            +
            +
            +
            + +
            + +
            +
            +
            +
            + +
            + +
            +
            +
            +
            + +
            + +
            +
            +
            + + +
            +
            diff --git a/templates/admin/user/deleted_users.mako b/templates/admin/user/deleted_users.mako new file mode 100644 index 00000000000..0847cf25a63 --- /dev/null +++ b/templates/admin/user/deleted_users.mako @@ -0,0 +1,91 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +## Render a row +<%def name="render_row( user, ctr, anchored, curr_anchor )"> + %if ctr % 2 == 1: + + %else: + + %endif + + ${user.email} + +
            + Undelete + Purge +
            + %if not anchored: + + + %endif + + + + +

            Deleted Users

            + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%if len( users ) == 0: + There are no deleted Galaxy users +%else: + + <% + render_quick_find = len( users ) > 50 + ctr = 0 + %> + %if render_quick_find: + <% + anchors = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'] + anchor_loc = 0 + anchored = False + curr_anchor = 'A' + %> + + + + %endif + + + + %for ctr, user in enumerate( users ): + %if render_quick_find and not user.email.upper().startswith( curr_anchor ): + <% anchored = False %> + %endif + %if render_quick_find and user.email.upper().startswith( curr_anchor ): + %if not anchored: + ${render_row( user, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( user, ctr, anchored, curr_anchor )} + %endif + %elif render_quick_find: + %for anchor in anchors[ anchor_loc: ]: + %if user.email.upper().startswith( anchor ): + %if not anchored: + <% curr_anchor = anchor %> + ${render_row( user, ctr, anchored, curr_anchor )} + <% anchored = True %> + %else: + ${render_row( user, ctr, anchored, curr_anchor )} + %endif + <% + anchor_loc = anchors.index( anchor ) + break + %> + %endif + %endfor + %else: + ${render_row( user, ctr, True, '' )} + %endif + %endfor +
            + Jump to letter: + %for a in anchors: + | ${a} + %endfor +
            Email
            +%endif diff --git a/templates/admin/user/reset_password.mako b/templates/admin/user/reset_password.mako new file mode 100644 index 00000000000..5c0e92de957 --- /dev/null +++ b/templates/admin/user/reset_password.mako @@ -0,0 +1,35 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +
            +
            Reset user password
            +
            +
            + +
            + + ${user.email} +
            +
            +
            + +
            + +
            +
            +
            +
            + +
            + +
            +
            +
            + +
            +
            +
            diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index dff00121fc0..9288f4c58da 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -362,16 +362,13 @@ class TwillTestCase( unittest.TestCase ): self.assertTrue( genome_build == dbkey ) # Functions associated with user accounts - def create( self, email='test@bx.psu.edu', password='testuser', confirm='testuser' ): - self.visit_page( "user/create?email=%s&password=%s&confirm=%s" %(email, password, confirm) ) - try: - self.check_page_for_string( "User with that email already exists" ) - except: - self.check_page_for_string( "Now logged in as %s" %email ) - self.home() - # Make sure a new private role was created for the user - self.visit_page( "user/set_default_permissions" ) - self.check_page_for_string( email ) + def create( self, email='test@bx.psu.edu', password='testuser' ): + self.visit_page( "user/create?email=%s&password=%s&confirm=%s" % ( email, password, password ) ) + self.check_page_for_string( "Now logged in as %s" %email ) + self.home() + # Make sure a new private role was created for the user + self.visit_page( "user/set_default_permissions" ) + self.check_page_for_string( email ) self.home() def user_set_default_permissions( self, permissions_out=[], permissions_in=[], role_id=2 ): # role.id = 2 is Private Role for test2@bx.psu.edu # NOTE: Twill has a bug that requires the ~/user/permissions page to contain at least 1 option value @@ -405,14 +402,19 @@ class TwillTestCase( unittest.TestCase ): self.last_page() self.check_page_for_string( 'Default history permissions have been changed.' ) self.home() - def login( self, email='test@bx.psu.edu', password='testuser'): + def login( self, email='test@bx.psu.edu', password='testuser' ): # test@bx.psu.edu is configured as an admin user - self.create( email=email, password=password, confirm=password ) - self.visit_page( "user/login?email=%s&password=%s" % (email, password) ) - self.check_page_for_string( "Now logged in as %s" %email ) - self.home() + try: + self.create( email=email, password=password ) + except: + self.home() + self.visit_page( "user/login?email=%s&password=%s" % ( email, password ) ) + self.last_page() + self.check_page_for_string( "Now logged in as %s" %email ) + self.home() def logout( self ): self.visit_page( "user/logout" ) + self.last_page() self.check_page_for_string( "You are no longer logged in" ) self.home() # Functions associated with browsers, cookies, HTML forms and page visits @@ -581,7 +583,41 @@ class TwillTestCase( unittest.TestCase ): self.assertNotEqual(count, maxiter) # Dataset Security stuff - def create_role( self, name='New Test Role', description="Very cool new test role", user_ids=[], group_ids=[], private_role='' ): + def create_new_account_as_admin( self, email='test4@bx.psu.edu', password='testuser' ): + """Create a new account for another user""" + self.visit_url( "%s/admin/create_new_user?email=%s&password=%s&confirm=%s&user_create_button=%s" \ + % ( self.url, email, password, password, 'Create' ) ) + self.last_page() + self.check_page_for_string( "Created new user account" ) + self.home() + def reset_password_as_admin( self, user_id=4, password='testreset' ): + """Reset a user password""" + self.visit_url( "%s/admin/reset_user_password?user_id=%s" % ( self.url, str( user_id ) ) ) + tc.fv( "1", "password", password ) + tc.fv( "1", "confirm", password ) + tc.submit( "reset_user_password_button" ) + self.last_page() + self.check_page_for_string( "Password reset" ) + self.home() + def mark_user_deleted( self, user_id=4 ): + """Mark a user as deleted""" + self.visit_url( "%s/admin/mark_user_deleted?user_id=%s" % ( self.url, str( user_id ) ) ) + self.last_page() + self.check_page_for_string( "The user has been marked as deleted." ) + self.home() + def undelete_user( self, user_id ): + """Undelete a user""" + self.visit_url( "%s/admin/undelete_user?user_id=%s" % ( self.url, user_id ) ) + self.last_page() + self.check_page_for_string( 'The user has been marked as not deleted' ) + self.home() + def purge_user( self, user_id ): + """Purge a user account""" + self.visit_url( "%s/admin/purge_user?user_id=%s" % ( self.url, user_id ) ) + self.last_page() + self.check_page_for_string( 'The user has been marked as purged.' ) + self.home() + def create_role( self, name='Role One', description="This is Role One", user_ids=[], group_ids=[], private_role='' ): """Create a new role""" self.visit_url( "%s/admin/create_role" % self.url ) form = tc.show() @@ -632,15 +668,15 @@ class TwillTestCase( unittest.TestCase ): self.last_page() self.check_page_for_string( 'The role has been marked as not deleted' ) self.home() - def purge_role( self, role_id, deleted=False ): + def purge_role( self, role_id ): """Purge an existing role""" - if not deleted: - self.mark_role_deleted( role_id ) self.visit_url( "%s/admin/purge_role?role_id=%s" % ( self.url, role_id ) ) self.last_page() - self.check_page_for_string( 'The role has been purged from the database' ) + msg = "The following have been purged from the database for the role: " + msg += "DefaultUserPermissions, DefaultHistoryPermissions, UserRoleAssociations, GroupRoleAssociations, ActionDatasetRoleAssociations." + self.check_page_for_string( msg ) self.home() - def create_group( self, name='New Test Group', user_ids=[], role_ids=[] ): + def create_group( self, name='Group One', user_ids=[], role_ids=[] ): """Create a new group with 2 members and 1 associated role""" self.visit_url( "%s/admin/create_group" % self.url ) form = tc.show() @@ -685,23 +721,17 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create group: %s' % str( err ) ) self.home() - def associate_groups_with_role( self, role_id, group_ids=[] ): + def associate_groups_with_role( self, role_id, group_names=[] ): """Add groups to an existing role""" # NOTE: To get this to work with twill, all select lists must contain at least 1 option value - # or twill throws an exception, which is: ParseError: OPTION outside of SELECT + # before tc.submit or twill throws an exception, which is: ParseError: OPTION outside of SELECT self.visit_url( "%s/admin/role?role_id=%s" % ( self.url, role_id ) ) self.check_page_for_string( 'Groups associated with' ) - # All groups must be in the out_groups form field - try: - for group in groups: - tc.fv( "1", "7", group_id ) # form field 7 is the select list named out_groups, note the buttons... - tc.submit( "groups_add_button" ) - tc.submit( "role_button" ) - except AssertionError, err: - self.home() - raise AssertionError( 'Exception caught attempting to associated groups with a role: %s' % str( err ) ) - except: - pass + # All group_ids passed in MUST be in the out_groups form field + for group_name in group_names: + tc.fv( "1", "out_groups", group_name ) # note the buttons... + tc.submit( "groups_add_button" ) + tc.submit( "role_button" ) self.home() def mark_group_deleted( self, group_id ): """Mark a group as deleted""" @@ -715,17 +745,15 @@ class TwillTestCase( unittest.TestCase ): self.last_page() self.check_page_for_string( 'The group has been marked as not deleted' ) self.home() - def purge_group( self, group_id, deleted=False ): + def purge_group( self, group_id ): """Purge an existing group""" - if not deleted: - self.mark_group_deleted( group_id ) self.visit_url( "%s/admin/purge_group?group_id=%s" % ( self.url, group_id ) ) self.last_page() - self.check_page_for_string( 'The group has been purged from the database' ) + self.check_page_for_string( "The following have been purged from the database for the group: UserGroupAssociations, GroupRoleAssociations." ) self.home() # Library stuff - def create_library( self, name='New Test Library', description='New Test Library Description' ): + def create_library( self, name='Library One', description='This is Library One' ): """Create a new library""" try: self.visit_url( "%s/admin/library?new=True" % self.url ) @@ -738,7 +766,7 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create library: %s' % str( err ) ) self.home() - def rename_library( self, library_id, name='New Test Library Renamed', description='New Test Library Description Re-described', root_folder='' ): + def rename_library( self, library_id, name='Library One Renamed', description='This is Library One Re-described', root_folder='' ): """Rename a library""" try: self.visit_url( "%s/admin/library?rename=True&id=%s" % ( self.url, library_id ) ) @@ -759,7 +787,7 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to rename a library: %s' % str( err ) ) self.home() - def add_folder( self, folder_id, name='New Test Folder', description='New Test Folder Description' ): + def add_folder( self, folder_id, name='Folder One', description='NThis is Folder One' ): """Create a new folder""" try: self.visit_url( "%s/admin/folder?id=%s&new=True" % ( self.url, folder_id ) ) @@ -772,7 +800,7 @@ class TwillTestCase( unittest.TestCase ): self.home() raise AssertionError( 'Exception caught attempting to create a new folder: %s' % str( err ) ) self.home() - def rename_folder( self, folder_id, name='New Test Folder Renamed', description='New Test Folder Description Re-described' ): + def rename_folder( self, folder_id, name='Folder One Renamed', description='This is Folder One Re-described' ): """Rename a Folder""" try: self.visit_url( "%s/admin/folder?rename=True&id=%s" % ( self.url, folder_id ) ) @@ -811,7 +839,6 @@ class TwillTestCase( unittest.TestCase ): # Create a new history self.new_history() self.upload_file( "1.bed" ) - self.verify_dataset_correctness( "1.bed" ) self.visit_url( "%s/admin/add_dataset_to_folder_from_history?folder_id=%s" % ( self.url, folder_id ) ) self.last_page() self.check_page_for_string( 'Active datasets in your current history' ) @@ -822,7 +849,7 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( 'Added the following datasets to the library folder: 1.bed' ) except AssertionError, err: self.home() - raise AssertionError( 'Exception caught attempting to create add a dataset to a folder: %s' % str( err ) ) + raise AssertionError( 'Exception caught attempting to add a dataset to a folder: %s' % str( err ) ) self.home() def add_datasets_from_library_dir( self, folder_id, extension='auto', dbkey='hg18', roles_tuple=[] ): """Add a directory of datasets to a folder""" diff --git a/test/functional/__init__.py b/test/functional/__init__.py index 828bb247ef6..95722061d54 100644 --- a/test/functional/__init__.py +++ b/test/functional/__init__.py @@ -37,7 +37,6 @@ def setup(): galaxy_test_port = os.environ.get( 'GALAXY_TEST_PORT', default_galaxy_test_port ) start_server = 'GALAXY_TEST_EXTERNAL' not in os.environ - if start_server: if 'GALAXY_TEST_DBPATH' in os.environ: db_path = os.environ['GALAXY_TEST_DBPATH'] @@ -73,10 +72,12 @@ def setup(): tool_path = "tools", test_conf = "test.conf", log_destination = "stdout", - use_heartbeat=False, + use_heartbeat = False, + allow_user_creation = True, + allow_user_deletion = True, admin_users = 'test@bx.psu.edu', library_import_dir = galaxy_test_file_dir, - global_conf= { "__file__": "universe_wsgi.ini.sample" } ) + global_conf = { "__file__": "universe_wsgi.ini.sample" } ) log.info( "Embedded Universe application started" ) diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index 8a95da8122e..f8a8a1664d7 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -1,4 +1,3 @@ -import sys import galaxy.model from galaxy.model.orm import * from base.twilltestcase import * @@ -6,7 +5,7 @@ from base.twilltestcase import * not_logged_in_security_msg = 'You must be logged in as an administrator to access this feature.' logged_in_security_msg = 'You must be an administrator to access this feature.' -class TestHistory( TwillTestCase ): +class TestSecurityAndLibraries( TwillTestCase ): def test_00_admin_features_when_not_logged_in( self ): """Testing admin_features when not logged in""" self.logout() @@ -113,9 +112,11 @@ class TestHistory( TwillTestCase ): self.new_history() latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() if not latest_history.default_permissions: - raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % latest_history.id ) + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % \ + latest_history.id ) if len( latest_history.default_permissions ) != len( galaxy.model.Dataset.permitted_actions.items() ): - raise AssertionError( '%d DefaultHistoryPermissions were created for history id %d, should have been %d' % ( len( latest_history.default_permissions ), latest_history.id, len( galaxy.model.Dataset.permitted_actions ) ) ) + raise AssertionError( '%d DefaultHistoryPermissions were created for history id %d, should have been %d' % \ + ( len( latest_history.default_permissions ), latest_history.id, len( galaxy.model.Dataset.permitted_actions ) ) ) dhps = [] for dhp in latest_history.default_permissions: dhps.append( dhp.action ) @@ -131,7 +132,8 @@ class TestHistory( TwillTestCase ): if not latest_dataset.actions: raise AssertionError( 'No ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) if len( latest_dataset.actions ) != len( latest_history.default_permissions ): - raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' % ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) + raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' % \ + ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) adras = [] for adra in latest_dataset.actions: adras.append( adra.action ) @@ -171,7 +173,8 @@ class TestHistory( TwillTestCase ): # Change DefaultHistoryPermissions for the current history self.history_set_default_permissions( permissions_out=permissions_out, permissions_in=permissions_in, role_id=role_id ) if not latest_history.default_permissions: - raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % latest_history.id ) + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when DefaultHistoryPermissions were changed' % \ + latest_history.id ) if len( latest_history.default_permissions ) != len( actions_in ): raise AssertionError( '%d DefaultHistoryPermissions were created for history id %d, should have been %d' \ % ( len( latest_history.default_permissions ), latest_history.id, len( permissions_in ) ) ) @@ -189,7 +192,8 @@ class TestHistory( TwillTestCase ): if not latest_dataset.actions: raise AssertionError( 'No ActionDatasetRoleAssociations were created for dataset id %d when it was created' % latest_dataset.id ) if len( latest_dataset.actions ) != len( latest_history.default_permissions ): - raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' % ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) + raise AssertionError( '%d ActionDatasetRoleAssociations were created for dataset id %d when it was created ( should have been %d )' \ + % ( len( latest_dataset.actions ), latest_dataset.id, len( latest_history.default_permissions ) ) ) adras = [] for adra in latest_dataset.actions: adras.append( adra.action ) @@ -197,55 +201,201 @@ class TestHistory( TwillTestCase ): adras.sort() self.home() self.logout() - def test_12_create_role( self ): - """Testing creating new non-private role with 2 members""" - self.login( email=testuser1.email ) - name = 'New Test Role' - description = 'Very cool new test role' - self.create_role( name=name, description=description, user_ids=[ str( testuser1.id ), str( testuser2.id ) ], private_role=testuser1.email ) + def test_12_create_new_user_account_as_admin( self ): + """Testing creating a new user account as admin""" + self.login( email='test@bx.psu.edu' ) + email = 'test4@bx.psu.edu' + password = 'testuser' + self.create_new_account_as_admin( email=email, password=password ) + # Get the user object for later tests + global testuser4 + testuser4 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test4@bx.psu.edu' ).first() + # Make sure DefaultUserPermissions were created + if not testuser4.default_permissions: + raise AssertionError( 'No DefaultUserPermissions were created for user %s when the admin created the account' % email ) + # Make sure a private role was created for the user + if not testuser4.roles: + raise AssertionError( 'No UserRoleAssociations were created for user %s when the admin created the account' % email ) + if len( testuser4.roles ) != 1: + raise AssertionError( '%d UserRoleAssociations were created for user %s when the admin created the account ( should have been 1 )' \ + % len( testuser4.roles ) ) + for ura in testuser4.roles: + role = galaxy.model.Role.get( ura.role_id ) + if role.type != 'private': + raise AssertionError( 'Role created for user %s when the admin created the account is not private, type is' \ + % str( role.type ) ) + # Make sure a history was not created + histories = galaxy.model.History.filter( galaxy.model.History.table.c.user_id==testuser4.id ).all() + if histories: + raise AssertionError( 'Histories were incorrectly created for user %s when the admin created the account' % email ) + # Make sure the user was not associated with any groups + if testuser4.groups: + raise AssertionError( 'Groups were incorrectly associated with user %s when the admin created the account' % email ) + def test_15_reset_password_as_admin( self ): + """Testing reseting a user password as admin""" + email = 'test4@bx.psu.edu' + self.reset_password_as_admin( user_id=testuser4.id, password='testreset' ) + self.home() + self.logout() + def test_18_login_after_password_reset( self ): + """Testing logging in after an admin reset a password - tests DefaultHistoryPermissions for accounts created by an admin""" + self.login( email='test4@bx.psu.edu', password='testreset' ) + # Make sure a History and HistoryDefaultPermissions exist for the user + latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + if not latest_history.user_id == testuser4.id: + raise AssertionError( 'A history was not created for user %s when he logged in' % email ) + if not latest_history.default_permissions: + raise AssertionError( 'No DefaultHistoryPermissions were created for history id %d when it was created' % latest_history.id ) + if len( latest_history.default_permissions ) > 1: + raise AssertionError( 'More than 1 DefaultHistoryPermissions were created for history id %d when it was created' % latest_history.id ) + dhp = galaxy.model.DefaultHistoryPermissions.filter( galaxy.model.DefaultHistoryPermissions.table.c.history_id==latest_history.id ).first() + if not dhp.action == galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action: + raise AssertionError( 'The DefaultHistoryPermission.action for history id %d is "%s", but it should be "%s"' \ + % ( latest_history.id, dhp.action, galaxy.model.Dataset.permitted_actions.DATASET_MANAGE_PERMISSIONS.action ) ) + # Upload a file to create a HistoryDatasetAssociation + self.upload_file( '1.bed' ) + self.home() + self.logout() + def test_21_mark_user_deleted( self ): + """Testing marking a user account as deleted""" + self.login( email='test@bx.psu.edu' ) + self.mark_user_deleted( user_id=testuser4.id ) + def test_24_undelete_user( self ): + """Testing undeleting a user account""" + self.undelete_user( user_id=testuser4.id ) + def test_27_create_role( self ): + """Testing creating new non-private role with 3 members""" + name = 'Role One' + description = 'This is Role One' + user_ids=[ str( testuser1.id ), str( testuser2.id ), str( testuser4.id ) ] + self.create_role( name=name, description=description, user_ids=user_ids, private_role=testuser1.email ) # Get the role object for later tests - global new_test_role - new_test_role = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() - def test_15_create_group( self ): - """Testing creating new group with 2 members and 1 associated role""" - name = 'New Test Group' - self.create_group( name=name, user_ids=[ str( testuser1.id ), str( testuser2.id ) ], role_ids=[ str( new_test_role.id ) ] ) + global role_one + role_one = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() + # Make sure UserRoleAssociations are correct + if not role_one.users: + raise AssertionError( 'No UserRoleAssociations were created for role id %d when it was created with 3 members' % role_one.id ) + if len( role_one.users ) != len( user_ids ): + raise AssertionError( '%d UserRoleAssociations were created for role id %d when it was created ( should have been %d )' \ + % ( len( role_one.users ), role_one.id, len( user_ids ) ) ) + # Each user should now have 2 role associations, their private role and role_one + for user in [ testuser1, testuser2, testuser4 ]: + user.refresh() + if not user.roles: + raise AssertionError( 'No UserRoleAssociations were created for user %s when a new role was created' % user.email ) + if len( user.roles ) != 2: + raise AssertionError( '%d UserRoleAssociations are associated with user %s ( should be 2 )' % ( len( user.roles ), user.email ) ) + def test_30_create_group( self ): + """Testing creating new group with 3 members and 1 associated role""" + name = 'Group One' + user_ids=[ str( testuser1.id ), str( testuser2.id ), str( testuser4.id ) ] + role_ids=[ str( role_one.id ) ] + self.create_group( name=name, user_ids=user_ids, role_ids=role_ids ) # Get the group object for later tests - global new_test_group - new_test_group = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() - def test_18_add_group_member( self ): + global group_one + group_one = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() + # Make sure UserGroupAssociations are correct + if not group_one.users: + raise AssertionError( 'No UserGroupAssociations were created for group id %d when it was created with 3 members' % group_one.id ) + if len( group_one.users ) != len( user_ids ): + raise AssertionError( '%d UserGroupAssociations were created for group id %d when it was created ( should have been %d )' \ + % ( len( group_one.users ), group_one.id, len( user_ids ) ) ) + # Each user should now have 1 group association, group_one + for user in [ testuser1, testuser2, testuser4 ]: + user.refresh() + if not user.groups: + raise AssertionError( 'No UserGroupAssociations were created for user %s when a new group was created' % user.email ) + if len( user.groups ) != 1: + raise AssertionError( '%d UserGroupAssociations are associated with user %s ( should be 1 )' % ( len( user.groups ), user.email ) ) + # Make sure GroupRoleAssociations are correct + if not group_one.roles: + raise AssertionError( 'No GroupRoleAssociations were created for group id %d when it was created with 3 members' % group_one.id ) + if len( group_one.roles ) != len( role_ids ): + raise AssertionError( '%d GroupRoleAssociations were created for group id %d when it was created ( should have been %d )' \ + % ( len( group_one.roles ), group_one.id, len( role_ids ) ) ) + def test_33_add_group_member( self ): """Testing editing membership of an existing group""" - name = 'Another Test Group' + name = 'Group Two' self.create_group( name=name ) # Get the group object for later tests - global another_test_group - another_test_group = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() - self.add_group_members( str( another_test_group.id ), [ str( testuser3.id ) ] ) - self.visit_url( "%s/admin/group_members_edit?group_id=%s" % ( self.url, str( another_test_group.id ) ) ) + global group_two + group_two = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() + user_ids = [ str( testuser3.id ) ] + self.add_group_members( str( group_two.id ), user_ids ) + self.visit_url( "%s/admin/group_members_edit?group_id=%s" % ( self.url, str( group_two.id ) ) ) self.check_page_for_string( testuser3.email ) - def test_21_associate_groups_with_role( self ): + # Make sure UserGroupAssociations are correct + if not group_two.users: + raise AssertionError( 'No UserGroupAssociations were created for group id %d when %d members were added' \ + % ( group_two.id, len( user_ids ) ) ) + if len( group_two.users ) != len( user_ids ): + raise AssertionError( '%d UserGroupAssociations were created for group id %d when %d members were added' \ + % ( len( group_two.users ), group_two.id, len( user_ids ) ) ) + # Create another group -needed for the following test + name = 'Group Three' + self.create_group( name=name ) + # Get the group object for later tests + global group_three + group_three = galaxy.model.Group.filter( galaxy.model.Group.table.c.name==name ).first() + def test_36_associate_groups_with_role( self ): """Testing adding existing groups to an existing role""" # NOTE: To get this to work with twill, all select lists on the ~/admin/role page must contain at least # 1 option value or twill throws an exception, which is: ParseError: OPTION outside of SELECT - # Due to this bug in twill, we create the role, associating it with at least 1 user and 1 group... - name = 'Another Test Role' - description = 'Another cool new test role' + # Due to this bug in twill, we create the role, associating it with at least 1 user and 1 group. We + # also must ensure that each of the form fields will contain at least 1 value prior to submitting the form, + # so we had to create group_three in the previous test + name = 'Role Two' + description = 'This is Role Two' + user_ids=[ str( testuser1.id ) ] + group_ids=[ str( group_two.id ) ] + private_role=testuser1.email + # STEP 1: create the role self.create_role( name=name, description=description, - user_ids=[ str( testuser1.id ) ], - group_ids=[ str( another_test_group.id ) ], - private_role=testuser1.email ) + user_ids=user_ids, + group_ids=group_ids, + private_role=private_role ) # Get the role object for later tests - global another_test_role - another_test_role = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() - # ...and then we associate the role with a group not yet associated - self.associate_groups_with_role( str( another_test_role.id ), group_ids=[ str( new_test_group.id ) ] ) - self.visit_page( 'admin/roles' ) - self.check_page_for_string( new_test_group.name ) - def test_24_create_library( self ): + global role_two + role_two = galaxy.model.Role.filter( galaxy.model.Role.table.c.name==name ).first() + # Make sure UserRoleAssociations are correct + if not role_two.users: + raise AssertionError( 'No UserRoleAssociations were created for role id %d when it was created with %d members' \ + % ( role_two.id, len( user_ids ) ) ) + if len( role_two.users ) != len( user_ids ): + raise AssertionError( '%d UserRoleAssociations were created for role id %d when it was created with %d members' \ + % ( len( role_two.users ), role_two.id, len( user_ids ) ) ) + # testuser1 should now have 3 role associations, private role, role_one, another+test_role + for user in [ testuser1 ]: + user.refresh() + if not user.roles: + raise AssertionError( 'No UserRoleAssociations were created for user %s when a new role was created' % user.email ) + if len( user.roles ) != 3: + raise AssertionError( '%d UserRoleAssociations are associated with user %s ( should be 3 )' % ( len( user.roles ), user.email ) ) + # Make sure GroupRoleAssociations are correct + if not role_two.groups: + raise AssertionError( 'No GroupRoleAssociations were created for role id %d when it was created with %d groups' \ + % ( role_two.id, len( group_ids ) ) ) + if len( role_two.groups ) != len( group_ids ): + raise AssertionError( '%d GroupRoleAssociations were created for role id %d when it was created ( should have been %d )' \ + % ( len( role_two.groups ), role_two.id, len( group_ids ) ) ) + # The group should also now be associated with 1 role + for group in [ group_two ]: + group.refresh() + if not group.roles: + raise AssertionError( 'No GroupRoleAssociations were created for group id %d when a new role was created' % group.id ) + if len( group.roles ) != 1: + raise AssertionError( '%d GroupRoleAssociations are associated with group id %d ( should be 1 )' % ( len( group.roles ), group.id ) ) + # STEP 2: associate the role with a group not yet associated + # TODO: Twill throws an exception on this... + #group_names = [ group_one.name ] + #self.associate_groups_with_role( str( role_two.id ), group_names=group_names ) + #self.visit_page( 'admin/roles' ) + #self.check_page_for_string( group_one.name ) + def test_39_create_library( self ): """Testing creating new library""" - name = 'New Test Library' - description = 'New Test Library Description' + name = 'Library One' + description = 'This is Library One' self.create_library( name=name, description=description ) self.visit_page( 'admin/libraries' ) self.check_page_for_string( name ) @@ -254,20 +404,20 @@ class TestHistory( TwillTestCase ): library = galaxy.model.Library.filter( and_( galaxy.model.Library.table.c.name==name, galaxy.model.Library.table.c.description==description, galaxy.model.Library.table.c.deleted==False ) ).first() - def test_27_rename_library( self ): + def test_42_rename_library( self ): """Testing renaming a library""" - self.rename_library( str( library.id ), name='New Test Library Renamed', description='New Test Library Description Re-described', root_folder='on' ) + self.rename_library( str( library.id ), name='Library One Renamed', description='This is Library One Re-described', root_folder='on' ) self.visit_page( 'admin/libraries' ) - self.check_page_for_string( "New Test Library Renamed" ) + self.check_page_for_string( "Library One Renamed" ) # Rename it back to what it was originally - self.rename_library( str( library.id ), name='New Test Library', description='New Test Library Description', root_folder='on' ) - def test_30_rename_root_folder( self ): + self.rename_library( str( library.id ), name='Library One', description='This is Library One', root_folder='on' ) + def test_45_rename_root_folder( self ): """Testing renaming a library root folder""" folder = library.root_folder - self.rename_folder( str( folder.id ), name='New Test Library Root Folder', description='New Test Library Root Folder Description' ) + self.rename_folder( str( folder.id ), name='Library One Root Folder', description='This is Library One root folder' ) self.visit_page( 'admin/libraries' ) - self.check_page_for_string( "New Test Library Root Folder" ) - def test_33_add_public_dataset_to_root_folder( self ): + self.check_page_for_string( "Library One Root Folder" ) + def test_48_add_public_dataset_to_root_folder( self ): """Testing adding a public dataset to a library root folder""" folder = library.root_folder self.add_dataset( '1.bed', str( folder.id ), extension='bed', dbkey='hg18', roles=[] ) @@ -275,7 +425,7 @@ class TestHistory( TwillTestCase ): self.check_page_for_string( "1.bed" ) self.check_page_for_string( "bed" ) self.check_page_for_string( "hg18" ) - def test_36_copy_dataset_from_history_to_root_folder( self ): + def test_51_copy_dataset_from_history_to_root_folder( self ): """Testing copying a dataset from the current history to a library root folder""" folder = library.root_folder self.add_dataset_to_folder_from_history( str( folder.id ) ) @@ -289,40 +439,41 @@ class TestHistory( TwillTestCase ): raise AssertionError( 'More than 1 ActionDatasetRoleAssociations created for dataset id: %d' % last_dataset_created.id ) for adra in adras: if not adra.action == 'manage permissions': - raise AssertionError( 'ActionDatasetRoleAssociation.action "%s" is not the DefaultHistoryPermission setting, which is "manage permissions"' % str( adra.action ) ) - def test_39_add_new_folder( self ): + raise AssertionError( 'ActionDatasetRoleAssociation.action "%s" is not the DefaultHistoryPermission setting, which is "manage permissions"' % \ + str( adra.action ) ) + def test_54_add_new_folder( self ): """Testing adding a folder to a library root folder""" root_folder = library.root_folder - name = 'New Test Folder' - description = 'New Test Folder Description' + name = 'Folder One' + description = 'This is Folder One' self.add_folder( str( root_folder.id ), name=name, description=description ) global new_test_folder new_test_folder = galaxy.model.LibraryFolder.filter( and_( galaxy.model.LibraryFolder.table.c.parent_id==root_folder.id, galaxy.model.LibraryFolder.table.c.name==name, galaxy.model.LibraryFolder.table.c.description==description ) ).first() self.visit_page( 'admin/libraries' ) - self.check_page_for_string( "New Test Folder" ) - def test_42_add_datasets_from_library_dir( self ): + self.check_page_for_string( "Folder One" ) + def test_57_add_datasets_from_library_dir( self ): """Testing adding several datasets from library directory to sub-folder""" - roles_tuple = [ ( str( new_test_role.id ), new_test_role.description ) ] + roles_tuple = [ ( str( role_one.id ), role_one.description ) ] self.add_datasets_from_library_dir( str( new_test_folder.id ), roles_tuple=roles_tuple ) - def test_45_mark_group_deleted( self ): + def test_60_mark_group_deleted( self ): """Testing marking a group as deleted""" self.visit_page( "admin/groups" ) - self.check_page_for_string( another_test_group.name ) - self.mark_group_deleted( str( another_test_group.id ) ) - def test_48_undelete_group( self ): + self.check_page_for_string( group_two.name ) + self.mark_group_deleted( str( group_two.id ) ) + def test_63_undelete_group( self ): """Testing undeleting a deleted group""" - self.undelete_group( str( another_test_group.id ) ) - def test_51_mark_role_deleted( self ): + self.undelete_group( str( group_two.id ) ) + def test_66_mark_role_deleted( self ): """Testing marking a role as deleted""" self.visit_page( "admin/roles" ) - self.check_page_for_string( another_test_role.description ) - self.mark_role_deleted( str( another_test_role.id ) ) - def test_54_undelete_role( self ): + self.check_page_for_string( role_two.description ) + self.mark_role_deleted( str( role_two.id ) ) + def test_69_undelete_role( self ): """Testing undeleting a deleted role""" - self.undelete_role( str( another_test_role.id ) ) - def test_57_mark_library_deleted( self ): + self.undelete_role( str( role_two.id ) ) + def test_72_mark_library_deleted( self ): """Testing marking a library as deleted""" self.mark_library_deleted( str( library.id ) ) # Make sure the library was deleted @@ -334,19 +485,21 @@ class TestHistory( TwillTestCase ): folder.refresh() # Make sure all of the library_folders are deleted if not folder.deleted: - raise AssertionError( 'The library_folder named "%s" has not been marked as deleted ( library.id: %s ).' % ( folder.name, str( library.id ) ) ) + raise AssertionError( 'The library_folder named "%s" has not been marked as deleted ( library.id: %s ).' % \ + ( folder.name, str( library.id ) ) ) check_folder( folder ) # Make sure all of the library_folder_dataset_associations are deleted for lfda in library_folder.datasets: lfda.refresh() if not lfda.deleted: - raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as deleted ( library.id: %s ).' % ( str( lfda.id ), lfda.name, str( library.id ) ) ) + raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as deleted ( library.id: %s ).' % \ + ( str( lfda.id ), lfda.name, str( library.id ) ) ) # Make sure none of the datasets have been deleted since that should occur only when the library is purged lfda.dataset.refresh() if lfda.dataset.deleted: raise AssertionError( 'The dataset with id "%s" has been marked as deleted when it should not have been.' % lfda.dataset.id ) check_folder( library.root_folder ) - def test_60_mark_library_undeleted( self ): + def test_75_mark_library_undeleted( self ): """Testing marking a library as not deleted""" self.mark_library_undeleted( str( library.id ) ) # Make sure the library is undeleted @@ -358,13 +511,15 @@ class TestHistory( TwillTestCase ): folder.refresh() # Make sure all of the library_folders are undeleted if folder.deleted: - raise AssertionError( 'The library_folder id %s named "%s" has not been marked as undeleted ( library.id: %s ).' % ( str( folder.id ), folder.name, str( library.id ) ) ) + raise AssertionError( 'The library_folder id %s named "%s" has not been marked as undeleted ( library.id: %s ).' % \ + ( str( folder.id ), folder.name, str( library.id ) ) ) check_folder( folder ) # Make sure all of the library_folder_dataset_associations are undeleted for lfda in library_folder.datasets: lfda.refresh() if lfda.deleted: - raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as undeleted ( library.id: %s ).' % ( str( lfda.id ), lfda.name, str( library.id ) ) ) + raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as undeleted ( library.id: %s ).' % \ + ( str( lfda.id ), lfda.name, str( library.id ) ) ) # Make sure all of the datasets have been undeleted if lfda.dataset.deleted: raise AssertionError( 'The dataset with id "%s" has not been marked as undeleted.' % lfda.dataset.id ) @@ -374,10 +529,55 @@ class TestHistory( TwillTestCase ): # Make sure the library is deleted again library.refresh() if not library.deleted: - raise AssertionError( 'The library id %s named "%s" has not been marked as deleted after it was undeleted.' % ( str( library.id ), library.name ) ) - def test_63_purge_group( self ): + raise AssertionError( 'The library id %s named "%s" has not been marked as deleted after it was undeleted.' % \ + ( str( library.id ), library.name ) ) + def test_78_purge_user( self ): + """Testing purging a user account""" + self.mark_user_deleted( user_id=testuser4.id ) + self.purge_user( user_id=testuser4.id ) + testuser4.refresh() + if not testuser4.purged: + raise AssertionError( 'User %s was not marked as purged.' % testuser4.email ) + # Make sure DefaultUserPermissions deleted EXCEPT FOR THE PRIVATE ROLE + if len( testuser4.default_permissions ) != 1: + raise AssertionError( 'DefaultUserPermissions for user %s were not deleted.' % testuser4.email ) + for dup in testuser4.default_permissions: + role = galaxy.model.Role.get( dup.role_id ) + if role.type != 'private': + raise AssertionError( 'DefaultUserPermissions for user %s are not related with the private role.' % testuser4.email ) + # Make sure History deleted + for history in testuser4.histories: + if not history.deleted: + raise AssertionError( 'User %s has active history id %d after their account was marked as purged.' % ( testuser4.email, hda.id ) ) + # Make sure DefaultHistoryPermissions deleted EXCEPT FOR THE PRIVATE ROLE + if len( history.default_permissions ) != 1: + raise AssertionError( 'DefaultHistoryPermissions for history id %d were not deleted.' % history.id ) + for dhp in history.default_permissions: + role = galaxy.model.Role.get( dhp.role_id ) + if role.type != 'private': + raise AssertionError( 'DefaultHistoryPermissions for history id %d are not related with the private role.' % history.id ) + # Make sure HistoryDatasetAssociation deleted + for hda in history.datasets: + if not hda.deleted: + raise AssertionError( 'HistoryDatasetAssociation id %d was not deleted.' % hda.id ) + # Make sure Dataset deleted + d = galaxy.model.Dataset.filter( galaxy.model.Dataset.table.c.id==hda.dataset_id ).first() + if not d.deleted: + raise AssertionError( 'Dataset id %d was not deleted.' % d.id ) + # Make sure UserGroupAssociations deleted + if testuser4.groups: + raise AssertionError( 'User %s has active group id %d after their account was marked as purged.' % ( testuser4.email, uga.id ) ) + # Make sure UserRoleAssociations deleted EXCEPT FOR THE PRIVATE ROLE + if len( testuser4.roles ) != 1: + raise AssertionError( 'UserRoleAssociations for user %s were not deleted.' % testuser4.email ) + for ura in testuser4.roles: + role = galaxy.model.Role.get( ura.role_id ) + if role.type != 'private': + raise AssertionError( 'UserRoleAssociations for user %s are not related with the private role.' % testuser4.email ) + def test_81_purge_group( self ): """Testing purging a group""" - group_id = str( another_test_group.id ) + group_id = str( group_two.id ) + self.mark_group_deleted( group_id ) self.purge_group( group_id ) # Make sure there are no UserGroupAssociations uga = galaxy.model.UserGroupAssociation.filter( galaxy.model.UserGroupAssociation.table.c.group_id == group_id ).all() @@ -387,10 +587,23 @@ class TestHistory( TwillTestCase ): gra = galaxy.model.GroupRoleAssociation.filter( galaxy.model.GroupRoleAssociation.table.c.group_id == group_id ).all() if gra: raise AssertionError( "Purging the group did not delete the GroupRoleAssociations for group_id '%s'" % group_id ) - def test_66_purge_role( self ): + def test_84_purge_role( self ): """Testing purging a role""" - role_id = str( another_test_role.id ) + role_id = str( role_two.id ) + self.mark_role_deleted( role_id ) self.purge_role( role_id ) + # Make sure there are no UserRoleAssociations + uras = galaxy.model.UserRoleAssociation.filter( galaxy.model.UserRoleAssociation.table.c.role_id == role_id ).all() + if uras: + raise AssertionError( "Purging the role did not delete the UserRoleAssociations for role_id '%s'" % role_id ) + # Make sure there are no DefaultUserPermissions associated with the Role + dups = galaxy.model.DefaultUserPermissions.filter( galaxy.model.DefaultUserPermissions.table.c.role_id == role_id ).all() + if dups: + raise AssertionError( "Purging the role did not delete the DefaultUserPermissions for role_id '%s'" % role_id ) + # Make sure there are no DefaultHistoryPermissions associated with the Role + dhps = galaxy.model.DefaultHistoryPermissions.filter( galaxy.model.DefaultHistoryPermissions.table.c.role_id == role_id ).all() + if dhps: + raise AssertionError( "Purging the role did not delete the DefaultHistoryPermissions for role_id '%s'" % role_id ) # Make sure there are no GroupRoleAssociations gra = galaxy.model.GroupRoleAssociation.filter( galaxy.model.GroupRoleAssociation.table.c.role_id == role_id ).all() if gra: @@ -399,7 +612,7 @@ class TestHistory( TwillTestCase ): adra = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.role_id == role_id ).all() if adra: raise AssertionError( "Purging the role did not delete the ActionDatasetRoleAssociations for role_id '%s'" % role_id ) - def test_69_purge_library( self ): + def test_87_purge_library( self ): """Testing purging a library""" self.purge_library( str( library.id ) ) # Make sure the library was purged @@ -417,10 +630,12 @@ class TestHistory( TwillTestCase ): for lfda in library_folder.datasets: lfda.refresh() if not lfda.deleted: - raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as deleted.' % ( str( lfda.id ), lfda.name ) ) + raise AssertionError( 'The library_folder_dataset_association id %s named "%s" has not been marked as deleted.' % \ + ( str( lfda.id ), lfda.name ) ) # Make sure all of the datasets have been deleted dataset = lfda.dataset dataset.refresh() if not dataset.deleted: - raise AssertionError( 'The dataset with id "%s" has not been marked as deleted when it should have been.' % str( lfda.dataset.id ) ) + raise AssertionError( 'The dataset with id "%s" has not been marked as deleted when it should have been.' % \ + str( lfda.dataset.id ) ) check_folder( library.root_folder ) diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index c6bc12a88ce..63021acc775 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -141,6 +141,9 @@ use_interactive = True # Can users register new accounts? #allow_user_creation = True +# Can an admin user delete user accounts? +#allow_user_deletion = False + # ---- Job Execution -------------------------------------------------------- # Number of concurrent jobs to run (local job runner) From 6b0712c8eeb56ebcaa86cc524d437f48b5c3883c Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 28 Nov 2008 15:39:47 -0500 Subject: [PATCH 114/267] Bug fix for my last commit. --- test/base/twilltestcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 9288f4c58da..b64b1498d41 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -176,7 +176,7 @@ class TwillTestCase( unittest.TestCase ): def share_history( self, id=None, email='test2@bx.psu.edu' ): """Share a history with a different user""" - self.create( email=email, password='testuser', confirm='testuser' ) + self.create( email=email, password='testuser' ) history_list = self.get_histories() self.assertTrue( history_list ) if id is None: # take last id From 0073f3e42c93bf57f22d57b2bc18c1dd84281002 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 28 Nov 2008 16:59:49 -0500 Subject: [PATCH 115/267] Fixes for functional tests for sharing a history. --- test/base/twilltestcase.py | 1 - test/functional/test_history_functions.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index b64b1498d41..07e0d7806e9 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -176,7 +176,6 @@ class TwillTestCase( unittest.TestCase ): def share_history( self, id=None, email='test2@bx.psu.edu' ): """Share a history with a different user""" - self.create( email=email, password='testuser' ) history_list = self.get_histories() self.assertTrue( history_list ) if id is None: # take last id diff --git a/test/functional/test_history_functions.py b/test/functional/test_history_functions.py index 8b0b12819fa..26a036a0b61 100644 --- a/test/functional/test_history_functions.py +++ b/test/functional/test_history_functions.py @@ -48,9 +48,10 @@ class TestHistory( TwillTestCase ): """Testing sharing a history with another user""" self.upload_file('1.bed', dbkey='hg18') id, name, email = self.share_history() + self.last_page() try: self.check_page_for_string( 'History (%s) has been shared with: %s' %(name, email) ) - except TwillAssertionError: + except: self.check_page_for_string( "The history or histories you've chosen to share contain datasets that the user you're sharing with does not have permission to access." ) self.logout() self.login( email='test2@bx.psu.edu' ) From c8a71a49f865f2141eaf8be341f42e5ba14f445d Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Mon, 1 Dec 2008 13:24:01 -0500 Subject: [PATCH 116/267] Add RGenetic datatypes to datatypes.conf.sample and remove the always true sniff method of SNPMatrix --- datatypes_conf.xml.sample | 24 ++++++++++++++++++++++++ lib/galaxy/datatypes/genetics.py | 8 ++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index 7db33f2e7d7..23974aab8b6 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -140,6 +140,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + both are always True (is sqlite actually storing a string here, which is always true when not empty?) + 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 ) ) + + #these alters are the same, regardless if we are using sqlite + #hda table + 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 ) ) + + #MetadataFile 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 ) ) + + + #now create indexes for new columns in 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 ) ) + + + #Now we shutdown the app + app.shutdown() + del app + print + print "Columns added to tables, restarting app, with database_create_tables=True" + + #We restart the app, this time with create_tables == True + configuration['database_create_tables'] = True + app = galaxy.app.UniverseApplication( global_conf = ini_file, **configuration ) + + ##Now we add foreign key constraints as necessary for 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: + #Can't do this in SQLite + #SQLite ignores (but parses on initial table creation) foreign key constraints anyway, see: http://www.sqlite.org/omitted.html (there is someway to set up behavior using triggers) + print_warning( "Adding foreign key constraints to table is not supported in SQLite." ) + + #create default permisions for users, histories, etc + print "creating private roles and setting defaults for existing users and their histories and datasets" + + #Setup each user with a private role and then set their default permisions and set permisions on existing histories and datasets + for user in app.model.User.query().all(): + print "Setting up user: %s." % user.email + if not app.model.security_agent.get_private_user_role( user ): + app.model.security_agent.create_private_user_role( user ) + else: + print_warning( "user (%s) already has a private role, (re)setting defaults anyway" % ( user.email ) ) + #set default permissions and permissions on existing items + #we will do this even if a role for the user already exists (slower, but safer) + app.model.security_agent.user_set_default_permissions( user, history=True, dataset=True, bypass_manage_permission=True ) + + app.model.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 $@ From 40a55c183285bb78fcc30f74cff95eae8e4ab02e Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 12 Dec 2008 11:27:37 -0500 Subject: [PATCH 139/267] Add ability ot rename roles and groups, 2 fixes for filtering out deleted roles, some code cleanup and additional functional tests to cover new features. --- lib/galaxy/web/controllers/admin.py | 39 ++++++++++++- .../admin/dataset_security/group_create.mako | 4 +- .../admin/dataset_security/group_rename.mako | 34 +++++++++++ .../dataset_security/group_roles_edit.mako | 2 + templates/admin/dataset_security/groups.mako | 1 + .../admin/dataset_security/role_rename.mako | 41 +++++++++++++ templates/admin/dataset_security/roles.mako | 1 + templates/admin/library/browser.mako | 8 +-- templates/admin/library/rename_library.mako | 2 +- test/base/twilltestcase.py | 19 +++++- .../functional/test_security_and_libraries.py | 58 +++++++++++++------ 11 files changed, 180 insertions(+), 29 deletions(-) create mode 100644 templates/admin/dataset_security/group_rename.mako create mode 100644 templates/admin/dataset_security/role_rename.mako diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index eca6fb85aee..b1a6721c166 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -104,8 +104,25 @@ class Admin( BaseController ): msg = util.restore_text( params.get( 'msg', '' ) ) messagetype = params.get( 'messagetype', 'done' ) role = trans.app.model.Role.get( int( params.role_id ) ) - if 'role_members_edit_button' in kwd: + if params.get( 'role_members_edit_button', False ): self.role_members_edit( trans, **kwd ) + if params.get( 'rename', False ): + if params.rename == 'submitted': + 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 = 'The role has been renamed to %s' % new_name + return trans.response.send_redirect( web.url_for( action='roles', msg=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 = [] @@ -302,6 +319,21 @@ class Admin( BaseController ): msg = util.restore_text( params.get( 'msg', '' ) ) messagetype = params.get( 'messagetype', 'done' ) group = trans.app.model.Group.get( group_id ) + if params.get( 'rename', False ): + if params.rename == 'submitted': + 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 = 'The group has been renamed to %s' % new_name + return trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) + return trans.fill_template( '/admin/dataset_security/group_rename.mako', group=group, msg=msg, messagetype=messagetype ) # Get the group members users = [] for uga in group.members: @@ -413,7 +445,8 @@ class Admin( BaseController ): gra.flush() msg = "Group updated with a total of %s associated roles" % len( roles ) trans.response.send_redirect( web.url_for( action='groups', msg=msg, messagetype='done' ) ) - roles=trans.app.model.Role.filter( trans.app.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE ) \ + roles=trans.app.model.Role.filter( and_( trans.app.model.Role.table.c.type != trans.app.model.Role.types.PRIVATE, + trans.app.model.Role.table.c.deleted == False ) ) \ .order_by( trans.app.model.Role.table.c.name ).all() group_roles = [] for gra in group.roles: @@ -1251,7 +1284,7 @@ class Admin( BaseController ): yield build_name, dbkey, ( dbkey==last_used_build ) dbkeys = get_dbkey_options( last_used_build ) # Send list of roles to the form so the dataset can be associated with 1 or more of them. - roles = trans.app.model.Role.query().order_by( trans.app.model.Role.c.name ).all() + 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, diff --git a/templates/admin/dataset_security/group_create.mako b/templates/admin/dataset_security/group_create.mako index 42d896bc51b..69e82acc749 100644 --- a/templates/admin/dataset_security/group_create.mako +++ b/templates/admin/dataset_security/group_create.mako @@ -22,9 +22,9 @@ %if not anchored: - ${role.description} +  ${role.name}: ${role.description} %else: - ${role.description} +  ${role.name}: ${role.description} %endif diff --git a/templates/admin/dataset_security/group_rename.mako b/templates/admin/dataset_security/group_rename.mako new file mode 100644 index 00000000000..0e068f3ed3c --- /dev/null +++ b/templates/admin/dataset_security/group_rename.mako @@ -0,0 +1,34 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +
            +
            Change group name
            +
            +
            +
            + +
            + +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            + +
            +
            +
            + +
            +
            +
            diff --git a/templates/admin/dataset_security/group_roles_edit.mako b/templates/admin/dataset_security/group_roles_edit.mako index b2b591b9236..c574715d1c3 100644 --- a/templates/admin/dataset_security/group_roles_edit.mako +++ b/templates/admin/dataset_security/group_roles_edit.mako @@ -15,6 +15,7 @@ ${role.name} %endif + ${role.description} ${role.type} %if not anchored: @@ -59,6 +60,7 @@ %endif Select to associate role with ${group.name} + Description Role Type %for ctr, role in enumerate( roles ): diff --git a/templates/admin/dataset_security/groups.mako b/templates/admin/dataset_security/groups.mako index a0b58986aa4..366a9951ef2 100644 --- a/templates/admin/dataset_security/groups.mako +++ b/templates/admin/dataset_security/groups.mako @@ -12,6 +12,7 @@ ${group.name}
            + Rename this group Change associated users Change associated roles Mark group deleted diff --git a/templates/admin/dataset_security/role_rename.mako b/templates/admin/dataset_security/role_rename.mako new file mode 100644 index 00000000000..d70844d6f55 --- /dev/null +++ b/templates/admin/dataset_security/role_rename.mako @@ -0,0 +1,41 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +
            +
            Change role name and description
            +
            +
            +
            + +
            + +
            +
            +
            +
            + +
            + +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            + +
            +
            +
            + +
            +
            +
            diff --git a/templates/admin/dataset_security/roles.mako b/templates/admin/dataset_security/roles.mako index 6edf49985fb..1f7d9df74b0 100644 --- a/templates/admin/dataset_security/roles.mako +++ b/templates/admin/dataset_security/roles.mako @@ -12,6 +12,7 @@ ${role.name} diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako index 49720cfa256..06646e0720a 100644 --- a/templates/admin/library/browser.mako +++ b/templates/admin/library/browser.mako @@ -103,10 +103,10 @@ def name_sorted( l ):
            %if not deleted:
            - Add a new dataset to this folder - Copy a dataset from your history to this folder - Create a new sub-folder in this folder - Rename this folder + Add a new dataset to this folder + Copy a dataset from your history to this folder + Create a new sub-folder in this folder + Rename this folder %if subfolder: Remove this folder and its contents from the library %endif diff --git a/templates/admin/library/rename_library.mako b/templates/admin/library/rename_library.mako index 9493ad1a7a3..9114248a942 100644 --- a/templates/admin/library/rename_library.mako +++ b/templates/admin/library/rename_library.mako @@ -6,7 +6,7 @@ %endif
            -
            Edit library name and description
            +
            Change library name and description
            diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 37f18fc5058..865f595809d 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -671,6 +671,15 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( description ) self.home() return previously_created + def rename_role( self, role_id, name='Role One Renamed', description='This is Role One Re-described' ): + """Rename a role""" + self.home() + self.visit_url( "%s/admin/role?rename=True&role_id=%s" % ( self.url, role_id ) ) + self.check_page_for_string( 'Change role name and description' ) + tc.fv( "1", "name", name ) + tc.fv( "1", "description", description ) + tc.submit( "rename_role_button" ) + self.home() def mark_role_deleted( self, role_id ): """Mark a role as deleted""" self.home() @@ -747,6 +756,14 @@ class TwillTestCase( unittest.TestCase ): self.check_page_for_string( name ) self.home() return previously_created + def rename_group( self, group_id, name='Group One Renamed' ): + """Rename a group""" + self.home() + self.visit_url( "%s/admin/group?rename=True&group_id=%s" % ( self.url, group_id ) ) + self.check_page_for_string( 'Change group name' ) + tc.fv( "1", "name", name ) + tc.submit( "rename_group_button" ) + self.home() def group_members_edit( self, group_id, user_ids=[] ): """Add members to an existing group""" self.home() @@ -820,7 +837,7 @@ class TwillTestCase( unittest.TestCase ): """Rename a library""" self.home() self.visit_url( "%s/admin/library?rename=True&id=%s" % ( self.url, library_id ) ) - self.check_page_for_string( 'Edit library name and description' ) + self.check_page_for_string( 'Change library name and description' ) tc.fv( "1", "name", name ) tc.fv( "1", "description", description ) if root_folder: diff --git a/test/functional/test_security_and_libraries.py b/test/functional/test_security_and_libraries.py index b7d743d1e9e..f86e0ca911d 100644 --- a/test/functional/test_security_and_libraries.py +++ b/test/functional/test_security_and_libraries.py @@ -6,7 +6,7 @@ not_logged_in_security_msg = 'You must be logged in as an administrator to acces logged_in_security_msg = 'You must be an administrator to access this feature.' class TestSecurityAndLibraries( TwillTestCase ): - def test_00_admin_features_when_not_logged_in( self ): + def test_000_admin_features_when_not_logged_in( self ): """Testing admin_features when not logged in""" self.logout() self.visit_url( "%s/admin" % self.url ) @@ -271,7 +271,7 @@ class TestSecurityAndLibraries( TwillTestCase ): """Testing undeleting a user account""" self.undelete_user( user_id=regular_user3.id ) def test_045_create_role( self ): - """Testing creating new role with 3 members""" + """Testing creating new role with 3 members, then renaming it""" name = 'Role One' description = "This is Role One's description" user_ids=[ str( admin_user.id ), str( regular_user1.id ), str( regular_user3.id ) ] @@ -296,8 +296,18 @@ class TestSecurityAndLibraries( TwillTestCase ): if not previously_created and len( user.roles ) != 2: raise AssertionError( '%d UserRoleAssociations are associated with user %s ( should be 2 )' \ % ( len( user.roles ), user.email ) ) + # Rename the role + rename = "Role One's been Renamed" + redescription="This is Role One's Re-described" + self.rename_role( str( role_one.id ), name=rename, description=redescription ) + self.home() + self.visit_page( 'admin/roles' ) + self.check_page_for_string( rename ) + self.check_page_for_string( redescription ) + # Reset the role back to the original name and description + self.rename_role( str( role_one.id ), name=name, description=description ) def test_050_create_group( self ): - """Testing creating new group with 3 members and 1 associated role""" + """Testing creating new group with 3 members and 1 associated role, then renaming it""" name = "Group One's Name" user_ids=[ str( admin_user.id ), str( regular_user1.id ), str( regular_user3.id ) ] role_ids=[ str( role_one.id ) ] @@ -323,6 +333,14 @@ class TestSecurityAndLibraries( TwillTestCase ): if len( group_one.roles ) != len( role_ids ): raise AssertionError( '%d GroupRoleAssociations were created for group id %d when it was created ( should have been %d )' \ % ( len( group_one.roles ), group_one.id, len( role_ids ) ) ) + # Rename the group + rename = "Group One's been Renamed" + self.rename_group( str( group_one.id ), name=rename, ) + self.home() + self.visit_page( 'admin/groups' ) + self.check_page_for_string( rename ) + # Reset the group back to the original name + self.rename_group( str( group_one.id ), name=name ) def test_055_add_members_and_role_to_group( self ): """Testing editing user membership and role associations of an existing group""" name = 'Group Two' @@ -457,6 +475,7 @@ class TestSecurityAndLibraries( TwillTestCase ): rename = "Library One's been Renamed" redescription="This is Library One's Re-described" self.rename_library( str( library_one.id ), name=rename, description=redescription, root_folder='on' ) + self.home() self.visit_page( 'admin/libraries' ) self.check_page_for_string( rename ) self.check_page_for_string( redescription ) @@ -467,6 +486,7 @@ class TestSecurityAndLibraries( TwillTestCase ): rename = "Library One's Root Folder" redescription = "This is Library One's root folder" self.rename_folder( str( folder.id ), name=rename, description=redescription ) + self.home() self.visit_page( 'admin/libraries' ) self.check_page_for_string( rename ) self.check_page_for_string( redescription ) @@ -699,8 +719,10 @@ class TestSecurityAndLibraries( TwillTestCase ): """Testing adding 3 datasets from a library directory to a folder""" roles_tuple = [ ( str( role_one.id ), role_one.description ) ] self.add_datasets_from_library_dir( str( folder_one.id ), roles_tuple=roles_tuple ) - # It would be nice if twill functioned such that the above statement resulted in a - # form with the uploaded datasets selected, but it does not (they're not checked ), + def test_120_change_permissions_on_datasets_imported_from_library( self ): + """Testing changing the permissions on library datasets imported into a history""" + # It would be nice if twill functioned such that the above test resulted in a + # form with the uploaded datasets selected, but it does not ( they're not checked ), # so we'll have to simulate this behavior ( not ideal ) for the 'edit' action. We # first need to get the lfda.id for the 3 new datasets latest_3_lfdas = galaxy.model.LibraryFolderDatasetAssociation.query() \ @@ -781,7 +803,7 @@ class TestSecurityAndLibraries( TwillTestCase ): except: pass # This is the behavior we want check_edit_page2( latest_3_lfdas ) - def test_120_mark_group_deleted( self ): + def test_125_mark_group_deleted( self ): """Testing marking a group as deleted""" self.home() self.visit_url( '%s/admin/groups' % self.url ) @@ -795,13 +817,13 @@ class TestSecurityAndLibraries( TwillTestCase ): raise AssertionError( '%s incorrectly lost all members when it was marked as deleted.' % group_two.name ) if not group_two.roles: raise AssertionError( '%s incorrectly lost all role associations when it was marked as deleted.' % group_two.name ) - def test_125_undelete_group( self ): + def test_130_undelete_group( self ): """Testing undeleting a deleted group""" self.undelete_group( str( group_two.id ) ) group_two.refresh() if group_two.deleted: raise AssertionError( '%s was not correctly marked as not deleted.' % group_two.name ) - def test_130_mark_role_deleted( self ): + def test_135_mark_role_deleted( self ): """Testing marking a role as deleted""" self.home() self.visit_url( '%s/admin/roles' % self.url ) @@ -815,10 +837,10 @@ class TestSecurityAndLibraries( TwillTestCase ): raise AssertionError( '%s incorrectly lost all user associations when it was marked as deleted.' % role_two.name ) if not role_two.groups: raise AssertionError( '%s incorrectly lost all group associations when it was marked as deleted.' % role_two.name ) - def test_135_undelete_role( self ): + def test_140_undelete_role( self ): """Testing undeleting a deleted role""" self.undelete_role( str( role_two.id ) ) - def test_140_mark_library_deleted( self ): + def test_145_mark_library_deleted( self ): """Testing marking a library as deleted""" self.mark_library_deleted( str( library_one.id ) ) # Make sure the library was deleted @@ -844,7 +866,7 @@ class TestSecurityAndLibraries( TwillTestCase ): if lfda.dataset.deleted: raise AssertionError( 'The dataset with id "%s" has been marked as deleted when it should not have been.' % lfda.dataset.id ) check_folder( library_one.root_folder ) - def test_145_undelete_library( self ): + def test_150_undelete_library( self ): """Testing marking a library as not deleted""" self.undelete_library( str( library_one.id ) ) # Make sure the library is undeleted @@ -876,7 +898,7 @@ class TestSecurityAndLibraries( TwillTestCase ): if not library_one.deleted: raise AssertionError( 'The library id %s named "%s" has not been marked as deleted after it was undeleted.' % \ ( str( library_one.id ), library_one.name ) ) - def test_150_purge_user( self ): + def test_155_purge_user( self ): """Testing purging a user account""" self.mark_user_deleted( user_id=regular_user3.id ) self.purge_user( user_id=regular_user3.id ) @@ -921,7 +943,7 @@ class TestSecurityAndLibraries( TwillTestCase ): role = galaxy.model.Role.get( ura.role_id ) if role.type != 'private': raise AssertionError( 'UserRoleAssociations for user %s are not related with the private role.' % regular_user3.email ) - def test_155_manually_unpurge_user( self ): + def test_160_manually_unpurge_user( self ): """Testing manually un-purging a user account""" # Reset the user for later test runs. The user's private Role and DefaultUserPermissions for that role # should have been preserved, so all we need to do is reset purged and deleted. @@ -929,7 +951,7 @@ class TestSecurityAndLibraries( TwillTestCase ): regular_user3.purged = False regular_user3.deleted = False regular_user3.flush() - def test_160_purge_group( self ): + def test_165_purge_group( self ): """Testing purging a group""" group_id = str( group_two.id ) self.mark_group_deleted( group_id ) @@ -944,7 +966,7 @@ class TestSecurityAndLibraries( TwillTestCase ): raise AssertionError( "Purging the group did not delete the GroupRoleAssociations for group_id '%s'" % group_id ) # Undelete the group for later test runs self.undelete_group( group_id ) - def test_165_purge_role( self ): + def test_170_purge_role( self ): """Testing purging a role""" role_id = str( role_two.id ) self.mark_role_deleted( role_id ) @@ -969,14 +991,14 @@ class TestSecurityAndLibraries( TwillTestCase ): adra = galaxy.model.ActionDatasetRoleAssociation.filter( galaxy.model.ActionDatasetRoleAssociation.table.c.role_id == role_id ).all() if adra: raise AssertionError( "Purging the role did not delete the ActionDatasetRoleAssociations for role_id '%s'" % role_id ) - def test_170_manually_unpurge_role( self ): + def test_175_manually_unpurge_role( self ): """Testing manually un-purging a role""" # Manually unpurge, then undelete the role for later test runs # TODO: If we decide to implement the GUI feature for un-purging a role, replace this with a method call role_two.purged = False role_two.flush() self.undelete_role( str( role_two.id ) ) - def test_175_purge_library( self ): + def test_180_purge_library( self ): """Testing purging a library""" self.purge_library( str( library_one.id ) ) # Make sure the library was purged @@ -1003,7 +1025,7 @@ class TestSecurityAndLibraries( TwillTestCase ): raise AssertionError( 'The dataset with id "%s" has not been marked as deleted when it should have been.' % \ str( lfda.dataset.id ) ) check_folder( library_one.root_folder ) - def test_180_reset_data_for_later_test_runs( self ): + def test_185_reset_data_for_later_test_runs( self ): """Reseting data to enable later test runs to pass""" ################## # Reset admin_user From 797b6e1d870ba4614f7f7afc5586a6b2dc33e4a5 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 12 Dec 2008 11:58:23 -0500 Subject: [PATCH 140/267] Filter out deleted users from report in security branch. --- lib/galaxy/webapps/reports/controllers/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/webapps/reports/controllers/users.py b/lib/galaxy/webapps/reports/controllers/users.py index 8459cfeabc4..dfaf3de6986 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: From 394c26fa1f3c016482d1ce409ea7c2e61a50bc57 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 12 Dec 2008 14:56:35 -0500 Subject: [PATCH 141/267] Fix for __get_or_create_remote_user, show an error message if the user's account has been deleted instead of automatically undeleting it. --- lib/galaxy/web/framework/__init__.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index f426ebf0da5..eecdbbb0e2a 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -300,19 +300,8 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): user = self.app.model.User( email=remote_user_email ) user.set_password_cleartext( 'external' ) user.external = True - #self.log_event( "Automatically created account '%s'", user.email ) - # TODO: make sure this correctly handles deleted / purged users elif user.deleted: - if user.purged: - # If the user has been purged, all associations have been deleted except for the private role - # and the DefaultUserPermissions and DefaultHistoryPermissions associated with it. We'll - # restore the user, but all of their previous histories and other associations will have been - # deleted. - user.purged = False - # If the user was not purged, the state of all of their associations at the time they were deleted - # will have been preserved. - user.deleted = False - user.flush() + 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 ): """ From 4e48d41c8aec6ac50a608cffe21dd5dcb87b999c Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 15 Dec 2008 14:13:34 -0500 Subject: [PATCH 142/267] Add a necessary closing
            tag for the library browser, caused indendation problems in Safari, but now fixed. --- templates/admin/library/common.mako | 111 ++++++++++++++-------------- templates/library/common.mako | 97 ++++++++++++------------ 2 files changed, 105 insertions(+), 103 deletions(-) diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako index ab4b91c9454..2384d3850b8 100644 --- a/templates/admin/library/common.mako +++ b/templates/admin/library/common.mako @@ -1,63 +1,64 @@ ## Render the dataset `data` <%def name="render_dataset( data, selected, deleted )">
            + + ## Header row for library items (name, state, action buttons) +
            + + + + + + + +
            + %if selected: + + %else: + + %endif + ${data.display_name()} + %if not deleted: + + + %endif + ${data.ext}${data.dbkey}${data.info}
            +
            - ## Header row for library items (name, state, action buttons) -
            - - - - - - - -
            - %if selected: - - %else: - - %endif - ${data.display_name()} - %if not deleted: - - - %endif - ${data.ext}${data.dbkey}${data.info}
            -
            - - ## Body for library items, extra info and actions, data "peek" -
            -
            ${data.blurb}
            -
            - %if data.has_data: - %for display_app in data.datatype.get_display_types(): - <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> - %if len( display_links ) > 0: - ${data.datatype.get_display_label(display_app)} - %for display_name, display_link in display_links: - ${display_name} - %endfor - %endif - %endfor + ## Body for library items, extra info and actions, data "peek" +
            +
            ${data.blurb}
            +
            + %if data.has_data: + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor + %endif + %endfor + %endif +
            + %if data.peek != "no peek": +
            ${data.display_peek()}
            + %endif + ## Recurse for child datasets + %if len( data.visible_children ) > 0: +
            + There are ${len( data.visible_children )} secondary datasets. + %for idx, child in enumerate( data.visible_children ): + ${ render_dataset( child, selected, deleted ) } + %endfor +
            %endif
            - %if data.peek != "no peek": -
            ${data.display_peek()}
            - %endif - ## Recurse for child datasets - %if len( data.visible_children ) > 0: -
            - There are ${len( data.visible_children )} secondary datasets. - %for idx, child in enumerate( data.visible_children ): - ${ render_dataset( child, selected, deleted ) } - %endfor -
            - %endif
            diff --git a/templates/library/common.mako b/templates/library/common.mako index ad4123da392..91f90b29a78 100644 --- a/templates/library/common.mako +++ b/templates/library/common.mako @@ -1,56 +1,57 @@ ## Render the dataset `data` <%def name="render_dataset( data )">
            - - ## Header row for library items (name, state, action buttons) -
            - - - - - - - -
            - - ${data.display_name()} - -
            - View or edit this dataset's attributes and permissions - %if data.has_data: - Download this dataset + + ## Header row for library items (name, state, action buttons) +
            + + + + + + + +
            + + ${data.display_name()} + + + ${data.ext}${data.dbkey}${data.info}
            +
            + + ## Body for library items, extra info and actions, data "peek" +
            +
            ${data.blurb}
            +
            + %if data.has_data: + %for display_app in data.datatype.get_display_types(): + <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> + %if len( display_links ) > 0: + ${data.datatype.get_display_label(display_app)} + %for display_name, display_link in display_links: + ${display_name} + %endfor %endif -
            -
            ${data.ext}${data.dbkey}${data.info}
            -
            - - ## Body for library items, extra info and actions, data "peek" -
            -
            ${data.blurb}
            -
            - %if data.has_data: - %for display_app in data.datatype.get_display_types(): - <% display_links = data.datatype.get_display_links( data, display_app, app, request.base ) %> - %if len( display_links ) > 0: - ${data.datatype.get_display_label(display_app)} - %for display_name, display_link in display_links: - ${display_name} - %endfor - %endif - %endfor + %endfor + %endif +
            + %if data.peek != "no peek": +
            ${data.display_peek()}
            + %endif + ## Recurse for child datasets + %if len( data.visible_children ) > 0: +
            + There are ${len( data.visible_children )} secondary datasets. + %for idx, child in enumerate( data.visible_children ): + ${render_dataset( child )} + %endfor +
            %endif
            - %if data.peek != "no peek": -
            ${data.display_peek()}
            - %endif - ## Recurse for child datasets - %if len( data.visible_children ) > 0: -
            - There are ${len( data.visible_children )} secondary datasets. - %for idx, child in enumerate( data.visible_children ): - ${render_dataset( child )} - %endfor -
            - %endif
            From b6d561fb4299d3e9a26fab2fa51611109515c158 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Mon, 15 Dec 2008 16:37:54 -0500 Subject: [PATCH 143/267] Allow users to download a selection of datasets directly from the library in an archive format of their choosing. --- lib/galaxy/web/controllers/library.py | 92 ++++++++++++++++++++++--- static/june_2007_style/blue/library.css | 6 ++ static/june_2007_style/library.css.tmpl | 6 ++ templates/library/browser.mako | 83 ++++++++++++++++------ tools/data_source/access_libraries.xml | 2 +- 5 files changed, 156 insertions(+), 33 deletions(-) diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index f6c4d4123fe..21dd1b9ec6c 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,6 +1,11 @@ from galaxy.web.base.controller import * from galaxy.model.orm import * -import logging +import logging, tempfile, zipfile, tarfile, os, sys + +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__ ) @@ -9,21 +14,88 @@ class Library( BaseController ): def browse( self, trans, **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 ) + return trans.fill_template( '/library/browser.mako', libraries=libraries, default_action=kwd.get( 'default_action', None ) ) index = browse @web.expose def import_datasets( self, trans, import_ids=[], **kwd ): if not import_ids: - return trans.show_error_message( "You must select at least one dataset to import" ) + return trans.show_error_message( "You must select at least one dataset" ) if not isinstance( import_ids, list ): import_ids = [ import_ids ] - history = trans.get_history() - for id in import_ids: - dataset = trans.app.model.LibraryFolderDatasetAssociation.get( id ).to_history_dataset_association() - history.add_dataset( dataset ) - dataset.flush() - history.flush() - return trans.show_ok_message( "%i dataset(s) have been imported in to your history" % len( import_ids ), refresh_frames=['history'] ) + p = util.Params( kwd ) + if not p.action: + return trans.show_error_message( "You must select an action to perform on selected datasets" ) + if p.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 in to 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.action ) + if p.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.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.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""" diff --git a/static/june_2007_style/blue/library.css b/static/june_2007_style/blue/library.css index 76f589af067..6a3004ad548 100644 --- a/static/june_2007_style/blue/library.css +++ b/static/june_2007_style/blue/library.css @@ -63,3 +63,9 @@ pre.peek th a.expandLink { text-decoration: none; } + +span.expandLink { + width: 100%; + height: 100%; + display: block; +} diff --git a/static/june_2007_style/library.css.tmpl b/static/june_2007_style/library.css.tmpl index 5b4a736a70d..75f8678a05c 100644 --- a/static/june_2007_style/library.css.tmpl +++ b/static/june_2007_style/library.css.tmpl @@ -63,3 +63,9 @@ pre.peek th a.expandLink { text-decoration: none; } + +span.expandLink { + width: 100%; + height: 100%; + display: block; +} diff --git a/templates/library/browser.mako b/templates/library/browser.mako index ba1d313b07d..f326fc25119 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -16,11 +16,33 @@ def name_sorted( l ): @@ -98,16 +120,18 @@ def name_sorted( l ): subfolder = True %>
          • +
            - + ${parent.name} %if parent.description: - ${parent.description} %endif +
          • %if subfolder: -
              +
          • Change default permissions for the current history
          • %endif
          • Show deleted datasets in history
          • -
          • Delete current history
          • +
          • Delete current history
          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 605693f630f..3ec8a41581e 100644 --- a/templates/history/share.mako +++ b/templates/history/share.mako @@ -5,7 +5,7 @@
          Share Histories
          Current NameNew Name
          - + %for history in histories: @@ -39,7 +39,7 @@ vertical-align: top; } - + %for history in histories: %endfor diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index ef0ccecb703..0f09a8c4d0a 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -13,7 +13,7 @@ from elementtree import ElementTree buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? -twill.set_output(buffer) +## twill.set_output(buffer) tc.config('use_tidy', 0) # Dial ClientCookie logging down (very noisy) @@ -107,10 +107,10 @@ class TwillTestCase( unittest.TestCase ): history_list = self.get_histories() self.assertTrue( history_list ) if id is None: - history = history_list[-1] + history = history_list[0] id = history.get( 'id' ) id = str( id ) - self.visit_page( "history_delete?id=%s" %(id) ) + self.visit_page( "history/list?operation=delete&id=%s" %(id) ) def get_histories( self ): """Returns all histories""" @@ -135,7 +135,7 @@ class TwillTestCase( unittest.TestCase ): def histories_as_xml_tree( self ): """Returns a parsed xml object of all histories""" self.home() - self.visit_page( 'history_available?as_xml=True' ) + self.visit_page( 'history/list_as_xml' ) xml = self.last_page() tree = ElementTree.fromstring(xml) return tree @@ -164,7 +164,7 @@ class TwillTestCase( unittest.TestCase ): old_name = elem.get( 'name' ) self.assertTrue( old_name ) id = str( id ) - self.visit_page( "history_rename?id=%s&name=%s" %(id, name) ) + self.visit_page( "history/rename?id=%s&name=%s" %(id, name) ) return id, old_name, name def set_history( self ): @@ -188,25 +188,25 @@ class TwillTestCase( unittest.TestCase ): id = str( id ) name = elem.get( 'name' ) self.assertTrue( name ) - self.visit_url( "%s/history_share?id=%s&email=%s&history_share_btn=Submit" % ( self.url, id, email ) ) + self.visit_url( "%s/history/share?id=%s&email=%s&history_share_btn=Submit" % ( self.url, id, email ) ) return id, name, email def share_history_containing_private_datasets( self, history_id, email='test@bx.psu.edu' ): """Attempt to share a history containing private datasets with a different user""" - self.visit_url( "%s/history_share?id=%s&email=%s&history_share_btn=Submit" % ( self.url, history_id, email ) ) + self.visit_url( "%s/history/share?id=%s&email=%s&history_share_btn=Submit" % ( self.url, history_id, email ) ) self.last_page() self.check_page_for_string( "The history or histories you've chosen to share contain datasets" ) self.check_page_for_string( "How would you like to proceed?" ) self.home() def make_datasets_public( self, history_id, email='test@bx.psu.edu' ): """Make private datasets public in order to share a history with a different user""" - self.visit_url( "%s/history_share?id=%s&email=%s&action=public&submit=Ok" % ( self.url, history_id, email ) ) + self.visit_url( "%s/history/share?id=%s&email=%s&action=public&submit=Ok" % ( self.url, history_id, email ) ) self.last_page() check_str = "History (Unnamed history) has been shared with: %s" % email self.check_page_for_string( check_str ) self.home() def privately_share_dataset( self, history_id, email='test@bx.psu.edu' ): """Make private datasets public in order to share a history with a different user""" - self.visit_url( "%s/history_share?id=%s&email=%s&action=private&submit=Ok" % ( self.url, history_id, email ) ) + self.visit_url( "%s/history/share?id=%s&email=%s&action=private&submit=Ok" % ( self.url, history_id, email ) ) self.last_page() check_str = "History (Unnamed history) has been shared with: %s" % email self.check_page_for_string( check_str ) @@ -223,10 +223,10 @@ class TwillTestCase( unittest.TestCase ): hid = str(hid) elems = [ elem for elem in data_list if elem.get('hid') == hid ] self.assertEqual(len(elems), 1) - self.visit_page( "history_switch?id=%s" % elems[0].get('id') ) + self.visit_page( "history/list?operation=switch&id=%s" % elems[0].get('id') ) def view_stored_histories( self ): - self.visit_page( "history_available" ) + self.visit_page( "history/list" ) # Functions associated with datasets (history items) and meta data def get_job_stderr( self, id ): @@ -554,7 +554,10 @@ class TwillTestCase( unittest.TestCase ): tc.submit( button ) def visit_page( self, page ): - tc.go("./%s" % page) + # tc.go("./%s" % page) + if not page.startswith( "/" ): + page = "/" + page + tc.go( self.url + page ) tc.code( 200 ) def visit_url( self, url ): diff --git a/test/functional/test_history_functions.py b/test/functional/test_history_functions.py index 31d0c0fd499..89f9aa3accb 100644 --- a/test/functional/test_history_functions.py +++ b/test/functional/test_history_functions.py @@ -16,7 +16,7 @@ class TestHistory( TwillTestCase ): if len(self.get_history()) > 0: raise AssertionError("test_new_history_then_delete failed") self.delete_history() - self.check_page_for_string( 'History deleted:' ) + self.check_page_for_string( 'Deleted 1 histories' ) def test_10_history_options_when_logged_in( self ): """Testing history options when logged in""" self.history_options() @@ -32,10 +32,10 @@ class TestHistory( TwillTestCase ): """Testing viewing previously stored histories""" self.view_stored_histories() self.check_page_for_string( 'Stored Histories' ) - self.check_page_for_string( ' Date: Mon, 12 Jan 2009 18:33:19 -0500 Subject: [PATCH 161/267] History controller (missed in last commit) --- lib/galaxy/web/controllers/history.py | 278 ++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 lib/galaxy/web/controllers/history.py 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 From 3950563374b0d992cc0b015530a48c200bb9cc7b Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 12 Jan 2009 18:35:18 -0500 Subject: [PATCH 162/267] Restore twill's buffer (nose captures this now so it is useful for debugging, perhaps a flag?) --- test/base/twilltestcase.py | 224 +++++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 96 deletions(-) diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 0f09a8c4d0a..614c4a9eb1a 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -13,7 +13,7 @@ from elementtree import ElementTree buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? -## twill.set_output(buffer) +twill.set_output(buffer) tc.config('use_tidy', 0) # Dial ClientCookie logging down (very noisy) @@ -629,51 +629,56 @@ class TwillTestCase( unittest.TestCase ): tc.submit( "reset_user_password_button" ) self.check_page_for_string( "Password reset" ) self.home() - def mark_user_deleted( self, user_id=4, email='' ): + def mark_user_deleted( self, user_id=4 ): """Mark a user as deleted""" self.home() self.visit_url( "%s/admin/mark_user_deleted?user_id=%s" % ( self.url, str( user_id ) ) ) - check_str = "User '%s' has been marked as deleted." % email - self.check_page_for_string( check_str ) + self.check_page_for_string( "The user has been marked as deleted." ) self.home() - def undelete_user( self, user_id=4, email='' ): + def undelete_user( self, user_id ): """Undelete a user""" self.home() self.visit_url( "%s/admin/undelete_user?user_id=%s" % ( self.url, user_id ) ) - check_str = "User '%s' has been marked as not deleted" % email - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The user has been marked as not deleted' ) self.home() - def purge_user( self, user_id, email ): + def purge_user( self, user_id ): """Purge a user account""" self.home() self.visit_url( "%s/admin/purge_user?user_id=%s" % ( self.url, user_id ) ) - check_str = "User '%s' has been marked as purged." % email - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The user has been marked as purged.' ) self.home() - def associate_roles_and_groups_with_user( self, user_id, email, role_ids=[], group_ids=[] ): + def user_roles_edit( self, user_id, role_ids=[] ): + """Change roles associated with an existing user""" self.home() - url = "%s/admin/user?user_id=%s&user_roles_groups_edit_button=Save" % ( self.url, user_id ) - if role_ids: - url += "&in_roles=%s" % ','.join( role_ids ) - if group_ids: - url += "&in_groups=%s" % ','.join( group_ids ) - self.visit_url( url ) - check_str = "User '%s' has been updated with %d associated roles and %d associated groups" % ( email, len( role_ids ), len( group_ids ) ) - self.check_page_for_string( check_str ) + self.visit_url( "%s/admin/user_roles_edit?user_id=%s" % ( self.url, user_id ) ) + self.check_page_for_string( 'Select to associate role with' ) + for role_id in role_ids: + tc.fv( "1", "roles", role_id ) + tc.submit( "user_roles_edit_button" ) + self.check_page_for_string( 'User updated with a total of' ) self.home() # Tests associated with roles - def create_role( self, name='Role One', description="This is Role One", in_user_ids=[], in_group_ids=[], private_role='' ): + def create_role( self, name='Role One', description="This is Role One", user_ids=[], group_ids=[], private_role='' ): """Create a new role""" - url = "%s/admin/create_role?create_role_button=Save&name=%s&description=%s" % ( self.url, name.replace( ' ', '+' ), description.replace( ' ', '+' ) ) - if in_user_ids: - url += "&in_users=%s" % ','.join( in_user_ids ) - if in_group_ids: - url += "&in_groups=%s" % ','.join( in_group_ids ) self.home() - self.visit_url( url ) - check_str = "Role '%s' has been created with %d associated users and %d associated groups" % ( name, len( in_user_ids ), len( in_group_ids ) ) - self.check_page_for_string( check_str ) + self.visit_url( "%s/admin/create_role" % self.url ) + self.check_page_for_string( "Create Role" ) + tc.fv( "1", "name", name ) + tc.fv( "1", "description", description ) + for user_id in user_ids: + tc.fv( "1", "users", user_id ) + for group_id in group_ids: + tc.fv( "1", "groups", group_id ) + tc.submit( "create_role_button" ) + check_str = "The new role has been created with %s associated users and %s associated groups" % ( str( len( user_ids ) ), str( len( group_ids ) ) ) + try: + self.check_page_for_string( check_str ) + previously_created = False + except: + # The role may have been created on a previous test run + self.check_page_for_string( "A role with that name already exists" ) + previously_created = True if private_role: # Make sure no private roles are displayed try: @@ -684,9 +689,10 @@ class TwillTestCase( unittest.TestCase ): # Reaching here is the behavior we want since no private roles should be displayed pass self.home() - self.visit_url( "%s/admin/roles" % self.url ) - self.check_page_for_string( name ) + self.visit_page( "admin/roles" ) + self.check_page_for_string( description ) self.home() + return previously_created def rename_role( self, role_id, name='Role One Renamed', description='This is Role One Re-described' ): """Rename a role""" self.home() @@ -696,56 +702,82 @@ class TwillTestCase( unittest.TestCase ): tc.fv( "1", "description", description ) tc.submit( "rename_role_button" ) self.home() - def mark_role_deleted( self, role_id, role_name ): + def mark_role_deleted( self, role_id ): """Mark a role as deleted""" self.home() self.visit_url( "%s/admin/mark_role_deleted?role_id=%s" % ( self.url, role_id ) ) - check_str = "Role '%s' has been marked as deleted" % role_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The role has been marked as deleted' ) self.home() - def undelete_role( self, role_id, role_name ): + def undelete_role( self, role_id ): """Undelete an existing role""" self.home() self.visit_url( "%s/admin/undelete_role?role_id=%s" % ( self.url, role_id ) ) - check_str = "Role '%s' has been marked as not deleted" % role_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The role has been marked as not deleted' ) self.home() - def purge_role( self, role_id, role_name ): + def purge_role( self, role_id ): """Purge an existing role""" self.home() self.visit_url( "%s/admin/purge_role?role_id=%s" % ( self.url, role_id ) ) - check_str = "The following have been purged from the database for role '%s': " % role_name + check_str = "The following have been purged from the database for the role: " check_str += "DefaultUserPermissions, DefaultHistoryPermissions, UserRoleAssociations, GroupRoleAssociations, ActionDatasetRoleAssociations." self.check_page_for_string( check_str ) self.home() - def associate_users_and_groups_with_role( self, role_id, role_name, user_ids=[], group_ids=[] ): + def associate_groups_with_role( self, role_id, group_names=[] ): + """Add groups to an existing role""" + # NOTE: To get this to work with twill, all select lists must contain at least 1 option value + # before tc.submit or twill throws an exception, which is: ParseError: OPTION outside of SELECT self.home() - url = "%s/admin/role?role_id=%s&role_members_edit_button=Save" % ( self.url, role_id ) - if user_ids: - url += "&in_users=%s" % ','.join( user_ids ) - if group_ids: - url += "&in_groups=%s" % ','.join( group_ids ) - self.visit_url( url ) - check_str = "Role '%s' has been updated with %d associated users and %d associated groups" % ( role_name, len( user_ids ), len( group_ids ) ) - self.check_page_for_string( check_str ) + self.visit_url( "%s/admin/role?role_id=%s" % ( self.url, role_id ) ) + self.check_page_for_string( 'Groups associated with' ) + # All group_ids passed in MUST be in the out_groups form field + for group_name in group_names: + tc.fv( "1", "out_groups", group_name ) # note the buttons... + tc.submit( "groups_add_button" ) + tc.submit( "role_members_edit_button" ) self.home() - + def associate_users_with_role( self, role_id, user_emails=[] ): + """Add a users to an existing role""" + # NOTE: To get this to work with twill, all select lists must contain at least 1 option value + # before tc.submit or twill throws an exception, which is: ParseError: OPTION outside of SELECT + self.home() + self.visit_url( "%s/admin/role?role_id=%s" % ( self.url, role_id ) ) + self.check_page_for_string( 'Users associated with' ) + for user_email in user_emails: + tc.fv( "1", "out_users", user_email ) + tc.submit( "users_add_button" ) + tc.submit( "role_members_edit_button" ) + self.home() + # Tests associated with groups - def create_group( self, name='Group One', in_user_ids=[], in_role_ids=[] ): - """Create a new group""" - url = "%s/admin/create_group?create_group_button=Save&name=%s" % ( self.url, name.replace( ' ', '+' ) ) - if in_user_ids: - url += "&in_users=%s" % ','.join( in_user_ids ) - if in_role_ids: - url += "&in_roles=%s" % ','.join( in_role_ids ) + def create_group( self, name='Group One', user_ids=[], role_ids=[] ): + """Create a new group with members and associated role""" self.home() - self.visit_url( url ) - check_str = "Group '%s' has been created with %d associated users and %d associated roles" % ( name, len( in_user_ids ), len( in_role_ids ) ) - self.check_page_for_string( check_str ) + self.visit_url( "%s/admin/create_group" % self.url ) + self.check_page_for_string( "Create Group" ) + # Make sure no private roles are displayed + try: + self.check_page_for_string( 'Private Role for' ) + raise AssertionError( 'Private role displayed on Create Group page' ) + except AssertionError: + # Reaching here is the behavior we want since no private roles should be displayed + pass + tc.fv( "1", "name", name ) + for user_id in user_ids: + tc.fv( "1", "members", user_id ) + for role_id in role_ids: + tc.fv( "1", "roles", role_id ) + tc.submit( "create_group_button" ) + try: + self.check_page_for_string( "The new group has been created" ) + previously_created = False + except: + self.check_page_for_string( "A group with that name already exists" ) + previously_created = True self.home() - self.visit_url( "%s/admin/groups" % self.url ) + self.visit_page( "admin/groups" ) self.check_page_for_string( name ) self.home() + return previously_created def rename_group( self, group_id, name='Group One Renamed' ): """Rename a group""" self.home() @@ -754,60 +786,63 @@ class TwillTestCase( unittest.TestCase ): tc.fv( "1", "name", name ) tc.submit( "rename_group_button" ) self.home() - def associate_users_and_roles_with_group( self, group_id, group_name, user_ids=[], role_ids=[] ): + def group_members_edit( self, group_id, user_ids=[] ): + """Add members to an existing group""" self.home() - url = "%s/admin/group?group_id=%s&group_roles_users_edit_button=Save" % ( self.url, group_id ) - if user_ids: - url += "&in_users=%s" % ','.join( user_ids ) - if role_ids: - url += "&in_roles=%s" % ','.join( role_ids ) - self.visit_url( url ) - check_str = "Group '%s' has been updated with %d associated roles and %d associated users" % ( group_name, len( role_ids ), len( user_ids ) ) - self.check_page_for_string( check_str ) + self.visit_url( "%s/admin/group_members_edit?group_id=%s" % ( self.url, group_id ) ) + self.check_page_for_string( 'Select to add user to' ) + for user_id in user_ids: + tc.fv( "1", "members", user_id ) + tc.submit( "group_members_edit_button" ) + self.check_page_for_string( 'Group membership has been updated' ) self.home() - def mark_group_deleted( self, group_id, group_name ): + def group_roles_edit( self, group_id, role_ids=[] ): + """Change roles associated with an existing group""" + self.home() + self.visit_url( "%s/admin/group_roles_edit?group_id=%s" % ( self.url, group_id ) ) + self.check_page_for_string( 'Select to associate role with' ) + for role_id in role_ids: + tc.fv( "1", "roles", role_id ) + tc.submit( "group_roles_edit_button" ) + self.check_page_for_string( 'Group updated with a total of' ) + self.home() + def mark_group_deleted( self, group_id ): """Mark a group as deleted""" self.home() self.visit_url( "%s/admin/mark_group_deleted?group_id=%s" % ( self.url, group_id ) ) - check_str = "Group '%s' has been marked as deleted" % group_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The group has been marked as deleted' ) self.home() - def undelete_group( self, group_id, group_name ): + def undelete_group( self, group_id ): """Undelete an existing group""" self.home() self.visit_url( "%s/admin/undelete_group?group_id=%s" % ( self.url, group_id ) ) - check_str = "Group '%s' has been marked as not deleted" % group_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The group has been marked as not deleted' ) self.home() - def purge_group( self, group_id, group_name ): + def purge_group( self, group_id ): """Purge an existing group""" self.home() self.visit_url( "%s/admin/purge_group?group_id=%s" % ( self.url, group_id ) ) - check_str = "The following have been purged from the database for group '%s': UserGroupAssociations, GroupRoleAssociations." % group_name - self.check_page_for_string( check_str ) + self.check_page_for_string( "The following have been purged from the database for the group: UserGroupAssociations, GroupRoleAssociations." ) self.home() # Utility methods to test removal of associations - def remove_role_from_group( self, role_id, role_name, group_id, group_name ): + def remove_role_from_group( self, role_id, group_id ): """Remove a role from a group""" self.home() self.visit_url( "%s/admin/remove_role_from_group?role_id=%s&group_id=%s" % ( self.url, role_id, group_id ) ) - check_str = "Role '%s' removed from group '%s'" % ( role_name, group_name ) - self.check_page_for_string( check_str ) + self.check_page_for_string( 'Role removed from group' ) self.home() - def remove_user_from_group( self, user_id, email, group_id, group_name ): + def remove_user_from_group( self, user_id, group_id ): """Remove a user from a group""" self.home() self.visit_url( "%s/admin/remove_user_from_group?user_id=%s&group_id=%s" % ( self.url, user_id, group_id ) ) - check_str = "User '%s' removed from group '%s'" % ( email, group_name ) - self.check_page_for_string( check_str ) + self.check_page_for_string( 'User removed from group' ) self.home() - def remove_user_from_role( self, user_id, email, role_id, role_name ): + def remove_user_from_role( self, user_id, role_id ): """Remove a user from a role""" self.home() self.visit_url( "%s/admin/remove_user_from_role?user_id=%s&role_id=%s" % ( self.url, user_id, role_id ) ) - check_str = "User '%s' removed from role '%s'" % ( email, role_name ) - self.check_page_for_string( check_str ) + self.check_page_for_string( 'User removed from role' ) self.home() # Library stuff @@ -889,28 +924,25 @@ class TwillTestCase( unittest.TestCase ): library_dir = "%s" % self.file_dir tc.fv( "1", "server_dir", "library" ) for role_tuple in roles_tuple: - tc.fv( "1", "roles", role_tuple[1] ) # role_tuple[1] is the role name + tc.fv( "1", "roles", role_tuple[1] ) # role_tuple[1] is the role description tc.submit( "new_dataset_button" ) self.check_page_for_string( '3 new datasets added to the library' ) self.home() - def mark_library_deleted( self, library_id, library_name ): + def mark_library_deleted( self, library_id ): """Mark a library as deleted""" self.home() self.visit_url( "%s/admin/library?id=%s&delete=True" % ( self.url, library_id ) ) - check_str = "Library '%s' and all of its contents have been marked deleted" % library_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The library and all of its contents have been marked deleted' ) self.home() - def undelete_library( self, library_id, library_name ): + def undelete_library( self, library_id ): """Mark a library as not deleted""" self.home() self.visit_url( "%s/admin/undelete_library?id=%s" % ( self.url, library_id ) ) - check_str = "Library '%s' and all of its contents have been marked not deleted" % library_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The library and all of its contents have been marked not deleted' ) self.home() - def purge_library( self, library_id, library_name ): + def purge_library( self, library_id ): """Purge a library""" self.home() self.visit_url( "%s/admin/purge_library?id=%s" % ( self.url, library_id ) ) - check_str = "Library '%s' and all of its contents have been purged" % library_name - self.check_page_for_string( check_str ) + self.check_page_for_string( 'The library and all of its contents have been purged' ) self.home() From 2e86322d45fdf63d725ab5dfc3dbf55b48938f3d Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 12 Jan 2009 20:42:37 -0500 Subject: [PATCH 163/267] Backed out changeset bc87cbb31647 Accidently overwrote 'twilltestcase.py' with and older version. --- test/base/twilltestcase.py | 224 ++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 128 deletions(-) diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 614c4a9eb1a..0f09a8c4d0a 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -13,7 +13,7 @@ from elementtree import ElementTree buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? -twill.set_output(buffer) +## twill.set_output(buffer) tc.config('use_tidy', 0) # Dial ClientCookie logging down (very noisy) @@ -629,56 +629,51 @@ class TwillTestCase( unittest.TestCase ): tc.submit( "reset_user_password_button" ) self.check_page_for_string( "Password reset" ) self.home() - def mark_user_deleted( self, user_id=4 ): + def mark_user_deleted( self, user_id=4, email='' ): """Mark a user as deleted""" self.home() self.visit_url( "%s/admin/mark_user_deleted?user_id=%s" % ( self.url, str( user_id ) ) ) - self.check_page_for_string( "The user has been marked as deleted." ) + check_str = "User '%s' has been marked as deleted." % email + self.check_page_for_string( check_str ) self.home() - def undelete_user( self, user_id ): + def undelete_user( self, user_id=4, email='' ): """Undelete a user""" self.home() self.visit_url( "%s/admin/undelete_user?user_id=%s" % ( self.url, user_id ) ) - self.check_page_for_string( 'The user has been marked as not deleted' ) + check_str = "User '%s' has been marked as not deleted" % email + self.check_page_for_string( check_str ) self.home() - def purge_user( self, user_id ): + def purge_user( self, user_id, email ): """Purge a user account""" self.home() self.visit_url( "%s/admin/purge_user?user_id=%s" % ( self.url, user_id ) ) - self.check_page_for_string( 'The user has been marked as purged.' ) + check_str = "User '%s' has been marked as purged." % email + self.check_page_for_string( check_str ) self.home() - def user_roles_edit( self, user_id, role_ids=[] ): - """Change roles associated with an existing user""" + def associate_roles_and_groups_with_user( self, user_id, email, role_ids=[], group_ids=[] ): self.home() - self.visit_url( "%s/admin/user_roles_edit?user_id=%s" % ( self.url, user_id ) ) - self.check_page_for_string( 'Select to associate role with' ) - for role_id in role_ids: - tc.fv( "1", "roles", role_id ) - tc.submit( "user_roles_edit_button" ) - self.check_page_for_string( 'User updated with a total of' ) + url = "%s/admin/user?user_id=%s&user_roles_groups_edit_button=Save" % ( self.url, user_id ) + if role_ids: + url += "&in_roles=%s" % ','.join( role_ids ) + if group_ids: + url += "&in_groups=%s" % ','.join( group_ids ) + self.visit_url( url ) + check_str = "User '%s' has been updated with %d associated roles and %d associated groups" % ( email, len( role_ids ), len( group_ids ) ) + self.check_page_for_string( check_str ) self.home() # Tests associated with roles - def create_role( self, name='Role One', description="This is Role One", user_ids=[], group_ids=[], private_role='' ): + def create_role( self, name='Role One', description="This is Role One", in_user_ids=[], in_group_ids=[], private_role='' ): """Create a new role""" + url = "%s/admin/create_role?create_role_button=Save&name=%s&description=%s" % ( self.url, name.replace( ' ', '+' ), description.replace( ' ', '+' ) ) + if in_user_ids: + url += "&in_users=%s" % ','.join( in_user_ids ) + if in_group_ids: + url += "&in_groups=%s" % ','.join( in_group_ids ) self.home() - self.visit_url( "%s/admin/create_role" % self.url ) - self.check_page_for_string( "Create Role" ) - tc.fv( "1", "name", name ) - tc.fv( "1", "description", description ) - for user_id in user_ids: - tc.fv( "1", "users", user_id ) - for group_id in group_ids: - tc.fv( "1", "groups", group_id ) - tc.submit( "create_role_button" ) - check_str = "The new role has been created with %s associated users and %s associated groups" % ( str( len( user_ids ) ), str( len( group_ids ) ) ) - try: - self.check_page_for_string( check_str ) - previously_created = False - except: - # The role may have been created on a previous test run - self.check_page_for_string( "A role with that name already exists" ) - previously_created = True + self.visit_url( url ) + check_str = "Role '%s' has been created with %d associated users and %d associated groups" % ( name, len( in_user_ids ), len( in_group_ids ) ) + self.check_page_for_string( check_str ) if private_role: # Make sure no private roles are displayed try: @@ -689,10 +684,9 @@ class TwillTestCase( unittest.TestCase ): # Reaching here is the behavior we want since no private roles should be displayed pass self.home() - self.visit_page( "admin/roles" ) - self.check_page_for_string( description ) + self.visit_url( "%s/admin/roles" % self.url ) + self.check_page_for_string( name ) self.home() - return previously_created def rename_role( self, role_id, name='Role One Renamed', description='This is Role One Re-described' ): """Rename a role""" self.home() @@ -702,82 +696,56 @@ class TwillTestCase( unittest.TestCase ): tc.fv( "1", "description", description ) tc.submit( "rename_role_button" ) self.home() - def mark_role_deleted( self, role_id ): + def mark_role_deleted( self, role_id, role_name ): """Mark a role as deleted""" self.home() self.visit_url( "%s/admin/mark_role_deleted?role_id=%s" % ( self.url, role_id ) ) - self.check_page_for_string( 'The role has been marked as deleted' ) + check_str = "Role '%s' has been marked as deleted" % role_name + self.check_page_for_string( check_str ) self.home() - def undelete_role( self, role_id ): + def undelete_role( self, role_id, role_name ): """Undelete an existing role""" self.home() self.visit_url( "%s/admin/undelete_role?role_id=%s" % ( self.url, role_id ) ) - self.check_page_for_string( 'The role has been marked as not deleted' ) + check_str = "Role '%s' has been marked as not deleted" % role_name + self.check_page_for_string( check_str ) self.home() - def purge_role( self, role_id ): + def purge_role( self, role_id, role_name ): """Purge an existing role""" self.home() self.visit_url( "%s/admin/purge_role?role_id=%s" % ( self.url, role_id ) ) - check_str = "The following have been purged from the database for the role: " + check_str = "The following have been purged from the database for role '%s': " % role_name check_str += "DefaultUserPermissions, DefaultHistoryPermissions, UserRoleAssociations, GroupRoleAssociations, ActionDatasetRoleAssociations." self.check_page_for_string( check_str ) self.home() - def associate_groups_with_role( self, role_id, group_names=[] ): - """Add groups to an existing role""" - # NOTE: To get this to work with twill, all select lists must contain at least 1 option value - # before tc.submit or twill throws an exception, which is: ParseError: OPTION outside of SELECT + def associate_users_and_groups_with_role( self, role_id, role_name, user_ids=[], group_ids=[] ): self.home() - self.visit_url( "%s/admin/role?role_id=%s" % ( self.url, role_id ) ) - self.check_page_for_string( 'Groups associated with' ) - # All group_ids passed in MUST be in the out_groups form field - for group_name in group_names: - tc.fv( "1", "out_groups", group_name ) # note the buttons... - tc.submit( "groups_add_button" ) - tc.submit( "role_members_edit_button" ) + url = "%s/admin/role?role_id=%s&role_members_edit_button=Save" % ( self.url, role_id ) + if user_ids: + url += "&in_users=%s" % ','.join( user_ids ) + if group_ids: + url += "&in_groups=%s" % ','.join( group_ids ) + self.visit_url( url ) + check_str = "Role '%s' has been updated with %d associated users and %d associated groups" % ( role_name, len( user_ids ), len( group_ids ) ) + self.check_page_for_string( check_str ) self.home() - def associate_users_with_role( self, role_id, user_emails=[] ): - """Add a users to an existing role""" - # NOTE: To get this to work with twill, all select lists must contain at least 1 option value - # before tc.submit or twill throws an exception, which is: ParseError: OPTION outside of SELECT - self.home() - self.visit_url( "%s/admin/role?role_id=%s" % ( self.url, role_id ) ) - self.check_page_for_string( 'Users associated with' ) - for user_email in user_emails: - tc.fv( "1", "out_users", user_email ) - tc.submit( "users_add_button" ) - tc.submit( "role_members_edit_button" ) - self.home() - + # Tests associated with groups - def create_group( self, name='Group One', user_ids=[], role_ids=[] ): - """Create a new group with members and associated role""" + def create_group( self, name='Group One', in_user_ids=[], in_role_ids=[] ): + """Create a new group""" + url = "%s/admin/create_group?create_group_button=Save&name=%s" % ( self.url, name.replace( ' ', '+' ) ) + if in_user_ids: + url += "&in_users=%s" % ','.join( in_user_ids ) + if in_role_ids: + url += "&in_roles=%s" % ','.join( in_role_ids ) self.home() - self.visit_url( "%s/admin/create_group" % self.url ) - self.check_page_for_string( "Create Group" ) - # Make sure no private roles are displayed - try: - self.check_page_for_string( 'Private Role for' ) - raise AssertionError( 'Private role displayed on Create Group page' ) - except AssertionError: - # Reaching here is the behavior we want since no private roles should be displayed - pass - tc.fv( "1", "name", name ) - for user_id in user_ids: - tc.fv( "1", "members", user_id ) - for role_id in role_ids: - tc.fv( "1", "roles", role_id ) - tc.submit( "create_group_button" ) - try: - self.check_page_for_string( "The new group has been created" ) - previously_created = False - except: - self.check_page_for_string( "A group with that name already exists" ) - previously_created = True + self.visit_url( url ) + check_str = "Group '%s' has been created with %d associated users and %d associated roles" % ( name, len( in_user_ids ), len( in_role_ids ) ) + self.check_page_for_string( check_str ) self.home() - self.visit_page( "admin/groups" ) + self.visit_url( "%s/admin/groups" % self.url ) self.check_page_for_string( name ) self.home() - return previously_created def rename_group( self, group_id, name='Group One Renamed' ): """Rename a group""" self.home() @@ -786,63 +754,60 @@ class TwillTestCase( unittest.TestCase ): tc.fv( "1", "name", name ) tc.submit( "rename_group_button" ) self.home() - def group_members_edit( self, group_id, user_ids=[] ): - """Add members to an existing group""" + def associate_users_and_roles_with_group( self, group_id, group_name, user_ids=[], role_ids=[] ): self.home() - self.visit_url( "%s/admin/group_members_edit?group_id=%s" % ( self.url, group_id ) ) - self.check_page_for_string( 'Select to add user to' ) - for user_id in user_ids: - tc.fv( "1", "members", user_id ) - tc.submit( "group_members_edit_button" ) - self.check_page_for_string( 'Group membership has been updated' ) + url = "%s/admin/group?group_id=%s&group_roles_users_edit_button=Save" % ( self.url, group_id ) + if user_ids: + url += "&in_users=%s" % ','.join( user_ids ) + if role_ids: + url += "&in_roles=%s" % ','.join( role_ids ) + self.visit_url( url ) + check_str = "Group '%s' has been updated with %d associated roles and %d associated users" % ( group_name, len( role_ids ), len( user_ids ) ) + self.check_page_for_string( check_str ) self.home() - def group_roles_edit( self, group_id, role_ids=[] ): - """Change roles associated with an existing group""" - self.home() - self.visit_url( "%s/admin/group_roles_edit?group_id=%s" % ( self.url, group_id ) ) - self.check_page_for_string( 'Select to associate role with' ) - for role_id in role_ids: - tc.fv( "1", "roles", role_id ) - tc.submit( "group_roles_edit_button" ) - self.check_page_for_string( 'Group updated with a total of' ) - self.home() - def mark_group_deleted( self, group_id ): + def mark_group_deleted( self, group_id, group_name ): """Mark a group as deleted""" self.home() self.visit_url( "%s/admin/mark_group_deleted?group_id=%s" % ( self.url, group_id ) ) - self.check_page_for_string( 'The group has been marked as deleted' ) + check_str = "Group '%s' has been marked as deleted" % group_name + self.check_page_for_string( check_str ) self.home() - def undelete_group( self, group_id ): + def undelete_group( self, group_id, group_name ): """Undelete an existing group""" self.home() self.visit_url( "%s/admin/undelete_group?group_id=%s" % ( self.url, group_id ) ) - self.check_page_for_string( 'The group has been marked as not deleted' ) + check_str = "Group '%s' has been marked as not deleted" % group_name + self.check_page_for_string( check_str ) self.home() - def purge_group( self, group_id ): + def purge_group( self, group_id, group_name ): """Purge an existing group""" self.home() self.visit_url( "%s/admin/purge_group?group_id=%s" % ( self.url, group_id ) ) - self.check_page_for_string( "The following have been purged from the database for the group: UserGroupAssociations, GroupRoleAssociations." ) + check_str = "The following have been purged from the database for group '%s': UserGroupAssociations, GroupRoleAssociations." % group_name + self.check_page_for_string( check_str ) self.home() # Utility methods to test removal of associations - def remove_role_from_group( self, role_id, group_id ): + def remove_role_from_group( self, role_id, role_name, group_id, group_name ): """Remove a role from a group""" self.home() self.visit_url( "%s/admin/remove_role_from_group?role_id=%s&group_id=%s" % ( self.url, role_id, group_id ) ) - self.check_page_for_string( 'Role removed from group' ) + check_str = "Role '%s' removed from group '%s'" % ( role_name, group_name ) + self.check_page_for_string( check_str ) self.home() - def remove_user_from_group( self, user_id, group_id ): + def remove_user_from_group( self, user_id, email, group_id, group_name ): """Remove a user from a group""" self.home() self.visit_url( "%s/admin/remove_user_from_group?user_id=%s&group_id=%s" % ( self.url, user_id, group_id ) ) - self.check_page_for_string( 'User removed from group' ) + check_str = "User '%s' removed from group '%s'" % ( email, group_name ) + self.check_page_for_string( check_str ) self.home() - def remove_user_from_role( self, user_id, role_id ): + def remove_user_from_role( self, user_id, email, role_id, role_name ): """Remove a user from a role""" self.home() self.visit_url( "%s/admin/remove_user_from_role?user_id=%s&role_id=%s" % ( self.url, user_id, role_id ) ) - self.check_page_for_string( 'User removed from role' ) + check_str = "User '%s' removed from role '%s'" % ( email, role_name ) + self.check_page_for_string( check_str ) self.home() # Library stuff @@ -924,25 +889,28 @@ class TwillTestCase( unittest.TestCase ): library_dir = "%s" % self.file_dir tc.fv( "1", "server_dir", "library" ) for role_tuple in roles_tuple: - tc.fv( "1", "roles", role_tuple[1] ) # role_tuple[1] is the role description + tc.fv( "1", "roles", role_tuple[1] ) # role_tuple[1] is the role name tc.submit( "new_dataset_button" ) self.check_page_for_string( '3 new datasets added to the library' ) self.home() - def mark_library_deleted( self, library_id ): + def mark_library_deleted( self, library_id, library_name ): """Mark a library as deleted""" self.home() self.visit_url( "%s/admin/library?id=%s&delete=True" % ( self.url, library_id ) ) - self.check_page_for_string( 'The library and all of its contents have been marked deleted' ) + check_str = "Library '%s' and all of its contents have been marked deleted" % library_name + self.check_page_for_string( check_str ) self.home() - def undelete_library( self, library_id ): + def undelete_library( self, library_id, library_name ): """Mark a library as not deleted""" self.home() self.visit_url( "%s/admin/undelete_library?id=%s" % ( self.url, library_id ) ) - self.check_page_for_string( 'The library and all of its contents have been marked not deleted' ) + check_str = "Library '%s' and all of its contents have been marked not deleted" % library_name + self.check_page_for_string( check_str ) self.home() - def purge_library( self, library_id ): + def purge_library( self, library_id, library_name ): """Purge a library""" self.home() self.visit_url( "%s/admin/purge_library?id=%s" % ( self.url, library_id ) ) - self.check_page_for_string( 'The library and all of its contents have been purged' ) + check_str = "Library '%s' and all of its contents have been purged" % library_name + self.check_page_for_string( check_str ) self.home() From a02598a430bd96bdfa6819ffbff29b137f358a6d Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 12 Jan 2009 20:43:38 -0500 Subject: [PATCH 164/267] Throw away twill's output again. --- test/base/twilltestcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/base/twilltestcase.py b/test/base/twilltestcase.py index 0f09a8c4d0a..1b1627f7711 100644 --- a/test/base/twilltestcase.py +++ b/test/base/twilltestcase.py @@ -13,7 +13,7 @@ from elementtree import ElementTree buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? -## twill.set_output(buffer) +twill.set_output(buffer) tc.config('use_tidy', 0) # Dial ClientCookie logging down (very noisy) From 1b3978609f8a0f29138d913a691e3af45eb9218c Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 13 Jan 2009 16:36:46 -0500 Subject: [PATCH 165/267] Add SOLiD Quality datatype and add a sniffer for Color Space FASTA. --- lib/galaxy/datatypes/qualityscore.py | 40 +++++++++++++++++++++++++++- lib/galaxy/datatypes/sequence.py | 39 +++++++++++++++++++++------ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/lib/galaxy/datatypes/qualityscore.py b/lib/galaxy/datatypes/qualityscore.py index cfd5b280adb..766761e82dc 100644 --- a/lib/galaxy/datatypes/qualityscore.py +++ b/lib/galaxy/datatypes/qualityscore.py @@ -28,5 +28,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/sequence.py b/lib/galaxy/datatypes/sequence.py index 2ec55345cc3..0033ff79a1f 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 @@ -94,15 +95,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" From e9d2b617be50a496037fbc935959a1edbb4b114d Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Tue, 13 Jan 2009 16:40:23 -0500 Subject: [PATCH 166/267] Datatypes config for solidqual, as well as solidqual and csfasta sniffers --- datatypes_conf.xml.sample | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index 431d9da7c95..3d1f94b94d5 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -38,6 +38,7 @@ + @@ -175,15 +176,17 @@ - - - - - - - - - - + + + + + + + + + + + + From 43b925d59bac06c3e7bf033ea401f9de96751a30 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 14 Jan 2009 17:56:30 -0500 Subject: [PATCH 167/267] New tool menu styles for labels and top-level tools. --- static/june_2007_style/blue/tool_menu.css | 15 +++++++++++--- static/june_2007_style/tool_menu.css.tmpl | 15 +++++++++++--- templates/root/tool_menu.mako | 25 +++++++++-------------- 3 files changed, 34 insertions(+), 21 deletions(-) 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/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/templates/root/tool_menu.mako b/templates/root/tool_menu.mako index c35d9868519..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,18 +125,16 @@ 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
          From 85632afd4f4723a1f072a4ef963fe6a8e95155f8 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 16 Jan 2009 15:02:44 -0500 Subject: [PATCH 168/267] Add the ability to keep specified permissions boxes from being displayed on certain forms. --- templates/dataset/security_common.mako | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/templates/dataset/security_common.mako b/templates/dataset/security_common.mako index 039a9a7ba6d..0643e22f1f3 100644 --- a/templates/dataset/security_common.mako +++ b/templates/dataset/security_common.mako @@ -29,7 +29,8 @@ -<%def name="render_permission_form( obj, obj_name, form_url, id_name, id, all_roles )"> +## 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=[] )"> <% if isinstance( obj, trans.app.model.User ): current_actions = obj.default_permissions @@ -73,9 +74,11 @@
          %for k, v in trans.app.model.Dataset.permitted_actions.items(): -
          - ${render_select( current_actions, k, v, all_roles )} -
          + %if k not in do_not_render: +
          + ${render_select( current_actions, k, v, all_roles )} +
          + %endif %endfor
          From 2c85c6fee962f972722afc60781ee2090f3c48d1 Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Mon, 19 Jan 2009 12:12:36 -0500 Subject: [PATCH 169/267] Preliminary commity for lca. Still in progress. --- static/welcome.html | 107 +++++++++++++++++++++---------- tools/taxonomy/find_diag_hits.py | 7 +- tools/taxonomy/lca.py | 47 +++++++++++++- tools/taxonomy/lca.xml | 38 ++++++++++- 4 files changed, 159 insertions(+), 40 deletions(-) diff --git a/static/welcome.html b/static/welcome.html index 5bd2ab245cd..08021b9fe94 100644 --- a/static/welcome.html +++ b/static/welcome.html @@ -6,47 +6,88 @@ + + + + + + + + + + +
          -
          - Workflows are finally here! -
          - Watch how you can (Click link to play)... - -
          - For more screencasts click here. -
          -
          +
          + The Galaxy Main Server was recently upgraded. -
          - Unsequenced Genomes of the World | October 2008 -
          -
          - -
          - Costa's hummingbird (Calypte costae) | Kings Canyon NP, California -
          -
          +
          + There was some instability as Galaxy was restarted numerous times between 8:30 and 10:00 AM EST (UTC -0500) this morning (Wednesday, January 14). Any jobs from this time that ended in error can be tried again. If you continue to have any problems, please contact the Galaxy Team by using the 'report this error' link within a red history item, or emailing galaxy-bugs@bx.psu.edu. +
          + +

          +

          + We are hiring! +
          + Thanks to your support and an unprecedented level of usage, we are looking for an experienced software developer to join our team. For more information about this position, please, click here.

          -
          - Galaxy is for Biologists -
          - Use this site to access popular sources of data like the UCSC Table Browser. Run analyses right on the spot using a variety of integrated tools. Your results are always available and can be easily shared with others. Just watch how. -
          -
          -
          - Galaxy is for Developers -
          - Galaxy is an easy-to-use, open-source, scalable framework for tool and data integration. Stop wasting time writing interfaces and get your tools used by biologists! Galaxy includes everything you need to get started, so download and start integrating! -
          + +
          +Introducing Galactic Quickies +
          + Galactic quickies are super-short screencasts that are always under 5 minutes. We thought it may be a good way to spread the word about Galaxy's functionality while keeping the "annoyance factor" to the minimum. The quickies will be updated weekly. +
          +
          History Name:Number of Datasets:Share Link
          + + + +
          + +
          + + + +
          +
          + For a high resolution version click here. +
          +
          +
          + +
          +

          Galaxy team is a part of BX at Penn State.


          This project is supported in part by NSF and the Huck Institutes of the Life Sciences.

          Galaxy build: $Rev$

          diff --git a/tools/taxonomy/find_diag_hits.py b/tools/taxonomy/find_diag_hits.py index a481cab65dd..993c066bb2b 100644 --- a/tools/taxonomy/find_diag_hits.py +++ b/tools/taxonomy/find_diag_hits.py @@ -71,7 +71,8 @@ taxRank = { 'genus' :20, 'subgenus' :21, 'species' :22, - 'subspecies' :23 + 'subspecies' :23, + 'order' :13 } @@ -157,12 +158,16 @@ try: for item in cur.fetchall(): out_string = '%s\t%s\t%d\t' % ( item[0], item[1], item[2] ) out_string += rankName + out_string += '\t' + out_string += str(taxRank[rankName]) print >>out_file, out_string else: cur.execute('select rank, count(*) from %s_count where N = 1 and length(rank)>1 group by rank' % rank) for item in cur.fetchall(): out_string = '%s\t%s\t' % ( item[0], item[1] ) out_string += rankName + out_string += '\t' + out_string += str(taxRank[rankName]) print >>out_file, out_string except Exception, e: stop_err("%s\n" % e) diff --git a/tools/taxonomy/lca.py b/tools/taxonomy/lca.py index beb356f160b..0b9785d9dd5 100644 --- a/tools/taxonomy/lca.py +++ b/tools/taxonomy/lca.py @@ -14,6 +14,32 @@ def main(): try: inputfile = sys.argv[1] outfile = sys.argv[2] + rank_bound = int( sys.argv[3] ) + """ + Mapping of ranks: + root :2, + superkingdom:3, + kingdom :4, + subkingdom :5, + superphylum :6, + phylum :7, + subphylum :8, + superclass :9, + class :10, + subclass :11, + superorder :12, + order :13, + suborder :14, + superfamily :15, + family :16, + subfamily :17, + tribe :18, + subtribe :19, + genus :20, + subgenus :21, + species :22, + subspecies :23, + """ except: stop_err("Syntax error: Use correct syntax: program infile outfile") group_col = 0 @@ -91,7 +117,16 @@ def main(): out_list[k+1] = 'n' k += 1 - print >>fout, '\t'.join(out_list) + # print >>fout, '\t'.join(out_list) + + if rank_bound == 0: + print >>fout, ''.join(out_list) + print 'n'*( 24 - rank_bound ) + else: + print '\t'.join(out_list[rank_bound:24]) + if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): + print >>fout, '\t'.join(out_list) + block_valid = True prev_item = item @@ -134,9 +169,15 @@ def main(): while k < 23: out_list[k+1] = 'n' k += 1 - - print >>fout, '\t'.join(out_list) + if rank_bound == 0: + print >>fout, '\t'.join(out_list) + else: + print ''.join(out_list[rank_bound:24]) + print 'n'*( 24 - rank_bound ) + if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): + print >>fout, '\t'.join(out_list) + if skipped_lines > 0: msg= "Skipped %d invalid lines starting with line %d. Value '%s' in column %d is not numeric." % ( skipped_lines, first_invalid_line, invalid_value, invalid_column ) print msg diff --git a/tools/taxonomy/lca.xml b/tools/taxonomy/lca.xml index 8ba556c50dd..f608a16cfc3 100644 --- a/tools/taxonomy/lca.xml +++ b/tools/taxonomy/lca.xml @@ -1,12 +1,44 @@ - lca.py $input1 $out_file1 + lca.py $input1 $out_file1 $rank_bound - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + +**What it does** + +When performing metagenomic analyses it is often necessary to identify sequence reads corresponding to a particular taxonomic group, or, in other words, diagnostic of a particular taxonomic rank. This utility performs this analysis. It takes data generated by *Taxonomy manipulation->Fetch Taxonomic Ranks* as input and outputs either a list of sequence reads unique to a particular taxonomic rank, or a list of taxonomic ranks and the count of unique reads corresponding to each rank. + + + \ No newline at end of file From 6a0aa166aa8a1691071eed96f248e593da5c15fc Mon Sep 17 00:00:00 2001 From: Ian Schenck Date: Mon, 19 Jan 2009 17:45:28 -0500 Subject: [PATCH 170/267] Initial GeneTrack commit. Most parts are in, but dependencies will still be a problem. --- datatypes_conf.xml.sample | 2 + lib/galaxy/datatypes/coverage.py | 30 +++++ lib/galaxy/datatypes/registry.py | 6 +- lib/galaxy/datatypes/tracks.py | 30 +++++ lib/galaxy/web/controllers/genetrack.py | 161 ++++++++++++++++++++++++ scripts/paster.py | 2 +- static/genetrack/genetrack.css | 78 ++++++++++++ static/genetrack/genetrack.js | 79 ++++++++++++ tool_conf.xml.sample | 4 +- tools/sr_mapping/lastz_wrapper.xml | 2 +- tools/visualization/genetrack.py | 139 ++++++++++++++++++++ tools/visualization/genetrack.xml | 53 ++++++++ tools/visualization/genetrack_code.py | 13 ++ 13 files changed, 594 insertions(+), 5 deletions(-) create mode 100644 lib/galaxy/datatypes/coverage.py create mode 100644 lib/galaxy/datatypes/tracks.py create mode 100644 lib/galaxy/web/controllers/genetrack.py create mode 100644 static/genetrack/genetrack.css create mode 100644 static/genetrack/genetrack.js create mode 100644 tools/visualization/genetrack.py create mode 100644 tools/visualization/genetrack.xml create mode 100644 tools/visualization/genetrack_code.py diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index 431d9da7c95..391f7937663 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -82,6 +82,8 @@ + + diff --git a/lib/galaxy/datatypes/coverage.py b/lib/galaxy/datatypes/coverage.py new file mode 100644 index 00000000000..4bd76425a71 --- /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 ) + \ No newline at end of file diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index c27cce91188..1639e904964 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 @@ -97,12 +97,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/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/web/controllers/genetrack.py b/lib/galaxy/web/controllers/genetrack.py new file mode 100644 index 00000000000..ae81a277cd4 --- /dev/null +++ b/lib/galaxy/web/controllers/genetrack.py @@ -0,0 +1,161 @@ +import time, glob, os + +import pkg_resources +pkg_resources.require("GeneTrack") + +import atlas +from atlas import sql +from atlas import util as atlas_utils +from atlas.web import formlib +from mako import exceptions +from mako.template import Template +from mako.lookup import TemplateLookup +from galaxy.web.base.controller import * + +pkg_resources.require( "Paste" ) +import paste.httpexceptions + +# SETUP Track Builders +from mod454.trackbuilder import build_tracks +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), + ) + from atlas import hdf + 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() + + param = atlas.Param( word=word ) + # search with features based on param.feature + + # 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 + """ + 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), + ) + from atlas import hdf + 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() + + # 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 ) ) + + # keep image at a sane size + param.width = min( [2000, int(param.img_size)] ) + + # get the template and the function used to generate the tracks + tmpl_name, track_maker = conf.PLOT_MAPPER[param.plot] + + if track_maker is not None: + # generate the name that the image will be stored at + fname, fpath = atlas_utils.make_tempfile( dir=conf.IMAGE_DIR, suffix='.png') + param.fname = fname + + # generate the track + track_chart = track_maker( param=param, conf=conf ) + 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/scripts/paster.py b/scripts/paster.py index 7624257a665..446cb9de90b 100755 --- a/scripts/paster.py +++ b/scripts/paster.py @@ -12,7 +12,7 @@ assert sys.version_info[:2] >= ( 2, 4 ) new_path = [ os.path.join( os.getcwd(), "lib" ) ] new_path.extend( sys.path[1:] ) # remove scripts/ from the path sys.path = new_path - +print sys.path from galaxy import eggs import pkg_resources 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/tool_conf.xml.sample b/tool_conf.xml.sample index f46c9cef558..47cf0c1e734 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -11,7 +11,6 @@ - @@ -302,4 +301,7 @@
      +
      + +
      diff --git a/tools/sr_mapping/lastz_wrapper.xml b/tools/sr_mapping/lastz_wrapper.xml index e3aabf57243..f29d5fb7168 100644 --- a/tools/sr_mapping/lastz_wrapper.xml +++ b/tools/sr_mapping/lastz_wrapper.xml @@ -82,7 +82,7 @@ - + lastz diff --git a/tools/visualization/genetrack.py b/tools/visualization/genetrack.py new file mode 100644 index 00000000000..bd27d5df4d2 --- /dev/null +++ b/tools/visualization/genetrack.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +""" +Run GeneTrack(atlas) with a faked conf file to generate GeneTrack data files. + +usage: %prog + -l, --label=N: Data label for fit curve/peak plot + -1, --fits=N/N/N/N/N,...: Data files (interval format) for fit curve/peak plot + -2, --feats=N:M/N/N/N/N/N,...: Data files (interval format) for features. + -d, --data=N: Output path for hdf5 and sqlite databases. + -o, --output=N: Output path for export file. +""" +from galaxy import eggs +import pkg_resources +pkg_resources.require("GeneTrack") +pkg_resources.require("bx-python") + +from atlas import commands +from bx.cookbook import doc_optparse +import os +import commands as oscommands +import tempfile + +SIGMA = 20 +WIDTH = 5 * SIGMA +EXCLUSION_ZONE = 147 + +def main(label, fit, feats, data_dir, output): + os.mkdir(data_dir) + conf = DummyConf( + __name__=label, + CLOBBER = True, + DATA_SIZE = 3*10**6, + MINIMUM_PEAK_SIZE = 0.1, + LOADER_ENABLED = True, + FITTER_ENABLED = True, + PREDICTOR_ENABLED = True, + EXPORTER_ENABLED = True, + LOADER = loader, + FITTER = fitter, + PREDICTOR = predictor, + EXPORTER = exporter, + HDF_DATABASE = os.path.join( data_dir, "data.hdf" ), + SQL_URI = "sqlite:///%s" % os.path.join( data_dir, "features.sqlite" ), + SIGMA = SIGMA, + WIDTH = WIDTH, + DATA_LABEL = label, + FIT_LABEL = "%s-SIGMA-%d" % ( label,SIGMA ), + PEAK_LABEL = "PRED-%s-SIGMA-%d" % ( label,SIGMA ), + EXCLUSION_ZONE = EXCLUSION_ZONE, + LEFT_SHIFT = EXCLUSION_ZONE / 2, + RIGHT_SHIFT = EXCLUSION_ZONE / 2, + EXPORT_LABELS = [ "PRED-%s-SIGMA-%d" % ( label,SIGMA ) ], + EXPORT_DIR = os.path.join( data_dir ), + DATA_FILE=fit[1], + fit=fit, + feats=feats, + ) + commands.execute(conf) + +# mod454 seems to be a module without a package. The necessary funcitons are +# stubbed out here until I'm sure of their final home. INS + +def loader( conf ): + from atlas import hdf + from mod454.schema import Mod454Schema as Schema + last_chrom = table = None + db = hdf.hdf_open( conf.HDF_DATABASE, mode='a', title='HDF database') + gp = hdf.create_group( db=db, name=conf.DATA_LABEL, desc='data group', clobber=conf.CLOBBER ) + fit_meta = conf.fit[2] + # iterate over the file and insert into table + for line in open( conf.fit[1], "r" ): + if line.startswith("chrom"): continue #Skip possible header + if line.startswith("#"): continue + fields = line.rstrip('\r\n').split('\t') + chrom = fields[fit_meta.chromCol] + if chrom != last_chrom: + if table: table.flush() + table = hdf.create_table( db=db, name=chrom, where=gp, schema=Schema, clobber=False ) + last_chrom = chrom + try: + position = int(fields[fit_meta.positionCol]) + forward = float(fields[fit_meta.forwardCol]) + reverse = fit_meta.reverseCol > -1 and float(fields[fit_meta.reverseCol]) or 0.0 + row = ( position, forward, reverse, forward+reverse, ) + table.append( [ row ] ) + except ValueError: + # Ignore bad lines + pass + table.flush() + db.close() + +def fitter( conf ): + from mod454.fitter import fitter as mod454_fitter + return mod454_fitter( conf ) + +def predictor( conf ): + from mod454.predictor import predictor as mod454_predictor + return mod454_predictor( conf ) + +def exporter( conf ): + return commands.bed_exporter(conf) + +class Bunch( object ): + def __init__(self, **kwargs): + for key,value in kwargs.items(): + setattr( self, key, value ) + +class DummyConf( Bunch ): + """ + Fake conf module for genetrack/atlas. + """ + pass + +if __name__ == "__main__": + options, args = doc_optparse.parse( __doc__ ) + try: + label = options.label + fit_name, fit_meta = options.fits.split(':')[0], [int(x)-1 for x in options.fits.split(':')[1:]] + fit_meta = Bunch(chromCol=fit_meta[0], positionCol=fit_meta[1], forwardCol=fit_meta[2], reverseCol=fit_meta[3]) + fit = ( label, fit_name, fit_meta, ) + # split apart the string into nested lists, preserves order + if options.feats: + feats = [ ( + feat_label, + fname, + Bunch(chromCol=int(chromCol)-1, startCol=int(startCol)-1, endCol=int(endCol)-1, + strandCol=int(strandCol)-1, nameCol=int(nameCol)-1), + ) + for feat_label, fname, chromCol, startCol, endCol, strandCol, nameCol + in ( feat.split(':') for feat in options.feats.split(',') )] + else: + feats = [] + data_dir = options.data + output = options.output + except: + doc_optparse.exception() + + main(label, fit, feats, data_dir, output) + \ No newline at end of file diff --git a/tools/visualization/genetrack.xml b/tools/visualization/genetrack.xml new file mode 100644 index 00000000000..65c5f519b31 --- /dev/null +++ b/tools/visualization/genetrack.xml @@ -0,0 +1,53 @@ + + + Track creator/viewer + + + + + + + genetrack.py -l $data_label + -1 ${fit_data}:${fit_data.metadata.chromCol}:${fit_data.metadata.positionCol}:${fit_data.metadata.forwardCol}:${fit_data.metadata.reverseCol} + #if $feature_data + -2 + #end if + #for $data in $feature_data + ${data.name}:${data.input}:${data.input.metadata.chromCol}:${data.input.metadata.startCol}:${data.input.metadata.endCol}:${data.input.metadata.strandCol}:${data.input.metadata.nameCol}, + #end for + -d ${genetrack.files_path} + -o ${bed_out} + + + + + [a-zA-Z0-9]{0,25} + + + + + + [a-zA-Z0-9]{0,25} + + + + + + + + + + +This tool takes the input Fit Data and creates a peak and curve plot showing +the reads and fitness on each basepair. Features can be plotted below as tracks. + +----- + +**Syntax** + +- **Track Label** is the name of the generated track. +- **Fit Data** are the datasets to calculate coverage/reads across basepairs and generate a curve. +- **Features** are additional datasets (interval format) to be plotted below as tracks. + + + diff --git a/tools/visualization/genetrack_code.py b/tools/visualization/genetrack_code.py new file mode 100644 index 00000000000..9c20ec27b73 --- /dev/null +++ b/tools/visualization/genetrack_code.py @@ -0,0 +1,13 @@ +import sets, os +from galaxy import eggs +from galaxy import jobs +from galaxy.tools.parameters import DataToolParameter + +def exec_after_process(app, inp_data, out_data, param_dict, tool=None, stdout=None, stderr=None): + """ + Copy data_label to genetrack.metadata.label + """ + out_data['genetrack'].metadata.label = param_dict['data_label'] + out_data['genetrack'].info = "Use the link below to view the custom track." + out_data['bed_out'].info = "" + \ No newline at end of file From 84535eb6e8dc5963a4ece2a283e4db2109edebd3 Mon Sep 17 00:00:00 2001 From: Guruprasad Anada Date: Tue, 20 Jan 2009 13:24:12 -0500 Subject: [PATCH 171/267] Changes to exception handling in LCA. --- tools/taxonomy/lca.py | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/tools/taxonomy/lca.py b/tools/taxonomy/lca.py index 0b9785d9dd5..ca6a320af24 100644 --- a/tools/taxonomy/lca.py +++ b/tools/taxonomy/lca.py @@ -1,10 +1,9 @@ #!/usr/bin/env python #Guruprasad Ananda """ -This tool provides the SQL "group by" functionality. +Least Common Ancestor tool. """ import sys, string, re, commands, tempfile, random -#from rpy import * def stop_err(msg): sys.stderr.write(msg) @@ -42,8 +41,8 @@ def main(): """ except: stop_err("Syntax error: Use correct syntax: program infile outfile") + group_col = 0 - tmpfile = tempfile.NamedTemporaryFile() try: @@ -68,10 +67,6 @@ def main(): prev_vals = [] remaining_vals = [] skipped_lines = 0 - first_invalid_line = 0 - invalid_line = '' - invalid_value = '' - invalid_column = 0 fout = open(outfile, "w") cols = range(1,25) block_valid = False @@ -105,8 +100,6 @@ def main(): out_list[1] = str(prev_vals[0][0]) out_list[2] = str(prev_vals[1][0]) out_list[24] = str(prev_vals[23][0]) - #print >> fout, prev_vals - #sys.exit() for k, col in enumerate(cols): if col >= 3 and col < 24: if len(set(prev_vals[k])) == 1: @@ -116,17 +109,14 @@ def main(): while k < 23: out_list[k+1] = 'n' k += 1 - - # print >>fout, '\t'.join(out_list) if rank_bound == 0: - print >>fout, ''.join(out_list) - print 'n'*( 24 - rank_bound ) + print >>fout, '\t'.join(out_list) + #print 'n'*( 24 - rank_bound ) else: - print '\t'.join(out_list[rank_bound:24]) + #print '\t'.join(out_list[rank_bound:24]) if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): print >>fout, '\t'.join(out_list) - block_valid = True prev_item = item @@ -145,15 +135,11 @@ def main(): val_list.append(fields[col].strip()) prev_vals.append(val_list) - except Exception, exc: + except: skipped_lines += 1 - if not first_invalid_line: - first_invalid_line = ii+1 else: skipped_lines += 1 - if not first_invalid_line: - first_invalid_line = ii+1 - + # Handle the last grouped value out_list = ['']*25 out_list[0] = str(prev_item) @@ -173,14 +159,13 @@ def main(): if rank_bound == 0: print >>fout, '\t'.join(out_list) else: - print ''.join(out_list[rank_bound:24]) - print 'n'*( 24 - rank_bound ) + #print ''.join(out_list[rank_bound:24]) + #print 'n'*( 24 - rank_bound ) if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): print >>fout, '\t'.join(out_list) if skipped_lines > 0: - msg= "Skipped %d invalid lines starting with line %d. Value '%s' in column %d is not numeric." % ( skipped_lines, first_invalid_line, invalid_value, invalid_column ) - print msg + print "Skipped %d invalid lines." % ( skipped_lines ) if __name__ == "__main__": main() \ No newline at end of file From 42adeabbc9630e81ca565c053632d579beadd2e1 Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Wed, 21 Jan 2009 09:51:06 -0500 Subject: [PATCH 172/267] Lca commit. The tool itself is written by guru with minor modification made by me. --- tool_conf.xml.sample | 1 + tools/taxonomy/lca.py | 19 ++++++++++----- tools/taxonomy/lca.xml | 55 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/tool_conf.xml.sample b/tool_conf.xml.sample index f46c9cef558..bb1dcbab138 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -156,6 +156,7 @@ +
    diff --git a/tools/taxonomy/lca.py b/tools/taxonomy/lca.py index ca6a320af24..d70425db366 100644 --- a/tools/taxonomy/lca.py +++ b/tools/taxonomy/lca.py @@ -99,7 +99,10 @@ def main(): out_list[0] = str(prev_item) out_list[1] = str(prev_vals[0][0]) out_list[2] = str(prev_vals[1][0]) - out_list[24] = str(prev_vals[23][0]) + try: + out_list[24] = str(prev_vals[23][0]) + except: + pass for k, col in enumerate(cols): if col >= 3 and col < 24: if len(set(prev_vals[k])) == 1: @@ -111,12 +114,12 @@ def main(): k += 1 if rank_bound == 0: - print >>fout, '\t'.join(out_list) + print >>fout, '\t'.join(out_list).strip() #print 'n'*( 24 - rank_bound ) else: #print '\t'.join(out_list[rank_bound:24]) if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): - print >>fout, '\t'.join(out_list) + print >>fout, '\t'.join(out_list).strip() block_valid = True prev_item = item @@ -145,7 +148,11 @@ def main(): out_list[0] = str(prev_item) out_list[1] = str(prev_vals[0][0]) out_list[2] = str(prev_vals[1][0]) - out_list[24] = str(prev_vals[23][0]) + try: + out_list[24] = str(prev_vals[23][0]) + except: + pass + for k, col in enumerate(cols): if col >= 3 and col < 24: if len(set(prev_vals[k])) == 1: @@ -157,12 +164,12 @@ def main(): k += 1 if rank_bound == 0: - print >>fout, '\t'.join(out_list) + print >>fout, '\t'.join(out_list).strip() else: #print ''.join(out_list[rank_bound:24]) #print 'n'*( 24 - rank_bound ) if ''.join(out_list[rank_bound:24]) != 'n'*( 24 - rank_bound ): - print >>fout, '\t'.join(out_list) + print >>fout, '\t'.join(out_list).strip() if skipped_lines > 0: print "Skipped %d invalid lines." % ( skipped_lines ) diff --git a/tools/taxonomy/lca.xml b/tools/taxonomy/lca.xml index f608a16cfc3..ca5e246d41a 100644 --- a/tools/taxonomy/lca.xml +++ b/tools/taxonomy/lca.xml @@ -1,12 +1,12 @@ - + lca.py $input1 $out_file1 $rank_bound - - - + + + @@ -32,13 +32,54 @@ - + + + + + + + + - + **What it does** -When performing metagenomic analyses it is often necessary to identify sequence reads corresponding to a particular taxonomic group, or, in other words, diagnostic of a particular taxonomic rank. This utility performs this analysis. It takes data generated by *Taxonomy manipulation->Fetch Taxonomic Ranks* as input and outputs either a list of sequence reads unique to a particular taxonomic rank, or a list of taxonomic ranks and the count of unique reads corresponding to each rank. +This tool identifies the lowest taxonomic rank for which a mategenomic sequencing read is diagnostic. It takes datasets produced by *Fetch Taxonomic Ranks* tool (aka Taxonomy format) as the input. + +------- + +**Example** + +Suppose you have two reads, **read_1** and **read_2**, with the following taxonomic profiles (scroll sideways to see the entire dataset):: + + read_1 1 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum1 subphylum1 superclass1 class1 subclass1 superorder1 order1 suborder1 superfamily1 family1 subfamily1 tribe1 subtribe1 genus1 subgenus1 species1 subspecies1 + read_1 2 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum1 subphylum1 superclass1 class1 subclass1 superorder1 order1 suborder1 superfamily1 family1 subfamily1 tribe1 subtribe1 genus2 subgenus2 species2 subspecies2 + read_2 3 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum3 subphylum3 superclass3 class3 subclass3 superorder3 order3 suborder3 superfamily3 family3 subfamily3 tribe3 subtribe3 genus3 subgenus3 species3 subspecies3 + read_2 4 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum4 subphylum4 superclass4 class4 subclass4 superorder4 order4 suborder4 superfamily4 family4 subfamily4 tribe4 subtribe4 genus4 subgenus4 species4 subspecies4 + +For **read_1** taxonomic labels are consistent until the genus level, where the taxonomy splits into two branches, one ending with *subspecies1* and the other with *subspecies2*. This implies **that the lowest taxomomic rank read_1 can identify is SUBTRIBE**. Similarly, read_2 is diagnostic up until the **superphylum** level. As a results the output of this tool will be:: + + read_1 2 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum1 subphylum1 superclass1 class1 subclass1 superorder1 order1 suborder1 superfamily1 family1 subfamily1 tribe1 subtribe1 n n n n + read_2 3 root superkingdom1 kingdom1 subkingdom1 superphylum1 n n n n n n n n n n n n n n n n n + +where, **n** means *EMPTY*. + +-------- + +**What's up with the drop down?** + +Why do we need the *require the lowest rank to be at least* dropdown? Let's look at the above example again. Suppose you need to find only those reads that are diagnostic on at least phylum level. To do this you need to set the *require the lowest rank to be at least* to **phylum**. As a result your output will look like this:: + + read_1 2 root superkingdom1 kingdom1 subkingdom1 superphylum1 phylum1 subphylum1 superclass1 class1 subclass1 superorder1 order1 suborder1 superfamily1 family1 subfamily1 tribe1 subtribe1 n n n n + +.. class:: infomark + +Note, that **read_2** is now omitted as it matches two phyla (**phylum3** and **phylum4**) and therefore is not diagnostic (but rather cosmopolitan) on *phylum* level. + + + + \ No newline at end of file From b8e76a8ca61d7dbbd42fd7927019e47cfdfdf3cf Mon Sep 17 00:00:00 2001 From: Ian Schenck Date: Wed, 21 Jan 2009 11:45:30 -0500 Subject: [PATCH 173/267] Made Genetrack load only if the dependencies import --- datatypes_conf.xml.sample | 34 ++-- .../converters/interval_to_coverage.py | 80 +++++++++ .../converters/interval_to_coverage.xml | 18 ++ lib/galaxy/web/base/controller.py | 5 +- lib/galaxy/web/buildapp.py | 7 +- lib/galaxy/web/controllers/genetrack.py | 160 +++++++++++++++--- scripts/paster.py | 1 - templates/genetrack/base.html | 29 ++++ templates/genetrack/index.html | 75 ++++++++ templates/genetrack/search.html | 55 ++++++ tools/visualization/genetrack.py | 84 +++++++-- tools/visualization/genetrack.xml | 15 +- 12 files changed, 502 insertions(+), 61 deletions(-) create mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.py create mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.xml create mode 100644 templates/genetrack/base.html create mode 100644 templates/genetrack/index.html create mode 100644 templates/genetrack/search.html diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index 76bba775482..eead3e0e436 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -26,6 +26,7 @@ + @@ -38,7 +39,6 @@ - @@ -173,24 +173,20 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.py b/lib/galaxy/datatypes/converters/interval_to_coverage.py new file mode 100644 index 00000000000..9b4bfccfa10 --- /dev/null +++ b/lib/galaxy/datatypes/converters/interval_to_coverage.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +""" +Converter to generate 3 (or 4) column base-pair coverage from an interval file. + +usage: %prog bed_file out_file + -1, --cols1=N,N,N,N: Columns for chrom, start, end, strand in interval file + -2, --cols2=N,N,N,N: Columns for chrom, start, end, strand in coverage file +""" +import sys +from galaxy import eggs +import pkg_resources; pkg_resources.require( "bx-python" ) +from bx.intervals import io +from bx.cookbook import doc_optparse + +INTERVAL_METADATA = ('chromCol', + 'startCol', + 'endCol', + 'strandCol',) + +COVERAGE_METADATA = ('chromCol', + 'positionCol', + 'forwardCol', + 'reverseCol',) + +def main( interval, coverage ): + chroms = dict() + for record in interval: + if not type( record ) is io.GenomicInterval: continue + chrom = chroms[record.chrom] = chroms.get(record.chrom, dict()) + for position in xrange(record.start, record.end): + coverages = chrom[position] = chrom.get(position,[0,0]) + if record.strand == "-": coverages[1] += 1 + else: coverages[0] += 1 + for chrom in sorted(chroms.iterkeys()): + positions = chroms[chrom] + for position in sorted(positions.iterkeys()): + coverage.write( chrom=chrom, position=position, forward=positions[position][0], reverse=positions[position][1] ) + +class CoverageWriter( object ): + def __init__( self, out_stream=None, chromCol=0, positionCol=1, forwardCol=2, reverseCol=3 ): + self.chromCol, self.positionCol, self.forwardCol, self.reverseCol = chromCol, positionCol, forwardCol, reverseCol + self.nfields = max( chromCol, positionCol, forwardCol, reverseCol )+1 + self.out_stream = out_stream + self.nlines = 0 + + def write(self, chrom="chr", position=0, forward=0, reverse=0 ): + self.nlines += 1 + if self.nlines % 64000: self.out_stream.flush() + outlist = [None] * self.nfields + outlist[self.chromCol] = str(chrom) + outlist[self.positionCol] = str(position) + if self.reverseCol == -1: outlist[self.forwardCol] = str(forward + reverse) + else: + outlist[self.forwardCol] = str(forward) + outlist[self.reverseCol] = str(reverse) + self.out_stream.write("%s\n" % "\t".join( outlist )) + + def flush(self): + self.out_stream.flush() + +if __name__ == "__main__": + options, args = doc_optparse.parse( __doc__ ) + try: + chr_col_1, start_col_1, end_col_1, strand_col_1 = [int(x)-1 for x in options.cols1.split(',')] + chr_col_2, position_col_2, forward_col_2, reverse_col_2 = [int(x)-1 for x in options.cols2.split(',')] + in_fname, out_fname = args + except: + doc_optparse.exception() + + coverage = CoverageWriter( out_stream = open(out_fname, "a"), + chromCol = chr_col_2, positionCol = position_col_2, + forwardCol = forward_col_2, reverseCol = reverse_col_2, ) + interval = io.NiceReaderWrapper( open(in_fname, "r"), + chrom_col=chr_col_1, + start_col=start_col_1, + end_col=end_col_1, + strand_col=strand_col_1, + fix_strand=True ) + main( interval, coverage ) + coverage.flush() \ No newline at end of file diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.xml b/lib/galaxy/datatypes/converters/interval_to_coverage.xml new file mode 100644 index 00000000000..3ad64f7b9ba --- /dev/null +++ b/lib/galaxy/datatypes/converters/interval_to_coverage.xml @@ -0,0 +1,18 @@ + + + + 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/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 797a4aca263..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 diff --git a/lib/galaxy/web/controllers/genetrack.py b/lib/galaxy/web/controllers/genetrack.py index ae81a277cd4..f4b13e2b09c 100644 --- a/lib/galaxy/web/controllers/genetrack.py +++ b/lib/galaxy/web/controllers/genetrack.py @@ -1,22 +1,127 @@ import time, glob, os +from itertools import cycle -import pkg_resources -pkg_resources.require("GeneTrack") - -import atlas -from atlas import sql -from atlas import util as atlas_utils -from atlas.web import formlib 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 +color = cycle( [LIGHT, WHITE] ) + +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={}): + # draw the ORF tracks + all = feature_filter(feature_query(session=session, param=param), name=label, kdict=label_dict) + if len(all) == 0: return [] + 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 -from mod454.trackbuilder import build_tracks 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') @@ -96,6 +201,7 @@ class WebRoot(BaseController): """ 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 ) ) @@ -108,10 +214,15 @@ class WebRoot(BaseController): FIT_LABEL = "%s-SIGMA-%d" % (data.metadata.label, 20), PRED_LABEL = "PRED-%s-SIGMA-%d" % (data.metadata.label, 20), ) - from atlas import hdf - 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() + 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 ) @@ -147,14 +258,25 @@ class WebRoot(BaseController): # get the template and the function used to generate the tracks tmpl_name, track_maker = conf.PLOT_MAPPER[param.plot] - if track_maker is not None: - # generate the name that the image will be stored at - fname, fpath = atlas_utils.make_tempfile( dir=conf.IMAGE_DIR, suffix='.png') - param.fname = fname + charts = [] - # generate the track - track_chart = track_maker( param=param, conf=conf ) - track_chart.save(fname=fpath) + fname, fpath = atlas_utils.make_tempfile( dir=conf.IMAGE_DIR, suffix='.png') + param.fname = fname + + # set the scale of the plot + param.xscale = [ param.start, param.end ] + + # when visualizing on wide scales labels are not useful + param.show_labels = ( param.end - param.start ) <= SHOW_LABEL_LIMIT + + 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}) ) + 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/scripts/paster.py b/scripts/paster.py index 446cb9de90b..5b6c9262493 100755 --- a/scripts/paster.py +++ b/scripts/paster.py @@ -12,7 +12,6 @@ assert sys.version_info[:2] >= ( 2, 4 ) new_path = [ os.path.join( os.getcwd(), "lib" ) ] new_path.extend( sys.path[1:] ) # remove scripts/ from the path sys.path = new_path -print sys.path from galaxy import eggs import pkg_resources 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/tools/visualization/genetrack.py b/tools/visualization/genetrack.py index bd27d5df4d2..14c290f05d2 100644 --- a/tools/visualization/genetrack.py +++ b/tools/visualization/genetrack.py @@ -14,15 +14,19 @@ import pkg_resources pkg_resources.require("GeneTrack") pkg_resources.require("bx-python") -from atlas import commands -from bx.cookbook import doc_optparse -import os import commands as oscommands +from atlas import commands +from atlas import sql +from bx.cookbook import doc_optparse +from bx.intervals import io + +import os import tempfile +from functools import partial SIGMA = 20 WIDTH = 5 * SIGMA -EXCLUSION_ZONE = 147 +EXCLUSION_ZONE = 147 def main(label, fit, feats, data_dir, output): os.mkdir(data_dir) @@ -31,14 +35,14 @@ def main(label, fit, feats, data_dir, output): CLOBBER = True, DATA_SIZE = 3*10**6, MINIMUM_PEAK_SIZE = 0.1, - LOADER_ENABLED = True, - FITTER_ENABLED = True, - PREDICTOR_ENABLED = True, - EXPORTER_ENABLED = True, + LOADER_ENABLED = False, + FITTER_ENABLED = False, + PREDICTOR_ENABLED = False, + EXPORTER_ENABLED = False, LOADER = loader, FITTER = fitter, PREDICTOR = predictor, - EXPORTER = exporter, + EXPORTER = partial( commands.exporter, formatter=commands.bed_formatter), HDF_DATABASE = os.path.join( data_dir, "data.hdf" ), SQL_URI = "sqlite:///%s" % os.path.join( data_dir, "features.sqlite" ), SIGMA = SIGMA, @@ -51,12 +55,23 @@ def main(label, fit, feats, data_dir, output): RIGHT_SHIFT = EXCLUSION_ZONE / 2, EXPORT_LABELS = [ "PRED-%s-SIGMA-%d" % ( label,SIGMA ) ], EXPORT_DIR = os.path.join( data_dir ), - DATA_FILE=fit[1], + DATA_FILE=fit and fit[1] or None, fit=fit, feats=feats, ) + if fit: + # Turn on fit processing. + conf.LOADER_ENABLED = True, + conf.FITTER_ENABLED = True, + conf.PREDICTOR_ENABLED = True, + conf.EXPORTER_ENABLED = True, + for feat in feats: + load_feature_files(conf, feats) commands.execute(conf) - + outname = "%s.%s.txt" % (conf.__name__, conf.EXPORT_LABELS[0] ) + if os.path.exists( os.path.join(data_dir, outname) ): + os.rename( os.path.join(data_dir, outname), output) + # mod454 seems to be a module without a package. The necessary funcitons are # stubbed out here until I'm sure of their final home. INS @@ -97,8 +112,40 @@ def predictor( conf ): from mod454.predictor import predictor as mod454_predictor return mod454_predictor( conf ) -def exporter( conf ): - return commands.bed_exporter(conf) +def load_feature_files( conf, feats): + """ + Loads features from file names + """ + engine = sql.get_engine( conf.SQL_URI ) + sql.drop_indices(engine) + conn = engine.connect() + for label, fname, col_spec in feats: + label_id = sql.make_label(engine, name=label, clobber=False) + reader = io.NiceReaderWrapper( open(fname,"r"), + chrom_col=col_spec.chromCol, + start_col=col_spec.startCol, + end_col=col_spec.endCol, + strand_col=col_spec.strandCol, + fix_strand=False ) + values = list() + for interval in reader: + print interval + if not type( interval ) is io.GenomicInterval: continue + row = {'label_id':label_id, + 'name':col_spec.nameCol == -1 and "%s-%s" % (str(interval.start), str(interval.end)) or interval.fields[col_spec.nameCol], + 'altname':"", + 'chrom':interval.chrom, + 'start':interval.start, + 'end':interval.end, + 'strand':interval.strand, + 'value':0, + 'freetext':""} + values.append(row) + insert = sql.feature_table.insert() + conn.execute( insert, values) + conn.close() + sql.create_indices(engine) + class Bunch( object ): def __init__(self, **kwargs): @@ -115,9 +162,12 @@ if __name__ == "__main__": options, args = doc_optparse.parse( __doc__ ) try: label = options.label - fit_name, fit_meta = options.fits.split(':')[0], [int(x)-1 for x in options.fits.split(':')[1:]] - fit_meta = Bunch(chromCol=fit_meta[0], positionCol=fit_meta[1], forwardCol=fit_meta[2], reverseCol=fit_meta[3]) - fit = ( label, fit_name, fit_meta, ) + if options.fits: + fit_name, fit_meta = options.fits.split(':')[0], [int(x)-1 for x in options.fits.split(':')[1:]] + fit_meta = Bunch(chromCol=fit_meta[0], positionCol=fit_meta[1], forwardCol=fit_meta[2], reverseCol=fit_meta[3]) + fit = ( label, fit_name, fit_meta, ) + else: + fit = [] # split apart the string into nested lists, preserves order if options.feats: feats = [ ( @@ -127,7 +177,7 @@ if __name__ == "__main__": strandCol=int(strandCol)-1, nameCol=int(nameCol)-1), ) for feat_label, fname, chromCol, startCol, endCol, strandCol, nameCol - in ( feat.split(':') for feat in options.feats.split(',') )] + in ( feat.split(':') for feat in options.feats.split(',') if len(feat) > 0 )] else: feats = [] data_dir = options.data diff --git a/tools/visualization/genetrack.xml b/tools/visualization/genetrack.xml index 65c5f519b31..8ab46560391 100644 --- a/tools/visualization/genetrack.xml +++ b/tools/visualization/genetrack.xml @@ -8,7 +8,10 @@ genetrack.py -l $data_label - -1 ${fit_data}:${fit_data.metadata.chromCol}:${fit_data.metadata.positionCol}:${fit_data.metadata.forwardCol}:${fit_data.metadata.reverseCol} + #if not str($fit_data) == "None" + -1 + ${fit_data}:${fit_data.metadata.chromCol}:${fit_data.metadata.positionCol}:${fit_data.metadata.forwardCol}:${fit_data.metadata.reverseCol} + #end if #if $feature_data -2 #end if @@ -23,7 +26,7 @@ [a-zA-Z0-9]{0,25} - + @@ -36,7 +39,13 @@ - + + + tables + atlas + pychartdir + numpy + This tool takes the input Fit Data and creates a peak and curve plot showing the reads and fitness on each basepair. Features can be plotted below as tracks. From 427bc02028301528dea46670a61a7557d8e12dd1 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Wed, 21 Jan 2009 16:03:59 -0500 Subject: [PATCH 174/267] Backed out changeset 83a0c394a797 --- datatypes_conf.xml.sample | 36 +-- .../converters/interval_to_coverage.py | 80 ----- .../converters/interval_to_coverage.xml | 18 -- lib/galaxy/datatypes/coverage.py | 30 -- lib/galaxy/datatypes/registry.py | 6 +- lib/galaxy/datatypes/tracks.py | 30 -- lib/galaxy/web/base/controller.py | 5 +- lib/galaxy/web/buildapp.py | 7 +- lib/galaxy/web/controllers/genetrack.py | 283 ------------------ scripts/paster.py | 1 + static/genetrack/genetrack.css | 78 ----- static/genetrack/genetrack.js | 79 ----- templates/genetrack/base.html | 29 -- templates/genetrack/index.html | 75 ----- templates/genetrack/search.html | 55 ---- tool_conf.xml.sample | 4 +- tools/sr_mapping/lastz_wrapper.xml | 2 +- tools/visualization/genetrack.py | 189 ------------ tools/visualization/genetrack.xml | 62 ---- tools/visualization/genetrack_code.py | 13 - 20 files changed, 26 insertions(+), 1056 deletions(-) delete mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.py delete mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.xml delete mode 100644 lib/galaxy/datatypes/coverage.py delete mode 100644 lib/galaxy/datatypes/tracks.py delete mode 100644 lib/galaxy/web/controllers/genetrack.py delete mode 100644 static/genetrack/genetrack.css delete mode 100644 static/genetrack/genetrack.js delete mode 100644 templates/genetrack/base.html delete mode 100644 templates/genetrack/index.html delete mode 100644 templates/genetrack/search.html delete mode 100644 tools/visualization/genetrack.py delete mode 100644 tools/visualization/genetrack.xml delete mode 100644 tools/visualization/genetrack_code.py diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index eead3e0e436..58a8929f7a4 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -26,7 +26,6 @@ - @@ -39,6 +38,7 @@ + @@ -83,8 +83,6 @@ - - @@ -173,20 +171,24 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.py b/lib/galaxy/datatypes/converters/interval_to_coverage.py deleted file mode 100644 index 9b4bfccfa10..00000000000 --- a/lib/galaxy/datatypes/converters/interval_to_coverage.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -""" -Converter to generate 3 (or 4) column base-pair coverage from an interval file. - -usage: %prog bed_file out_file - -1, --cols1=N,N,N,N: Columns for chrom, start, end, strand in interval file - -2, --cols2=N,N,N,N: Columns for chrom, start, end, strand in coverage file -""" -import sys -from galaxy import eggs -import pkg_resources; pkg_resources.require( "bx-python" ) -from bx.intervals import io -from bx.cookbook import doc_optparse - -INTERVAL_METADATA = ('chromCol', - 'startCol', - 'endCol', - 'strandCol',) - -COVERAGE_METADATA = ('chromCol', - 'positionCol', - 'forwardCol', - 'reverseCol',) - -def main( interval, coverage ): - chroms = dict() - for record in interval: - if not type( record ) is io.GenomicInterval: continue - chrom = chroms[record.chrom] = chroms.get(record.chrom, dict()) - for position in xrange(record.start, record.end): - coverages = chrom[position] = chrom.get(position,[0,0]) - if record.strand == "-": coverages[1] += 1 - else: coverages[0] += 1 - for chrom in sorted(chroms.iterkeys()): - positions = chroms[chrom] - for position in sorted(positions.iterkeys()): - coverage.write( chrom=chrom, position=position, forward=positions[position][0], reverse=positions[position][1] ) - -class CoverageWriter( object ): - def __init__( self, out_stream=None, chromCol=0, positionCol=1, forwardCol=2, reverseCol=3 ): - self.chromCol, self.positionCol, self.forwardCol, self.reverseCol = chromCol, positionCol, forwardCol, reverseCol - self.nfields = max( chromCol, positionCol, forwardCol, reverseCol )+1 - self.out_stream = out_stream - self.nlines = 0 - - def write(self, chrom="chr", position=0, forward=0, reverse=0 ): - self.nlines += 1 - if self.nlines % 64000: self.out_stream.flush() - outlist = [None] * self.nfields - outlist[self.chromCol] = str(chrom) - outlist[self.positionCol] = str(position) - if self.reverseCol == -1: outlist[self.forwardCol] = str(forward + reverse) - else: - outlist[self.forwardCol] = str(forward) - outlist[self.reverseCol] = str(reverse) - self.out_stream.write("%s\n" % "\t".join( outlist )) - - def flush(self): - self.out_stream.flush() - -if __name__ == "__main__": - options, args = doc_optparse.parse( __doc__ ) - try: - chr_col_1, start_col_1, end_col_1, strand_col_1 = [int(x)-1 for x in options.cols1.split(',')] - chr_col_2, position_col_2, forward_col_2, reverse_col_2 = [int(x)-1 for x in options.cols2.split(',')] - in_fname, out_fname = args - except: - doc_optparse.exception() - - coverage = CoverageWriter( out_stream = open(out_fname, "a"), - chromCol = chr_col_2, positionCol = position_col_2, - forwardCol = forward_col_2, reverseCol = reverse_col_2, ) - interval = io.NiceReaderWrapper( open(in_fname, "r"), - chrom_col=chr_col_1, - start_col=start_col_1, - end_col=end_col_1, - strand_col=strand_col_1, - fix_strand=True ) - main( interval, coverage ) - coverage.flush() \ No newline at end of file diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.xml b/lib/galaxy/datatypes/converters/interval_to_coverage.xml deleted file mode 100644 index 3ad64f7b9ba..00000000000 --- a/lib/galaxy/datatypes/converters/interval_to_coverage.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - 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 deleted file mode 100644 index 4bd76425a71..00000000000 --- a/lib/galaxy/datatypes/coverage.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -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 ) - \ No newline at end of file diff --git a/lib/galaxy/datatypes/registry.py b/lib/galaxy/datatypes/registry.py index 816851d7437..b7cd9b58b7c 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, coverage, tracks +import data, tabular, interval, images, sequence, qualityscore, genetics, xml import galaxy.util from galaxy.util.odict import odict @@ -94,14 +94,12 @@ 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(), - 'genetrack' : tracks.GeneTrack(), + 'gff3' : interval.Gff3(), 'interval' : interval.Interval(), 'laj' : images.Laj(), 'lav' : sequence.Lav(), diff --git a/lib/galaxy/datatypes/tracks.py b/lib/galaxy/datatypes/tracks.py deleted file mode 100644 index 1c5a9291bef..00000000000 --- a/lib/galaxy/datatypes/tracks.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -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/web/base/controller.py b/lib/galaxy/web/base/controller.py index f2e95dfbcbd..534124e2131 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -28,7 +28,4 @@ class BaseController( object ): Root = BaseController """ Deprecated: `BaseController` used to be available under the name `Root` -""" - -class ControllerUnavailable( Exception ): - pass \ No newline at end of file +""" \ No newline at end of file diff --git a/lib/galaxy/web/buildapp.py b/lib/galaxy/web/buildapp.py index 65a5d315ea8..797a4aca263 100644 --- a/lib/galaxy/web/buildapp.py +++ b/lib/galaxy/web/buildapp.py @@ -28,18 +28,13 @@ 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 - try: - module = __import__( module_name ) - except ControllerUnavailable, exc: - log.debug("%s could not be loaded: %s" % (module_name, str(exc))) - continue + module = __import__( module_name ) for comp in module_name.split( "." )[1:]: module = getattr( module, comp ) # Look for a controller inside the modules diff --git a/lib/galaxy/web/controllers/genetrack.py b/lib/galaxy/web/controllers/genetrack.py deleted file mode 100644 index f4b13e2b09c..00000000000 --- a/lib/galaxy/web/controllers/genetrack.py +++ /dev/null @@ -1,283 +0,0 @@ -import time, glob, os -from itertools import cycle - -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 -color = cycle( [LIGHT, WHITE] ) - -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={}): - # draw the ORF tracks - all = feature_filter(feature_query(session=session, param=param), name=label, kdict=label_dict) - if len(all) == 0: return [] - 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), - ) - from atlas import hdf - 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() - - param = atlas.Param( word=word ) - # search with features based on param.feature - - # 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 ) ) - - # keep image at a sane size - param.width = min( [2000, int(param.img_size)] ) - - # get the template and the function used to generate the tracks - tmpl_name, track_maker = conf.PLOT_MAPPER[param.plot] - - charts = [] - - fname, fpath = atlas_utils.make_tempfile( dir=conf.IMAGE_DIR, suffix='.png') - param.fname = fname - - # set the scale of the plot - param.xscale = [ param.start, param.end ] - - # when visualizing on wide scales labels are not useful - param.show_labels = ( param.end - param.start ) <= SHOW_LABEL_LIMIT - - 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}) ) - 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/scripts/paster.py b/scripts/paster.py index 5b6c9262493..7624257a665 100755 --- a/scripts/paster.py +++ b/scripts/paster.py @@ -12,6 +12,7 @@ assert sys.version_info[:2] >= ( 2, 4 ) new_path = [ os.path.join( os.getcwd(), "lib" ) ] new_path.extend( sys.path[1:] ) # remove scripts/ from the path sys.path = new_path + from galaxy import eggs import pkg_resources diff --git a/static/genetrack/genetrack.css b/static/genetrack/genetrack.css deleted file mode 100644 index 668300be06d..00000000000 --- a/static/genetrack/genetrack.css +++ /dev/null @@ -1,78 +0,0 @@ - -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 deleted file mode 100644 index be88d107ab8..00000000000 --- a/static/genetrack/genetrack.js +++ /dev/null @@ -1,79 +0,0 @@ -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/templates/genetrack/base.html b/templates/genetrack/base.html deleted file mode 100644 index cd381d54b93..00000000000 --- a/templates/genetrack/base.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - -${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 deleted file mode 100644 index 23dee5cea67..00000000000 --- a/templates/genetrack/index.html +++ /dev/null @@ -1,75 +0,0 @@ -## 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 deleted file mode 100644 index d707a6da479..00000000000 --- a/templates/genetrack/search.html +++ /dev/null @@ -1,55 +0,0 @@ -## 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/tool_conf.xml.sample b/tool_conf.xml.sample index e05bdfdfe6a..bb1dcbab138 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -11,6 +11,7 @@ + @@ -302,7 +303,4 @@
    -
    - -
    diff --git a/tools/sr_mapping/lastz_wrapper.xml b/tools/sr_mapping/lastz_wrapper.xml index f29d5fb7168..e3aabf57243 100644 --- a/tools/sr_mapping/lastz_wrapper.xml +++ b/tools/sr_mapping/lastz_wrapper.xml @@ -82,7 +82,7 @@ - + lastz diff --git a/tools/visualization/genetrack.py b/tools/visualization/genetrack.py deleted file mode 100644 index 14c290f05d2..00000000000 --- a/tools/visualization/genetrack.py +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env python -""" -Run GeneTrack(atlas) with a faked conf file to generate GeneTrack data files. - -usage: %prog - -l, --label=N: Data label for fit curve/peak plot - -1, --fits=N/N/N/N/N,...: Data files (interval format) for fit curve/peak plot - -2, --feats=N:M/N/N/N/N/N,...: Data files (interval format) for features. - -d, --data=N: Output path for hdf5 and sqlite databases. - -o, --output=N: Output path for export file. -""" -from galaxy import eggs -import pkg_resources -pkg_resources.require("GeneTrack") -pkg_resources.require("bx-python") - -import commands as oscommands -from atlas import commands -from atlas import sql -from bx.cookbook import doc_optparse -from bx.intervals import io - -import os -import tempfile -from functools import partial - -SIGMA = 20 -WIDTH = 5 * SIGMA -EXCLUSION_ZONE = 147 - -def main(label, fit, feats, data_dir, output): - os.mkdir(data_dir) - conf = DummyConf( - __name__=label, - CLOBBER = True, - DATA_SIZE = 3*10**6, - MINIMUM_PEAK_SIZE = 0.1, - LOADER_ENABLED = False, - FITTER_ENABLED = False, - PREDICTOR_ENABLED = False, - EXPORTER_ENABLED = False, - LOADER = loader, - FITTER = fitter, - PREDICTOR = predictor, - EXPORTER = partial( commands.exporter, formatter=commands.bed_formatter), - HDF_DATABASE = os.path.join( data_dir, "data.hdf" ), - SQL_URI = "sqlite:///%s" % os.path.join( data_dir, "features.sqlite" ), - SIGMA = SIGMA, - WIDTH = WIDTH, - DATA_LABEL = label, - FIT_LABEL = "%s-SIGMA-%d" % ( label,SIGMA ), - PEAK_LABEL = "PRED-%s-SIGMA-%d" % ( label,SIGMA ), - EXCLUSION_ZONE = EXCLUSION_ZONE, - LEFT_SHIFT = EXCLUSION_ZONE / 2, - RIGHT_SHIFT = EXCLUSION_ZONE / 2, - EXPORT_LABELS = [ "PRED-%s-SIGMA-%d" % ( label,SIGMA ) ], - EXPORT_DIR = os.path.join( data_dir ), - DATA_FILE=fit and fit[1] or None, - fit=fit, - feats=feats, - ) - if fit: - # Turn on fit processing. - conf.LOADER_ENABLED = True, - conf.FITTER_ENABLED = True, - conf.PREDICTOR_ENABLED = True, - conf.EXPORTER_ENABLED = True, - for feat in feats: - load_feature_files(conf, feats) - commands.execute(conf) - outname = "%s.%s.txt" % (conf.__name__, conf.EXPORT_LABELS[0] ) - if os.path.exists( os.path.join(data_dir, outname) ): - os.rename( os.path.join(data_dir, outname), output) - -# mod454 seems to be a module without a package. The necessary funcitons are -# stubbed out here until I'm sure of their final home. INS - -def loader( conf ): - from atlas import hdf - from mod454.schema import Mod454Schema as Schema - last_chrom = table = None - db = hdf.hdf_open( conf.HDF_DATABASE, mode='a', title='HDF database') - gp = hdf.create_group( db=db, name=conf.DATA_LABEL, desc='data group', clobber=conf.CLOBBER ) - fit_meta = conf.fit[2] - # iterate over the file and insert into table - for line in open( conf.fit[1], "r" ): - if line.startswith("chrom"): continue #Skip possible header - if line.startswith("#"): continue - fields = line.rstrip('\r\n').split('\t') - chrom = fields[fit_meta.chromCol] - if chrom != last_chrom: - if table: table.flush() - table = hdf.create_table( db=db, name=chrom, where=gp, schema=Schema, clobber=False ) - last_chrom = chrom - try: - position = int(fields[fit_meta.positionCol]) - forward = float(fields[fit_meta.forwardCol]) - reverse = fit_meta.reverseCol > -1 and float(fields[fit_meta.reverseCol]) or 0.0 - row = ( position, forward, reverse, forward+reverse, ) - table.append( [ row ] ) - except ValueError: - # Ignore bad lines - pass - table.flush() - db.close() - -def fitter( conf ): - from mod454.fitter import fitter as mod454_fitter - return mod454_fitter( conf ) - -def predictor( conf ): - from mod454.predictor import predictor as mod454_predictor - return mod454_predictor( conf ) - -def load_feature_files( conf, feats): - """ - Loads features from file names - """ - engine = sql.get_engine( conf.SQL_URI ) - sql.drop_indices(engine) - conn = engine.connect() - for label, fname, col_spec in feats: - label_id = sql.make_label(engine, name=label, clobber=False) - reader = io.NiceReaderWrapper( open(fname,"r"), - chrom_col=col_spec.chromCol, - start_col=col_spec.startCol, - end_col=col_spec.endCol, - strand_col=col_spec.strandCol, - fix_strand=False ) - values = list() - for interval in reader: - print interval - if not type( interval ) is io.GenomicInterval: continue - row = {'label_id':label_id, - 'name':col_spec.nameCol == -1 and "%s-%s" % (str(interval.start), str(interval.end)) or interval.fields[col_spec.nameCol], - 'altname':"", - 'chrom':interval.chrom, - 'start':interval.start, - 'end':interval.end, - 'strand':interval.strand, - 'value':0, - 'freetext':""} - values.append(row) - insert = sql.feature_table.insert() - conn.execute( insert, values) - conn.close() - sql.create_indices(engine) - - -class Bunch( object ): - def __init__(self, **kwargs): - for key,value in kwargs.items(): - setattr( self, key, value ) - -class DummyConf( Bunch ): - """ - Fake conf module for genetrack/atlas. - """ - pass - -if __name__ == "__main__": - options, args = doc_optparse.parse( __doc__ ) - try: - label = options.label - if options.fits: - fit_name, fit_meta = options.fits.split(':')[0], [int(x)-1 for x in options.fits.split(':')[1:]] - fit_meta = Bunch(chromCol=fit_meta[0], positionCol=fit_meta[1], forwardCol=fit_meta[2], reverseCol=fit_meta[3]) - fit = ( label, fit_name, fit_meta, ) - else: - fit = [] - # split apart the string into nested lists, preserves order - if options.feats: - feats = [ ( - feat_label, - fname, - Bunch(chromCol=int(chromCol)-1, startCol=int(startCol)-1, endCol=int(endCol)-1, - strandCol=int(strandCol)-1, nameCol=int(nameCol)-1), - ) - for feat_label, fname, chromCol, startCol, endCol, strandCol, nameCol - in ( feat.split(':') for feat in options.feats.split(',') if len(feat) > 0 )] - else: - feats = [] - data_dir = options.data - output = options.output - except: - doc_optparse.exception() - - main(label, fit, feats, data_dir, output) - \ No newline at end of file diff --git a/tools/visualization/genetrack.xml b/tools/visualization/genetrack.xml deleted file mode 100644 index 8ab46560391..00000000000 --- a/tools/visualization/genetrack.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - Track creator/viewer - - - - - - - genetrack.py -l $data_label - #if not str($fit_data) == "None" - -1 - ${fit_data}:${fit_data.metadata.chromCol}:${fit_data.metadata.positionCol}:${fit_data.metadata.forwardCol}:${fit_data.metadata.reverseCol} - #end if - #if $feature_data - -2 - #end if - #for $data in $feature_data - ${data.name}:${data.input}:${data.input.metadata.chromCol}:${data.input.metadata.startCol}:${data.input.metadata.endCol}:${data.input.metadata.strandCol}:${data.input.metadata.nameCol}, - #end for - -d ${genetrack.files_path} - -o ${bed_out} - - - - - [a-zA-Z0-9]{0,25} - - - - - - [a-zA-Z0-9]{0,25} - - - - - - - - - - - tables - atlas - pychartdir - numpy - - -This tool takes the input Fit Data and creates a peak and curve plot showing -the reads and fitness on each basepair. Features can be plotted below as tracks. - ------ - -**Syntax** - -- **Track Label** is the name of the generated track. -- **Fit Data** are the datasets to calculate coverage/reads across basepairs and generate a curve. -- **Features** are additional datasets (interval format) to be plotted below as tracks. - - - diff --git a/tools/visualization/genetrack_code.py b/tools/visualization/genetrack_code.py deleted file mode 100644 index 9c20ec27b73..00000000000 --- a/tools/visualization/genetrack_code.py +++ /dev/null @@ -1,13 +0,0 @@ -import sets, os -from galaxy import eggs -from galaxy import jobs -from galaxy.tools.parameters import DataToolParameter - -def exec_after_process(app, inp_data, out_data, param_dict, tool=None, stdout=None, stderr=None): - """ - Copy data_label to genetrack.metadata.label - """ - out_data['genetrack'].metadata.label = param_dict['data_label'] - out_data['genetrack'].info = "Use the link below to view the custom track." - out_data['bed_out'].info = "" - \ No newline at end of file From 82d09f5824c43256114adebbc3f661ab8e3e9dce Mon Sep 17 00:00:00 2001 From: Ian Schenck Date: Wed, 21 Jan 2009 16:48:54 -0500 Subject: [PATCH 175/267] Finally remerged changeset. Should be good now. --- datatypes_conf.xml.sample | 3 + .../converters/interval_to_coverage.py | 80 +++++ .../converters/interval_to_coverage.xml | 18 ++ lib/galaxy/datatypes/coverage.py | 30 ++ lib/galaxy/datatypes/registry.py | 6 +- lib/galaxy/datatypes/tracks.py | 30 ++ lib/galaxy/web/base/controller.py | 5 +- lib/galaxy/web/buildapp.py | 7 +- lib/galaxy/web/controllers/genetrack.py | 283 ++++++++++++++++++ static/genetrack/genetrack.css | 78 +++++ static/genetrack/genetrack.js | 79 +++++ templates/genetrack/base.html | 29 ++ templates/genetrack/index.html | 75 +++++ templates/genetrack/search.html | 55 ++++ tool_conf.xml.sample | 3 + tools/sr_mapping/lastz_wrapper.xml | 2 +- tools/visualization/genetrack.py | 189 ++++++++++++ tools/visualization/genetrack.xml | 62 ++++ tools/visualization/genetrack_code.py | 13 + 19 files changed, 1042 insertions(+), 5 deletions(-) create mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.py create mode 100644 lib/galaxy/datatypes/converters/interval_to_coverage.xml create mode 100644 lib/galaxy/datatypes/coverage.py create mode 100644 lib/galaxy/datatypes/tracks.py create mode 100644 lib/galaxy/web/controllers/genetrack.py create mode 100644 static/genetrack/genetrack.css create mode 100644 static/genetrack/genetrack.js create mode 100644 templates/genetrack/base.html create mode 100644 templates/genetrack/index.html create mode 100644 templates/genetrack/search.html create mode 100644 tools/visualization/genetrack.py create mode 100644 tools/visualization/genetrack.xml create mode 100644 tools/visualization/genetrack_code.py diff --git a/datatypes_conf.xml.sample b/datatypes_conf.xml.sample index 58a8929f7a4..c7e15a4bde7 100644 --- a/datatypes_conf.xml.sample +++ b/datatypes_conf.xml.sample @@ -5,8 +5,10 @@ + + @@ -17,6 +19,7 @@ + diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.py b/lib/galaxy/datatypes/converters/interval_to_coverage.py new file mode 100644 index 00000000000..9b4bfccfa10 --- /dev/null +++ b/lib/galaxy/datatypes/converters/interval_to_coverage.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +""" +Converter to generate 3 (or 4) column base-pair coverage from an interval file. + +usage: %prog bed_file out_file + -1, --cols1=N,N,N,N: Columns for chrom, start, end, strand in interval file + -2, --cols2=N,N,N,N: Columns for chrom, start, end, strand in coverage file +""" +import sys +from galaxy import eggs +import pkg_resources; pkg_resources.require( "bx-python" ) +from bx.intervals import io +from bx.cookbook import doc_optparse + +INTERVAL_METADATA = ('chromCol', + 'startCol', + 'endCol', + 'strandCol',) + +COVERAGE_METADATA = ('chromCol', + 'positionCol', + 'forwardCol', + 'reverseCol',) + +def main( interval, coverage ): + chroms = dict() + for record in interval: + if not type( record ) is io.GenomicInterval: continue + chrom = chroms[record.chrom] = chroms.get(record.chrom, dict()) + for position in xrange(record.start, record.end): + coverages = chrom[position] = chrom.get(position,[0,0]) + if record.strand == "-": coverages[1] += 1 + else: coverages[0] += 1 + for chrom in sorted(chroms.iterkeys()): + positions = chroms[chrom] + for position in sorted(positions.iterkeys()): + coverage.write( chrom=chrom, position=position, forward=positions[position][0], reverse=positions[position][1] ) + +class CoverageWriter( object ): + def __init__( self, out_stream=None, chromCol=0, positionCol=1, forwardCol=2, reverseCol=3 ): + self.chromCol, self.positionCol, self.forwardCol, self.reverseCol = chromCol, positionCol, forwardCol, reverseCol + self.nfields = max( chromCol, positionCol, forwardCol, reverseCol )+1 + self.out_stream = out_stream + self.nlines = 0 + + def write(self, chrom="chr", position=0, forward=0, reverse=0 ): + self.nlines += 1 + if self.nlines % 64000: self.out_stream.flush() + outlist = [None] * self.nfields + outlist[self.chromCol] = str(chrom) + outlist[self.positionCol] = str(position) + if self.reverseCol == -1: outlist[self.forwardCol] = str(forward + reverse) + else: + outlist[self.forwardCol] = str(forward) + outlist[self.reverseCol] = str(reverse) + self.out_stream.write("%s\n" % "\t".join( outlist )) + + def flush(self): + self.out_stream.flush() + +if __name__ == "__main__": + options, args = doc_optparse.parse( __doc__ ) + try: + chr_col_1, start_col_1, end_col_1, strand_col_1 = [int(x)-1 for x in options.cols1.split(',')] + chr_col_2, position_col_2, forward_col_2, reverse_col_2 = [int(x)-1 for x in options.cols2.split(',')] + in_fname, out_fname = args + except: + doc_optparse.exception() + + coverage = CoverageWriter( out_stream = open(out_fname, "a"), + chromCol = chr_col_2, positionCol = position_col_2, + forwardCol = forward_col_2, reverseCol = reverse_col_2, ) + interval = io.NiceReaderWrapper( open(in_fname, "r"), + chrom_col=chr_col_1, + start_col=start_col_1, + end_col=end_col_1, + strand_col=strand_col_1, + fix_strand=True ) + main( interval, coverage ) + coverage.flush() \ No newline at end of file diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.xml b/lib/galaxy/datatypes/converters/interval_to_coverage.xml new file mode 100644 index 00000000000..3ad64f7b9ba --- /dev/null +++ b/lib/galaxy/datatypes/converters/interval_to_coverage.xml @@ -0,0 +1,18 @@ + + + + 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..4bd76425a71 --- /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 ) + \ No newline at end of file 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/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/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 797a4aca263..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 diff --git a/lib/galaxy/web/controllers/genetrack.py b/lib/galaxy/web/controllers/genetrack.py new file mode 100644 index 00000000000..f4b13e2b09c --- /dev/null +++ b/lib/galaxy/web/controllers/genetrack.py @@ -0,0 +1,283 @@ +import time, glob, os +from itertools import cycle + +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 +color = cycle( [LIGHT, WHITE] ) + +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={}): + # draw the ORF tracks + all = feature_filter(feature_query(session=session, param=param), name=label, kdict=label_dict) + if len(all) == 0: return [] + 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), + ) + from atlas import hdf + 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() + + param = atlas.Param( word=word ) + # search with features based on param.feature + + # 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 ) ) + + # keep image at a sane size + param.width = min( [2000, int(param.img_size)] ) + + # get the template and the function used to generate the tracks + tmpl_name, track_maker = conf.PLOT_MAPPER[param.plot] + + charts = [] + + fname, fpath = atlas_utils.make_tempfile( dir=conf.IMAGE_DIR, suffix='.png') + param.fname = fname + + # set the scale of the plot + param.xscale = [ param.start, param.end ] + + # when visualizing on wide scales labels are not useful + param.show_labels = ( param.end - param.start ) <= SHOW_LABEL_LIMIT + + 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}) ) + 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/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/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/tool_conf.xml.sample b/tool_conf.xml.sample index bb1dcbab138..b70330e3a0d 100644 --- a/tool_conf.xml.sample +++ b/tool_conf.xml.sample @@ -303,4 +303,7 @@ +
    + +
    diff --git a/tools/sr_mapping/lastz_wrapper.xml b/tools/sr_mapping/lastz_wrapper.xml index e3aabf57243..f29d5fb7168 100644 --- a/tools/sr_mapping/lastz_wrapper.xml +++ b/tools/sr_mapping/lastz_wrapper.xml @@ -82,7 +82,7 @@
    - + lastz diff --git a/tools/visualization/genetrack.py b/tools/visualization/genetrack.py new file mode 100644 index 00000000000..14c290f05d2 --- /dev/null +++ b/tools/visualization/genetrack.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +""" +Run GeneTrack(atlas) with a faked conf file to generate GeneTrack data files. + +usage: %prog + -l, --label=N: Data label for fit curve/peak plot + -1, --fits=N/N/N/N/N,...: Data files (interval format) for fit curve/peak plot + -2, --feats=N:M/N/N/N/N/N,...: Data files (interval format) for features. + -d, --data=N: Output path for hdf5 and sqlite databases. + -o, --output=N: Output path for export file. +""" +from galaxy import eggs +import pkg_resources +pkg_resources.require("GeneTrack") +pkg_resources.require("bx-python") + +import commands as oscommands +from atlas import commands +from atlas import sql +from bx.cookbook import doc_optparse +from bx.intervals import io + +import os +import tempfile +from functools import partial + +SIGMA = 20 +WIDTH = 5 * SIGMA +EXCLUSION_ZONE = 147 + +def main(label, fit, feats, data_dir, output): + os.mkdir(data_dir) + conf = DummyConf( + __name__=label, + CLOBBER = True, + DATA_SIZE = 3*10**6, + MINIMUM_PEAK_SIZE = 0.1, + LOADER_ENABLED = False, + FITTER_ENABLED = False, + PREDICTOR_ENABLED = False, + EXPORTER_ENABLED = False, + LOADER = loader, + FITTER = fitter, + PREDICTOR = predictor, + EXPORTER = partial( commands.exporter, formatter=commands.bed_formatter), + HDF_DATABASE = os.path.join( data_dir, "data.hdf" ), + SQL_URI = "sqlite:///%s" % os.path.join( data_dir, "features.sqlite" ), + SIGMA = SIGMA, + WIDTH = WIDTH, + DATA_LABEL = label, + FIT_LABEL = "%s-SIGMA-%d" % ( label,SIGMA ), + PEAK_LABEL = "PRED-%s-SIGMA-%d" % ( label,SIGMA ), + EXCLUSION_ZONE = EXCLUSION_ZONE, + LEFT_SHIFT = EXCLUSION_ZONE / 2, + RIGHT_SHIFT = EXCLUSION_ZONE / 2, + EXPORT_LABELS = [ "PRED-%s-SIGMA-%d" % ( label,SIGMA ) ], + EXPORT_DIR = os.path.join( data_dir ), + DATA_FILE=fit and fit[1] or None, + fit=fit, + feats=feats, + ) + if fit: + # Turn on fit processing. + conf.LOADER_ENABLED = True, + conf.FITTER_ENABLED = True, + conf.PREDICTOR_ENABLED = True, + conf.EXPORTER_ENABLED = True, + for feat in feats: + load_feature_files(conf, feats) + commands.execute(conf) + outname = "%s.%s.txt" % (conf.__name__, conf.EXPORT_LABELS[0] ) + if os.path.exists( os.path.join(data_dir, outname) ): + os.rename( os.path.join(data_dir, outname), output) + +# mod454 seems to be a module without a package. The necessary funcitons are +# stubbed out here until I'm sure of their final home. INS + +def loader( conf ): + from atlas import hdf + from mod454.schema import Mod454Schema as Schema + last_chrom = table = None + db = hdf.hdf_open( conf.HDF_DATABASE, mode='a', title='HDF database') + gp = hdf.create_group( db=db, name=conf.DATA_LABEL, desc='data group', clobber=conf.CLOBBER ) + fit_meta = conf.fit[2] + # iterate over the file and insert into table + for line in open( conf.fit[1], "r" ): + if line.startswith("chrom"): continue #Skip possible header + if line.startswith("#"): continue + fields = line.rstrip('\r\n').split('\t') + chrom = fields[fit_meta.chromCol] + if chrom != last_chrom: + if table: table.flush() + table = hdf.create_table( db=db, name=chrom, where=gp, schema=Schema, clobber=False ) + last_chrom = chrom + try: + position = int(fields[fit_meta.positionCol]) + forward = float(fields[fit_meta.forwardCol]) + reverse = fit_meta.reverseCol > -1 and float(fields[fit_meta.reverseCol]) or 0.0 + row = ( position, forward, reverse, forward+reverse, ) + table.append( [ row ] ) + except ValueError: + # Ignore bad lines + pass + table.flush() + db.close() + +def fitter( conf ): + from mod454.fitter import fitter as mod454_fitter + return mod454_fitter( conf ) + +def predictor( conf ): + from mod454.predictor import predictor as mod454_predictor + return mod454_predictor( conf ) + +def load_feature_files( conf, feats): + """ + Loads features from file names + """ + engine = sql.get_engine( conf.SQL_URI ) + sql.drop_indices(engine) + conn = engine.connect() + for label, fname, col_spec in feats: + label_id = sql.make_label(engine, name=label, clobber=False) + reader = io.NiceReaderWrapper( open(fname,"r"), + chrom_col=col_spec.chromCol, + start_col=col_spec.startCol, + end_col=col_spec.endCol, + strand_col=col_spec.strandCol, + fix_strand=False ) + values = list() + for interval in reader: + print interval + if not type( interval ) is io.GenomicInterval: continue + row = {'label_id':label_id, + 'name':col_spec.nameCol == -1 and "%s-%s" % (str(interval.start), str(interval.end)) or interval.fields[col_spec.nameCol], + 'altname':"", + 'chrom':interval.chrom, + 'start':interval.start, + 'end':interval.end, + 'strand':interval.strand, + 'value':0, + 'freetext':""} + values.append(row) + insert = sql.feature_table.insert() + conn.execute( insert, values) + conn.close() + sql.create_indices(engine) + + +class Bunch( object ): + def __init__(self, **kwargs): + for key,value in kwargs.items(): + setattr( self, key, value ) + +class DummyConf( Bunch ): + """ + Fake conf module for genetrack/atlas. + """ + pass + +if __name__ == "__main__": + options, args = doc_optparse.parse( __doc__ ) + try: + label = options.label + if options.fits: + fit_name, fit_meta = options.fits.split(':')[0], [int(x)-1 for x in options.fits.split(':')[1:]] + fit_meta = Bunch(chromCol=fit_meta[0], positionCol=fit_meta[1], forwardCol=fit_meta[2], reverseCol=fit_meta[3]) + fit = ( label, fit_name, fit_meta, ) + else: + fit = [] + # split apart the string into nested lists, preserves order + if options.feats: + feats = [ ( + feat_label, + fname, + Bunch(chromCol=int(chromCol)-1, startCol=int(startCol)-1, endCol=int(endCol)-1, + strandCol=int(strandCol)-1, nameCol=int(nameCol)-1), + ) + for feat_label, fname, chromCol, startCol, endCol, strandCol, nameCol + in ( feat.split(':') for feat in options.feats.split(',') if len(feat) > 0 )] + else: + feats = [] + data_dir = options.data + output = options.output + except: + doc_optparse.exception() + + main(label, fit, feats, data_dir, output) + \ No newline at end of file diff --git a/tools/visualization/genetrack.xml b/tools/visualization/genetrack.xml new file mode 100644 index 00000000000..8ab46560391 --- /dev/null +++ b/tools/visualization/genetrack.xml @@ -0,0 +1,62 @@ + + + Track creator/viewer + + + + + + + genetrack.py -l $data_label + #if not str($fit_data) == "None" + -1 + ${fit_data}:${fit_data.metadata.chromCol}:${fit_data.metadata.positionCol}:${fit_data.metadata.forwardCol}:${fit_data.metadata.reverseCol} + #end if + #if $feature_data + -2 + #end if + #for $data in $feature_data + ${data.name}:${data.input}:${data.input.metadata.chromCol}:${data.input.metadata.startCol}:${data.input.metadata.endCol}:${data.input.metadata.strandCol}:${data.input.metadata.nameCol}, + #end for + -d ${genetrack.files_path} + -o ${bed_out} + + + + + [a-zA-Z0-9]{0,25} + + + + + + [a-zA-Z0-9]{0,25} + + + + + + + + + + + tables + atlas + pychartdir + numpy + + +This tool takes the input Fit Data and creates a peak and curve plot showing +the reads and fitness on each basepair. Features can be plotted below as tracks. + +----- + +**Syntax** + +- **Track Label** is the name of the generated track. +- **Fit Data** are the datasets to calculate coverage/reads across basepairs and generate a curve. +- **Features** are additional datasets (interval format) to be plotted below as tracks. + + + diff --git a/tools/visualization/genetrack_code.py b/tools/visualization/genetrack_code.py new file mode 100644 index 00000000000..9c20ec27b73 --- /dev/null +++ b/tools/visualization/genetrack_code.py @@ -0,0 +1,13 @@ +import sets, os +from galaxy import eggs +from galaxy import jobs +from galaxy.tools.parameters import DataToolParameter + +def exec_after_process(app, inp_data, out_data, param_dict, tool=None, stdout=None, stderr=None): + """ + Copy data_label to genetrack.metadata.label + """ + out_data['genetrack'].metadata.label = param_dict['data_label'] + out_data['genetrack'].info = "Use the link below to view the custom track." + out_data['bed_out'].info = "" + \ No newline at end of file From 1cbc37f634b439ee06b70224cda893ff71f606d2 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Thu, 22 Jan 2009 11:56:01 -0500 Subject: [PATCH 176/267] Fix for sharing workflows ( typo ). --- lib/galaxy/web/controllers/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 514ebac758e..c4584960baa 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -71,7 +71,7 @@ class WorkflowController( BaseController ): # Load workflow from database stored = get_stored_workflow( trans, id ) if email: - other = model.User.filter( and_( model.user.table.c.email==email, + other = model.User.filter( and_( model.User.table.c.email==email, model.User.table.c.deleted==False ) ).first() if not other: mtype = "error" From 43891dced2b49866054bb67c8ae1b595e1159f38 Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Thu, 22 Jan 2009 12:06:29 -0500 Subject: [PATCH 177/267] updated bar charter to use png instead of pdf --- tools/plotting/bar_chart.py | 6 +++--- tools/plotting/bar_chart.xml | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tools/plotting/bar_chart.py b/tools/plotting/bar_chart.py index 9b533430391..29e1fa4dd69 100644 --- a/tools/plotting/bar_chart.py +++ b/tools/plotting/bar_chart.py @@ -14,7 +14,7 @@ a generic histogram builder based on gnuplot backend yrange_max - maximal value at the y_axis (integer) to set yrange to autoscaling assign 0 to yrange_min and yrange_max graph_file - file to write histogram image to - pdf_size - as X,Y pair in inches (e.g., 11,8 or 8,11 etc.) + img_size - as X,Y pair in pixels (e.g., 800,600 or 600,800 etc.) This tool required gnuplot and gnuplot.py @@ -48,7 +48,7 @@ def main(tmpFileName): ymin = sys.argv[6] ymax = sys.argv[7] img_file = sys.argv[8] - pdf_size = sys.argv[9] + img_size = sys.argv[9] except: stop_err("Check arguments\n") @@ -119,7 +119,7 @@ def main(tmpFileName): if xtic == 0: g('unset xtics') g(title) g(ylabel) - g_term = 'set terminal pdf size ' + pdf_size + g_term = 'set terminal png tiny size ' + img_size g(g_term) g_out = 'set output "' + img_file + '"' if ymin != ymax: diff --git a/tools/plotting/bar_chart.xml b/tools/plotting/bar_chart.xml index b758b8a91c2..560a37dc661 100644 --- a/tools/plotting/bar_chart.xml +++ b/tools/plotting/bar_chart.xml @@ -23,13 +23,13 @@ - - - - - - - + + + + + + + From 720d44dcd3b1acdddf70c1e56970e578a648e227 Mon Sep 17 00:00:00 2001 From: Anton Nekrutenko Date: Thu, 22 Jan 2009 12:45:00 -0500 Subject: [PATCH 178/267] Changed grouping output to tabular --- tools/stats/grouping.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/stats/grouping.xml b/tools/stats/grouping.xml index 8749b5dc922..84e3f5595bf 100644 --- a/tools/stats/grouping.xml +++ b/tools/stats/grouping.xml @@ -33,7 +33,7 @@ - + rpy From 894d600eb9554730ce8f24bfb6281a4c461464a6 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 22 Jan 2009 16:58:05 -0500 Subject: [PATCH 179/267] For uploads in the admin controller > 250MB, check the first 50MB and don't convert newlines if no carriage returns are found. --- lib/galaxy/datatypes/sniff.py | 16 ++++++++++++++++ lib/galaxy/web/controllers/admin.py | 8 +++++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 23a6e90b87a..c0e32c41b5f 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -1038,8 +1038,14 @@ class Admin( BaseController ): if space_to_tab: line_count = sniff.convert_newlines_sep2tabs( temp_name ) - else: + 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: From 1cc9d344fa3382c2523f9696572f884379fdc19d Mon Sep 17 00:00:00 2001 From: Ian Schenck Date: Thu, 22 Jan 2009 20:28:17 -0500 Subject: [PATCH 180/267] Pretty fast coverage converter. It's now trading cycles with IO. Without going to a pure C solution, this really won't get better. --- .../converters/interval_to_coverage.py | 134 ++++++++++++++---- 1 file changed, 103 insertions(+), 31 deletions(-) diff --git a/lib/galaxy/datatypes/converters/interval_to_coverage.py b/lib/galaxy/datatypes/converters/interval_to_coverage.py index 9b4bfccfa10..a73d25a4ddf 100644 --- a/lib/galaxy/datatypes/converters/interval_to_coverage.py +++ b/lib/galaxy/datatypes/converters/interval_to_coverage.py @@ -11,6 +11,12 @@ from galaxy import eggs import pkg_resources; pkg_resources.require( "bx-python" ) from bx.intervals import io from bx.cookbook import doc_optparse +import psyco_full +import commands +import os +from os import environ +import tempfile +from bisect import bisect INTERVAL_METADATA = ('chromCol', 'startCol', @@ -23,41 +29,99 @@ COVERAGE_METADATA = ('chromCol', 'reverseCol',) def main( interval, coverage ): - chroms = dict() + """ + Uses a sliding window of partitions to count coverages. + Every interval record adds its start and end to the partitions. The result + is a list of partitions, or every position that has a (maybe) different + number of basepairs covered. We don't worry about merging because we pop + as the sorted intervals are read in. As the input start positions exceed + the partition positions in partitions, coverages are kicked out in bulk. + """ + partitions = [] + forward_covs = [] + reverse_covs = [] + offset = 0 + chrom = None + lastchrom = None for record in interval: - if not type( record ) is io.GenomicInterval: continue - chrom = chroms[record.chrom] = chroms.get(record.chrom, dict()) - for position in xrange(record.start, record.end): - coverages = chrom[position] = chrom.get(position,[0,0]) - if record.strand == "-": coverages[1] += 1 - else: coverages[0] += 1 - for chrom in sorted(chroms.iterkeys()): - positions = chroms[chrom] - for position in sorted(positions.iterkeys()): - coverage.write( chrom=chrom, position=position, forward=positions[position][0], reverse=positions[position][1] ) - + chrom = record.chrom + if lastchrom and not lastchrom == chrom and partitions: + for partition in xrange(0, len(partitions)-1): + forward = forward_covs[partition] + reverse = reverse_covs[partition] + if forward+reverse > 0: + coverage.write(chrom=chrom, position=xrange(partitions[partition],partitions[partition+1]), + forward=forward, reverse=reverse) + partitions = [] + forward_covs = [] + reverse_covs = [] + + start_index = bisect(partitions, record.start) + forward = int(record.strand == "+") + reverse = int(record.strand == "-") + forward_base = 0 + reverse_base = 0 + if start_index > 0: + forward_base = forward_covs[start_index-1] + reverse_base = reverse_covs[start_index-1] + partitions.insert(start_index, record.start) + forward_covs.insert(start_index, forward_base) + reverse_covs.insert(start_index, reverse_base) + end_index = bisect(partitions, record.end) + for index in xrange(start_index, end_index): + forward_covs[index] += forward + reverse_covs[index] += reverse + partitions.insert(end_index, record.end) + forward_covs.insert(end_index, forward_covs[end_index-1] - forward ) + reverse_covs.insert(end_index, reverse_covs[end_index-1] - reverse ) + + if partitions: + for partition in xrange(0, start_index): + forward = forward_covs[partition] + reverse = reverse_covs[partition] + if forward+reverse > 0: + coverage.write(chrom=chrom, position=xrange(partitions[partition],partitions[partition+1]), + forward=forward, reverse=reverse) + partitions = partitions[start_index:] + forward_covs = forward_covs[start_index:] + reverse_covs = reverse_covs[start_index:] + + lastchrom = chrom + + # Finish the last chromosome + if partitions: + for partition in xrange(0, len(partitions)-1): + forward = forward_covs[partition] + reverse = reverse_covs[partition] + if forward+reverse > 0: + coverage.write(chrom=chrom, position=xrange(partitions[partition],partitions[partition+1]), + forward=forward, reverse=reverse) + class CoverageWriter( object ): def __init__( self, out_stream=None, chromCol=0, positionCol=1, forwardCol=2, reverseCol=3 ): - self.chromCol, self.positionCol, self.forwardCol, self.reverseCol = chromCol, positionCol, forwardCol, reverseCol - self.nfields = max( chromCol, positionCol, forwardCol, reverseCol )+1 self.out_stream = out_stream + self.reverseCol = reverseCol self.nlines = 0 - - def write(self, chrom="chr", position=0, forward=0, reverse=0 ): - self.nlines += 1 - if self.nlines % 64000: self.out_stream.flush() - outlist = [None] * self.nfields - outlist[self.chromCol] = str(chrom) - outlist[self.positionCol] = str(position) - if self.reverseCol == -1: outlist[self.forwardCol] = str(forward + reverse) - else: - outlist[self.forwardCol] = str(forward) - outlist[self.reverseCol] = str(reverse) - self.out_stream.write("%s\n" % "\t".join( outlist )) - - def flush(self): - self.out_stream.flush() + positions = {str(chromCol):'%(chrom)s', + str(positionCol):'%(position)d', + str(forwardCol):'%(forward)d', + str(reverseCol):'%(reverse)d'} + if reverseCol < 0: + self.template = "%(0)s\t%(1)s\t%(2)s\n" % positions + else: + self.template = "%(0)s\t%(1)s\t%(2)s\t%(3)s\n" % positions + + def write(self, **kwargs ): + if self.reverseCol < 0: kwargs['forward'] += kwargs['reverse'] + posgen = kwargs['position'] + for position in posgen: + kwargs['position'] = position + self.out_stream.write(self.template % kwargs) + def close(self): + self.out_stream.flush() + self.out_stream.close() + if __name__ == "__main__": options, args = doc_optparse.parse( __doc__ ) try: @@ -67,14 +131,22 @@ if __name__ == "__main__": except: doc_optparse.exception() + # Sort through a tempfile first + temp_file = tempfile.NamedTemporaryFile(mode="r") + environ['LC_ALL'] = 'POSIX' + commandline = "sort -f -n -k %d -k %d -k %d -o %s %s" % (chr_col_1+1,start_col_1+1,end_col_1+1, temp_file.name, in_fname) + errorcode, stdout = commands.getstatusoutput(commandline) + coverage = CoverageWriter( out_stream = open(out_fname, "a"), chromCol = chr_col_2, positionCol = position_col_2, forwardCol = forward_col_2, reverseCol = reverse_col_2, ) - interval = io.NiceReaderWrapper( open(in_fname, "r"), + temp_file.seek(0) + interval = io.NiceReaderWrapper( temp_file, chrom_col=chr_col_1, start_col=start_col_1, end_col=end_col_1, strand_col=strand_col_1, fix_strand=True ) main( interval, coverage ) - coverage.flush() \ No newline at end of file + temp_file.close() + coverage.close() \ No newline at end of file From 49f162e2cc3e9a3e4376d3ae0cde8d4b81ca24d6 Mon Sep 17 00:00:00 2001 From: Daniel Blankenberg Date: Fri, 23 Jan 2009 14:42:29 -0500 Subject: [PATCH 181/267] First pass at library enhancements allowing users to modify libraries and allowing admins to create information templates that can be used by users when they upload datasets to a library. User interface is very basic and needs work. Tests need to be created. Some features have not been implemented: info templates are always optional to fill out when adding datasets, it should be possible to have admin set whether it is optional or required (the field exists in the tables) user's cannot modify a Library users cannot add/edit info templates, even if they have full permissions on a folder etc. A lot of code cleanup is needed: theres alot of repeated code that needs to be modularized some table/class names should probably be changed (i.e. the LibraryFolderDatasetAssociation has no information about a folder, but the name remains the same to prevent breaking existing code) etc. --- lib/galaxy/model/__init__.py | 186 +++++- lib/galaxy/model/mapping.py | 200 ++++++- lib/galaxy/security/__init__.py | 145 ++++- lib/galaxy/tools/util/maf_utilities.py | 2 + lib/galaxy/web/controllers/admin.py | 191 +++++- lib/galaxy/web/controllers/library.py | 562 +++++++++++++++++- lib/galaxy/web/controllers/root.py | 5 +- .../library/add_dataset_from_history.mako | 8 + templates/admin/library/browser.mako | 4 +- templates/admin/library/common.mako | 61 +- templates/admin/library/dataset.mako | 18 +- .../admin/library/item_info_template.mako | 165 +++++ templates/admin/library/library_dataset.mako | 64 ++ templates/admin/library/new_dataset.mako | 8 + templates/admin/library/rename_folder.mako | 12 + templates/admin/library/rename_library.mako | 11 + templates/dataset/edit_attributes.mako | 16 +- templates/dataset/security_common.mako | 19 +- templates/library/browser.mako | 24 +- templates/library/common.mako | 110 +++- templates/library/dataset_manage_list.mako | 28 + templates/library/display_info.mako | 28 + templates/library/library_dataset.mako | 99 +++ templates/library/manage_folder.mako | 85 +++ templates/library/new_dataset.mako | 150 +++++ templates/library/new_info.mako | 20 + 26 files changed, 2154 insertions(+), 67 deletions(-) create mode 100644 templates/admin/library/item_info_template.mako create mode 100644 templates/admin/library/library_dataset.mako create mode 100644 templates/library/dataset_manage_list.mako create mode 100644 templates/library/display_info.mako create mode 100644 templates/library/library_dataset.mako create mode 100644 templates/library/manage_folder.mako create mode 100644 templates/library/new_dataset.mako create mode 100644 templates/library/new_info.mako diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 655fe6893da..51431381063 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -256,6 +256,22 @@ class ActionDatasetRoleAssociation( object ): 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 @@ -514,6 +530,23 @@ class DatasetInstance( object ): 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, @@ -566,11 +599,14 @@ class HistoryDatasetAssociation( DatasetInstance ): deleted=self.deleted, parent_id=parent_id, copied_from_history_dataset_association = self, - folder = target_folder ) + #folder = target_folder + ) des.flush() des.metadata = self.metadata #need to set after flushed, as MetadataFiles require dataset.id if target_folder: - target_folder.add_dataset( des ) + 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: @@ -613,7 +649,7 @@ class History( object ): self.galaxy_sessions.append( association ) def add_dataset( self, dataset, parent_id=None, genome_build=None, set_hid = True ): if isinstance( dataset, Dataset ): - dataset = HistoryDatasetAssociation( dataset = dataset ) + dataset = 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." @@ -650,6 +686,11 @@ class Library( object ): 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" @@ -658,6 +699,7 @@ class LibraryFolder( object ): 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 @@ -668,20 +710,91 @@ class LibraryFolder( object ): 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 LibraryFolderDatasetAssociation( DatasetInstance ): +class LibraryDataset( object ): + #This class acts as a proxy to the currently selected LFDA def __init__( self, folder = None, order_id = None, - copied_from_history_dataset_association = None, - copied_from_library_folder_dataset_association = None, - **kwd ): - DatasetInstance.__init__( self, **kwd ) + 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 ): @@ -734,6 +847,63 @@ class LibraryFolderDatasetAssociation( DatasetInstance ): 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 ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 40abaa49eea..2c5a04ecfc9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -13,7 +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 +from galaxy.security import GalaxyRBACAgent, LibraryRBACAgent metadata = MetaData() context = Session = scoped_session( sessionmaker( autoflush=False, transactional=False ) ) @@ -159,6 +159,20 @@ ActionDatasetRoleAssociation.table = Table( "action_dataset_role_association", m 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 ), @@ -171,13 +185,25 @@ DefaultHistoryPermissions.table = Table( "default_history_permissions", metadata Column( "action", TEXT ), Column( "role_id", Integer, ForeignKey( "role.id" ), index=True ) ) -LibraryFolderDatasetAssociation.table = Table( "library_folder_dataset_association", metadata, +LibraryDataset.table = Table( "library_dataset", metadata, Column( "id", Integer, primary_key=True ), - Column( "dataset_id", Integer, ForeignKey( "dataset.id" ), index=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 ) ), @@ -214,6 +240,70 @@ LibraryFolder.table = Table( "library_folder", metadata, 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 ), @@ -387,6 +477,10 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab 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 ) ), @@ -492,6 +586,18 @@ assign_mapper( context, ActionDatasetRoleAssociation, ActionDatasetRoleAssociati ) ) +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, @@ -509,14 +615,14 @@ assign_mapper( context, LibraryFolder, LibraryFolder.table, 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( LibraryFolderDatasetAssociation, - primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.folder_id == LibraryFolder.table.c.id ) ), - order_by=asc( LibraryFolderDatasetAssociation.table.c.order_id ), + 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( LibraryFolderDatasetAssociation, - primaryjoin=( ( LibraryFolderDatasetAssociation.table.c.folder_id == LibraryFolder.table.c.id ) & ( not_( LibraryFolderDatasetAssociation.table.c.deleted ) ) ), - order_by=asc( LibraryFolderDatasetAssociation.table.c.order_id ), + 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( @@ -525,14 +631,54 @@ assign_mapper( context, LibraryFolder, LibraryFolder.table, 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 ), - folder=relation( LibraryFolder ), + 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 ), @@ -546,6 +692,39 @@ assign_mapper( context, LibraryFolderDatasetAssociation, LibraryFolderDatasetAss 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, @@ -687,6 +866,7 @@ def init( file_path, url, engine_options={}, create_tables=False ): 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(): diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 3932868341e..d747416d537 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -17,13 +17,23 @@ class Action( object ): class RBACAgent: """Class that handles galaxy security""" permitted_actions = Bunch( - DATASET_EDIT_METADATA = Action( - "edit metadata", "Role members can edit this dataset's metadata in the library", "grant" ), + #DATASET_EDIT_METADATA = Action( + # "edit metadata", "Role members can edit this dataset's metadata in the library", "grant" ), DATASET_MANAGE_PERMISSIONS = Action( "manage permissions", "Role members can manage the roles associated with this dataset", "grant" ), DATASET_ACCESS = Action( "access", "Role members can import this dataset into their history for analysis", "restrict" ) ) + #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 @@ -383,6 +393,137 @@ class GalaxyRBACAgent( RBACAgent ): 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( trans.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: diff --git a/lib/galaxy/tools/util/maf_utilities.py b/lib/galaxy/tools/util/maf_utilities.py index 097a5332f2f..14f4fc0b244 100644 --- a/lib/galaxy/tools/util/maf_utilities.py +++ b/lib/galaxy/tools/util/maf_utilities.py @@ -141,6 +141,8 @@ def maf_index_by_uid( maf_uid, index_location_file ): maf_files = fields[4].replace( "\n", "" ).replace( "\r", "" ).split( "," ) return bx.align.maf.MultiIndexed( maf_files, keep_open = True, parse_e_rows = False ) except Exception, e: + print maf_uid + print e raise 'MAF UID (%s) found, but configuration appears to be malformed: %s' % ( maf_uid, e ) except: pass diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index c0e32c41b5f..754e2086f23 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -815,6 +815,8 @@ class Admin( BaseController ): 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' ) ) @@ -871,6 +873,17 @@ class Admin( BaseController ): 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 ): @@ -942,6 +955,8 @@ class Admin( BaseController ): 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' ) ) @@ -990,6 +1005,54 @@ class Admin( BaseController ): 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 ): @@ -1006,7 +1069,7 @@ class Admin( BaseController ): 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 ): + 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 ) @@ -1050,14 +1113,26 @@ class Admin( BaseController ): 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 ) - folder = trans.app.model.LibraryFolder.get( folder_id ) - folder.add_dataset( dataset, genome_build=last_used_build ) 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 ) @@ -1078,7 +1153,13 @@ class Admin( BaseController ): 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 @@ -1090,7 +1171,7 @@ class Admin( BaseController ): msg = 'Select a file, enter a URL or Text, or select a server directory.' else: msg = 'Select a file, enter a URL or enter Text.' - trans.response.send_redirect( web.url_for( action='dataset', folder_id=folder_id, msg=util.sanitize_text( msg ), messagetype='done' ) ) + 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 @@ -1112,7 +1193,8 @@ class Admin( BaseController ): last_used_build, roles, info="uploaded file", - space_to_tab=space_to_tab ) + 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: @@ -1127,7 +1209,8 @@ class Admin( BaseController ): last_used_build, roles, info="uploaded url", - space_to_tab=space_to_tab ) + 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 @@ -1182,7 +1265,7 @@ class Admin( BaseController ): messagetype='error' ) ) # No dataset(s) specified, display upload form - elif not id: + 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 @@ -1199,7 +1282,8 @@ class Admin( BaseController ): last_used_build=last_used_build, roles=roles, msg=msg, - messagetype=messagetype ) + messagetype=messagetype, + replace_dataset=replace_dataset ) else: if id.count( ',' ): ids = id.split( ',' ) @@ -1222,7 +1306,16 @@ class Admin( BaseController ): 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 ) @@ -1357,6 +1450,86 @@ class Admin( BaseController ): 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): diff --git a/lib/galaxy/web/controllers/library.py b/lib/galaxy/web/controllers/library.py index 21dd1b9ec6c..22e1b9a3ed4 100644 --- a/lib/galaxy/web/controllers/library.py +++ b/lib/galaxy/web/controllers/library.py @@ -1,6 +1,7 @@ from galaxy.web.base.controller import * from galaxy.model.orm import * -import logging, tempfile, zipfile, tarfile, os, sys +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 @@ -11,10 +12,10 @@ log = logging.getLogger( __name__ ) class Library( BaseController ): @web.expose - def browse( self, trans, **kwd ): + 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 ) ) + 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 ): @@ -23,9 +24,9 @@ class Library( BaseController ): if not isinstance( import_ids, list ): import_ids = [ import_ids ] p = util.Params( kwd ) - if not p.action: + if not p.do_action: return trans.show_error_message( "You must select an action to perform on selected datasets" ) - if p.action == 'add': + 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() @@ -37,8 +38,8 @@ class Library( BaseController ): # Can't use mkstemp - the file must not exist first try: tmpd = tempfile.mkdtemp() - tmpf = os.path.join( tmpd, 'library_download.' + p.action ) - if p.action == 'zip': + 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: @@ -49,13 +50,13 @@ class Library( BaseController ): 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.action == 'tgz': + 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.action == 'tbz': + elif p.do_action == 'tbz': try: archive = tarfile.open( tmpf, 'w:bz2' ) except tarfile.CompressionError: @@ -118,3 +119,546 @@ class Library( BaseController ): 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 52604219687..0f7ba0e9f46 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -223,7 +223,8 @@ class RootController( BaseController ): if trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_ACCESS, dataset=data ): params = util.Params( kwd, safe=False ) - if lid is None or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset=data ): + 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 @@ -306,7 +307,7 @@ class RootController( BaseController ): 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 ) + 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 ) diff --git a/templates/admin/library/add_dataset_from_history.mako b/templates/admin/library/add_dataset_from_history.mako index e8fd815bba5..6d3799c5a2d 100644 --- a/templates/admin/library/add_dataset_from_history.mako +++ b/templates/admin/library/add_dataset_from_history.mako @@ -10,7 +10,15 @@
    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} diff --git a/templates/admin/library/browser.mako b/templates/admin/library/browser.mako index 06646e0720a..4362070bad0 100644 --- a/templates/admin/library/browser.mako +++ b/templates/admin/library/browser.mako @@ -106,7 +106,7 @@ def name_sorted( l ): Add a new dataset to this folder Copy a dataset from your history to this folder Create a new sub-folder in this folder - Rename this folder + Edit this folder %if subfolder: Remove this folder and its contents from the library %endif @@ -189,7 +189,7 @@ def name_sorted( l ): %if not deleted: %else: diff --git a/templates/admin/library/common.mako b/templates/admin/library/common.mako index 2384d3850b8..c92343a541d 100644 --- a/templates/admin/library/common.mako +++ b/templates/admin/library/common.mako @@ -1,6 +1,12 @@ ## Render the dataset `data` <%def name="render_dataset( data, selected, deleted )"> -
    + <% + #the data id should be the underlying lfda id, to prevent id collision (could happen when displaying children, which are always ldfas); and to function more seemlessly with existing code + data_id = data.id + if isinstance( data, trans.app.model.LibraryDataset ): + data_id = data.library_folder_dataset_association.id + %> +
    ## Header row for library items (name, state, action buttons)
    @@ -8,19 +14,19 @@ %if selected: - + %else: - + %endif ${data.display_name()} %if not deleted: - - ## Body for library items, extra info and actions, data "peek" -
    +
    ${data.blurb}
    %if data.has_data: @@ -48,7 +54,7 @@ %endif
    %if data.peek != "no peek": -
    ${data.display_peek()}
    +
    ${data.display_peek()}
    %endif ## Recurse for child datasets %if len( data.visible_children ) > 0: @@ -62,3 +68,38 @@
    + + +<%def name="render_available_templates( library_item )"> +<% +library_item_ids = {} +if isinstance( library_item, trans.app.model.Library ): + library_item_ids['library_id']=library_item.id +elif isinstance( library_item, trans.app.model.LibraryDataset ): + library_item_ids['library_dataset_id']=library_item.id +elif isinstance( library_item, trans.app.model.LibraryFolder ): + library_item_ids['folder_id']=library_item.id +elif isinstance( library_item, trans.app.model.LibraryFolderDatasetAssociation ): + library_item_ids['library_folder_dataset_association_id']=library_item.id + +%> +
    +
    Available Templates
    +
    + %for available_template_assoc in library_item.library_item_info_template_associations: +
    + ${available_template_assoc.library_item_info_template.name}: ${available_template_assoc.library_item_info_template.description} +
    +
    + %endfor +
    + Click here to create a new template for this library item. +
    +
    +
    +
    +
    + +

    + + \ No newline at end of file diff --git a/templates/admin/library/dataset.mako b/templates/admin/library/dataset.mako index 68c620f8e42..5aea34ea4d4 100644 --- a/templates/admin/library/dataset.mako +++ b/templates/admin/library/dataset.mako @@ -1,7 +1,13 @@ <%inherit file="/base.mako"/> <%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/admin/library/common.mako" import="render_available_templates" /> +%if msg: + ${render_msg( msg, messagetype )} +%endif + <%def name="title()">Edit Dataset Attributes <%def name="datatype( dataset, datatypes )"> @@ -25,9 +31,15 @@ <% name_str = '%d selected datasets' % len( dataset ) %> - ${render_permission_form( dataset[0].dataset, name_str, h.url_for( action='dataset' ), 'id', ",".join( [ str(d.id) for d in dataset ] ), roles )} + ${render_permission_form( dataset[0], name_str, h.url_for( action='dataset' ), 'id', ",".join( [ str(d.id) for d in dataset ] ), roles )} %else: - ${render_permission_form( dataset.dataset, dataset.name, h.url_for( action='dataset' ), 'id', dataset.id, roles )} + ${render_permission_form( dataset, dataset.name, h.url_for( action='dataset' ), 'id', dataset.id, roles )} +%endif + +%if dataset.library_dataset.library_folder_dataset_association == dataset: + ${render_msg( 'You are currently viewing the latest version of this Library Dataset, you can go here to manage versions.' % ( h.url_for( controller='admin', action='library_dataset', id=dataset.library_dataset.id ) ), 'info' )} +%else: + ${render_msg( 'You are currently viewing an expired version of this Library Dataset, you can go here to manage versions.' % ( h.url_for( controller='admin', action='library_dataset', id=dataset.library_dataset.id ) ), 'warning' )} %endif %if not isinstance( dataset, list ): @@ -103,3 +115,5 @@

    %endif + +${render_available_templates( dataset )} diff --git a/templates/admin/library/item_info_template.mako b/templates/admin/library/item_info_template.mako new file mode 100644 index 00000000000..46e4e8451f9 --- /dev/null +++ b/templates/admin/library/item_info_template.mako @@ -0,0 +1,165 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> + +<%def name="title()">Edit Library Item Info Template + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%if library_item_info_template: +

    +
    Edit Library Item Info Template
    +
    + + + + + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + + + + %for template_element in library_item_info_template.elements: +
    Edit Element
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + %endfor + + %for element_count in range( new_element_count ): +
    +
    Create Element ${1+element_count}
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + %endfor +
    + +
    + +
    +
    +
    + + + + +
    +
    + +

    +%else: +

    +
    Create Library Item Info Template
    +
    +
    + %if library_id: + + %elif library_dataset_id: + + %elif folder_id: + + %elif library_folder_dataset_association_id: + + %endif + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + + + %for element_count in range( new_element_count ): +
    +
    Create Element ${1+element_count}
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + %endfor +
    + +
    + +
    +
    +
    + + +
    +
    + +

    + + +%endif \ No newline at end of file diff --git a/templates/admin/library/library_dataset.mako b/templates/admin/library/library_dataset.mako new file mode 100644 index 00000000000..4a365249ccb --- /dev/null +++ b/templates/admin/library/library_dataset.mako @@ -0,0 +1,64 @@ +<%inherit file="/base.mako"/> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/admin/library/common.mako" import="render_available_templates" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +<%def name="title()">Edit Library Dataset Attributes + +<% + roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all() +%> + + +${render_permission_form( dataset, dataset.name, h.url_for( action='library_dataset' ), 'id', dataset.id, roles )} + +

    +
    Edit Attributes
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + ${dataset.library_folder_dataset_association.name} (current) + %for expired_dataset in dataset.expired_datasets: +
    + ${expired_dataset.name} + %endfor +
    +
    + Replace this dataset with a new version: upload | from your history +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +

    + +${render_available_templates( dataset )} \ No newline at end of file diff --git a/templates/admin/library/new_dataset.mako b/templates/admin/library/new_dataset.mako index 074d6e6f7ca..b604abe424c 100644 --- a/templates/admin/library/new_dataset.mako +++ b/templates/admin/library/new_dataset.mako @@ -11,7 +11,15 @@

    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
    diff --git a/templates/admin/library/rename_folder.mako b/templates/admin/library/rename_folder.mako index 8638edf60a1..9629c9ed278 100644 --- a/templates/admin/library/rename_folder.mako +++ b/templates/admin/library/rename_folder.mako @@ -1,5 +1,7 @@ <%inherit file="/base.mako"/> <%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +<%namespace file="/admin/library/common.mako" import="render_available_templates" /> %if msg: ${render_msg( msg, messagetype )} @@ -39,3 +41,13 @@
    + +

    + +<% + roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all() +%> + +${render_permission_form( folder, folder.name, h.url_for( action='folder' ), 'id', folder.id, roles )} + +${render_available_templates( folder )} \ No newline at end of file diff --git a/templates/admin/library/rename_library.mako b/templates/admin/library/rename_library.mako index 9114248a942..ccb3b5a437d 100644 --- a/templates/admin/library/rename_library.mako +++ b/templates/admin/library/rename_library.mako @@ -1,5 +1,7 @@ <%inherit file="/base.mako"/> <%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/dataset/security_common.mako" import="render_permission_form" /> +<%namespace file="/admin/library/common.mako" import="render_available_templates" /> %if msg: ${render_msg( msg, messagetype )} @@ -46,3 +48,12 @@

    + +

    +<% + roles = trans.app.model.Role.filter( trans.app.model.Role.table.c.deleted==False ).order_by( trans.app.model.Role.table.c.name ).all() +%> + +${render_permission_form( library, library.name, h.url_for( action='library' ), 'id', library.id, roles )} + +${render_available_templates( library )} diff --git a/templates/dataset/edit_attributes.mako b/templates/dataset/edit_attributes.mako index 3a450ba83d3..9545dd8d0ef 100644 --- a/templates/dataset/edit_attributes.mako +++ b/templates/dataset/edit_attributes.mako @@ -1,4 +1,7 @@ <%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="/library/common.mako" import="render_existing_library_item_info" /> + <%def name="title()">Edit Dataset Attributes @@ -20,9 +23,14 @@ id_name = 'id' elif isinstance( data, trans.app.model.LibraryFolderDatasetAssociation ): id_name = 'lid' + lda_source, library_source = data.source_library_dataset %> -%if ( id_name == 'id' or trans.app.security_agent.allow_action( trans.user, data.permitted_actions.DATASET_EDIT_METADATA, dataset = data ) ): +%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
    @@ -193,3 +201,9 @@
    %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 index 0643e22f1f3..a57b658df5d 100644 --- a/templates/dataset/security_common.mako +++ b/templates/dataset/security_common.mako @@ -32,6 +32,7 @@ ## 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 @@ -41,6 +42,22 @@ 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 @@ -73,7 +90,7 @@

    - %for k, v in trans.app.model.Dataset.permitted_actions.items(): + %for k, v in permitted_actions: %if k not in do_not_render:
    ${render_select( current_actions, k, v, all_roles )} diff --git a/templates/library/browser.mako b/templates/library/browser.mako index f326fc25119..08ae3fdb9d3 100644 --- a/templates/library/browser.mako +++ b/templates/library/browser.mako @@ -107,7 +107,10 @@ def name_sorted( l ): <%def name="render_folder( parent, parent_pad )"> <% - if not trans.app.security_agent.check_folder_contents( trans.user, parent ): + 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: @@ -127,8 +130,17 @@ def name_sorted( l ): %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: