diff --git a/app/core/admin.py b/app/core/admin.py index e3aaebac..43713d8b 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -27,6 +27,7 @@ from .models import ReferrerType from .models import Sdg from .models import Skill +from .models import SocBroad from .models import SocMajor from .models import SocMinor from .models import StackElement @@ -278,6 +279,13 @@ class ProjectStatusAdmin(admin.ModelAdmin): list_display = ("name", "description") +@admin.register(SocBroad) +class SocBroadAdmin(admin.ModelAdmin): + list_display = ("title", "occ_code", "soc_minor") + list_filter = ("soc_minor",) + search_fields = ("title", "occ_code") + + @admin.register(SocMajor) class SocMajorAdmin(admin.ModelAdmin): list_display = ("occ_code", "title") diff --git a/app/core/api/serializers.py b/app/core/api/serializers.py index 2306b98a..b688ea17 100644 --- a/app/core/api/serializers.py +++ b/app/core/api/serializers.py @@ -22,6 +22,7 @@ from core.models import ReferrerType from core.models import Sdg from core.models import Skill +from core.models import SocBroad from core.models import SocMajor from core.models import SocMinor from core.models import StackElement @@ -427,6 +428,20 @@ class Meta: read_only_fields = ("uuid", "created_at", "updated_at") +class SocBroadSerializer(serializers.ModelSerializer): + class Meta: + model = SocBroad + fields = ( + "uuid", + "created_at", + "updated_at", + "soc_minor", + "occ_code", + "title", + ) + read_only_fields = ("uuid", "created_at", "updated_at") + + class SocMajorSerializer(serializers.ModelSerializer): """Used to retrieve soc_major info""" diff --git a/app/core/api/urls.py b/app/core/api/urls.py index 61d9b809..582bef76 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -22,6 +22,7 @@ from .views import ReferrerViewSet from .views import SdgViewSet from .views import SkillViewSet +from .views import SocBroadViewSet from .views import SocMajorViewSet from .views import SocMinorViewSet from .views import StackElementTypeViewSet @@ -65,6 +66,7 @@ router.register(r"check-types", CheckTypeViewSet, basename="check-type") router.register(r"project-statuses", ProjectStatusViewSet, basename="project-status") router.register(r"project-urls", ProjectUrlViewSet, basename="project-url") +router.register(r"soc-broads", SocBroadViewSet, basename="soc-broad") router.register(r"soc-majors", SocMajorViewSet, basename="soc-major") router.register(r"soc-minors", SocMinorViewSet, basename="soc-minor") router.register(r"url-types", UrlTypeViewSet, basename="url-type") diff --git a/app/core/api/views.py b/app/core/api/views.py index a30f1270..282d4902 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -37,6 +37,7 @@ from ..models import ReferrerType from ..models import Sdg from ..models import Skill +from ..models import SocBroad from ..models import SocMajor from ..models import SocMinor from ..models import StackElement @@ -67,6 +68,7 @@ from .serializers import ReferrerTypeSerializer from .serializers import SdgSerializer from .serializers import SkillSerializer +from .serializers import SocBroadSerializer from .serializers import SocMajorSerializer from .serializers import SocMinorSerializer from .serializers import StackElementSerializer @@ -436,6 +438,20 @@ class UserPermissionViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = UserPermissionSerializer +@extend_schema_view( + list=extend_schema(description="Return a list of all SOC broad occupations"), + create=extend_schema(description="Create a new SOC broad occupation"), + retrieve=extend_schema(description="Return the details of a SOC broad occupation"), + destroy=extend_schema(description="Delete a SOC broad occupation"), + update=extend_schema(description="Update a SOC broad occupation"), + partial_update=extend_schema(description="Patch a SOC broad occupation"), +) +class SocBroadViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated] + queryset = SocBroad.objects.select_related("soc_minor").all().order_by("title") + serializer_class = SocBroadSerializer + + @extend_schema_view( list=extend_schema(description="Return a list of all the soc majors"), create=extend_schema(description="Create a new soc major"), diff --git a/app/core/migrations/0046_socbroad.py b/app/core/migrations/0046_socbroad.py new file mode 100644 index 00000000..886f9bf6 --- /dev/null +++ b/app/core/migrations/0046_socbroad.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2025-12-03 21:40 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0045_wintype'), + ] + + operations = [ + migrations.CreateModel( + name='SocBroad', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('occ_code', models.CharField(max_length=255)), + ('title', models.CharField(max_length=255)), + ('soc_minor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='soc_broads', to='core.socminor')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/core/migrations/max_migration.txt b/app/core/migrations/max_migration.txt index a16d3b12..4515531f 100644 --- a/app/core/migrations/max_migration.txt +++ b/app/core/migrations/max_migration.txt @@ -1 +1 @@ -0045_wintype +0046_socbroad diff --git a/app/core/models.py b/app/core/models.py index 9cb1f89b..eb38a804 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -488,6 +488,23 @@ def __str__(self): return f"{self.name}" +class SocBroad(AbstractBaseModel): + """ + Broad SOC category tied to a SocMinor. + """ + + soc_minor = models.ForeignKey( + "SocMinor", + on_delete=models.CASCADE, + related_name="soc_broads", + ) + occ_code = models.CharField(max_length=255) + title = models.CharField(max_length=255) + + def __str__(self) -> str: + return self.title + + class SocMajor(AbstractBaseModel): occ_code = models.CharField(max_length=255) title = models.CharField(max_length=255) diff --git a/app/core/tests/conftest.py b/app/core/tests/conftest.py index 4b6d8513..1558b969 100644 --- a/app/core/tests/conftest.py +++ b/app/core/tests/conftest.py @@ -25,6 +25,7 @@ from ..models import ReferrerType from ..models import Sdg from ..models import Skill +from ..models import SocBroad from ..models import SocMajor from ..models import SocMinor from ..models import StackElement @@ -335,6 +336,15 @@ def project_status(): ) +@pytest.fixture +def soc_broad(soc_minor): + return SocBroad.objects.create( + soc_minor=soc_minor, + occ_code="15-1252", + title="Software Developers", + ) + + @pytest.fixture def soc_major(): return SocMajor.objects.create(occ_code="22-2222", title="Test Soc Major") diff --git a/app/core/tests/test_api.py b/app/core/tests/test_api.py index ad579d0a..2bb3b47f 100644 --- a/app/core/tests/test_api.py +++ b/app/core/tests/test_api.py @@ -10,6 +10,7 @@ from core.models import ProgramArea from core.models import ProjectStackElementXref from core.models import ProjectUrl +from core.models import SocBroad from core.models import UrlStatusType from core.models import UserCheck from core.models import UserPermission @@ -43,6 +44,7 @@ CHECK_TYPE_URL = reverse("check-type-list") PROJECT_STATUSES_URL = reverse("project-status-list") PROJECT_URLS_URL = reverse("project-url-list") +SOC_BROAD_URL = reverse("soc-broad-list") SOC_MAJOR_URL = reverse("soc-major-list") SOC_MINORS_URL = reverse("soc-minor-list") URL_TYPE_URL = reverse("url-type-list") @@ -436,6 +438,32 @@ def test_create_project_status(auth_client): assert res.data["name"] == payload["name"] +def test_list_soc_broads(auth_client, soc_broad): + res = auth_client.get(SOC_BROAD_URL) + + assert res.status_code == status.HTTP_200_OK + assert len(res.data) == 1 + assert res.data[0]["title"] == soc_broad.title + assert res.data[0]["soc_minor"] == soc_broad.soc_minor.pk + + +def test_create_soc_broad(auth_client, soc_minor): + payload = { + "soc_minor": soc_minor.pk, + "occ_code": "15-1211", + "title": "Data Analysts", + } + + res = auth_client.post(SOC_BROAD_URL, payload) + + assert res.status_code == status.HTTP_201_CREATED + + created = SocBroad.objects.get(uuid=res.data["uuid"]) + assert created.soc_minor == soc_minor + assert created.occ_code == payload["occ_code"] + assert created.title == payload["title"] + + def test_create_soc_major(auth_client): """Test that we can create a soc major""" diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py index 2689a93b..0d438d0c 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -175,6 +175,18 @@ def test_leadership_type_project_relationship(project, leadership_type): assert project.leadership_type == leadership_type +def test_soc_broad_str(soc_broad): + assert str(soc_broad) == soc_broad.title + + +def test_soc_broad_relationships(soc_broad, soc_minor): + # forward relation + assert soc_broad.soc_minor == soc_minor + + # reverse relation via related_name + assert list(soc_minor.soc_broads.all()) == [soc_broad] + + def test_soc_major(soc_major): assert str(soc_major) == "Test Soc Major"