Skip to content
Merged
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
17 changes: 17 additions & 0 deletions apps/accounts/migrations/0006_alter_projectuser_managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 6.0.5 on 2026-09-07 08:11

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("accounts", "0005_alter_peoplegrouplocation_type"),
]

operations = [
migrations.AlterModelManagers(
name="projectuser",
managers=[],
),
]
56 changes: 53 additions & 3 deletions apps/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from apps.newsfeed.models import Event, Instruction, News
from apps.organizations.models import Organization
from apps.projects.models import AbstractLocation, Project
from apps.skills.models import Skill
from services.keycloak.exceptions import RemoteKeycloakAccountNotFound
from services.keycloak.interface import KeycloakService
from services.keycloak.models import KeycloakAccount
Expand Down Expand Up @@ -707,6 +708,43 @@ def get_event_related_queryset(
**{f"{event_related_name}__in": self.get_event_queryset()}
)

def get_skills_queryset(self) -> QuerySet["Skill"]:
if self.is_superuser:
return Skill.objects.all()

filters = (
# own user
Q(user__pk=self.pk)
|
# public all user quand see
Q(user__privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC)
# only user in same orga
| Q(
user__privacy_settings__skills=PrivacySettings.PrivacyChoices.ORGANIZATION,
user__groups__organizations__in=self.get_organizations_queryset(),
)
)

org_admin = Group.objects.filter(
Q(
organizations__isnull=False,
organizations__in=self.get_organizations_queryset(),
users=self,
)
& (
Q(name__contains=GroupData.Role.ADMINS)
| Q(name__contains=GroupData.Role.FACILITATORS)
)
)
filters |= Q(
user__privacy_settings__skills=PrivacySettings.PrivacyChoices.HIDE,
user__groups__organizations__in=Organization.objects.filter(
groups__in=org_admin
),
)

return Skill.objects.filter(filters).distinct()

def can_see_project(self, project: "Project") -> bool:
"""Whether the user can see the project."""
return self.get_project_queryset().contains(project)
Expand Down Expand Up @@ -985,9 +1023,18 @@ def get_permissions_representations(self):
"""Return a list of the permissions representations."""
return []

def get_organizations_queryset(self) -> QuerySet[Organization]:
"""Return the organizations related to this model."""
return Organization.objects.none()

def get_related_organizations(self) -> list["Organization"]:
"""Return the organizations related to this model."""
return []
return list(self.get_organizations_queryset())

def get_skills_queryset(self) -> QuerySet["Skill"]:
return Skill.objects.filter(
user__privacy_settings__skills=PrivacySettings.PrivacyChoices.PUBLIC
)


class InvitationUser(AnonymousUser):
Expand Down Expand Up @@ -1032,6 +1079,9 @@ def _query_function(self, queryset, *ar, **kw):
get_instruction_related_queryset = _query_function
get_event_related_queryset = _query_function

def get_related_organizations(self) -> list["Organization"]:
def get_organizations_queryset(self) -> QuerySet[Organization]:
"""Return the organizations related to this model."""
return list(Organization.objects.all())
return Organization.objects.all()

def get_skills_queryset(self) -> QuerySet["Skill"]:
return Skill.objects.all()
26 changes: 26 additions & 0 deletions apps/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ class UserSerializer(
read_only=True, required=False, allow_null=True
)

# Write only profile picture fields
profile_picture_file = serializers.ImageField(
write_only=True, required=False, allow_null=True
)
profile_picture_scale_x = serializers.FloatField(
write_only=True, required=False, allow_null=True
)
profile_picture_scale_y = serializers.FloatField(
write_only=True, required=False, allow_null=True
)
profile_picture_left = serializers.FloatField(
write_only=True, required=False, allow_null=True
)
profile_picture_top = serializers.FloatField(
write_only=True, required=False, allow_null=True
)
profile_picture_natural_ratio = serializers.FloatField(
write_only=True, required=False, allow_null=True
)

class Meta:
model = ProjectUser
read_only_fields = [
Expand Down Expand Up @@ -190,6 +210,12 @@ class Meta:
"skype",
"landline_phone",
"twitter",
"profile_picture_file",
"profile_picture_scale_x",
"profile_picture_scale_y",
"profile_picture_left",
"profile_picture_top",
"profile_picture_natural_ratio",
]

