Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coldfront REST API #632

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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