Skip to content
9 changes: 8 additions & 1 deletion apps/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
HasMultipleIDs,
HasOwner,
HasPermissionsSetup,
HasRelatedLocationContent,
HasRelatedModules,
OrganizationRelated,
)
Expand All @@ -44,7 +45,9 @@
from services.translator.mixins import HasAutoTranslatedFields


class PeopleGroupLocation(OrganizationRelated, AbstractLocation):
class PeopleGroupLocation(
OrganizationRelated, HasRelatedLocationContent, AbstractLocation
):
"""base location for group"""

people_group = models.ForeignKey(
Expand All @@ -53,6 +56,10 @@ class PeopleGroupLocation(OrganizationRelated, AbstractLocation):
related_name="locations",
)

@classmethod
def get_related_content(cls):
return cls.people_group.field.name

def get_related_organizations(self) -> list["Organization"]:
return [self.people_group.organization]

Expand Down
15 changes: 11 additions & 4 deletions apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
MultipleIDViewsetMixin,
NestedOrganizationViewMixins,
NestedPeopleGroupViewMixins,
QuerySerializersMixin,
)
from apps.files.models import Image
from apps.files.views import ImageStorageView
Expand Down Expand Up @@ -90,6 +91,7 @@
PeopleGroupRemoveFeaturedProjectsSerializer,
PeopleGroupRemoveTeamMembersSerializer,
PeopleGroupSerializer,
PeopleGroupSuperLightSerializer,
PrivacySettingsSerializer,
UserAdminListSerializer,
UserLighterSerializer,
Expand Down Expand Up @@ -519,7 +521,9 @@ def refresh_keycloak_actions_link(self, request, *args, **kwargs):
)


class PeopleGroupViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet):
class PeopleGroupViewSet(
QuerySerializersMixin, MultipleIDViewsetMixin, viewsets.ModelViewSet
):
queryset = PeopleGroup.objects.all()
serializer_class = PeopleGroupSerializer
filterset_class = PeopleGroupFilter
Expand All @@ -531,6 +535,10 @@ class PeopleGroupViewSet(MultipleIDViewsetMixin, viewsets.ModelViewSet):
OrderingFilter,
)
multiple_lookup_fields = [(PeopleGroup, "id")]
query_serializers = {
"light": PeopleGroupLightSerializer,
"superlight": PeopleGroupSuperLightSerializer,
}

def get_permissions(self):
codename = map_action_to_permission(self.action, "peoplegroup")
Expand All @@ -557,9 +565,8 @@ def get_queryset(self) -> QuerySet:
return PeopleGroup.objects.none()

def get_serializer_class(self):
if self.action == "list":
return PeopleGroupLightSerializer
return self.serializer_class
query = "light" if self.action == "list" else None
return super().get_serializer_class(query)

def get_serializer_context(self):
context = super().get_serializer_context()
Expand Down
5 changes: 5 additions & 0 deletions apps/commons/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,8 @@ def similars(self, threshold: float = 0.15) -> QuerySet[Self]:
pk=self.pk
)
return type(self).objects.none()


class HasRelatedLocationContent:
def get_related_content(self):
raise NotImplementedError
28 changes: 27 additions & 1 deletion apps/commons/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.shortcuts import get_object_or_404
from rest_framework import mixins, viewsets
from drf_spectacular.utils import OpenApiParameter as _OpenApiParameter
from rest_framework import mixins, serializers, viewsets
from rest_framework.response import Response
from rest_framework.settings import api_settings

Expand Down Expand Up @@ -165,3 +166,28 @@ def initial(self, request, *args, **kwargs):
)

super().initial(request, *args, **kwargs)


class QuerySerializersMixin:
"""return specified serializer from queryparams"""

query_serializers: dict[str, serializers.Serializer] = {}

def get_serializer_class(self, query=None) -> serializers.Serializer:
query = query or self.request.query_params.get("serializer")
serializer = None
if query:
serializer = self.query_serializers.get(query)

return serializer or super().get_serializer_class()

