Skip to content

Commit

Permalink
Consolidate membership updates into a service
Browse files Browse the repository at this point in the history
  • Loading branch information
Syriiin committed May 31, 2024
1 parent 54602b2 commit 9e3c4c1
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 232 deletions.
3 changes: 2 additions & 1 deletion leaderboards/management/commands/refreshleaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from leaderboards.enums import LeaderboardAccessType
from leaderboards.models import Leaderboard
from leaderboards.services import update_membership
from profiles.models import OsuUser


Expand All @@ -26,7 +27,7 @@ def handle(self, *args, **options):
else:
users = leaderboard.members.all().values("id")
for user_id in tqdm([u["id"] for u in users]):
leaderboard.update_membership(user_id)
update_membership(leaderboard, user_id)

self.stdout.write(
self.style.SUCCESS(
Expand Down
129 changes: 1 addition & 128 deletions leaderboards/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import typing
from datetime import datetime

from django.db import models, transaction
from django.db import models
from django.db.models import Max, Q
from rest_framework.exceptions import PermissionDenied

from common.osu.enums import Gamemode
from common.osu.utils import calculate_pp_total
Expand Down Expand Up @@ -115,131 +113,6 @@ def get_top_membership(self):
.order_by("-pp")
).first()

def update_membership(self, user_id):
"""
Update a membership for a user_id ensuring all scores that fit the criteria are added
"""
# Dont't update memberships for archived leaderboards
if self.archived:
return

# Get or create Membership model
try:
membership = self.memberships.select_for_update().get(user_id=user_id)
# Clear all currently added scores
membership.scores.clear()
join_date = membership.join_date
except Membership.DoesNotExist:
if (
self.access_type
in (
LeaderboardAccessType.PUBLIC_INVITE_ONLY,
LeaderboardAccessType.PRIVATE,
)
and self.owner_id != user_id
):
# Check if user has been invited
try:
invitees = self.invitees.filter(id=user_id)
except OsuUser.DoesNotExist:
raise PermissionDenied(
"You must be invited to join this leaderboard."
)

# Invite is being accepted
self.invitees.remove(*invitees)

# Create new membership
membership = Membership(user_id=user_id, leaderboard=self)
join_date = datetime.now()

# Get scores
scores = Score.objects.filter(
user_stats__user_id=user_id, gamemode=self.gamemode
)

if not self.allow_past_scores:
scores = scores.filter(date__gte=join_date)

if self.score_filter:
scores = scores.apply_score_filter(self.score_filter)

scores = scores.get_score_set(score_set=self.score_set)
membership.score_count = len(
scores
) # len because we're evaluating the queryset anyway

# Add scores to membership
if self.score_set == ScoreSet.NORMAL:
membership.pp = calculate_pp_total(
score.performance_total for score in scores
)
elif self.score_set == ScoreSet.NEVER_CHOKE:
membership.pp = calculate_pp_total(
(
score.nochoke_performance_total
if score.result & ScoreResult.CHOKE
else score.performance_total
)
for score in scores
)
elif self.score_set == ScoreSet.ALWAYS_FULL_COMBO:
membership.pp = calculate_pp_total(
score.nochoke_performance_total for score in scores
)

# Fetch rank
membership.rank = self.memberships.filter(pp__gt=membership.pp).count() + 1

if self.notification_discord_webhook_url != "":
# Check for new top score
pp_record = self.get_pp_record()
player_top_score = scores.first()
if (
pp_record is not None
and player_top_score is not None
and player_top_score.performance_total > pp_record
):
# TODO: fix this being here. needs to be here to avoid a circular import at the moment
from leaderboards.tasks import send_leaderboard_top_score_notification

# NOTE: need to use a function with default params here so the closure has the correct variables
def send_notification(
leaderboard_id=self.id,
score_id=player_top_score.id,
):
send_leaderboard_top_score_notification.delay(
leaderboard_id, score_id
)

transaction.on_commit(send_notification)

# Check for new top player
leaderboard_top_player = self.get_top_membership()
if (
leaderboard_top_player is not None
and leaderboard_top_player.user_id != membership.user_id
and membership.rank == 1
and membership.pp > 0
):
from leaderboards.tasks import send_leaderboard_top_player_notification

# NOTE: need to use a function with default params here so the closure has the correct variables
def send_notification(
leaderboard_id=self.id,
user_id=membership.user_id,
):
send_leaderboard_top_player_notification.delay(
leaderboard_id, user_id
)

transaction.on_commit(send_notification)

membership.save()
membership.scores.add(*scores)

return membership

def update_member_count(self):
"""
Updates Leaderboard.member_count with the count of Leaderboard.memberships
Expand Down
143 changes: 140 additions & 3 deletions leaderboards/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.db import transaction
from rest_framework.exceptions import PermissionDenied

from leaderboards.models import Leaderboard
from common.osu.utils import calculate_pp_total
from leaderboards.enums import LeaderboardAccessType
from leaderboards.models import Leaderboard, Membership, MembershipScore
from profiles.enums import ScoreResult, ScoreSet
from profiles.models import OsuUser, Score, UserStats


@transaction.atomic
Expand All @@ -13,7 +18,7 @@ def create_leaderboard(owner_id, leaderboard):
leaderboard.member_count = 1
leaderboard.score_filter.save()
leaderboard.save()
leaderboard.update_membership(owner_id)
update_membership(leaderboard, owner_id)
return leaderboard


Expand All @@ -23,7 +28,7 @@ def create_membership(leaderboard_id, user_id):
Creates a membership with a community leaderboard and update Leaderboard.member_count
"""
leaderboard = Leaderboard.community_leaderboards.get(id=leaderboard_id)
membership = leaderboard.update_membership(user_id)
membership = update_membership(leaderboard, user_id)
leaderboard.update_member_count()
return membership

Expand All @@ -36,3 +41,135 @@ def delete_membership(membership):
membership.delete()
membership.leaderboard.update_member_count()
return True


@transaction.atomic
def update_membership(leaderboard: Leaderboard, user_id: int):
"""
Creates or updates a membership for a given user on a given leaderboard
"""
if leaderboard.archived:
raise ValueError("Cannot update membership on an archived leaderboard")

try:
membership = leaderboard.memberships.select_for_update().get(user_id=user_id)
except Membership.DoesNotExist:
if (
leaderboard.access_type
in (
LeaderboardAccessType.PUBLIC_INVITE_ONLY,
LeaderboardAccessType.PRIVATE,
)
and leaderboard.owner_id != user_id
):
# Check if user has been invited
try:
invitees = leaderboard.invitees.filter(id=user_id)
except OsuUser.DoesNotExist:
raise PermissionDenied("You must be invited to join this leaderboard.")

# Invite is being accepted
leaderboard.invitees.remove(*invitees)

# Create new membership
membership = Membership.objects.create(
user_id=user_id,
leaderboard=leaderboard,
pp=0,
score_count=0,
rank=leaderboard.member_count + 1,
)

try:
user_stats = UserStats.objects.get(
user_id=user_id, gamemode=leaderboard.gamemode
)
except UserStats.DoesNotExist:
return membership

if leaderboard.score_filter:
scores = user_stats.scores.apply_score_filter(leaderboard.score_filter)
else:
scores = user_stats.scores.all()

if not leaderboard.allow_past_scores:
scores = scores.filter(date__gte=membership.join_date)

scores = scores.get_score_set(score_set=leaderboard.score_set)

def get_performance_total(score: Score, score_set: ScoreSet):
if score_set == ScoreSet.NORMAL:
return score.performance_total
elif score_set == ScoreSet.NEVER_CHOKE:
return (
score.nochoke_performance_total
if score.result & ScoreResult.CHOKE
else score.performance_total
)
elif score_set == ScoreSet.ALWAYS_FULL_COMBO:
return score.nochoke_performance_total

membership_scores = [
MembershipScore(
membership=membership,
score=score,
performance_total=get_performance_total(
score, ScoreSet(leaderboard.score_set)
),
)
for score in scores
]

MembershipScore.objects.bulk_create(
membership_scores,
update_conflicts=True,
update_fields=["performance_total"],
unique_fields=["membership_id", "score_id"],
)

membership.score_count = len(membership_scores)

membership.pp = calculate_pp_total(
score.performance_total for score in membership_scores
)

membership.rank = leaderboard.memberships.filter(pp__gt=membership.pp).count() + 1

membership.save()

if leaderboard.notification_discord_webhook_url != "":
# Check for new top score
pp_record = leaderboard.get_pp_record()
player_top_score = scores.first()
if (
pp_record is not None
and player_top_score is not None
and player_top_score.performance_total > pp_record
):
# NOTE: need to use a function with default params here so the closure has the correct variables
def send_notification(
leaderboard_id=leaderboard.id,
score_id=player_top_score.id,
):
send_leaderboard_top_score_notification.delay(leaderboard_id, score_id)

transaction.on_commit(send_notification)

# Check for new top player
leaderboard_top_player = leaderboard.get_top_membership()
if (
leaderboard_top_player is not None
and leaderboard_top_player.user_id != membership.user_id
and membership.rank == 1
and membership.pp > 0
):
# NOTE: need to use a function with default params here so the closure has the correct variables
def send_notification(
leaderboard_id=leaderboard.id,
user_id=membership.user_id,
):
send_leaderboard_top_player_notification.delay(leaderboard_id, user_id)

transaction.on_commit(send_notification)

return membership
Loading

0 comments on commit 9e3c4c1

Please sign in to comment.