diff --git a/app/core/api/serializers.py b/app/core/api/serializers.py index cc9cc42d..8f70a418 100644 --- a/app/core/api/serializers.py +++ b/app/core/api/serializers.py @@ -104,6 +104,7 @@ class ProjectSerializer(serializers.ModelSerializer): """Used to retrieve project info""" sdgs = serializers.StringRelatedField(many=True) + program_areas = serializers.StringRelatedField(many=True) class Meta: model = Project @@ -122,6 +123,7 @@ class Meta: "image_hero", "image_icon", "sdgs", + "program_areas", ) read_only_fields = ( "uuid", @@ -232,9 +234,11 @@ class Meta: class ProgramAreaSerializer(serializers.ModelSerializer): """Used to retrieve program_area info""" + projects = serializers.StringRelatedField(many=True) + class Meta: model = ProgramArea - fields = ("uuid", "name", "description", "image") + fields = ("uuid", "name", "description", "image", "projects") read_only_fields = ("uuid", "created_at", "updated_at") diff --git a/app/core/migrations/0032_projectprogramareaxref_project_program_areas.py b/app/core/migrations/0032_projectprogramareaxref_project_program_areas.py new file mode 100644 index 00000000..fe8db8d0 --- /dev/null +++ b/app/core/migrations/0032_projectprogramareaxref_project_program_areas.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.11 on 2024-11-12 20:15 + +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="ProjectProgramAreaXref", + 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"), + ), + ( + "program_area", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.programarea", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.project" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="project", + name="program_areas", + field=models.ManyToManyField( + blank=True, + related_name="projects", + through="core.ProjectProgramAreaXref", + to="core.programarea", + ), + ), + ] diff --git a/app/core/migrations/max_migration.txt b/app/core/migrations/max_migration.txt index 05eaff6d..2fadbf53 100644 --- a/app/core/migrations/max_migration.txt +++ b/app/core/migrations/max_migration.txt @@ -1 +1 @@ -0031_userstatustype_user_user_status +0032_projectprogramareaxref_project_program_areas diff --git a/app/core/models.py b/app/core/models.py index 1fffb341..472a9b6b 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -149,6 +149,12 @@ class Project(AbstractBaseModel): sdgs = models.ManyToManyField( "Sdg", related_name="projects", blank=True, through="ProjectSdgXref" ) + program_areas = models.ManyToManyField( + "ProgramArea", + related_name="projects", + blank=True, + through="ProjectProgramAreaXref", + ) def __str__(self): return f"{self.name}" @@ -433,6 +439,11 @@ def __str__(self): return self.title +class ProjectProgramAreaXref(AbstractBaseModel): + project = models.ForeignKey(Project, on_delete=models.CASCADE) + program_area = models.ForeignKey(ProgramArea, on_delete=models.CASCADE) + + class ProjectSdgXref(AbstractBaseModel): """ Joins an SDG to a project diff --git a/app/core/tests/test_api.py b/app/core/tests/test_api.py index 47ea463b..7d606f53 100644 --- a/app/core/tests/test_api.py +++ b/app/core/tests/test_api.py @@ -20,10 +20,11 @@ FAQS_VIEWED_URL = reverse("faq-viewed-list") AFFILIATE_URL = reverse("affiliate-list") LOCATION_URL = reverse("location-list") -PROGRAM_AREA_URL = reverse("program-area-list") +PROGRAM_AREAS_URL = reverse("program-area-list") SKILL_URL = reverse("skill-list") STACK_ELEMENT_URL = reverse("stack-element-list") PERMISSION_TYPE = reverse("permission-type-list") +PROJECTS_URL = reverse("project-list") STACK_ELEMENT_TYPE_URL = reverse("stack-element-type-list") SDGS_URL = reverse("sdg-list") AFFILIATION_URL = reverse("affiliation-list") @@ -261,7 +262,7 @@ def test_create_program_area(auth_client): "description": "About program area", "image": "http://www.imageurl.com", } - res = auth_client.post(PROGRAM_AREA_URL, payload) + res = auth_client.post(PROGRAM_AREAS_URL, payload) assert res.status_code == status.HTTP_201_CREATED assert res.data["name"] == payload["name"] @@ -274,9 +275,9 @@ def test_list_program_area(auth_client): "description": "About program area", "image": "http://www.imageurl.com", } - res = auth_client.post(PROGRAM_AREA_URL, payload) + res = auth_client.post(PROGRAM_AREAS_URL, payload) - res = auth_client.get(PROGRAM_AREA_URL) + res = auth_client.get(PROGRAM_AREAS_URL) program_areas = ProgramArea.objects.all() expected_data = ProgramAreaSerializer(program_areas, many=True).data @@ -430,3 +431,24 @@ def test_create_user_status_type(auth_client): res = auth_client.post(USER_STATUS_TYPES_URL, payload) assert res.status_code == status.HTTP_201_CREATED assert res.data["name"] == payload["name"] + + +def test_project_program_area_xref(auth_client, project, program_area): + def get_object(objects, target_uuid): + for obj in objects: + if str(obj["uuid"]) == str(target_uuid): + return obj + return None + + project.program_areas.add(program_area) + proj_res = auth_client.get(PROJECTS_URL) + test_proj = get_object(proj_res.data, project.uuid) + assert test_proj is not None + assert len(test_proj["program_areas"]) == 1 + assert program_area.name in test_proj["program_areas"] + + program_area_res = auth_client.get(PROGRAM_AREAS_URL) + test_program_ar = get_object(program_area_res.data, program_area.uuid) + assert test_program_ar is not None + assert len(test_program_ar["projects"]) == 1 + assert project.name in test_program_ar["projects"] diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py index 9de26469..1dd364e8 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -3,6 +3,8 @@ import pytest from ..models import Event +from ..models import ProgramArea +from ..models import ProjectProgramAreaXref from ..models import ProjectSdgXref from ..models import Sdg @@ -149,6 +151,24 @@ def test_soc_major(soc_major): assert str(soc_major) == "Test Soc Major" +def test_project_program_area_relationship(project): + workforce_development_program_area = ProgramArea.objects.get( + name="Workforce Development" + ) + project.program_areas.add(workforce_development_program_area) + assert project.program_areas.count() == 1 + assert project.program_areas.contains(workforce_development_program_area) + assert workforce_development_program_area.projects.contains(project) + workforce_development_program_area_xref = ProjectProgramAreaXref.objects.get( + project=project, program_area=workforce_development_program_area + ) + assert workforce_development_program_area_xref.created_at is not None + project.program_areas.remove(workforce_development_program_area) + assert project.program_areas.count() == 0 + assert not workforce_development_program_area.projects.contains(project) + assert not project.program_areas.contains(workforce_development_program_area) + + def test_project_sdg_relationship(project): climate_action_sdg = Sdg.objects.get(name="Climate Action") diff --git a/scripts/createsuperuser.sh b/scripts/createsuperuser.sh index 9f7ffdb4..8c299a9d 100755 --- a/scripts/createsuperuser.sh +++ b/scripts/createsuperuser.sh @@ -8,5 +8,5 @@ set -x # This command requires the DJANGO_SUPERUSER_USERNAME and # DJANGO_SUPERUSER_PASSWORD environmental variables to be set when django starts -echo "DJANGO_SUPERUSER_USERNAME: $DJANGO_SUPERUSER_USERNAME" +# echo "DJANGO_SUPERUSER_USERNAME: $DJANGO_SUPERUSER_USERNAME" docker-compose exec web python manage.py createsuperuser --no-input