From 8eed634afaf65d16c66d3e00be61574b8bb46883 Mon Sep 17 00:00:00 2001 From: dmartin4820 Date: Tue, 17 Sep 2024 22:30:47 -0700 Subject: [PATCH] docs(add-model): create tips for many-to-many relationships --- docs/how-to/add-model-and-api-endpoints.md | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/docs/how-to/add-model-and-api-endpoints.md b/docs/how-to/add-model-and-api-endpoints.md index ef67ac5c..1365208f 100644 --- a/docs/how-to/add-model-and-api-endpoints.md +++ b/docs/how-to/add-model-and-api-endpoints.md @@ -51,6 +51,23 @@ Let's start! assert str(recurring_event) == payload["name"] ``` + For testing many-to-many relationships, we can add + + ```python title="app/core/tests/test_models.py" linenums="1" + def test_project_recurring_event_relationship(project): + recurring_event = RecurringEvent.objects.get(name="{Name of Recurring Event}") + + project.recurring_events.add(recurring_event) + assert project.recurring_events.count() == 1 + assert project.recurring_events.contains(recurring_event) + assert recurring_event.projects.contains(project) + + project.sdgs.remove(recurring_event) + assert project.recurring_events.count() == 0 + assert not project.recurring_events.contains(recurring_event) + assert not recurring_event.projects.contains(project) + ``` + 1. See it fail ```bash @@ -99,6 +116,45 @@ class RecurringEvent(AbstractBaseModel): # (1)! 1. Try to add the relationships to non-existent models, but comment them out. Another developer will complete them when they go to implement those models. 1. Always override the `__str__` function to output something more meaningful than the default. It lets us do a quick test of the model by calling `str([model])`. It's also useful for the admin site model list view. +??? note "Updating models.py for many-to-many relationships" + For adding many-to-many relationships with additional fields, such as `ended_on`, we can add + + ```python title="app/core/tests/test_models.py" linenums="1" + class Project(AbstractBaseModel): + ... + recurring_events = models.ManyToManyField( + "RecurringEvent", + related_name="projects", + blank=True, + through="ProjectRecurringEventXref", + ) + ... + + + class ProjectRecurringEventXref(AbstractBaseModel): + """ + Joins a recurring event to a project + """ + + recurring_event_id = models.ForeignKey(RecurringEvent, on_delete=models.CASCADE) + project_id = models.ForeignKey(Project, on_delete=models.CASCADE) + ended_on = models.DateField("Ended on", null=True, blank=True) + ``` + + For adding many-to-many relationships without additional fields, we can just add + + ```python title="app/core/tests/test_models.py" linenums="1" + class Project(AbstractBaseModel): + ... + recurring_events = models.ManyToManyField( + "RecurringEvent", + related_name="projects", + blank=True, + through="ProjectRecurringEventXref", + ) + ... + ``` + ### Run migrations This generates the database migration files @@ -247,6 +303,67 @@ This is code that serializes objects into strings for the API endpoints, and des In `app/core/api/serializers.py` +??? note "Updating serializers.py for many-to-many relationships" + Following the many-to-many relationship between project and recurring event from above, + + Update the existing serializer classes + + ```python title="app/core/api/serializers.py" linenums="1" + class ProjectSerializer(serializers.ModelSerializer): + """Used to retrieve project info""" + + recurring_events = serializers.StringRelatedField(many=True) + + class Meta: + model = Project + fields = ( + "uuid", + "name", + "description", + "created_at", + "updated_at", + "completed_at", + "github_org_id", + "github_primary_repo_id", + "hide", + "google_drive_id", + "image_logo", + "image_hero", + "image_icon", + "recurring_events", + ) + read_only_fields = ( + "uuid", + "created_at", + "updated_at", + "completed_at", + ) + + + class RecurringEventSerializer(serializers.ModelSerializer): + """Used to retrieve recurring_event info""" + + projects = serializers.StringRelatedField(many=True) + + class Meta: + model = RecurringEvent + fields = ( + "uuid", + "name", + "start_time", + "duration_in_min", + "video_conference_url", + "additional_info", + "project", + "projects", + ) + read_only_fields = ( + "uuid", + "created_at", + "updated_at", + ) + ``` + 1. Import the new model ```python title="app/core/api/serializers.py" linenums="1" @@ -491,6 +608,35 @@ In `app/core/api/urls.py` ./scripts/test.sh ``` +??? note "Test many-to-many relationships" + In `app/core/tests/test_api.py` + + 1. Import API URL + + ```python title="app/core/tests/test_api.py" linenums="1" + PROJECT_URL = reverse("project-list") + ``` + + 1. Add test case + + ```python title="app/core/tests/test_api.py" linenums="1" + def test_project_sdg_xref(auth_client, project, sdg): + project.sdgs.add(sdg) + project.save() + + proj_res = auth_client.get(PROJECT_URL) + sdg_res = auth_client.get(SDG_URL) + + assert filter(lambda proj: str(proj["uuid"]) == str(project.pk), proj_res.data) + assert filter(lambda _sdg: str(_sdg["uuid"]) == str(sdg.pk), sdg_res) + ``` + + 1. Run the test script to show it passing + + ```bash + ./scripts/test.sh + ``` + ??? note "Check and commit" This is a good place to pause, check, and commit progress.