Skip to content

Commit

Permalink
add api
Browse files Browse the repository at this point in the history
Signed-off-by: geistling <[email protected]>

update testing module
  • Loading branch information
claire-peters committed Dec 4, 2024
1 parent 082685d commit 5b4b619
Show file tree
Hide file tree
Showing 10 changed files with 580 additions and 0 deletions.
21 changes: 21 additions & 0 deletions coldfront/config/plugins/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from coldfront.config.base import INSTALLED_APPS

INSTALLED_APPS += [
'django_filters',
'rest_framework',
'coldfront.plugins.api'
]

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
}
1 change: 1 addition & 0 deletions coldfront/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'PLUGIN_AUTH_OIDC': 'plugins/openid.py',
'PLUGIN_AUTH_LDAP': 'plugins/ldap.py',
'PLUGIN_LDAP_USER_SEARCH': 'plugins/ldap_user_search.py',
'PLUGIN_API': 'plugins/api.py',
}

# This allows plugins to be enabled via environment variables. Can alternatively
Expand Down
3 changes: 3 additions & 0 deletions coldfront/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
if settings.RESEARCH_OUTPUT_ENABLE:
urlpatterns.append(path('research-output/', include('coldfront.core.research_output.urls')))

if 'coldfront.plugins.api' in settings.INSTALLED_APPS:
urlpatterns.append(path('api/', include('coldfront.plugins.api.urls')))

if 'coldfront.plugins.iquota' in settings.INSTALLED_APPS:
urlpatterns.append(path('iquota/', include('coldfront.plugins.iquota.urls')))

Expand Down
Empty file.
157 changes: 157 additions & 0 deletions coldfront/plugins/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from datetime import timedelta

from django.contrib.auth import get_user_model
from rest_framework import serializers

from coldfront.core.resource.models import Resource
from coldfront.core.project.models import Project, ProjectUser
from coldfront.core.allocation.models import Allocation, AllocationChangeRequest


class UserSerializer(serializers.ModelSerializer):

class Meta:
model = get_user_model()
fields = (
'id',
'username',
'first_name',
'last_name',
'is_active',
'is_superuser',
'is_staff',
'date_joined',
)


class ResourceSerializer(serializers.ModelSerializer):
resource_type = serializers.SlugRelatedField(slug_field='name', read_only=True)

class Meta:
model = Resource
fields = ('id', 'resource_type', 'name', 'description', 'is_allocatable')


class AllocationSerializer(serializers.ModelSerializer):
resource = serializers.ReadOnlyField(source='get_resources_as_string')
project = serializers.SlugRelatedField(slug_field='title', read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)

class Meta:
model = Allocation
fields = (
'id',
'project',
'resource',
'status',
)


class AllocationRequestSerializer(serializers.ModelSerializer):
project = serializers.SlugRelatedField(slug_field='title', read_only=True)
resource = serializers.ReadOnlyField(source='get_resources_as_string', read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)
fulfilled_date = serializers.DateTimeField(read_only=True)
created_by = serializers.SerializerMethodField(read_only=True)
fulfilled_by = serializers.SerializerMethodField(read_only=True)
time_to_fulfillment = serializers.DurationField(read_only=True)

class Meta:
model = Allocation
fields = (
'id',
'project',
'resource',
'status',
'created',
'created_by',
'fulfilled_date',
'fulfilled_by',
'time_to_fulfillment',
)

def get_created_by(self, obj):
historical_record = obj.history.earliest()
creator = historical_record.history_user if historical_record else None
if not creator:
return None
return historical_record.history_user.username

def get_fulfilled_by(self, obj):
historical_records = obj.history.filter(status__name='Active')
if historical_records:
user = historical_records.earliest().history_user
if user:
return user.username
return None


class AllocationChangeRequestSerializer(serializers.ModelSerializer):
allocation = AllocationSerializer(read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)
created_by = serializers.SerializerMethodField(read_only=True)
fulfilled_date = serializers.DateTimeField(read_only=True)
fulfilled_by = serializers.SerializerMethodField(read_only=True)
time_to_fulfillment = serializers.DurationField(read_only=True)

