Skip to content

Commit

Permalink
Merge pull request #242 from fasrc/cp_api2
Browse files Browse the repository at this point in the history
Add API plugin
  • Loading branch information
claire-peters authored Jul 27, 2023
2 parents 1e4cccf + 90da0ea commit 5b535a3
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 15 deletions.
1 change: 1 addition & 0 deletions coldfront/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'django_tables2',
'table',
'rest_framework_datatables',
'rest_framework',
'easy_pdf',
]

Expand Down
11 changes: 11 additions & 0 deletions coldfront/config/plugins/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from coldfront.config.base import INSTALLED_APPS

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

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
3 changes: 2 additions & 1 deletion coldfront/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
'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',
'PLUGIN_LDAP': 'plugins/ldap_fasrc.py',
'PLUGIN_SFTOCF': 'plugins/sftocf.py',
'PLUGIN_FASRC': 'plugins/fasrc.py',
'PLUGIN_IFX': 'plugins/ifx.py',
'PLUGIN_FASRC_MONITORING': 'plugins/fasrc_monitoring.py'
'PLUGIN_FASRC_MONITORING': 'plugins/fasrc_monitoring.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 @@ -45,5 +45,8 @@
if 'coldfront.plugins.ifx' in settings.INSTALLED_APPS:
urlpatterns.append(path('ifx/', include('coldfront.plugins.ifx.urls')))

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

if 'coldfront.plugins.fasrc_monitoring' in settings.INSTALLED_APPS:
urlpatterns.append(path('', include('coldfront.plugins.fasrc_monitoring.urls')))
20 changes: 9 additions & 11 deletions coldfront/core/allocation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
logger = logging.getLogger(__name__)

ALLOCATION_ATTRIBUTE_VIEW_LIST = import_from_settings(
'ALLOCATION_ATTRIBUTE_VIEW_LIST', [])
ALLOCATION_FUNCS_ON_EXPIRE = import_from_settings(
'ALLOCATION_FUNCS_ON_EXPIRE', [])
'ALLOCATION_ATTRIBUTE_VIEW_LIST', []
)
ALLOCATION_FUNCS_ON_EXPIRE = import_from_settings('ALLOCATION_FUNCS_ON_EXPIRE', [])
ALLOCATION_RESOURCE_ORDERING = import_from_settings(
'ALLOCATION_RESOURCE_ORDERING',
['-is_allocatable', 'name'])
'ALLOCATION_RESOURCE_ORDERING', ['-is_allocatable', 'name']
)

class AllocationPermission(Enum):
""" A project permission stores the user and manager fields of a project. """
Expand Down Expand Up @@ -83,15 +83,15 @@ class Meta:

permissions = (
('can_view_all_allocations', 'Can view all allocations'),
('can_review_allocation_requests',
'Can review allocation requests'),
('can_review_allocation_requests', 'Can review allocation requests'),
('can_manage_invoice', 'Can manage invoice'),
)

