Skip to content

Commit 06dbb3d

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", } To update an allocation, a partial JSON payload like so to `/api/allocations/{allocation id}`: { "attributes": [ {"attribute_type": "OpenShift Limit on CPU Quota", "value": 25}, {"attribute_type": "OpenShift Limit on RAM Quota (MiB)", "value": 1600}, ] }
1 parent 7ca0a28 commit 06dbb3d

File tree

3 files changed

+181
-25
lines changed

3 files changed

+181
-25
lines changed
Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1+
import logging
2+
13
from rest_framework import serializers
24

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

617

718
class ProjectSerializer(serializers.ModelSerializer):
819
class Meta:
920
model = Project
1021
fields = ["id", "title", "pi", "description", "field_of_science", "status"]
22+
read_only_fields = ["title", "pi", "description", "field_of_science", "status"]
1123

24+
id = serializers.IntegerField()
1225
pi = serializers.SerializerMethodField()
1326
field_of_science = serializers.SerializerMethodField()
1427
status = serializers.SerializerMethodField()
@@ -23,28 +36,88 @@ def get_status(self, obj: Project) -> str:
2336
return obj.status.name
2437

2538

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

31-
resource = serializers.SerializerMethodField()
73+
resources = ResourceSerializer(many=True)
3274
project = ProjectSerializer()
33-
attributes = serializers.SerializerMethodField()
34-
status = serializers.SerializerMethodField()
75+
attributes = AllocationAttributeSerializer(
76+
many=True, source="allocationattribute_set", required=False
77+
)
78+
status = serializers.SlugRelatedField(
79+
slug_field="name", queryset=AllocationStatusChoice.objects.all()
80+
)
81+
82+
# TODO (Quan): What about default start/end dates? Default quantity? Description? Justification?
83+
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, status=validated_data["status"]
89+
)
90+
allocation.resources.add(resource_obj)
91+
allocation.save()
3592

36-
def get_resource(self, obj: Allocation) -> dict:
37-
resource = obj.resources.first()
38-
return {"name": resource.name, "resource_type": resource.resource_type.name}
93+
# TODO (Quan): If the status is `Active`, do we fire the `activate_allocation`
94+
# signal as well to allow creating the project on remote cluster?
3995

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
96+
for attribute in validated_data.pop("allocationattribute_set", []):
97+
AllocationAttribute.objects.create(
98+
allocation=allocation,
99+
allocation_attribute_type=attribute["allocation_attribute_type"],
100+
value=attribute["value"],
45101
)
46-
for a in attrs
47-
}
48102

49-
def get_status(self, obj: Allocation) -> str:
50-
return obj.status.name
103+
logger.info(
104+
f"Created allocation {allocation.id} for project {project_obj.title}"
105+
)
106+
return allocation
107+
108+
def update(self, allocation: Allocation, validated_data):
109+
"""Only allow updating allocation attributes for now"""
110+
# TODO (Quan) Do we want to allow updating any other allocation properties?
111+
new_allocation_attributes = validated_data.pop("allocationattribute_set", [])
112+
for attribute in new_allocation_attributes:
113+
allocation_attribute, _ = AllocationAttribute.objects.get_or_create(
114+
allocation=allocation,
115+
allocation_attribute_type=attribute["allocation_attribute_type"],
116+
)
117+
allocation_attribute.value = attribute["value"]
118+
allocation_attribute.save()
119+
120+
logger.info(
121+
f"Updated allocation {allocation.id} for project {allocation.project.title}"
122+
)
123+
return allocation

src/coldfront_plugin_api/tests/unit/test_allocations.py

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_filter_allocations(self):
106106
).json()
107107
self.assertEqual(len(r_json), 3)
108108
self.assertEqual(
109-
r_json[0]["resource"]["resource_type"], self.resource.resource_type.name
109+
r_json[0]["resources"][0]["resource_type"], self.resource.resource_type.name
110110
)
111111

