From b7ed6af00ccc719372093388ac2f5fc60c6163b8 Mon Sep 17 00:00:00 2001 From: geistling <34081638+geistling@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:39:33 -0500 Subject: [PATCH 1/2] add api --- coldfront/config/plugins/api.py | 21 ++ coldfront/config/settings.py | 1 + coldfront/config/urls.py | 3 + coldfront/plugins/api/__init__.py | 0 coldfront/plugins/api/serializers.py | 157 ++++++++++++++ coldfront/plugins/api/tests.py | 81 +++++++ coldfront/plugins/api/urls.py | 16 ++ coldfront/plugins/api/views.py | 310 +++++++++++++++++++++++++++ requirements.txt | 2 + setup.py | 2 + 10 files changed, 593 insertions(+) create mode 100644 coldfront/config/plugins/api.py create mode 100644 coldfront/plugins/api/__init__.py create mode 100644 coldfront/plugins/api/serializers.py create mode 100644 coldfront/plugins/api/tests.py create mode 100644 coldfront/plugins/api/urls.py create mode 100644 coldfront/plugins/api/views.py diff --git a/coldfront/config/plugins/api.py b/coldfront/config/plugins/api.py new file mode 100644 index 000000000..c9b636f43 --- /dev/null +++ b/coldfront/config/plugins/api.py @@ -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' + ], +} diff --git a/coldfront/config/settings.py b/coldfront/config/settings.py index 7ab3ccfc1..de2b08b87 100644 --- a/coldfront/config/settings.py +++ b/coldfront/config/settings.py @@ -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 diff --git a/coldfront/config/urls.py b/coldfront/config/urls.py index cabf338dc..0afbfa30c 100644 --- a/coldfront/config/urls.py +++ b/coldfront/config/urls.py @@ -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'))) diff --git a/coldfront/plugins/api/__init__.py b/coldfront/plugins/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/plugins/api/serializers.py b/coldfront/plugins/api/serializers.py new file mode 100644 index 000000000..31a17e11f --- /dev/null +++ b/coldfront/plugins/api/serializers.py @@ -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') diff --git a/coldfront/plugins/api/tests.py b/coldfront/plugins/api/tests.py new file mode 100644 index 000000000..e3d0b9db4 --- /dev/null +++ b/coldfront/plugins/api/tests.py @@ -0,0 +1,81 @@ +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_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) diff --git a/coldfront/plugins/api/urls.py b/coldfront/plugins/api/urls.py new file mode 100644 index 000000000..827f35265 --- /dev/null +++ b/coldfront/plugins/api/urls.py @@ -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')) +] diff --git a/coldfront/plugins/api/views.py b/coldfront/plugins/api/views.py new file mode 100644 index 000000000..673b175d9 --- /dev/null +++ b/coldfront/plugins/api/views.py @@ -0,0 +1,310 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.db.models import OuterRef, Subquery, Q, F, ExpressionWrapper, fields +from django.db.models.functions import Cast +from django_filters import rest_framework as filters +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from simple_history.utils import get_history_model_for_model + +from coldfront.core.utils.common import import_from_settings +from coldfront.core.allocation.models import Allocation, AllocationChangeRequest +from coldfront.core.project.models import Project +from coldfront.core.resource.models import Resource +from coldfront.plugins.api import serializers + +UNFULFILLED_ALLOCATION_STATUSES = ['Denied'] + import_from_settings( + 'PENDING_ALLOCATION_STATUSES', ['New', 'In Progress', 'On Hold', 'Pending Activation'] +) + +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 not (self.request.user.is_superuser or self.request.user.has_perm( + 'allocation.can_view_all_allocations' + )): + allocations = allocations.filter( + Q(project__status__name__in=['New', 'Active']) & + ( + ( + Q(project__projectuser__role__name__contains='Manager') + & Q(project__projectuser__user=self.request.user) + ) + | Q(project__pi=self.request.user) + ) + ).distinct() + + allocations = allocations.order_by('project') + + return allocations + + +class AllocationRequestFilter(filters.FilterSet): + '''Filters for AllocationChangeRequestViewSet. + created_before is the date the request was created before. + created_after is the date the request was created after. + ''' + created = filters.DateFromToRangeFilter() + fulfilled = filters.BooleanFilter(method='filter_fulfilled') + fulfilled_date = filters.DateFromToRangeFilter() + time_to_fulfillment = filters.NumericRangeFilter(method='filter_time_to_fulfillment') + + class Meta: + model = Allocation + fields = [ + 'created', + 'fulfilled', + 'fulfilled_date', + 'time_to_fulfillment', + ] + + def filter_fulfilled(self, queryset, name, value): + if value: + return queryset.filter(status__name='Approved') + else: + return queryset.filter(status__name__in=UNFULFILLED_ALLOCATION_STATUSES) + + def filter_time_to_fulfillment(self, queryset, name, value): + if value.start is not None: + queryset = queryset.filter( + time_to_fulfillment__gte=timedelta(days=int(value.start)) + ) + if value.stop is not None: + queryset = queryset.filter( + time_to_fulfillment__lte=timedelta(days=int(value.stop)) + ) + return queryset + + +class AllocationRequestViewSet(viewsets.ReadOnlyModelViewSet): + '''Report view on allocations requested through Coldfront. + Data: + - id: allocation id + - project: project name + - resource: resource name + - status: current status of the allocation + - created: date created + - created_by: user who submitted the allocation request + - fulfilled_date: date the allocation's status was first set to "Active" + - fulfilled_by: user who first set the allocation status to "Active" + - time_to_fulfillment: time between request creation and time_to_fulfillment + displayed as "DAY_INTEGER HH:MM:SS" + + Filters: + - created_before/created_after (structure date as 'YYYY-MM-DD') + - fulfilled (boolean) + Set to true to return all approved requests, false to return all pending and denied requests. + - fulfilled_date_before/fulfilled_date_after (structure date as 'YYYY-MM-DD') + - time_to_fulfillment_max/time_to_fulfillment_min (integer) + Set to the maximum/minimum number of days between request creation and time_to_fulfillment. + ''' + serializer_class = serializers.AllocationRequestSerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = AllocationRequestFilter + permission_classes = [IsAuthenticated, IsAdminUser] + + def get_queryset(self): + HistoricalAllocation = get_history_model_for_model(Allocation) + + # Subquery to get the earliest historical record for each allocation + earliest_history = HistoricalAllocation.objects.filter( + id=OuterRef('pk') + ).order_by('history_date').values('status__name')[:1] + + fulfilled_date = HistoricalAllocation.objects.filter( + id=OuterRef('pk'), status__name='Active' + ).order_by('history_date').values('modified')[:1] + + # Annotate allocations with the status_id of their earliest historical record + allocations = Allocation.objects.annotate( + earliest_status_name=Subquery(earliest_history) + ).filter(earliest_status_name='New').order_by('created') + + allocations = allocations.annotate( + fulfilled_date=Subquery(fulfilled_date) + ) + + allocations = allocations.annotate( + time_to_fulfillment=ExpressionWrapper( + (Cast(Subquery(fulfilled_date), fields.DateTimeField()) - F('created')), + output_field=fields.DurationField() + ) + ) + return allocations + + +class AllocationChangeRequestFilter(filters.FilterSet): + '''Filters for AllocationChangeRequestViewSet. + created_before is the date the request was created before. + created_after is the date the request was created after. + ''' + created = filters.DateFromToRangeFilter() + fulfilled = filters.BooleanFilter(method='filter_fulfilled') + fulfilled_date = filters.DateFromToRangeFilter() + time_to_fulfillment = filters.NumericRangeFilter(method='filter_time_to_fulfillment') + + class Meta: + model = AllocationChangeRequest + fields = [ + 'created', + 'fulfilled', + 'fulfilled_date', + 'time_to_fulfillment', + ] + + def filter_fulfilled(self, queryset, name, value): + if value: + return queryset.filter(status__name='Approved') + else: + return queryset.filter(status__name__in=['Pending', 'Denied']) + + def filter_time_to_fulfillment(self, queryset, name, value): + if value.start is not None: + queryset = queryset.filter( + time_to_fulfillment__gte=timedelta(days=int(value.start)) + ) + if value.stop is not None: + queryset = queryset.filter( + time_to_fulfillment__lte=timedelta(days=int(value.stop)) + ) + return queryset + + +class AllocationChangeRequestViewSet(viewsets.ReadOnlyModelViewSet): + ''' + Data: + - allocation: allocation object details + - justification: justification provided at time of filing + - status: request status + - created: date created + - created_by: user who created the object. + - fulfilled_date: date the allocationchangerequests's status was first set to "Approved" + - fulfilled_by: user who last modified an approved object. + + Query parameters: + - created_before/created_after (structure date as 'YYYY-MM-DD') + - fulfilled (boolean) + Set to true to return all approved requests, false to return all pending and denied requests. + - fulfilled_date_before/fulfilled_date_after (structure date as 'YYYY-MM-DD') + - time_to_fulfillment_max/time_to_fulfillment_min (integer) + Set to the maximum/minimum number of days between request creation and time_to_fulfillment. + ''' + serializer_class = serializers.AllocationChangeRequestSerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = AllocationChangeRequestFilter + + def get_queryset(self): + requests = AllocationChangeRequest.objects.prefetch_related( + 'allocation', 'allocation__project', 'allocation__project__pi' + ) + + if not (self.request.user.is_superuser or self.request.user.is_staff): + requests = requests.filter( + Q(allocation__project__status__name__in=['New', 'Active']) & + ( + ( + Q(allocation__project__projectuser__role__name__contains='Manager') + & Q(allocation__project__projectuser__user=self.request.user) + ) + | Q(allocation__project__pi=self.request.user) + ) + ).distinct() + + HistoricalAllocationChangeRequest = get_history_model_for_model( + AllocationChangeRequest + ) + + fulfilled_date = HistoricalAllocationChangeRequest.objects.filter( + id=OuterRef('pk'), status__name='Approved' + ).order_by('history_date').values('modified')[:1] + + requests = requests.annotate(fulfilled_date=Subquery(fulfilled_date)) + + requests = requests.annotate( + time_to_fulfillment=ExpressionWrapper( + (Cast(Subquery(fulfilled_date), fields.DateTimeField()) - F('created')), + output_field=fields.DurationField() + ) + ) + requests = requests.order_by('created') + + return requests + + +class ProjectViewSet(viewsets.ReadOnlyModelViewSet): + ''' + Query parameters: + - allocations (default false) + Show related allocation data. + - project_users (default false) + Show related user data. + ''' + serializer_class = serializers.ProjectSerializer + + def get_queryset(self): + projects = Project.objects.prefetch_related('status') + + if not ( + self.request.user.is_superuser + or self.request.user.is_staff + or self.request.user.has_perm('project.can_view_all_projects') + ): + projects = projects.filter( + Q(status__name__in=['New', 'Active']) & + ( + ( + Q(projectuser__role__name__contains='Manager') + & Q(projectuser__user=self.request.user) + ) + | Q(pi=self.request.user) + ) + ).distinct().order_by('pi') + + if self.request.query_params.get('project_users') in ['True', 'true']: + projects = projects.prefetch_related('projectuser_set') + + if self.request.query_params.get('allocations') in ['True', 'true']: + projects = projects.prefetch_related('allocation_set') + + return projects.order_by('pi') + + +class UserFilter(filters.FilterSet): + is_staff = filters.BooleanFilter() + is_active = filters.BooleanFilter() + is_superuser = filters.BooleanFilter() + username = filters.CharFilter(field_name='username', lookup_expr='exact') + + class Meta: + model = get_user_model() + fields = ['is_staff', 'is_active', 'is_superuser', 'username'] + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + '''Staff and superuser-only view for user data. + Filter parameters: + - username (exact) + - is_active + - is_superuser + - is_staff + ''' + serializer_class = serializers.UserSerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = UserFilter + permission_classes = [IsAuthenticated, IsAdminUser] + + def get_queryset(self): + queryset = get_user_model().objects.all() + return queryset diff --git a/requirements.txt b/requirements.txt index efcf7fbbe..e7b5d4a7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ Django==4.2.11 django-crispy-forms==2.1 crispy-bootstrap4==2024.1 django-environ==0.11.2 +django-filter==24.2 django-model-utils==4.4.0 django-picklefield==3.1 django-q==1.3.9 @@ -17,6 +18,7 @@ django-simple-history==3.5.0 django-split-settings==1.3.0 django-sslserver==0.22 django-su==1.0.0 +djangorestframework==3.15.2 doi2bib==0.4.0 factory-boy==3.3.0 Faker==24.1.0 diff --git a/setup.py b/setup.py index c6764581d..7c532f5fe 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ 'django-crispy-forms==2.1', 'crispy-bootstrap4==2024.1', 'django-environ==0.11.2', + 'django-filter==24.2', 'django-model-utils==4.4.0', 'django-picklefield==3.1', 'django-q==1.3.9', @@ -43,6 +44,7 @@ 'django-split-settings==1.3.0', 'django-sslserver==0.22', 'django-su==1.0.0', + 'djangorestframework==3.15.2', 'doi2bib==0.4.0', 'factory-boy==3.3.0', 'Faker==24.1.0', From 50ffb4ced9f62e9bc2bd559f7ab25daa43b6663f Mon Sep 17 00:00:00 2001 From: geistling <34081638+geistling@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:02:10 -0500 Subject: [PATCH 2/2] update testing module --- coldfront/plugins/api/tests.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/coldfront/plugins/api/tests.py b/coldfront/plugins/api/tests.py index e3d0b9db4..0ffbabf17 100644 --- a/coldfront/plugins/api/tests.py +++ b/coldfront/plugins/api/tests.py @@ -1,24 +1,11 @@ from rest_framework import status -from rest_framework.test import APITestCase, APIRequestFactory -from coldfront.core.test_helpers.factories import setup_models, AllocationFactory +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""" - - 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)) - ] + """Tests for the Coldfront REST API""" def test_requires_login(self): """Test that the API requires authentication"""