@cached_property
Expand Down
20 changes: 12 additions & 8 deletions apps/accounts/tests/views/test_privacy_settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def set_user_privacy_settings(user, privacy):
user.privacy_settings.email = privacy
user.privacy_settings.save()

def assert_fields_visible(self, user, data):
def assert_fields_visible(self, user, data, skills):
self.assertEqual(data["facebook"], user.facebook)
self.assertEqual(data["twitter"], user.twitter)
self.assertEqual(data["skype"], user.skype)
Expand All @@ -45,11 +45,11 @@ def assert_fields_visible(self, user, data):
self.assertEqual(data["website"], user.website)
self.assertEqual(data["profile_picture"]["id"], user.profile_picture.id)
self.assertEqual(
{skill["id"] for skill in data["skills"]},
{skill["id"] for skill in skills},
{skill.id for skill in user.skills.all()},
)

def assert_fields_hidden(self, data):
def assert_fields_hidden(self, data, skills):
self.assertIsNone(data["facebook"])
self.assertIsNone(data["twitter"])
self.assertIsNone(data["skype"])
Expand All @@ -61,7 +61,7 @@ def assert_fields_hidden(self, data):
self.assertIsNone(data["medium"])
self.assertIsNone(data["website"])
self.assertIsNone(data["profile_picture"])
self.assertEqual(data["skills"], [])
self.assertEqual(skills, [])

