From 0019f227cfc5f4895481a3f1ae5eabf2c29d5a31 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Wed, 13 Nov 2024 18:13:50 +0100 Subject: [PATCH] Update achievement_score --- lego/api/v1.py | 4 +- lego/apps/achievements/models.py | 8 +- lego/apps/achievements/serializers.py | 2 - .../achievements/utils/calculation_utils.py | 76 ++++++++----------- lego/apps/achievements/views.py | 6 +- lego/apps/users/models.py | 5 +- lego/apps/users/serializers/users.py | 4 +- 7 files changed, 48 insertions(+), 57 deletions(-) diff --git a/lego/api/v1.py b/lego/api/v1.py index 430aca96e..e2ee1fad9 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -82,7 +82,9 @@ from lego.utils.views import SiteMetaViewSet router = routers.DefaultRouter() -router.register(r"achievements/leaderboard", LeaderBoardViewSet, basename="achievements") +router.register( + r"achievements/leaderboard", LeaderBoardViewSet, basename="achievements" +) router.register(r"announcements", AnnouncementViewSet, basename="announcements") router.register(r"articles", ArticlesViewSet) router.register(r"bdb", AdminCompanyViewSet, basename="bdb") diff --git a/lego/apps/achievements/models.py b/lego/apps/achievements/models.py index 71c5bcee8..42889b718 100644 --- a/lego/apps/achievements/models.py +++ b/lego/apps/achievements/models.py @@ -1,7 +1,6 @@ from django.db import models from lego.apps.users.models import User -from lego.utils.decorators import abakus_cached_property from lego.utils.models import BasisModel from .constants import ACHIEVEMENT_IDENTIFIERS @@ -29,8 +28,11 @@ def percentage(self): def save(self, *args, **kwargs): super().save(*args, **kwargs) - if hasattr(self.user, '_cached_properties') and 'achievement_score' in self.user.__dict__: - del self.user.__dict__['achievement_score'] + if ( + hasattr(self.user, "_cached_properties") + and "achievement_score" in self.user.__dict__ + ): + del self.user.__dict__["achievement_score"] class Meta: constraints = [ diff --git a/lego/apps/achievements/serializers.py b/lego/apps/achievements/serializers.py index d43ffe43a..478b173ce 100644 --- a/lego/apps/achievements/serializers.py +++ b/lego/apps/achievements/serializers.py @@ -1,7 +1,5 @@ from lego.apps.achievements.models import Achievement -from lego.apps.achievements.utils.calculation_utils import calculate_user_rank from lego.utils.serializers import BasisModelSerializer -from rest_framework import serializers class AchievementSerializer(BasisModelSerializer): diff --git a/lego/apps/achievements/utils/calculation_utils.py b/lego/apps/achievements/utils/calculation_utils.py index b1b1c1745..5a6679f90 100644 --- a/lego/apps/achievements/utils/calculation_utils.py +++ b/lego/apps/achievements/utils/calculation_utils.py @@ -1,52 +1,40 @@ - -import math - -from lego.apps.achievements.constants import EVENT_IDENTIFIER, EVENT_PRICE_IDENTIFIER, EVENT_RANK_IDENTIFIER, MEETING_IDENTIFIER, PENALTY_IDENTIFIER, POLL_IDENTIFIER, QUOTE_IDENTIFIER - -ACHIEVEMENT_DATA = { - EVENT_IDENTIFIER: [0, 1, 2, 3, 4, 6], - EVENT_RANK_IDENTIFIER: [7, 8, 9], - QUOTE_IDENTIFIER: [2], - EVENT_PRICE_IDENTIFIER: [2, 3, 5], - MEETING_IDENTIFIER: [2], - POLL_IDENTIFIER: [0, 2, 4], - PENALTY_IDENTIFIER: [0, 3, 5, 6], +from lego.apps.achievements.constants import ( + EVENT_IDENTIFIER, + EVENT_PRICE_IDENTIFIER, + EVENT_RANK_IDENTIFIER, + MEETING_IDENTIFIER, + PENALTY_IDENTIFIER, + POLL_IDENTIFIER, + QUOTE_IDENTIFIER, +) + +ACHIEVEMENT_RARITIES = { + EVENT_IDENTIFIER: [0, 1, 2, 3, 4, 6], + EVENT_RANK_IDENTIFIER: [7, 8, 9], + QUOTE_IDENTIFIER: [1], + EVENT_PRICE_IDENTIFIER: [2, 3, 5], + MEETING_IDENTIFIER: [1], + POLL_IDENTIFIER: [0, 2, 4], + PENALTY_IDENTIFIER: [0, 3, 5, 6], } +delta = 0.1 -def calculate_user_rank(user, alpha=0.68, beta=0.45, gamma=0.43, w=0.75): - N = len(ACHIEVEMENT_DATA) - highest_rarity = 0 - weighted_rarities_product = 1 - achievement_count = user.achievements.count() - - if achievement_count == 0: - return 0 - - for achievement in user.achievements.all(): - identifier = achievement.identifier - level = achievement.level - - rarity_list = ACHIEVEMENT_DATA.get(identifier, []) - rarity = rarity_list[level] if level < len(rarity_list) else 0 - - highest_rarity = max(highest_rarity, rarity) - - weighted_rarity = math.log(rarity + 2) ** 2 - weighted_rarities_product *= weighted_rarity +max_possible_score = sum( + max(rarity_list) + 1 + (max(rarity_list) * delta) + for key, rarity_list in ACHIEVEMENT_RARITIES.items() + if key != EVENT_RANK_IDENTIFIER +) - G = math.pow(weighted_rarities_product, 1 / achievement_count) - - G_normalized = G / (math.log(9 + 2) ** 2) - baseline = (highest_rarity + 1) / 2 +def calculate_user_rank(user): + score = 0.0 - G_blended = w * G_normalized + (1 - w) * baseline / (math.log(9 + 2) ** 2) + user_achievements = user.achievements.all() + for achievement in user_achievements: + rarity_list = ACHIEVEMENT_RARITIES.get(achievement.identifier, []) - highest_rarity_component = alpha * (math.sqrt(highest_rarity + 1) / math.sqrt(10)) - geometric_mean_component = beta * G_blended - achievement_count_component = gamma * (math.log(achievement_count + 1) / math.log(N + 1)) + value = rarity_list[achievement.level] + score += value + 1 + (achievement.level * delta) - rank = (100 * (highest_rarity_component + geometric_mean_component + achievement_count_component)) / (alpha + beta + gamma) - rounded = round(rank, 2) - return rounded \ No newline at end of file + return round((score / max_possible_score) * 100, 2) if max_possible_score else 0 diff --git a/lego/apps/achievements/views.py b/lego/apps/achievements/views.py index 3e32780e7..f3a1a2093 100644 --- a/lego/apps/achievements/views.py +++ b/lego/apps/achievements/views.py @@ -1,8 +1,8 @@ -from rest_framework import permissions, mixins, viewsets +from rest_framework import mixins, permissions, viewsets from rest_framework.response import Response -from lego.apps.users.serializers.users import PublicUserWithGroupsSerializer from lego.apps.users.models import User +from lego.apps.users.serializers.users import PublicUserWithGroupsSerializer class LeaderBoardViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): @@ -20,4 +20,4 @@ def list(self, request, *args, **kwargs): serializer.data, key=lambda x: x["achievement_score"], reverse=True )[:50] - return Response(sorted_data) \ No newline at end of file + return Response(sorted_data) diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index 453d2f1b0..93fba5805 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -10,8 +10,8 @@ from django.db import models, transaction from django.db.models import Q from django.utils import timezone -from django.utils.timezone import datetime, timedelta from django.utils.functional import cached_property +from django.utils.timezone import datetime, timedelta from mptt.fields import TreeForeignKey from mptt.models import MPTTModel @@ -446,10 +446,11 @@ def email_address(self): # Return the internal address if all requirements for a GSuite account are met. return internal_address return self.email - + @cached_property def achievement_score(self): from lego.apps.achievements.utils.calculation_utils import calculate_user_rank + return calculate_user_rank(self) @profile_picture.setter # type: ignore diff --git a/lego/apps/users/serializers/users.py b/lego/apps/users/serializers/users.py index 5921df99f..58285b089 100644 --- a/lego/apps/users/serializers/users.py +++ b/lego/apps/users/serializers/users.py @@ -59,7 +59,7 @@ class Meta(PublicUserSerializer.Meta): "past_memberships", "memberships", "achievements", - "achievement_score" + "achievement_score", ) @@ -251,7 +251,7 @@ class Meta: "github_username", "linkedin_id", "achievements", - "achievement_score" + "achievement_score", )