112112
# Filter by 1 attribute with conditional or
@@ -118,8 +118,11 @@ def test_filter_allocations(self):
118118
aa3.value,
119119
)
120120
).json()
121+
r_attribute_dict = {
122+
a["attribute_type"]: a["value"] for a in r_json[0]["attributes"]
123+
}
121124
self.assertEqual(len(r_json), 3)
122-
self.assertIn(attributes.QUOTA_LIMITS_CPU, r_json[0]["attributes"])
125+
self.assertIn(attributes.QUOTA_LIMITS_CPU, r_attribute_dict)
123126

124127
# Filter by two allocation attributes, with conditional or
125128
r_json = self.admin_client.get(
@@ -130,12 +133,13 @@ def test_filter_allocations(self):
130133
aa4.value,
131134
)
132135
).json()
136+
r_attribute_dict = {
137+
a["attribute_type"]: a["value"] for a in r_json[0]["attributes"]
138+
}
133139
self.assertEqual(len(r_json), 1)
140+
self.assertEqual(r_attribute_dict[attributes.QUOTA_LIMITS_CPU], str(aa1.value))
134141
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
142+
r_attribute_dict[attributes.QUOTA_LIMITS_MEMORY], str(aa4.value)
139143
)
140144

141145
# Filter by non-existant attribute
@@ -146,3 +150,82 @@ def test_filter_allocations(self):
146150
"/api/allocations?fake_model_attribute=fake"
147151
).json()
148152
self.assertEqual(r_json, [])
153+
154+
def test_create_allocation(self):
155+
user = self.new_user()
156+
project = self.new_project(pi=user)
157+
158+
payload = {
159+
"attributes": [
160+
{"attribute_type": "OpenShift Limit on CPU Quota", "value": 8},
161+
{"attribute_type": "OpenShift Limit on RAM Quota (MiB)", "value": 16},
162+
],
163+
"project": {"id": project.id},
164+
"resources": [{"id": self.resource.id}],
165+
"status": "New",
166+
}
167+
168+
self.admin_client.post("/api/allocations", payload, format="json")
169+
170+
created_allocation = allocation_models.Allocation.objects.get(
171+
project=project,
172+
resources__in=[self.resource],
173+
)
174+
self.assertEqual(created_allocation.status.name, "New")
175+
176+
allocation_models.AllocationAttribute.objects.get(
177+
allocation=created_allocation,
178+
allocation_attribute_type=allocation_models.AllocationAttributeType.objects.get(
179+
name="OpenShift Limit on CPU Quota"
180+
),
181+
value=8,
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 RAM Quota (MiB)"
187+
),
188+
value=16,
189+
)
190+
191+
def test_update_allocation(self):
192+
user = self.new_user()
193+
project = self.new_project(pi=user)
194+
allocation = self.new_allocation(project, self.resource, 1)
195+
196+
# Add initial attributes
197+
self.new_allocation_attribute(allocation, "OpenShift Limit on CPU Quota", 4)
198+
self.new_allocation_attribute(
199+
allocation, "OpenShift Limit on RAM Quota (MiB)", 8
200+
)
201+
202+
payload = {
203+
"attributes": [
204+
{
205+
"attribute_type": "OpenShift Limit on CPU Quota",
206+
"value": 8,
207+
}, # update CPU
208+
{
209+
"attribute_type": "OpenShift Limit on RAM Quota (MiB)",
210+
"value": 16,
211+
}, # update RAM
212+
],
213+
}
214+
215+
response = self.admin_client.patch(
216+
f"/api/allocations/{allocation.id}", payload, format="json"
217+
)
218+
self.assertEqual(response.status_code, 200)
219+
220+
updated_allocation = allocation_models.Allocation.objects.get(id=allocation.id)
221+
222+
allocation_models.AllocationAttribute.objects.get(
223+
allocation=updated_allocation,
224+
allocation_attribute_type__name="OpenShift Limit on CPU Quota",
225+
value=8,
226+
)
227+
allocation_models.AllocationAttribute.objects.get(
228+
allocation=updated_allocation,
229+
allocation_attribute_type__name="OpenShift Limit on RAM Quota (MiB)",
230+
value=16,
231+
)

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)