Skip to content
Open
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
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 @@ -89,6 +90,7 @@
PeopleGroupRemoveFeaturedProjectsSerializer,
PeopleGroupRemoveTeamMembersSerializer,
PeopleGroupSerializer,
PeopleGroupSuperLightSerializer,
PrivacySettingsSerializer,
UserAdminListSerializer,
UserLighterSerializer,
Expand Down Expand Up @@ -518,7 +520,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 @@ -530,6 +534,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 @@ -556,9 +564,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", "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"]
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