diff --git a/app/core/admin.py b/app/core/admin.py index 52c09077..a462934f 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -16,6 +16,7 @@ from .models import PracticeArea from .models import ProgramArea from .models import Project +from .models import ProjectStatus from .models import Sdg from .models import Skill from .models import SocMajor @@ -244,6 +245,11 @@ class CheckTypeAdmin(admin.ModelAdmin): list_display = ("name", "description") +@admin.register(ProjectStatus) +class ProjectStatusAdmin(admin.ModelAdmin): + list_display = ("name", "description") + + @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 cc9cc42d..b0e3e0b9 100644 --- a/app/core/api/serializers.py +++ b/app/core/api/serializers.py @@ -12,6 +12,7 @@ from core.models import PracticeArea from core.models import ProgramArea from core.models import Project +from core.models import ProjectStatus from core.models import Sdg from core.models import Skill from core.models import SocMajor @@ -362,6 +363,17 @@ class Meta: read_only_fields = ("uuid", "created_at", "updated_at") +class ProjectStatusSerializer(serializers.ModelSerializer): + """ + Used to retrieve project_status info + """ + + class Meta: + model = ProjectStatus + fields = ("uuid", "name", "description") + 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 7c6c2ef5..33ef9277 100644 --- a/app/core/api/urls.py +++ b/app/core/api/urls.py @@ -11,6 +11,7 @@ from .views import PermissionTypeViewSet from .views import PracticeAreaViewSet from .views import ProgramAreaViewSet +from .views import ProjectStatusViewSet from .views import ProjectViewSet from .views import SdgViewSet from .views import SkillViewSet @@ -47,6 +48,7 @@ basename="affiliation", ) router.register(r"check-types", CheckTypeViewSet, basename="check-type") +router.register(r"project-statuses", ProjectStatusViewSet, basename="project-status") router.register(r"soc-majors", SocMajorViewSet, basename="soc-major") router.register(r"url-types", UrlTypeViewSet, basename="url-type") router.register( diff --git a/app/core/api/views.py b/app/core/api/views.py index f537e7b8..679a3caa 100644 --- a/app/core/api/views.py +++ b/app/core/api/views.py @@ -22,6 +22,7 @@ from ..models import PracticeArea from ..models import ProgramArea from ..models import Project +from ..models import ProjectStatus from ..models import Sdg from ..models import Skill from ..models import SocMajor @@ -41,6 +42,7 @@ from .serializers import PracticeAreaSerializer from .serializers import ProgramAreaSerializer from .serializers import ProjectSerializer +from .serializers import ProjectStatusSerializer from .serializers import SdgSerializer from .serializers import SkillSerializer from .serializers import SocMajorSerializer @@ -356,6 +358,20 @@ class CheckTypeViewSet(viewsets.ModelViewSet): serializer_class = CheckTypeSerializer +@extend_schema_view( + list=extend_schema(description="Return a list of all the project statuses"), + create=extend_schema(description="Create a new project status"), + retrieve=extend_schema(description="Return the details of an project status"), + destroy=extend_schema(description="Delete a project status"), + update=extend_schema(description="Update a project status"), + partial_update=extend_schema(description="Patch a project status"), +) +class ProjectStatusViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated] + queryset = ProjectStatus.objects.all() + serializer_class = ProjectStatusSerializer + + @extend_schema_view( list=extend_schema(description="Return a list of all the user permissions"), retrieve=extend_schema(description="Return the details of a user permission"), diff --git a/app/core/initial_data/PD_ Table and field explanations - ProjectStatus - Data.csv b/app/core/initial_data/PD_ Table and field explanations - ProjectStatus - Data.csv new file mode 100644 index 00000000..44c1186b --- /dev/null +++ b/app/core/initial_data/PD_ Table and field explanations - ProjectStatus - Data.csv @@ -0,0 +1,6 @@ +name,description +Active,Has a project team and current meetings +On Hold,No project team or meetings scheduled +Completed,Project is completed +Closed,"Closed, possibly not completed (usually does not show up on the website). Unlikely to be reopened." +Deleted,"Holds for 90 days before final removal (used for test project entries, or mistakes that do not need to be remembered)" diff --git a/app/core/migrations/0032_projectstatus_project_current_status_id.py b/app/core/migrations/0032_projectstatus_project_current_status_id.py new file mode 100644 index 00000000..e1ae26af --- /dev/null +++ b/app/core/migrations/0032_projectstatus_project_current_status_id.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.11 on 2024-11-21 05:47 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0031_userstatustype_user_user_status"), + ] + + operations = [ + migrations.CreateModel( + name="ProjectStatus", + 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"), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="project", + name="current_status_id", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="core.projectstatus", + ), + ), + ] diff --git a/app/core/migrations/max_migration.txt b/app/core/migrations/max_migration.txt index 05eaff6d..19e29d8b 100644 --- a/app/core/migrations/max_migration.txt +++ b/app/core/migrations/max_migration.txt @@ -1 +1 @@ -0031_userstatustype_user_user_status +0032_projectstatus_project_current_status_id diff --git a/app/core/models.py b/app/core/models.py index 1fffb341..739ecc22 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -116,6 +116,18 @@ def __str__(self): return f"{self.email}" +class ProjectStatus(AbstractBaseModel): + """ + Dictionary of status options for project + """ + + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + + def __str__(self): + return f"{self.name}" + + class Project(AbstractBaseModel): """ List of projects @@ -137,7 +149,9 @@ class Project(AbstractBaseModel): "Authorization: token [gh_PAT]" \ https://api.github.com/repos/[org]/[repo]', ) - # current_status_id = models.ForeignKey("status", on_delete=models.PROTECT) + current_status_id = models.ForeignKey( + ProjectStatus, null=True, on_delete=models.PROTECT + ) hide = models.BooleanField(default=True) # location_id = models.ForeignKey("location", on_delete=models.PROTECT) google_drive_id = models.CharField(max_length=255, blank=True) diff --git a/app/core/tests/conftest.py b/app/core/tests/conftest.py index 7d2b2889..5cd6cad6 100644 --- a/app/core/tests/conftest.py +++ b/app/core/tests/conftest.py @@ -15,6 +15,7 @@ from ..models import PracticeArea from ..models import ProgramArea from ..models import Project +from ..models import ProjectStatus from ..models import Sdg from ..models import Skill from ..models import SocMajor @@ -289,6 +290,24 @@ def check_type(): ) +@pytest.fixture +def project_1(): + return Project.objects.create(name="Project 1") + + +@pytest.fixture +def project_2(): + return Project.objects.create(name="Project 2") + + +@pytest.fixture +def project_status(): + return ProjectStatus.objects.create( + name="This is a test project_status", + description="This is a test project_status", + ) + + @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 47ea463b..877f95ce 100644 --- a/app/core/tests/test_api.py +++ b/app/core/tests/test_api.py @@ -28,6 +28,7 @@ SDGS_URL = reverse("sdg-list") AFFILIATION_URL = reverse("affiliation-list") CHECK_TYPE_URL = reverse("check-type-list") +PROJECT_STATUSES_URL = reverse("project-status-list") SOC_MAJOR_URL = reverse("soc-major-list") URL_TYPE_URL = reverse("url-type-list") @@ -374,6 +375,16 @@ def test_create_check_type(auth_client): assert res.data["name"] == payload["name"] +def test_create_project_status(auth_client): + payload = { + "name": "This is a api-test project status name", + "description": "This is a api-test project status description", + } + res = auth_client.post(PROJECT_STATUSES_URL, payload) + assert res.status_code == status.HTTP_201_CREATED + assert res.data["name"] == payload["name"] + + 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 9de26469..27f06709 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -4,6 +4,7 @@ from ..models import Event from ..models import ProjectSdgXref +from ..models import ProjectStatus from ..models import Sdg pytestmark = pytest.mark.django_db @@ -169,6 +170,34 @@ def test_project_sdg_relationship(project): assert not climate_action_sdg.projects.contains(project) +def test_project_status(project_status): + assert str(project_status) == "This is a test project_status" + assert project_status.description == "This is a test project_status" + + +def test_project_has_a_project_status_relationship( + project_1, + project_2, +): + active_project_status = ProjectStatus.objects.get(name="Active") + closed_project_status = ProjectStatus.objects.get(name="Closed") + + active_project_status.project_set.add(project_1) + active_project_status.project_set.add(project_2) + assert active_project_status.project_set.count() == 2 + + assert project_1.current_status_id == active_project_status + assert project_2.current_status_id == active_project_status + + active_project_status.project_set.remove(project_1) + closed_project_status.project_set.add(project_1) + + assert active_project_status.project_set.count() == 1 + assert closed_project_status.project_set.count() == 1 + + assert project_1.current_status_id == closed_project_status + + def test_url_type(url_type): assert str(url_type) == "This is a test url type name" diff --git a/app/data/migrations/0009_projectstatus_seed.py b/app/data/migrations/0009_projectstatus_seed.py new file mode 100644 index 00000000..3b5542ba --- /dev/null +++ b/app/data/migrations/0009_projectstatus_seed.py @@ -0,0 +1,31 @@ +from django.db import migrations + +from core.models import ProjectStatus + + +def forward(__code__, __reverse_code__): + items = [ + ("Active", "Has a project team and current meetings"), + ("On Hold", "No project team or meetings scheduled "), + ("Completed", "Project is completed"), + ( + "Closed", + "Closed, possibly not completed (usually does not show up on the website). Unlikely to be reopened.", + ), + ( + "Deleted", + "Holds for 90 days before final removal (used for test project entries, or mistakes that do not need to be remembered)", + ), + ] + for name, description in items: + ProjectStatus.objects.create(name=name, description=description) + + +def reverse(__code__, __reverse_code__): + ProjectStatus.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [("data", "0008_userstatustype_seed")] + + operations = [migrations.RunPython(forward, reverse)] diff --git a/app/data/migrations/max_migration.txt b/app/data/migrations/max_migration.txt index dd9f83ee..da714ab9 100644 --- a/app/data/migrations/max_migration.txt +++ b/app/data/migrations/max_migration.txt @@ -1 +1 @@ -0008_userstatustype_seed +0009_projectstatus_seed