Skip to content

Commit

Permalink
Merge pull request #71 from Syriiin/add-score-mutations
Browse files Browse the repository at this point in the history
Add score mutations
  • Loading branch information
Syriiin authored Jun 10, 2024
2 parents bab5e20 + d6e0034 commit af18121
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 62 deletions.
41 changes: 25 additions & 16 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from common.osu.enums import Gamemode, Mods
from leaderboards.enums import LeaderboardAccessType
from leaderboards.models import Invite, Leaderboard
from leaderboards.services import create_membership
from leaderboards.services import create_leaderboard, create_membership
from osuauth.models import User
from profiles.enums import ScoreResult, ScoreSet
from profiles.enums import ScoreMutation, ScoreResult, ScoreSet
from profiles.models import Beatmap, OsuUser, Score, ScoreFilter, UserStats
from profiles.services import refresh_user_from_api


@pytest.fixture
Expand Down Expand Up @@ -71,6 +72,11 @@ def user_stats(osu_user: OsuUser):
)


@pytest.fixture
def stub_user_stats():
return refresh_user_from_api(user_id=5701575, gamemode=Gamemode.STANDARD)


@pytest.fixture
def beatmap():
return Beatmap.objects.create(
Expand Down Expand Up @@ -130,6 +136,7 @@ def score(user_stats: UserStats, beatmap: Beatmap):
difficulty_calculator_engine="test diffcalc engine",
difficulty_calculator_version="test diffcalc version",
result=ScoreResult.NO_BREAK,
mutation=ScoreMutation.NONE,
)


Expand All @@ -147,20 +154,22 @@ def leaderboard(score_filter: ScoreFilter):
join_date=datetime(2023, 1, 1, tzinfo=timezone.utc),
disabled=False,
)
user = User.objects.create(username=osu_user.id, osu_user=osu_user)
return Leaderboard.objects.create(
gamemode=Gamemode.STANDARD,
score_set=ScoreSet.NORMAL,
access_type=LeaderboardAccessType.PUBLIC,
name="test leaderboard",
description="test leaderboard",
icon_url="",
allow_past_scores=True,
member_count=0,
archived=False,
notification_discord_webhook_url="",
score_filter=score_filter,
owner=user.osu_user,
User.objects.create(username=osu_user.id, osu_user=osu_user)
return create_leaderboard(
osu_user.id,
Leaderboard(
gamemode=Gamemode.STANDARD,
score_set=ScoreSet.NORMAL,
access_type=LeaderboardAccessType.PUBLIC,
name="test leaderboard",
description="test leaderboard",
icon_url="",
allow_past_scores=True,
member_count=0,
archived=False,
notification_discord_webhook_url="",
score_filter=score_filter,
),
)


Expand Down
2 changes: 1 addition & 1 deletion leaderboards/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def update_membership(leaderboard: Leaderboard, user_id: int):
rank=leaderboard.member_count + 1,
)

