From bb23336b2725ed4c18eab5408ddb6d232ef5f2dc Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 18:03:10 +0200 Subject: [PATCH 01/14] Remove debug code --- django/accounts/tests.py | 3 --- django/stats/models.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/django/accounts/tests.py b/django/accounts/tests.py index 06f045c..be313e9 100644 --- a/django/accounts/tests.py +++ b/django/accounts/tests.py @@ -215,9 +215,6 @@ def test_one_missing(self, mock__SquadMembership__update_stats): self.assertEqual(len(ScheduledNotification.objects.all()), 1) notification_text = ScheduledNotification.objects.get().text - print('-' * 80) - print(notification_text) - print('-' * 80) self.assertEqual( notification_text, 'We have changes in the 30-days leaderboard! 🎆' '\n' diff --git a/django/stats/models.py b/django/stats/models.py index b0ed302..2072990 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -729,10 +729,6 @@ def get_or_none(qs, **kwargs): except ObjectDoesNotExist: return None except MultipleObjectsReturned: - print('-' * 10) - for obj in qs.filter(**kwargs).all(): - print(obj) - print('-' * 10) raise From e4d56d2e95a94e6bb23d6c77560347c1dc609e16 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 18:23:50 +0200 Subject: [PATCH 02/14] Add tests for repetitive `MatchBadge.award` calls --- django/stats/tests.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/django/stats/tests.py b/django/stats/tests.py index a83cc4b..1acf287 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -202,11 +202,14 @@ def test_quad_kill(self): create_kill_event(mp1, mp2, round = 1), ] ) - models.MatchBadge.award(mp1, list()) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'quad-kill')), 1) - badge = models.MatchBadge.objects.filter(badge_type = 'quad-kill').get() - self.assertEqual(badge.participation.pk, mp1.pk) - self.assertEqual(badge.frequency, 1) + # Test twice. The badge should be awarded only once. + for itr in range(2): + with self.subTest(itr = itr): + models.MatchBadge.award(mp1, list()) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'quad-kill')), 1) + badge = models.MatchBadge.objects.filter(badge_type = 'quad-kill').get() + self.assertEqual(badge.participation.pk, mp1.pk) + self.assertEqual(badge.frequency, 1) def test_quad_kill_twice(self): pmatch = Match__create_from_data().test() @@ -290,8 +293,10 @@ def test_carrier_badge(self): # Test with ADR right above the threshold mp1.adr = 1.81 * mp2.adr mp1.save() - models.MatchBadge.award(mp1, list()) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) + for itr in range(2): # Test twice, the badge should be awarded only once + with self.subTest(itr = itr): + models.MatchBadge.award(mp1, list()) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) def test_peach_price(self): pmatch = Match__create_from_data().test() @@ -307,8 +312,10 @@ def test_peach_price(self): # Test with ADR right above the threshold mp5.adr = 0.74 * mp4.adr mp5.save() - models.MatchBadge.award(mp5, list()) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 1) + for itr in range(2): # Test twice, the badge should be awarded only once + with self.subTest(itr = itr): + models.MatchBadge.award(mp5, list()) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 1) class Squad__do_changelog_announcements(TestCase): From 522d4ba2d8c1e084b72e7dcddc52ad3bbb079653 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 19:39:45 +0200 Subject: [PATCH 03/14] Add `Match.award_badges` (tests fail) --- django/stats/models.py | 21 +++++++++++++++++---- django/stats/tests.py | 27 ++++++++++++++++++--------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/django/stats/models.py b/django/stats/models.py index 2072990..e7187af 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -550,8 +550,18 @@ def create_from_data(data: dict) -> Self: squad = Squad.objects.get(uuid = squad_id) squad.handle_new_match(m) + m.award_badges() return m + def award_badges(self): + """ + Award the badges for all who participated in this match. + + This does not include badges which require the previous match history. + """ + for mp in self.matchparticipation_set.all(): + MatchBadge.award(mp) + def __str__(self): return f'{self.map_name} ({self.date_and_time})' @@ -1033,14 +1043,17 @@ class MatchBadge(models.Model): frequency = models.PositiveSmallIntegerField(null = False, default = 1) @staticmethod - def award(participation, old_participations): - if len(old_participations) >= 10: - MatchBadge.award_surpass_yourself_badge(participation, old_participations[-20:]) + def award(participation): MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace') MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill') MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆') MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.75, emoji = '🍑') + @staticmethod + def award_with_history(participation, old_participations): + if len(old_participations) >= 10: + MatchBadge.award_surpass_yourself_badge(participation, old_participations[-20:]) + @staticmethod def award_surpass_yourself_badge(participation, old_participations): badge_type = MatchBadgeType.objects.get(slug = 'surpass-yourself') @@ -1246,7 +1259,7 @@ def run(self): continue participation = pmatch.get_participation(self.account.steam_profile) - MatchBadge.award(participation, old_participations) + MatchBadge.award_with_history(participation, old_participations) old_participations.append(participation) diff --git a/django/stats/tests.py b/django/stats/tests.py index 1acf287..0dc5f73 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -186,7 +186,7 @@ class MatchBadge__award(TestCase): def test_no_awards(self): pmatch = Match__create_from_data().test() participation = pmatch.get_participation('76561197967680028') - models.MatchBadge.award(participation, list()) + models.MatchBadge.award(participation) self.assertEqual(len(models.MatchBadge.objects.filter(participation = participation)), 0) def test_quad_kill(self): @@ -205,7 +205,7 @@ def test_quad_kill(self): # Test twice. The badge should be awarded only once. for itr in range(2): with self.subTest(itr = itr): - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'quad-kill')), 1) badge = models.MatchBadge.objects.filter(badge_type = 'quad-kill').get() self.assertEqual(badge.participation.pk, mp1.pk) @@ -228,7 +228,7 @@ def test_quad_kill_twice(self): create_kill_event(mp1, mp2, round = 2), ] ) - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'quad-kill')), 1) badge = models.MatchBadge.objects.filter(badge_type = 'quad-kill').get() self.assertEqual(badge.participation.pk, mp1.pk) @@ -248,7 +248,7 @@ def test_ace(self): create_kill_event(mp1, mp2, round = 1), ] ) - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'ace')), 1) badge = models.MatchBadge.objects.filter(badge_type = 'ace').get() self.assertEqual(badge.participation.pk, mp1.pk) @@ -273,7 +273,7 @@ def test_ace_twice(self): create_kill_event(mp1, mp2, round = 2), ] ) - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'ace')), 1) badge = models.MatchBadge.objects.filter(badge_type = 'ace').get() self.assertEqual(badge.participation.pk, mp1.pk) @@ -287,7 +287,7 @@ def test_carrier_badge(self): # Test with ADR right below the threshold mp1.adr = 1.79 * mp2.adr mp1.save() - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 0) # Test with ADR right above the threshold @@ -295,7 +295,7 @@ def test_carrier_badge(self): mp1.save() for itr in range(2): # Test twice, the badge should be awarded only once with self.subTest(itr = itr): - models.MatchBadge.award(mp1, list()) + models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) def test_peach_price(self): @@ -306,7 +306,7 @@ def test_peach_price(self): # Test with ADR right below the threshold mp5.adr = 0.76 * mp4.adr mp5.save() - models.MatchBadge.award(mp5, list()) + models.MatchBadge.award(mp5) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 0) # Test with ADR right above the threshold @@ -314,10 +314,19 @@ def test_peach_price(self): mp5.save() for itr in range(2): # Test twice, the badge should be awarded only once with self.subTest(itr = itr): - models.MatchBadge.award(mp5, list()) + models.MatchBadge.award(mp5) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 1) +class MatchBadge__award_with_history(TestCase): + + def test_no_awards(self): + pmatch = Match__create_from_data().test() + participation = pmatch.get_participation('76561197967680028') + models.MatchBadge.award_with_history(participation, list()) + self.assertEqual(len(models.MatchBadge.objects.filter(participation = participation)), 0) + + class Squad__do_changelog_announcements(TestCase): changelog = [ From 234388f15d18019b954b8cf0abe616f563e08999 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 19:52:12 +0200 Subject: [PATCH 04/14] Add django/stats/migrations/0021_award_missing_match_badges.py --- .../0021_award_missing_match_badges.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 django/stats/migrations/0021_award_missing_match_badges.py diff --git a/django/stats/migrations/0021_award_missing_match_badges.py b/django/stats/migrations/0021_award_missing_match_badges.py new file mode 100644 index 0000000..c354c27 --- /dev/null +++ b/django/stats/migrations/0021_award_missing_match_badges.py @@ -0,0 +1,18 @@ +from django.db import migrations + + +def forwards(apps, schema_editor): + Match = apps.get_model('stats', 'Match') + for pmatch in Match.objects.using(schema_editor.connection.alias).all(): + pmatch.award_badges() + + +class Migration(migrations.Migration): + dependencies = [ + ('stats', '0020_rename_completed_timestamp_updatetask_completion_timestamp'), + ] + + operations = [ + migrations.RunPython(forwards), + ] + From 9bce543c274413f9cdb3349dc8405b4d6863e20e Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 20:15:10 +0200 Subject: [PATCH 05/14] Fix tests --- django/stats/models.py | 14 ++++++++++ django/stats/tests.py | 63 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/django/stats/models.py b/django/stats/models.py index e7187af..e358e92 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -1128,6 +1128,20 @@ class Meta: ) ] + def __str__(self): + return ( + f'{self.frequency}x ' + f'{self.badge_type.name} for {self.participation.player.name} ' + f'({self.participation.player.steamid})' + ) + + def __eq__(self, other): + return ( + isinstance(other, MatchBadge) + and self.participation.pk == other.participation.pk + and self.badge_type.pk == other.badge_type.pk + ) + class UpdateTask(models.Model): """ diff --git a/django/stats/tests.py b/django/stats/tests.py index 0dc5f73..1730852 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -181,15 +181,64 @@ def test(self): return pmatch +class Match__award_badges(TestCase): + + def test(self): + pmatch = Match__create_from_data().test() + self.assertEqual( + list( + models.MatchBadge.objects.filter( + participation = pmatch.get_participation('76561199034015511'), + ) + ), + [ + models.MatchBadge( + badge_type = models.MatchBadgeType.objects.get(pk = 'quad-kill'), + participation = pmatch.get_participation('76561199034015511'), + frequency = 1, + ) + ] + ) + self.assertEqual( + list( + models.MatchBadge.objects.filter( + participation = pmatch.get_participation('76561198298259382'), + ) + ), + [ + models.MatchBadge( + badge_type = models.MatchBadgeType.objects.get(pk = 'quad-kill'), + participation = pmatch.get_participation('76561198298259382'), + frequency = 1, + ) + ] + ) + self.assertEqual( + list( + models.MatchBadge.objects.filter( + participation = pmatch.get_participation('76561197962477966'), + ) + ), + [ + models.MatchBadge( + badge_type = models.MatchBadgeType.objects.get(pk = 'peach'), + participation = pmatch.get_participation('76561197962477966'), + frequency = 1, + ) + ] + ) + + +@patch('stats.models.Match.award_badges') class MatchBadge__award(TestCase): - def test_no_awards(self): + def test_no_awards(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() participation = pmatch.get_participation('76561197967680028') models.MatchBadge.award(participation) self.assertEqual(len(models.MatchBadge.objects.filter(participation = participation)), 0) - def test_quad_kill(self): + def test_quad_kill(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp1 = pmatch.get_participation('76561197967680028') mp2 = pmatch.get_participation('76561197961345487') @@ -211,7 +260,7 @@ def test_quad_kill(self): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_quad_kill_twice(self): + def test_quad_kill_twice(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp1 = pmatch.get_participation('76561197967680028') mp2 = pmatch.get_participation('76561197961345487') @@ -234,7 +283,7 @@ def test_quad_kill_twice(self): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_ace(self): + def test_ace(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp1 = pmatch.get_participation('76561197967680028') mp2 = pmatch.get_participation('76561197961345487') @@ -254,7 +303,7 @@ def test_ace(self): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_ace_twice(self): + def test_ace_twice(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp1 = pmatch.get_participation('76561197967680028') mp2 = pmatch.get_participation('76561197961345487') @@ -279,7 +328,7 @@ def test_ace_twice(self): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_carrier_badge(self): + def test_carrier_badge(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp1 = pmatch.get_participation('76561197967680028') mp2 = pmatch.get_participation('76561197961345487') @@ -298,7 +347,7 @@ def test_carrier_badge(self): models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) - def test_peach_price(self): + def test_peach_price(self, mock__Match__award_badges): pmatch = Match__create_from_data().test() mp4 = pmatch.get_participation('76561198067716219') mp5 = pmatch.get_participation('76561197962477966') From f65e4e924b5af71d9f851061f9c60e994f25ca33 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 20:45:52 +0200 Subject: [PATCH 06/14] Drop django/stats/migrations/0021_award_missing_match_badges.py --- .../0021_award_missing_match_badges.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 django/stats/migrations/0021_award_missing_match_badges.py diff --git a/django/stats/migrations/0021_award_missing_match_badges.py b/django/stats/migrations/0021_award_missing_match_badges.py deleted file mode 100644 index c354c27..0000000 --- a/django/stats/migrations/0021_award_missing_match_badges.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import migrations - - -def forwards(apps, schema_editor): - Match = apps.get_model('stats', 'Match') - for pmatch in Match.objects.using(schema_editor.connection.alias).all(): - pmatch.award_badges() - - -class Migration(migrations.Migration): - dependencies = [ - ('stats', '0020_rename_completed_timestamp_updatetask_completion_timestamp'), - ] - - operations = [ - migrations.RunPython(forwards), - ] - From 53709e964a7893805a053465f2c3ce018dd2f31c Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 20:46:03 +0200 Subject: [PATCH 07/14] Add `award_missing_badges` admin action --- django/stats/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/django/stats/admin.py b/django/stats/admin.py index 5bfa364..c59fb9e 100644 --- a/django/stats/admin.py +++ b/django/stats/admin.py @@ -18,6 +18,12 @@ class MatchParticipationInline(admin.TabularInline): ordering = ('team', '-adr') +@admin.action(description='Award missing badges') +def award_missing_badges(modeladmin, request, queryset): + for pmatch in queryset.all(): + pmatch.award_badges() + + @admin.register(Match) class MatchAdmin(admin.ModelAdmin): @@ -36,6 +42,8 @@ def has_add_permission(self, request, obj = None): MatchParticipationInline, ] + actions = [award_missing_badges] + @admin.display(description='Session') def session_list(self, pmatch): sessions = pmatch.sessions.all() From 1e381d9518e4d4c7f7054c68f91a861336695f1d Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 20:47:11 +0200 Subject: [PATCH 08/14] Reduce Peach Price margin to 0.667 --- django/stats/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/stats/models.py b/django/stats/models.py index e358e92..2193609 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -1047,7 +1047,7 @@ def award(participation): MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace') MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill') MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆') - MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.75, emoji = '🍑') + MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.667, emoji = '🍑') @staticmethod def award_with_history(participation, old_participations): From a368bc9134804462f7bd4e878c08c94c405ba43a Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 20:48:47 +0200 Subject: [PATCH 09/14] Add `reaward_badges` admin action --- django/stats/admin.py | 16 +++++++++++++++- django/stats/models.py | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/django/stats/admin.py b/django/stats/admin.py index c59fb9e..b09a43f 100644 --- a/django/stats/admin.py +++ b/django/stats/admin.py @@ -24,6 +24,17 @@ def award_missing_badges(modeladmin, request, queryset): pmatch.award_badges() +@admin.action(description='Re-award badges') +def reaward_badges(modeladmin, request, queryset): + for pmatch in queryset.all(): + MatchBadge.objects.filter( + participation__pmatch = pmatch, + ).exclude( + badge_type__slug = 'surpass-yourself', + ).delete() + pmatch.award_badges() + + @admin.register(Match) class MatchAdmin(admin.ModelAdmin): @@ -42,7 +53,10 @@ def has_add_permission(self, request, obj = None): MatchParticipationInline, ] - actions = [award_missing_badges] + actions = [ + award_missing_badges, + reaward_badges, + ] @admin.display(description='Session') def session_list(self, pmatch): diff --git a/django/stats/models.py b/django/stats/models.py index 2193609..47c7d1f 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -1142,6 +1142,15 @@ def __eq__(self, other): and self.badge_type.pk == other.badge_type.pk ) + def __hash__(self): + return hash( + ( + self.participation.pk, + self.badge_type.pk, + self.frequency, + ) + ) + class UpdateTask(models.Model): """ From a7b8aaed8c0b39525519774ee6de186933b8160c Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 21:19:02 +0200 Subject: [PATCH 10/14] Mute discord updates on `reaward_badges` admin action --- django/stats/admin.py | 4 +++- django/stats/models.py | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/django/stats/admin.py b/django/stats/admin.py index b09a43f..3611e7c 100644 --- a/django/stats/admin.py +++ b/django/stats/admin.py @@ -32,7 +32,7 @@ def reaward_badges(modeladmin, request, queryset): ).exclude( badge_type__slug = 'surpass-yourself', ).delete() - pmatch.award_badges() + pmatch.award_badges(mute_discord = True) @admin.register(Match) @@ -58,6 +58,8 @@ def has_add_permission(self, request, obj = None): reaward_badges, ] + list_max_show_all = 1000 + @admin.display(description='Session') def session_list(self, pmatch): sessions = pmatch.sessions.all() diff --git a/django/stats/models.py b/django/stats/models.py index 47c7d1f..5a8c6a9 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -553,14 +553,14 @@ def create_from_data(data: dict) -> Self: m.award_badges() return m - def award_badges(self): + def award_badges(self, mute_discord = False): """ Award the badges for all who participated in this match. This does not include badges which require the previous match history. """ for mp in self.matchparticipation_set.all(): - MatchBadge.award(mp) + MatchBadge.award(mp, mute_discord = mute_discord) def __str__(self): return f'{self.map_name} ({self.date_and_time})' @@ -1043,11 +1043,11 @@ class MatchBadge(models.Model): frequency = models.PositiveSmallIntegerField(null = False, default = 1) @staticmethod - def award(participation): - MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace') - MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill') - MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆') - MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.667, emoji = '🍑') + def award(participation, **kwargs): + MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace', **kwargs) + MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill', **kwargs) + MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆', **kwargs) + MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.667, emoji = '🍑', **kwargs) @staticmethod def award_with_history(participation, old_participations): @@ -1077,7 +1077,7 @@ def award_surpass_yourself_badge(participation, old_participations): m.squad.notify_on_discord(text) @staticmethod - def award_kills_in_one_round_badges(participation, kill_number, badge_type_slug): + def award_kills_in_one_round_badges(participation, kill_number, badge_type_slug, mute_discord = False): badge_type = MatchBadgeType.objects.get(slug = badge_type_slug) if MatchBadge.objects.filter(badge_type=badge_type, participation=participation).exists(): return @@ -1090,11 +1090,12 @@ def award_kills_in_one_round_badges(participation, kill_number, badge_type_slug) f'<{participation.player.steamid}> has achieved **{badge_type.name}**{frequency} on ' f'*{participation.pmatch.map_name}* recently!' ) - for m in participation.player.squad_memberships.all(): - m.squad.notify_on_discord(text) + if not mute_discord: + for m in participation.player.squad_memberships.all(): + m.squad.notify_on_discord(text) @staticmethod - def award_margin_badge(participation, badge_type_slug, order, margin, emoji): + def award_margin_badge(participation, badge_type_slug, order, margin, emoji, mute_discord = False): kpi = order[1:] if order[0] in '+-' else order badge_type = MatchBadgeType.objects.get(slug = badge_type_slug) if MatchBadge.objects.filter(badge_type=badge_type, participation=participation).exists(): @@ -1115,8 +1116,9 @@ def award_margin_badge(participation, badge_type_slug, order, margin, emoji): f'{emoji} <{participation.player.steamid}> has qualified for the **{badge_type.name}** ' f'on *{participation.pmatch.map_name}*!' ) - for m in participation.player.squad_memberships.all(): - m.squad.notify_on_discord(text) + if not mute_discord: + for m in participation.player.squad_memberships.all(): + m.squad.notify_on_discord(text) class Meta: verbose_name = 'Match-based badge' From ee705cbf664d45f6c09c3f10a767efdc9f404d4f Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 21:35:48 +0200 Subject: [PATCH 11/14] Hide badges for non-squad members --- django/stats/static/stats.css | 10 +++++++++- django/stats/templates/stats/sessions-list.html | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/django/stats/static/stats.css b/django/stats/static/stats.css index 7ff58f1..5b76b46 100644 --- a/django/stats/static/stats.css +++ b/django/stats/static/stats.css @@ -665,7 +665,11 @@ div.session .match-scoreboard table tr td .player-name { margin-left: 1mm; } -div.session .match-scoreboard table tr td .badge-list { +div.session .match-scoreboard table tr.is-not-squad-member td { + opacity: 0.5; +} + +div.session .match-scoreboard table tr.is-squad-member td .badge-list { display: inline-block; height: 0; overflow: visible; @@ -673,6 +677,10 @@ div.session .match-scoreboard table tr td .badge-list { filter: drop-shadow(0 0 1mm #ccc); } +div.session .match-scoreboard table tr.is-not-squad-member td .badge-list { + display: none; +} + div.session .match-scoreboard table tr td .badge { width: auto; height: 2em; diff --git a/django/stats/templates/stats/sessions-list.html b/django/stats/templates/stats/sessions-list.html index 067004b..ba1bbcc 100644 --- a/django/stats/templates/stats/sessions-list.html +++ b/django/stats/templates/stats/sessions-list.html @@ -58,7 +58,7 @@ {% for mp in m.matchparticipation_set.all %}{% if mp.team == team_idx|add:"0" %} - +
From 2489edd4b41b41e01f1a6d74f417911d0ac61529 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 21:53:23 +0200 Subject: [PATCH 12/14] Fix tests --- django/stats/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/stats/tests.py b/django/stats/tests.py index 1730852..e32d7ed 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -353,13 +353,13 @@ def test_peach_price(self, mock__Match__award_badges): mp5 = pmatch.get_participation('76561197962477966') # Test with ADR right below the threshold - mp5.adr = 0.76 * mp4.adr + mp5.adr = 0.668 * mp4.adr mp5.save() models.MatchBadge.award(mp5) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 0) # Test with ADR right above the threshold - mp5.adr = 0.74 * mp4.adr + mp5.adr = 0.666 * mp4.adr mp5.save() for itr in range(2): # Test twice, the badge should be awarded only once with self.subTest(itr = itr): From 587e35747e6fbfdccd94ec37ff316e43305f5035 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Sun, 6 Oct 2024 23:55:54 +0200 Subject: [PATCH 13/14] Add `max_adr = 50, max_kd = 0.5` constraints for Peach Price --- django/stats/models.py | 40 +++++++++--- django/stats/tests.py | 136 +++++++++++++++++++++++++++++------------ 2 files changed, 129 insertions(+), 47 deletions(-) diff --git a/django/stats/models.py b/django/stats/models.py index 5a8c6a9..2a8c624 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -1047,7 +1047,9 @@ def award(participation, **kwargs): MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace', **kwargs) MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill', **kwargs) MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆', **kwargs) - MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.667, emoji = '🍑', **kwargs) + MatchBadge.award_margin_badge( + participation, 'peach', order = 'adr', margin = 0.67, emoji = '🍑', max_adr = 50, max_kd = 0.5, **kwargs, + ) @staticmethod def award_with_history(participation, old_participations): @@ -1095,21 +1097,41 @@ def award_kills_in_one_round_badges(participation, kill_number, badge_type_slug, m.squad.notify_on_discord(text) @staticmethod - def award_margin_badge(participation, badge_type_slug, order, margin, emoji, mute_discord = False): + def award_margin_badge(participation, badge_type_slug, order, margin, emoji, mute_discord = False, **bounds): kpi = order[1:] if order[0] in '+-' else order badge_type = MatchBadgeType.objects.get(slug = badge_type_slug) if MatchBadge.objects.filter(badge_type=badge_type, participation=participation).exists(): return teammates = participation.pmatch.matchparticipation_set.filter(team = participation.team).order_by(order) - awarded = teammates[0].pk == participation.pk and any( - ( - order[0] == '-' and getattr(teammates[0], kpi) > margin * getattr(teammates[1], kpi), - order[0] != '-' and getattr(teammates[0], kpi) < margin * getattr(teammates[1], kpi), - ) - ) + # Define the requirements for the badge + requirements = [ + teammates[0].pk == participation.pk, + any( + ( + order[0] == '-' and getattr(teammates[0], kpi) > margin * getattr(teammates[1], kpi), + order[0] != '-' and getattr(teammates[0], kpi) < margin * getattr(teammates[1], kpi), + ) + ), + ] - if awarded: + # Add the bound checks to the requirements + req_bounds = list() + for bound_key, bound_val in bounds.items(): + func_name, attr_name = bound_key.split('_') + attr = getattr(participation, attr_name) + match func_name: + case 'min': + req_bounds.append(attr >= bound_val) + case 'max': + req_bounds.append(attr <= bound_val) + case _: + raise ValueError(f'Invalid function name: "{func_name}"') + if req_bounds: + requirements.append(any(req_bounds)) + + # Check the requirements and award the badge + if all(requirements): log.info(f'{participation.player.name} received the {badge_type.name}') MatchBadge.objects.create(badge_type = badge_type, participation = participation) text = ( diff --git a/django/stats/tests.py b/django/stats/tests.py index e32d7ed..8bc2ae6 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -229,19 +229,28 @@ def test(self): ) -@patch('stats.models.Match.award_badges') class MatchBadge__award(TestCase): - def test_no_awards(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - participation = pmatch.get_participation('76561197967680028') + def setUp(self): + with patch('stats.models.Match.award_badges'): + self.pmatch = Match__create_from_data().test() + self.mp5 = self.pmatch.get_participation('76561197962477966') + self.teammates = list( + self.mp5.pmatch.matchparticipation_set.filter( + team = self.mp5.team, + ).exclude( + pk = self.mp5.pk, + ).order_by('-adr') + ) + + def test_no_awards(self): + participation = self.pmatch.get_participation('76561197967680028') models.MatchBadge.award(participation) self.assertEqual(len(models.MatchBadge.objects.filter(participation = participation)), 0) - def test_quad_kill(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_quad_kill(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -260,10 +269,9 @@ def test_quad_kill(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_quad_kill_twice(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_quad_kill_twice(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -283,10 +291,9 @@ def test_quad_kill_twice(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_ace(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_ace(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -303,10 +310,9 @@ def test_ace(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_ace_twice(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_ace_twice(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -328,10 +334,9 @@ def test_ace_twice(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_carrier_badge(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_carrier_badge(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') # Test with ADR right below the threshold mp1.adr = 1.79 * mp2.adr @@ -347,24 +352,79 @@ def test_carrier_badge(self, mock__Match__award_badges): models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) - def test_peach_price(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp4 = pmatch.get_participation('76561198067716219') - mp5 = pmatch.get_participation('76561197962477966') - - # Test with ADR right below the threshold - mp5.adr = 0.668 * mp4.adr - mp5.save() - models.MatchBadge.award(mp5) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 0) + def test_peach_price__within_bounds(self): + """ + Test the 🍑 Peach Price when the constraints ✅ "ADR <50" and ✅ "K/D <0.5" are met. + """ + for mp in self.teammates: + mp.adr = 50 + mp.save() + self.mp5.kills = 1 + self.mp5.deaths = 3 # Test with ADR right above the threshold - mp5.adr = 0.666 * mp4.adr - mp5.save() + self.mp5.adr = 0.68 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 0) + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() for itr in range(2): # Test twice, the badge should be awarded only once with self.subTest(itr = itr): - models.MatchBadge.award(mp5) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 1) + models.MatchBadge.award(self.mp5) + self.assertEqual( + len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1, + ) + + def test_peach_price__kd_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ✅ "ADR <50" is met but ❌ "K/D <0.5" is not. + """ + for mp in self.teammates: + mp.adr = 50 + mp.save() + self.mp5.kills = 2 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1) + + def test_peach_price__adr_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ✅ "K/D <0.5" is met but ❌ "ADR <50" is not. + """ + for mp in self.teammates: + mp.adr = 100 + mp.save() + self.mp5.kills = 1 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1) + + def test_peach_price__kd_and_adr_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ❌ "ADR <50" and ❌ "K/D <0.5" both are not met. + """ + for mp in self.teammates: + mp.adr = 100 + mp.save() + self.mp5.kills = 2 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 0) class MatchBadge__award_with_history(TestCase): From 5af485a68a8c8c91abd8986559b23f36bd0a601f Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Mon, 7 Oct 2024 00:08:31 +0200 Subject: [PATCH 14/14] Reformat legend items --- django/static/base.css | 4 ++++ django/stats/static/stats.css | 10 +++++----- django/stats/templates/stats/squads.html | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/django/static/base.css b/django/static/base.css index 3c2d6c2..969c3e2 100644 --- a/django/static/base.css +++ b/django/static/base.css @@ -1,3 +1,7 @@ +.nowrap { + white-space: nowrap; +} + a:link, a:visited, a:active, a:hover { color: #3633fb; text-decoration: underline; diff --git a/django/stats/static/stats.css b/django/stats/static/stats.css index 5b76b46..61fd87b 100644 --- a/django/stats/static/stats.css +++ b/django/stats/static/stats.css @@ -301,8 +301,8 @@ div.squad-members-appendix .legend-item { vertical-align: middle; margin-left: 5mm; position: relative; - width: 70mm; - height: 3.5em; + width: 85mm; + height: 3.1em; margin-top: 2mm; margin-bottom: 2mm; } @@ -314,7 +314,7 @@ div.squad-members-appendix .legend-item .badge { left: 0; top: 0; border-right: 1px solid #ccc; - padding-right: 2mm; + padding-right: 3mm; height: 100%; background-size: 85%; background-repeat: no-repeat; @@ -326,7 +326,7 @@ div.squad-members-appendix .legend-item .legend-item-label { vertical-align: middle; font-size: 100%; position: absolute; - left: 10mm; + left: 12mm; white-space: nowrap; } @@ -334,7 +334,7 @@ div.squad-members-appendix .legend-item .legend-item-description { display: block; font-size: 77%; position: absolute; - left: 10mm; + left: 12mm; top: 1.8em; } diff --git a/django/stats/templates/stats/squads.html b/django/stats/templates/stats/squads.html index 0cd3d94..0203927 100644 --- a/django/stats/templates/stats/squads.html +++ b/django/stats/templates/stats/squads.html @@ -80,7 +80,7 @@
Peach Price
-
Be the red lantern in your team, with a substantial deficit in ADR.
+
Be the red lantern of your team with K/D ≤0.5, ADR ≤50, and a substantial deficit in ADR.