From e832887e2afaee343b14a1bebc165de6e95ddb24 Mon Sep 17 00:00:00 2001 From: Kraust Date: Sat, 28 Sep 2024 14:00:27 -0400 Subject: [PATCH] Add Support for solo CSE/KASE, Add Statistics Page, Cleaned up Ladder Page. (#82) --- combatlog/models/combatlog.py | 4 +- combatlog/views/combatlog.py | 12 +- ladder/fixtures/ladders_solo.json | 30 ++++- ladder/management/commands/metrics.py | 22 ++++ ladder/models/ladder_entry.py | 27 ++++- ladder/models/variant.py | 2 +- ladder/templates/ladder_entry.html | 37 +++++-- ladder/templatetags/ladder_entry.py | 1 + ladder/views/ladder_entry.py | 49 +++++++- requirements.txt | 2 +- ruff.toml | 68 ++++++++++++ ui/static/img/help-circle.svg | 1 + ui/templates/stats.html | 55 +++++++++ ui/templates/template.html | 11 +- ui/templatetags/ladder.py | 154 +++++++++++++++++++++++++- ui/urls/ui.py | 12 +- 16 files changed, 453 insertions(+), 34 deletions(-) create mode 100644 ladder/management/commands/metrics.py create mode 100644 ruff.toml create mode 100644 ui/static/img/help-circle.svg create mode 100644 ui/templates/stats.html diff --git a/combatlog/models/combatlog.py b/combatlog/models/combatlog.py index 54e1070..e91277a 100644 --- a/combatlog/models/combatlog.py +++ b/combatlog/models/combatlog.py @@ -311,11 +311,13 @@ def update_metadata_file(self, file, force=False): def update_metadata(self, data, force=False): """Parse the Combat Log and create Metadata""" - with tempfile.NamedTemporaryFile() as file: + with tempfile.NamedTemporaryFile(delete=False) as file: file.write(data) file.flush() + file.close() res = self.update_metadata_file(file, force) self.put_data(data) + os.unlink(file.name) return res diff --git a/combatlog/views/combatlog.py b/combatlog/views/combatlog.py index 9dfac5a..9e0bb59 100644 --- a/combatlog/views/combatlog.py +++ b/combatlog/views/combatlog.py @@ -14,11 +14,13 @@ from rest_framework.viewsets import GenericViewSet from combatlog.models import CombatLog -from combatlog.serializers import (CombatLogSerializer, - CombatLogUploadResponseSerializer, - CombatLogUploadSerializer, - CombatLogUploadV2ResponseSerializer, - CombatLogUploadV2Serializer) +from combatlog.serializers import ( + CombatLogSerializer, + CombatLogUploadResponseSerializer, + CombatLogUploadSerializer, + CombatLogUploadV2ResponseSerializer, + CombatLogUploadV2Serializer, +) from core.pagination import PageNumberPagination LOGGER = logging.getLogger("django") diff --git a/ladder/fixtures/ladders_solo.json b/ladder/fixtures/ladders_solo.json index 2c43b91..1bbcb37 100644 --- a/ladder/fixtures/ladders_solo.json +++ b/ladder/fixtures/ladders_solo.json @@ -11,7 +11,7 @@ "is_space": true, "internal_name": "Infected Space", "internal_difficulty": "Elite", - "manual_review_threshold": 2000000 + "manual_review_threshold": 3000000 } }, { @@ -153,5 +153,33 @@ "internal_name": "Hive Space", "internal_difficulty": null } + }, + { + "model": "ladder.Ladder", + "pk": 1012, + "fields": { + "name": "Cure Found", + "difficulty": "Elite", + "metric": "DPS", + "variant": "Default", + "is_solo": true, + "is_space": true, + "internal_name": "Cure Found", + "internal_difficulty": "Elite" + } + }, + { + "model": "ladder.Ladder", + "pk": 1013, + "fields": { + "name": "Khitomer Vortex", + "difficulty": "Elite", + "metric": "DPS", + "variant": "Default", + "is_solo": true, + "is_space": true, + "internal_name": "Khitomer Space", + "internal_difficulty": "Elite" + } } ] diff --git a/ladder/management/commands/metrics.py b/ladder/management/commands/metrics.py new file mode 100644 index 0000000..c92948a --- /dev/null +++ b/ladder/management/commands/metrics.py @@ -0,0 +1,22 @@ +""" Generate Ladder Variants """ + +import logging + +from django.core.management.base import BaseCommand + +from ladder.models import LadderEntry + +LOGGER = logging.getLogger("django") + + +class Command(BaseCommand): + help = "Display Ladder Metrics" + + def handle(self, *args, **options): + max_bucket = 5000000 + idx = 0 + while idx <= max_bucket: + entries = LadderEntry.objects.order_by( + "data__handle", "-data__DPS").distinct("data__handle").filter(data__DPS__gte=idx) + print(f"{idx},{len(entries)}") + idx += 10000 diff --git a/ladder/models/ladder_entry.py b/ladder/models/ladder_entry.py index 9c87cbe..add07e9 100644 --- a/ladder/models/ladder_entry.py +++ b/ladder/models/ladder_entry.py @@ -13,11 +13,36 @@ class LadderEntry(BaseModel): player = models.TextField() data = models.JSONField() - combatlog = models.ForeignKey("combatlog.CombatLog", on_delete=models.CASCADE) + combatlog = models.ForeignKey( + "combatlog.CombatLog", on_delete=models.CASCADE) ladder = models.ForeignKey(Ladder, on_delete=models.CASCADE) visible = models.BooleanField(default=True) + def ladder_entry_channels(self): + channels = [] + if self.ladder.internal_name in [ + "Infected Space", + ] and self.ladder.internal_difficulty in ["Advanced", "Elite"]: + if self.data.get("DPS", 0) >= 500000: + channels.append("DPS-#s-Prime") + if self.data.get("DPS", 0) >= 150000: + channels.append("DPS-#s-Elites") + elif self.ladder.internal_name in [ + "Hive Space", + ] and self.ladder.internal_difficulty in ["Elite"]: + if self.data.get("DPS", 0) >= 500000: + channels.append("DPS-#s-Prime") + if self.data.get("DPS", 0) >= 150000: + channels.append("DPS-#s-Elites") + elif self.ladder.internal_name in [ + "Bug Hunt", + "Nukera Prime: Transdimensional Tactics", + ]: + if self.data.get("DPS", 0) >= 1000: + channels.append("DPS-#s-Ground") + return channels + def __str__(self): return f"{self.player} | {self.ladder.name} ({self.ladder.difficulty}, {self.ladder.variant.name}) - {self.data['DPS']:,.0f} DPS" diff --git a/ladder/models/variant.py b/ladder/models/variant.py index 1f3c0c9..69fa6d4 100644 --- a/ladder/models/variant.py +++ b/ladder/models/variant.py @@ -1,4 +1,4 @@ -""" Variant Models """ +"""Variant Models""" from django.db import models diff --git a/ladder/templates/ladder_entry.html b/ladder/templates/ladder_entry.html index c4a650c..404fcca 100644 --- a/ladder/templates/ladder_entry.html +++ b/ladder/templates/ladder_entry.html @@ -4,7 +4,19 @@ {% load static %} {% load ladder %} {% load ladder_entry %} -
+ +
@@ -71,9 +83,12 @@ selected {%else%} {%endif%} - value="0">2+ Players + value="0">Team
+
@@ -83,18 +98,18 @@ - - + + - - + + - + @@ -130,17 +145,17 @@ {% if entry.ladder.is_solo %} Solo {% else %} - 2+ Players + Team {% endif %} diff --git a/ladder/templatetags/ladder_entry.py b/ladder/templatetags/ladder_entry.py index 5b6c10f..e48cc5b 100644 --- a/ladder/templatetags/ladder_entry.py +++ b/ladder/templatetags/ladder_entry.py @@ -3,6 +3,7 @@ import logging from django import template + from ladder.models import Ladder, LadderEntry register = template.Library() diff --git a/ladder/views/ladder_entry.py b/ladder/views/ladder_entry.py index 380fe4f..003614e 100644 --- a/ladder/views/ladder_entry.py +++ b/ladder/views/ladder_entry.py @@ -2,18 +2,22 @@ import logging -from core.filters import BaseFilterBackend -from core.pagination import PageNumberPagination from django.core.exceptions import ImproperlyConfigured from django.db.models.query import QuerySet +from django.http import HttpResponseRedirect, StreamingHttpResponse +from django.urls import reverse +from django.utils import timezone from django_filters.views import FilterView -from ladder.filters import LadderEntryFilter -from ladder.models import LadderEntry -from ladder.serializers import LadderEntrySerializer from rest_framework.filters import OrderingFilter from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.viewsets import GenericViewSet +from core.filters import BaseFilterBackend +from core.pagination import PageNumberPagination +from ladder.filters import LadderEntryFilter +from ladder.models import LadderEntry, Variant +from ladder.serializers import LadderEntrySerializer + LOGGER = logging.getLogger("django") @@ -74,6 +78,26 @@ def get_queryset(self): return queryset.filter(visible=True) + def get(self, request, *args, **kwargs): + """Override GET to add parameters into the request object.""" + if "search" not in request.GET: + base_url = reverse("ladder_entries") + query_string = request.GET.copy() + query_string["search"] = "1" + query_string["ladder__variant__name"] = ( + Variant.objects.filter(start_date__lte=timezone.now()) + .order_by("-start_date") + .first() + .name + ) + url = f"{base_url}?{query_string.urlencode()}" + return HttpResponseRedirect(url) + else: + request.GET._mutable = True + request.GET.pop("search") + request.GET._mutable = False + return super().get(request, *args, **kwargs) + class LadderInvitesView(FilterView): """LadderEntry View""" @@ -97,3 +121,18 @@ def get_queryset(self): .order_by("ladder__id") .distinct("ladder__difficulty", "player") ) + + def get_stream(self): + """Return queryset as stream response.""" + yield "
"
+        for entry in self.get_queryset():
+            for channel in entry.ladder_entry_channels():
+                yield f"/channel_invite {channel} {entry.player}\n"
+        yield "
" + + def get(self, request, *args, **kwargs): + """Overload for HTTP Get Method.""" + response = StreamingHttpResponse(self.get_stream()) + response["Cache-Control"] = "no-cache" + response["X-Accel-Buffering"] = "no" + return response diff --git a/requirements.txt b/requirements.txt index 3bf16a6..2042940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Django==4.2.9 djangorestframework==3.14.0 drf_yasg==1.21.7 django_filter==23.5 -STO-OSCR>=2024.9b20 +STO-OSCR>=2024.9b220 whitenoise==6.6.0 psycopg[binary,pool]==3.1.18 requests>=2.31.0 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..9eafb1d --- /dev/null +++ b/ruff.toml @@ -0,0 +1,68 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 +fix = true + +# Assume Python 3.8 +target-version = "py38" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# isort like functionality +extend-select = ["I"] + +[lint.isort] +combine-as-imports = true + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/ui/static/img/help-circle.svg b/ui/static/img/help-circle.svg new file mode 100644 index 0000000..6ac0e7e --- /dev/null +++ b/ui/static/img/help-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/templates/stats.html b/ui/templates/stats.html new file mode 100644 index 0000000..dc2a4ac --- /dev/null +++ b/ui/templates/stats.html @@ -0,0 +1,55 @@ +{% extends 'template.html' %} +{% block 'content' %} +{% load ladder %} +{% get_current_variant as instance %} +{% get_variant_metrics instance as current_metrics %} +{% get_variant_metrics None as all_time_metrics %} +
+
+

Current Season: {{instance.name}}

+
    +
  • Number of Characters on Ladder: {{current_metrics.characters.count}}
  • +
  • Number of Characters on Ladder (1m DPS+, Space): {{current_metrics.characters.space.1000000}}
  • +
  • Number of Characters on Ladder (500k DPS+, Space): {{current_metrics.characters.space.500000}}
  • +
  • Number of Characters on Ladder (200k DPS+, Space): {{current_metrics.characters.space.300000}}
  • +
  • Number of Characters on Ladder (100k DPS+, Space): {{current_metrics.characters.space.100000}}
  • +
  • Number of Characters on Ladder (10k DPS+, Ground): {{current_metrics.characters.ground.10000}}
  • +
  • Number of Characters on Ladder (5k DPS+, Ground): {{current_metrics.characters.ground.5000}}
  • +
  • Number of Characters on Ladder (3k DPS+, Ground): {{current_metrics.characters.ground.3000}}
  • +
  • Number of Characters on Ladder (1k DPS+, Ground): {{current_metrics.characters.ground.1000}}
  • +
  • Number of Accounts on Ladder: {{current_metrics.accounts.count}}
  • +
  • Number of Accounts on Ladder (1m DPS+, Space): {{current_metrics.accounts.space.1000000}}
  • +
  • Number of Accounts on Ladder (500k DPS+, Space): {{current_metrics.accounts.space.500000}}
  • +
  • Number of Accounts on Ladder (200k DPS+, Space): {{current_metrics.accounts.space.300000}}
  • +
  • Number of Accounts on Ladder (100k DPS+, Space): {{current_metrics.accounts.space.100000}}
  • +
  • Number of Accounts on Ladder (10k DPS+, Ground): {{current_metrics.accounts.ground.10000}}
  • +
  • Number of Accounts on Ladder (5k DPS+, Ground): {{current_metrics.accounts.ground.5000}}
  • +
  • Number of Accounts on Ladder (3k DPS+, Ground): {{current_metrics.accounts.ground.3000}}
  • +
  • Number of Accounts on Ladder (1k DPS+, Ground): {{current_metrics.accounts.ground.1000}}
  • +
+
+
+

All Time

+
    +
  • Number of Characters on Ladder: {{all_time_metrics.characters.count}}
  • +
  • Number of Characters on Ladder (1m DPS+, Space): {{all_time_metrics.characters.space.1000000}}
  • +
  • Number of Characters on Ladder (500k DPS+, Space): {{all_time_metrics.characters.space.500000}}
  • +
  • Number of Characters on Ladder (200k DPS+, Space): {{all_time_metrics.characters.space.300000}}
  • +
  • Number of Characters on Ladder (100k DPS+, Space): {{all_time_metrics.characters.space.100000}}
  • +
  • Number of Characters on Ladder (10k DPS+, Ground): {{all_time_metrics.characters.ground.10000}}
  • +
  • Number of Characters on Ladder (5k DPS+, Ground): {{all_time_metrics.characters.ground.5000}}
  • +
  • Number of Characters on Ladder (3k DPS+, Ground): {{all_time_metrics.characters.ground.3000}}
  • +
  • Number of Characters on Ladder (1k DPS+, Ground): {{all_time_metrics.characters.ground.1000}}
  • +
  • Number of Accounts on Ladder: {{all_time_metrics.accounts.count}}
  • +
  • Number of Accounts on Ladder (1m DPS+, Space): {{all_time_metrics.accounts.space.1000000}}
  • +
  • Number of Accounts on Ladder (500k DPS+, Space): {{all_time_metrics.accounts.space.500000}}
  • +
  • Number of Accounts on Ladder (200k DPS+, Space): {{all_time_metrics.accounts.space.300000}}
  • +
  • Number of Accounts on Ladder (100k DPS+, Space): {{all_time_metrics.accounts.space.100000}}
  • +
  • Number of Accounts on Ladder (10k DPS+, Ground): {{all_time_metrics.accounts.ground.10000}}
  • +
  • Number of Accounts on Ladder (5k DPS+, Ground): {{all_time_metrics.accounts.ground.5000}}
  • +
  • Number of Accounts on Ladder (3k DPS+, Ground): {{all_time_metrics.accounts.ground.3000}}
  • +
  • Number of Accounts on Ladder (1k DPS+, Ground): {{all_time_metrics.accounts.ground.1000}}
  • +
+
+
+{% endblock %} diff --git a/ui/templates/template.html b/ui/templates/template.html index 97d6d81..1b17042 100644 --- a/ui/templates/template.html +++ b/ui/templates/template.html @@ -7,6 +7,7 @@ + OSCR | Open Source Combatlog Reader @@ -21,15 +22,19 @@
-
- {% block 'content' %} - {% endblock %} +
+
+ {% block 'content' %} + {% endblock %}
diff --git a/ui/templatetags/ladder.py b/ui/templatetags/ladder.py index 613cc33..0305fa5 100644 --- a/ui/templatetags/ladder.py +++ b/ui/templatetags/ladder.py @@ -3,8 +3,9 @@ import logging from django import template +from django.utils import timezone -from ladder.models import Ladder +from ladder.models import Ladder, LadderEntry, Variant register = template.Library() LOGGER = logging.getLogger("django") @@ -20,3 +21,154 @@ def get_ladders(): if entries[ladder.name].get(ladder.difficulty) is None: entries[ladder.name][ladder.difficulty] = ladder.is_solo return entries + + +@register.simple_tag +def get_current_variant(): + """Return the current Ladder""" + return ( + Variant.objects.filter(start_date__lte=timezone.now()) + .order_by("-start_date") + .first() + ) + + +@register.simple_tag +def get_variant_metrics(instance): + """Return ladder metrics""" + LOGGER.info(f"Ladder: {instance}") + if instance: + queryset = LadderEntry.objects.filter(ladder__variant=instance).order_by( + "data__handle", "-data__DPS" + ) + else: + queryset = LadderEntry.objects.order_by("data__handle", "-data__DPS") + + return { + "characters": { + "count": queryset.values("player").distinct().count(), + "space": { + "1000000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=1000000, + ) + .values("player") + .distinct() + .count(), + "500000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=500000, + ) + .values("player") + .distinct() + .count(), + "300000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=300000, + ) + .values("player") + .distinct() + .count(), + "100000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=100000, + ) + .values("player") + .distinct() + .count(), + }, + "ground": { + "10000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=10000, + ) + .values("player") + .distinct() + .count(), + "5000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=5000, + ) + .values("player") + .distinct() + .count(), + "3000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=3000, + ) + .values("player") + .distinct() + .count(), + "1000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=1000, + ) + .values("player") + .distinct() + .count(), + }, + }, + "accounts": { + "count": queryset.values("data__handle").distinct().count(), + "space": { + "1000000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=1000000, + ) + .values("data__handle") + .distinct() + .count(), + "500000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=500000, + ) + .values("data__handle") + .distinct() + .count(), + "300000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=300000, + ) + .values("data__handle") + .distinct() + .count(), + "100000": queryset.filter( + ladder__is_space=True, + data__DPS__gte=100000, + ) + .values("data__handle") + .distinct() + .count(), + }, + "ground": { + "10000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=10000, + ) + .values("data__handle") + .distinct() + .count(), + "5000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=5000, + ) + .values("data__handle") + .distinct() + .count(), + "3000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=3000, + ) + .values("data__handle") + .distinct() + .count(), + "1000": queryset.filter( + ladder__is_space=False, + data__DPS__gte=1000, + ) + .values("data__handle") + .distinct() + .count(), + }, + }, + } diff --git a/ui/urls/ui.py b/ui/urls/ui.py index 386226f..dc264a4 100644 --- a/ui/urls/ui.py +++ b/ui/urls/ui.py @@ -1,8 +1,12 @@ -""" URL URLs """ +"""URL URLs""" -# from django.urls import path -# from django.views.generic import TemplateView +from django.urls import path +from django.views.generic import TemplateView urlpatterns = [ - # path("ui/", TemplateView.as_view(template_name="ui.html")), + path( + "ui/stats/", + TemplateView.as_view(template_name="stats.html"), + name="stats", + ), ]
ResultRankResultRank Date PlayerVariantMapVariantLadder Difficulty DPS Damage Time Highest Damage AbilityGroup SizeGroup
- + {% if entry.combatlog.youtube %} - + {% else %} - + {% endif %}