From 7a41b0cb48b5e556e0b802ccfbc04bbc8197b194 Mon Sep 17 00:00:00 2001 From: Nate Coraor Date: Thu, 4 Aug 2011 10:39:16 -0400 Subject: [PATCH] User/group disk quotas. --- lib/galaxy/app.py | 6 + lib/galaxy/config.py | 1 + lib/galaxy/jobs/__init__.py | 11 +- lib/galaxy/model/__init__.py | 47 ++ lib/galaxy/model/mapping.py | 46 ++ lib/galaxy/quota/__init__.py | 149 ++++++ lib/galaxy/util/__init__.py | 26 + lib/galaxy/web/base/controller.py | 482 +++++++++++++++++++ lib/galaxy/web/controllers/admin.py | 113 +++++ lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/root.py | 21 +- static/june_2007_style/blue/panel_layout.css | 6 + static/june_2007_style/blue_colors.ini | 6 + static/june_2007_style/masthead.css.tmpl | 2 +- static/june_2007_style/panel_layout.css.tmpl | 50 ++ templates/admin/quota/grid.mako | 1 + templates/admin/quota/quota.mako | 84 ++++ templates/admin/quota/quota_create.mako | 124 +++++ templates/admin/quota/quota_edit.mako | 47 ++ templates/admin/quota/quota_rename.mako | 53 ++ templates/admin/quota/quota_set_default.mako | 45 ++ templates/root/history.mako | 54 ++- templates/user/index.mako | 7 +- templates/webapps/galaxy/admin/index.mako | 1 + templates/webapps/galaxy/base_panels.mako | 45 ++ universe_wsgi.ini.sample | 3 + 26 files changed, 1426 insertions(+), 6 deletions(-) create mode 100644 lib/galaxy/quota/__init__.py create mode 100644 templates/admin/quota/grid.mako create mode 100644 templates/admin/quota/quota.mako create mode 100644 templates/admin/quota/quota_create.mako create mode 100644 templates/admin/quota/quota_edit.mako create mode 100644 templates/admin/quota/quota_rename.mako create mode 100644 templates/admin/quota/quota_set_default.mako diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 77e26a37d19..a9dc38eef3c 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -7,6 +7,7 @@ from galaxy.web import security import galaxy.model import galaxy.datatypes.registry import galaxy.security +import galaxy.quota from galaxy.tags.tag_handler import GalaxyTagHandler from galaxy.tools.imp_exp import load_history_imp_exp_tools from galaxy.sample_tracking import external_service_types @@ -57,6 +58,11 @@ class UniverseApplication( object ): #Load security policy self.security_agent = self.model.security_agent self.host_security_agent = galaxy.security.HostAgent( model=self.security_agent.model, permitted_actions=self.security_agent.permitted_actions ) + # Load quota management + if self.config.enable_quotas: + self.quota_agent = galaxy.quota.QuotaAgent( self.model ) + else: + self.quota_agent = galaxy.quota.NoQuotaAgent( self.model ) # Heartbeat and memdump for thread / heap profiling self.heartbeat = None self.memdump = None diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index ff69704f8a9..fa12ae6ec5e 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -45,6 +45,7 @@ class Configuration( object ): # web API self.enable_api = string_as_bool( kwargs.get( 'enable_api', False ) ) self.enable_openid = string_as_bool( kwargs.get( 'enable_openid', False ) ) + self.enable_quotas = string_as_bool( kwargs.get( 'enable_quotas', False ) ) self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root ) self.tool_data_path = resolve_path( kwargs.get( "tool_data_path", "tool-data" ), os.getcwd() ) self.len_file_path = kwargs.get( "len_file_path", resolve_path(os.path.join(self.tool_data_path, 'shared','ucsc','chrom'), self.root) ) diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 44ca21df912..6b2fd213fec 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -203,7 +203,7 @@ class JobQueue( object ): elif job_state == JOB_DELETED: log.info( "job %d deleted by user while still queued" % job.id ) elif job_state == JOB_ADMIN_DELETED: - job.info( "job %d deleted by admin while still queued" % job.id ) + log.info( "job %d deleted by admin while still queued" % job.id ) else: log.error( "unknown job state '%s' for job %d" % ( job_state, job.id ) ) if not self.track_jobs_in_database: @@ -229,6 +229,15 @@ class JobQueue( object ): return JOB_DELETED elif job.state == model.Job.states.ERROR: return JOB_ADMIN_DELETED + elif self.app.config.enable_quotas: + quota = self.app.quota_agent.get_quota( job.user ) + if quota is not None: + try: + usage = self.app.quota_agent.get_usage( user=job.user, history=job.history ) + if usage > quota: + return JOB_WAIT + except AssertionError, e: + pass # No history, should not happen with an anon user for dataset_assoc in job.input_datasets + job.input_library_datasets: idata = dataset_assoc.dataset if not idata: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 7c66cbe51e6..681c4ca7f6a 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -483,6 +483,53 @@ class Role( object, APIItem ): self.type = type self.deleted = deleted +class UserQuotaAssociation( object ): + def __init__( self, user, quota ): + self.user = user + self.quota = quota + +class GroupQuotaAssociation( object ): + def __init__( self, group, quota ): + self.group = group + self.quota = quota + +class Quota( object ): + valid_operations = ( '+', '-', '=' ) + def __init__( self, name="", description="", amount=0, operation="=" ): + self.name = name + self.description = description + if amount is None: + self.bytes = -1 + else: + self.bytes = amount + self.operation = operation + def get_amount( self ): + if self.bytes == -1: + return None + return self.bytes + def set_amount( self, amount ): + if amount is None: + self.bytes = -1 + else: + self.bytes = amount + amount = property( get_amount, set_amount ) + @property + def display_amount( self ): + if self.bytes == -1: + return "unlimited" + else: + return util.nice_size( self.bytes ) + +class DefaultQuotaAssociation( Quota ): + types = Bunch( + UNREGISTERED = 'unregistered', + REGISTERED = 'registered' + ) + def __init__( self, type, quota ): + assert type in self.types.__dict__.values(), 'Invalid type' + self.type = type + self.quota = quota + class DatasetPermissions( object ): def __init__( self, action, dataset, role ): self.action = action diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 06c1ae2d245..a4c00f70d20 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -197,6 +197,37 @@ Role.table = Table( "role", metadata, Column( "type", String( 40 ), index=True ), Column( "deleted", Boolean, index=True, default=False ) ) +UserQuotaAssociation.table = Table( "user_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupQuotaAssociation.table = Table( "group_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +Quota.table = Table( "quota", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", String( 255 ), index=True, unique=True ), + Column( "description", TEXT ), + Column( "bytes", Integer ), + Column( "operation", String( 8 ) ), + Column( "deleted", Boolean, index=True, default=False ) ) + +DefaultQuotaAssociation.table = Table( "default_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "type", String( 32 ), index=True, unique=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ) ) + DatasetPermissions.table = Table( "dataset_permissions", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -1251,6 +1282,21 @@ assign_mapper( context, GroupRoleAssociation, GroupRoleAssociation.table, ) ) +assign_mapper( context, Quota, Quota.table, + properties=dict( users=relation( UserQuotaAssociation ), + groups=relation( GroupQuotaAssociation ) ) ) + +assign_mapper( context, UserQuotaAssociation, UserQuotaAssociation.table, + properties=dict( user=relation( User, backref="quotas" ), + quota=relation( Quota ) ) ) + +assign_mapper( context, GroupQuotaAssociation, GroupQuotaAssociation.table, + properties=dict( group=relation( Group, backref="quotas" ), + quota=relation( Quota ) ) ) + +assign_mapper( context, DefaultQuotaAssociation, DefaultQuotaAssociation.table, + properties=dict( quota=relation( Quota, backref="default" ) ) ) + assign_mapper( context, DatasetPermissions, DatasetPermissions.table, properties=dict( dataset=relation( Dataset, backref="actions" ), diff --git a/lib/galaxy/quota/__init__.py b/lib/galaxy/quota/__init__.py new file mode 100644 index 00000000000..3c544877129 --- /dev/null +++ b/lib/galaxy/quota/__init__.py @@ -0,0 +1,149 @@ +""" +Galaxy Quotas + +""" +import logging, socket, operator +from datetime import datetime, timedelta +from galaxy import util +from galaxy.util.bunch import Bunch +from galaxy.model.orm import * + +log = logging.getLogger(__name__) + +class NoQuotaAgent( object ): + """Base quota agent, always returns no quota""" + def __init__( self, model ): + self.model = model + self.sa_session = model.context + def get_quota( self, user, nice_size=False ): + return None + @property + def default_quota( self ): + return None + def get_usage( self, trans=None, user=False, history=False ): + if trans: + user = trans.user + history = trans.history + assert user is not False, "Could not determine user." + if not user: + assert history, "Could not determine anonymous user's history." + usage = history.get_disk_size() + else: + usage = user.total_disk_usage + return usage + def get_percent( self, trans=None, user=False, history=False, usage=False, quota=False ): + return None + +class QuotaAgent( NoQuotaAgent ): + """Class that handles galaxy quotas""" + def get_quota( self, user, nice_size=False ): + """ + Calculated like so: + 1. Anonymous users get the default quota. + 2. Logged in users start with the highest of their associated '=' + quotas or the default quota, if there are no associated '=' + quotas. If an '=' unlimited (-1 in the database) quota is found + during this process, the user has no quota (aka unlimited). + 3. Quota is increased or decreased by any corresponding '+' or '-' + quotas. + """ + if not user: + return self.default_unregistered_quota + quotas = [] + for group in [ uga.group for uga in user.groups ]: + for quota in [ gqa.quota for gqa in group.quotas ]: + if quota not in quotas: + quotas.append( quota ) + for quota in [ uqa.quota for uqa in user.quotas ]: + if quota not in quotas: + quotas.append( quota ) + use_default = True + max = 0 + adjustment = 0 + for quota in quotas: + if quota.deleted: + continue + if quota.operation == '=' and quota.bytes == -1: + return None + elif quota.operation == '=': + use_default = False + if quota.bytes > max: + max = quota.bytes + elif quota.operation == '+': + adjustment += quota.bytes + elif quota.operation == '-': + adjustment -= quota.bytes + if use_default: + max = self.default_registered_quota + if max is None: + return None + quota = max + adjustment + if quota <= 0: + quota = 0 + if nice_size: + if quota is not None: + quota = util.nice_size( quota ) + else: + quota = 'unlimited' + return quota + @property + def default_unregistered_quota( self ): + return self._default_quota( self.model.DefaultQuotaAssociation.types.UNREGISTERED ) + @property + def default_registered_quota( self ): + return self._default_quota( self.model.DefaultQuotaAssociation.types.REGISTERED ) + def _default_quota( self, default_type ): + dqa = self.sa_session.query( self.model.DefaultQuotaAssociation ).filter( self.model.DefaultQuotaAssociation.table.c.type==default_type ).first() + if not dqa: + return None + if dqa.quota.bytes < 0: + return None + return dqa.quota.bytes + def set_default_quota( self, default_type, quota ): + # Unset the current default(s) associated with this quota, if there are any + for dqa in quota.default: + self.sa_session.delete( dqa ) + # Unset the current users/groups associated with this quota + for uqa in quota.users: + self.sa_session.delete( uqa ) + for gqa in quota.groups: + self.sa_session.delete( gqa ) + # Find the old default, assign the new quota if it exists + dqa = self.sa_session.query( self.model.DefaultQuotaAssociation ).filter( self.model.DefaultQuotaAssociation.table.c.type==default_type ).first() + if dqa: + dqa.quota = quota + # Or create if necessary + else: + dqa = self.model.DefaultQuotaAssociation( default_type, quota ) + self.sa_session.add( dqa ) + self.sa_session.flush() + def get_percent( self, trans=None, user=False, history=False, usage=False, quota=False ): + if trans: + user = trans.user + history = trans.history + if quota is False: + quota = self.get_quota( user ) + if quota is None: + return None + if usage is False: + usage = self.get_usage( trans, user, history ) + percent = int( float( usage ) / quota * 100 ) + if percent > 100: + percent = 100 + return percent + def set_entity_quota_associations( self, quotas=[], users=[], groups=[], delete_existing_assocs=True ): + for quota in quotas: + if delete_existing_assocs: + flush_needed = False + for a in quota.users + quota.groups: + self.sa_session.delete( a ) + flush_neeeded = True + if flush_needed: + self.sa_session.flush() + for user in users: + uqa = self.model.UserQuotaAssociation( user, quota ) + self.sa_session.add( uqa ) + for group in groups: + gqa = self.model.GroupQuotaAssociation( group, quota ) + self.sa_session.add( gqa ) + self.sa_session.flush() diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 54f0b3082be..58e85208c64 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -542,6 +542,32 @@ def nice_size(size): return "%.1f %s" % (size, word) return '??? bytes' +def size_to_bytes( size ): + """ + Returns a number of bytes if given a reasably formatted string with the size + """ + # Assume input in bytes if we can convert directly to an int + try: + return int( size ) + except: + pass + # Otherwise it must have non-numeric characters + size_re = re.compile( '([\d\.]+)\s*([tgmk]b?|b|bytes?)$' ) + size_match = re.match( size_re, size.lower() ) + assert size_match is not None + size = float( size_match.group(1) ) + multiple = size_match.group(2) + if multiple.startswith( 't' ): + return int( size * 1024**4 ) + elif multiple.startswith( 'g' ): + return int( size * 1024**3 ) + elif multiple.startswith( 'm' ): + return int( size * 1024**2 ) + elif multiple.startswith( 'k' ): + return int( size * 1024 ) + elif multiple.startswith( 'b' ): + return int( size ) + def send_mail( frm, to, subject, body, config ): """ Sends an email. diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index f0837d6a93c..f870f236e9f 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -1111,6 +1111,7 @@ class Admin( object ): user_list_grid = None role_list_grid = None group_list_grid = None + quota_list_grid = None @web.expose @web.require_admin @@ -1518,6 +1519,481 @@ class Admin( object ): message=util.sanitize_text( message ), status='done' ) ) + # Galaxy Quota Stuff + @web.expose + @web.require_admin + def quotas( self, trans, **kwargs ): + if 'operation' in kwargs: + operation = kwargs['operation'].lower() + if operation == "quotas": + return self.quota( trans, **kwargs ) + if operation == "create": + return self.create_quota( trans, **kwargs ) + if operation == "delete": + return self.mark_quota_deleted( trans, **kwargs ) + if operation == "undelete": + return self.undelete_quota( trans, **kwargs ) + if operation == "purge": + return self.purge_quota( trans, **kwargs ) + if operation == "change amount": + return self.edit_quota( trans, **kwargs ) + if operation == "manage users and groups": + return self.manage_users_and_groups_for_quota( trans, **kwargs ) + if operation == "rename": + return self.rename_quota( trans, **kwargs ) + if operation == "edit": + return self.edit_quota( trans, **kwargs ) + # Render the list view + return self.quota_list_grid( trans, **kwargs ) + + @web.expose + @web.require_admin + def create_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + name = util.restore_text( params.get( 'name', '' ) ) + description = util.restore_text( params.get( 'description', '' ) ) + amount = util.restore_text( params.get( 'amount', '' ).strip() ) + if amount.lower() in ( 'unlimited', 'none', 'no limit' ): + create_amount = None + else: + try: + create_amount = util.size_to_bytes( amount ) + except AssertionError: + create_amount = False + operation = params.get( 'operation', '' ) + default = params.get( 'default', 'no' ) + in_users = util.listify( params.get( 'in_users', [] ) ) + out_users = util.listify( params.get( 'out_users', [] ) ) + in_groups = util.listify( params.get( 'in_groups', [] ) ) + out_groups = util.listify( params.get( 'out_groups', [] ) ) + if params.get( 'create_quota_button', False ): + if not name or not description: + message = "Enter a valid name and a description." + status = 'error' + elif trans.sa_session.query( trans.app.model.Quota ).filter( trans.app.model.Quota.table.c.name==name ).first(): + message = "Quota names must be unique and a quota with that name already exists, so choose another name." + status = 'error' + elif not params.get( 'amount', None ): + message = "Enter a valid quota amount." + status = 'error' + elif create_amount is False: + message = "Unable to parse the provided amount." + status = 'error' + elif operation not in trans.app.model.Quota.valid_operations: + message = "Enter a valid operation." + status = 'error' + elif default != 'no' and default not in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + message = "Enter a valid default type." + status = 'error' + elif default != 'no' and operation != '=': + message = "Operation for a default quota must be '='." + status = 'error' + operation = '=' + else: + # Create the quota + quota = trans.app.model.Quota( name=name, description=description, amount=create_amount, operation=operation ) + trans.sa_session.add( quota ) + # If this is a default quota, create the DefaultQuotaAssociation + if default != 'no': + trans.app.quota_agent.set_default_quota( default, quota ) + else: + # Create the UserQuotaAssociations + for user in [ trans.sa_session.query( trans.app.model.User ).get( x ) for x in in_users ]: + uqa = trans.app.model.UserQuotaAssociation( user, quota ) + trans.sa_session.add( uqa ) + # Create the GroupQuotaAssociations + for group in [ trans.sa_session.query( trans.app.model.Group ).get( x ) for x in in_groups ]: + gqa = trans.app.model.GroupQuotaAssociation( group, quota ) + trans.sa_session.add( gqa ) + trans.sa_session.flush() + message = "Quota '%s' has been created with %d associated users and %d associated groups." % ( quota.name, len( in_users ), len( in_groups ) ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + in_users = map( int, in_users ) + in_groups = map( int, in_groups ) + new_in_users = [] + new_in_groups = [] + for user in trans.sa_session.query( trans.app.model.User ) \ + .filter( trans.app.model.User.table.c.deleted==False ) \ + .order_by( trans.app.model.User.table.c.email ): + if user.id in in_users: + new_in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + for group in trans.sa_session.query( trans.app.model.Group ) \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ): + if group.id in in_groups: + new_in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + return trans.fill_template( '/admin/quota/quota_create.mako', + webapp=webapp, + name=name, + description=description, + amount=amount, + operation=operation, + default=default, + in_users=new_in_users, + out_users=out_users, + in_groups=new_in_groups, + out_groups=out_groups, + message=message, + status=status ) + @web.expose + @web.require_admin + def rename_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + error = True + try: + assert id, 'No quota ids received for renaming' + quota = get_quota( trans, id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.id != 1, 'The default quota cannot be renamed' + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + if params.get( 'rename_quota_button', False ): + old_name = quota.name + new_name = util.restore_text( params.name ) + new_description = util.restore_text( params.description ) + if not new_name: + message = 'Enter a valid name' + status='error' + elif trans.sa_session.query( trans.app.model.Quota ).filter( trans.app.model.Quota.table.c.name==new_name ).first(): + message = 'A quota with that name already exists' + status = 'error' + else: + quota.name = new_name + quota.description = new_description + trans.sa_session.add( quota ) + trans.sa_session.flush() + message = "Quota '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + return trans.fill_template( '/admin/quota/quota_rename.mako', + quota=quota, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def manage_users_and_groups_for_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + error = True + try: + assert id, 'No quota ids received for managing users and groups' + quota = get_quota( trans, id ) + assert quota, 'Quota id (%s) is invalid' % id + assert not quota.default, 'Default quotas cannot be associated with specific users and groups' + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + if params.get( 'quota_members_edit_button', False ): + in_users = [ trans.sa_session.query( trans.app.model.User ).get( x ) for x in util.listify( params.in_users ) ] + in_groups = [ trans.sa_session.query( trans.app.model.Group ).get( x ) for x in util.listify( params.in_groups ) ] + trans.app.quota_agent.set_entity_quota_associations( quotas=[ quota ], users=in_users, groups=in_groups ) + trans.sa_session.refresh( quota ) + message = "Quota '%s' has been updated with %d associated users and %d associated groups" % ( quota.name, len( in_users ), len( in_groups ) ) + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status=status ) ) + in_users = [] + out_users = [] + in_groups = [] + out_groups = [] + for user in trans.sa_session.query( trans.app.model.User ) \ + .filter( trans.app.model.User.table.c.deleted==False ) \ + .order_by( trans.app.model.User.table.c.email ): + if user in [ x.user for x in quota.users ]: + in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + for group in trans.sa_session.query( trans.app.model.Group ) \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ): + if group in [ x.group for x in quota.groups ]: + in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + return trans.fill_template( '/admin/quota/quota.mako', + quota=quota, + in_users=in_users, + out_users=out_users, + in_groups=in_groups, + out_groups=out_groups, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def edit_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for editing" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if params.get( 'edit_quota_button', False ): + amount = util.restore_text( params.get( 'amount', '' ).strip() ) + if amount.lower() in ( 'unlimited', 'none', 'no limit' ): + new_amount = None + else: + try: + new_amount = util.size_to_bytes( amount ) + except AssertionError: + new_amount = False + operation = params.get( 'operation', None ) + if not params.get( 'amount', None ): + message = 'Enter a valid amount' + status='error' + elif new_amount is False: + message = 'Unable to parse the provided amount' + status = 'error' + elif operation not in trans.app.model.Quota.valid_operations: + message = 'Enter a valid operation' + status = 'error' + else: + quota.amount = new_amount + quota.operation = operation + trans.sa_session.add( quota ) + trans.sa_session.flush() + message = "Quota '%s' is now '%s'" % ( quota.name, quota.operation + quota.display_amount ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + + return trans.fill_template( '/admin/quota/quota_edit.mako', + quota=quota, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def set_quota_default( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + default = params.get( 'default', '' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for managing defaults" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if params.get( 'set_default_quota_button', False ): + if default != 'no' and default not in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + message = "Enter a valid default type." + status = 'error' + else: + if default != 'no': + trans.app.quota_agent.set_default_quota( default, quota ) + message = "Quota '%s' is now the default for %s users" % ( quota.name, default ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + if not default: + default = 'no' + return trans.fill_template( '/admin/quota/quota_set_default.mako', + quota=quota, + webapp=webapp, + default=default, + message=message, + status=status ) + @web.expose + @web.require_admin + def unset_quota_default( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + default = params.get( 'default', '' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for managing defaults" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if not quota.default: + message = "Quota '%s' is not a default." % quota.name + status = 'error' + else: + message = "Quota '%s' is no longer the default for %s users." % ( quota.name, quota.default[0].type ) + status = 'done' + for dqa in quota.default: + trans.sa_session.delete( dqa ) + trans.sa_session.flush() + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status=status ) ) + + @web.expose + @web.require_admin + def mark_quota_deleted( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for deleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert not quota.default, "Quota '%s' is a default, please unset it as a default before deleting it" % ( quota.name ) + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Deleted %d quotas: " % len( ids ) + for quota in quotas: + quota.deleted = True + trans.sa_session.add( quota ) + message += " %s " % quota.name + trans.sa_session.flush() + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + @web.expose + @web.require_admin + def undelete_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for undeleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.deleted, "Quota '%s' has not been deleted, so it cannot be undeleted." % quota.name + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Undeleted %d quotas: " % len( ids ) + for quota in quotas: + quota.deleted = False + trans.sa_session.add( quota ) + trans.sa_session.flush() + message += " %s " % quota.name + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + @web.expose + @web.require_admin + def purge_quota( self, trans, **kwd ): + # This method should only be called for a Quota that has previously been deleted. + # Purging a deleted Quota deletes all of the following from the database: + # - UserQuotaAssociations where quota_id == Quota.id + # - GroupQuotaAssociations where quota_id == Quota.id + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for undeleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.deleted, "Quota '%s' has not been deleted, so it cannot be purged." % quota.name + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Purged %d quotas: " % len( ids ) + for quota in quotas: + # Delete UserQuotaAssociations + for uqa in quota.users: + trans.sa_session.delete( uqa ) + # Delete GroupQuotaAssociations + for gqa in quota.groups: + trans.sa_session.delete( gqa ) + trans.sa_session.flush() + message += " %s " % quota.name + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) # Galaxy Group Stuff @web.expose @web.require_admin @@ -2235,3 +2711,9 @@ def get_group( trans, id ): if not group: return trans.show_error_message( "Group not found for id (%s)" % str( id ) ) return group +def get_quota( trans, id ): + """Get a Quota from the database by id.""" + # Load user from database + id = trans.security.decode_id( id ) + quota = trans.sa_session.query( trans.model.Quota ).get( id ) + return quota diff --git a/lib/galaxy/web/controllers/admin.py b/lib/galaxy/web/controllers/admin.py index 83944b3abec..4ba6a684287 100644 --- a/lib/galaxy/web/controllers/admin.py +++ b/lib/galaxy/web/controllers/admin.py @@ -268,8 +268,121 @@ class GroupListGrid( grids.Grid ): preserve_state = False use_paging = True +class QuotaListGrid( grids.Grid ): + class NameColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + return quota.name + class DescriptionColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + if quota.description: + return quota.description + return '' + class AmountColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + return quota.operation + quota.display_amount + class StatusColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.deleted: + return "deleted" + elif quota.default: + return "default for %s users" % quota.default[0].type + return "" + class UsersColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.users: + return len( quota.users ) + return 0 + class GroupsColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.groups: + return len( quota.groups ) + return 0 + + # Grid definition + webapp = "galaxy" + title = "Quotas" + model_class = model.Quota + template='/admin/quota/grid.mako' + default_sort_key = "name" + columns = [ + NameColumn( "Name", + key="name", + link=( lambda item: dict( operation="Manage users and groups", id=item.id, webapp="galaxy" ) if not item.default else dict( operation="Change amount", id=item.id, webapp="galaxy" ) ), + model_class=model.Quota, + attach_popup=True, + filterable="advanced" ), + DescriptionColumn( "Description", + key='description', + model_class=model.Quota, + attach_popup=False, + filterable="advanced" ), + AmountColumn( "Amount", + key='amount', + model_class=model.Quota, + attach_popup=False, + filterable="advanced" ), + UsersColumn( "Users", attach_popup=False ), + GroupsColumn( "Groups", attach_popup=False ), + StatusColumn( "Status", attach_popup=False ), + # Columns that are valid for filtering but are not visible. + grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ) + ] + columns.append( grids.MulticolFilterColumn( "Search", + cols_to_filter=[ columns[0], columns[1], columns[2] ], + key="free-text-search", + visible=False, + filterable="standard" ) ) + global_actions = [ + grids.GridAction( "Add new quota", dict( controller='admin', action='quotas', operation='create' ) ) + ] + operations = [ grids.GridOperation( "Rename", + condition=( lambda item: not item.deleted ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="rename_quota" ) ), + grids.GridOperation( "Change amount", + condition=( lambda item: not item.deleted ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="edit_quota" ) ), + grids.GridOperation( "Manage users and groups", + condition=( lambda item: not item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="manage_users_and_groups_for_quota" ) ), + grids.GridOperation( "Set as different type of default", + condition=( lambda item: item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="set_quota_default" ) ), + grids.GridOperation( "Set as default", + condition=( lambda item: not item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="set_quota_default" ) ), + grids.GridOperation( "Unset as default", + condition=( lambda item: item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="unset_quota_default" ) ), + grids.GridOperation( "Delete", + condition=( lambda item: not item.deleted and not item.default ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="mark_quota_deleted" ) ), + grids.GridOperation( "Undelete", + condition=( lambda item: item.deleted ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="undelete_quota" ) ), + grids.GridOperation( "Purge", + condition=( lambda item: item.deleted ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="purge_quota" ) ) ] + standard_filters = [ + grids.GridColumnFilter( "Active", args=dict( deleted=False ) ), + grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), + grids.GridColumnFilter( "All", args=dict( deleted='All' ) ) + ] + num_rows_per_page = 50 + preserve_state = False + use_paging = True + class AdminGalaxy( BaseController, Admin ): user_list_grid = UserListGrid() role_list_grid = RoleListGrid() group_list_grid = GroupListGrid() + quota_list_grid = QuotaListGrid() diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index 7752550c6f8..e86a9b43d45 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -936,7 +936,7 @@ class DatasetInterface( BaseController, UsesAnnotations, UsesHistory, UsesHistor assert topmost_parent in history.datasets, "Data does not belong to current history" # If the user is anonymous, make sure the HDA is owned by the current session. if not user: - assert trans.galaxy_session.id in [ s.id for s in hda.history.galaxy_sessions ], 'Invalid history dataset ID' + assert trans.galaxy_session.current_history_id == trans.history.id, 'Invalid history dataset ID' # If the user is known, make sure the HDA is owned by the current user. else: assert topmost_parent.history.user == trans.user, 'Invalid history dataset ID' diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 213f36f7a91..0c28989d5aa 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -126,6 +126,7 @@ class RootController( BaseController, UsesHistory, UsesAnnotations ): hda_id = hda_id, show_deleted = show_deleted, show_hidden=show_hidden, + over_quota=trans.app.quota_agent.get_percent( trans=trans ) >= 100, message=message, status=status ) @@ -192,7 +193,25 @@ class RootController( BaseController, UsesHistory, UsesAnnotations ): @web.json def history_get_disk_size( self, trans ): - return trans.history.get_disk_size( nice_size=True ) + rval = { 'history' : trans.history.get_disk_size( nice_size=True ) } + for k, v in self.__user_get_usage( trans ).items(): + rval['global_' + k] = v + return rval + + @web.json + def user_get_usage( self, trans ): + return self.__user_get_usage( trans ) + + def __user_get_usage( self, trans ): + usage = trans.app.quota_agent.get_usage( trans ) + percent = trans.app.quota_agent.get_percent( trans=trans, usage=usage ) + rval = {} + if percent is None: + rval['usage'] = util.nice_size( usage ) + else: + rval['percent'] = percent + return rval + ## ---- Dataset display / editing ---------------------------------------- diff --git a/static/june_2007_style/blue/panel_layout.css b/static/june_2007_style/blue/panel_layout.css index 978cb7e224d..298ee40af41 100644 --- a/static/june_2007_style/blue/panel_layout.css +++ b/static/june_2007_style/blue/panel_layout.css @@ -36,6 +36,12 @@ div.unified-panel-body{position:absolute;top:2em;bottom:0;width:100%;margin-top: #masthead a{color:#eeeeee;text-decoration:none;} #masthead .title{font-family:verdana;padding:3px 10px;font-size:175%;font-weight:bold;z-index:-1;} #masthead a:hover{text-decoration:underline;} +.quota-meter-container{position:absolute;top:0;right:0;height:32px;} +.quota-meter{position:absolute;top:8px;right:8px;height:16px;width:100px;background-color:#C1C9E5;;} +.quota-meter-bar{position:absolute;top:0;left:0;height:16px;background-color:#969DB3;;} +.quota-meter-bar-warn{background-color:#FFB400;;} +.quota-meter-bar-error{background-color:#FF4343;;} +.quota-meter-text{position:absolute;top:50%;left:0;width:100px;height:16px;margin-top:-6px;text-align:center;z-index:9001;color:#000;;} .tab-group{margin:0;padding:0 10px;height:100%;white-space:nowrap;cursor:default;background:transparent;} .tab-group .tab{background:#2C3143;position:relative;float:left;margin:0;padding:0 1em;height:32px;line-height:32px;text-align:left;} .tab-group .tab .submenu{display:none;position:absolute;z-index:16000;left:0;top:32px;padding:1em;margin:-1em;padding-top:0;margin-top:0;background-color:rgba(0,0,0,0.5);-moz-border-radius:0 0 1em 1em;-webkit-border-bottom-right-radius:1em;-webkit-border-bottom-left-radius:1em;} diff --git a/static/june_2007_style/blue_colors.ini b/static/june_2007_style/blue_colors.ini index 3c6b9631458..b0fd5ba5482 100644 --- a/static/june_2007_style/blue_colors.ini +++ b/static/june_2007_style/blue_colors.ini @@ -59,6 +59,12 @@ masthead_text=#eeeeee masthead_bg_hatch=- masthead_link=#eeeeee masthead_active_tab_bg=#222532 +# Quota meter +quota_meter_bg=#C1C9E5; +quota_meter_bar=#969DB3; +quota_meter_warn_bar=#FFB400; +quota_meter_error_bar=#FF4343; +quota_meter_text=#000; # ---- Layout ----------------------------------------------------------------- # Overall background color (including space between panels) layout_bg=#eee diff --git a/static/june_2007_style/masthead.css.tmpl b/static/june_2007_style/masthead.css.tmpl index a6d3b7ea5c7..f80fcfbef08 100644 --- a/static/june_2007_style/masthead.css.tmpl +++ b/static/june_2007_style/masthead.css.tmpl @@ -59,4 +59,4 @@ span.link-group span.active-link 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 ac0e59188e6..614079a91a4 100644 --- a/static/june_2007_style/panel_layout.css.tmpl +++ b/static/june_2007_style/panel_layout.css.tmpl @@ -259,6 +259,56 @@ div.unified-panel-body { text-decoration: underline; } +.quota-meter-container +{ + position: absolute; + top: 0; + right: 0; + height: 32px; +} + +.quota-meter +{ + position: absolute; + top: 8px; + right: 8px; + height: 16px; + width: 100px; + background-color: $quota_meter_bg; +} + +.quota-meter-bar +{ + position: absolute; + top: 0; + left: 0; + height: 16px; + background-color: $quota_meter_bar; +} + +.quota-meter-bar-warn +{ + background-color: $quota_meter_warn_bar; +} + +.quota-meter-bar-error +{ + background-color: $quota_meter_error_bar; +} + +.quota-meter-text +{ + position: absolute; + top: 50%; + left: 0; + width: 100px; + height: 16px; + margin-top: -6px; + text-align: center; + z-index: 9001; + color: $quota_meter_text; +} + ## Tabs .tab-group { diff --git a/templates/admin/quota/grid.mako b/templates/admin/quota/grid.mako new file mode 100644 index 00000000000..6a41b688c5e --- /dev/null +++ b/templates/admin/quota/grid.mako @@ -0,0 +1 @@ +<%inherit file="/grid_base.mako"/> diff --git a/templates/admin/quota/quota.mako b/templates/admin/quota/quota.mako new file mode 100644 index 00000000000..47550494492 --- /dev/null +++ b/templates/admin/quota/quota.mako @@ -0,0 +1,84 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<%def name="render_select( name, options )"> + + + + + +%if message: + ${render_msg( message, status )} +%endif + +
+
Quota '${quota.name}'
+
+
+ +
+
+ + ${render_select( "in_users", in_users )}
+ +
+
+ + ${render_select( "out_users", out_users )}
+ +
+
+
+
+ + ${render_select( "in_groups", in_groups )}
+ +
+
+ + ${render_select( "out_groups", out_groups )}
+ +
+
+
+ +
+
+
+
diff --git a/templates/admin/quota/quota_create.mako b/templates/admin/quota/quota_create.mako new file mode 100644 index 00000000000..56f287f8dcf --- /dev/null +++ b/templates/admin/quota/quota_create.mako @@ -0,0 +1,124 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<%def name="render_select( name, options )"> + + + + + +<% + from galaxy.web.form_builder import SelectField + operation_selectfield = SelectField( 'operation' ) + for op in ( '=', '+', '-' ): + selected = op == operation + operation_selectfield.add_option( op, op, selected ) + default_selectfield = SelectField( 'default' ) + selected = 'no' == default + default_selectfield.add_option( 'No', 'no', selected ) + for typ in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + selected = typ == default + default_selectfield.add_option( 'Yes, ' + typ, typ, selected ) +%> + +%if message: + ${render_msg( message, status )} +%endif + +
+
Create quota
+
+
+
+ + + +
+
+ + +
+
+ + +
+ Examples: "10000MB", "99 gb", "0.2T", "unlimited" +
+
+
+ + ${operation_selectfield.get_html()} +
+
+ + ${default_selectfield.get_html()} +
+ Warning: Any user or group associations selected below will be ignored if this quota is used as a default. +
+
+
+
+ + ${render_select( "in_users", in_users )}
+ +
+
+ + ${render_select( "out_users", out_users )}
+ +
+
+
+
+ + ${render_select( "in_groups", in_groups )}
+ +
+
+ + ${render_select( "out_groups", out_groups )}
+ +
+
+
+ +
+
+
+
diff --git a/templates/admin/quota/quota_edit.mako b/templates/admin/quota/quota_edit.mako new file mode 100644 index 00000000000..5fffe937dca --- /dev/null +++ b/templates/admin/quota/quota_edit.mako @@ -0,0 +1,47 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<% + from galaxy.web.form_builder import SelectField + operation_selectfield = SelectField( 'operation' ) + for op in ( '=', '+', '-' ): + selected = op == quota.operation + operation_selectfield.add_option( op, op, selected ) +%> + +%if message: + ${render_msg( message, status )} +%endif + +
+
Change quota amount
+
+
+ + +
+ + +
+ Examples: "10000MB", "99 gb", "0.2T", "unlimited" +
+
+
+ + ${operation_selectfield.get_html()} +
+
+ +
+
+
+
diff --git a/templates/admin/quota/quota_rename.mako b/templates/admin/quota/quota_rename.mako new file mode 100644 index 00000000000..e0398ca6129 --- /dev/null +++ b/templates/admin/quota/quota_rename.mako @@ -0,0 +1,53 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +%if message: + ${render_msg( message, status )} +%endif + +
+
Change quota name and description
+
+
+
+ + +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/templates/admin/quota/quota_set_default.mako b/templates/admin/quota/quota_set_default.mako new file mode 100644 index 00000000000..8e52dcfebf0 --- /dev/null +++ b/templates/admin/quota/quota_set_default.mako @@ -0,0 +1,45 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<%def name="javascripts()"> + ${parent.javascripts()} + + + +<% + from galaxy.web.form_builder import SelectField + default_selectfield = SelectField( 'default' ) + selected = 'no' == default + default_selectfield.add_option( 'No', 'no', selected ) + for typ in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + selected = typ == default + default_selectfield.add_option( 'Yes, ' + typ, typ, selected ) +%> + +%if message: + ${render_msg( message, status )} +%endif + +
+
Create quota
+
+
+ + +
+ + ${default_selectfield.get_html()} +
+ Warning: Any users or groups associated with this quota will be disassociated. +
+
+
+ +
+
+
+
diff --git a/templates/root/history.mako b/templates/root/history.mako index c6e394ab33a..9340cd1921a 100644 --- a/templates/root/history.mako +++ b/templates/root/history.mako @@ -272,8 +272,41 @@ $(function() { %if hda_id: self.location = "#${hda_id}"; %endif + + // Update the Quota Meter + $.ajax( { + type: "POST", + url: "${h.url_for( controller='root', action='user_get_usage' )}", + dataType: "json", + success : function ( data ) { + $.each( data, function( type, val ) { + quota_meter_updater( type, val ); + }); + } + }); }); +// Updates the Quota Meter +var quota_meter_updater = function ( type, val ) { + if ( type == "usage" ) { + $("#quota-meter-bar", window.top.document).css( "width", "0" ); + $("#quota-meter-text", window.top.document).text( "Using " + val ); + } else if ( type == "percent" ) { + $("#quota-meter-bar", window.top.document).removeClass("quota-meter-bar-warn quota-meter-bar-error"); + if ( val >= 100 ) { + $("#quota-meter-bar", window.top.document).addClass("quota-meter-bar-error"); + $("#quota-message-container").slideDown(); + } else if ( val >= 85 ) { + $("#quota-meter-bar", window.top.document).addClass("quota-meter-bar-warn"); + $("#quota-message-container").slideUp(); + } else { + $("#quota-message-container").slideUp(); + } + $("#quota-meter-bar", window.top.document).css( "width", val + "px" ); + $("#quota-meter-text", window.top.document).text( "Using " + val + "%" ); + } +} + // Looks for changes in dataset state using an async request. Keeps // calling itself (via setTimeout) until all datasets are in a terminal // state. @@ -334,7 +367,15 @@ var updater_callback = function ( tracked_datasets ) { url: "${h.url_for( controller='root', action='history_get_disk_size' )}", dataType: "json", success: function( data ) { - $("#history-size").text( data ); + $.each( data, function( type, val ) { + if ( type == "history" ) { + $("#history-size").text( val ); + } else if ( type == "global_usage" ) { + quota_meter_updater( "usage", val ); + } else if ( type == "global_percent" ) { + quota_meter_updater( "percent", val ); + } + }); } }); check_history_size = false; @@ -471,6 +512,17 @@ div.form-row { %endif +%if over_quota: +
+%else: + + %if not datasets:
diff --git a/templates/user/index.mako b/templates/user/index.mako index 1a776dace75..4e5fc44c343 100644 --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -22,7 +22,12 @@
  • ${_('Manage your information')}
  • %endif -

    You are currently using ${trans.user.get_disk_usage( nice_size=True )} of disk space in this Galaxy instance.

    +

    + You are using ${trans.user.get_disk_usage( nice_size=True )} of disk space in this Galaxy instance. + %if trans.app.config.enable_quotas: + Your disk quota is: ${trans.app.quota_agent.get_quota( trans.user, nice_size=True )}. + %endif +

    %else: %if not message:

    ${n_('You are currently not logged in.')}

    diff --git a/templates/webapps/galaxy/admin/index.mako b/templates/webapps/galaxy/admin/index.mako index a29b7a4663a..c8c9f8b61c9 100644 --- a/templates/webapps/galaxy/admin/index.mako +++ b/templates/webapps/galaxy/admin/index.mako @@ -55,6 +55,7 @@
    diff --git a/templates/webapps/galaxy/base_panels.mako b/templates/webapps/galaxy/base_panels.mako index f9336a3ced6..7832ec40e40 100644 --- a/templates/webapps/galaxy/base_panels.mako +++ b/templates/webapps/galaxy/base_panels.mako @@ -3,6 +3,11 @@ ## Default title <%def name="title()">Galaxy +<%def name="javascripts()"> + ${parent.javascripts()} + ${h.js( "jquery.tipsy" )} + + ## Masthead <%def name="masthead()"> @@ -172,5 +177,45 @@ %endif
    + + ## Quota meter + <% + bar_style = "quota-meter-bar" + usage = 0 + percent = 0 + quota = None + try: + usage = trans.app.quota_agent.get_usage( trans=trans ) + quota = trans.app.quota_agent.get_quota( trans.user ) + percent = trans.app.quota_agent.get_percent( usage=usage, quota=quota ) + if percent is not None: + if percent >= 100: + bar_style += " quota-meter-bar-error" + elif percent >= 85: + bar_style += " quota-meter-bar-warn" + else: + percent = 0 + except AssertionError: + pass # Probably no history yet + tooltip = None + if not trans.user and quota and trans.app.config.allow_user_creation: + if trans.app.quota_agent.default_registered_quota is None or trans.app.quota_agent.default_unregistered_quota < trans.app.quota_agent.default_registered_quota: + tooltip = "Your disk quota is %s. You can increase your quota by registering a Galaxy account." % util.nice_size( quota ) + %> + +
    + %if tooltip: +
    + %else: +
    + %endif +
    + %if quota is not None: +
    Using ${percent}%
    + %else: +
    Using ${util.nice_size( usage )}
    + %endif +
    +
    diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index 527f1e15e4b..350289abc38 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -454,6 +454,9 @@ use_interactive = True # users in the help text. #ftp_upload_site = None +# Enable enforcement of quotas. Quotas can be set from the Admin interface. +#enable_quotas = False + # -- Job Execution # If running multiple Galaxy processes, one can be designated as the job