User/group disk quotas.

This commit is contained in:
Nate Coraor
2011-08-04 10:39:16 -04:00
parent 4720488fa4
commit 7a41b0cb48
26 changed files with 1426 additions and 6 deletions
+6
View File
@@ -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
+1
View File
@@ -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) )
+10 -1
View File
@@ -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:
+47
View File
@@ -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
+46
View File
@@ -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" ),
+149
View File
@@ -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()
+26
View File
@@ -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.
+482
View File
@@ -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
+113
View File
@@ -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 "<strong>default for %s users</strong>" % 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()
+1 -1
View File
@@ -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'
+20 -1
View File
@@ -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 ----------------------------------------
@@ -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;}
+6
View File
@@ -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
+1 -1
View File
@@ -59,4 +59,4 @@ span.link-group span.active-link
margin-left: -3px; margin-right: -3px;
padding-bottom: 10px;
margin-bottom: -10px;
}
}
@@ -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 {
+1
View File
@@ -0,0 +1 @@
<%inherit file="/grid_base.mako"/>
+84
View File
@@ -0,0 +1,84 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
$("input:text:first").focus();
})
</script>
</%def>
<%def name="render_select( name, options )">
<select name="${name}" id="${name}" style="min-width: 250px; height: 150px;" multiple>
%for option in options:
<option value="${option[0]}">${option[1]}</option>
%endfor
</select>
</%def>
<script type="text/javascript">
$().ready(function() {
$('#users_add_button').click(function() {
return !$('#out_users option:selected').remove().appendTo('#in_users');
});
$('#users_remove_button').click(function() {
return !$('#in_users option:selected').remove().appendTo('#out_users');
});
$('#groups_add_button').click(function() {
return !$('#out_groups option:selected').remove().appendTo('#in_groups');
});
$('#groups_remove_button').click(function() {
return !$('#in_groups option:selected').remove().appendTo('#out_groups');
});
$('form#associate_role_user_group').submit(function() {
$('#in_users option').each(function(i) {
$(this).attr("selected", "selected");
});
$('#in_groups option').each(function(i) {
$(this).attr("selected", "selected");
});
});
});
</script>
%if message:
${render_msg( message, status )}
%endif
<div class="toolForm">
<div class="toolFormTitle">Quota '${quota.name}'</div>
<div class="toolFormBody">
<form name="associate_quota_user_group" id="associate_quota_user_group" action="${h.url_for( action='manage_users_and_groups_for_quota', id=trans.security.encode_id( quota.id ) )}" method="post" >
<input name="webapp" type="hidden" value="${webapp}" size=40"/>
<div class="form-row">
<div style="float: left; margin-right: 10px;">
<label>Users associated with '${quota.name}'</label>
${render_select( "in_users", in_users )}<br/>
<input type="submit" id="users_remove_button" value=">>"/>
</div>
<div>
<label>Users not associated with '${quota.name}'</label>
${render_select( "out_users", out_users )}<br/>
<input type="submit" id="users_add_button" value="<<"/>
</div>
</div>
<div class="form-row">
<div style="float: left; margin-right: 10px;">
<label>Groups associated with '${quota.name}'</label>
${render_select( "in_groups", in_groups )}<br/>
<input type="submit" id="groups_remove_button" value=">>"/>
</div>
<div>
<label>Groups not associated with '${quota.name}'</label>
${render_select( "out_groups", out_groups )}<br/>
<input type="submit" id="groups_add_button" value="<<"/>
</div>
</div>
<div class="form-row">
<input type="submit" name="quota_members_edit_button" value="Save"/>
</div>
</form>
</div>
</div>
+124
View File
@@ -0,0 +1,124 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
$("input:text:first").focus();
})
</script>
</%def>
<%def name="render_select( name, options )">
<select name="${name}" id="${name}" style="min-width: 250px; height: 150px;" multiple>
%for option in options:
<option value="${option[0]}">${option[1]}</option>
%endfor
</select>
</%def>
<script type="text/javascript">
$().ready(function() {
$('#groups_add_button').click(function() {
return !$('#out_groups option:selected').remove().appendTo('#in_groups');
});
$('#groups_remove_button').click(function() {
return !$('#in_groups option:selected').remove().appendTo('#out_groups');
});
$('#users_add_button').click(function() {
return !$('#out_users option:selected').remove().appendTo('#in_users');
});
$('#users_remove_button').click(function() {
return !$('#in_users option:selected').remove().appendTo('#out_users');
});
$('form#associate_quota_group_user').submit(function() {
$('#in_groups option').each(function(i) {
$(this).attr("selected", "selected");
});
$('#in_users option').each(function(i) {
$(this).attr("selected", "selected");
});
});
});
</script>
<%
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
<div class="toolForm">
<div class="toolFormTitle">Create quota</div>
<div class="toolFormBody">
<form name="associate_quota_group_user" id="associate_quota_group_user" action="${h.url_for( action='create_quota' )}" method="post" >
<div class="form-row">
<input name="webapp" type="hidden" value="${webapp}" size=40"/>
<label>Name:</label>
<input name="name" type="textfield" value="${name}" size=40"/>
</div>
<div class="form-row">
<label>Description:</label>
<input name="description" type="textfield" value="${description}" size=40"/>
</div>
<div class="form-row">
<label>Amount</label>
<input name="amount" type="textfield" value="${amount}" size=40"/>
<div class="toolParamHelp" style="clear: both;">
Examples: "10000MB", "99 gb", "0.2T", "unlimited"
</div>
</div>
<div class="form-row">
<label>Assign, increase by amount, or decrease by amount?</label>
${operation_selectfield.get_html()}
</div>
<div class="form-row">
<label>Is this quota a default for a class of users (if yes, what type)?</label>
${default_selectfield.get_html()}
<div class="toolParamHelp" style="clear: both;">
Warning: Any user or group associations selected below will be ignored if this quota is used as a default.
</div>
</div>
<div class="form-row">
<div style="float: left; margin-right: 10px;">
<label>Users associated with new quota</label>
${render_select( "in_users", in_users )}<br/>
<input type="submit" id="users_remove_button" value=">>"/>
</div>
<div>
<label>Users not associated with new quota</label>
${render_select( "out_users", out_users )}<br/>
<input type="submit" id="users_add_button" value="<<"/>
</div>
</div>
<div class="form-row">
<div style="float: left; margin-right: 10px;">
<label>Groups associated with new quota</label>
${render_select( "in_groups", in_groups )}<br/>
<input type="submit" id="groups_remove_button" value=">>"/>
</div>
<div>
<label>Groups not associated with new quota</label>
${render_select( "out_groups", out_groups )}<br/>
<input type="submit" id="groups_add_button" value="<<"/>
</div>
</div>
<div class="form-row">
<input type="submit" name="create_quota_button" value="Save"/>
</div>
</form>
</div>
</div>
+47
View File
@@ -0,0 +1,47 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
$("input:text:first").focus();
})
</script>
</%def>
<%
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
<div class="toolForm">
<div class="toolFormTitle">Change quota amount</div>
<div class="toolFormBody">
<form name="library" action="${h.url_for( controller='admin', action='edit_quota' )}" method="post" >
<input name="webapp" type="hidden" value="${webapp}"/>
<input name="id" type="hidden" value="${trans.security.encode_id( quota.id )}"/>
<div class="form-row">
<label>Amount</label>
<input name="amount" type="textfield" value="${quota.display_amount}" size=40"/>
<div class="toolParamHelp" style="clear: both;">
Examples: "10000MB", "99 gb", "0.2T", "unlimited"
</div>
</div>
<div class="form-row">
<label>Assign, increase by amount, or decrease by amount?</label>
${operation_selectfield.get_html()}
</div>
<div class="form-row">
<input type="submit" name="edit_quota_button" value="Save"/>
</div>
</form>
</div>
</div>
+53
View File
@@ -0,0 +1,53 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
$("input:text:first").focus();
})
</script>
</%def>
%if message:
${render_msg( message, status )}
%endif
<div class="toolForm">
<div class="toolFormTitle">Change quota name and description</div>
<div class="toolFormBody">
<form name="library" action="${h.url_for( controller='admin', action='rename_quota' )}" method="post" >
<div class="form-row">
<input name="webapp" type="hidden" value="${webapp}" size=40"/>
<label>Name:</label>
<div style="float: left; width: 250px; margin-right: 10px;">
<input type="text" name="name" value="${quota.name}" size="40"/>
</div>
<div style="clear: both"></div>
</div>
<div class="form-row">
<label>Description:</label>
<div style="float: left; width: 250px; margin-right: 10px;">
<input name="description" type="textfield" value="${quota.description}" size=40"/>
</div>
<div style="clear: both"></div>
</div>
<div class="form-row">
<div style="float: left; width: 250px; margin-right: 10px;">
<input type="hidden" name="rename" value="submitted"/>
</div>
<div style="clear: both"></div>
</div>
<div class="form-row">
<div style="float: left; width: 250px; margin-right: 10px;">
<input type="hidden" name="id" value="${trans.security.encode_id( quota.id )}"/>
</div>
<div style="clear: both"></div>
</div>
<div class="form-row">
<input type="submit" name="rename_quota_button" value="Save"/>
</div>
</form>
</div>
</div>
@@ -0,0 +1,45 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%def name="javascripts()">
${parent.javascripts()}
<script type="text/javascript">
$(function(){
$("input:text:first").focus();
})
</script>
</%def>
<%
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
<div class="toolForm">
<div class="toolFormTitle">Create quota</div>
<div class="toolFormBody">
<form name="set_quota_default" id="set_quota_default" action="${h.url_for( action='set_quota_default' )}" method="post" >
<input name="webapp" type="hidden" value="${webapp}" size=40"/>
<input name="id" type="hidden" value="${trans.security.encode_id( quota.id )}"/>
<div class="form-row">
<label>Is this quota a default for a class of users (if yes, what type)?</label>
${default_selectfield.get_html()}
<div class="toolParamHelp" style="clear: both;">
Warning: Any users or groups associated with this quota will be disassociated.
</div>
</div>
<div class="form-row">
<input type="submit" name="set_default_quota_button" value="Save"/>
</div>
</form>
</div>
</div>
+53 -1
View File
@@ -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
</div>
%if over_quota:
<div id="quota-message-container">
%else:
<div id="quota-message-container" style="display: none;">
%endif
<div id="quota-message" class="errormessage">
You are over your disk quota. Tool execution is on hold until your disk usage drops below your allocated quota.
</div>
<br/>
</div>
%if not datasets:
<div class="infomessagesmall" id="emptyHistoryMessage">
+6 -1
View File
@@ -22,7 +22,12 @@
<li><a href="${h.url_for( controller='user', action='manage_user_info', cntrller=cntrller, webapp='community' )}">${_('Manage your information')}</a></li>
%endif
</ul>
<p>You are currently using <strong>${trans.user.get_disk_usage( nice_size=True )}</strong> of disk space in this Galaxy instance.</p>
<p>
You are using <strong>${trans.user.get_disk_usage( nice_size=True )}</strong> of disk space in this Galaxy instance.
%if trans.app.config.enable_quotas:
Your disk quota is: <strong>${trans.app.quota_agent.get_quota( trans.user, nice_size=True )}</strong>.
%endif
</p>
%else:
%if not message:
<p>${n_('You are currently not logged in.')}</p>
@@ -55,6 +55,7 @@
</div>
<div class="toolSectionBody">
<div class="toolSectionBg">
<div class="toolTitle"><a href="${h.url_for( controller='admin', action='quotas', webapp=webapp )}" target="galaxy_main">Manage quotas</a></div>
<div class="toolTitle"><a href="${h.url_for( controller='library_admin', action='browse_libraries' )}" target="galaxy_main">Manage data libraries</a></div>
</div>
</div>
+45
View File
@@ -3,6 +3,11 @@
## Default title
<%def name="title()">Galaxy</%def>
<%def name="javascripts()">
${parent.javascripts()}
${h.js( "jquery.tipsy" )}
</%def>
## Masthead
<%def name="masthead()">
@@ -172,5 +177,45 @@
%endif
</a>
</div>
## 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 )
%>
<div class="quota-meter-container">
%if tooltip:
<div id="quota-meter" class="quota-meter tooltip" title="${tooltip}">
%else:
<div id="quota-meter" class="quota-meter">
%endif
<div id="quota-meter-bar" class="${bar_style}" style="width: ${percent}px;"></div>
%if quota is not None:
<div id="quota-meter-text" class="quota-meter-text">Using ${percent}%</div>
%else:
<div id="quota-meter-text" class="quota-meter-text">Using ${util.nice_size( usage )}</div>
%endif
</div>
</div>
</%def>
+3
View File
@@ -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