From ef3170fc3ae5c07a73e3caab6e68d42ae08cc140 Mon Sep 17 00:00:00 2001 From: lladdy Date: Tue, 15 Feb 2022 16:44:27 +1030 Subject: [PATCH 1/3] Generate seperate stats plot with last bot update marked --- ...tionparticipation_elo_graph_update_plot.py | 21 +++++ .../core/models/competition_participation.py | 7 +- aiarena/core/stats/stats_generator.py | 77 ++++++++++++------- 3 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 aiarena/core/migrations/0041_competitionparticipation_elo_graph_update_plot.py diff --git a/aiarena/core/migrations/0041_competitionparticipation_elo_graph_update_plot.py b/aiarena/core/migrations/0041_competitionparticipation_elo_graph_update_plot.py new file mode 100644 index 00000000..11e36258 --- /dev/null +++ b/aiarena/core/migrations/0041_competitionparticipation_elo_graph_update_plot.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.9 on 2022-02-15 06:10 + +import aiarena.core.models.competition_participation +import aiarena.core.storage +from django.db import migrations +import private_storage.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0040_alter_arenaclientstatus_logged_at'), + ] + + operations = [ + migrations.AddField( + model_name='competitionparticipation', + name='elo_graph_update_plot', + field=private_storage.fields.PrivateFileField(blank=True, null=True, storage=aiarena.core.storage.OverwritePrivateStorage(base_url='/'), upload_to=aiarena.core.models.competition_participation.elo_graph_update_plot_upload_to), + ), + ] diff --git a/aiarena/core/models/competition_participation.py b/aiarena/core/models/competition_participation.py index 548b48e7..9e44310b 100644 --- a/aiarena/core/models/competition_participation.py +++ b/aiarena/core/models/competition_participation.py @@ -4,12 +4,13 @@ from django.db import models from django.utils.text import slugify from django.core.validators import MinValueValidator +from private_storage.fields import PrivateFileField from aiarena.settings import ELO_START_VALUE from .bot import Bot from .mixins import LockableModelMixin from .competition import Competition -from ..storage import OverwriteStorage +from ..storage import OverwriteStorage, OverwritePrivateStorage from ..validators import validate_not_nan, validate_not_inf logger = logging.getLogger(__name__) @@ -18,6 +19,9 @@ def elo_graph_upload_to(instance, filename): return '/'.join(['graphs', f'{instance.competition_id}_{instance.bot.id}_{instance.bot.name}.png']) +def elo_graph_update_plot_upload_to(instance, filename): + return '/'.join(['graphs', f'{instance.competition_id}_{instance.bot.id}_{instance.bot.name}_update_plot.png']) + class CompetitionParticipation(models.Model, LockableModelMixin): competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='participations') @@ -33,6 +37,7 @@ class CompetitionParticipation(models.Model, LockableModelMixin): crash_perc = models.FloatField(blank=True, null=True, validators=[validate_not_nan, validate_not_inf]) crash_count = models.IntegerField(default=0) elo_graph = models.FileField(upload_to=elo_graph_upload_to, storage=OverwriteStorage(), blank=True, null=True) + elo_graph_update_plot = PrivateFileField(upload_to=elo_graph_update_plot_upload_to, storage=OverwritePrivateStorage(base_url='/'), blank=True, null=True) highest_elo = models.IntegerField(blank=True, null=True) slug = models.SlugField(max_length=255, blank=True) active = models.BooleanField(default=True) diff --git a/aiarena/core/stats/stats_generator.py b/aiarena/core/stats/stats_generator.py index 58941230..d8dcc0d6 100644 --- a/aiarena/core/stats/stats_generator.py +++ b/aiarena/core/stats/stats_generator.py @@ -1,12 +1,13 @@ import io +from datetime import datetime import matplotlib.dates as mdates import matplotlib.pyplot as plt import pandas as pd -from django.db import connection, transaction +from django.db import connection from django.db.models import Max -from aiarena.core.models import MatchParticipation, CompetitionParticipation +from aiarena.core.models import MatchParticipation, CompetitionParticipation, Bot from aiarena.core.models.competition_bot_matchup_stats import CompetitionBotMatchupStats @@ -44,9 +45,12 @@ def update_stats(sp: CompetitionParticipation): match__round__competition=sp.competition) \ .aggregate(Max('resultant_elo'))['resultant_elo__max'] - graph = StatsGenerator._generate_elo_graph(sp.bot.id, sp.competition_id) - if graph is not None: - sp.elo_graph.save('elo.png', graph) + graph1, graph2 = StatsGenerator._generate_elo_graph(sp.bot.id, sp.competition_id) + if graph1 is not None: + sp.elo_graph.save('elo.png', graph1) + + if graph2 is not None: + sp.elo_graph_update_plot.save('elo_update_plot.png', graph2) else: sp.win_count = 0 sp.loss_count = 0 @@ -58,24 +62,28 @@ def update_stats(sp: CompetitionParticipation): @staticmethod def _update_matchup_stats(sp: CompetitionParticipation): - for competition_participation in CompetitionParticipation.objects.filter(competition=sp.competition).exclude(bot=sp.bot): + for competition_participation in CompetitionParticipation.objects.filter(competition=sp.competition).exclude( + bot=sp.bot): with connection.cursor() as cursor: matchup_stats = CompetitionBotMatchupStats.objects.select_for_update() \ .get_or_create(bot=sp, opponent=competition_participation)[0] - matchup_stats.match_count = StatsGenerator._calculate_matchup_count(cursor, competition_participation, sp) + matchup_stats.match_count = StatsGenerator._calculate_matchup_count(cursor, competition_participation, + sp) if matchup_stats.match_count != 0: matchup_stats.win_count = StatsGenerator._calculate_win_count(cursor, competition_participation, sp) matchup_stats.win_perc = matchup_stats.win_count / matchup_stats.match_count * 100 - matchup_stats.loss_count = StatsGenerator._calculate_loss_count(cursor, competition_participation, sp) + matchup_stats.loss_count = StatsGenerator._calculate_loss_count(cursor, competition_participation, + sp) matchup_stats.loss_perc = matchup_stats.loss_count / matchup_stats.match_count * 100 matchup_stats.tie_count = StatsGenerator._calculate_tie_count(cursor, competition_participation, sp) matchup_stats.tie_perc = matchup_stats.tie_count / matchup_stats.match_count * 100 - matchup_stats.crash_count = StatsGenerator._calculate_crash_count(cursor, competition_participation, sp) + matchup_stats.crash_count = StatsGenerator._calculate_crash_count(cursor, competition_participation, + sp) matchup_stats.crash_perc = matchup_stats.crash_count / matchup_stats.match_count * 100 else: matchup_stats.win_count = 0 @@ -196,27 +204,42 @@ def _get_data(bot_id, competition_id): return elo_over_time @staticmethod - def _generate_plot_image(df): - plot = io.BytesIO() - - fig, ax = plt.subplots(figsize=(12, 9)) - ax.plot(df["Date"], df['ELO'], color='#86c232') - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.spines["left"].set_color('#86c232') - ax.spines["bottom"].set_color('#86c232') - ax.autoscale(enable=True, axis='x') - ax.get_xaxis().tick_bottom() - ax.get_yaxis().tick_left() - ax.xaxis.set_major_formatter(mdates.DateFormatter('%b-%d')) - ax.tick_params(axis='x', colors='#86c232', labelsize=16) - ax.tick_params(axis='y', colors='#86c232', labelsize=16) + def _generate_plot_images(df, update_date: datetime): + plot1 = io.BytesIO() + plot2 = io.BytesIO() + + legend = [] + + fig, ax1 = plt.subplots(1, 1, figsize=(12, 9), sharex='all', sharey='all') + legend.append('ELO') + ax1.plot(df["Date"], df['ELO'], color='#86c232') + # ax.plot(df["Date"], df['ELO'], color='#86c232') + ax1.spines["top"].set_visible(False) + ax1.spines["right"].set_visible(False) + ax1.spines["left"].set_color('#86c232') + ax1.spines["bottom"].set_color('#86c232') + ax1.autoscale(enable=True, axis='x') + ax1.get_xaxis().tick_bottom() + ax1.get_yaxis().tick_left() + ax1.xaxis.set_major_formatter(mdates.DateFormatter('%b-%d')) + ax1.tick_params(axis='x', colors='#86c232', labelsize=16) + ax1.tick_params(axis='y', colors='#86c232', labelsize=16) + # if update_date: + + ax1.legend(legend, loc='lower center', fontsize='xx-large') + plt.title('ELO over time', fontsize=20, color=('#86c232')) plt.tight_layout() # Avoids savefig cutting off x-label - plt.savefig(plot, format="png", transparent=True) + plt.savefig(plot1, format="png", transparent=True) + + legend.append('Last update') + ax1.legend(legend, loc='lower center', fontsize='xx-large') + ax1.vlines([update_date], + min(df['ELO']), max(df['ELO']), colors='r', linestyles='--') + plt.savefig(plot2, format="png", transparent=True) plt.cla() # Clears axis in preparation for new graph - return plot + return plot1, plot2 @staticmethod def _generate_elo_graph(bot_id: int, competition_id: int): @@ -226,6 +249,6 @@ def _generate_elo_graph(bot_id: int, competition_id: int): df['2'] = pd.to_datetime(df['2']) df.columns = ['Name', 'ELO', 'Date'] - return StatsGenerator._generate_plot_image(df) + return StatsGenerator._generate_plot_images(df, Bot.objects.get(id=bot_id).bot_zip_updated) else: return None From 9e124823952c6225072186c8f7ab068226a4d414 Mon Sep 17 00:00:00 2001 From: lladdy Date: Tue, 15 Feb 2022 17:12:43 +1030 Subject: [PATCH 2/3] Fix error in 403 template --- aiarena/frontend/templates/403.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiarena/frontend/templates/403.html b/aiarena/frontend/templates/403.html index 94efacbe..bf2333c7 100644 --- a/aiarena/frontend/templates/403.html +++ b/aiarena/frontend/templates/403.html @@ -6,7 +6,7 @@ {% if user.is_authenticated %} You dont have permissions to do that! {% else %} - You need to be {% trans "logged in" %} to do that! + You need to be "logged in" to do that! {% endif %} {% endblock %} \ No newline at end of file From dd175626830663cd7b40ae0842426e65898a3ff0 Mon Sep 17 00:00:00 2001 From: lladdy Date: Tue, 15 Feb 2022 17:29:10 +1030 Subject: [PATCH 3/3] Display elo update plot graph to supporter users --- .../core/models/competition_participation.py | 2 +- aiarena/core/stats/stats_generator.py | 5 ++-- .../templates/bot_competition_stats.html | 7 +++++- aiarena/frontend/views.py | 25 +++++++++++++++++++ aiarena/urls.py | 1 + 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/aiarena/core/models/competition_participation.py b/aiarena/core/models/competition_participation.py index 9e44310b..8e4efb63 100644 --- a/aiarena/core/models/competition_participation.py +++ b/aiarena/core/models/competition_participation.py @@ -20,7 +20,7 @@ def elo_graph_upload_to(instance, filename): return '/'.join(['graphs', f'{instance.competition_id}_{instance.bot.id}_{instance.bot.name}.png']) def elo_graph_update_plot_upload_to(instance, filename): - return '/'.join(['graphs', f'{instance.competition_id}_{instance.bot.id}_{instance.bot.name}_update_plot.png']) + return '/'.join(['competitions', 'stats', f'{instance.id}_elo_graph_update_plot.png']) class CompetitionParticipation(models.Model, LockableModelMixin): diff --git a/aiarena/core/stats/stats_generator.py b/aiarena/core/stats/stats_generator.py index d8dcc0d6..a7b41175 100644 --- a/aiarena/core/stats/stats_generator.py +++ b/aiarena/core/stats/stats_generator.py @@ -211,7 +211,6 @@ def _generate_plot_images(df, update_date: datetime): legend = [] fig, ax1 = plt.subplots(1, 1, figsize=(12, 9), sharex='all', sharey='all') - legend.append('ELO') ax1.plot(df["Date"], df['ELO'], color='#86c232') # ax.plot(df["Date"], df['ELO'], color='#86c232') ax1.spines["top"].set_visible(False) @@ -226,10 +225,10 @@ def _generate_plot_images(df, update_date: datetime): ax1.tick_params(axis='y', colors='#86c232', labelsize=16) # if update_date: + legend.append('ELO') ax1.legend(legend, loc='lower center', fontsize='xx-large') plt.title('ELO over time', fontsize=20, color=('#86c232')) - plt.tight_layout() # Avoids savefig cutting off x-label plt.savefig(plot1, format="png", transparent=True) @@ -237,6 +236,8 @@ def _generate_plot_images(df, update_date: datetime): ax1.legend(legend, loc='lower center', fontsize='xx-large') ax1.vlines([update_date], min(df['ELO']), max(df['ELO']), colors='r', linestyles='--') + legend.append('Last update') + ax1.legend(legend, loc='lower center', fontsize='xx-large') plt.savefig(plot2, format="png", transparent=True) plt.cla() # Clears axis in preparation for new graph return plot1, plot2 diff --git a/aiarena/frontend/templates/bot_competition_stats.html b/aiarena/frontend/templates/bot_competition_stats.html index babbbd42..30bd93f6 100644 --- a/aiarena/frontend/templates/bot_competition_stats.html +++ b/aiarena/frontend/templates/bot_competition_stats.html @@ -79,7 +79,12 @@
- {% if competitionparticipation.elo_graph %} + {# If this user owns the bot, and is a supporter, display the private ELO graph #} + {% if competitionparticipation.bot.user == user and user.patreon_level != 'none' and competitionparticipation.elo_graph_update_plot %} +
+ ELOGraph +
+ {% elif competitionparticipation.elo_graph %}
ELOGraph
diff --git a/aiarena/frontend/views.py b/aiarena/frontend/views.py index 5fa233ec..dabd8d57 100644 --- a/aiarena/frontend/views.py +++ b/aiarena/frontend/views.py @@ -459,6 +459,31 @@ def get_context_data(self, **kwargs): return context +class BotCompetitionStatsEloUpdatePlot(DetailView): + model = CompetitionParticipation + template_name = 'bot_competition_stats.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['competition_bot_matchups'] = self.object.competition_matchup_stats.filter( + opponent__competition=context['competitionparticipation'].competition).order_by('-win_perc').distinct() + context['updated'] = context['competition_bot_matchups'][0].updated if context['competition_bot_matchups'] else "Never" + return context + + +class BotCompetitionStatsEloGraphUpdatePlot(PrivateStorageDetailView): + model = CompetitionParticipation + model_file_field = 'elo_graph_update_plot' + + content_disposition = 'inline' + + def get_content_disposition_filename(self, private_file): + return 'elo_graph_update_plot.png' + + def can_access_file(self, private_file): + # Allow if the owner of the bot and a patreon supporter + return private_file.parent_object.bot.user == private_file.request.user and private_file.request.user.patreon_level != 'None' + class BotUpdateForm(forms.ModelForm): """ diff --git a/aiarena/urls.py b/aiarena/urls.py index 3f929d7c..773e0a34 100644 --- a/aiarena/urls.py +++ b/aiarena/urls.py @@ -84,6 +84,7 @@ path('competitions/stats//', core_views.BotCompetitionStatsDetail.as_view()), path('competitions/stats//', core_views.BotCompetitionStatsDetail.as_view(), name='bot_competition_stats'), + path('competitions/stats/_elo_graph_update_plot.png', core_views.BotCompetitionStatsEloGraphUpdatePlot.as_view()), path('botupload/', core_views.BotUpload.as_view(), name='botupload'), path('requestmatch/', core_views.RequestMatch.as_view(), name='requestmatch'),