Skip to content
6 changes: 3 additions & 3 deletions apps/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ def get_news_queryset(self) -> QuerySet["News"]:
self._news_queryset = News.objects.all()
else:
groups = PeopleGroup.objects.filter(groups__users=self)
organizations = self.get_related_organizations()
organizations = self.get_organizations_queryset()
self._news_queryset = News.objects.filter(
Q(visible_by_all=True)
| Q(people_groups__in=groups)
Expand All @@ -570,7 +570,7 @@ def get_instruction_queryset(self) -> QuerySet["Instruction"]:
self._instruction_queryset = Instruction.objects.all()
else:
groups = PeopleGroup.objects.filter(groups__users=self)
organizations = self.get_related_organizations()
organizations = self.get_organizations_queryset()
self._instruction_queryset = Instruction.objects.filter(
Q(visible_by_all=True)
| Q(people_groups__in=groups)
Expand All @@ -592,7 +592,7 @@ def get_event_queryset(self) -> QuerySet["Event"]:
self._event_queryset = Event.objects.all()
else:
groups = PeopleGroup.objects.filter(groups__users=self)
organizations = self.get_related_organizations()
organizations = self.get_organizations_queryset()
self._event_queryset = Event.objects.filter(
Q(visible_by_all=True)
| Q(people_groups__in=groups)
Expand Down
37 changes: 20 additions & 17 deletions apps/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from apps.commons.models import GroupData
from apps.commons.serializers import (
BaseLocationSerializer,
PrivacySerializer,
StringsImagesSerializer,
)
from apps.files.models import Image
Expand Down Expand Up @@ -69,6 +70,7 @@ class Meta:
@auto_translated
class UserSerializer(
ModulesSerializers,
PrivacySerializer,
StringsImagesSerializer,
serializers.ModelSerializer,
):
Expand Down Expand Up @@ -218,23 +220,6 @@ class Meta:
"profile_picture_natural_ratio",
]

@cached_property
def _user_acces(self):
request = self.context.get("request")
if request:
return request.user.get_user_queryset().values_list("pk", flat=True)
return []

def to_representation(self, instance: ProjectUser):
force_display = self.context.get("force_display", False)
if force_display or instance.pk in self._user_acces:
return super().to_representation(instance)

return {
**AnonymousUser.serialize(with_permissions=False),
"current_org_role": None,
}

def _validate_role(
self,
group: Group,
Expand Down Expand Up @@ -405,6 +390,24 @@ def to_internal_value(self, data):

return super().to_internal_value(data)

@cached_property
def _user_acces(self):
request = self.context.get("request")
if request:
return request.user.get_user_queryset().values_list("pk", flat=True)
return []

def to_representation(self, instance: ProjectUser):
# TODO(remi): optimize this
force_display = self.context.get("force_display", False)
if force_display or instance.pk in self._user_acces:
return super().to_representation(instance)

return {
**AnonymousUser.serialize(with_permissions=False),
"current_org_role": None,
}


@auto_translated
class UserLightSerializer(UserSerializer):
Expand Down
10 changes: 7 additions & 3 deletions apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def get_queryset(self):
if self.action == "admin_list":
queryset = self.annotate_keycloak_email_verified(queryset)

return queryset.select_related("researcher")
return queryset.select_related("researcher", "privacy_settings")

def get_object(self):
"""
Expand Down Expand Up @@ -257,7 +257,7 @@ def get_serializer_context(self):
permission_classes=[HasBasePermission("get_user_by_email", "accounts")],
)
def get_by_email(self, request, *args, **kwargs):
queryset = ProjectUser.objects.all()
queryset = ProjectUser.objects.all().select_related("privacy_settings")
if self.organization is not None:
queryset = self.annotate_organization_role(queryset, self.organization)
user = queryset.filter(
Expand Down Expand Up @@ -746,7 +746,11 @@ def remove_member(self, request, *args, **kwargs):
def member(self, request, *args, **kwargs):
group = self.get_object()

queryset = group.modules_by_user(request.user).members()
queryset = (
group.modules_by_user(request.user)
.members()
.select_related("privacy_settings")
)

page = self.paginate_queryset(queryset)
if page is not None:
Expand Down
97 changes: 9 additions & 88 deletions apps/commons/fields.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import inspect
from contextlib import suppress

from django.contrib.auth.models import Group
from django.db.models import Q, QuerySet
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
Expand All @@ -12,9 +9,7 @@
from rest_framework.serializers import BaseSerializer

from apps.accounts.models import PrivacySettings, ProjectUser
from apps.accounts.utils import get_superadmins_group
from apps.commons.models import GroupData
from services.crisalid.models import Researcher
from apps.commons.serializers import PrivacySerializer


@extend_schema_field(OpenApiTypes.UUID)
Expand Down Expand Up @@ -161,6 +156,8 @@ def to_internal_value(self, data):


class PrivacySettingFieldMixin:
parent: PrivacySerializer

def __init__(self, **kwargs):
self.privacy_field = kwargs.pop("privacy_field", "")
self.default_value = kwargs.pop("default_value", None)
Expand All @@ -169,90 +166,14 @@ def __init__(self, **kwargs):
]
super().__init__(**kwargs)

def _get_user(self, value):
if isinstance(value, ProjectUser):
return value
if isinstance(value, Researcher):
return value.user

user_data = getattr(self.parent, "instance", None) or getattr(
self.parent, "queryset", None
)
if (
user_data
and isinstance(user_data, QuerySet)
and user_data.model == ProjectUser
and user_data.count() == 1
):
return user_data.get()
if (
user_data
and isinstance(user_data, QuerySet)
and user_data.model == ProjectUser
and self.source_attrs
):
try:
return user_data.filter(**{self.source_attrs[0]: value}).first()
except TypeError: # filter raises a TypeError if queryset has been sliced
user_data = list(user_data)
if user_data and isinstance(user_data, ProjectUser):
return user_data
if user_data and isinstance(user_data, list) and len(user_data) == 1:
return user_data[0]
if user_data and isinstance(user_data, list) and self.source_attrs:
return [
user
for user in user_data
if getattr(user, self.source_attrs[0]) == value
][0]
if self.source_attrs:
with suppress(
ProjectUser.MultipleObjectsReturned, ProjectUser.DoesNotExist
):
return ProjectUser.objects.get(**{self.source_attrs[0]: value})
return None

def _check_privacy_settings(self, value):
instance = self._get_user(value)
assert isinstance(instance, ProjectUser)
request = self.context.get("request")
assert request is not None

if instance == request.user or request.user.groups.contains(
get_superadmins_group()
):
return True
settings, _ = PrivacySettings.objects.get_or_create(user=instance)
match getattr(settings, self.privacy_field):
case PrivacySettings.PrivacyChoices.PUBLIC:
return True
case PrivacySettings.PrivacyChoices.ORGANIZATION:
return instance.groups.filter(
organizations__isnull=False,
organizations__in=request.user.get_related_organizations(),
).exists()
case PrivacySettings.PrivacyChoices.HIDE:
if not request.user.is_authenticated or not isinstance(
request.user, ProjectUser
):
return False
return Group.objects.filter(
Q(
organizations__isnull=False,
organizations__in=instance.get_organizations_queryset(),
users=request.user,
)
& (
Q(data__role=GroupData.Role.ADMINS)
| Q(data__role=GroupData.Role.FACILITATORS)
)
).exists()
return False
def get_attribute(self, instance):
self._instance = instance
return super().get_attribute(instance)

def to_representation(self, value):
if self._check_privacy_settings(value):
return super().to_representation(value)
return self.default_value
if self.parent._field_is_private(self._instance, self.privacy_field):
return self.default_value
return super().to_representation(value)


class PrivacySettingProtectedCharField(PrivacySettingFieldMixin, serializers.CharField):
Expand Down
94 changes: 92 additions & 2 deletions apps/commons/serializers.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import logging
from collections.abc import Collection
from typing import Any
from functools import cache
from typing import Any, Optional

from django.contrib.auth.models import Group
from django.db.models import Model, Q
from django.utils.translation import gettext_lazy as _
from rest_framework import mixins, serializers, viewsets
from rest_framework.settings import import_from_string

from apps.accounts.models import ProjectUser
from apps.accounts.models import PrivacySettings, ProjectUser
from apps.commons.mixins import HasOwner
from apps.commons.models import GroupData
from apps.commons.utils import process_text, remove_images_text
from apps.files.models import Image
from apps.organizations.models import Organization
from apps.projects.models import Project

logger = logging.getLogger(__name__)


class ProjectRelatedSerializer(serializers.ModelSerializer):
"""Base serializer for serializers related to projects."""
Expand Down Expand Up @@ -216,3 +223,86 @@ def valiate_lat(self, value):

def valiate_lng(self, value):
return self._check_gis(super().validate_lng(value))


class PrivacySerializer:
instance: ProjectUser

def __init__(self, *ar, **kw):
super().__init__(*ar, **kw)
self._privacy_settings = cache(self._privacy_settings)

def _get_user(self, instance):
if isinstance(instance, ProjectUser):
return instance
if isinstance(instance, HasOwner):
return instance.get_owner()

logger.warning(
"Invalid get user from privacySerializer: user=%r", type(instance)
)
return None

def _privacy_settings(
self, instance
) -> tuple[Optional[PrivacySettings], bool, bool]:

instance = self._get_user(instance)

if instance is None:
return None, False, False
try:
settings = instance.privacy_settings
except ProjectUser.privacy_settings.RelatedObjectDoesNotExist:
# if user are not privacy_settings set, create a empty one (whitout save)
settings = PrivacySettings(user=instance)

request = self.context.get("request")

if request is None:
logger.warning("Request is not set in serialier %r", type(self))
return None, False, False

user: ProjectUser = request.user

if user.is_anonymous:
is_in_org = is_org_admin = False
elif user.pk == instance.pk or user.is_superuser:
is_in_org = is_org_admin = True
else:
is_in_org = instance.groups.filter(
organizations__isnull=False,
organizations__in=request.user.get_organizations_queryset(),
).exists()

is_org_admin = Group.objects.filter(
Q(
organizations__isnull=False,
organizations__in=instance.get_organizations_queryset(),
users=request.user,
)
& (
Q(data__role=GroupData.Role.ADMINS)
| Q(data__role=GroupData.Role.FACILITATORS)
)
).exists()

return settings, is_in_org, is_org_admin

def _field_is_private(self, instance, field: str) -> bool:
"""check if field from privacysettings is private from user"""

privacy_settings, is_in_org, is_org_admin = self._privacy_settings(instance)

# not privacy_settings, return all privayc field
if privacy_settings is None:
return True

match getattr(privacy_settings, field):
case PrivacySettings.PrivacyChoices.PUBLIC:
return False
case PrivacySettings.PrivacyChoices.ORGANIZATION:
return not is_in_org
case PrivacySettings.PrivacyChoices.HIDE:
return not is_org_admin
return True
3 changes: 2 additions & 1 deletion apps/invitations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ class AccessRequestSerializer(
slug_field="code", queryset=Organization.objects.all()
)
user = UserMultipleIdRelatedField(
queryset=ProjectUser.objects.all(), allow_null=True
queryset=ProjectUser.objects.all().select_related("privacy_settings"),
allow_null=True,
)

class Meta:
Expand Down
9 changes: 8 additions & 1 deletion apps/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from .document import DocumentModules
from .group import PeopleGroupModules
from .project import ProjectModules
from .tab import TabModules
from .user import UserModules

__all__ = ["PeopleGroupModules", "ProjectModules", "TabModules", "UserModules"]
__all__ = [
"PeopleGroupModules",
"ProjectModules",
"TabModules",
"UserModules",
"DocumentModules",
]
Loading
Loading