Skip to content

Commit

Permalink
Update achievement_score
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasdeluna committed Nov 13, 2024
1 parent dba3d4f commit de955c3
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 68 deletions.
4 changes: 3 additions & 1 deletion lego/api/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions lego/apps/achievements/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Achievement(TypedDict):

AchievementCollection = dict[str, Achievement]

#Remember to update rarity list in /utils/calculation_utils.py when adding new achievement

EVENT_IDENTIFIER = "event_count"
EVENT_RANK_IDENTIFIER = "event_rank"
Expand Down
8 changes: 5 additions & 3 deletions lego/apps/achievements/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import models

from lego.apps.achievements.utils.calculation_utils import calculate_user_rank
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
Expand Down Expand Up @@ -29,8 +29,10 @@ 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']

# Recalculate and update the user's achievement score
self.user.achievement_score = calculate_user_rank(self.user)
self.user.save(update_fields=["achievement_score"])

class Meta:
constraints = [
Expand Down
2 changes: 0 additions & 2 deletions lego/apps/achievements/serializers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
78 changes: 33 additions & 45 deletions lego/apps/achievements/utils/calculation_utils.py
Original file line number Diff line number Diff line change
@@ -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
#Remember to update this rarity list when adding new achievement
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
)

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

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
return score if MAX_POSSIBLE_SCORE else 0
17 changes: 8 additions & 9 deletions lego/apps/achievements/views.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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):
serializer_class = PublicUserWithGroupsSerializer
permission_classes = [permissions.IsAuthenticated]

def get_queryset(self):
return User.objects.filter(achievements__isnull=False).distinct()
return (
User.objects.filter(achievements__isnull=False)
.order_by("-achievement_score")
.distinct()[:50]
)

def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)

sorted_data = sorted(
serializer.data, key=lambda x: x["achievement_score"], reverse=True
)[:50]

return Response(sorted_data)
return Response(serializer.data)
28 changes: 28 additions & 0 deletions lego/apps/users/migrations/0045_user_achievement_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.apps import apps
from django.db import migrations, models


def populate_achievement_score(apps, schema_editor):
from lego.apps.achievements.utils.calculation_utils import calculate_user_rank

User = apps.get_model("users", "User")

for user in User.objects.all():
user.achievement_score = calculate_user_rank(user)
user.save(update_fields=["achievement_score"])


class Migration(migrations.Migration):

dependencies = [
("users", "0044_alter_membership_role_alter_membershiphistory_role"),
]

operations = [
migrations.AddField(
model_name="user",
name="achievement_score",
field=models.FloatField(default=0),
),
migrations.RunPython(populate_achievement_score),
]
13 changes: 7 additions & 6 deletions lego/apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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 mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
Expand Down Expand Up @@ -380,6 +379,7 @@ class User(
help_text="Enter a valid LinkedIn ID.",
validators=[linkedin_id_validator, ReservedNameValidator()],
)
achievement_score = models.FloatField(default=0, null=False, blank=False)

objects = AbakusUserManager() # type: ignore

Expand Down Expand Up @@ -422,6 +422,12 @@ def delete(self, using=None, force=False):
event.unregister(event.registrations.get(user=self))
super(User, self).delete(using=using, force=force)

def save(self, *args, **kwargs):
from lego.apps.achievements.utils.calculation_utils import calculate_user_rank

self.achievement_score = calculate_user_rank(self)
super().save(*args, **kwargs)

@property
def full_name(self):
return self.get_full_name()
Expand All @@ -446,11 +452,6 @@ 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
def profile_picture(self, value):
Expand Down
15 changes: 13 additions & 2 deletions lego/apps/users/serializers/users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import exceptions, serializers

from lego.apps.achievements.serializers import AchievementSerializer
from lego.apps.achievements.utils.calculation_utils import MAX_POSSIBLE_SCORE
from lego.apps.email.serializers import PublicEmailListSerializer
from lego.apps.files.fields import ImageField
from lego.apps.ical.models import ICalToken
Expand Down Expand Up @@ -53,13 +54,18 @@ class PublicUserWithGroupsSerializer(PublicUserWithAbakusGroupsSerializer):
past_memberships = PastMembershipSerializer(many=True)
memberships = MembershipSerializer(many=True)
achievements = AchievementSerializer(many=True)
achievement_score = serializers.SerializerMethodField()

def get_achievement_score(self, obj):

return round((obj.achievement_score / MAX_POSSIBLE_SCORE) * 100, 2)

class Meta(PublicUserSerializer.Meta):
fields = PublicUserWithAbakusGroupsSerializer.Meta.fields + ( # type: ignore
"past_memberships",
"memberships",
"achievements",
"achievement_score"
"achievement_score",
)


Expand Down Expand Up @@ -185,6 +191,7 @@ class CurrentUserSerializer(serializers.ModelSerializer):
abakus_email_lists = PublicEmailListSerializer(many=True)
photo_consents = serializers.SerializerMethodField()
achievements = AchievementSerializer(many=True)
achievement_score = serializers.SerializerMethodField()

def get_user_ical_token(self, user):
ical_token = ICalToken.objects.get_or_create(user=user)[0]
Expand Down Expand Up @@ -218,6 +225,10 @@ def validate_username(self, username):

return username

def get_achievement_score(self, obj):

return round((obj.achievement_score / MAX_POSSIBLE_SCORE) * 100, 2)

class Meta:
model = User
fields = (
Expand Down Expand Up @@ -251,7 +262,7 @@ class Meta:
"github_username",
"linkedin_id",
"achievements",
"achievement_score"
"achievement_score",
)


Expand Down

0 comments on commit de955c3

Please sign in to comment.