Skip to content

Commit

Permalink
Merge branch 'main' into upgrading-next.js
Browse files Browse the repository at this point in the history
  • Loading branch information
DrillableBit authored Jan 7, 2025
2 parents e456819 + e4b7c8e commit 93bcf0e
Show file tree
Hide file tree
Showing 32 changed files with 459 additions and 201 deletions.
49 changes: 44 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/pip"
schedule:
interval: daily
open-pull-requests-limit: 10
# Python dependencies
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

# Frontend dependencies (security update grouped)
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "daily"
open-pull-requests-limit: 10
allow:
- dependency-name: "*"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-major"
- "version-update:semver-minor"
- "version-update:semver-patch"

# Frontend dependencies (security updates grouped)
# - package-ecosystem: npm
# directory: "/frontend"
# schedule:
# interval: daily
# open-pull-requests-limit: 0

# Frontend dependencies all updates grouped by major/minor
# - package-ecosystem: npm
# directory: "/frontend"
# schedule:
# interval: daily
# open-pull-requests-limit: 10
# groups:
# minor-and-patch:
# update-types:
# - "minor"
# - "patch"

# major-updates:
# update-types:
# - "major"
2 changes: 1 addition & 1 deletion aiarena/api/arenaclient/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_match_requests_no_open_competition(self):
"""

self.test_client.login(self.staffUser1)
game = self.test_client.create_game("StarCraft II")
game = self.test_client.create_game("StarCraft II", ".SC2Map")
game_mode = self.test_client.create_gamemode("Melee", game.id)
BotRace.create_all_races()
Map.objects.create(name="testmap", game_mode=game_mode)
Expand Down
8 changes: 8 additions & 0 deletions aiarena/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from rest_framework import serializers

from aiarena.core.models import Bot, Map


# From: https://www.guguweb.com/2022/01/23/django-rest-framework-authentication-the-easy-way/

Expand Down Expand Up @@ -42,3 +44,9 @@ def validate(self, attrs):
# It will be used in the view.
attrs["user"] = user
return attrs


class RequestMatchSerializer(serializers.Serializer):
bot1 = serializers.PrimaryKeyRelatedField(queryset=Bot.objects.all())
bot2 = serializers.PrimaryKeyRelatedField(queryset=Bot.objects.all())
map = serializers.PrimaryKeyRelatedField(queryset=Map.objects.all())
34 changes: 34 additions & 0 deletions aiarena/api/tests/test_bot_view_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.test import TestCase
from django.urls import reverse

from rest_framework.test import APIClient

from aiarena.core.tests.test_mixins import MatchReadyMixin


class BotSerializerTestCase(MatchReadyMixin, TestCase):
def test_download_bot_zip_success(self):
"""
Note that this test is for essentially a defunct feature. Downloads would be via AWS S3.
"""
self.client = APIClient()
self.client.force_authenticate(user=self.regularUser1)
bot = self.regularUser1Bot1 # owned by the current user

# URL base name for BotViewSet is api_bot, action url_path is zip
self.url = reverse("api_bot-download-zip", kwargs={"pk": bot.id})
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_download_bot_zip_unauthorized(self):
"""
Note that this test is for essentially a defunct feature. Downloads would be via AWS S3.
"""
self.client = APIClient()
self.client.force_authenticate(user=self.regularUser1)
bot = self.staffUser1Bot1 # owned by someone else

# URL base name for BotViewSet is api_bot, action url_path is zip
self.url = reverse("api_bot-download-zip", kwargs={"pk": bot.id})
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
83 changes: 83 additions & 0 deletions aiarena/api/tests/test_match_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

from rest_framework import status
from rest_framework.test import APIClient

from aiarena.core.models import Map, Match
from aiarena.core.tests.test_mixins import MatchReadyMixin


User = get_user_model()


class MatchRequestsViewSetTests(MatchReadyMixin, TestCase):
def setUp(self):
super().setUp()
self.client = APIClient()
self.client.force_authenticate(user=self.regularUser1)

# Create test map
self.test_map = Map.objects.first()

self.url = reverse("api_request_match-request-single")

def test_request_match_failure_not_supporter(self):
"""Test match request failure - user is not a supporter"""
user = self.staffUser1
user.patreon_level = "none"
response = self.request_match(user)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(Match.objects.count(), 0)

def test_request_match_success_supporter(self):
"""Test match request successful - user is a supporter"""
user = self.staffUser1
user.patreon_level = "bronze"
response = self.request_match(user)

self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn("match_id", response.data)
self.assertEqual(Match.objects.count(), 1)
match = Match.objects.first()
self.assertEqual(match.requested_by, User.objects.get(id=user.id))
self.assertEqual(match.map, self.test_map)

def request_match(self, user):
data = {
"bot1": self.regularUser1Bot1.id,
"bot2": self.regularUser1Bot2.id,
"map": self.test_map.id,
}
self.client.force_authenticate(user=user)
return self.client.post(self.url, data, format="json")

def test_request_match_invalid_bot(self):
"""Test match request with invalid bot ID"""
data = {
"bot1": 99999, # Non-existent bot ID
"bot2": self.regularUser1Bot2.id,
}

user = self.staffUser1
user.patreon_level = "bronze"
self.client.force_authenticate(user=user)
response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Match.objects.count(), 0)

def test_request_match_unauthenticated(self):
"""Test match request without authentication"""
self.client.force_authenticate(user=None)

data = {
"bot1": self.regularUser1Bot1.id,
"bot2": self.regularUser1Bot2.id,
}

response = self.client.post(self.url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(Match.objects.count(), 0)
1 change: 1 addition & 0 deletions aiarena/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
router.register(r"results", publicapi_views.ResultViewSet, basename="api_result")
router.register(r"rounds", publicapi_views.RoundViewSet, basename="api_round")
router.register(r"users", publicapi_views.UserViewSet, basename="api_user")
router.register(r"match-requests", publicapi_views.MatchRequestsViewSet, basename="api_request_match")

# stream
router.register(r"stream/next-replay", stream_views.StreamNextReplayViewSet, basename="api_stream_nextreplay")
Expand Down
94 changes: 82 additions & 12 deletions aiarena/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from rest_framework.viewsets import ViewSet

from aiarena.api import serializers as api_serializers
from aiarena.api.serializers import RequestMatchSerializer
from aiarena.api.view_filters import BotFilter, MatchFilter, MatchParticipationFilter, ResultFilter
from aiarena.core.models import (
Bot,
Expand All @@ -41,6 +42,7 @@
)
from aiarena.core.models.bot_race import BotRace
from aiarena.core.permissions import IsServiceOrAdminUser
from aiarena.core.services import MatchRequests, SupporterBenefits
from aiarena.patreon.models import PatreonUnlinkedDiscordUID


Expand Down Expand Up @@ -524,22 +526,36 @@ def perform_update(self, serializer):

@action(detail=True, methods=["GET"], name="Download a bot's zip file", url_path="zip")
def download_zip(self, request, *args, **kwargs):
bot = Bot.objects.get(id=kwargs["pk"])
if bot.can_download_bot_zip(request.user):
response = HttpResponse(FileWrapper(bot.bot_zip), content_type="application/zip")
response["Content-Disposition"] = f'inline; filename="{bot.name}.zip"'
return response
else:
"""
Download a bot's zip file.
TODO: this is a defunct feature. Downloads should be via AWS S3. Ideally this should be cleaned up.
"""
try:
bot = Bot.objects.get(id=kwargs["pk"])
if bot.can_download_bot_zip(request.user):
response = HttpResponse(FileWrapper(bot.bot_zip), content_type="application/zip")
response["Content-Disposition"] = f'inline; filename="{bot.name}.zip"'
return response
else:
raise Http404()
except Bot.DoesNotExist:
raise Http404()

@action(detail=True, methods=["GET"], name="Download a bot's data file", url_path="data")
def download_data(self, request, *args, **kwargs):
bot = Bot.objects.get(id=kwargs["pk"])
if bot.can_download_bot_data(request.user):
response = HttpResponse(FileWrapper(bot.bot_data), content_type="application/zip")
response["Content-Disposition"] = f'inline; filename="{bot.name}_data.zip"'
return response
else:
"""
Download a bot's data file.
TODO: this is a defunct feature. Downloads should be via AWS S3. Ideally this should be cleaned up.
"""
try:
bot = Bot.objects.get(id=kwargs["pk"])
if bot.can_download_bot_data(request.user):
response = HttpResponse(FileWrapper(bot.bot_data), content_type="application/zip")
response["Content-Disposition"] = f'inline; filename="{bot.name}_data.zip"'
return response
else:
raise Http404()
except Bot.DoesNotExist:
raise Http404()


Expand Down Expand Up @@ -1040,3 +1056,57 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):


# !ATTENTION! IF YOU CHANGE THE API ANNOUNCE IT TO USERS


class MatchRequestsViewSet(viewsets.ViewSet):
"""
Match request view
"""

permission_classes = [permissions.IsAuthenticated]

@swagger_auto_schema(
method="post",
request_body=RequestMatchSerializer,
responses={
201: openapi.Response(
"Match requested successfully",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"message": openapi.Schema(type=openapi.TYPE_STRING),
"match_id": openapi.Schema(type=openapi.TYPE_INTEGER),
},
),
),
400: "Bad Request",
},
)
@action(detail=False, methods=["post"])
def request_single(self, request):
"""
Request a match between two bots.
"""

allowed, reject_message = SupporterBenefits.can_request_match_via_api(request.user)
if not allowed:
return Response({"message": reject_message}, status=status.HTTP_403_FORBIDDEN)

serializer = RequestMatchSerializer(data=request.data)
if serializer.is_valid():
bot1 = serializer.validated_data["bot1"]
bot2 = serializer.validated_data["bot2"]
map_instance = serializer.validated_data.get("map")

try:
match = MatchRequests.request_match(request.user, bot1, bot2, map_instance)
return Response(
{"message": "Match requested successfully", "match_id": match.id}, status=status.HTTP_201_CREATED
)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


# !ATTENTION! IF YOU CHANGE THE API ANNOUNCE IT TO USERS
2 changes: 1 addition & 1 deletion aiarena/core/management/commands/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def run_seed(self, num_acs: int, matches):

ac_client = AcApiTestingClient(api_token=Token.objects.first().key)

game = client.create_game("StarCraft II")
game = client.create_game("StarCraft II", ".SC2Map")
gamemode = client.create_gamemode("Melee", game.id)

protoss, terran, zerg = create_game_races()
Expand Down
2 changes: 1 addition & 1 deletion aiarena/core/management/commands/seed_integration_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def handle(self, *args, **options):

create_arena_clients_with_matching_tokens(self.stdout, client, num_acs, devadmin)

game = client.create_game("StarCraft II")
game = client.create_game("StarCraft II", ".SC2Map")
gamemode = client.create_gamemode("Melee", game.id)

competition = create_open_competition_with_map(
Expand Down
17 changes: 17 additions & 0 deletions aiarena/core/migrations/0079_game_map_file_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.16 on 2024-12-08 15:28

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0078_remove_user_can_request_games_for_another_authors_bot"),
]

operations = [
migrations.AddField(
model_name="game",
name="map_file_extension",
field=models.CharField(default=".SC2Map", max_length=20),
),
]
4 changes: 2 additions & 2 deletions aiarena/core/models/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def can_download_bot_zip(self, user):
- This user is a trusted arenaclient
"""
return (
self.user == user
self.user.id == user.id
or self.bot_zip_publicly_downloadable
or user.is_staff
or (user.is_arenaclient and user.arenaclient.trusted)
Expand All @@ -285,7 +285,7 @@ def can_download_bot_data(self, user):
- This user is a trusted arenaclient
"""
return (
self.user == user
self.user.id == user.id
or self.bot_data_publicly_downloadable
or user.is_staff
or (user.is_arenaclient and user.arenaclient.trusted)
Expand Down
4 changes: 3 additions & 1 deletion aiarena/core/models/competition_participation.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ class CompetitionParticipation(models.Model, LockableModelMixin):

def validate_unique(self, exclude=None):
if self.active:
bot_limit = self.bot.user.get_active_bots_limit()
from ..services import SupporterBenefits # avoid circular import

bot_limit = SupporterBenefits.get_active_bots_limit(self.bot.user)
if (
CompetitionParticipation.objects.exclude(pk=self.pk)
.filter(bot__user=self.bot.user, active=True)
Expand Down
Loading

0 comments on commit 93bcf0e

Please sign in to comment.