@classmethod
def OpenApiParameter( # noqa: N802
cls, serializers: dict[str, serializers.Serializer]
) -> _OpenApiParameter:
return _OpenApiParameter(
name="serializer",
description="change output serializer",
required=False,
enum=serializers.keys(),
)
18 changes: 15 additions & 3 deletions apps/newsfeed/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
from django.db import models

from apps.commons.enums import Language
from apps.commons.mixins import HasOwner, OrganizationRelated
from apps.commons.mixins import (
HasOwner,
HasRelatedLocationContent,
OrganizationRelated,
)
from apps.projects.models import AbstractLocation
from services.translator.mixins import HasAutoTranslatedFields

Expand Down Expand Up @@ -62,7 +66,7 @@ class NewsfeedType(models.TextChoices):
)


class NewsLocation(AbstractLocation):
class NewsLocation(HasRelatedLocationContent, AbstractLocation):
news = models.OneToOneField(
"newsfeed.News",
related_name="location",
Expand All @@ -71,6 +75,10 @@ class NewsLocation(AbstractLocation):
blank=True,
)

@classmethod
def get_related_content(cls):
return cls.news.field.name

def get_related_organizations(self) -> list["Organization"]:
"""Return the organizations related to this model."""
return self.news.get_related_organizations()
Expand Down Expand Up @@ -199,7 +207,7 @@ def is_owned_by(self, user: "ProjectUser") -> bool:
return self.owner == user


class EventLocation(AbstractLocation):
class EventLocation(HasRelatedLocationContent, AbstractLocation):
event = models.OneToOneField(
"newsfeed.Event",
related_name="location",
Expand All @@ -208,6 +216,10 @@ class EventLocation(AbstractLocation):
blank=False,
)

@classmethod
def get_related_content(cls):
return cls.event.field.name

def get_related_organizations(self) -> list["Organization"]:
"""Return the organizations related to this model."""
return self.event.get_related_organizations()
Expand Down
10 changes: 7 additions & 3 deletions apps/newsfeed/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from apps.accounts.permissions import HasBasePermission
from apps.commons.permissions import ReadOnly
from apps.commons.utils import map_action_to_permission
from apps.commons.views import ListViewSet
from apps.commons.views import ListViewSet, QuerySerializersMixin
from apps.files.models import Image
from apps.files.views import ImageStorageView
from apps.organizations.permissions import HasOrganizationPermission
Expand All @@ -22,9 +22,11 @@
from .filters import EventFilter, InstructionFilter, NewsFilter
from .models import Event, Instruction, News, Newsfeed
from .serializers import (
EventLightSerializer,
EventSerializer,
InstructionSerializer,
NewsfeedSerializer,
NewsLightSerializer,
NewsSerializer,
)

Expand Down Expand Up @@ -119,7 +121,7 @@ def get_queryset(self):
return self.merge_querysets(announcements, news, projects)


class NewsViewSet(viewsets.ModelViewSet):
class NewsViewSet(QuerySerializersMixin, viewsets.ModelViewSet):
"""Main endpoints for news."""

serializer_class = NewsSerializer
Expand All @@ -128,6 +130,7 @@ class NewsViewSet(viewsets.ModelViewSet):
ordering_fields = ("updated_at", "created_at", "publication_date")
lookup_field = "id"
lookup_value_regex = "[^/]+"
query_serializers = {"light": NewsLightSerializer}

def get_permissions(self):
codename = map_action_to_permission(self.action, "news")
Expand Down Expand Up @@ -321,7 +324,7 @@ def add_image_to_model(self, image):
return None


class EventViewSet(viewsets.ModelViewSet):
class EventViewSet(QuerySerializersMixin, viewsets.ModelViewSet):
"""Main endpoints for projects."""

serializer_class = EventSerializer
Expand All @@ -330,6 +333,7 @@ class EventViewSet(viewsets.ModelViewSet):
ordering_fields = ("start_date", "end_date", "updated_at", "created_at")
lookup_field = "id"
lookup_value_regex = "[^/]+"
query_serializers = {"light": EventLightSerializer}

