mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #11315 from davelopez/prepare_fastapi_route_quotas
Prepare FastAPI route for quotas
This commit is contained in:
@@ -1,211 +0,0 @@
|
||||
"""
|
||||
Contains administrative functions
|
||||
"""
|
||||
import logging
|
||||
|
||||
from galaxy import util
|
||||
from galaxy.exceptions import ActionInputError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminActions:
|
||||
"""
|
||||
Mixin for controllers that provide administrative functionality.
|
||||
"""
|
||||
|
||||
def _create_quota(self, params, decode_id=None):
|
||||
if params.amount.lower() in ('unlimited', 'none', 'no limit'):
|
||||
create_amount = None
|
||||
else:
|
||||
try:
|
||||
create_amount = util.size_to_bytes(params.amount)
|
||||
except AssertionError:
|
||||
create_amount = False
|
||||
if not params.name or not params.description:
|
||||
raise ActionInputError("Enter a valid name and a description.")
|
||||
elif self.sa_session.query(self.app.model.Quota).filter(self.app.model.Quota.name == params.name).first():
|
||||
raise ActionInputError("Quota names must be unique and a quota with that name already exists, so choose another name.")
|
||||
elif not params.get('amount', None):
|
||||
raise ActionInputError("Enter a valid quota amount.")
|
||||
elif create_amount is False:
|
||||
raise ActionInputError("Unable to parse the provided amount.")
|
||||
elif params.operation not in self.app.model.Quota.valid_operations:
|
||||
raise ActionInputError("Enter a valid operation.")
|
||||
elif params.default != 'no' and params.default not in self.app.model.DefaultQuotaAssociation.types.__members__.values():
|
||||
raise ActionInputError("Enter a valid default type.")
|
||||
elif params.default != 'no' and params.operation != '=':
|
||||
raise ActionInputError("Operation for a default quota must be '='.")
|
||||
elif create_amount is None and params.operation != '=':
|
||||
raise ActionInputError("Operation for an unlimited quota must be '='.")
|
||||
else:
|
||||
# Create the quota
|
||||
quota = self.app.model.Quota(name=params.name, description=params.description, amount=create_amount, operation=params.operation)
|
||||
self.sa_session.add(quota)
|
||||
# If this is a default quota, create the DefaultQuotaAssociation
|
||||
if params.default != 'no':
|
||||
self.app.quota_agent.set_default_quota(params.default, quota)
|
||||
message = f"Default quota '{quota.name}' has been created."
|
||||
else:
|
||||
# Create the UserQuotaAssociations
|
||||
in_users = [self.sa_session.query(self.app.model.User).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_users)]
|
||||
in_groups = [self.sa_session.query(self.app.model.Group).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_groups)]
|
||||
if None in in_users:
|
||||
raise ActionInputError("One or more invalid user id has been provided.")
|
||||
for user in in_users:
|
||||
uqa = self.app.model.UserQuotaAssociation(user, quota)
|
||||
self.sa_session.add(uqa)
|
||||
# Create the GroupQuotaAssociations
|
||||
if None in in_groups:
|
||||
raise ActionInputError("One or more invalid group id has been provided.")
|
||||
for group in in_groups:
|
||||
gqa = self.app.model.GroupQuotaAssociation(group, quota)
|
||||
self.sa_session.add(gqa)
|
||||
message = "Quota '%s' has been created with %d associated users and %d associated groups." % (quota.name, len(in_users), len(in_groups))
|
||||
self.sa_session.flush()
|
||||
return quota, message
|
||||
|
||||
def _rename_quota(self, quota, params):
|
||||
if not params.name:
|
||||
raise ActionInputError('Enter a valid name.')
|
||||
elif params.name != quota.name and self.sa_session.query(self.app.model.Quota).filter(self.app.model.Quota.name == params.name).first():
|
||||
raise ActionInputError('A quota with that name already exists.')
|
||||
else:
|
||||
old_name = quota.name
|
||||
quota.name = params.name
|
||||
quota.description = params.description
|
||||
self.sa_session.add(quota)
|
||||
self.sa_session.flush()
|
||||
message = f"Quota '{old_name}' has been renamed to '{params.name}'."
|
||||
return message
|
||||
|
||||
def _manage_users_and_groups_for_quota(self, quota, params, decode_id=None):
|
||||
if quota.default:
|
||||
raise ActionInputError('Default quotas cannot be associated with specific users and groups.')
|
||||
else:
|
||||
in_users = [self.sa_session.query(self.app.model.User).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_users)]
|
||||
if None in in_users:
|
||||
raise ActionInputError("One or more invalid user id has been provided.")
|
||||
in_groups = [self.sa_session.query(self.app.model.Group).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_groups)]
|
||||
if None in in_groups:
|
||||
raise ActionInputError("One or more invalid group id has been provided.")
|
||||
self.app.quota_agent.set_entity_quota_associations(quotas=[quota], users=in_users, groups=in_groups)
|
||||
self.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))
|
||||
return message
|
||||
|
||||
def _edit_quota(self, quota, params):
|
||||
if params.amount.lower() in ('unlimited', 'none', 'no limit'):
|
||||
new_amount = None
|
||||
else:
|
||||
try:
|
||||
new_amount = util.size_to_bytes(params.amount)
|
||||
except (AssertionError, ValueError):
|
||||
new_amount = False
|
||||
if not params.amount:
|
||||
raise ActionInputError('Enter a valid amount.')
|
||||
elif new_amount is False:
|
||||
raise ActionInputError('Unable to parse the provided amount.')
|
||||
elif params.operation not in self.app.model.Quota.valid_operations:
|
||||
raise ActionInputError('Enter a valid operation.')
|
||||
else:
|
||||
quota.amount = new_amount
|
||||
quota.operation = params.operation
|
||||
self.sa_session.add(quota)
|
||||
self.sa_session.flush()
|
||||
message = f"Quota '{quota.name}' is now '{quota.operation + quota.display_amount}'."
|
||||
return message
|
||||
|
||||
def _set_quota_default(self, quota, params):
|
||||
if params.default != 'no' and params.default not in self.app.model.DefaultQuotaAssociation.types.__members__.values():
|
||||
raise ActionInputError('Enter a valid default type.')
|
||||
else:
|
||||
if params.default != 'no':
|
||||
self.app.quota_agent.set_default_quota(params.default, quota)
|
||||
message = f"Quota '{quota.name}' is now the default for {params.default} users."
|
||||
else:
|
||||
if quota.default:
|
||||
message = f"Quota '{quota.name}' is no longer the default for {quota.default[0].type} users."
|
||||
for dqa in quota.default:
|
||||
self.sa_session.delete(dqa)
|
||||
self.sa_session.flush()
|
||||
else:
|
||||
message = f"Quota '{quota.name}' is not a default."
|
||||
return message
|
||||
|
||||
def _unset_quota_default(self, quota, params=None):
|
||||
if not quota.default:
|
||||
raise ActionInputError(f"Quota '{quota.name}' is not a default.")
|
||||
else:
|
||||
message = f"Quota '{quota.name}' is no longer the default for {quota.default[0].type} users."
|
||||
for dqa in quota.default:
|
||||
self.sa_session.delete(dqa)
|
||||
self.sa_session.flush()
|
||||
return message
|
||||
|
||||
def _delete_quota(self, quota, params=None):
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if q.default:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' is a default, please unset it as a default before deleting it.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas are defaults, please unset them as defaults before deleting them: {', '.join(names)}")
|
||||
message = "Deleted %d quotas: " % len(quotas)
|
||||
for q in quotas:
|
||||
q.deleted = True
|
||||
self.sa_session.add(q)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
|
||||
def _undelete_quota(self, quota, params=None):
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if not q.deleted:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' has not been deleted, so it cannot be undeleted.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas have not been deleted so they cannot be undeleted: {', '.join(names)}")
|
||||
message = "Undeleted %d quotas: " % len(quotas)
|
||||
for q in quotas:
|
||||
q.deleted = False
|
||||
self.sa_session.add(q)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
|
||||
def _purge_quota(self, quota, params=None):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if not q.deleted:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' has not been deleted, so it cannot be purged.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas have not been deleted so they cannot be undeleted: {', '.join(names)}")
|
||||
message = "Purged %d quotas: " % len(quotas)
|
||||
for q in quotas:
|
||||
# Delete UserQuotaAssociations
|
||||
for uqa in q.users:
|
||||
self.sa_session.delete(uqa)
|
||||
# Delete GroupQuotaAssociations
|
||||
for gqa in q.groups:
|
||||
self.sa_session.delete(gqa)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
Manager and Serializers for Quotas.
|
||||
|
||||
For more information about quotas: https://galaxyproject.org/admin/disk-quotas/
|
||||
"""
|
||||
import logging
|
||||
from typing import (
|
||||
cast,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from sqlalchemy import (
|
||||
false,
|
||||
true
|
||||
)
|
||||
|
||||
from galaxy import model, util
|
||||
from galaxy.app import StructuredApp
|
||||
from galaxy.exceptions import ActionInputError
|
||||
from galaxy.managers import base
|
||||
from galaxy.managers.context import ProvidesUserContext
|
||||
from galaxy.quota import DatabaseQuotaAgent
|
||||
from galaxy.quota._schema import (
|
||||
CreateQuotaParams,
|
||||
CreateQuotaResult,
|
||||
DefaultQuotaValues,
|
||||
DeleteQuotaPayload,
|
||||
QuotaDetails,
|
||||
QuotaOperation,
|
||||
QuotaSummaryList,
|
||||
UpdateQuotaParams,
|
||||
)
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from galaxy.web import url_for
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotaManager:
|
||||
"""Interface/service object to interact with Quotas."""
|
||||
|
||||
def __init__(self, app: StructuredApp):
|
||||
self.app = app
|
||||
|
||||
@property
|
||||
def sa_session(self):
|
||||
return self.app.model.context
|
||||
|
||||
@property
|
||||
def quota_agent(self) -> DatabaseQuotaAgent:
|
||||
return cast(DatabaseQuotaAgent, self.app.quota_agent)
|
||||
|
||||
def create_quota(self, payload: dict, decode_id=None) -> Tuple[model.Quota, str]:
|
||||
params = CreateQuotaParams.parse_obj(payload)
|
||||
create_amount = self._parse_amount(params.amount)
|
||||
if self.sa_session.query(model.Quota).filter(model.Quota.name == params.name).first():
|
||||
raise ActionInputError("Quota names must be unique and a quota with that name already exists, please choose another name.")
|
||||
elif create_amount is False:
|
||||
raise ActionInputError("Unable to parse the provided amount.")
|
||||
elif params.operation not in model.Quota.valid_operations:
|
||||
raise ActionInputError("Enter a valid operation.")
|
||||
elif params.default != DefaultQuotaValues.NO and params.operation != QuotaOperation.EXACT:
|
||||
raise ActionInputError("Operation for a default quota must be '='.")
|
||||
elif create_amount is None and params.operation != QuotaOperation.EXACT:
|
||||
raise ActionInputError("Operation for an unlimited quota must be '='.")
|
||||
# Create the quota
|
||||
quota = model.Quota(name=params.name, description=params.description, amount=create_amount, operation=params.operation)
|
||||
self.sa_session.add(quota)
|
||||
# If this is a default quota, create the DefaultQuotaAssociation
|
||||
if params.default != DefaultQuotaValues.NO:
|
||||
self.quota_agent.set_default_quota(params.default, quota)
|
||||
message = f"Default quota '{quota.name}' has been created."
|
||||
else:
|
||||
# Create the UserQuotaAssociations
|
||||
in_users = [self.sa_session.query(model.User).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_users)]
|
||||
in_groups = [self.sa_session.query(model.Group).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_groups)]
|
||||
if None in in_users:
|
||||
raise ActionInputError("One or more invalid user id has been provided.")
|
||||
for user in in_users:
|
||||
uqa = model.UserQuotaAssociation(user, quota)
|
||||
self.sa_session.add(uqa)
|
||||
# Create the GroupQuotaAssociations
|
||||
if None in in_groups:
|
||||
raise ActionInputError("One or more invalid group id has been provided.")
|
||||
for group in in_groups:
|
||||
gqa = model.GroupQuotaAssociation(group, quota)
|
||||
self.sa_session.add(gqa)
|
||||
message = f"Quota '{quota.name}' has been created with {len(in_users)} associated users and {len(in_groups)} associated groups."
|
||||
self.sa_session.flush()
|
||||
return quota, message
|
||||
|
||||
def _parse_amount(self, amount: str) -> Optional[Union[int, bool]]:
|
||||
if amount.lower() in ('unlimited', 'none', 'no limit'):
|
||||
return None
|
||||
try:
|
||||
return util.size_to_bytes(amount)
|
||||
except AssertionError:
|
||||
return False
|
||||
|
||||
def rename_quota(self, quota, params) -> str:
|
||||
if not params.name:
|
||||
raise ActionInputError('Enter a valid name.')
|
||||
elif params.name != quota.name and self.sa_session.query(model.Quota).filter(model.Quota.name == params.name).first():
|
||||
raise ActionInputError('A quota with that name already exists.')
|
||||
else:
|
||||
old_name = quota.name
|
||||
quota.name = params.name
|
||||
if params.description:
|
||||
quota.description = params.description
|
||||
self.sa_session.add(quota)
|
||||
self.sa_session.flush()
|
||||
message = f"Quota '{old_name}' has been renamed to '{params.name}'."
|
||||
return message
|
||||
|
||||
def manage_users_and_groups_for_quota(self, quota, params, decode_id=None) -> str:
|
||||
if quota.default:
|
||||
raise ActionInputError('Default quotas cannot be associated with specific users and groups.')
|
||||
else:
|
||||
in_users = [self.sa_session.query(model.User).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_users)]
|
||||
if None in in_users:
|
||||
raise ActionInputError("One or more invalid user id has been provided.")
|
||||
in_groups = [self.sa_session.query(model.Group).get(decode_id(x) if decode_id else x) for x in util.listify(params.in_groups)]
|
||||
if None in in_groups:
|
||||
raise ActionInputError("One or more invalid group id has been provided.")
|
||||
self.quota_agent.set_entity_quota_associations(quotas=[quota], users=in_users, groups=in_groups)
|
||||
self.sa_session.refresh(quota)
|
||||
message = f"Quota '{quota.name}' has been updated with {len(in_users)} associated users and {len(in_groups)} associated groups."
|
||||
return message
|
||||
|
||||
def edit_quota(self, quota, params) -> str:
|
||||
if params.amount.lower() in ('unlimited', 'none', 'no limit'):
|
||||
new_amount = None
|
||||
else:
|
||||
try:
|
||||
new_amount = util.size_to_bytes(params.amount)
|
||||
except (AssertionError, ValueError):
|
||||
new_amount = False
|
||||
if not params.amount:
|
||||
raise ActionInputError('Enter a valid amount.')
|
||||
elif new_amount is False:
|
||||
raise ActionInputError('Unable to parse the provided amount.')
|
||||
elif params.operation not in model.Quota.valid_operations:
|
||||
raise ActionInputError('Enter a valid operation.')
|
||||
else:
|
||||
quota.amount = new_amount
|
||||
quota.operation = params.operation
|
||||
self.sa_session.add(quota)
|
||||
self.sa_session.flush()
|
||||
message = f"Quota '{quota.name}' is now '{quota.operation}{quota.display_amount}'."
|
||||
return message
|
||||
|
||||
def set_quota_default(self, quota, params) -> str:
|
||||
if params.default != 'no' and params.default not in model.DefaultQuotaAssociation.types.__members__.values():
|
||||
raise ActionInputError('Enter a valid default type.')
|
||||
else:
|
||||
if params.default != 'no':
|
||||
self.quota_agent.set_default_quota(params.default, quota)
|
||||
message = f"Quota '{quota.name}' is now the default for {params.default} users."
|
||||
else:
|
||||
if quota.default:
|
||||
message = f"Quota '{quota.name}' is no longer the default for {quota.default[0].type} users."
|
||||
for dqa in quota.default:
|
||||
self.sa_session.delete(dqa)
|
||||
self.sa_session.flush()
|
||||
else:
|
||||
message = f"Quota '{quota.name}' is not a default."
|
||||
return message
|
||||
|
||||
def unset_quota_default(self, quota, params=None) -> str:
|
||||
if not quota.default:
|
||||
raise ActionInputError(f"Quota '{quota.name}' is not a default.")
|
||||
else:
|
||||
message = f"Quota '{quota.name}' is no longer the default for {quota.default[0].type} users."
|
||||
for dqa in quota.default:
|
||||
self.sa_session.delete(dqa)
|
||||
self.sa_session.flush()
|
||||
return message
|
||||
|
||||
def delete_quota(self, quota, params=None) -> str:
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if q.default:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' is a default, please unset it as a default before deleting it.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas are defaults, please unset them as defaults before deleting them: {', '.join(names)}")
|
||||
message = f"Deleted {len(quotas)} quotas: "
|
||||
for q in quotas:
|
||||
q.deleted = True
|
||||
self.sa_session.add(q)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
|
||||
def undelete_quota(self, quota, params=None) -> str:
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if not q.deleted:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' has not been deleted, so it cannot be undeleted.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas have not been deleted so they cannot be undeleted: {', '.join(names)}")
|
||||
message = f"Undeleted {len(quotas)} quotas: "
|
||||
for q in quotas:
|
||||
q.deleted = False
|
||||
self.sa_session.add(q)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
|
||||
def purge_quota(self, quota, params=None):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
quotas = util.listify(quota)
|
||||
names = []
|
||||
for q in quotas:
|
||||
if not q.deleted:
|
||||
names.append(q.name)
|
||||
if len(names) == 1:
|
||||
raise ActionInputError(f"Quota '{names[0]}' has not been deleted, so it cannot be purged.")
|
||||
elif len(names) > 1:
|
||||
raise ActionInputError(f"Quotas have not been deleted so they cannot be undeleted: {', '.join(names)}")
|
||||
message = f"Purged {len(quotas)} quotas: "
|
||||
for q in quotas:
|
||||
# Delete UserQuotaAssociations
|
||||
for uqa in q.users:
|
||||
self.sa_session.delete(uqa)
|
||||
# Delete GroupQuotaAssociations
|
||||
for gqa in q.groups:
|
||||
self.sa_session.delete(gqa)
|
||||
names.append(q.name)
|
||||
self.sa_session.flush()
|
||||
message += ', '.join(names)
|
||||
return message
|
||||
|
||||
def get_quota(self, trans, id: EncodedDatabaseIdField, deleted: Optional[bool] = None) -> model.Quota:
|
||||
return base.get_object(trans, id, 'Quota', check_ownership=False, check_accessible=False, deleted=deleted)
|
||||
|
||||
|
||||
class QuotasService:
|
||||
"""Interface/service object shared by controllers for interacting with quotas."""
|
||||
|
||||
def __init__(self, app: StructuredApp):
|
||||
self.quota_manager: QuotaManager = QuotaManager(app)
|
||||
|
||||
def index(self, trans: ProvidesUserContext, deleted: bool = False) -> QuotaSummaryList:
|
||||
"""Displays a collection (list) of quotas."""
|
||||
rval = []
|
||||
query = trans.sa_session.query(model.Quota)
|
||||
if deleted:
|
||||
route = 'deleted_quota'
|
||||
query = query.filter(model.Quota.deleted == true())
|
||||
else:
|
||||
route = 'quota'
|
||||
query = query.filter(model.Quota.deleted == false())
|
||||
for quota in query:
|
||||
item = quota.to_dict(value_mapper={'id': trans.security.encode_id})
|
||||
encoded_id = trans.security.encode_id(quota.id)
|
||||
item['url'] = self._url_for(route, id=encoded_id)
|
||||
rval.append(item)
|
||||
return QuotaSummaryList.parse_obj(rval)
|
||||
|
||||
def show(self, trans: ProvidesUserContext, id: EncodedDatabaseIdField, deleted: bool = False) -> QuotaDetails:
|
||||
"""Displays information about a quota."""
|
||||
quota = self.quota_manager.get_quota(trans, id, deleted=deleted)
|
||||
rval = quota.to_dict(view='element', value_mapper={'id': trans.security.encode_id, 'total_disk_usage': float})
|
||||
return QuotaDetails.parse_obj(rval)
|
||||
|
||||
def create(self, trans: ProvidesUserContext, params: CreateQuotaParams) -> CreateQuotaResult:
|
||||
"""Creates a new quota."""
|
||||
payload = params.dict()
|
||||
self.validate_in_users_and_groups(trans, payload)
|
||||
quota, message = self.quota_manager.create_quota(payload)
|
||||
item = quota.to_dict(value_mapper={'id': trans.security.encode_id})
|
||||
item['url'] = self._url_for('quota', id=trans.security.encode_id(quota.id))
|
||||
item['message'] = message
|
||||
return CreateQuotaResult.parse_obj(item)
|
||||
|
||||
def update(self, trans: ProvidesUserContext, id: EncodedDatabaseIdField, params: UpdateQuotaParams) -> str:
|
||||
"""Modifies a quota."""
|
||||
payload = params.dict()
|
||||
self.validate_in_users_and_groups(trans, payload)
|
||||
quota = self.quota_manager.get_quota(trans, id, deleted=False)
|
||||
|
||||
params = UpdateQuotaParams(**payload)
|
||||
# FIXME: Doing it this way makes the update non-atomic if a method fails after an earlier one has succeeded.
|
||||
methods = []
|
||||
if params.name or params.description:
|
||||
methods.append(self.quota_manager.rename_quota)
|
||||
if params.amount:
|
||||
methods.append(self.quota_manager.edit_quota)
|
||||
if params.default == DefaultQuotaValues.NO:
|
||||
methods.append(self.quota_manager.unset_quota_default)
|
||||
elif params.default:
|
||||
methods.append(self.quota_manager.set_quota_default)
|
||||
if params.in_users or params.in_groups:
|
||||
methods.append(self.quota_manager.manage_users_and_groups_for_quota)
|
||||
|
||||
messages = []
|
||||
for method in methods:
|
||||
message = method(quota, params)
|
||||
messages.append(message)
|
||||
return '; '.join(messages)
|
||||
|
||||
def delete(self, trans: ProvidesUserContext, id: EncodedDatabaseIdField, payload: Optional[DeleteQuotaPayload] = None) -> str:
|
||||
"""Marks a quota as deleted."""
|
||||
quota = self.quota_manager.get_quota(trans, id, deleted=False) # deleted quotas are not technically members of this collection
|
||||
message = self.quota_manager.delete_quota(quota)
|
||||
if payload and payload.purge:
|
||||
message += self.quota_manager.purge_quota(quota)
|
||||
return message
|
||||
|
||||
def undelete(self, trans: ProvidesUserContext, id: EncodedDatabaseIdField) -> str:
|
||||
"""Restores a previously deleted quota."""
|
||||
quota = self.quota_manager.get_quota(trans, id, deleted=True)
|
||||
return self.quota_manager.undelete_quota(quota)
|
||||
|
||||
def validate_in_users_and_groups(self, trans, payload):
|
||||
"""
|
||||
For convenience, in_users and in_groups can be encoded IDs or emails/group names in the API.
|
||||
"""
|
||||
def get_id(item, model_class, column):
|
||||
try:
|
||||
return trans.security.decode_id(item)
|
||||
except Exception:
|
||||
pass # maybe an email/group name
|
||||
# this will raise if the item is invalid
|
||||
return trans.sa_session.query(model_class).filter(column == item).first().id
|
||||
new_in_users = []
|
||||
new_in_groups = []
|
||||
invalid = []
|
||||
for item in util.listify(payload.get('in_users', [])):
|
||||
try:
|
||||
new_in_users.append(get_id(item, model.User, model.User.email))
|
||||
except Exception:
|
||||
invalid.append(item)
|
||||
for item in util.listify(payload.get('in_groups', [])):
|
||||
try:
|
||||
new_in_groups.append(get_id(item, model.Group, model.Group.name))
|
||||
except Exception:
|
||||
invalid.append(item)
|
||||
if invalid:
|
||||
msg = f"The following value(s) for associated users and/or groups could not be parsed: {', '.join(invalid)}."
|
||||
msg += " Valid values are email addresses of users, names of groups, or IDs of both."
|
||||
raise Exception(msg)
|
||||
payload['in_users'] = list(map(str, new_in_users))
|
||||
payload['in_groups'] = list(map(str, new_in_groups))
|
||||
|
||||
def _url_for(self, *args, **kargs):
|
||||
try:
|
||||
return url_for(*args, **kargs)
|
||||
except AttributeError:
|
||||
return "*deprecated attribute not filled in by FastAPI server*"
|
||||
@@ -0,0 +1,247 @@
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
)
|
||||
|
||||
from galaxy.schema.fields import (
|
||||
EncodedDatabaseIdField,
|
||||
ModelClassField,
|
||||
)
|
||||
from galaxy.schema.schema import (
|
||||
GroupModel,
|
||||
UserModel,
|
||||
)
|
||||
|
||||
QUOTA_MODEL_CLASS_NAME = "Quota"
|
||||
USER_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "UserQuotaAssociation"
|
||||
GROUP_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "GroupQuotaAssociation"
|
||||
DEFAULT_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "DefaultQuotaAssociation"
|
||||
|
||||
|
||||
class QuotaOperation(str, Enum):
|
||||
EXACT = "="
|
||||
ADD = "+"
|
||||
SUBTRACT = "-"
|
||||
|
||||
|
||||
class DefaultQuotaTypes(str, Enum): # TODO: should this replace lib.galaxy.model.DefaultQuotaAssociation.types at some point?
|
||||
UNREGISTERED = 'unregistered'
|
||||
REGISTERED = 'registered'
|
||||
|
||||
|
||||
class DefaultQuotaValues(str, Enum):
|
||||
UNREGISTERED = 'unregistered'
|
||||
REGISTERED = 'registered'
|
||||
NO = "no"
|
||||
|
||||
|
||||
QuotaNameField = Field(
|
||||
...,
|
||||
title="Name",
|
||||
description="The name of the quota. This must be unique within a Galaxy instance.",
|
||||
)
|
||||
|
||||
QuotaDescriptionField = Field(
|
||||
...,
|
||||
title="Description",
|
||||
description="Detailed text description for this Quota.",
|
||||
)
|
||||
|
||||
QuotaOperationField = Field(
|
||||
QuotaOperation.EXACT,
|
||||
title="Operation",
|
||||
description=(
|
||||
"Quotas can have one of three `operations`:"
|
||||
"- `=` : The quota is exactly the amount specified"
|
||||
"- `+` : The amount specified will be added to the amounts of the user's other associated quota definitions"
|
||||
"- `-` : The amount specified will be subtracted from the amounts of the user's other associated quota definitions"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DefaultQuota(BaseModel): # TODO: should this replace lib.galaxy.model.DefaultQuotaAssociation at some point?
|
||||
model_class: str = ModelClassField(DEFAULT_QUOTA_ASSOCIATION_MODEL_CLASS_NAME)
|
||||
type: DefaultQuotaTypes = Field(
|
||||
...,
|
||||
title="Type",
|
||||
description=(
|
||||
"The type of the default quota. Either one of:\n"
|
||||
" - `registered`: the associated quota will affect registered users.\n"
|
||||
" - `unregistered`: the associated quota will affect unregistered users.\n"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class UserQuota(BaseModel):
|
||||
model_class: str = ModelClassField(USER_QUOTA_ASSOCIATION_MODEL_CLASS_NAME)
|
||||
user: UserModel = Field(
|
||||
...,
|
||||
title="User",
|
||||
description="Information about a user associated with a quota.",
|
||||
)
|
||||
|
||||
|
||||
class GroupQuota(BaseModel):
|
||||
model_class: str = ModelClassField(GROUP_QUOTA_ASSOCIATION_MODEL_CLASS_NAME)
|
||||
group: GroupModel = Field(
|
||||
...,
|
||||
title="Group",
|
||||
description="Information about a user group associated with a quota.",
|
||||
)
|
||||
|
||||
|
||||
class QuotaBase(BaseModel):
|
||||
"""Base model containing common fields for Quotas."""
|
||||
model_class: str = ModelClassField(QUOTA_MODEL_CLASS_NAME)
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
...,
|
||||
title="ID",
|
||||
description="The `encoded identifier` of the quota.",
|
||||
)
|
||||
name: str = QuotaNameField
|
||||
|
||||
|
||||
class QuotaSummary(QuotaBase):
|
||||
"""Contains basic information about a Quota"""
|
||||
url: str = Field(
|
||||
...,
|
||||
title="URL",
|
||||
description="The relative URL to get this particular Quota details from the rest API.",
|
||||
deprecated=True,
|
||||
)
|
||||
|
||||
|
||||
class QuotaSummaryList(BaseModel):
|
||||
__root__: List[QuotaSummary] = Field(
|
||||
default=[],
|
||||
title='List with summary information of Quotas.',
|
||||
)
|
||||
|
||||
|
||||
class QuotaDetails(QuotaBase):
|
||||
description: str = QuotaDescriptionField
|
||||
bytes: str = Field(
|
||||
...,
|
||||
title="Bytes",
|
||||
description="The amount, expressed in bytes, of this Quota.",
|
||||
)
|
||||
operation: QuotaOperation = QuotaOperationField
|
||||
display_amount: str = Field(
|
||||
...,
|
||||
title="Display Amount",
|
||||
description="Human-readable representation of the `amount` field.",
|
||||
)
|
||||
default: List[DefaultQuota] = Field(
|
||||
[],
|
||||
title="Default",
|
||||
description="A list indicating which types of default user quotas, if any, are associated with this quota.",
|
||||
)
|
||||
users: List[UserQuota] = Field(
|
||||
[],
|
||||
title="Users",
|
||||
description="A list of specific users associated with this quota.",
|
||||
)
|
||||
groups: List[GroupQuota] = Field(
|
||||
[],
|
||||
title="Groups",
|
||||
description="A list of specific groups of users associated with this quota.",
|
||||
)
|
||||
|
||||
|
||||
class CreateQuotaResult(QuotaSummary):
|
||||
message: str = Field(
|
||||
...,
|
||||
title="Message",
|
||||
description="Text message describing the result of the operation.",
|
||||
)
|
||||
|
||||
|
||||
class CreateQuotaParams(BaseModel):
|
||||
name: str = QuotaNameField
|
||||
description: str = QuotaDescriptionField
|
||||
amount: str = Field(
|
||||
...,
|
||||
title="Amount",
|
||||
description="Quota size (E.g. ``10000MB``, ``99 gb``, ``0.2T``, ``unlimited``)",
|
||||
)
|
||||
operation: QuotaOperation = QuotaOperationField
|
||||
default: DefaultQuotaValues = Field(
|
||||
default=DefaultQuotaValues.NO,
|
||||
title="Default",
|
||||
description=(
|
||||
"Whether or not this is a default quota. Valid values"
|
||||
" are ``no``, ``unregistered``, ``registered``. None is"
|
||||
" equivalent to ``no``."
|
||||
),
|
||||
)
|
||||
in_users: Optional[List[str]] = Field(
|
||||
default=[],
|
||||
title="Users",
|
||||
description="A list of user IDs or user emails to associate with this quota.",
|
||||
)
|
||||
in_groups: Optional[List[str]] = Field(
|
||||
default=[],
|
||||
title="Groups",
|
||||
description="A list of group IDs or names to associate with this quota.",
|
||||
)
|
||||
|
||||
|
||||
class UpdateQuotaParams(BaseModel):
|
||||
name: Optional[str] = Field(
|
||||
default=None,
|
||||
title="Name",
|
||||
description="The new name of the quota. This must be unique within a Galaxy instance.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
None,
|
||||
title="Description",
|
||||
description="Detailed text description for this Quota.",
|
||||
)
|
||||
amount: Optional[str] = Field(
|
||||
None,
|
||||
title="Amount",
|
||||
description="Quota size (E.g. ``10000MB``, ``99 gb``, ``0.2T``, ``unlimited``)",
|
||||
)
|
||||
operation: QuotaOperation = Field(
|
||||
QuotaOperation.EXACT,
|
||||
title="Operation",
|
||||
description=(
|
||||
"One of (``+``, ``-``, ``=``). If you wish to change this value,"
|
||||
" you must also provide the ``amount``, otherwise it will not take effect."
|
||||
)
|
||||
)
|
||||
default: Optional[DefaultQuotaValues] = Field(
|
||||
default=None,
|
||||
title="Default",
|
||||
description=(
|
||||
"Whether or not this is a default quota. Valid values"
|
||||
" are ``no``, ``unregistered``, ``registered``."
|
||||
' Calling this method with ``default="no"`` on a'
|
||||
" non-default quota will throw an error. Not"
|
||||
" passing this parameter is equivalent to passing ``no``."
|
||||
),
|
||||
)
|
||||
in_users: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
title="Users",
|
||||
description="A list of user IDs or user emails to associate with this quota.",
|
||||
)
|
||||
in_groups: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
title="Groups",
|
||||
description="A list of group IDs or names to associate with this quota.",
|
||||
)
|
||||
|
||||
|
||||
class DeleteQuotaPayload(BaseModel):
|
||||
purge: bool = Field(
|
||||
False,
|
||||
title="Purge",
|
||||
description="Whether to also purge the Quota after deleting it.",
|
||||
)
|
||||
@@ -36,6 +36,7 @@ from galaxy.schema.fields import (
|
||||
from galaxy.schema.types import RelativeUrl
|
||||
|
||||
USER_MODEL_CLASS_NAME = "User"
|
||||
GROUP_MODEL_CLASS_NAME = "Group"
|
||||
HDA_MODEL_CLASS_NAME = "HistoryDatasetAssociation"
|
||||
DC_MODEL_CLASS_NAME = "DatasetCollection"
|
||||
DCE_MODEL_CLASS_NAME = "DatasetCollectionElement"
|
||||
@@ -180,6 +181,21 @@ class UserModel(Model):
|
||||
model_class: str = ModelClassField(USER_MODEL_CLASS_NAME)
|
||||
|
||||
|
||||
class GroupModel(BaseModel):
|
||||
"""User group model"""
|
||||
model_class: str = ModelClassField(GROUP_MODEL_CLASS_NAME)
|
||||
id: EncodedDatabaseIdField = Field(
|
||||
..., # Required
|
||||
title='ID',
|
||||
description='Encoded group ID',
|
||||
)
|
||||
name: str = Field(
|
||||
..., # Required
|
||||
title="Name",
|
||||
description="The name of the group.",
|
||||
)
|
||||
|
||||
|
||||
class JobSourceType(str, Enum):
|
||||
"""Available types of job sources (model classes) that produce dataset collections."""
|
||||
Job = "Job"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Mixins for parsing web form and API parameters
|
||||
"""
|
||||
from galaxy import util
|
||||
|
||||
|
||||
class BaseParamParser:
|
||||
def get_params(self, kwargs):
|
||||
params = util.Params(kwargs)
|
||||
# set defaults if unset
|
||||
updates = dict(webapp=params.get('webapp', 'galaxy'),
|
||||
message=util.restore_text(params.get('message', '')),
|
||||
status=util.restore_text(params.get('status', 'done')))
|
||||
params.update(updates)
|
||||
return params
|
||||
|
||||
|
||||
class QuotaParamParser(BaseParamParser):
|
||||
def get_quota_params(self, kwargs):
|
||||
params = self.get_params(kwargs)
|
||||
updates = dict(name=util.restore_text(params.get('name', '')),
|
||||
description=util.restore_text(params.get('description', '')),
|
||||
amount=util.restore_text(params.get('amount', '').strip()),
|
||||
operation=params.get('operation', ''),
|
||||
default=params.get('default', ''),
|
||||
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', [])))
|
||||
params.update(updates)
|
||||
return params
|
||||
@@ -191,37 +191,6 @@ class BaseAPIController(BaseController):
|
||||
log.exception("Exception in get_object check for %s %s.", class_name, str(id))
|
||||
raise HTTPInternalServerError(comment=util.unicodify(e))
|
||||
|
||||
def validate_in_users_and_groups(self, trans, payload):
|
||||
"""
|
||||
For convenience, in_users and in_groups can be encoded IDs or emails/group names in the API.
|
||||
"""
|
||||
def get_id(item, model_class, column):
|
||||
try:
|
||||
return trans.security.decode_id(item)
|
||||
except Exception:
|
||||
pass # maybe an email/group name
|
||||
# this will raise if the item is invalid
|
||||
return trans.sa_session.query(model_class).filter(column == item).first().id
|
||||
new_in_users = []
|
||||
new_in_groups = []
|
||||
invalid = []
|
||||
for item in util.listify(payload.get('in_users', [])):
|
||||
try:
|
||||
new_in_users.append(get_id(item, trans.app.model.User, trans.app.model.User.table.c.email))
|
||||
except Exception:
|
||||
invalid.append(item)
|
||||
for item in util.listify(payload.get('in_groups', [])):
|
||||
try:
|
||||
new_in_groups.append(get_id(item, trans.app.model.Group, trans.app.model.Group.name))
|
||||
except Exception:
|
||||
invalid.append(item)
|
||||
if invalid:
|
||||
msg = f"The following value(s) for associated users and/or groups could not be parsed: {', '.join(invalid)}."
|
||||
msg += " Valid values are email addresses of users, names of groups, or IDs of both."
|
||||
raise Exception(msg)
|
||||
payload['in_users'] = list(map(str, new_in_users))
|
||||
payload['in_groups'] = list(map(str, new_in_groups))
|
||||
|
||||
def not_implemented(self, trans, **kwd):
|
||||
raise HTTPNotImplemented()
|
||||
|
||||
@@ -1346,12 +1315,6 @@ class SharableMixin:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class UsesQuotaMixin:
|
||||
|
||||
def get_quota(self, trans, id, check_ownership=False, check_accessible=False, deleted=None):
|
||||
return self.get_object(trans, id, 'Quota', check_ownership=False, check_accessible=False, deleted=deleted)
|
||||
|
||||
|
||||
class UsesTagsMixin(SharableItemSecurityMixin):
|
||||
|
||||
def get_tag_handler(self, trans) -> tags.GalaxyTagHandler:
|
||||
|
||||
@@ -3,155 +3,215 @@ API operations on Quota objects.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from paste.httpexceptions import HTTPBadRequest
|
||||
from sqlalchemy import (
|
||||
false,
|
||||
true
|
||||
)
|
||||
from fastapi import Path
|
||||
from fastapi.param_functions import Body
|
||||
|
||||
from galaxy import (
|
||||
util,
|
||||
web
|
||||
web,
|
||||
)
|
||||
from galaxy.actions.admin import AdminActions
|
||||
from galaxy.exceptions import ActionInputError
|
||||
from galaxy.web.params import QuotaParamParser
|
||||
from galaxy.webapps.base.controller import (
|
||||
url_for,
|
||||
UsesQuotaMixin
|
||||
from galaxy.managers.context import ProvidesUserContext
|
||||
from galaxy.managers.quotas import QuotasService
|
||||
from galaxy.quota._schema import (
|
||||
CreateQuotaParams,
|
||||
CreateQuotaResult,
|
||||
DeleteQuotaPayload,
|
||||
QuotaDetails,
|
||||
QuotaSummaryList,
|
||||
UpdateQuotaParams,
|
||||
)
|
||||
from galaxy.schema.fields import EncodedDatabaseIdField
|
||||
from . import (
|
||||
BaseGalaxyAPIController,
|
||||
depends,
|
||||
DependsOnTrans,
|
||||
Router,
|
||||
)
|
||||
from . import BaseGalaxyAPIController
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotaAPIController(BaseGalaxyAPIController, AdminActions, UsesQuotaMixin, QuotaParamParser):
|
||||
router = Router(tags=['quotas'])
|
||||
|
||||
|
||||
QuotaIdPathParam: EncodedDatabaseIdField = Path(
|
||||
..., # Required
|
||||
title="Quota ID",
|
||||
description="The encoded identifier of the Quota."
|
||||
)
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIQuota:
|
||||
service: QuotasService = depends(QuotasService)
|
||||
|
||||
@router.get(
|
||||
'/api/quotas',
|
||||
summary="Displays a list with information of quotas that are currently active.",
|
||||
require_admin=True,
|
||||
)
|
||||
def index(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
) -> QuotaSummaryList:
|
||||
"""Displays a list with information of quotas that are currently active."""
|
||||
return self.service.index(trans)
|
||||
|
||||
@router.get(
|
||||
'/api/quotas/deleted',
|
||||
summary="Displays a list with information of quotas that have been deleted.",
|
||||
require_admin=True,
|
||||
)
|
||||
def index_deleted(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
) -> QuotaSummaryList:
|
||||
"""Displays a list with information of quotas that have been deleted."""
|
||||
return self.service.index(trans, deleted=True)
|
||||
|
||||
@router.get(
|
||||
'/api/quotas/{id}',
|
||||
summary="Displays details on a particular active quota.",
|
||||
require_admin=True,
|
||||
)
|
||||
def show(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = QuotaIdPathParam
|
||||
) -> QuotaDetails:
|
||||
"""Displays details on a particular active quota."""
|
||||
return self.service.show(trans, id)
|
||||
|
||||
@router.get(
|
||||
'/api/quotas/deleted/{id}',
|
||||
summary="Displays details on a particular quota that has been deleted.",
|
||||
require_admin=True,
|
||||
)
|
||||
def show_deleted(
|
||||
self,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
id: EncodedDatabaseIdField = QuotaIdPathParam,
|
||||
) -> QuotaDetails:
|
||||
"""Displays details on a particular quota that has been deleted."""
|
||||
return self.service.show(trans, id, deleted=True)
|
||||
|
||||
@router.post(
|
||||
'/api/quotas',
|
||||
summary="Creates a new quota.",
|
||||
require_admin=True,
|
||||
)
|
||||
def create(
|
||||
self,
|
||||
payload: CreateQuotaParams,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
) -> CreateQuotaResult:
|
||||
"""Creates a new quota."""
|
||||
return self.service.create(trans, payload)
|
||||
|
||||
@router.put(
|
||||
'/api/quotas/{id}',
|
||||
summary="Updates an existing quota.",
|
||||
require_admin=True,
|
||||
)
|
||||
def update(
|
||||
self,
|
||||
payload: UpdateQuotaParams,
|
||||
id: EncodedDatabaseIdField = QuotaIdPathParam,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
) -> str:
|
||||
"""Updates an existing quota."""
|
||||
return self.service.update(trans, id, payload)
|
||||
|
||||
@router.delete(
|
||||
'/api/quotas/{id}',
|
||||
summary="Deletes an existing quota.",
|
||||
require_admin=True,
|
||||
)
|
||||
def delete(
|
||||
self,
|
||||
id: EncodedDatabaseIdField = QuotaIdPathParam,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
payload: DeleteQuotaPayload = Body(None), # Optional
|
||||
) -> str:
|
||||
"""Deletes an existing quota."""
|
||||
return self.service.delete(trans, id, payload)
|
||||
|
||||
@router.post(
|
||||
'/api/quotas/deleted/{id}/undelete',
|
||||
summary="Restores a previously deleted quota.",
|
||||
require_admin=True,
|
||||
)
|
||||
def undelete(
|
||||
self,
|
||||
id: EncodedDatabaseIdField = QuotaIdPathParam,
|
||||
trans: ProvidesUserContext = DependsOnTrans,
|
||||
) -> str:
|
||||
"""Restores a previously deleted quota."""
|
||||
return self.service.undelete(trans, id)
|
||||
|
||||
|
||||
class QuotaAPIController(BaseGalaxyAPIController):
|
||||
|
||||
service: QuotasService = depends(QuotasService)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def index(self, trans, deleted='False', **kwd):
|
||||
"""
|
||||
GET /api/quotas
|
||||
GET /api/quotas/deleted
|
||||
Displays a collection (list) of quotas.
|
||||
"""
|
||||
rval = []
|
||||
deleted = util.string_as_bool(deleted)
|
||||
query = trans.sa_session.query(trans.app.model.Quota)
|
||||
if deleted:
|
||||
route = 'deleted_quota'
|
||||
query = query.filter(trans.app.model.Quota.deleted == true())
|
||||
else:
|
||||
route = 'quota'
|
||||
query = query.filter(trans.app.model.Quota.deleted == false())
|
||||
for quota in query:
|
||||
item = quota.to_dict(value_mapper={'id': trans.security.encode_id})
|
||||
encoded_id = trans.security.encode_id(quota.id)
|
||||
item['url'] = url_for(route, id=encoded_id)
|
||||
rval.append(item)
|
||||
return rval
|
||||
return self.service.index(trans, deleted)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def show(self, trans, id, deleted='False', **kwd):
|
||||
"""
|
||||
GET /api/quotas/{encoded_quota_id}
|
||||
GET /api/quotas/deleted/{encoded_quota_id}
|
||||
Displays information about a quota.
|
||||
"""
|
||||
quota = self.get_quota(trans, id, deleted=util.string_as_bool(deleted))
|
||||
return quota.to_dict(view='element', value_mapper={'id': trans.security.encode_id, 'total_disk_usage': float})
|
||||
deleted = util.string_as_bool(deleted)
|
||||
return self.service.show(trans, id, deleted)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def create(self, trans, payload, **kwd):
|
||||
"""
|
||||
POST /api/quotas
|
||||
Creates a new quota.
|
||||
"""
|
||||
try:
|
||||
self.validate_in_users_and_groups(trans, payload)
|
||||
except Exception as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
params = self.get_quota_params(payload)
|
||||
try:
|
||||
quota, message = self._create_quota(params)
|
||||
except ActionInputError as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
item = quota.to_dict(value_mapper={'id': trans.security.encode_id})
|
||||
item['url'] = url_for('quota', id=trans.security.encode_id(quota.id))
|
||||
item['message'] = message
|
||||
return item
|
||||
params = CreateQuotaParams(**payload)
|
||||
return self.service.create(trans, params)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def update(self, trans, id, payload, **kwd):
|
||||
"""
|
||||
PUT /api/quotas/{encoded_quota_id}
|
||||
Modifies a quota.
|
||||
"""
|
||||
try:
|
||||
self.validate_in_users_and_groups(trans, payload)
|
||||
except Exception as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
|
||||
quota = self.get_quota(trans, id, deleted=False)
|
||||
|
||||
# FIXME: Doing it this way makes the update non-atomic if a method fails after an earlier one has succeeded.
|
||||
payload['id'] = id
|
||||
params = self.get_quota_params(payload)
|
||||
methods = []
|
||||
if payload.get('name', None) or payload.get('description', None):
|
||||
methods.append(self._rename_quota)
|
||||
if payload.get('amount', None):
|
||||
methods.append(self._edit_quota)
|
||||
if payload.get('default', None) == 'no':
|
||||
methods.append(self._unset_quota_default)
|
||||
elif payload.get('default', None):
|
||||
methods.append(self._set_quota_default)
|
||||
if payload.get('in_users', None) or payload.get('in_groups', None):
|
||||
methods.append(self._manage_users_and_groups_for_quota)
|
||||
|
||||
messages = []
|
||||
for method in methods:
|
||||
try:
|
||||
message = method(quota, params)
|
||||
except ActionInputError as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
messages.append(message)
|
||||
return '; '.join(messages)
|
||||
params = UpdateQuotaParams(**payload)
|
||||
return self.service.update(trans, id, params)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def delete(self, trans, id, **kwd):
|
||||
"""
|
||||
DELETE /api/quotas/{encoded_quota_id}
|
||||
Deletes a quota
|
||||
"""
|
||||
quota = self.get_quota(trans, id, deleted=False) # deleted quotas are not technically members of this collection
|
||||
|
||||
# a request body is optional here
|
||||
payload = kwd.get('payload', {})
|
||||
payload['id'] = id
|
||||
params = self.get_quota_params(payload)
|
||||
|
||||
try:
|
||||
message = self._delete_quota(quota, params)
|
||||
if util.string_as_bool(payload.get('purge', False)):
|
||||
message += self._purge_quota(quota, params)
|
||||
except ActionInputError as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
return message
|
||||
payload = DeleteQuotaPayload(**kwd.get('payload', {}))
|
||||
return self.service.delete(trans, id, payload)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
@web.expose_api
|
||||
def undelete(self, trans, id, **kwd):
|
||||
"""
|
||||
POST /api/quotas/deleted/{encoded_quota_id}/undelete
|
||||
Undeletes a quota
|
||||
"""
|
||||
quota = self.get_quota(trans, id, deleted=True)
|
||||
try:
|
||||
return self._undelete_quota(quota)
|
||||
except ActionInputError as e:
|
||||
raise HTTPBadRequest(detail=util.unicodify(e))
|
||||
return self.service.undelete(trans, id)
|
||||
|
||||
@@ -9,8 +9,8 @@ from galaxy import (
|
||||
util,
|
||||
web
|
||||
)
|
||||
from galaxy.actions.admin import AdminActions
|
||||
from galaxy.exceptions import ActionInputError, MessageException
|
||||
from galaxy.managers.quotas import QuotaManager
|
||||
from galaxy.model import tool_shed_install as install_model
|
||||
from galaxy.security.validate_user_input import validate_password
|
||||
from galaxy.tool_shed.util.repository_util import get_ids_of_tool_shed_repositories_being_installed
|
||||
@@ -22,9 +22,7 @@ from galaxy.util import (
|
||||
from galaxy.util.tool_shed import common_util, encoding_util
|
||||
from galaxy.web import url_for
|
||||
from galaxy.web.framework.helpers import grids, time_ago
|
||||
from galaxy.web.params import QuotaParamParser
|
||||
from galaxy.webapps.base import controller
|
||||
from galaxy.webapps.base.controller import UsesQuotaMixin
|
||||
from tool_shed.util.web_util import escape
|
||||
|
||||
|
||||
@@ -503,7 +501,7 @@ class ToolVersionListGrid(grids.Grid):
|
||||
return trans.install_model.context.query(self.model_class)
|
||||
|
||||
|
||||
class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaParamParser):
|
||||
class AdminGalaxy(controller.JSAppLauncher):
|
||||
|
||||
user_list_grid = UserListGrid()
|
||||
role_list_grid = RoleListGrid()
|
||||
@@ -522,6 +520,10 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
activate_operation = grids.GridOperation("Activate User", condition=(lambda item: not item.active), allow_multiple=False)
|
||||
resend_activation_email = grids.GridOperation("Resend Activation Email", condition=(lambda item: not item.active), allow_multiple=False)
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.quota_manager: QuotaManager = QuotaManager(app)
|
||||
|
||||
@web.expose
|
||||
@web.require_admin
|
||||
def index(self, trans, **kwd):
|
||||
@@ -646,13 +648,13 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
operation = kwargs.pop('operation').lower()
|
||||
try:
|
||||
if operation == 'delete':
|
||||
message = self._delete_quota(quotas)
|
||||
message = self.quota_manager.delete_quota(quotas)
|
||||
elif operation == 'undelete':
|
||||
message = self._undelete_quota(quotas)
|
||||
message = self.quota_manager.undelete_quota(quotas)
|
||||
elif operation == 'purge':
|
||||
message = self._purge_quota(quotas)
|
||||
message = self.quota_manager.purge_quota(quotas)
|
||||
elif operation == 'unset as default':
|
||||
message = self._unset_quota_default(quotas[0])
|
||||
message = self.quota_manager.unset_quota_default(quotas[0])
|
||||
except ActionInputError as e:
|
||||
message, status = (e.err_msg, 'error')
|
||||
if message:
|
||||
@@ -703,7 +705,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
build_select_input('in_users', 'Users', all_users, [])]}
|
||||
else:
|
||||
try:
|
||||
quota, message = self._create_quota(util.Params(payload), decode_id=trans.security.decode_id)
|
||||
quota, message = self.quota_manager.create_quota(payload, decode_id=trans.security.decode_id)
|
||||
return {'message': message}
|
||||
except ActionInputError as e:
|
||||
return self.message_exception(trans, e.err_msg)
|
||||
@@ -730,7 +732,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
}
|
||||
else:
|
||||
try:
|
||||
return {'message': self._rename_quota(quota, util.Params(payload))}
|
||||
return {'message': self.quota_manager.rename_quota(quota, util.Params(payload))}
|
||||
except ActionInputError as e:
|
||||
return self.message_exception(trans, e.err_msg)
|
||||
|
||||
@@ -766,7 +768,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
build_select_input('in_users', 'Users', all_users, in_users)]}
|
||||
else:
|
||||
try:
|
||||
return {'message': self._manage_users_and_groups_for_quota(quota, util.Params(payload), decode_id=trans.security.decode_id)}
|
||||
return {'message': self.quota_manager.manage_users_and_groups_for_quota(quota, util.Params(payload), decode_id=trans.security.decode_id)}
|
||||
except ActionInputError as e:
|
||||
return self.message_exception(trans, e.err_msg)
|
||||
|
||||
@@ -794,7 +796,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
}
|
||||
else:
|
||||
try:
|
||||
return {'message': self._edit_quota(quota, util.Params(payload))}
|
||||
return {'message': self.quota_manager.edit_quota(quota, util.Params(payload))}
|
||||
except ActionInputError as e:
|
||||
return self.message_exception(trans, e.err_msg)
|
||||
|
||||
@@ -822,7 +824,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
}
|
||||
else:
|
||||
try:
|
||||
return {'message': self._set_quota_default(quota, util.Params(payload))}
|
||||
return {'message': self.quota_manager.set_quota_default(quota, util.Params(payload))}
|
||||
except ActionInputError as e:
|
||||
return self.message_exception(trans, e.err_msg)
|
||||
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../lib/galaxy/schema
|
||||
+170
-10
@@ -1,3 +1,8 @@
|
||||
|
||||
import json
|
||||
|
||||
from requests import delete
|
||||
|
||||
from galaxy_test.base.populators import (
|
||||
DatasetPopulator,
|
||||
)
|
||||
@@ -15,18 +20,173 @@ class QuotaIntegrationTestCase(integration_util.IntegrationTestCase):
|
||||
super().setUp()
|
||||
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
|
||||
|
||||
def test_quota_crud(self):
|
||||
quotas = self.dataset_populator.get_quotas()
|
||||
assert len(quotas) == 0
|
||||
def test_create(self):
|
||||
self._create_quota_with_name("test-create-quota")
|
||||
|
||||
payload = {
|
||||
'name': 'defaultquota1',
|
||||
'description': 'first default quota',
|
||||
def test_index(self):
|
||||
self._create_quota_with_name("test-index-quota")
|
||||
index_response = self._get("quotas")
|
||||
index_response.raise_for_status()
|
||||
json_response = index_response.json()
|
||||
assert len(json_response) > 0
|
||||
|
||||
def test_index_deleted(self):
|
||||
quota = self._create_quota_with_name("test-index-deleted-quota")
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
index_response = self._get("quotas/deleted")
|
||||
index_response.raise_for_status()
|
||||
json_response = index_response.json()
|
||||
assert len(json_response) > 0
|
||||
|
||||
def test_show(self):
|
||||
quota_name = "test-show-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
show_response = self._get(f"quotas/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert json_response["name"] == quota["name"]
|
||||
|
||||
def test_show_deleted(self):
|
||||
quota_name = "test-show-deleted-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
show_response = self._get(f"quotas/deleted/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert json_response["name"] == quota["name"]
|
||||
|
||||
def test_update(self):
|
||||
quota_name = "test-update-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
|
||||
new_quota_name = "updated-quota-name"
|
||||
update_payload = json.dumps({
|
||||
'name': new_quota_name,
|
||||
})
|
||||
put_response = self._put(f"quotas/{quota_id}", data=update_payload)
|
||||
put_response.raise_for_status()
|
||||
assert "has been renamed to" in put_response.text
|
||||
|
||||
show_response = self._get(f"quotas/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert json_response["name"] == new_quota_name
|
||||
|
||||
def test_delete(self):
|
||||
quota_name = "test-delete-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
self._assert_quota_is_deleted(quota_id)
|
||||
|
||||
def test_delete_and_purge(self):
|
||||
quota_name = "test-delete-purge-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete_and_purge(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
self._assert_quota_is_deleted(quota_id)
|
||||
|
||||
def test_delete_and_purge_with_user(self):
|
||||
user_email = "test@galaxy.test"
|
||||
self.galaxy_interactor.ensure_user_with_email(user_email)
|
||||
|
||||
quota_name = "test-delete-purge-quota-user"
|
||||
payload = self._build_quota_payload_with_name(quota_name)
|
||||
payload["in_users"].append(user_email)
|
||||
create_response = self._post("quotas", data=payload, json=True)
|
||||
create_response.raise_for_status()
|
||||
quota = create_response.json()
|
||||
quota_id = quota["id"]
|
||||
|
||||
show_response = self._get(f"quotas/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert user_email in str(json_response["users"])
|
||||
|
||||
delete_response = self._delete_and_purge(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
show_response = self._get(f"quotas/deleted/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert user_email not in str(json_response["users"])
|
||||
|
||||
def test_undelete(self):
|
||||
quota_name = "test-undelete-quota"
|
||||
quota = self._create_quota_with_name(quota_name)
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete(f"quotas/{quota_id}")
|
||||
delete_response.raise_for_status()
|
||||
self._assert_quota_is_deleted(quota_id)
|
||||
|
||||
undelete_response = self._post(f"quotas/deleted/{quota_id}/undelete")
|
||||
undelete_response.raise_for_status()
|
||||
|
||||
show_response = self._get(f"quotas/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
|
||||
show_response = self._get(f"quotas/deleted/{quota_id}")
|
||||
self._assert_status_code_is(show_response, 400)
|
||||
|
||||
def test_400_when_delete_default(self):
|
||||
quota_name = "test-delete-default-quota"
|
||||
quota = self._create_quota_with_name(quota_name, is_default=True)
|
||||
quota_id = quota["id"]
|
||||
delete_response = self._delete(f"quotas/{quota_id}")
|
||||
self._assert_status_code_is(delete_response, 400)
|
||||
|
||||
def test_400_when_quota_name_already_exists(self):
|
||||
quota_name = "test-duplicated-quota"
|
||||
self._create_quota_with_name(quota_name)
|
||||
payload = self._build_quota_payload_with_name(quota_name)
|
||||
create_response = self._post("quotas", data=payload)
|
||||
self._assert_status_code_is(create_response, 400)
|
||||
|
||||
def test_400_when_show_unknown_quota(self):
|
||||
quota_id = "unknown-id"
|
||||
show_response = self._get(f"quotas/{quota_id}")
|
||||
self._assert_status_code_is(show_response, 400)
|
||||
|
||||
def _create_quota_with_name(self, quota_name: str, is_default: bool = False):
|
||||
payload = self._build_quota_payload_with_name(quota_name, is_default)
|
||||
create_response = self._post("quotas", data=payload, json=True)
|
||||
create_response.raise_for_status()
|
||||
return create_response.json()
|
||||
|
||||
def _build_quota_payload_with_name(self, quota_name: str, is_default: bool = False):
|
||||
default = "registered" if is_default else "no"
|
||||
return {
|
||||
'name': quota_name,
|
||||
'description': f'Quota {quota_name} description',
|
||||
'amount': '100MB',
|
||||
'operation': '=',
|
||||
'default': 'registered',
|
||||
'default': default,
|
||||
'in_users': [],
|
||||
'in_groups': []
|
||||
}
|
||||
self.dataset_populator.create_quota(payload)
|
||||
|
||||
quotas = self.dataset_populator.get_quotas()
|
||||
assert len(quotas) == 1
|
||||
def _delete_and_purge(self, url):
|
||||
headers = self.galaxy_interactor.api_key_header(
|
||||
key=self.galaxy_interactor.api_key,
|
||||
admin=True,
|
||||
anon=False,
|
||||
headers=None,
|
||||
)
|
||||
json_data = json.dumps({
|
||||
'purge': 'true'
|
||||
})
|
||||
api_url = self._api_url(url)
|
||||
return delete(api_url, data=json_data, headers=headers)
|
||||
|
||||
def _assert_quota_is_deleted(self, quota_id: str):
|
||||
show_response = self._get(f"quotas/deleted/{quota_id}")
|
||||
show_response.raise_for_status()
|
||||
json_response = show_response.json()
|
||||
assert json_response["id"] == quota_id
|
||||
|
||||
Reference in New Issue
Block a user