Skip to content

Commit

Permalink
Release v1.10.2
Browse files Browse the repository at this point in the history
  • Loading branch information
lladdy authored Jan 31, 2023
2 parents 096d288 + 63fa53e commit dbcdaaa
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 112 deletions.
9 changes: 7 additions & 2 deletions aiarena/api/arenaclient/ac_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from django.db import transaction, connection
from django.db.models import F

from aiarena.api.arenaclient.exceptions import LadderDisabled
from aiarena.api.arenaclient.exceptions import LadderDisabled, NotEnoughAvailableBots, MaxActiveRounds, NoMaps, \
CompetitionPaused, CompetitionClosing
from aiarena.core.api import Matches
from aiarena.core.models import Match, Competition

Expand Down Expand Up @@ -43,7 +44,11 @@ def next_competition_match(arenaclient: ArenaClient):
with transaction.atomic():
# this call will apply a select for update, so we do it inside an atomic block
if Competitions.check_has_matches_to_play_and_apply_locks(competition):
return Matches.start_next_match_for_competition(arenaclient, competition)
try:
return Matches.start_next_match_for_competition(arenaclient, competition)
except (NoMaps, NotEnoughAvailableBots, MaxActiveRounds, CompetitionPaused, CompetitionClosing) as e:
logger.debug(f"Skipping competition {id}: {e}")
continue

return None

Expand Down
47 changes: 15 additions & 32 deletions aiarena/api/arenaclient/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
from rest_framework.exceptions import APIException


class EloSanityCheckException(APIException):
pass
class NoMaps(Exception):
def __init__(self):
super().__init__('There are no active maps available for a match.')


class NoMaps(APIException):
status_code = 200
default_detail = 'There are no active maps available for a match.'
default_code = 'no_maps'


class NotEnoughActiveBots(APIException):
status_code = 200
default_detail = 'Not enough active bots available for a match. Wait until more bots are activated.'
default_code = 'not_enough_active_bots'


class NotEnoughAvailableBots(APIException):
status_code = 200
default_detail = 'Not enough available bots for a match. Wait until more bots become available.'
default_code = 'not_enough_available_bots'
class NotEnoughAvailableBots(Exception):
def __init__(self):
super().__init__('Not enough available bots for a match. Wait until more bots become available.')


class MaxActiveRounds(APIException):
status_code = 200
default_detail = 'There are available bots, but the ladder has reached the maximum active rounds allowed and ' \
'serving a new match would require generating a new one. Please wait until matches from current ' \
'rounds become available.'
default_code = 'max_active_rounds'
class MaxActiveRounds(Exception):
def __init__(self):
super().__init__('This competition has reached it\'s maximum active rounds.')


class LadderDisabled(APIException):
Expand All @@ -37,16 +22,14 @@ class LadderDisabled(APIException):
default_code = 'ladder_disabled'


class CompetitionPaused(APIException):
status_code = 200
default_detail = 'The competition is paused.'
default_code = 'competition_paused'
class CompetitionPaused(Exception):
def __init__(self):
super().__init__('This competition is paused.')


class CompetitionClosing(APIException):
status_code = 200
default_detail = 'This competition is closing.'
default_code = 'competition_closing'
class CompetitionClosing(Exception):
def __init__(self):
super().__init__('This competition is closing.')

class NoGameForClient(APIException):
status_code = 200
Expand Down
28 changes: 18 additions & 10 deletions aiarena/api/arenaclient/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,15 @@ def test_max_active_rounds(self):
self.assertEqual(response.status_code, 201)

# Round 3 - should fail due to active round limit
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertTrue('detail' in response.data)
self.assertEqual(u'There are available bots, but the ladder has reached the maximum active rounds allowed and ' \
'serving a new match would require generating a new one. Please wait until matches from current ' \
'rounds become available.',
response.data['detail'])
with self.assertLogs(logger='aiarena.api.arenaclient.ac_coordinator', level='DEBUG') as log:
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertTrue('detail' in response.data)
self.assertEqual(u'No game available for client.',
response.data['detail'])
self.assertIn(f'DEBUG:aiarena.api.arenaclient.ac_coordinator:Skipping competition {comp.id}: '
'This competition has reached it\'s maximum active rounds.',
log.output)

def test_match_blocking(self):
# create an extra arena client for this test
Expand All @@ -232,9 +234,15 @@ def test_match_blocking(self):

# we shouldn't be able to get a new match
self.test_ac_api_client.set_api_token(Token.objects.create(user=self.arenaclientUser2).key)
response = self.test_ac_api_client.post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertEqual(u"Not enough available bots for a match. Wait until more bots become available.", response.data['detail'])

with self.assertLogs(logger='aiarena.api.arenaclient.ac_coordinator', level='DEBUG') as log:
response = self.test_ac_api_client.post_to_matches()
self.assertIn(f'DEBUG:aiarena.api.arenaclient.ac_coordinator:Skipping competition {comp.id}: '
f'Not enough available bots for a match. Wait until more bots become available.',
log.output)

