diff --git a/coldfront/core/portal/tests.py b/coldfront/core/portal/tests.py
index 69796f91d0..0cb1db8076 100644
--- a/coldfront/core/portal/tests.py
+++ b/coldfront/core/portal/tests.py
@@ -2,9 +2,14 @@
from coldfront.core.test_helpers import utils
from coldfront.core.test_helpers.factories import setup_models
from coldfront.core.allocation.models import AllocationChangeRequest, AllocationChangeStatusChoice
+from coldfront.core.test_helpers.factories import setup_models, AttributeTypeFactory, ProjectFactory, ResourceFactory, ResourceTypeFactory, ResourceAttributeTypeFactory, ResourceAttributeFactory
+from coldfront.core.project.models import Project
+from coldfront.core.resource.models import AttributeType, ResourceType
UTIL_FIXTURES = ['coldfront/core/test_helpers/test_data/test_fixtures/ifx.json']
-
+RESOURCE_FIXTURES = [
+ "coldfront/core/test_helpers/test_data/test_fixtures/resource.json",
+]
class PortalViewTest(TestCase):
"""Base class for portal view tests
@@ -29,6 +34,7 @@ def test_center_summary(self):
class HomePageTest(PortalViewTest):
+ fixtures = RESOURCE_FIXTURES
def test_pi_home_page(self):
"""check that the pi home page displays properly with the existing database
@@ -85,3 +91,42 @@ def test_home_page_projects_display(self):
# allocationuser not belonging to project cannot see project
response = utils.login_and_get_page(self.client, self.nonproj_allocationuser, '')
self.assertEqual(response.context['project_list'].count(), 0)
+
+ def test_home_page_managed_resources_display(self):
+ """check that managed resources display properly on the home page
+ """
+ ProjectFactory(pi=self.pi_user, title="managed_lab")
+ ProjectFactory(pi=self.admin_user, title="admin_lab")
+ text_attribute_type = AttributeType.objects.get(name="Text")
+ managed_resource = ResourceFactory(name="managed_lab", resource_type__name='Compute Node')
+ managed_resource2 = ResourceFactory(name="managed_lab2", resource_type__name='Compute Node')
+ admin_resource = ResourceFactory(name="admin_lab", resource_type__name='Compute Node')
+ owner_resourcer_attr_type = ResourceAttributeTypeFactory(name="Owner", attribute_type=text_attribute_type)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab",
+ resource=managed_resource)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab",
+ resource=managed_resource2)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="admin_lab",
+ resource=admin_resource)
+ utils.page_contains_for_user(self, self.pi_user, '', 'Managed Resources')
+ utils.page_contains_for_user(self, self.admin_user, '', 'Managed Resources')
+ utils.page_contains_for_user(self, self.pi_user, '', 'managed_lab')
+ utils.page_contains_for_user(self, self.pi_user, '', 'managed_lab2')
+ utils.page_does_not_contain_for_user(self, self.pi_user, '', 'admin_lab')
+ utils.page_contains_for_user(self, self.admin_user, '', 'admin_lab')
+ utils.page_does_not_contain_for_user(self, self.admin_user, '', 'managed_lab')
+ utils.page_does_not_contain_for_user(self, self.admin_user, '', 'managed_lab2')
+
+ def test_home_page_archive_resources_dont_show(self):
+ ProjectFactory(pi=self.pi_user, title="managed_lab")
+ text_attribute_type = AttributeType.objects.get(name="Text")
+ owner_resourcer_attr_type = ResourceAttributeTypeFactory(name="Owner", attribute_type=text_attribute_type)
+ archived_resource = ResourceFactory(name="archived_resource", resource_type__name='Compute Node', is_available=False)
+ archived_resource2 = ResourceFactory(name="archived_resource2", resource_type__name='Compute Node', is_available=False)
+ active_resource = ResourceFactory(name="active_resource", resource_type__name='Compute Node')
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab", resource=archived_resource)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab", resource=archived_resource2)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab", resource=active_resource)
+ utils.page_contains_for_user(self, self.pi_user, '', 'active_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, '', 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, '', 'archived_resource2')
diff --git a/coldfront/core/portal/views.py b/coldfront/core/portal/views.py
index 2ec63233f9..c847753fdd 100644
--- a/coldfront/core/portal/views.py
+++ b/coldfront/core/portal/views.py
@@ -15,10 +15,11 @@
)
from coldfront.core.project.models import Project
from coldfront.core.publication.models import Publication
-from coldfront.core.resource.models import Resource
+from coldfront.core.resource.models import Resource, ResourceAttribute
from coldfront.config.env import ENV
from coldfront.core.department.models import Department, DepartmentMember
from coldfront.core.utils.common import import_from_settings
+from pandas.io.clipboard import is_available
if ENV.bool('PLUGIN_SFTOCF', default=False):
from coldfront.plugins.sftocf.utils import StarFishRedash, STARFISH_SERVER
@@ -82,10 +83,12 @@ def home(request):
department_list = Department.objects.filter(
id__in=user_depts.values_list('organization_id')
)
-
- resource_list = Resource.objects.filter(
- allowed_users=request.user)
-
+ project_title_list = [project.title for project in project_list]
+ owned_resources = [attribute.resource.pk for attribute in ResourceAttribute.objects.filter(
+ resource_attribute_type__name='Owner',
+ value__in=project_title_list
+ )]
+ resource_list = Resource.objects.filter(Q(allowed_users=request.user) | Q(pk__in=owned_resources)).filter(is_available=True).distinct()
context['resource_list'] = resource_list
context['department_list'] = department_list
context['project_list'] = project_list
diff --git a/coldfront/core/resource/templates/resource_archived_list.html b/coldfront/core/resource/templates/resource_archived_list.html
new file mode 100644
index 0000000000..d99996a91d
--- /dev/null
+++ b/coldfront/core/resource/templates/resource_archived_list.html
@@ -0,0 +1,63 @@
+{% extends "list_view.html" %}
+
+{% block title %}
+Project List
+{% endblock %}
+
+{% block page_title%}Archived Resources{% endblock %}
+
+{% block presearch %}
+
+{% endblock %}
+
+
+{% block list_title %}Resource{{count|pluralize}}: {{count}}{% endblock %}
+
+{% block table_contents %}
+
+
+ |
+ ID
+ Sort ID asc
+ Sort ID desc
+ |
+
+ Resource Name
+ Sort Resource Name asc
+ Sort Resource Name desc
+ |
+
+ Parent Resource
+ Sort Parent Resource asc
+ Sort Parent Resource desc
+ |
+
+ Resource Type
+ Sort Resource Type asc
+ Sort Resource Type desc
+ |
+
+
+
+ {% for resource in item_list %}
+
+ | {{ resource.id }} |
+ {{ resource }} |
+ {{ resource.parent_resource }} |
+ {{ resource.resource_type.name }} |
+
+ {% endfor %}
+
+{% endblock %}
+
+{% block activelink %}
+$("#navbar-project-menu").addClass("active");
+$("#navbar-resource").addClass("active");
+
+{% endblock %}
diff --git a/coldfront/core/resource/templates/resource_detail.html b/coldfront/core/resource/templates/resource_detail.html
index 69d077b993..7b5503e767 100644
--- a/coldfront/core/resource/templates/resource_detail.html
+++ b/coldfront/core/resource/templates/resource_detail.html
@@ -12,7 +12,11 @@
{% block content %}
-
+{% if resource.is_available == False %}
+
+ This is a retired resource! You cannot make any changes.
+
+{% endif %}
Resource Detail
diff --git a/coldfront/core/resource/templates/resource_list.html b/coldfront/core/resource/templates/resource_list.html
index da74e6f96b..e5cbc78bc1 100644
--- a/coldfront/core/resource/templates/resource_list.html
+++ b/coldfront/core/resource/templates/resource_list.html
@@ -3,6 +3,15 @@
{% block title %}Resource List{% endblock %}
{% block page_title %}Resources{% endblock %}
+{% block presearch %}
+
+{% endblock %}
{% block list_title %}Resource{{count|pluralize}}: {{count}}{% endblock %}
@@ -46,4 +55,5 @@
{% block activelink %}
$("#navbar-project-menu").addClass("active");
$("#navbar-resource").addClass("active");
+
{% endblock %}
diff --git a/coldfront/core/resource/tests.py b/coldfront/core/resource/tests.py
index b364d45a4a..c6e291e3b5 100644
--- a/coldfront/core/resource/tests.py
+++ b/coldfront/core/resource/tests.py
@@ -1,12 +1,20 @@
+from boto3 import resource
+from django.db.models import Q
from django.test import TestCase
from coldfront.core.test_helpers import utils
-from coldfront.core.test_helpers.factories import setup_models
+from coldfront.core.test_helpers.factories import setup_models, AttributeTypeFactory, ProjectFactory, ResourceFactory, ResourceTypeFactory, ResourceAttributeTypeFactory, ResourceAttributeFactory
+from coldfront.core.project.models import Project
+from coldfront.core.resource.models import AttributeType, ResourceType
UTIL_FIXTURES = [
"coldfront/core/test_helpers/test_data/test_fixtures/ifx.json",
]
+RESOURCE_FIXTURES = [
+ "coldfront/core/test_helpers/test_data/test_fixtures/resource.json",
+]
+
BACKEND = "django.contrib.auth.backends.ModelBackend"
@@ -27,11 +35,72 @@ def resource_access_tstbase(self, url):
utils.test_logged_out_redirect_to_login(self, url)
utils.test_user_can_access(self, self.admin_user, url) # admin can access
+
class ResourceListViewTest(ResourceViewBaseTest):
"""Tests for ResourceListView"""
+ fixtures = RESOURCE_FIXTURES
def setUp(self):
- self.client.force_login(self.admin_user, backend=BACKEND)
+ self.client.force_login(self.pi_user, backend=BACKEND)
+ self.url = f'/resource/'
+
+ def test_only_user_managed_compute_nodes_show(self):
+ ProjectFactory(pi=self.pi_user, title="managed_lab")
+ ProjectFactory(pi=self.admin_user, title="admin_lab")
+ text_attribute_type = AttributeType.objects.get(name="Text")
+ managed_resource = ResourceFactory(name="managed_lab", resource_type__name='Compute Node')
+ admin_resource = ResourceFactory(name="admin_lab", resource_type__name='Compute Node')
+ owner_resourcer_attr_type = ResourceAttributeTypeFactory(name="Owner", attribute_type=text_attribute_type)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="managed_lab", resource=managed_resource)
+ ResourceAttributeFactory(resource_attribute_type=owner_resourcer_attr_type, value="admin_lab", resource=admin_resource)
+ utils.page_contains_for_user(self, self.pi_user, self.url, 'managed_lab')
+ utils.page_does_not_contain_for_user(self, self.pi_user, self.url, 'admin_lab')
+ utils.page_contains_for_user(self, self.admin_user, self.url, 'admin_lab')
+ utils.page_contains_for_user(self, self.admin_user, self.url, 'managed_lab')
+
+ def test_retired_resources_filter_shows(self):
+ utils.page_contains_for_user(self, self.pi_user, self.url, 'View retired resources')
+ utils.page_contains_for_user(self, self.admin_user, self.url, 'View retired resources')
+
+ def test_archive_resources_dont_show(self):
+ ResourceFactory(name="archived_resource", resource_type__name='Compute Node', is_available=False)
+ ResourceFactory(name="archived_resource2", resource_type__name='Compute Node', is_available=False)
+ ResourceFactory(name="active_resource", resource_type__name='Compute Node')
+ utils.page_contains_for_user(self, self.pi_user, self.url, 'active_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, self.url, 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, self.url, 'archived_resource2')
+
+
+
+class ResourceArchivedListViewTest(ResourceViewBaseTest):
+ """Tests for ResourceArchivedListView"""
+ fixtures = RESOURCE_FIXTURES
+
+ def setUp(self):
+ self.client.force_login(self.pi_user, backend=BACKEND)
+ self.url = f'/resource/archived/'
+
+ def test_archive_resources_show(self):
+ ResourceFactory(name="archived_resource", resource_type__name='Compute Node', is_available=False)
+ ResourceFactory(name="active_resource", resource_type__name='Compute Node')
+ utils.page_contains_for_user(self, self.pi_user, self.url, 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, self.url, 'active_resource')
+ utils.page_contains_for_user(self, self.admin_user, self.url, 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.admin_user, self.url, 'active_resource')
+
+ def test_can_filter_by_name(self):
+ AttributeType.objects.get(name="Text")
+ ResourceFactory(name="archived_resource", resource_type__name='Compute Node', is_available=False)
+ ResourceFactory(name="archived_resource2", resource_type__name='Compute Node', is_available=False)
+ ResourceFactory(name="active_resource", resource_type__name='Compute Node')
+ search_url = f'{self.url}?resource_name=archived_resource'
+ utils.page_contains_for_user(self, self.pi_user, search_url, 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, search_url, 'archived_resource2')
+ utils.page_does_not_contain_for_user(self, self.pi_user, search_url, 'active_resource')
+ search_url = f'{self.url}?resource_name=archived_resource2'
+ utils.page_contains_for_user(self, self.pi_user, search_url, 'archived_resource2')
+ utils.page_does_not_contain_for_user(self, self.pi_user, search_url, 'archived_resource')
+ utils.page_does_not_contain_for_user(self, self.pi_user, search_url, 'active_resource')
class ClusterResourceDetailViewTest(ResourceViewBaseTest):
@@ -146,6 +215,7 @@ def test_resource_attribute_create_access(self):
utils.test_user_cannot_access(self, self.resource_allowed_user, self.url)
utils.test_user_cannot_access(self, self.pi_user, self.url)
+
class ClusterResourceAttributeDeleteViewTest(ResourceViewBaseTest):
"""Tests for ResourceAttributeDeleteView"""
diff --git a/coldfront/core/resource/urls.py b/coldfront/core/resource/urls.py
index afb501f813..30ec4fad7e 100644
--- a/coldfront/core/resource/urls.py
+++ b/coldfront/core/resource/urls.py
@@ -5,6 +5,7 @@
urlpatterns = [
path('', resource_views.ResourceListView.as_view(),
name='resource-list'),
+ path('archived/', resource_views.ResourceArchivedListView.as_view(), name='resource-archived-list'),
path('
/', resource_views.ResourceDetailView.as_view(),
name='resource-detail'),
path('/resourceattribute/add',
diff --git a/coldfront/core/resource/views.py b/coldfront/core/resource/views.py
index 17ed02ee09..129067a91b 100644
--- a/coldfront/core/resource/views.py
+++ b/coldfront/core/resource/views.py
@@ -28,6 +28,7 @@
from coldfront.plugins.slurm.utils import SlurmError
+from coldfront.core.project.models import Project
logger = logging.getLogger(__name__)
@@ -193,6 +194,18 @@ def test_func(self):
messages.error(
self.request, 'You do not have permission to add resource attributes.')
+ def dispatch(self, request, *args, **kwargs):
+ resource_obj = get_object_or_404(Resource, pk=self.kwargs.get('pk'))
+ err = None
+ if resource_obj.is_available is False:
+ err = 'You cannot add resource attributes to retired allocations.'
+ if err:
+ messages.error(request, err)
+ return HttpResponseRedirect(
+ reverse('resource-detail', kwargs={'pk': resource_obj.pk})
+ )
+ return super().dispatch(request, *args, **kwargs)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.kwargs.get('pk')
@@ -227,6 +240,18 @@ def test_func(self):
messages.error(
self.request, 'You do not have permission to delete resource attributes.')
+ def dispatch(self, request, *args, **kwargs):
+ resource_obj = get_object_or_404(Resource, pk=self.kwargs.get('pk'))
+ err = None
+ if resource_obj.is_available is False:
+ err = 'You cannot delete resource attributes from retired allocations.'
+ if err:
+ messages.error(request, err)
+ return HttpResponseRedirect(
+ reverse('resource-detail', kwargs={'pk': resource_obj.pk})
+ )
+ return super().dispatch(request, *args, **kwargs)
+
def get(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
resource_obj = get_object_or_404(Resource, pk=pk)
@@ -302,6 +327,14 @@ def get_queryset(self):
order_by = self.return_order()
resource_search_form = ResourceSearchForm(self.request.GET)
+ project_list = Project.objects.filter(
+ Q(status__name__in=['New', 'Active', ]) & (
+ Q(pi=self.request.user) | (
+ Q(projectuser__user=self.request.user)
+ & Q(projectuser__status__name='Active')
+ )
+ )
+ ).distinct().order_by('-created')
resources = Resource.objects.filter(is_available=True)
if order_by == 'name':
@@ -363,7 +396,14 @@ def get_queryset(self):
Q(resourceattribute__resource_attribute_type__name='Vendor') &
Q(resourceattribute__value=data.get('vendor'))
)
- return resources.distinct()
+ project_title_list = [project.title for project in project_list]
+ not_owned_compute_nodes = [attribute.resource.pk for attribute in ResourceAttribute.objects.filter(
+ resource_attribute_type__name='Owner',
+ resource__resource_type__name='Compute Node'
+ ).exclude(value__in=project_title_list)]
+ if self.request.user.is_superuser:
+ return resources.distinct()
+ return resources.exclude(pk__in=not_owned_compute_nodes).distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(
@@ -371,6 +411,76 @@ def get_context_data(self, **kwargs):
return context
+class ResourceArchivedListView(ResourceListView):
+ template_name = 'resource_archived_list.html'
+
+ def get_queryset(self):
+
+ order_by = self.return_order()
+ resource_search_form = ResourceSearchForm(self.request.GET)
+
+ if order_by == 'name':
+ direction = self.request.GET.get('direction')
+ if direction == 'asc':
+ resources = Resource.objects.all().order_by(Lower('name'))
+ elif direction == 'des':
+ resources = (Resource.objects.all().order_by(Lower('name')).reverse())
+ else:
+ resources = Resource.objects.all().order_by(order_by)
+ else:
+ resources = Resource.objects.all().order_by(order_by)
+ if resource_search_form.is_valid():
+ data = resource_search_form.cleaned_data
+
+ if data.get('show_allocatable_resources'):
+ resources = resources.filter(is_allocatable=True)
+ if data.get('resource_name'):
+ resources = resources.filter(
+ name__icontains=data.get('resource_name')
+ )
+ if data.get('resource_type'):
+ resources = resources.filter(
+ resource_type=data.get('resource_type')
+ )
+
+ if data.get('model'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='Model') &
+ Q(resourceattribute__value=data.get('model'))
+ )
+ if data.get('serialNumber'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='SerialNumber') &
+ Q(resourceattribute__value=data.get('serialNumber'))
+ )
+ if data.get('installDate'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='InstallDate') &
+ Q(resourceattribute__value=data.get('installDate').strftime('%m/%d/%Y'))
+ )
+ if data.get('serviceStart'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type_name='ServiceStart') &
+ Q(resourceattribute__value=data.get('serviceStart').strftime('%m/%d/%Y'))
+ )
+ if data.get('serviceEnd'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='ServiceEnd') &
+ Q(resourceattribute__value=data.get('serviceEnd').strftime('%m/%d/%Y'))
+ )
+ if data.get('warrantyExpirationDate'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='WarrantyExpirationDate') &
+ Q(resourceattribute__value=data.get('warrantyExpirationDate').strftime('%m/%d/%Y'))
+ )
+ if data.get('vendor'):
+ resources = resources.filter(
+ Q(resourceattribute__resource_attribute_type__name='Vendor') &
+ Q(resourceattribute__value=data.get('vendor'))
+ )
+ return resources.exclude(is_available=True).distinct()
+
+
class ResourceAllocationsEditView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
template_name = 'resource_allocations_edit.html'
@@ -388,6 +498,8 @@ def dispatch(self, request, *args, **kwargs):
err = None
if 'Storage' in resource_obj.resource_type.name:
err = 'You cannot bulk-edit storage allocations.'
+ if resource_obj.is_available is False:
+ err = 'You cannot edit retired allocations.'
if err:
messages.error(request, err)
return HttpResponseRedirect(
diff --git a/coldfront/core/test_helpers/factories.py b/coldfront/core/test_helpers/factories.py
index 98c54928d6..7d922ead9c 100644
--- a/coldfront/core/test_helpers/factories.py
+++ b/coldfront/core/test_helpers/factories.py
@@ -36,6 +36,8 @@
from coldfront.core.grant.models import GrantFundingAgency, GrantStatusChoice
from coldfront.core.publication.models import PublicationSource
+from coldfront.core.allocation.models import AttributeType
+from coldfront.core.resource.models import ResourceAttribute, ResourceAttributeType
### Default values and Faker provider setup ###
@@ -212,7 +214,29 @@ class Meta:
description = factory.Faker('sentence')
resource_type = SubFactory(ResourceTypeFactory)
+class AttributeTypeFactory(DjangoModelFactory):
+ class Meta:
+ model = AttributeType
+
+ name = factory.Faker("word")
+
+class ResourceAttributeTypeFactory(DjangoModelFactory):
+ class Meta:
+ model = ResourceAttributeType
+
+ attribute_type = factory.SubFactory(AttributeTypeFactory)
+ name = factory.Faker("word")
+ is_required = factory.Faker("boolean")
+ is_unique_per_resource = factory.Faker("boolean")
+ is_value_unique = factory.Faker("boolean")
+
+class ResourceAttributeFactory(DjangoModelFactory):
+ class Meta:
+ model = ResourceAttribute
+ django_get_or_create = ('resource', 'resource_attribute_type')
+ value = 'admin_lab'
+ resource = factory.SubFactory(ResourceFactory)
### Allocation factories ###
diff --git a/coldfront/core/test_helpers/test_data/test_fixtures/resource.json b/coldfront/core/test_helpers/test_data/test_fixtures/resource.json
new file mode 100644
index 0000000000..23ad32b7fa
--- /dev/null
+++ b/coldfront/core/test_helpers/test_data/test_fixtures/resource.json
@@ -0,0 +1,97 @@
+[
+
+ {
+ "model": "resource.attributetype",
+ "pk": 1,
+ "fields": {
+ "created": "2024-10-10T01:18:54.259Z",
+ "modified": "2024-10-10T01:18:54.259Z",
+ "name": "Active/Inactive"
+ }
+ },
+ {
+ "model": "resource.attributetype",
+ "pk": 2,
+ "fields": {
+ "created": "2024-10-10T01:18:54.266Z",
+ "modified": "2024-10-10T01:18:54.266Z",
+ "name": "Date"
+ }
+ },
+ {
+ "model": "resource.attributetype",
+ "pk": 3,
+ "fields": {
+ "created": "2024-10-10T01:18:54.277Z",
+ "modified": "2024-10-10T01:18:54.277Z",
+ "name": "Int"
+ }
+ },
+
+ {
+ "model": "resource.attributetype",
+ "pk": 5,
+ "fields": {
+ "created": "2024-10-10T01:18:54.290Z",
+ "modified": "2024-10-10T01:18:54.290Z",
+ "name": "Text"
+ }
+ },
+ {
+ "model": "resource.attributetype",
+ "pk": 7,
+ "fields": {
+ "created": "2024-10-10T01:18:54.300Z",
+ "modified": "2024-10-10T01:18:54.300Z",
+ "name": "Attribute Expanded Text"
+ }
+ },
+ {
+ "model": "resource.attributetype",
+ "pk": 8,
+ "fields": {
+ "created": "2024-10-10T01:18:54.305Z",
+ "modified": "2024-10-10T01:18:54.305Z",
+ "name": "Float"
+ }
+ },
+ {
+ "model": "resource.resourceattributetype",
+ "pk": 10,
+ "fields": {
+ "created": "2024-10-11T00:26:36.739Z",
+ "modified": "2024-10-11T00:26:36.739Z",
+ "attribute_type": 5,
+ "name": "slurm_cluster",
+ "is_required": false,
+ "is_unique_per_resource": false,
+ "is_value_unique": false
+ }
+ },
+ {
+ "model": "resource.resourceattributetype",
+ "pk": 15,
+ "fields": {
+ "created": "2025-03-21T21:50:07.174Z",
+ "modified": "2025-03-21T21:50:07.174Z",
+ "attribute_type": 5,
+ "name": "Owner",
+ "is_required": false,
+ "is_unique_per_resource": false,
+ "is_value_unique": false
+ }
+ },
+ {
+ "model": "resource.resourceattributetype",
+ "pk": 16,
+ "fields": {
+ "created": "2025-03-21T21:50:07.177Z",
+ "modified": "2025-03-21T21:50:07.177Z",
+ "attribute_type": 2,
+ "name": "ServiceEnd",
+ "is_required": false,
+ "is_unique_per_resource": false,
+ "is_value_unique": false
+ }
+ }
+]