Skip to content

Commit 8964950

Browse files
committed
Allow creating and updating allocations in backwards-compatible way.
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 8964950

File tree

3 files changed

+209
-7
lines changed

3 files changed

+209
-7
lines changed

src/coldfront_plugin_api/serializers.py

Lines changed: 116 additions & 6 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,15 +38,62 @@ 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 = [
74+
"id",
75+
"project",
76+
"description",
77+
"resource",
78+
"status",
79+
"attributes",
80+
"requested_resource",
81+
"requested_attributes",
82+
]
3083

3184
resource = serializers.SerializerMethodField()
3285
project = ProjectSerializer()
3386
attributes = serializers.SerializerMethodField()
34-
status = serializers.SerializerMethodField()
87+
status = serializers.SlugRelatedField(
88+
slug_field="name", queryset=AllocationStatusChoice.objects.all()
89+
)
90+
91+
requested_attributes = AllocationAttributeSerializer(
92+
many=True, source="allocationattribute_set", required=False, write_only=True
93+
)
94+
requested_resource = serializers.SlugRelatedField(
95+
slug_field="name", queryset=Resource.objects.all(), write_only=True
96+
)
3597

3698
def get_resource(self, obj: Allocation) -> dict:
3799
resource = obj.resources.first()
@@ -46,5 +108,53 @@ def get_attributes(self, obj: Allocation):
46108
for a in attrs
47109
}
48110

49-
def get_status(self, obj: Allocation) -> str:
50-
return obj.status.name
111+
def create(self, validated_data):
112+
project_obj = Project.objects.get(id=validated_data["project"]["id"])
113+
allocation = Allocation.objects.create(
114+
project=project_obj,
115+
status=validated_data["status"],
116+
justification="",
117+
start_date=datetime.now(),
118+
end_date=datetime.now() + timedelta(days=365),
119+
)
120+
allocation.resources.add(validated_data["requested_resource"])
121+
allocation.save()
122+
123+
for attribute in validated_data.pop("allocationattribute_set", []):
124+
AllocationAttribute.objects.create(
125+
allocation=allocation,
126+
allocation_attribute_type=attribute["allocation_attribute_type"],
127+
value=attribute["value"],
128+
)
129+
130+
logger.info(
131+
f"Created allocation {allocation.id} for project {project_obj.title}"
132+
)
133+
return allocation
134+
135+
def update(self, allocation: Allocation, validated_data):
136+
"""
137+
Only allow updating allocation status for now
138+
139+
Certain status transitions will have side effects (activating/deactivating allocations)
140+
"""
141+
142+
old_status = allocation.status.name
143+
new_status = validated_data.get("status", old_status).name
144+
145+
allocation.status = validated_data.get("status", allocation.status)
146+
allocation.save()
147+
148+
if old_status == "New" and new_status == "Active":
149+
signals.allocation_activate.send(
150+
sender=self.__class__, allocation_pk=allocation.pk
151+
)
152+
elif old_status == "Active" and new_status in ["Denied", "Revoked"]:
153+
signals.allocation_disable.send(
154+
sender=self.__class__, allocation_pk=allocation.pk
155+
)
156+
157+
logger.info(
158+
f"Updated allocation {allocation.id} for project {allocation.project.title}"
159+
)
160+
return allocation

src/coldfront_plugin_api/tests/unit/test_allocations.py

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

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)