self.assertEqual(response.status_code, 200)
self.assertEqual(u"No game available for client.", response.data['detail'])

Matches.request_match(self.regularUser2, bot1, bot1.get_random_active_excluding_self(),
game_mode=GameMode.objects.first())
Expand Down
3 changes: 2 additions & 1 deletion aiarena/core/d_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db.models.fields import CharField, TextField
from django.db.models import Q, Aggregate
from rest_framework.exceptions import ValidationError
from django.contrib.postgres.aggregates import StringAgg

# File for housing utils that require 'django' or would break CI if placed in utils.py
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -61,7 +62,7 @@ def filter_tags(qs, value, tags_field_name, tags_lookup_expr="iexact", user_fiel
if tags_lookup_expr == 'icontains':
qs = method(qs)(user_query).distinct()
# Create string with all tag names and run icontains on the string
qs = qs.annotate(all_tags=GroupConcat(tags_field_name))
qs = qs.annotate(all_tags=StringAgg(tags_field_name, delimiter=","))
tags_lookup = '%s__%s' % ('all_tags', tags_lookup_expr)
for v in tags:
tag_query &= Q(**{tags_lookup: v})
Expand Down
1 change: 0 additions & 1 deletion aiarena/core/models/round.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from django.utils.html import escape
from django.utils.safestring import mark_safe

from aiarena.api.arenaclient.exceptions import NoMaps, NotEnoughActiveBots, CompetitionPaused, CompetitionClosing
from .map import Map
from .mixins import LockableModelMixin
from .competition import Competition
Expand Down
20 changes: 14 additions & 6 deletions aiarena/core/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,13 @@ def test_competition_states(self):
self._finish_competition_rounds(competition2.id)

# this should fail due to a new round trying to generate while the competition is paused
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertEqual(u'The competition is paused.', response.data['detail'])
with self.assertLogs(logger='aiarena.api.arenaclient.ac_coordinator', level='DEBUG') as log:
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertEqual(u'No game available for client.', response.data['detail'])
self.assertIn(f'DEBUG:aiarena.api.arenaclient.ac_coordinator:Skipping competition {competition1.id}: '
f'This competition is paused.',
log.output)

# reopen the competition
competition1.open()
Expand Down Expand Up @@ -325,9 +329,13 @@ def test_competition_states(self):
self.assertEqual(updated_bot.game_display_id, bot.game_display_id)

# no maps
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertEqual(u'no_maps', response.data['detail'].code)
with self.assertLogs(logger='aiarena.api.arenaclient.ac_coordinator', level='DEBUG') as log:
response = self._post_to_matches()
self.assertEqual(response.status_code, 200)
self.assertEqual(u'No game available for client.', response.data['detail'])
self.assertIn(f'DEBUG:aiarena.api.arenaclient.ac_coordinator:Skipping competition {competition2.id}: '
f'There are no active maps available for a match.',
log.output)

map = Map.objects.first()
map.competitions.add(competition2)
Expand Down
16 changes: 8 additions & 8 deletions aiarena/frontend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
<span class="slide-select" id="newsSlideSelect" onclick="selectSlide({{ forloop.counter }})"></span>
{% endfor %}
</div>
</div>
</div>
<div class="divider-full div-transparent-full "></div>
</div>

Expand All @@ -104,7 +104,7 @@
<td><strong>DIV</strong></td>
<td><strong>ELO</strong></td>
</thead>
<tbody>
<tbody>
{% for participant in item.top10 %}
<tr>
<td style="text-align: center">{{ forloop.counter }}</td>
Expand All @@ -116,7 +116,7 @@
<td>{{ participant.bot.as_truncated_html_link }}</td>
<td style="text-align: center">{{ participant.division_num }}</td>
<td>
{{ participant.elo }}
{{ participant.elo }}
{% with trend=participant.trend %}
{% if trend > 40 %}
<em class="material-icons" style="padding: 0; margin:0; vertical-align: -0.3em; transform: rotate(-90deg);" title="ELO gained {{trend}} in the last 30 games">
Expand All @@ -142,7 +142,7 @@
{% endwith %}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
Expand Down Expand Up @@ -194,17 +194,17 @@
for (var i = 0; i < youtube.length; i++) {
var embed = youtube[i].dataset.embed.match(/[\w\-]{11,}/)[0];
var source = "https://img.youtube.com/vi/"+ embed+"/sddefault.jpg";

var image = new Image();
image.src = source;
image.width=420;
image.height=215;
image.addEventListener( "load", function() {
youtube[ i ].appendChild( image );
}( i ) );

youtube[i].addEventListener( "click", function() {

youtube[i].addEventListener( "click", function() {
var embed = this.dataset.embed.match(/[\w\-]{11,}/)[0];
var iframe = document.createElement( "iframe" );

iframe.setAttribute( "frameborder", "0" );
Expand All @@ -213,7 +213,7 @@

this.innerHTML = "";
this.appendChild( iframe );
} );
} );
};

} )();
Expand Down
4 changes: 4 additions & 0 deletions aiarena/frontend/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def test_render_pages(self):
response = self.client.get('/finance/')
self.assertEqual(response.status_code, 404)