@parameterized.expand(
[
Expand All @@ -86,7 +86,7 @@ def assert_fields_hidden(self, data):
(TestRoles.OWNER, PrivacyChoices.HIDE, True),
(TestRoles.SUPERADMIN, PrivacyChoices.HIDE, True),
(TestRoles.ORG_ADMIN, PrivacyChoices.HIDE, True),
(TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, False),
(TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, True),
(TestRoles.ORG_USER, PrivacyChoices.HIDE, False),
(TestRoles.ORG_VIEWER, PrivacyChoices.HIDE, False),
]
Expand All @@ -108,10 +108,14 @@ def test_view_fields_retrieve_user(
self.client.force_authenticate(user)
response = self.client.get(reverse("ProjectUser-detail", args=(instance.id,)))
self.assertEqual(response.status_code, status.HTTP_200_OK)

skills = self.client.get(reverse("Skill-list", args=(instance.id,))).json()[
"results"
]
if fields_visible:
self.assert_fields_visible(instance, response.data)
self.assert_fields_visible(instance, response.data, skills)
else:
self.assert_fields_hidden(response.data)
self.assert_fields_hidden(response.data, skills)

@parameterized.expand(
[
Expand All @@ -134,7 +138,7 @@ def test_view_fields_retrieve_user(
(TestRoles.OWNER, PrivacyChoices.HIDE, True),
(TestRoles.SUPERADMIN, PrivacyChoices.HIDE, True),
(TestRoles.ORG_ADMIN, PrivacyChoices.HIDE, True),
(TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, False),
(TestRoles.ORG_FACILITATOR, PrivacyChoices.HIDE, True),
(TestRoles.ORG_USER, PrivacyChoices.HIDE, False),
]
)
Expand Down
11 changes: 6 additions & 5 deletions apps/accounts/tests/views/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ def test_notifications_count(self):
self.client.force_authenticate(user)
response = self.client.get(reverse("ProjectUser-detail", args=(user.id,)))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["notifications"], 5)
self.assertEqual(response.json()["modules"]["notifications"], 5)

@patch("services.keycloak.interface.KeycloakService.send_email")
def test_language_from_organization(self, mocked):
Expand Down Expand Up @@ -992,13 +992,14 @@ def test_get_people_groups(self):
]
)
response = self.client.get(
reverse("ProjectUser-detail", args=(user.id,))
reverse("ProjectUser-groups", args=(user.id,))
+ f"?current_org_pk={organization.pk}"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()
self.assertEqual(len(content["people_groups"]), 1)
self.assertEqual(content["people_groups"][0]["id"], people_group.id)
contents = response.json()["results"]

self.assertEqual(len(contents), 1)
self.assertEqual(contents[0]["id"], people_group.id)

def test_check_permissions(self):
user = UserFactory()
Expand Down
3 changes: 1 addition & 2 deletions apps/accounts/tests/views/test_user_publication_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,10 @@ def test_view_users_in_notifications(self, role, expected_users):
for user_type in self.users
}
response = self.client.get(
reverse("Notification-list", args=(organization.code,))
reverse("Notification-list", args=(organization.code, user.pk))
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = response.json()["results"]
self.assertEqual(len(content), len(expected_users))
self.assertEqual(
{
(notification["sender"]["id"], notification["id"])
Expand Down
50 changes: 23 additions & 27 deletions apps/accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import uuid
from functools import cached_property

from django.conf import settings
from django.db import transaction
from django.db.models import Case, Prefetch, Q, QuerySet, Value, When
from django.db.models import Case, Q, QuerySet, Value, When
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils import translation
Expand Down Expand Up @@ -58,7 +59,6 @@
from apps.organizations.permissions import HasOrganizationPermission
from apps.organizations.serializers import ProjectCategoryLightSerializer
from apps.projects.serializers import LocationSerializer, ProjectLightSerializer
from apps.skills.models import Skill
from services.google.models import GoogleAccount, GoogleGroup
from services.google.tasks import (
create_google_account,
Expand Down Expand Up @@ -151,6 +151,14 @@ def get_permissions(self):
]
return super().get_permissions()

@cached_property
def organization(self):
current_org_pk = self.request.query_params.get("current_org_pk")
if not current_org_pk:
return None

return get_object_or_404(Organization.objects.filter(pk=current_org_pk))

def annotate_organization_role(
self, queryset: QuerySet, organization: Organization
) -> QuerySet:
Expand Down Expand Up @@ -192,18 +200,13 @@ def annotate_keycloak_email_verified(self, queryset: QuerySet) -> QuerySet:

def get_queryset(self):
queryset = self.request.user.get_user_queryset()
organization_pk = self.request.query_params.get("current_org_pk")
if organization_pk is not None:
organization = Organization.objects.get(pk=organization_pk)
queryset = self.annotate_organization_role(queryset, organization)
if self.organization is not None:
queryset = self.annotate_organization_role(queryset, self.organization)

if self.action == "admin_list":
queryset = self.annotate_keycloak_email_verified(queryset)
skills_prefetch = Prefetch(
"skills", queryset=Skill.objects.select_related("tag")
)
return queryset.prefetch_related(skills_prefetch, "groups").select_related(
"researcher"
)

return queryset.select_related("researcher")

def get_object(self):
"""
Expand Down Expand Up @@ -233,11 +236,7 @@ def get_serializer_class(self):

def get_serializer_context(self):
context = super().get_serializer_context()
context.update({"request": self.request})
current_org_pk = self.request.query_params.get("current_org_pk")
if current_org_pk:
organization = get_object_or_404(Organization, pk=current_org_pk)
context.update({"organization": organization})
context.update({"request": self.request, "organization": self.organization})
return context

@extend_schema(
Expand All @@ -259,10 +258,8 @@ def get_serializer_context(self):
)
def get_by_email(self, request, *args, **kwargs):
queryset = ProjectUser.objects.all()
current_org_pk = request.query_params.get("current_org_pk")
if current_org_pk is not None:
organization = Organization.objects.get(pk=current_org_pk)
queryset = self.annotate_organization_role(queryset, organization)
if self.organization is not None:
queryset = self.annotate_organization_role(queryset, self.organization)
user = queryset.filter(
Q(email=kwargs.get("email")) | Q(personal_email=kwargs.get("email"))
).distinct()
Expand Down Expand Up @@ -373,7 +370,7 @@ def has_permissions(self, request, *args, **kwargs):
)
def groups(self, request, *args, **kwargs):
user = self.get_object()
queryset = user.modules_by_user(request.user).groups()
queryset = user.modules_by_user(request.user, self.organization).groups()

page = self.paginate_queryset(queryset)
if page is not None:
Expand Down Expand Up @@ -1102,17 +1099,16 @@ class UserProfilePictureView(NestedUserViewMixins, ImageStorageView):
]

def get_queryset(self):
return self.user.images.all()
return Image.objects.filter(user=self.user)

@staticmethod
def upload_to(instance, filename) -> str:
return f"account/profile/{uuid.uuid4()}#{instance.name}"

def add_image_to_model(self, image):
user = ProjectUser.objects.get(id=self.kwargs["user_id"])
user.profile_picture = image
user.save()
image.owner = user
self.user.profile_picture = image
self.user.save()
image.owner = self.user
image.save()
return f"/v1/user/{self.kwargs['user_id']}/profile-picture/{image.id}"

Expand Down
Loading
Loading