Skip to content

Commit 4d2206d

Browse files
committed
Allow creating and updating allocations
To create an allocation, a JSON payload can be uploaded to `/api/allocations`: { "attributes": [ {"attribute_type": "OpenShift Limit on CPU Quota", "value": 8}, {"attribute_type": "OpenShift Limit on RAM Quota (MiB)", "value": 16}, ], "project": {"id": project.id}, "resources": [{"id": self.resource.id}], "status": "New", } Updating allocation status is done via a PATCH request to `/api/allocations/{id}` with a JSON payload: { "status": "Active" } Certain status transitions trigger signals: - New -> Active: allocation_activate - Active -> Denied: allocation_deactivate
1 parent 7ca0a28 commit 4d2206d

File tree

3 files changed

+205
-25
lines changed

3 files changed

+205
-25
lines changed
Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1+
import logging
2+
from datetime import datetime, timedelta
3+
14
from rest_framework import serializers
25

3-
from coldfront.core.allocation.models import Allocation, AllocationAttribute
4-
from coldfront.core.allocation.models import Project
6+
from coldfront.core.allocation.models import (
7+
Allocation,
8+
AllocationAttribute,
9+
AllocationStatusChoice,
10+
AllocationAttributeType,
11+
)
12+
from coldfront.core.allocation.models import Project, Resource
13+
from coldfront.core.allocation import signals
14+
15+
16+
logger = logging.getLogger(__name__)
17+
logger.setLevel(logging.INFO)
518

619

720
class ProjectSerializer(serializers.ModelSerializer):
821
class Meta:
922
model = Project
1023
fields = ["id", "title", "pi", "description", "field_of_science", "status"]
24+
read_only_fields = ["title", "pi", "description", "field_of_science", "status"]
1125

26+
id = serializers.IntegerField()
1227
pi = serializers.SerializerMethodField()
1328
field_of_science = serializers.SerializerMethodField()
1429
status = serializers.SerializerMethodField()
@@ -23,28 +38,97 @@ def get_status(self, obj: Project) -> str:
2338
return obj.status.name
2439

2540

41+
class AllocationAttributeSerializer(serializers.ModelSerializer):
42+
class Meta:
43+
model = AllocationAttribute
44+
fields = ["attribute_type", "value"]
45+
46+
attribute_type = (
47+
serializers.SlugRelatedField( # Peforms validation to ensure attribute exists
48+
read_only=False,
49+
slug_field="name",
50+
queryset=AllocationAttributeType.objects.all(),
51+
source="allocation_attribute_type",
52+
)
53+
)
54+
value = serializers.CharField(read_only=False)
55+
56+
57+
class ResourceSerializer(serializers.ModelSerializer):
58+
class Meta:
59+
model = Resource
60+
fields = ["id", "name", "resource_type"]
61+
62+
id = serializers.IntegerField()
63+
name = serializers.CharField(required=False)
64+
resource_type = serializers.SerializerMethodField(required=False)
65+
66+
def get_resource_type(self, obj: Resource):
67+
return obj.resource_type.name
68+
69+
2670
class AllocationSerializer(serializers.ModelSerializer):
2771
class Meta:
2872
model = Allocation
29-
fields = ["id", "project", "description", "resource", "status", "attributes"]
73+
fields = ["id", "project", "description", "resources", "status", "attributes"]
3074

31-
resource = serializers.SerializerMethodField()
75+
resources = ResourceSerializer(many=True)
3276
project = ProjectSerializer()
33-
attributes = serializers.SerializerMethodField()
34-
status = serializers.SerializerMethodField()
77+
attributes = AllocationAttributeSerializer(
78+
many=True, source="allocationattribute_set", required=False
79+
)
80+
status = serializers.SlugRelatedField(
81+
slug_field="name", queryset=AllocationStatusChoice.objects.all()
82+
)
3583

36-
def get_resource(self, obj: Allocation) -> dict:
37-
resource = obj.resources.first()
38-
return {"name": resource.name, "resource_type": resource.resource_type.name}
84+
def create(self, validated_data):
85+
project_obj = Project.objects.get(id=validated_data["project"]["id"])
86+
resource_obj = Resource.objects.get(id=validated_data["resources"][0]["id"])
87+
allocation = Allocation.objects.create(
88+
project=project_obj,
89+
status=validated_data["status"],
90+
justification="",
91+
start_date=datetime.now(),
92+
end_date=datetime.now() + timedelta(days=365),
93+
)
94+
allocation.resources.add(resource_obj)
95+
allocation.save()
3996