# because this gets broken sometimes
response = self.client.get('/sitemap.xml/')
self.assertEqual(response.status_code, 200)


class RequestMatchTestCase(FullDataSetMixin, TestCase):

Expand Down
84 changes: 39 additions & 45 deletions aiarena/frontend/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ class BotResultTable(tables.Table):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.tags_by_all_value = kwargs.pop('tags_by_all_value', False)
super().__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

# Settings for the Table
class Meta:
Expand Down Expand Up @@ -467,18 +467,6 @@ 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'
Expand Down Expand Up @@ -846,33 +834,36 @@ def get_context_data(self, **kwargs):
# Order competitions as they are to be shown on home page
competitions = competitions.order_by('-n_active_bots','-interest','-num_participants')
context['competitions'] = []

elo_trend_n_matches = config.ELO_TREND_N_MATCHES
for comp in competitions:
if Round.objects.filter(competition=comp).count() > 0:
top10 = Ladders.get_competition_ranked_participants(
comp, amount=10).prefetch_related(
Prefetch('bot', queryset=Bot.objects.all().only('user_id', 'name')),
Prefetch('bot__user', queryset=User.objects.all().only('patreon_level'))
)
)
# top 10 bots

relative_result = RelativeResult.with_row_number([x.bot.id for x in top10], comp)


sql, params = relative_result.query.sql_with_params()

with connection.cursor() as cursor:
cursor.execute("""
SELECT bot_id as id, SUM("elo_change") trend FROM ({}) row_numbers
WHERE "row_number" <= %s
GROUP BY bot_id
""".format(sql),
[*params, elo_trend_n_matches],)
rows = cursor.fetchall()
for participant in top10:
participant.trend = next(iter([x[1] for x in rows if x[0] == participant.bot.id]), None)



# This check avoids a potential EmptyResultSet exception.
# See https://code.djangoproject.com/ticket/26061
if relative_result.count() > 0:
sql, params = relative_result.query.sql_with_params()

with connection.cursor() as cursor:
cursor.execute("""
SELECT bot_id as id, SUM("elo_change") trend FROM ({}) row_numbers
WHERE "row_number" <= %s
GROUP BY bot_id
""".format(sql),
[*params, elo_trend_n_matches], )
rows = cursor.fetchall()
for participant in top10:
participant.trend = next(iter([x[1] for x in rows if x[0] == participant.bot.id]), None)

context['competitions'].append({
'competition': comp,
'top10': top10,
Expand Down Expand Up @@ -998,9 +989,9 @@ def get_context_data(self, **kwargs):
for map in maps:
map_names.append(map.name)
context['map_names'] = map_names

elo_trend_n_matches = config.ELO_TREND_N_MATCHES

rounds = Round.objects.filter(competition_id=self.object.id).order_by('-id')
page = self.request.GET.get('page', 1)
paginator = Paginator(rounds, 30)
Expand All @@ -1022,19 +1013,22 @@ def get_context_data(self, **kwargs):
Prefetch('bot__user', queryset=User.objects.all().only('patreon_level', 'username','type'))))

relative_result = RelativeResult.with_row_number([x.bot.id for x in all_participants], self.object)

sql, params = relative_result.query.sql_with_params()

with connection.cursor() as cursor:
cursor.execute("""
SELECT bot_id as id, SUM("elo_change") trend FROM ({}) row_numbers
WHERE "row_number" <= %s
GROUP BY bot_id
""".format(sql),
[*params, elo_trend_n_matches],)
rows = cursor.fetchall()
for participant in all_participants:
participant.trend = next(iter([x[1] for x in rows if x[0] == participant.bot.id]), None)

# This check avoids a potential EmptyResultSet exception.
# See https://code.djangoproject.com/ticket/26061
if relative_result.count() > 0:
sql, params = relative_result.query.sql_with_params()

with connection.cursor() as cursor:
cursor.execute("""
SELECT bot_id as id, SUM("elo_change") trend FROM ({}) row_numbers
WHERE "row_number" <= %s
GROUP BY bot_id
""".format(sql),
[*params, elo_trend_n_matches],)
rows = cursor.fetchall()
for participant in all_participants:
participant.trend = next(iter([x[1] for x in rows if x[0] == participant.bot.id]), None)

context['divisions'] = dict()
to_title = lambda x: f"Awaiting Entry" if x==CompetitionParticipation.DEFAULT_DIVISION else f"Division {x}"
Expand Down
Loading

0 comments on commit dbcdaaa

Please sign in to comment.