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
2 changes: 1 addition & 1 deletion docker/backend/post_deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ python3 manage.py makemigrations --no-input
python3 manage.py migrate --no-input

# Load fixtures
python3 manage.py loaddata 01_complexities 02_sections 03_axes 04_conditioners 05_ideologies 06_axis_definitions 07_conditioner_definitions
python3 manage.py loaddata 01_complexities 02_sections 03_axes 04_conditioners 05_ideologies 06_axis_definitions 07_conditioner_definitions 08_geography 09_religions 10_tags 11_associations

# Populate Test Data & Init MinIO (Only in Non-Prod)
if [ "$ENVIRONMENT" != "prod" ] && [ "$ENVIRONMENT" != "production" ]; then
Expand Down
2 changes: 0 additions & 2 deletions src/apps/core/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@
RegisterSerializer,
UserSetPasswordSerializer,
UserVerificationSerializer,
AffinitySerializer,
IdeologyAffinitySerializer,
)
34 changes: 1 addition & 33 deletions src/apps/core/api/serializers/user_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,14 @@
from core.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _
from ideology.api.serializers import (
ComplexityAffinitySerializer,
TargetIdeologySerializer,
)
from rest_framework import serializers
from rest_framework.serializers import ErrorDetail

from .base_user_serializers import PublicUserSerializer, SimpleUserSerializer
from .base_user_serializers import SimpleUserSerializer

logger = logging.getLogger(__name__)


class AffinitySerializer(serializers.Serializer):
target_user = PublicUserSerializer(read_only=True, allow_null=True)
total_affinity = serializers.FloatField(
min_value=0.0,
max_value=100.0,
allow_null=True,
source="total",
help_text=_("Overall affinity percentage. Null if no common axes."),
)
complexities = ComplexityAffinitySerializer(
many=True, help_text=_("Affinity grouped by abstraction level.")
)


class IdeologyAffinitySerializer(serializers.Serializer):
target_ideology = TargetIdeologySerializer(read_only=True, allow_null=True)
total_affinity = serializers.FloatField(
min_value=0.0,
max_value=100.0,
allow_null=True,
source="total",
help_text=_("Overall affinity percentage. Null if no common axes."),
)
complexities = ComplexityAffinitySerializer(
many=True, help_text=_("Affinity grouped by abstraction level.")
)


class UserVerificationSerializer(SimpleUserSerializer):
class Meta(SimpleUserSerializer.Meta):
fields = SimpleUserSerializer.Meta.fields + ["is_verified"]
Expand Down
10 changes: 0 additions & 10 deletions src/apps/core/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@
),
path("me/", core_views.MeDetailView.as_view(), name="me"),
path("me/password/", core_views.UserSetPasswordView.as_view(), name="set-password"),
path(
"users/affinity/<str:uuid>/",
core_views.UserAffinityView.as_view(),
name="user-affinity",
),
path(
"users/affinity/ideology/<str:uuid>/",
core_views.UserIdeologyAffinityView.as_view(),
name="user-ideology-affinity",
),
path(
"geography/countries/",
core_views.CountryListView.as_view(),
Expand Down
2 changes: 0 additions & 2 deletions src/apps/core/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@
from .user_views import (
MeDetailView,
UserSetPasswordView,
UserAffinityView,
UserIdeologyAffinityView,
)
48 changes: 1 addition & 47 deletions src/apps/core/api/views/user_views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
from core.api.permissions import IsVerified
from core.api.serializers import (
AffinitySerializer,
MeSerializer,
UserSetPasswordSerializer,
)
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from ideology.api.serializers import IdeologyAffinitySerializer
from ideology.models import CompletedAnswer, Ideology
from rest_framework import status
from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView, UpdateAPIView
from rest_framework.generics import RetrieveUpdateAPIView, UpdateAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

Expand Down Expand Up @@ -54,45 +50,3 @@ def update(self, request, *args, **kwargs):
serializer.save()

return Response(MeSerializer(user).data, status=status.HTTP_200_OK)


@extend_schema(
tags=["users"],
summary=_("Get affinity with a Completed Answer"),
description=_(
"Calculates the ideological affinity (0-100%) between the current user's active answers "
"and a specific CompletedAnswer (identified by UUID). The target might be anonymous."
),
)
class UserAffinityView(GenericAPIView):
permission_classes = [IsAuthenticated, IsVerified]
queryset = CompletedAnswer.objects.all()
lookup_field = "uuid"
serializer_class = AffinitySerializer

def get(self, request, *args, **kwargs):
target_answer = self.get_object()
data = request.user.calculate_detailed_affinity_with(target_answer)
serializer = self.get_serializer(data)
return Response(serializer.data, status=status.HTTP_200_OK)


@extend_schema(
tags=["users"],
summary=_("Get affinity with an Ideology"),
description=_(
"Calculates the ideological affinity (0-100%) between the current user's active answers "
"and the defined values of a specific Ideology (identified by UUID)."
),
)
class UserIdeologyAffinityView(GenericAPIView):
permission_classes = [IsAuthenticated, IsVerified]
queryset = Ideology.objects.all()
lookup_field = "uuid"
serializer_class = IdeologyAffinitySerializer

def get(self, request, *args, **kwargs):
ideology = self.get_object()
data = request.user.calculate_ideology_affinity(ideology)
serializer = self.get_serializer(data)
return Response(serializer.data, status=status.HTTP_200_OK)
2 changes: 1 addition & 1 deletion src/apps/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 6.0.1 on 2026-02-02 12:46
# Generated by Django 6.0.1 on 2026-02-04 18:07

import uuid

Expand Down
2 changes: 1 addition & 1 deletion src/apps/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .abstract import TimeStampedUUIDModel, UUIDModel
from .abstract import TimeStampedUUIDModel, UUIDModel, VisibleMixin
from .user import User
from .geo import Country, Region
12 changes: 12 additions & 0 deletions src/apps/core/models/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid

from django.db import models
from django.utils.translation import gettext_lazy as _
from model_utils import models as model_utils_models

logger = logging.getLogger(__name__)
Expand All @@ -26,3 +27,14 @@ def __str__(self):
class TimeStampedUUIDModel(UUIDModel, model_utils_models.TimeStampedModel):
class Meta:
abstract = True


class VisibleMixin(models.Model):
visible = models.BooleanField(
default=True,
verbose_name=_("Visible"),
help_text=_("Boolean to show if the item is visible or not in the frontend."),
)

class Meta:
abstract = True
1 change: 1 addition & 0 deletions src/apps/core/models/managers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .user_managers import CustomUserManager
from .mixins import VisibleManagerMixin
11 changes: 11 additions & 0 deletions src/apps/core/models/managers/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.db import models


class VisibleManagerMixin(models.Manager):
@property
def visible(self):
return self.get_queryset().filter(visible=True)

@property
def not_visible(self):
return self.get_queryset().filter(visible=False)
74 changes: 0 additions & 74 deletions src/apps/core/models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import uuid
from typing import Any

from core.exceptions.user_exceptions import UserAlreadyVerifiedException
from core.models.managers import CustomUserManager
Expand Down Expand Up @@ -142,76 +141,3 @@ def _schedule_tasks():
self,
send_notification,
)

