Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create table project_program_area_xref #376

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ RUN \
apt-get update \
&& apt-get install --no-install-recommends -yqq \
netcat=1.10-46 \
gcc=4:10.2.1-1 \
graphviz=2.42.2-5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix the issue we were encountering when running ./scripts/buildrun.sh, removing this line was just a local quick fix. Instead it may be better to specify graphviz=2.42.* so that the newest patch is installed instead (i.e. graphviz_2.42.2-5+deb11u1 instead of graphviz_2.42.2-5).

gcc=4:10.2.1-1

# install dependencies
# Copy requirements file
COPY ./requirements.txt .

# hadolint ignore=DL3042
RUN \
--mount=type=cache,target=/root/.cache \
Expand Down
10 changes: 10 additions & 0 deletions app/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .models import PracticeArea
from .models import ProgramArea
from .models import Project
from .models import ProjectProgramAreaXref
from .models import Sdg
from .models import Skill
from .models import StackElementType
Expand Down Expand Up @@ -123,6 +124,15 @@ class ProjectAdmin(admin.ModelAdmin):
)


@admin.register(ProjectProgramAreaXref)
class ProjectProgramAreaXrefAdmin(admin.ModelAdmin):
list_display = (
"project_id",
"program_area_id",
"created_date",
)


@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = (
Expand Down
12 changes: 12 additions & 0 deletions app/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from core.models import PracticeArea
from core.models import ProgramArea
from core.models import Project
from core.models import ProjectProgramAreaXref
from core.models import Sdg
from core.models import Skill
from core.models import StackElementType
Expand Down Expand Up @@ -328,3 +329,14 @@ class Meta:
model = CheckType
fields = ("uuid", "name", "description")
read_only_fields = ("uuid", "created_at", "updated_at")


class ProjectProgramAreaXrefSerializer(serializers.ModelSerializer):
"""
Used to retrieve ProjectProgramAreaXref
"""

class Meta:
model = ProjectProgramAreaXref
fields = ("uuid", "project_id", "program_area_id", "created_date")
read_only_fields = ("uuid", "created_date")
7 changes: 7 additions & 0 deletions app/core/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .views import PermissionTypeViewSet
from .views import PracticeAreaViewSet
from .views import ProgramAreaViewSet
from .views import ProjectProgramAreaXrefViewSet
from .views import ProjectViewSet
from .views import SdgViewSet
from .views import SkillViewSet
Expand Down Expand Up @@ -42,6 +43,12 @@
basename="affiliation",
)
router.register(r"check-types", CheckTypeViewSet, basename="check-type")
router.register(
r"project-program-area-xrefs",
ProjectProgramAreaXrefViewSet,
basename="project-program-area-xref",
)

urlpatterns = [
path("me/", UserProfileAPIView.as_view(), name="my_profile"),
]
Expand Down
18 changes: 18 additions & 0 deletions app/core/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..models import PracticeArea
from ..models import ProgramArea
from ..models import Project
from ..models import ProjectProgramAreaXref
from ..models import Sdg
from ..models import Skill
from ..models import StackElementType
Expand All @@ -36,6 +37,7 @@
from .serializers import PermissionTypeSerializer
from .serializers import PracticeAreaSerializer
from .serializers import ProgramAreaSerializer
from .serializers import ProjectProgramAreaXrefSerializer
from .serializers import ProjectSerializer
from .serializers import SdgSerializer
from .serializers import SkillSerializer
Expand Down Expand Up @@ -346,3 +348,19 @@ class CheckTypeViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = CheckType.objects.all()
serializer_class = CheckTypeSerializer


@extend_schema_view(
list=extend_schema(description="Return a list of all project program area xref"),
create=extend_schema(description="Create a new project program area xref"),
retrieve=extend_schema(
description="Return the details of a project program area xref"
),
destroy=extend_schema(description="Delete a project program area xref"),
update=extend_schema(description="Update a project program area xref"),
partial_update=extend_schema(description="Patch a project program area xref"),
)
class ProjectProgramAreaXrefViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = ProjectProgramAreaXref.objects.all()
serializer_class = ProjectProgramAreaXrefSerializer
60 changes: 60 additions & 0 deletions app/core/migrations/0025_projectprogramareaxref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated by Django 4.2.11 on 2024-08-29 19:28

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
("core", "0024_checktype"),
]

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"),
),
(
"created_date",
models.DateTimeField(
blank=True, null=True, verbose_name="Created at"
),
),
(
"program_area_id",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="core.programarea",
),
),
(
"project_id",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.project"
),
),
],
options={
"abstract": False,
},
),
]
2 changes: 1 addition & 1 deletion app/core/migrations/max_migration.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0024_checktype
0025_projectprogramareaxref
9 changes: 9 additions & 0 deletions app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,12 @@ class CheckType(AbstractBaseModel):

