mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
User/group disk quotas.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) )
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" ),
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%inherit file="/grid_base.mako"/>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user