def get_permissions(self):
codename = map_action_to_permission(self.action, "event")
Expand Down
7 changes: 6 additions & 1 deletion apps/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
HasMultipleIDs,
HasOwner,
HasPermissionsSetup,
HasRelatedLocationContent,
ProjectRelated,
)
from apps.commons.models import GroupData
Expand Down Expand Up @@ -896,7 +897,7 @@ class Meta:


# TODO(remi): rename to ProjectLocation ?
class Location(ProjectRelated, AbstractLocation):
class Location(ProjectRelated, HasRelatedLocationContent, AbstractLocation):
"""A project location on Earth.

Attributes
Expand All @@ -911,6 +912,10 @@ class Location(ProjectRelated, AbstractLocation):
Project, on_delete=models.CASCADE, related_name="locations"
)

@classmethod
def get_related_content(cls):
return cls.project.field.name

def get_related_project(self) -> Optional["Project"]:
"""Return the projects related to this model."""
return self.project
Expand Down
17 changes: 16 additions & 1 deletion apps/projects/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
)
from apps.skills.models import Tag
from apps.skills.serializers import TagRelatedField, TagSerializer
from services.translator.serializers import auto_translated
from services.translator.serializers import (
auto_translated,
generate_translated_fields,
)

from .exceptions import (
AddProjectToOrganizationPermissionError,
Expand Down Expand Up @@ -989,3 +992,15 @@ def get_string_images_kwargs(
"project_id": instance.tab.project.id,
"tab_id": instance.tab.id,
}


@generate_translated_fields(("title", "description"))
class GeneralLocationSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField()
description = serializers.CharField()
content_id = serializers.CharField()
content_type = serializers.CharField()
lat = serializers.FloatField()
lng = serializers.FloatField()
type = serializers.CharField()
20 changes: 13 additions & 7 deletions apps/projects/tests/views/test_read_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,15 @@ def test_list_project_location(self, role, retrieved_locations):
reverse("General-location-list", args=(self.organization.code,))
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()
locations = response.json()

# projects (from organization, not organization_other)
self.assertEqual(len(content["projects"]), len(retrieved_locations))
self.assertSetEqual(
{a["id"] for a in content["projects"]},
{
location["id"]
for location in locations
if location["content_type"] == "project"
},
{a.id for a in [self.locations[a] for a in retrieved_locations]},
)

Expand All @@ -141,11 +144,14 @@ def test_list_group_location(self, role, retrieved_locations):
reverse("General-location-list", args=(self.organization.code,))
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()
locations = response.json()

# groups (from organization, not organization_other)
self.assertEqual(len(content["groups"]), len(retrieved_locations))
# projects (from organization, not organization_other)
self.assertSetEqual(
{a["id"] for a in content["groups"]},
{
location["id"]
for location in locations
if location["content_type"] == "people_group"
},
{a.id for a in [self.locations_group[a] for a in retrieved_locations]},
)
34 changes: 34 additions & 0 deletions apps/projects/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import Any, TypeVar

from django.db.models import CharField, QuerySet, Value
from django.db.models.functions import Cast
from rest_framework import serializers
from rest_framework.utils import model_meta

from apps.organizations.models import Organization
from services.translator.serializers import prefix_fields_langs

from .models import Project

Expand Down Expand Up @@ -61,3 +64,34 @@ def compute_project_changes(
changes[attr] = (old, new)

return changes


def annotate_queryset_location(*querysets: QuerySet) -> QuerySet:
"""annoate queryset for lazy load linked elements"""

all_qs: QuerySet = None
fields = (
"id",
"lat",
"lng",
"type",
"content_id",
"content_type",
"title",
"description",
# add generate field text
*prefix_fields_langs(("title", "description")),
)

for queryset in querysets:
model = queryset.model
content = model.get_related_content()
qs = queryset.annotate(
# cast linked object to string (project is slug so string, but news/events is pk so int)
content_id=Cast(f"{content}_id", output_field=CharField()),
content_type=Value(content),
).values(*fields)

all_qs = qs if all_qs is None else all_qs.union(qs)

return all_qs
Loading
Loading