def __str__(self):
return f"{self.name}"


class ProjectProgramAreaXref(AbstractBaseModel):
project_id = models.ForeignKey(Project, on_delete=models.CASCADE)
program_area_id = models.ForeignKey(ProgramArea, on_delete=models.CASCADE)
created_date = models.DateTimeField("Created at", null=True, blank=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inherited AbstractBaseModel class already creates a timestamp for creation; see here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did they use the DateTimeField in the Add the Model section of this guide: https://hackforla.github.io/peopledepot/how-to/add-model-and-api-endpoints/, and in the Project model as "completed_at", in the Event model as "start_time", and in the Affiliation model as "ended_at"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@del9ra there's some translation required between the DB notation in the ERD and spreadsheet and the Django data fields. We use an abstract base class (ABC) so that all the models will have create and modify timestamps. In the database design, many but not all models have the timestamps. We decided to make that a standard thing at one point.

So you should ignore anything from the database table that does the same as create and update timestamps.

At the time, I was trying to decide what to call these standard timestamps, and I found some page which discussed "on" vs "at", where "on" is usually a date and "at" is usually a time. So I added "_at" to the end of the names. But eventually I found a similar TimeStampedModel ABC in the widely-used django package django-extensions. It would've been a good idea to use that too.

Copy link
Member

@fyliu fyliu Sep 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... looks like this xref table doesn't have any extra data fields other than the relations (foreign keys). It means that we don't really need to create a Django model for this table at all. I'll have to go though the other issues and pick out similar issues before someone works on them. Like the issue where I noted this.

Copy link
Member

@fyliu fyliu Sep 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@del9ra I'm sorry, but I need to change this issue from "create xref table" to "create many to many relation between Project and ProgramArea models". We don't have any existing code in the repository to serve as an example, but I can find some other examples if you want to work on it. This guide looks good for the Django model.

These may be helpful also:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did they use the DateTimeField in the Add the Model section of this guide: https://hackforla.github.io/peopledepot/how-to/add-model-and-api-endpoints/, and in the Project model as "completed_at", in the Event model as "start_time", and in the Affiliation model as "ended_at"?

@del9ra Did you get your question answered? I'm not sure why those were included either. This should be added to the meeting agenda if we can't resolve this. I'm not sure of how the decisions were made for completed, start, ended and so on. Looking at the ERD also shows that completed_at has a type of "date" versus "DateTimeField". It looks like the ERD also needs to be updated for at least Project, if not all the other tables.

Copy link
Member Author

@del9ra del9ra Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@del9ra I'm sorry, but I need to change this issue from "create xref table" to "create many to many relation between Project and ProgramArea models". We don't have any existing code in the repository to serve as an example, but I can find some other examples if you want to work on it. This guide looks good for the Django model.

These may be helpful also:

Sounds good to me. I’ll work on the many-to-many relation. Thanks for letting me know!

Copy link
Member Author

@del9ra del9ra Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@del9ra there's some translation required between the DB notation in the ERD and spreadsheet and the Django data fields. We use an abstract base class (ABC) so that all the models will have create and modify timestamps. In the database design, many but not all models have the timestamps. We decided to make that a standard thing at one point.

So you should ignore anything from the database table that does the same as create and update timestamps.

At the time, I was trying to decide what to call these standard timestamps, and I found some page which discussed "on" vs "at", where "on" is usually a date and "at" is usually a time. So I added "_at" to the end of the names. But eventually I found a similar TimeStampedModel ABC in the widely-used django package django-extensions. It would've been a good idea to use that too.

@fyliu I noticed that we have the ended_at field in the Affiliation model; ended in Permission model, project_sponsor_partner_xref, permission_history; ended_on in project_language_xref and project_sdg_xref. How does this work in this case? Do we need to create a single field name for all of them in the ABC model?

Copy link
Member

@fyliu fyliu Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@del9ra Thank you for bringing this up!
We could. It looks like the ended field applies to xref tables where the relationships ended but we're keeping the row around (soft delete).
On the one hand, we don't have a lot of those tables to apply it to, so we shouldn't do this for all the models. Then again, you pointed out that the few we have are already inconsistent in naming, so maybe we should do something to make/keep them consistent.

I'm thinking we should bring up the inconsistent naming to @ExperimentsInHonesty and @Neecolaa and at least make an issue (ER maybe) to fix it on some tables. Maybe make them all ended_at datetime stamps to be simpler and consistent with the other timestamps. It's more clear what day it is even with timezone differences.

We should bring this to the meeting and decide on whether or not to make an ABC model or something else to help force this consistency.

Here's a list of stakeholders and what they might care about most:

  • (developer) what's easier for developers to code with less required training and less error?
  • (reviewer) what's easier for reviewers to review with less required training and less error?
  • (project) what's a quicker fix?
  • (project) what's a better long term solution?


def __str__(self):
return f"Project Id: {self.project_id}, Program area Id: {self.program_area_id}"
16 changes: 16 additions & 0 deletions app/core/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
SDG_URL = reverse("sdg-list")
AFFILIATION_URL = reverse("affiliation-list")
CHECK_TYPE_URL = reverse("check-type-list")
PROJECT_PROGRAM_AREA_XREF_URL = reverse("project-program-area-xref-list")


CREATE_USER_PAYLOAD = {
"username": "TestUserAPI",
Expand Down Expand Up @@ -357,3 +359,17 @@ def test_create_check_type(auth_client):
res = auth_client.post(CHECK_TYPE_URL, payload)
assert res.status_code == status.HTTP_201_CREATED
assert res.data["name"] == payload["name"]


def test_create_project_program_area_xref(auth_client, project, program_area):
"""Test that we can create a project program area xref"""

payload = {
"project_id": project.uuid,
"program_area_id": program_area.uuid,
"created_date": "2024-08-30 02:34:00",
}
res = auth_client.post(PROJECT_PROGRAM_AREA_XREF_URL, payload)
assert res.status_code == status.HTTP_201_CREATED
assert res.data["project_id"] == payload["project_id"]
assert res.data["program_area_id"] == payload["program_area_id"]
52 changes: 52 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import pytest
from django.test import TestCase

from ..models import Event
from ..models import ProgramArea
from ..models import Project
from ..models import ProjectProgramAreaXref

pytestmark = pytest.mark.django_db

Expand Down Expand Up @@ -118,3 +122,51 @@ def test_affiliation_is_neither_partner_and_sponsor(affiliation4):
def test_check_type(check_type):
assert str(check_type) == "This is a test check_type."
assert check_type.description == "This is a test check_type description."


class ProjectProgramAreaXrefTestCase(TestCase):
def setUp(self):
# retrieve the objects from the DB
self.program_area1 = ProgramArea.objects.get(name="Workforce Development")
self.program_area2 = ProgramArea.objects.get(name="Civic Tech Infrastructure")

# Create a Project object
self.project = Project.objects.create(name="Hack for LA Site")

# Create ProjectProgramAreaXref objects to link the project to program areas
self.project_program_area_xref1 = ProjectProgramAreaXref.objects.create(
project_id=self.project,
program_area_id=self.program_area1,
created_date="2024-08-30 02:34:00",
)
self.project_program_area_xref2 = ProjectProgramAreaXref.objects.create(
project_id=self.project,
program_area_id=self.program_area2,
created_date="2024-08-30 02:34:00",
)

def test_project_to_program_areas_relationship(self):
# Get all ProgramAreas associated with the project
program_areas = ProgramArea.objects.filter(
projectprogramareaxref__project_id=self.project
)
assert program_areas.count() == 2
# check if program_area1 is included in the retrieved program_areas
assert self.program_area1 in program_areas
assert self.program_area2 in program_areas

def test_program_area_to_projects_relationship(self):
# Get all Projects associated with the first ProgramArea
projects_for_area1 = Project.objects.filter(
projectprogramareaxref__program_area_id=self.program_area1
)
assert projects_for_area1.count() == 1
# asserts that self.project is included in the retrieved projects_for_area1
assert self.project in projects_for_area1

# Get all Projects associated with the second ProgramArea
projects_for_area2 = Project.objects.filter(
projectprogramareaxref__program_area_id=self.program_area2
)
assert projects_for_area2.count() == 1
assert self.project in projects_for_area2