-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor slurm tasks for testability (#406)
- Loading branch information
1 parent
1dda7d0
commit ec3f867
Showing
7 changed files
with
280 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
"""Custom database managers for encapsulating repeatable table queries. | ||
Manager classes encapsulate common database operations at the table level (as | ||
opposed to the level of individual records). At least one Manager exists for | ||
every database model. Managers are commonly exposed as an attribute of the | ||
associated model class called `objects`. | ||
""" | ||
|
||
from datetime import date | ||
from typing import TYPE_CHECKING | ||
|
||
from django.db.models import Manager, QuerySet, Sum | ||
|
||
from apps.users.models import ResearchGroup | ||
|
||
if TYPE_CHECKING: # pragma: nocover | ||
from apps.allocations.models import Cluster | ||
|
||
__all__ = ['AllocationManager'] | ||
|
||
|
||
class AllocationManager(Manager): | ||
"""Custom manager for the `Allocation` model. | ||
Provides query methods for fetching approved, active, and expired allocations, | ||
as well as calculating service units and historical usage. | ||
""" | ||
|
||
def approved_allocations(self, account: ResearchGroup, cluster: 'Cluster') -> QuerySet: | ||
"""Retrieve all approved allocations for a specific account and cluster. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
A queryset of approved Allocation objects. | ||
""" | ||
|
||
return self.filter(request__group=account, cluster=cluster, request__status='AP') | ||
|
||
def active_allocations(self, account: ResearchGroup, cluster: 'Cluster') -> QuerySet: | ||
"""Retrieve all active allocations for a specific account and cluster. | ||
Active allocations have been approved and are currently within their start/end date. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
A queryset of active Allocation objects. | ||
""" | ||
|
||
return self.approved_allocations(account, cluster).filter( | ||
request__active__lte=date.today(), request__expire__gt=date.today() | ||
) | ||
|
||
def expiring_allocations(self, account: ResearchGroup, cluster: 'Cluster') -> QuerySet: | ||
"""Retrieve all expiring allocations for a specific account and cluster. | ||
Expiring allocations have been approved and have passed their expiration date | ||
but do not yet have a final usage value set. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
A queryset of expired Allocation objects ordered by expiration date. | ||
""" | ||
|
||
return self.approved_allocations(account, cluster).filter( | ||
final=None, request__expire__lte=date.today() | ||
).order_by("request__expire") | ||
|
||
def active_service_units(self, account: ResearchGroup, cluster: 'Cluster') -> int: | ||
"""Calculate the total service units across all active allocations for an account and cluster. | ||
Active allocations have been approved and are currently within their start/end date. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
Total service units from active allocations. | ||
""" | ||
|
||
return self.active_allocations(account, cluster).aggregate( | ||
Sum("awarded") | ||
)['awarded__sum'] or 0 | ||
|
||
def expiring_service_units(self, account: ResearchGroup, cluster: 'Cluster') -> int: | ||
"""Calculate the total service units across all expiring allocations for an account and cluster. | ||
Expiring allocations have been approved and have passed their expiration date | ||
but do not yet have a final usage value set. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
Total service units from expired allocations. | ||
""" | ||
|
||
return self.expiring_allocations(account, cluster).aggregate( | ||
Sum("awarded") | ||
)['awarded__sum'] or 0 | ||
|
||
def historical_usage(self, account: ResearchGroup, cluster: 'Cluster') -> int: | ||
"""Calculate the total final usage for expired allocations of a specific account and cluster. | ||
Args: | ||
account: object representing the account. | ||
cluster: object representing the cluster. | ||
Returns: | ||
Total historical usage calculated from expired allocations. | ||
""" | ||
|
||
return self.approved_allocations(account, cluster).filter( | ||
request__expire__lte=date.today() | ||
).aggregate(Sum("final"))['final__sum'] or 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
117 changes: 117 additions & 0 deletions
117
keystone_api/apps/allocations/tests/test_managers/test_AllocationManager.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
"""Unit tests for the `AllocationManager` class.""" | ||
|
||
from django.test import TestCase | ||
from django.utils import timezone | ||
|
||
from apps.allocations.models import * | ||
from apps.users.models import * | ||
|
||
|
||
class GetAllocationData(TestCase): | ||
"""Test get methods used to retrieve allocation metadata/status.""" | ||
|
||
def setUp(self) -> None: | ||
"""Create test data.""" | ||
|
||
self.user = User.objects.create(username="user", password='foobar123!') | ||
self.group = ResearchGroup.objects.create(name="Research Group 1", pi=self.user) | ||
self.cluster = Cluster.objects.create(name="Test Cluster") | ||
|
||
# An allocation request pending review | ||
self.request1 = AllocationRequest.objects.create( | ||
group=self.group, | ||
status='PD', | ||
active=timezone.now().date(), | ||
expire=timezone.now().date() + timezone.timedelta(days=30) | ||
) | ||
self.allocation1 = Allocation.objects.create( | ||
requested=100, | ||
awarded=80, | ||
final=None, | ||
cluster=self.cluster, | ||
request=self.request1 | ||
) | ||
|
||
# An approved allocation request that is active | ||
self.request2 = AllocationRequest.objects.create( | ||
group=self.group, | ||
status='AP', | ||
active=timezone.now().date(), | ||
expire=timezone.now().date() + timezone.timedelta(days=30) | ||
) | ||
self.allocation2 = Allocation.objects.create( | ||
requested=100, | ||
awarded=80, | ||
final=None, | ||
cluster=self.cluster, | ||
request=self.request2 | ||
) | ||
|
||
# An approved allocation request that is expired without final usage | ||
self.request3 = AllocationRequest.objects.create( | ||
group=self.group, | ||
status='AP', | ||
active=timezone.now().date() - timezone.timedelta(days=60), | ||
expire=timezone.now().date() - timezone.timedelta(days=30) | ||
) | ||
self.allocation3 = Allocation.objects.create( | ||
requested=100, | ||
awarded=70, | ||
final=None, | ||
cluster=self.cluster, | ||
request=self.request3 | ||
) | ||
|
||
# An approved allocation request that is expired with final usage | ||
self.request4 = AllocationRequest.objects.create( | ||
group=self.group, | ||
status='AP', | ||
active=timezone.now().date() - timezone.timedelta(days=30), | ||
expire=timezone.now().date() | ||
) | ||
self.allocation4 = Allocation.objects.create( | ||
requested=100, | ||
awarded=60, | ||
final=60, | ||
cluster=self.cluster, | ||
request=self.request4 | ||
) | ||
|
||
def test_approved_allocations(self) -> None: | ||
"""Test the `approved_allocations` method returns only approved allocations.""" | ||
|
||
approved_allocations = Allocation.objects.approved_allocations(self.group, self.cluster) | ||
expected_allocations = [self.allocation2, self.allocation3, self.allocation4] | ||
self.assertQuerySetEqual(expected_allocations, approved_allocations, ordered=False) | ||
|
||
def test_active_allocations(self) -> None: | ||
"""Test the `active_allocations` method returns only active allocations.""" | ||
|
||
active_allocations = Allocation.objects.active_allocations(self.group, self.cluster) | ||
expected_allocations = [self.allocation2] | ||
self.assertQuerySetEqual(expected_allocations, active_allocations, ordered=False) | ||
|
||
def test_expired_allocations(self) -> None: | ||
"""Test the `expired_allocations` method returns only expired allocations.""" | ||
|
||
expiring_allocations = Allocation.objects.expiring_allocations(self.group, self.cluster) | ||
expected_allocations = [self.allocation3] | ||
self.assertQuerySetEqual(expected_allocations, expiring_allocations, ordered=False) | ||
|
||
def test_active_service_units(self) -> None: | ||
"""Test the `active_service_units` method returns the total awarded service units for active allocations.""" | ||
|
||
active_su = Allocation.objects.active_service_units(self.group, self.cluster) | ||
self.assertEqual(80, active_su) | ||
|
||
def test_expired_service_units(self) -> None: | ||
"""Test the `expired_service_units` method returns the total awarded service units for expired allocations.""" | ||
|
||
expiring_su = Allocation.objects.expiring_service_units(self.group, self.cluster) | ||
self.assertEqual(70, expiring_su) | ||
|
||
def test_historical_usage(self) -> None: | ||
"""Test the `historical_usage` method returns the total final usage for expired allocations.""" | ||
|
||
historical_usage = Allocation.objects.historical_usage(self.group, self.cluster) | ||
self.assertEqual(60, historical_usage) |
Oops, something went wrong.