scores = Score.objects.filter(
scores = Score.objects.filter_mutations().filter(
user_stats__user_id=user_id, user_stats__gamemode=leaderboard.gamemode
)

Expand Down
25 changes: 25 additions & 0 deletions leaderboards/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest

from common.osu.enums import Gamemode
from leaderboards.services import create_membership, update_membership
from profiles.services import fetch_scores


@pytest.mark.django_db
class TestMembershipServices:
@pytest.fixture
def membership(self, leaderboard, stub_user_stats):
return create_membership(leaderboard.id, stub_user_stats.user_id)

def test_create_membership(self, leaderboard, membership):
assert membership.leaderboard == leaderboard
assert membership.leaderboard.member_count == 2
assert membership.user.username == "Syrin"
assert membership.score_count == 4
assert membership.pp == 1215.8880634205632

def test_update_membership(self, membership):
fetch_scores(membership.user_id, [362949], Gamemode.STANDARD)
membership = update_membership(membership.leaderboard, membership.user_id)
assert membership.score_count == 5
assert membership.pp == 1399.645425686207
2 changes: 1 addition & 1 deletion leaderboards/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_get(self, arf, view, leaderboard, membership):
response = view(request, **kwargs)

assert response.status_code == HTTPStatus.OK
assert len(response.data) == 1
assert len(response.data) == 2

def test_post(self, arf, view, leaderboard, user):
kwargs = {
Expand Down
5 changes: 5 additions & 0 deletions profiles/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ class AllowedBeatmapStatus(IntEnum):
ANY = 0
RANKED_ONLY = 1 # ranked + approved
LOVED_ONLY = 2


class ScoreMutation(IntEnum):
NONE = 0
NO_CHOKE = 1
84 changes: 84 additions & 0 deletions profiles/management/commands/checkmutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from django.core.management.base import BaseCommand
from django.db.models import F, FilteredRelation, Q, QuerySet
from tqdm import tqdm

from profiles.enums import ScoreMutation, ScoreResult
from profiles.models import Score


class Command(BaseCommand):
help = "Checks all scores have required mutations created."

def add_arguments(self, parser):
parser.add_argument(
"--fix",
action="store_true",
help="Whether create missing mutations after detecting them",
)

def handle(self, *args, **options):
fix = options["fix"]
self.check_nochoke_mutations(fix)

def check_nochoke_mutations(self, create_missing_mutations: bool):
choke_scores = Score.objects.filter_mutations().filter(
result=F("result").bitand(ScoreResult.CHOKE)
)

choke_scores_missing_nochoke_mutation = (
Score.objects.filter_mutations()
.filter(result=F("result").bitand(ScoreResult.CHOKE))
.annotate(
nochoke_mutation=FilteredRelation(
"mutations",
condition=Q(mutations__mutation=ScoreMutation.NO_CHOKE),
)
)
.filter(nochoke_mutation=None)
)

total_count = choke_scores.count()
missing_count = choke_scores_missing_nochoke_mutation.count()

if missing_count == 0:
self.stdout.write(
self.style.SUCCESS(
f"All {total_count} choke scores have nochoke mutations created"
)
)
return

self.stdout.write(
self.style.ERROR(
f"{missing_count} / {total_count} choke scores are missing nochoke mutations ({(total_count - missing_count) / total_count * 100:.2f}% complete)"
)
)
if create_missing_mutations:
self.create_missing_nochoke_mutations(choke_scores_missing_nochoke_mutation)
self.stdout.write(
self.style.SUCCESS(
f"Created {missing_count} missing nochoke mutations for choke scores"
)
)
else:
self.stdout.write(
self.style.WARNING(
"Use --fix to create missing mutations for choke scores"
)
)

def create_missing_nochoke_mutations(self, choke_scores: QuerySet[Score]):
with tqdm(
desc="No-choke", total=choke_scores.count(), smoothing=0
) as progress_bar:
while len(page := choke_scores.select_related("beatmap")[:2000]) > 0:
self.create_missing_nochoke_mutations_page(page, progress_bar)

def create_missing_nochoke_mutations_page(
self, choke_scores: QuerySet[Score], progress_bar: tqdm
):
scores_to_create = []
for score in choke_scores:
scores_to_create.append(score.get_nochoke_mutation())
progress_bar.update(1)
Score.objects.bulk_create(scores_to_create)
2 changes: 1 addition & 1 deletion profiles/management/commands/recalculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.core.management.base import BaseCommand
from django.core.paginator import Paginator
from django.db.models import Count, QuerySet
from django.db.models import QuerySet
from tqdm import tqdm

from common.error_reporter import ErrorReporter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 4.2.13 on 2024-06-09 12:59

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
(
"profiles",
"0018_remove_difficultycalculation_unique_difficulty_calculation_and_more",
),
]

operations = [
migrations.RemoveConstraint(
model_name="score",
name="unique_score",
),
migrations.AddField(
model_name="score",
name="mutation",
field=models.IntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name="score",
name="original_score",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="mutations",
to="profiles.score",
),
),
migrations.AddConstraint(
model_name="score",
constraint=models.UniqueConstraint(
fields=("user_stats_id", "date", "mutation"), name="unique_score"
),
),
]
Loading

0 comments on commit af18121

Please sign in to comment.