40-
def get_attributes(self, obj: Allocation):
41-
attrs = AllocationAttribute.objects.filter(allocation=obj)
42-
return {
43-
a.allocation_attribute_type.name: obj.get_attribute(
44-
a.allocation_attribute_type.name
97+
for attribute in validated_data.pop("allocationattribute_set", []):
98+
AllocationAttribute.objects.create(
99+
allocation=allocation,
100+
allocation_attribute_type=attribute["allocation_attribute_type"],
101+
value=attribute["value"],
45102
)
46-
for a in attrs
47-
}
48103

49-
def get_status(self, obj: Allocation) -> str:
50-
return obj.status.name
104+
logger.info(
105+
f"Created allocation {allocation.id} for project {project_obj.title}"
106+
)
107+
return allocation
108+
109+
def update(self, allocation: Allocation, validated_data):
110+
"""
111+
Only allow updating allocation status for now
112+
113+
Certain status transitions will have side effects (activating/deactivating allocations)
114+
"""
115+
116+
old_status = allocation.status.name
117+
new_status = validated_data.get("status", old_status).name
118+
119+
allocation.status = validated_data.get("status", allocation.status)
120+
allocation.save()
121+
122+
if old_status == "New" and new_status == "Active":
123+
signals.allocation_activate.send(
124+
sender=self.__class__, allocation_pk=allocation.pk
125+
)
126+
elif old_status == "Active" and new_status in ["Denied", "Revoked"]:
127+
signals.allocation_disable.send(
128+
sender=self.__class__, allocation_pk=allocation.pk
129+
)
130+
131+
logger.info(
132+
f"Updated allocation {allocation.id} for project {allocation.project.title}"
133+
)
134+
return allocation

src/coldfront_plugin_api/tests/unit/test_allocations.py

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from os import devnull
2+
from datetime import datetime, timedelta
23
import sys
4+
from unittest.mock import patch, ANY
35

46
from coldfront.core.allocation import models as allocation_models
57
from django.core.management import call_command
@@ -106,7 +108,7 @@ def test_filter_allocations(self):
106108
).json()
107109
self.assertEqual(len(r_json), 3)
108110
self.assertEqual(
109-
r_json[0]["resource"]["resource_type"], self.resource.resource_type.name
111+
r_json[0]["resources"][0]["resource_type"], self.resource.resource_type.name
110112
)
111113

112114
# Filter by 1 attribute with conditional or
@@ -118,8 +120,11 @@ def test_filter_allocations(self):
118120
aa3.value,
119121
)
120122
).json()
123+
r_attribute_dict = {
124+
a["attribute_type"]: a["value"] for a in r_json[0]["attributes"]
125+
}
121126
self.assertEqual(len(r_json), 3)
122-
self.assertIn(attributes.QUOTA_LIMITS_CPU, r_json[0]["attributes"])
127+
self.assertIn(attributes.QUOTA_LIMITS_CPU, r_attribute_dict)
123128

124129
# Filter by two allocation attributes, with conditional or
125130
r_json = self.admin_client.get(
@@ -130,12 +135,13 @@ def test_filter_allocations(self):
130135
aa4.value,
131136
)
132137
).json()
138+
r_attribute_dict = {
139+
a["attribute_type"]: a["value"] for a in r_json[0]["attributes"]
140+
}
133141
self.assertEqual(len(r_json), 1)
142+
self.assertEqual(r_attribute_dict[attributes.QUOTA_LIMITS_CPU], str(aa1.value))
134143
self.assertEqual(
135-
r_json[0]["attributes"][attributes.QUOTA_LIMITS_CPU], aa1.value
136-
)
137-
self.assertEqual(
138-
r_json[0]["attributes"][attributes.QUOTA_LIMITS_MEMORY], aa4.value
144+
r_attribute_dict[attributes.QUOTA_LIMITS_MEMORY], str(aa4.value)
139145
)
140146