def get_affinity_data(self, target_data: dict[str, dict]) -> dict[str, Any]:
from core.services.affinity_calculator import AffinityCalculator
from ideology.models import UserAxisAnswer

my_data = UserAxisAnswer.objects.get_mapped_for_calculation(self)
return AffinityCalculator(my_data, target_data).calculate_detailed()

@staticmethod
def _hydrate_affinity_structure(affinity_data: dict[str, Any]) -> dict[str, Any]:
from ideology.models import (
IdeologyAbstractionComplexity,
IdeologyAxis,
IdeologySection,
)

complexity_uuids = set()
section_uuids = set()
axis_uuids = set()

for complexity_item in affinity_data["complexities"]:
complexity_uuids.add(complexity_item["complexity_uuid"])
for section_item in complexity_item["sections"]:
section_uuids.add(section_item["section_uuid"])
for axis_item in section_item["axes"]:
axis_uuids.add(axis_item["axis_uuid"])

complexities_map = {
c.uuid.hex: c
for c in IdeologyAbstractionComplexity.objects.filter(
uuid__in=complexity_uuids
)
}
sections_map = {
s.uuid.hex: s
for s in IdeologySection.objects.filter(uuid__in=section_uuids)
}
axes_map = {
a.uuid.hex: a for a in IdeologyAxis.objects.filter(uuid__in=axis_uuids)
}

for complexity_item in affinity_data["complexities"]:
complexity_item["complexity"] = complexities_map.get(
complexity_item["complexity_uuid"]
)
for section_item in complexity_item["sections"]:
section_item["section"] = sections_map.get(section_item["section_uuid"])
for axis_item in section_item["axes"]:
axis_item["axis"] = axes_map.get(axis_item["axis_uuid"])

return affinity_data

def calculate_detailed_affinity_with(self, target_answer) -> dict[str, Any]:
target_data_mapped = target_answer.get_mapped_for_calculation()
affinity_data = self.get_affinity_data(target_data_mapped)
hydrated_data = self._hydrate_affinity_structure(affinity_data)

return {
"target_user": target_answer.completed_by,
"total": hydrated_data["total"],
"complexities": hydrated_data["complexities"],
}

def calculate_ideology_affinity(self, ideology) -> dict[str, Any]:
target_data_mapped = ideology.get_mapped_for_calculation()
affinity_data = self.get_affinity_data(target_data_mapped)
hydrated_data = self._hydrate_affinity_structure(affinity_data)

return {
"target_ideology": ideology,
"total": hydrated_data["total"],
"complexities": hydrated_data["complexities"],
}
Loading