project = models.ForeignKey(Project, on_delete=models.CASCADE,)
resources = models.ManyToManyField(Resource)
status = models.ForeignKey(
AllocationStatusChoice, on_delete=models.CASCADE, verbose_name='Status')
AllocationStatusChoice, on_delete=models.CASCADE, verbose_name='Status'
)
quantity = models.IntegerField(default=1)
start_date = models.DateField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True)
Expand Down Expand Up @@ -247,12 +247,10 @@ def get_parent_resource(self):
Resource: the parent resource for the allocation
"""
if self.resources.count() == 0:
print('no parent resource')
return None
if self.resources.count() == 1:
return self.resources.first()
parent = self.resources.order_by(
*ALLOCATION_RESOURCE_ORDERING).first()
parent = self.resources.order_by(*ALLOCATION_RESOURCE_ORDERING).first()
if parent:
return parent
# Fallback
Expand Down
14 changes: 11 additions & 3 deletions coldfront/core/allocation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,14 @@ def form_valid(self, form):
quantity = form_data.get('quantity', 1)
allocation_account = form_data.get('allocation_account', None)
# A resource is selected that requires an account name selection but user has no account names
if ALLOCATION_ACCOUNT_ENABLED and resource_obj.name in ALLOCATION_ACCOUNT_MAPPING and AllocationAttributeType.objects.filter(
name=ALLOCATION_ACCOUNT_MAPPING[resource_obj.name]).exists() and not allocation_account:
if (
ALLOCATION_ACCOUNT_ENABLED
and resource_obj.name in ALLOCATION_ACCOUNT_MAPPING
and AllocationAttributeType.objects.filter(
name=ALLOCATION_ACCOUNT_MAPPING[resource_obj.name]
).exists()
and not allocation_account
):
err = 'You need to create an account name. Create it by clicking the link under the "Allocation account" field.'
form.add_error(None, format_html(err))
return self.form_invalid(form)
Expand Down Expand Up @@ -640,7 +646,9 @@ def form_valid(self, form):
)
for user in users:
AllocationUser.objects.create(
allocation=allocation_obj, user=user, status=allocation_user_active_status
allocation=allocation_obj,
user=user,
status=allocation_user_active_status
)

# if requested resource is on NESE, add to vars
Expand Down
83 changes: 83 additions & 0 deletions coldfront/plugins/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from rest_framework import serializers

from django.contrib.auth import get_user_model

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


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ('id', 'full_name')


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 AllocationPctUsageField(serializers.Field):
def to_representation(self, data):
if data.usage and float(data.size):
return round((data.usage / float(data.size) * 100), 2)
if data.usage == 0:
return 0
return None


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)
size = serializers.FloatField()
pct_full = AllocationPctUsageField(source='*')

class Meta:
model = Allocation
fields = (
'id',
'project',
'resource',
'status',
'path',
'size',
'usage',
'pct_full',
'cost',
)


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

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


class ProjectUserSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='full_name', 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='full_name', read_only=True)
status = serializers.SlugRelatedField(slug_field='name', read_only=True)
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', 'users', 'allocations')
57 changes: 57 additions & 0 deletions coldfront/plugins/api/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from rest_framework import status
from rest_framework.test import APITestCase, APIRequestFactory
from coldfront.core.test_helpers.factories import setup_models, AllocationFactory
from coldfront.core.allocation.models import Allocation
from coldfront.core.project.models import Project


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

fixtures = [
"coldfront/core/test_helpers/test_data/test_fixtures/ifx.json",
]

@classmethod
def setUpTestData(cls):
"""Create some test data"""
setup_models(cls)
cls.additional_allocations = [
AllocationFactory() for i in list(range(50))
]

def test_requires_login(self):
"""Test that the API requires authentication"""
response = self.client.get('/api/')
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), 1)

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), Allocation.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)
13 changes: 13 additions & 0 deletions coldfront/plugins/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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'resources', views.ResourceViewSet, basename='resources')
router.register(r'projects', views.ProjectViewSet, basename='projects')

urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
66 changes: 66 additions & 0 deletions coldfront/plugins/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from rest_framework import viewsets
from django.db.models import Q

from coldfront.core.allocation.models import Allocation
from coldfront.core.resource.models import Resource
from coldfront.core.project.models import Project
from coldfront.plugins.api import serializers


class ResourceViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.ResourceSerializer
queryset = Resource.objects.all()


class AllocationViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.AllocationSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

def get_queryset(self):
allocations = Allocation.objects.prefetch_related(
'project', 'project__pi', 'status'
)

if self.request.user.is_superuser or self.request.user.has_perm(
'allocation.can_view_all_allocations'
):
allocations = allocations.order_by('project')
else:
allocations = allocations.filter(
Q(project__status__name__in=['New', 'Active']) &
(
(
Q(project__projectuser__role__name='Manager')
& Q(project__projectuser__user=self.request.user)
)
| Q(project__pi=self.request.user)
)
).distinct().order_by('project')

return allocations


class ProjectViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.ProjectSerializer


def get_queryset(self):
projects = Project.objects.prefetch_related('status')

if self.request.user.is_superuser or self.request.user.has_perm(
'project.can_view_all_projects'
):
projects = projects.order_by('pi')
else:
projects = projects.filter(
Q(status__name__in=['New', 'Active']) &
(
(
Q(projectuser__role__name='Manager')
& Q(projectuser__user=self.request.user)
)
| Q(pi=self.request.user)
)
).distinct().order_by('pi')

return projects

0 comments on commit 5b535a3

Please sign in to comment.