class Meta:
model = AllocationChangeRequest
fields = (
'id',
'allocation',
'justification',
'status',
'created',
'created_by',
'fulfilled_date',
'fulfilled_by',
'time_to_fulfillment',
)

def get_created_by(self, obj):
historical_record = obj.history.earliest()
creator = historical_record.history_user if historical_record else None
if not creator:
return None
return historical_record.history_user.username

def get_fulfilled_by(self, obj):
if not obj.status.name == 'Approved':
return None
historical_record = obj.history.latest()
fulfiller = historical_record.history_user if historical_record else None
if not fulfiller:
return None
return historical_record.history_user.username


class ProjAllocationSerializer(serializers.ModelSerializer):
resource = serializers.ReadOnlyField(source='get_resources_as_string')
status = serializers.SlugRelatedField(slug_field='name', read_only=True)

class Meta:
model = Allocation
fields = ('id', 'resource', 'status')


class ProjectUserSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='username', read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)
role = serializers.SlugRelatedField(slug_field='name', read_only=True)

class Meta:
model = ProjectUser
fields = ('user', 'role', 'status')


class ProjectSerializer(serializers.ModelSerializer):
pi = serializers.SlugRelatedField(slug_field='username', read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)
project_users = ProjectUserSerializer(
source='projectuser_set', many=True, read_only=True)
allocations = ProjAllocationSerializer(
source='allocation_set', many=True, read_only=True)

class Meta:
model = Project
fields = ('id', 'title', 'pi', 'status', 'project_users', 'allocations')
68 changes: 68 additions & 0 deletions coldfront/plugins/api/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from rest_framework import status
from rest_framework.test import APITestCase
from coldfront.core.allocation.models import Allocation
from coldfront.core.project.models import Project


class ColdfrontAPI(APITestCase):
"""Tests for the Coldfront REST API"""

def test_requires_login(self):
"""Test that the API requires authentication"""
response = self.client.get('/api/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_allocation_request_api_permissions(self):
"""Test that accessing the allocation-request API view as an admin returns all
allocations, and that accessing it as a user is forbidden"""
# login as admin
self.client.force_login(self.admin_user)
response = self.client.get('/api/allocation-requests/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

self.client.force_login(self.pi_user)
response = self.client.get('/api/allocation-requests/', format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_allocation_api_permissions(self):
"""Test that accessing the allocation API view as an admin returns all
allocations, and that accessing it as a user returns only the allocations
for that user"""
# login as admin
self.client.force_login(self.admin_user)
response = self.client.get('/api/allocations/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), Allocation.objects.all().count())

self.client.force_login(self.pi_user)
response = self.client.get('/api/allocations/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)

def test_project_api_permissions(self):
"""Confirm permissions for project API:
admin user should be able to access everything
Projectusers should be able to access only their projects
"""
# login as admin
self.client.force_login(self.admin_user)
response = self.client.get('/api/projects/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), Project.objects.all().count())

self.client.force_login(self.pi_user)
response = self.client.get('/api/projects/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)

def test_user_api_permissions(self):
"""Test that accessing the user API view as an admin returns all
allocations, and that accessing it as a user is forbidden"""
# login as admin
self.client.force_login(self.admin_user)
response = self.client.get('/api/users/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

self.client.force_login(self.pi_user)
response = self.client.get('/api/users/', format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
16 changes: 16 additions & 0 deletions coldfront/plugins/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import include, path
from rest_framework import routers
from coldfront.plugins.api import views

router = routers.DefaultRouter()
router.register(r'allocations', views.AllocationViewSet, basename='allocations')
router.register(r'allocation-requests', views.AllocationRequestViewSet, basename='allocation-requests')
router.register(r'allocation-change-requests', views.AllocationChangeRequestViewSet, basename='allocation-change-requests')
router.register(r'projects', views.ProjectViewSet, basename='projects')
router.register(r'resources', views.ResourceViewSet, basename='resources')
router.register(r'users', views.UserViewSet, basename='users')

urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
Loading

0 comments on commit 5b4b619

Please sign in to comment.