141147
# Filter by non-existant attribute
@@ -146,3 +152,93 @@ def test_filter_allocations(self):
146152
"/api/allocations?fake_model_attribute=fake"
147153
).json()
148154
self.assertEqual(r_json, [])
155+
156+
def test_create_allocation(self):
157+
user = self.new_user()
158+
project = self.new_project(pi=user)
159+
160+
payload = {
161+
"attributes": [
162+
{"attribute_type": "OpenShift Limit on CPU Quota", "value": 8},
163+
{"attribute_type": "OpenShift Limit on RAM Quota (MiB)", "value": 16},
164+
],
165+
"project": {"id": project.id},
166+
"resources": [{"id": self.resource.id}],
167+
"status": "New",
168+
}
169+
170+
self.admin_client.post("/api/allocations", payload, format="json")
171+
172+
created_allocation = allocation_models.Allocation.objects.get(
173+
project=project,
174+
resources__in=[self.resource],
175+
)
176+
self.assertEqual(created_allocation.status.name, "New")
177+
self.assertEqual(created_allocation.justification, "")
178+
self.assertEqual(created_allocation.start_date, datetime.now().date())
179+
self.assertEqual(
180+
created_allocation.end_date, (datetime.now() + timedelta(days=365)).date()
181+
)
182+
183+
allocation_models.AllocationAttribute.objects.get(
184+
allocation=created_allocation,
185+
allocation_attribute_type=allocation_models.AllocationAttributeType.objects.get(
186+
name="OpenShift Limit on CPU Quota"
187+
),
188+
value=8,
189+
)
190+
allocation_models.AllocationAttribute.objects.get(
191+
allocation=created_allocation,
192+
allocation_attribute_type=allocation_models.AllocationAttributeType.objects.get(
193+
name="OpenShift Limit on RAM Quota (MiB)"
194+
),
195+
value=16,
196+
)
197+
198+
def test_update_allocation_status_new_to_active(self):
199+
user = self.new_user()
200+
project = self.new_project(pi=user)
201+
allocation = self.new_allocation(project, self.resource, 1)
202+
allocation.status = allocation_models.AllocationStatusChoice.objects.get(
203+
name="New"
204+
)
205+
allocation.save()
206+
207+
payload = {"status": "Active"}
208+
209+
with patch(
210+
"coldfront.core.allocation.signals.allocation_activate.send"
211+
) as mock_activate:
212+
response = self.admin_client.patch(
213+
f"/api/allocations/{allocation.id}?all=true", payload, format="json"
214+
)
215+
self.assertEqual(response.status_code, 200)
216+
allocation.refresh_from_db()
217+
self.assertEqual(allocation.status.name, "Active")
218+
mock_activate.assert_called_once_with(
219+
sender=ANY, allocation_pk=allocation.pk
220+
)
221+
222+
def test_update_allocation_status_active_to_denied(self):
223+
user = self.new_user()
224+
project = self.new_project(pi=user)
225+
allocation = self.new_allocation(project, self.resource, 1)
226+
allocation.status = allocation_models.AllocationStatusChoice.objects.get(
227+
name="Active"
228+
)
229+
allocation.save()
230+
231+
payload = {"status": "Denied"}
232+
233+
with patch(
234+
"coldfront.core.allocation.signals.allocation_disable.send"
235+
) as mock_disable:
236+
response = self.admin_client.patch(
237+
f"/api/allocations/{allocation.id}", payload, format="json"
238+
)
239+
self.assertEqual(response.status_code, 200)
240+
allocation.refresh_from_db()
241+
self.assertEqual(allocation.status.name, "Denied")
242+
mock_disable.assert_called_once_with(
243+
sender=ANY, allocation_pk=allocation.pk
244+
)

src/coldfront_plugin_api/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from coldfront_plugin_api import auth, serializers
1111

1212

13-
class AllocationViewSet(viewsets.ReadOnlyModelViewSet):
13+
class AllocationViewSet(viewsets.ModelViewSet):
1414
"""
1515
This viewset implements the API to Coldfront's allocation object
1616
The API allows filtering allocations by any of Coldfront's allocation model attributes,

0 commit comments

Comments
 (0)