diff --git a/.env.example b/.env.example
index b17fe55..0bd2321 100644
--- a/.env.example
+++ b/.env.example
@@ -15,4 +15,6 @@ DISCORD_CLIENT_SECRET=discord_client_secret
STEAM_API_KEY=some_steam_api_key
CSRF_TRUSTED_ORIGINS=http://localhost:8002
CORS_ALLOW_ALL_ORIGINS=True
-SECURE_SSL_REDIRECT=False
\ No newline at end of file
+SECURE_SSL_REDIRECT=False
+DJANGO_SUPERUSER_USERNAME=admin
+DJANGO_SUPERUSER_PASSWORD=password
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 22515e5..79f97d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -265,4 +265,6 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/django,python
*.sqlite3
-*.env.prod
\ No newline at end of file
+*.env.prod
+
+.idea/
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a1..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/cs2-battle-bot-api.iml b/.idea/cs2-battle-bot-api.iml
deleted file mode 100644
index 4693793..0000000
--- a/.idea/cs2-battle-bot-api.iml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
deleted file mode 100644
index d52521f..0000000
--- a/.idea/material_theme_project_new.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index b5b3f79..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index cc5bb4e..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/ruff.xml b/.idea/ruff.xml
deleted file mode 100644
index dc7b5c1..0000000
--- a/.idea/ruff.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 05d2815..bf8b74a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,11 +13,13 @@ RUN poetry config virtualenvs.create false
#RUN poetry config installer.no-binary cryptography
# Copy the project files for dependency installation
-COPY pyproject.toml ./poetry.lock /code/
+COPY ./pyproject.toml ./poetry.lock /code/
# Install project dependencies using Poetry
RUN poetry install --no-interaction --no-ansi
+
+
# Stage 2: Runtime environment
FROM builder AS runtime
ENV PYTHONUNBUFFERED 1
@@ -31,9 +33,11 @@ WORKDIR /app
# Copy only the necessary files from the build stage
COPY --from=builder /code /app
+COPY --from=builder /code/pyproject.toml /app/
+
+
# Copy the source code
COPY src /app/
-COPY scripts /app/scripts
# Change ownership to the dedicated user
diff --git a/poetry.lock b/poetry.lock
index 4d45ef1..43191e0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -316,13 +316,13 @@ test-randomorder = ["pytest-randomly"]
[[package]]
name = "django"
-version = "5.0.5"
+version = "5.0.6"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
- {file = "Django-5.0.5-py3-none-any.whl", hash = "sha256:8af4f166dc9a2bb822f9374cd78e34a10c286b402597fe2c7fb97c131656ba65"},
- {file = "Django-5.0.5.tar.gz", hash = "sha256:dc95c9cb2a37ba54599d9d1c8faf81609d36f3e74cd04395ce1300573e57baf9"},
+ {file = "Django-5.0.6-py3-none-any.whl", hash = "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905"},
+ {file = "Django-5.0.6.tar.gz", hash = "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f"},
]
[package.dependencies]
@@ -1221,4 +1221,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "f633f3240916b78163cfcfb9b3e5b5241fddda3651312faf4654d031007fcad7"
+content-hash = "9917045f6a776540ca286762b66f68e14ff89147a5935c05d8720a15eb270de7"
diff --git a/pyproject.toml b/pyproject.toml
index e8c47a1..66f66f6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cs2-battle-bot-api"
-version = "0.0.35"
+version = "0.0.36"
description = ""
authors = ["Adrian Ciolek "]
readme = "README.md"
@@ -11,7 +11,7 @@ packages = [
[tool.poetry.dependencies]
python = "^3.11"
-django = "^5.0.2"
+django = "^5.0.6"
httpx = "^0.27.0"
psycopg2-binary = "^2.9.9"
django-prefix-id = "^1.0.0"
diff --git a/scripts/install.sh b/scripts/install.sh
index 468a551..f679adb 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -1,24 +1,2 @@
#!/bin/bash
-echo "Building the project"
-docker compose up -d --build
-echo "Project is running"
-
-# Run migrations
-echo "Running migrations"
-docker compose exec app sh -c "cd src && python manage.py migrate"
-echo "Migrations complete"
-
-# Loading map fixtures
-echo "Loading map fixtures"
-docker compose exec app sh -c "cd src && python manage.py loaddata maps"
-echo "Map fixtures loaded"
-
-echo "Creating superuser"
-# Create superuser
-SUPER_USER=$(docker compose exec app sh -c "cd src && python manage.py shell -c \"from django.contrib.auth import get_user_model; User = get_user_model(); user = User.objects.create_superuser('admin', 'admin@myproject.com', 'password'); print(user);\"")
-echo "Superuser $SUPER_USER created"
-
-echo "Creating API key"
-# Create the API key
-API_KEY=$(docker compose exec app sh -c "cd src && python manage.py shell -c \"from rest_framework_api_key.models import APIKey; api_key, key = APIKey.objects.create_key(name='cs2-battle-bot'); print(key);\"")
-echo "Api key created: $API_KEY"
\ No newline at end of file
+docker compose exec app sh -c "cd src && ./install.sh"
\ No newline at end of file
diff --git a/src/cs2_battle_bot/settings.py b/src/cs2_battle_bot/settings.py
index 7d934ba..19a9e0a 100644
--- a/src/cs2_battle_bot/settings.py
+++ b/src/cs2_battle_bot/settings.py
@@ -192,9 +192,12 @@
def get_spectacular_settings():
# Load the pyproject.toml file
pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
- if DEBUG is True:
+ pyproject_data = None
+ try:
+ pyproject_data = toml.load(pyproject_path)
+ except FileNotFoundError:
pyproject_path = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
- pyproject_data = toml.load(pyproject_path)
+ pyproject_data = toml.load(pyproject_path)
# Get the name, version, and description
name = pyproject_data.get("tool", {}).get("poetry", {}).get("name", "")
@@ -204,6 +207,5 @@ def get_spectacular_settings():
# Assign them to the SPECTACULAR_SETTINGS dictionary
return {"TITLE": name, "VERSION": version, "DESCRIPTION": description}
-
# Use the function
SPECTACULAR_SETTINGS = get_spectacular_settings()
diff --git a/src/guilds/admin.py b/src/guilds/admin.py
index 37d2e37..9772bd0 100644
--- a/src/guilds/admin.py
+++ b/src/guilds/admin.py
@@ -2,5 +2,10 @@
from guilds.models import Guild
+class GuildAdmin(admin.ModelAdmin):
+ list_filter = ('created_at', 'updated_at')
+ search_fields = ('name', 'created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
# Register your models here.
-admin.site.register(Guild)
\ No newline at end of file
+admin.site.register(Guild, GuildAdmin)
\ No newline at end of file
diff --git a/src/guilds/migrations/0005_embed_embedfield_guild_embed_embed_fields.py b/src/guilds/migrations/0005_embed_embedfield_guild_embed_embed_fields.py
new file mode 100644
index 0000000..7125649
--- /dev/null
+++ b/src/guilds/migrations/0005_embed_embedfield_guild_embed_embed_fields.py
@@ -0,0 +1,51 @@
+# Generated by Django 5.0.6 on 2024-05-10 10:20
+
+import django.db.models.deletion
+import prefix_id.field
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0004_remove_guild_members'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Embed',
+ fields=[
+ ('id', prefix_id.field.PrefixIDField(editable=False, max_length=28, prefix='embed', primary_key=True, serialize=False, unique=True)),
+ ('title', models.CharField(blank=True, max_length=255, null=True)),
+ ('description', models.TextField(blank=True, null=True)),
+ ('color', models.CharField(blank=True, max_length=255, null=True)),
+ ('footer', models.CharField(blank=True, max_length=255, null=True)),
+ ('image', models.CharField(blank=True, max_length=255, null=True)),
+ ('thumbnail', models.CharField(blank=True, max_length=255, null=True)),
+ ('author', models.CharField(blank=True, max_length=255, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='EmbedField',
+ fields=[
+ ('id', prefix_id.field.PrefixIDField(editable=False, max_length=34, prefix='embed_field', primary_key=True, serialize=False, unique=True)),
+ ('name', models.CharField(blank=True, max_length=255, null=True)),
+ ('value', models.CharField(blank=True, max_length=255, null=True)),
+ ('inline', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='guild',
+ name='embed',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='guilds.embed'),
+ ),
+ migrations.AddField(
+ model_name='embed',
+ name='fields',
+ field=models.ManyToManyField(blank=True, to='guilds.embedfield'),
+ ),
+ ]
diff --git a/src/guilds/migrations/0006_embedfield_order.py b/src/guilds/migrations/0006_embedfield_order.py
new file mode 100644
index 0000000..3f0c90d
--- /dev/null
+++ b/src/guilds/migrations/0006_embedfield_order.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.6 on 2024-05-10 12:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0005_embed_embedfield_guild_embed_embed_fields'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='embedfield',
+ name='order',
+ field=models.IntegerField(default=1),
+ ),
+ ]
diff --git a/src/guilds/models.py b/src/guilds/models.py
index e5a63a8..8b9da8a 100644
--- a/src/guilds/models.py
+++ b/src/guilds/models.py
@@ -1,14 +1,42 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
+from django.db.models.signals import post_save
+from django.dispatch import receiver
from prefix_id import PrefixIDField
-from rest_framework_api_key.models import AbstractAPIKey
from players.models import DiscordUser, Player
UserModel = get_user_model()
+class EmbedField(models.Model):
+ id = PrefixIDField(primary_key=True, prefix="embed_field")
+ order = models.IntegerField(default=1)
+ name = models.CharField(max_length=255, null=True, blank=True)
+ value = models.CharField(max_length=255, null=True, blank=True)
+ inline = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+
+class Embed(models.Model):
+ id = PrefixIDField(primary_key=True, prefix="embed")
+ title = models.CharField(max_length=255, null=True, blank=True)
+ description = models.TextField(null=True, blank=True)
+ color = models.CharField(max_length=255, null=True, blank=True)
+ footer = models.CharField(max_length=255, null=True, blank=True)
+ image = models.CharField(max_length=255, null=True, blank=True)
+ thumbnail = models.CharField(max_length=255, null=True, blank=True)
+ author = models.CharField(max_length=255, null=True, blank=True)
+ fields = models.ManyToManyField(EmbedField, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return self.title
+
+
# Create your models here.
class GuildManager(models.Manager):
@@ -35,8 +63,43 @@ class Guild(models.Model):
lobby_channel = models.CharField(max_length=255, null=True, blank=True)
team1_channel = models.CharField(max_length=255, null=True, blank=True)
team2_channel = models.CharField(max_length=255, null=True, blank=True)
+ embed = models.ForeignKey(Embed, on_delete=models.CASCADE, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
+
+
+@receiver(post_save, sender=Guild)
+def create_guild(sender, instance, created, **kwargs):
+ if created:
+ embed = Embed.objects.create(
+ title=f"Guild configuration {instance.name}",
+ author=instance.owner.player.discord_user.username
+ )
+ fields = [
+ EmbedField.objects.create(
+ name="Lobby Channel",
+ value=f"@<{instance.lobby_channel}"
+ ),
+ EmbedField.objects.create(
+ name="Team 1 Channel",
+ value=f"@<{instance.team1_channel}"
+ ),
+ EmbedField.objects.create(
+ name="Team 2 Channel",
+ value=f"@<{instance.team2_channel}"
+ )
+ ]
+ embed.fields.set(fields)
+ instance.embed = embed
+ instance.save()
+ else:
+ instance.embed.title = f"Guild configuration {instance.name}"
+ instance.embed.author = instance.owner.player.discord_user.username
+ instance.embed.save()
+ instance.embed.fields.get(name="Lobby Channel").value = f"@<{instance.lobby_channel}"
+ instance.embed.fields.get(name="Team 1 Channel").value = f"@<{instance.team1_channel}"
+ instance.embed.fields.get(name="Team 2 Channel").value = f"@<{instance.team2_channel}"
+ instance.embed.save()
\ No newline at end of file
diff --git a/src/guilds/serializers.py b/src/guilds/serializers.py
index d8aee19..daddd24 100644
--- a/src/guilds/serializers.py
+++ b/src/guilds/serializers.py
@@ -1,11 +1,10 @@
from rest_framework import serializers
from accounts.serializers import UserSerializer
-from guilds.models import Guild
+from guilds.models import Guild, EmbedField, Embed
class GuildSerializer(serializers.ModelSerializer):
-
owner = UserSerializer(read_only=True)
class Meta:
@@ -24,4 +23,26 @@ class UpdateGuildSerializer(serializers.Serializer):
name = serializers.CharField(required=False)
lobby_channel = serializers.CharField(required=False)
team1_channel = serializers.CharField(required=False)
- team2_channel = serializers.CharField(required=False)
\ No newline at end of file
+ team2_channel = serializers.CharField(required=False)
+
+
+class EmbedFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = EmbedField
+ fields = ["name", "value", "inline"]
+
+
+class EmbedSerializer(serializers.ModelSerializer):
+ fields = EmbedFieldSerializer(many=True)
+
+ class Meta:
+ model = Embed
+ fields = ["title", "description", "color", "footer", "image", "thumbnail", "author", "fields"]
+
+ def to_representation(self, instance):
+ representation = super().to_representation(instance)
+ # Sort the fields by the 'order' attribute
+ representation['fields'] = sorted(instance.fields.all(), key=lambda x: x.order)
+ # Serialize the sorted fields
+ representation['fields'] = EmbedFieldSerializer(representation['fields'], many=True).data
+ return representation
diff --git a/src/install.sh b/src/install.sh
new file mode 100755
index 0000000..340b9bd
--- /dev/null
+++ b/src/install.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m' # No Color
+
+# Run migrations
+python manage.py migrate
+
+# Loading map fixtures
+python manage.py loaddata maps
+
+# Create superuser
+python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME"
+echo -e "${GREEN}Superuser ${RED}$DJANGO_SUPERUSER_USERNAME ${NC}created"
+
+# Create the API key
+API_KEY=$(python manage.py shell -c "from rest_framework_api_key.models import APIKey; api_key, key = APIKey.objects.create_key(name='cs2-battle-bot'); print(key);")
+echo -e "${GREEN}Api key created: ${RED}$API_KEY${NC}. Put this key to the .env file as API_KEY variable. You will not be able to see it again."
\ No newline at end of file
diff --git a/src/matches/admin.py b/src/matches/admin.py
index cf36196..f54553b 100644
--- a/src/matches/admin.py
+++ b/src/matches/admin.py
@@ -1,8 +1,46 @@
from django.contrib import admin
-from matches.models import Map, Match, MapBan
+from matches.models import Map, Match, MapBan, MapPool, MatchConfig, MapPick
+
+
+class MatchAdmin(admin.ModelAdmin):
+ list_filter = ('status', 'created_at', 'updated_at')
+ search_fields = ('team1__name', 'team2__name', 'status', 'winner_team__name', 'created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+class MatchConfigAdmin(admin.ModelAdmin):
+ list_filter = ('game_mode', 'type', 'clinch_series', 'max_players', 'created_at', 'updated_at')
+ search_fields = ('name', 'game_mode', 'type', 'clinch_series', 'max_players', 'created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+class MapAdmin(admin.ModelAdmin):
+ list_filter = ('guild', 'created_at', 'updated_at')
+ search_fields = ('name', 'guild__name', 'guild__id', 'created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+class MapPoolAdmin(admin.ModelAdmin):
+ list_filter = ('guild__name', 'created_at', 'updated_at')
+ search_fields = ('name', 'guild__name', 'guild__id', 'created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+class MapBanAdmin(admin.ModelAdmin):
+ list_filter = ('created_at', 'updated_at')
+ search_fields = ('created_at', 'updated_at')
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+class MapPickAdmin(MapBanAdmin):
+ pass
+
# Register your models here.
-admin.site.register(Match)
-admin.site.register(Map)
-admin.site.register(MapBan)
+admin.site.register(Match, MatchAdmin)
+admin.site.register(MatchConfig, MatchConfigAdmin)
+admin.site.register(Map, MapAdmin)
+admin.site.register(MapPool, MapPoolAdmin)
+admin.site.register(MapBan, MapBanAdmin)
+admin.site.register(MapPick, MapPickAdmin)
diff --git a/src/matches/migrations/0020_remove_match_clinch_series_remove_match_map_sides_and_more.py b/src/matches/migrations/0020_remove_match_clinch_series_remove_match_map_sides_and_more.py
new file mode 100644
index 0000000..57486b1
--- /dev/null
+++ b/src/matches/migrations/0020_remove_match_clinch_series_remove_match_map_sides_and_more.py
@@ -0,0 +1,77 @@
+# Generated by Django 5.0.6 on 2024-05-09 20:18
+
+import django.db.models.deletion
+import prefix_id.field
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0004_remove_guild_members'),
+ ('matches', '0019_match_last_map_ban_match_last_map_pick'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='match',
+ name='clinch_series',
+ ),
+ migrations.RemoveField(
+ model_name='match',
+ name='map_sides',
+ ),
+ migrations.RemoveField(
+ model_name='match',
+ name='maps',
+ ),
+ migrations.RemoveField(
+ model_name='match',
+ name='num_maps',
+ ),
+ migrations.RemoveField(
+ model_name='match',
+ name='players_per_team',
+ ),
+ migrations.RemoveField(
+ model_name='match',
+ name='type',
+ ),
+ migrations.CreateModel(
+ name='MapPool',
+ fields=[
+ ('id', prefix_id.field.PrefixIDField(editable=False, max_length=31, prefix='map_pool', primary_key=True, serialize=False, unique=True)),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('guild', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='map_pools', to='guilds.guild')),
+ ('maps', models.ManyToManyField(related_name='map_pools', to='matches.map')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='match',
+ name='map_pool',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='matches.mappool'),
+ ),
+ migrations.CreateModel(
+ name='MatchConfig',
+ fields=[
+ ('id', prefix_id.field.PrefixIDField(editable=False, max_length=35, prefix='match_config', primary_key=True, serialize=False, unique=True)),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ('game_mode', models.CharField(choices=[('COMPETITIVE', 'Competitive'), ('WINGMAN', 'Wingman'), ('AIM', 'Aim')], default='COMPETITIVE', max_length=255)),
+ ('type', models.CharField(choices=[('BO1', 'Bo1'), ('BO3', 'Bo3'), ('BO5', 'Bo5')], default='BO1', max_length=255)),
+ ('map_sides', models.JSONField(null=True)),
+ ('clinch_series', models.BooleanField(default=False)),
+ ('max_players', models.PositiveIntegerField(default=10)),
+ ('cvars', models.JSONField(null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('guild', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='match_configs', to='guilds.guild')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='match',
+ name='config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='matches.matchconfig'),
+ ),
+ ]
diff --git a/src/matches/migrations/0021_map_guild_alter_map_name_alter_map_tag.py b/src/matches/migrations/0021_map_guild_alter_map_name_alter_map_tag.py
new file mode 100644
index 0000000..8f92dce
--- /dev/null
+++ b/src/matches/migrations/0021_map_guild_alter_map_name_alter_map_tag.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.0.6 on 2024-05-09 20:22
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0004_remove_guild_members'),
+ ('matches', '0020_remove_match_clinch_series_remove_match_map_sides_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='map',
+ name='guild',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maps', to='guilds.guild'),
+ ),
+ migrations.AlterField(
+ model_name='map',
+ name='name',
+ field=models.CharField(max_length=255, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='map',
+ name='tag',
+ field=models.CharField(max_length=255, unique=True),
+ ),
+ ]
diff --git a/src/matches/migrations/0022_alter_map_guild_alter_mappool_guild_and_more.py b/src/matches/migrations/0022_alter_map_guild_alter_mappool_guild_and_more.py
new file mode 100644
index 0000000..404f969
--- /dev/null
+++ b/src/matches/migrations/0022_alter_map_guild_alter_mappool_guild_and_more.py
@@ -0,0 +1,102 @@
+# Generated by Django 5.0.6 on 2024-05-09 20:28
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0004_remove_guild_members'),
+ ('matches', '0021_map_guild_alter_map_name_alter_map_tag'),
+ ('players', '0005_alter_player_steam_user'),
+ ('servers', '0006_remove_server_max_players'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='map',
+ name='guild',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='maps', to='guilds.guild'),
+ ),
+ migrations.AlterField(
+ model_name='mappool',
+ name='guild',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='map_pools', to='guilds.guild'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='author',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='players.discorduser'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='config',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='matches.matchconfig'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='cvars',
+ field=models.JSONField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='guild',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='guilds.guild'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='last_map_ban',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches_last_map_ban', to='matches.mapban'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='last_map_pick',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches_last_map_pick', to='matches.mappick'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='map_pool',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='matches.mappool'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='maplist',
+ field=models.JSONField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='message_id',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='server',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='servers.server'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='status',
+ field=models.CharField(choices=[('CREATED', 'Created'), ('STARTED', 'Started'), ('LIVE', 'Live'), ('FINISHED', 'Finished'), ('CANCELLED', 'Cancelled')], default='CREATED', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='team1',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches_team1', to='players.team'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='team2',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches_team2', to='players.team'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='winner_team',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches_winner', to='players.team'),
+ ),
+ migrations.AlterField(
+ model_name='matchconfig',
+ name='guild',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='match_configs', to='guilds.guild'),
+ ),
+ ]
diff --git a/src/matches/migrations/0023_alter_mappick_map_alter_match_author_and_more.py b/src/matches/migrations/0023_alter_mappick_map_alter_match_author_and_more.py
new file mode 100644
index 0000000..8a00da5
--- /dev/null
+++ b/src/matches/migrations/0023_alter_mappick_map_alter_match_author_and_more.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.0.6 on 2024-05-09 21:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('matches', '0022_alter_map_guild_alter_mappool_guild_and_more'),
+ ('players', '0005_alter_player_steam_user'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='mappick',
+ name='map',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='map_picks', to='matches.map'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='author',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='players.discorduser'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='map_bans',
+ field=models.ManyToManyField(blank=True, related_name='matches_map_bans', to='matches.mapban'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='map_picks',
+ field=models.ManyToManyField(blank=True, related_name='matches_map_picks', to='matches.mappick'),
+ ),
+ migrations.AlterField(
+ model_name='matchconfig',
+ name='cvars',
+ field=models.JSONField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='matchconfig',
+ name='map_sides',
+ field=models.JSONField(blank=True, null=True),
+ ),
+ ]
diff --git a/src/matches/migrations/0024_remove_match_map_pool_matchconfig_map_pool.py b/src/matches/migrations/0024_remove_match_map_pool_matchconfig_map_pool.py
new file mode 100644
index 0000000..5eb9038
--- /dev/null
+++ b/src/matches/migrations/0024_remove_match_map_pool_matchconfig_map_pool.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.6 on 2024-05-09 21:11
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('matches', '0023_alter_mappick_map_alter_match_author_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='match',
+ name='map_pool',
+ ),
+ migrations.AddField(
+ model_name='matchconfig',
+ name='map_pool',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='match_configs', to='matches.mappool'),
+ ),
+ ]
diff --git a/src/matches/migrations/0025_matchconfig_shuffle_teams_alter_match_status.py b/src/matches/migrations/0025_matchconfig_shuffle_teams_alter_match_status.py
new file mode 100644
index 0000000..9d37af8
--- /dev/null
+++ b/src/matches/migrations/0025_matchconfig_shuffle_teams_alter_match_status.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.6 on 2024-05-09 21:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('matches', '0024_remove_match_map_pool_matchconfig_map_pool'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='matchconfig',
+ name='shuffle_teams',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='status',
+ field=models.CharField(choices=[('CREATED', 'Created'), ('STARTED', 'Started'), ('LOADED', 'Loaded'), ('LIVE', 'Live'), ('FINISHED', 'Finished'), ('CANCELLED', 'Cancelled')], default='CREATED', max_length=255),
+ ),
+ ]
diff --git a/src/matches/migrations/0026_match_embed.py b/src/matches/migrations/0026_match_embed.py
new file mode 100644
index 0000000..c616efc
--- /dev/null
+++ b/src/matches/migrations/0026_match_embed.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.0.6 on 2024-05-10 10:33
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guilds', '0005_embed_embedfield_guild_embed_embed_fields'),
+ ('matches', '0025_matchconfig_shuffle_teams_alter_match_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='match',
+ name='embed',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='guilds.embed'),
+ ),
+ ]
diff --git a/src/matches/migrations/0027_alter_match_status.py b/src/matches/migrations/0027_alter_match_status.py
new file mode 100644
index 0000000..dd57cf9
--- /dev/null
+++ b/src/matches/migrations/0027_alter_match_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.6 on 2024-05-10 14:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('matches', '0026_match_embed'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='match',
+ name='status',
+ field=models.CharField(choices=[('CREATED', 'Created'), ('CAPTAINS_SELECT', 'Captains Select'), ('MAP_VETO', 'Map Veto'), ('READY_TO_LOAD', 'Ready To Load'), ('LOADED', 'Loaded'), ('LIVE', 'Live'), ('FINISHED', 'Finished'), ('CANCELLED', 'Cancelled')], default='CREATED', max_length=255),
+ ),
+ ]
diff --git a/src/matches/migrations/0028_alter_match_last_map_ban_alter_match_last_map_pick.py b/src/matches/migrations/0028_alter_match_last_map_ban_alter_match_last_map_pick.py
new file mode 100644
index 0000000..422e7da
--- /dev/null
+++ b/src/matches/migrations/0028_alter_match_last_map_ban_alter_match_last_map_pick.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.6 on 2024-05-10 16:22
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('matches', '0027_alter_match_status'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='match',
+ name='last_map_ban',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches_last_map_ban', to='matches.mapban'),
+ ),
+ migrations.AlterField(
+ model_name='match',
+ name='last_map_pick',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='matches_last_map_pick', to='matches.mappick'),
+ ),
+ ]
diff --git a/src/matches/models.py b/src/matches/models.py
index 657eaba..4b89115 100644
--- a/src/matches/models.py
+++ b/src/matches/models.py
@@ -3,18 +3,25 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
+from django.db.models.signals import post_save, pre_save
+from django.dispatch import receiver
from prefix_id import PrefixIDField
-from players.models import Team
+from guilds.models import Embed, EmbedField
+from players.models import Team, Player
UserModel = get_user_model()
class MatchStatus(models.TextChoices):
CREATED = "CREATED"
- STARTED = "STARTED"
+ CAPTAINS_SELECT = "CAPTAINS_SELECT"
+ MAP_VETO = "MAP_VETO"
+ READY_TO_LOAD = "READY_TO_LOAD"
+ LOADED = "LOADED"
LIVE = "LIVE"
FINISHED = "FINISHED"
+ CANCELLED = "CANCELLED"
class MatchType(models.TextChoices):
@@ -23,15 +30,34 @@ class MatchType(models.TextChoices):
BO5 = "BO5"
+class GameMode(models.TextChoices):
+ COMPETITIVE = "COMPETITIVE"
+ WINGMAN = "WINGMAN"
+ AIM = "AIM"
+
+
class Map(models.Model):
id = PrefixIDField(primary_key=True, prefix="map")
- name = models.CharField(max_length=255)
- tag = models.CharField(max_length=255)
+ name = models.CharField(max_length=255, unique=True)
+ tag = models.CharField(max_length=255, unique=True)
+ guild = models.ForeignKey("guilds.Guild", on_delete=models.CASCADE, related_name="maps", null=True, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"<{self.name} - {self.tag}>"
+
+
+class MapPool(models.Model):
+ id = PrefixIDField(primary_key=True, prefix="map_pool")
+ name = models.CharField(max_length=255, unique=True)
+ maps = models.ManyToManyField(Map, related_name="map_pools")
+ guild = models.ForeignKey("guilds.Guild", on_delete=models.CASCADE, related_name="map_pools", null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
- return f"<{self.name} - {self.tag} - {self.id}>"
+ return self.name if not self.guild else f"<{self.name} - {self.guild.name}>"
class MapBan(models.Model):
@@ -48,7 +74,7 @@ class MapPick(models.Model):
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name="map_selected"
)
- map = models.ForeignKey(Map, on_delete=models.CASCADE, related_name="map_selected")
+ map = models.ForeignKey(Map, on_delete=models.CASCADE, related_name="map_picks")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -56,40 +82,31 @@ def __str__(self):
return f"<{self.team.name} - {self.map.name}>"
+class MatchConfig(models.Model):
+ id = PrefixIDField(primary_key=True, prefix="match_config")
+ name = models.CharField(max_length=255, unique=True)
+ game_mode = models.CharField(
+ max_length=255, choices=GameMode.choices, default=GameMode.COMPETITIVE
+ )
+ type = models.CharField(
+ max_length=255, choices=MatchType.choices, default=MatchType.BO1
+ )
+ map_pool = models.ForeignKey(MapPool, on_delete=models.CASCADE, related_name="match_configs", null=True, blank=True)
+ map_sides = models.JSONField(null=True, blank=True)
+ clinch_series = models.BooleanField(default=False)
+ max_players = models.PositiveIntegerField(default=10)
+ cvars = models.JSONField(null=True, blank=True)
+ guild = models.ForeignKey("guilds.Guild", on_delete=models.CASCADE, related_name="match_configs", null=True,
+ blank=True)
+ shuffle_teams = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return self.name
+
+
class MatchManager(models.Manager):
- def create_match(self, **kwargs):
- maps = Map.objects.all()
- maplist = kwargs.pop("maplist", None)
- if not maplist:
- maplist = [map.tag for map in maps]
- print(maplist)
- map_sides = kwargs.pop("map_sides", ["knife", "knife", "knife"])
- match_type = kwargs.pop("type", MatchType.BO1)
- team1 = kwargs.pop("team1", None)
- team2 = kwargs.pop("team2", None)
- author = kwargs.pop("author", None)
- server = kwargs.pop("server", None)
- num_maps = kwargs.pop("num_maps", 1 if match_type == MatchType.BO1 else 3)
- cvars = kwargs.pop("cvars", None)
- players_list = team1.players.all() | team2.players.all()
- player_per_team = len(players_list) / 2
- players_per_team_rounded = math.ceil(player_per_team)
- match = self.create(
- **kwargs,
- type=match_type,
- team1=team1,
- team2=team2,
- maplist=maplist,
- map_sides=map_sides,
- num_maps=num_maps,
- players_per_team=players_per_team_rounded,
- server=server,
- author=author,
- cvars=cvars,
- )
- match.maps.set(maps)
- match.save()
- return match
def check_server_is_available_for_match(self, server):
return not self.filter(
@@ -97,47 +114,46 @@ def check_server_is_available_for_match(self, server):
(Q(status=MatchStatus.LIVE) | Q(status=MatchStatus.STARTED))
).exists()
+
# Create your models here.
class Match(models.Model):
objects = MatchManager()
-
status = models.CharField(
max_length=255, choices=MatchStatus.choices, default=MatchStatus.CREATED
)
- type = models.CharField(
- max_length=255, choices=MatchType.choices, default=MatchType.BO1
- )
- maps = models.ManyToManyField(Map, related_name="matches_maps")
+ config = models.ForeignKey(MatchConfig, on_delete=models.CASCADE, related_name="matches", null=True, blank=True)
team1 = models.ForeignKey(
- Team, on_delete=models.CASCADE, related_name="matches_team1", null=True
+ "players.Team", on_delete=models.CASCADE, related_name="matches_team1", null=True, blank=True
)
team2 = models.ForeignKey(
- Team, on_delete=models.CASCADE, related_name="matches_team2", null=True
+ "players.Team", on_delete=models.CASCADE, related_name="matches_team2", null=True, blank=True
)
winner_team = models.ForeignKey(
- Team, on_delete=models.CASCADE, related_name="matches_winner", null=True
+ "players.Team", on_delete=models.CASCADE, related_name="matches_winner", null=True, blank=True
)
- map_bans = models.ManyToManyField(MapBan, related_name="matches_map_bans")
+ map_bans = models.ManyToManyField(MapBan, related_name="matches_map_bans", blank=True)
map_picks = models.ManyToManyField(
- MapPick, related_name="matches_map_picks"
+ MapPick, related_name="matches_map_picks", blank=True
)
- last_map_ban = models.ForeignKey(MapBan, on_delete=models.CASCADE, related_name="matches_last_map_ban", null=True)
- last_map_pick = models.ForeignKey(MapPick, on_delete=models.CASCADE, related_name="matches_last_map_pick", null=True)
- num_maps = models.PositiveIntegerField(default=1)
- maplist = models.JSONField(null=True)
- map_sides = models.JSONField(null=True)
- clinch_series = models.BooleanField(default=False)
- cvars = models.JSONField(null=True)
- players_per_team = models.PositiveIntegerField(default=5)
- message_id = models.CharField(max_length=255, null=True)
+ last_map_ban = models.ForeignKey(MapBan, on_delete=models.SET_NULL, related_name="matches_last_map_ban", null=True,
+ blank=True)
+ last_map_pick = models.ForeignKey(MapPick, on_delete=models.SET_NULL, related_name="matches_last_map_pick",
+ null=True, blank=True)
+ maplist = models.JSONField(null=True, blank=True)
+ cvars = models.JSONField(null=True, blank=True)
+ message_id = models.CharField(max_length=255, null=True, blank=True)
author = models.ForeignKey("players.DiscordUser", on_delete=models.CASCADE, related_name="matches", null=True)
server = models.ForeignKey(
- "servers.Server", on_delete=models.CASCADE, related_name="matches", null=True
+ "servers.Server", on_delete=models.CASCADE, related_name="matches", null=True, blank=True
)
- guild = models.ForeignKey("guilds.Guild", on_delete=models.CASCADE, related_name="matches", null=True)
+ guild = models.ForeignKey("guilds.Guild", on_delete=models.CASCADE, related_name="matches", null=True, blank=True)
+ embed = models.ForeignKey("guilds.Embed", on_delete=models.CASCADE, related_name="matches", null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
+ def __str__(self):
+ return f"<{self.status} - {self.config.name} - {self.pk}>"
+
@property
def api_key_header(self):
return "Authorization"
@@ -146,8 +162,6 @@ def api_key_header(self):
def load_match_command_name(self):
return "matchzy_loadmatch_url"
- def __str__(self):
- return f"<{self.team1.name} vs {self.team2.name}- {self.status} - {self.type} - {self.pk}>"
def get_team1_players_dict(self):
return {
@@ -161,20 +175,29 @@ def get_team2_players_dict(self):
"players": self.team2.get_players_dict(),
}
- def get_config(self):
- config = {
+ def get_matchzy_config(self):
+ num_maps = 1 if self.config.type == MatchType.BO1 else 3
+ players_count = self.team1.players.count() + self.team2.players.count()
+ players_per_team = players_count / 2
+ players_per_team_rounded = math.ceil(players_per_team)
+ matchzy_config = {
"matchid": self.pk,
"team1": self.get_team1_players_dict(),
"team2": self.get_team2_players_dict(),
- "num_maps": self.num_maps,
+ "num_maps": num_maps,
"maplist": self.maplist,
- "map_sides": self.map_sides,
- "clinch_series": self.clinch_series,
- "players_per_team": self.players_per_team,
+ "map_sides": self.config.map_sides,
+ "clinch_series": self.config.clinch_series,
+ "players_per_team": players_per_team_rounded,
}
if self.cvars:
- config["cvars"] = self.cvars
- return config
+ config_cvars = self.config.cvars
+ if config_cvars:
+ self.cvars.update(config_cvars)
+ matchzy_config["cvars"] = self.cvars
+ if self.config.game_mode == GameMode.WINGMAN:
+ matchzy_config["wingman"] = True
+ return matchzy_config
def get_connect_command(self):
return "" if not self.server else self.server.get_connect_string()
@@ -191,28 +214,279 @@ def create_webhook_cvars(self, webhook_url: str):
})
self.save()
- def get_maps_tags(self):
- return [map.tag for map in self.maps.all()]
-
def ban_map(self, team, map):
map_ban = MapBan.objects.create(team=team, map=map)
self.map_bans.add(map_ban)
- self.maps.remove(map)
- self.maplist.remove(map.tag)
self.last_map_ban = map_ban
+
+ if self.config.type == MatchType.BO1:
+ # 6 bans
+ map_bans_count = self.map_bans.count()
+ # 7 maps
+ map_pool_count = self.config.map_pool.maps.count() - 1
+ if map_bans_count == map_pool_count:
+ # Select the last map
+ map_to_select = self.config.map_pool.maps.exclude(tag__in=self.map_bans.values_list("map__tag", flat=True)).exclude(
+ tag__in=self.map_picks.values_list("map__tag", flat=True)).first()
+ self.maplist = [map_to_select.tag]
+ self.status = MatchStatus.READY_TO_LOAD
+ elif self.config.type == MatchType.BO3:
+ # 4 bans
+ map_bans_count = self.map_bans.count()
+ # 7 maps
+ map_pool_count = self.config.map_pool.maps.count() - 3
+ if map_bans_count == map_pool_count:
+ map_to_select = self.config.map_pool.maps.exclude(
+ tag__in=self.map_bans.values_list("map__tag", flat=True)).exclude(
+ tag__in=self.map_picks.values_list("map__tag", flat=True)).first()
+ self.maplist.append(map_to_select.tag)
+ self.status = MatchStatus.READY_TO_LOAD
+
self.save()
return self
def pick_map(self, team, map):
map_pick = MapPick.objects.create(team=team, map=map)
self.map_picks.add(map_pick)
- map_index = self.maplist.index(map.tag)
- self.maplist.pop(map_index)
- map_picks_count = self.map_picks.count()
- if map_picks_count == 1:
- self.maplist.insert(0, map.tag)
- else:
- self.maplist.insert(1, map.tag)
+ # add map to maplist without removing it
+ self.maplist.append(map.tag)
self.last_map_pick = map_pick
self.save()
- return self
\ No newline at end of file
+ return self
+
+ def shuffle_players(self):
+ players_list = list(self.team1.players.all()) + list(self.team2.players.all())
+ players_count = len(players_list)
+ middle_index = players_count // 2
+ team1_players = players_list[:middle_index]
+ team2_players = players_list[middle_index:]
+ self.team1.players.set(team1_players)
+ self.team1.leader = team1_players[0]
+ self.team2.players.set(team2_players)
+ self.team2.leader = team2_players[0]
+ self.save()
+
+ def change_teams_name(self):
+ self.team1.name = f"team_{self.team1.leader.steam_user.username}"
+ self.team1.save()
+ self.team2.name = f"team_{self.team2.leader.steam_user.username}"
+ self.team2.save()
+
+ def add_player_to_match(self, player, team: str = None):
+ if team:
+ if team == "team1":
+ if self.team2.players.filter(id=player.id).exists():
+ self.team2.players.remove(player)
+ self.team1.players.add(player)
+ elif team == "team2":
+ if self.team1.players.filter(id=player.id).exists():
+ self.team1.players.remove(player)
+ self.team2.players.add(player)
+ else:
+ if self.team1.players.count() < self.team2.players.count():
+ self.team1.players.add(player)
+ self.embed.fields.get(name="Team 1").value = f"""
+ ```
+ Name: {self.team1.name}
+ Captain: {self.team1.leader.player.discord_user.username if self.team1.leader else "No captain"}
+ ```
+ ```
+ {[player.discord_user.username for player in self.team1.players.all() if self.team1.players.count() > 0]}
+ ```
+ """
+ elif self.team2.players.count() < self.team1.players.count():
+ self.team2.players.add(player)
+ self.embed.fields.get(name="Team 2").value = f"""
+ ```
+ Name: {self.team2.name}
+ Captain: {self.team2.leader.player.discord_user.username if self.team2.leader else "No captain"}
+ ```
+ ```
+ {[player.discord_user.username for player in self.team2.players.all() if self.team2.players.count() > 0]}
+ ```
+ """
+ else:
+ self.team1.players.add(player)
+ self.embed.save()
+ self.save()
+
+ return self
+
+ def remove_player_from_match(self, player):
+ if player in self.team1.players.all():
+ self.team1.players.remove(player)
+ elif player in self.team2.players.all():
+ self.team2.players.remove(player)
+ self.save()
+ return self
+
+ def start_match(self):
+ if self.config.shuffle_teams:
+ self.shuffle_players()
+ self.change_teams_name()
+ self.status = MatchStatus.MAP_VETO
+ else:
+ self.status = MatchStatus.CAPTAINS_SELECT
+
+ self.save()
+ return self
+
+ def set_team_captain(self, player, team):
+ if team == "team1":
+ print("Setting team1 captain")
+ self.team1.leader = player
+ self.team1.save()
+ elif team == "team2":
+ print("Setting team2 captain")
+ self.team2.leader = player
+ self.team2.save()
+ if self.team1.leader and self.team2.leader:
+ self.status = MatchStatus.MAP_VETO
+ self.change_teams_name()
+ self.save()
+ return self
+
+ def get_team_by_player(self, player):
+ if player in self.team1.players.all():
+ return self.team1
+ elif player in self.team2.players.all():
+ return self.team2
+ return None
+
+ def get_maps_left(self):
+ maps_left = self.config.map_pool.maps.exclude(tag__in=self.map_bans.values_list("map__tag", flat=True)).exclude(
+ tag__in=self.map_picks.values_list("map__tag", flat=True))
+ if self.config.type == MatchType.BO1 and len(maps_left) == 1:
+ return []
+ return [map.tag for map in maps_left]
+
+ def get_next_ban_team(self):
+ return self.team1 if self.last_map_ban.team == self.team2 else self.team2
+
+@receiver(post_save, sender=Match)
+def match_post_save(sender, instance, created, **kwargs):
+ if created:
+ team1_player = Player.objects.get(discord_user=instance.author)
+ team1 = Team.objects.create(name="Team 1")
+ team1.players.add(team1_player)
+ team1.save()
+ team2 = Team.objects.create(name="Team 2")
+
+ instance.team1 = team1
+ instance.team2 = team2
+
+ print(f"Created {team1}")
+ print(f"Created {team2}")
+
+ # create embed
+ embed = Embed.objects.create(
+ title=f"Match {instance.pk} - {instance.config.name}",
+ description=f"""
+ ```
+ Config: {instance.config.name}
+ Type: {instance.config.type}
+ Status: {instance.status}
+ Game mode: {instance.config.game_mode}
+ Map Pool: {instance.config.map_pool.name if instance.config.map_pool else "No map pool"}
+ Max Players: {instance.config.max_players}
+ Map Sides: {instance.config.map_sides}
+ Clinch Series: {instance.config.clinch_series}
+ Custom cvars: {instance.config.cvars if instance.config.cvars else "No custom cvars"}
+ ```
+ """,
+ author=instance.author.username,
+ footer=f"{instance.pk} {instance.created_at}"
+ )
+ team1_players = [player.discord_user.username for player in team1.players.all()]
+ team2_players = [player.discord_user.username for player in team2.players.all()]
+ team1_players_str = "\n".join(team1_players)
+ team2_players_str = "\n".join(team2_players)
+
+ fields = [
+ EmbedField.objects.create(
+ order=1,
+ name="Team 1",
+ value=f"""
+ ```
+ Name: {instance.team1.name}
+ Captain: {instance.team1.leader.discord_user.username if instance.team1.leader else "No captain"}
+ ```
+ ```
+ {team1_players_str}
+ ```
+ """,
+ inline=True
+ ),
+ EmbedField.objects.create(
+ order=2,
+ name="Team 2",
+ value=f"""
+ ```
+ Name: {instance.team2.name}
+ Captain: {instance.team2.leader.discord_user.username if instance.team2.leader else "No captain"}
+ ```
+ ```
+ {team2_players_str}
+ ```
+ """,
+ inline=True
+ )
+ ]
+ embed.fields.set(fields)
+ instance.embed = embed
+ instance.save()
+ else:
+ print("Match updated")
+ print(f"Team 1 {instance.team1.players.count()}")
+ print(f"Team 2 {instance.team2.players.count()}")
+ instance.embed.title = f"Match {instance.pk}"
+ instance.embed.description = f"""
+ ```
+ Config: {instance.config.name}
+ Type: {instance.config.type}
+ Status: {instance.status}
+ Game mode: {instance.config.game_mode}
+ Map Pool: {instance.config.map_pool.name if instance.config.map_pool else "No map pool"}
+ Max Players: {instance.config.max_players}
+ Map Sides: {instance.config.map_sides}
+ Clinch Series: {instance.config.clinch_series}
+ Custom cvars: {instance.config.cvars if instance.config.cvars else "No custom cvars"}
+ ```
+ """
+
+ team1_players = [player.discord_user.username for player in instance.team1.players.all()]
+ team2_players = [player.discord_user.username for player in instance.team2.players.all()]
+ team1_players_str = "\n".join(team1_players)
+ team2_players_str = "\n".join(team2_players)
+
+ team1_field = instance.embed.fields.get(name="Team 1")
+ team1_field.value = f"""
+ ```
+ Name: {instance.team1.name}
+ Captain: {instance.team1.leader.discord_user.username if instance.team1.leader else "No captain"}
+ ```
+ ```
+ {team1_players_str}
+ ```
+ """
+ team1_field.save()
+ team2_field = instance.embed.fields.get(name="Team 2")
+ team2_field.value = f"""
+ ```
+ Name: {instance.team2.name}
+ Captain: {instance.team2.leader.discord_user.username if instance.team2.leader else "No captain"}
+ ```
+ ```
+ {team2_players_str}
+ ```
+ """
+ team2_field.save()
+ if instance.status == MatchStatus.READY_TO_LOAD:
+ if instance.server:
+ server_detail_field = EmbedField.objects.create(
+ order=instance.embed.fields.last().order + 1,
+ name="Server details",
+ value=f"```{instance.get_connect_command()}```"
+ )
+ instance.embed.fields.add(server_detail_field)
+ instance.embed.save()
diff --git a/src/matches/serializers.py b/src/matches/serializers.py
index 4fa5576..4f2734f 100644
--- a/src/matches/serializers.py
+++ b/src/matches/serializers.py
@@ -1,11 +1,16 @@
from enum import Enum
-import re
+
from rest_framework import serializers
from rest_framework.reverse import reverse_lazy
-from guilds.serializers import GuildSerializer
-from matches.models import Map, MapBan, MapPick, Match, MatchType, MatchStatus
+from guilds.models import Guild
+from guilds.serializers import GuildSerializer, EmbedSerializer
+from matches.models import Map, MapBan, MapPick, Match, MatchType, MatchStatus, MatchConfig, MapPool
+from matches.validators import ValidDiscordUser, DiscordUserCanJoinMatch, DiscordUserCanLeaveMatch, TeamCanBeJoined, \
+ DiscordUserIsInMatch, ValidMap, MapCanBeBanned
+from players.models import DiscordUser
from players.serializers import TeamSerializer, DiscordUserSerializer
+from servers.models import Server
from servers.serializers import ServerSerializer
@@ -26,6 +31,14 @@ class Meta:
fields = "__all__"
+class MapPoolSerializer(serializers.ModelSerializer):
+ maps = MapSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = MapPool
+ fields = "__all__"
+
+
class MapBanSerializer(serializers.ModelSerializer):
team = TeamSerializer(read_only=True)
map = MapSerializer(read_only=True)
@@ -44,7 +57,15 @@ class Meta:
fields = "__all__"
-class MatchConfigSerializer(serializers.Serializer):
+class MatchConfigSerializer(serializers.ModelSerializer):
+ map_pool = MapPoolSerializer(read_only=True)
+
+ class Meta:
+ model = MatchConfig
+ fields = "__all__"
+
+
+class MatchzyConfigSerializer(serializers.Serializer):
matchid = serializers.CharField()
team1 = serializers.DictField()
team2 = serializers.DictField()
@@ -56,11 +77,13 @@ class MatchConfigSerializer(serializers.Serializer):
)
)
clinch_series = serializers.BooleanField()
+ wingman = serializers.BooleanField(required=False)
players_per_team = serializers.IntegerField()
cvars = serializers.DictField(required=False)
class MatchSerializer(serializers.ModelSerializer):
+ config = MatchConfigSerializer(read_only=True)
team1 = TeamSerializer(read_only=True)
team2 = TeamSerializer(read_only=True)
maps = MapSerializer(many=True, read_only=True)
@@ -72,27 +95,27 @@ class MatchSerializer(serializers.ModelSerializer):
author = DiscordUserSerializer(read_only=True)
server = ServerSerializer(read_only=True, required=False, allow_null=True)
guild = GuildSerializer(read_only=True)
- config_url = serializers.SerializerMethodField(method_name="get_config_url")
- config = serializers.SerializerMethodField(method_name="get_config")
-
+ matchzy_config_url = serializers.SerializerMethodField(method_name="get_matchzy_config_url")
+ matchzy_config = serializers.SerializerMethodField(method_name="get_matchzy_config")
webhook_url = serializers.SerializerMethodField(method_name="get_webhook_url")
connect_command = serializers.CharField(
read_only=True, source="get_connect_command"
)
load_match_command = serializers.SerializerMethodField(method_name="get_load_match_command")
+ embed = EmbedSerializer(read_only=True, allow_null=True)
- def get_config_url(self, obj) -> str:
+ def get_matchzy_config_url(self, obj) -> str:
return reverse_lazy("match-config", args=[obj.id], request=self.context["request"])
def get_webhook_url(self, obj) -> str:
return reverse_lazy("match-webhook", args=[obj.id], request=self.context["request"])
def get_load_match_command(self, obj) -> str:
- config_url = self.get_config_url(obj)
+ config_url = self.get_matchzy_config_url(obj)
return f'{obj.load_match_command_name} "{config_url}" "{obj.api_key_header}" "Bearer {obj.get_author_token()}"'
- def get_config(self, obj) -> MatchConfigSerializer:
- return MatchConfigSerializer(obj.get_config()).data
+ def get_matchzy_config(self, obj) -> MatchConfigSerializer:
+ return MatchzyConfigSerializer(obj.get_matchzy_config()).data
class Meta:
model = Match
@@ -118,27 +141,31 @@ class MatchUpdateSerializer(serializers.Serializer):
guild_id = serializers.CharField(required=False)
-
class CreateMatchSerializer(serializers.Serializer):
- discord_users_ids = serializers.ListField(child=serializers.CharField())
+ config_name = serializers.CharField(required=True)
author_id = serializers.CharField(required=True)
server_id = serializers.CharField(required=False)
guild_id = serializers.CharField(required=True)
- match_type = serializers.ChoiceField(
- choices=MatchType.choices, default=MatchType.BO1
- )
- clinch_series = serializers.BooleanField(required=False, default=False)
- map_sides = serializers.ListField(
- child=serializers.ChoiceField(
- choices=["team1_ct", "team2_ct", "team1_t", "team2_t", "knife"]
- ),
- required=False,
- default=["knife", "knife", "knife"],
- )
- cvars = serializers.DictField(
- child=serializers.CharField(required=False), required=False
- )
- maplist = serializers.ListField(child=serializers.CharField(), required=False)
+
+ def validate_config_name(self, value):
+ if not MatchConfig.objects.filter(name=value).exists():
+ raise serializers.ValidationError(f"MatchConfig with name {value} does not exist")
+ return value
+
+ def validate_author_id(self, value):
+ if not DiscordUser.objects.filter(user_id=value).exists():
+ raise serializers.ValidationError(f"Author[DiscordUser] with id {value} does not exist")
+ return value
+
+ def validate_server_id(self, value):
+ if value and not Server.objects.filter(id=value).exists():
+ raise serializers.ValidationError(f"Server with id {value} does not exist")
+ return value
+
+ def validate_guild_id(self, value):
+ if not Guild.objects.filter(guild_id=value).exists():
+ raise serializers.ValidationError(f"Guild with id {value} does not exist")
+ return value
class MatchTeamWrapperSerializer(serializers.Serializer):
@@ -195,11 +222,12 @@ class MatchEventMapResultSerializer(MatchEventSerializer):
class InteractionUserSerializer(serializers.Serializer):
- interaction_user_id = serializers.CharField(required=True)
+ interaction_user_id = serializers.CharField(required=True, validators=[ValidDiscordUser()])
-class MatchBanMapSerializer(InteractionUserSerializer):
- map_tag = serializers.CharField(required=True)
+class MatchBanMapSerializer(serializers.Serializer):
+ interaction_user_id = serializers.CharField(required=True, validators=[ValidDiscordUser(), DiscordUserIsInMatch()])
+ map_tag = serializers.CharField(required=True, validators=[ValidMap(), MapCanBeBanned()])
class MatchPickMapSerializer(MatchBanMapSerializer):
@@ -207,11 +235,16 @@ class MatchPickMapSerializer(MatchBanMapSerializer):
class MatchBanMapResultSerializer(serializers.Serializer):
+ match = serializers.SerializerMethodField(method_name="get_match")
banned_map = serializers.SerializerMethodField(method_name="get_banned_map")
next_ban_team = serializers.SerializerMethodField(method_name="get_next_ban_team")
maps_left = serializers.ListField(child=serializers.CharField())
map_bans_count = serializers.IntegerField()
+
+ def get_match(self, obj) -> MatchSerializer:
+ return MatchSerializer(self.context["match"], context={"request": self.context["request"]}).data
+
def get_banned_map(self, obj) -> MapSerializer:
return MapSerializer(self.context["banned_map"]).data
@@ -232,5 +265,29 @@ def get_next_pick_team(self, obj) -> TeamSerializer:
return TeamSerializer(self.context["next_pick_team"]).data
-class MatchPlayerJoin(InteractionUserSerializer):
- pass
+class MatchPlayerJoin(serializers.Serializer):
+ interaction_user_id = serializers.CharField(required=True,
+ validators=[ValidDiscordUser(), DiscordUserCanJoinMatch()])
+ team = serializers.CharField(required=False, validators=[TeamCanBeJoined()])
+
+ def validate_team(self, value):
+ if value not in ["team1", "team2"]:
+ raise serializers.ValidationError("Team must be either 'team1' or 'team2'")
+ return value
+
+
+class MatchPlayerLeave(serializers.Serializer):
+ interaction_user_id = serializers.CharField(required=True,
+ validators=[ValidDiscordUser(), DiscordUserCanLeaveMatch(),
+ DiscordUserIsInMatch()])
+
+
+class MatchSelectCaptain(serializers.Serializer):
+ team = serializers.CharField(required=True)
+ interaction_user_id = serializers.CharField(required=True, validators=[ValidDiscordUser(),
+ DiscordUserIsInMatch(), ])
+
+ def validate_team(self, value):
+ if value not in ["team1", "team2"]:
+ raise serializers.ValidationError("Team must be either 'team1' or 'team2'")
+ return value
diff --git a/src/matches/tests/conftest.py b/src/matches/tests/conftest.py
index f32d0ee..a46fb68 100644
--- a/src/matches/tests/conftest.py
+++ b/src/matches/tests/conftest.py
@@ -2,10 +2,11 @@
from django.test import RequestFactory
from rest_framework.reverse import reverse_lazy
-from matches.models import Map, MatchType, Match
+from matches.models import Map, MatchType, Match, MapPool, MatchConfig, GameMode
from players.tests.conftest import player, discord_user_data, steam_user_data, players, teams, teams_with_players, default_author
from servers.tests.conftest import server, server_data
from guilds.tests.conftest import guild, guild_data
+
@pytest.fixture
def map_data():
return {
@@ -14,7 +15,6 @@ def map_data():
}
-@pytest.mark.django_db
@pytest.fixture(autouse=True)
def default_maps():
maps_dict = [
@@ -49,51 +49,121 @@ def default_maps():
]
maps_obj = [Map.objects.get_or_create(**map) for map in maps_dict]
+@pytest.fixture(autouse=True)
+def default_map_pools():
+ active_duty_map_pool = MapPool.objects.create(name="Competive Active Duty")
+ active_duty_map_pool.maps.set(Map.objects.all())
+ active_duty_map_pool.save()
+
+ wingman_map_pool = MapPool.objects.create(name="Wingman Active Duty")
+ wingman_map_pool.maps.set(Map.objects.filter(tag__in=["de_nuke", "de_inferno", "de_overpass", "de_vertigo"]))
+ wingman_map_pool.save()
+
+
+configs = [
+ "5v5_bo1_shuffle_teams_official",
+ "5v5_bo1_official",
+ "5v5_b03_shuffle_teams_official",
+ "5v5_b03_official",
+ "wingman_b01_official",
+ "wingman_b03_official"
+]
+
+@pytest.fixture(autouse=True)
+def default_match_configs():
+ active_duty_map_pool = MapPool.objects.get(name="Competive Active Duty")
+ wingman_map_pool = MapPool.objects.get(name="Wingman Active Duty")
+
+ MatchConfig.objects.create(
+ name=configs[0],
+ game_mode=GameMode.COMPETITIVE,
+ type=MatchType.BO1,
+ clinch_series=False,
+ max_players=10,
+ shuffle_teams=True,
+ map_pool=active_duty_map_pool,
+ )
+
+ MatchConfig.objects.create(
+ name=configs[1],
+ game_mode=GameMode.COMPETITIVE,
+ type=MatchType.BO1,
+ clinch_series=False,
+ max_players=10,
+ map_pool=active_duty_map_pool,
+ )
+
+ MatchConfig.objects.create(
+ name=configs[2],
+ game_mode=GameMode.COMPETITIVE,
+ type=MatchType.BO3,
+ clinch_series=False,
+ max_players=10,
+ shuffle_teams=True,
+ map_pool=active_duty_map_pool,
+ )
+
+ MatchConfig.objects.create(
+ name=configs[3],
+ game_mode=GameMode.COMPETITIVE,
+ type=MatchType.BO3,
+ clinch_series=False,
+ max_players=10,
+ map_pool=active_duty_map_pool,
+ )
+
+ MatchConfig.objects.create(
+ name=configs[4],
+ game_mode=GameMode.WINGMAN,
+ type=MatchType.BO1,
+ clinch_series=False,
+ max_players=4,
+ map_pool=wingman_map_pool,
+ )
+
+ MatchConfig.objects.create(
+ name=configs[5],
+ game_mode=GameMode.WINGMAN,
+ type=MatchType.BO3,
+ clinch_series=False,
+ max_players=4,
+ map_pool=wingman_map_pool,
+ )
@pytest.fixture
-def match_data(players, default_author, guild):
+def match_data(default_author, guild):
return {
- "discord_users_ids": [player.discord_user.user_id for player in players],
+ "config_name": "5v5_bo1_official",
"author_id": default_author.player.discord_user.user_id,
- "guild_id": guild.id,
- "match_type": MatchType.BO1,
- "players_per_team": 5,
- "clinch_series": False,
- "map_sides": ["knife", "knife", "knife"],
+ "guild_id": guild.guild_id,
}
@pytest.fixture
-def match(teams_with_players, players, default_author, guild):
+def match(default_author, guild):
factory = RequestFactory()
# Create a request
request = factory.get('/')
- team1, team2 = teams_with_players
- new_match: Match = Match.objects.create_match(
- team1=team1,
- team2=team2,
+ new_match = Match.objects.create(
+ config=MatchConfig.objects.get(name="5v5_bo1_official"),
author=default_author.player.discord_user,
- map_sides=["knife", "knife", "knife"],
guild=guild,
)
new_match.create_webhook_cvars(str(reverse_lazy("match-webhook", args=[new_match.pk], request=request)))
return new_match
@pytest.fixture
-def match_with_server(server, teams_with_players, default_author, guild):
- team1, team2 = teams_with_players
+def match_with_server(server, default_author, guild):
factory = RequestFactory()
# Create a request
request2 = factory.get('/')
- new_match = Match.objects.create_match(
- team1=team1,
- team2=team2,
+ new_match = Match.objects.create(
+ config=MatchConfig.objects.get(name="5v5_bo1_official"),
author=default_author.player.discord_user,
- map_sides=["knife", "knife", "knife"],
- server=server,
guild=guild,
+ server=server
)
new_match.create_webhook_cvars(str(reverse_lazy("match-webhook", args=[new_match.pk], request=request2)))
return new_match
\ No newline at end of file
diff --git a/src/matches/tests/test_matches_models.py b/src/matches/tests/test_matches_models.py
index 479f44d..10342c1 100644
--- a/src/matches/tests/test_matches_models.py
+++ b/src/matches/tests/test_matches_models.py
@@ -4,73 +4,134 @@
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse_lazy
-from matches.models import Match, MatchStatus, MatchType
+from matches.models import Match, MatchStatus, MatchType, MatchConfig, GameMode
+from matches.tests.conftest import configs
from servers.tests.conftest import server
from players.tests.conftest import teams_with_players, default_author
@pytest.mark.django_db
@pytest.mark.parametrize("with_server", [True, False])
-@pytest.mark.parametrize("match_type", [MatchType.BO1, MatchType.BO3])
-@pytest.mark.parametrize("clinch_series", [True, False])
-def test_match_model(teams_with_players, default_author, with_server, match_type, server, clinch_series):
- team1, team2 = teams_with_players
+@pytest.mark.parametrize("config", configs)
+def test_match_model(with_server, server, config, guild, default_author):
server = server if with_server else None
factory = RequestFactory()
# Create a request
request = factory.get('/')
- new_match: Match = Match.objects.create_match(
- team1=team1,
- team2=team2,
+ match_config = MatchConfig.objects.get(name=config)
+ new_match: Match = Match.objects.create(
+ config=match_config,
author=default_author.player.discord_user,
- type=match_type,
- clinch_series=clinch_series,
- map_sides=["knife", "knife", "knife"],
- server=server,
+ guild=guild,
+ server=server
)
new_match.create_webhook_cvars(str(reverse_lazy("match-webhook", args=[new_match.pk], request=request)))
assert new_match.status == MatchStatus.CREATED
- assert new_match.type == match_type
- assert new_match.team1 == team1
- assert new_match.team2 == team2
+
+ assert new_match.config.name == match_config.name
+ assert new_match.config.game_mode == match_config.game_mode
+ assert new_match.config.type == match_config.type
+ assert new_match.config.clinch_series == match_config.clinch_series
+ assert new_match.config.max_players == match_config.max_players
+ assert new_match.config.map_pool == match_config.map_pool
+ assert new_match.config.map_sides == match_config.map_sides
+
+ assert new_match.team1 is not None
+ assert new_match.team2 is not None
+ assert new_match.team1.name == "Team 1"
+ assert new_match.team2.name == "Team 2"
+ assert new_match.winner_team is None
+ assert new_match.map_bans.count() == 0
+ assert new_match.map_picks.count() == 0
+ assert new_match.last_map_ban is None
+ assert new_match.last_map_pick is None
+ assert new_match.maplist is None
+ assert new_match.server == server
+ assert new_match.guild == guild
assert new_match.author == default_author.player.discord_user
- assert new_match.players_per_team == 5
- assert new_match.clinch_series is clinch_series
- assert new_match.map_sides == ["knife", "knife", "knife"]
- assert new_match.maplist == new_match.maplist
+ assert new_match.message_id is None
+ assert new_match.embed is not None
team1_players_dict = new_match.get_team1_players_dict()
team2_players_dict = new_match.get_team2_players_dict()
- assert team1_players_dict["name"] == team1.name
- assert team2_players_dict["name"] == team2.name
+ assert team1_players_dict["name"] == "Team 1"
+ assert team2_players_dict["name"] == "Team 2"
- assert len(team1_players_dict["players"]) == 5
- assert len(team2_players_dict["players"]) == 5
+ assert len(team1_players_dict["players"]) == 1
+ assert len(team2_players_dict["players"]) == 0
- match_config = new_match.get_config()
-
- assert match_config["matchid"] == new_match.pk
- assert match_config["team1"] == team1_players_dict
- assert match_config["team2"] == team2_players_dict
- assert match_config["num_maps"] == 1 if match_type == MatchType.BO1 else 3
- assert match_config["maplist"] == new_match.maplist
- assert match_config["map_sides"] == ["knife", "knife", "knife"]
- assert match_config["clinch_series"] is clinch_series
- assert match_config["players_per_team"] == 5
- assert match_config["cvars"] == new_match.cvars
- assert match_config["cvars"]["matchzy_remote_log_url"] == reverse_lazy("match-webhook", args=[new_match.pk], request=request)
- assert match_config["cvars"]["matchzy_remote_log_header_key"] == new_match.api_key_header
- assert match_config["cvars"]["matchzy_remote_log_header_value"] == f"Bearer {new_match.get_author_token()}"
+ assert new_match.api_key_header == "Authorization"
+ assert new_match.load_match_command_name == "matchzy_loadmatch_url"
+ assert new_match.get_author_token() == Token.objects.get(user=default_author).key
+ assert new_match.get_connect_command() == "" if with_server is False else server.get_connect_string()
- connect_command = new_match.get_connect_command()
- assert connect_command == "" if with_server is False else server.get_connect_string()
+ matchzy_config = new_match.get_matchzy_config()
+ assert matchzy_config["matchid"] == new_match.pk
+ assert matchzy_config["team1"] == team1_players_dict
+ assert matchzy_config["team2"] == team2_players_dict
+ assert matchzy_config["num_maps"] == 1 if match_config.type == MatchType.BO1 else 3
+ assert matchzy_config["maplist"] == new_match.maplist
+ assert matchzy_config["map_sides"] == match_config.map_sides
+ assert matchzy_config["clinch_series"] == match_config.clinch_series
+ assert matchzy_config["players_per_team"] == 1
+ assert matchzy_config["cvars"] == new_match.cvars
+ assert matchzy_config["cvars"]["matchzy_remote_log_url"] == reverse_lazy("match-webhook", args=[new_match.pk], request=request)
+ assert matchzy_config["cvars"]["matchzy_remote_log_header_key"] == new_match.api_key_header
+ assert matchzy_config["cvars"]["matchzy_remote_log_header_value"] == f"Bearer {new_match.get_author_token()}"
- author_token = new_match.get_author_token()
- assert author_token == Token.objects.get(user=default_author).key
+ if match_config.game_mode == GameMode.WINGMAN:
+ assert matchzy_config["wingman"] is True
- assert new_match.load_match_command_name == "matchzy_loadmatch_url"
assert new_match.api_key_header == "Authorization"
+ assert new_match.load_match_command_name == "matchzy_loadmatch_url"
+@pytest.mark.django_db
+@pytest.mark.parametrize("config", configs)
+def test_match_get_team1_players_dict(match, config):
+ match.config = MatchConfig.objects.get(name=config)
+ match.save()
+ team1_dict = match.get_team1_players_dict()
+ assert team1_dict["name"] == match.team1.name
+ assert len(team1_dict["players"]) == match.team1.players.count()
+ assert len(team1_dict["players"]) == 1
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("config", configs)
+def test_match_get_team2_players_dict(match, config):
+ match.config = MatchConfig.objects.get(name=config)
+ match.save()
+ team2_dict = match.get_team2_players_dict()
+ assert team2_dict["name"] == match.team2.name
+ assert len(team2_dict["players"]) == match.team2.players.count()
+ assert len(team2_dict["players"]) == 0
+@pytest.mark.django_db
+@pytest.mark.parametrize("config", configs)
+def test_match_get_matchzy_config(rf, match, config):
+ request = rf.get("/")
+ match.config = MatchConfig.objects.get(name=config)
+ match.save()
+ matchzy_config = match.get_matchzy_config()
+ assert matchzy_config["matchid"] == match.pk
+ assert matchzy_config["team1"] == match.get_team1_players_dict()
+ assert matchzy_config["team2"] == match.get_team2_players_dict()
+ assert matchzy_config["num_maps"] == 1 if match.config.type == MatchType.BO1 else 3
+ assert matchzy_config["maplist"] == match.maplist
+ assert matchzy_config["map_sides"] == match.config.map_sides
+ assert matchzy_config["clinch_series"] == match.config.clinch_series
+ assert matchzy_config["players_per_team"] == 1
+ assert matchzy_config["cvars"] == match.cvars
+ assert matchzy_config["cvars"]["matchzy_remote_log_url"] == reverse_lazy("match-webhook", args=[match.pk], request=request)
+ assert matchzy_config["cvars"]["matchzy_remote_log_header_key"] == match.api_key_header
+ assert matchzy_config["cvars"]["matchzy_remote_log_header_value"] == f"Bearer {match.get_author_token()}"
+
+ if match.config.game_mode == GameMode.WINGMAN:
+ assert matchzy_config["wingman"] is True
+@pytest.mark.django_db
+@pytest.mark.parametrize("config", configs)
+def test_get_connect_command(match, server, config):
+ match.server = server
+ match.save()
+ assert match.get_connect_command() == server.get_connect_string()
diff --git a/src/matches/utils.py b/src/matches/utils.py
index bfd27c3..cac4999 100644
--- a/src/matches/utils.py
+++ b/src/matches/utils.py
@@ -8,6 +8,7 @@
import redis
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse_lazy
+from typing_extensions import deprecated
from guilds.models import Guild
from matches.models import (
@@ -16,7 +17,7 @@
MapBan,
MapPick,
MatchStatus,
- MatchType,
+ MatchType, MatchConfig,
)
from matches.serializers import (
CreateMatchSerializer,
@@ -30,7 +31,7 @@
MatchPickMapSerializer,
MatchPlayerJoin,
MatchSerializer, MatchBanMapResultSerializer, MatchPickMapResultSerializer, InteractionUserSerializer,
- MapSerializer,
+ MapSerializer, MatchPlayerLeave, MatchSelectCaptain,
)
from players.models import DiscordUser, Player, Team
from players.serializers import TeamSerializer
@@ -87,7 +88,7 @@ def check_server_is_available_for_match(server: Server) -> bool:
return True
-def create_match(request: Request) -> Response:
+def create_match_deprecated(request: Request) -> Response:
"""
Create a new match.
@@ -186,6 +187,42 @@ def create_match(request: Request) -> Response:
return Response(new_match_serializer.data, status=201)
+def create_match(request: Request) -> Response:
+ """
+ Create a new match.
+
+ Args:
+ -----
+ request (Request): Request object.
+
+ Returns:
+ --------
+ Response: Response object.
+ """
+ serializer = CreateMatchSerializer(data=request.data, context={"request": request})
+ serializer.is_valid(raise_exception=True)
+ author_id = serializer.validated_data.get("author_id")
+ server_id = serializer.validated_data.get("server_id")
+ guild_id = serializer.validated_data.get("guild_id")
+ config_name = serializer.validated_data.get("config_name")
+ author = DiscordUser.objects.get(user_id=author_id)
+ guild = Guild.objects.get(guild_id=guild_id)
+ config = MatchConfig.objects.get(name=config_name)
+
+ server = None
+ if server_id:
+ server = Server.objects.get(pk=server_id)
+ new_match: Match = Match.objects.create(
+ config=config,
+ author=author,
+ guild=guild,
+ server=server
+ )
+ new_match.create_webhook_cvars(webhook_url=str(reverse_lazy("match-webhook", args=[new_match.pk], request=request)))
+ new_match_serializer = MatchSerializer(new_match, context={"request": request})
+ return Response(new_match_serializer.data, status=201)
+
+
def load_match(pk: int, request) -> Response:
"""
Load a match into the server.
@@ -218,7 +255,7 @@ def load_match(pk: int, request) -> Response:
return Response(match_serializer.data, status=200)
-def ban_map(request: Request, pk: int) -> Response:
+def ban_map_deprecated(request: Request, pk: int) -> Response:
"""
Ban a map from the match.
@@ -306,16 +343,72 @@ def ban_map(request: Request, pk: int) -> Response:
for map_pick in match.map_picks.all():
maps_left_without_picked_map.remove(map_pick.map.tag)
ban_result_serializer = MatchBanMapResultSerializer(
- context={"banned_map": map, "next_ban_team": match.team1 if match.team2 == user_team else match.team2,},
+ context={"banned_map": map, "next_ban_team": match.team1 if match.team2 == user_team else match.team2, },
data={
"maps_left": maps_left_without_picked_map,
"map_bans_count": match.map_bans.count(),
}
- )
+ )
ban_result_serializer.is_valid(raise_exception=True)
return Response(ban_result_serializer.data, status=200)
+def ban_map(request: Request, pk: int) -> Response["MatchBanMapResultSerializer"]:
+ """
+ Ban a map from the match.
+
+ Args:
+ -----
+ request (Request): Request object.
+ pk (int): Match ID.
+
+ Returns:
+ --------
+ Response: Response object.
+ """
+ match: Match = get_object_or_404(Match, pk=pk)
+ match_map_ban_serializer = MatchBanMapSerializer(data=request.data, context={"match": match})
+ match_map_ban_serializer.is_valid(raise_exception=True)
+ interaction_user_id = match_map_ban_serializer.validated_data.get("interaction_user_id")
+ map_tag = match_map_ban_serializer.validated_data.get("map_tag")
+
+ player = Player.objects.get(discord_user__user_id=interaction_user_id)
+ map = Map.objects.get(tag=map_tag)
+ team = match.get_team_by_player(player)
+ if match.last_map_ban and match.last_map_ban.team == team:
+ return Response(
+ {"message": "You already banned a map. Wait for the other team to ban a map"},
+ status=400,
+ )
+ if match.map_bans.count() == 0 and team == match.team2:
+ return Response(
+ {"message": "Team 1 has to ban first"},
+ status=400,
+ )
+
+ if match.config.type == MatchType.BO1:
+ # 6 bans
+ map_bans_count = match.map_bans.count()
+ # 7 maps
+ map_pool_count = match.config.map_pool.maps.count() - 1
+ if map_bans_count == map_pool_count:
+ return Response({"message": "Only one map left. You can't ban more maps"}, status=400)
+
+ match.ban_map(team, map)
+ maps_left = match.get_maps_left()
+ next_ban_team = match.get_next_ban_team()
+ map_bans_count = match.map_bans.count()
+ map_ban_result_serializer = MatchBanMapResultSerializer(
+ context={"banned_map": map, "next_ban_team": next_ban_team, "match": match, "request": request},
+ data={
+ "maps_left": maps_left,
+ "map_bans_count": map_bans_count,
+ }
+ )
+ map_ban_result_serializer.is_valid(raise_exception=True)
+ return Response(map_ban_result_serializer.data, status=200)
+
+
def pick_map(request: Request, pk: int) -> Response["MatchPickMapResultSerializer"]:
"""
Pick a map for the match.
@@ -404,7 +497,7 @@ def pick_map(request: Request, pk: int) -> Response["MatchPickMapResultSerialize
maps_left_without_picked_map.remove(map_pick.map.tag)
map_pick_result_serializer = MatchPickMapResultSerializer(
- context={"picked_map": map, "next_pick_team": match.team1 if match.team2 == user_team else match.team2,},
+ context={"picked_map": map, "next_pick_team": match.team1 if match.team2 == user_team else match.team2, },
data={
"maps_left": maps_left_without_picked_map,
"map_picks_count": match.map_picks.count(),
@@ -531,9 +624,44 @@ def process_webhook(request: Request, pk) -> Response:
return Response({"event": redis_event, "data": data}, status=200)
+def recreate_match(request, pk: int) -> Response:
+ """
+ Create a new match with the same teams.
+
+ Args:
+ -----
+ pk (int): Match ID.
+
+ Returns:
+ --------
+ Response: Response object.
+ """
+ match: Match = get_object_or_404(Match, pk=pk)
+ serializer = InteractionUserSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ interaction_user_id = serializer.validated_data.get("interaction_user_id")
+ author = get_object_or_404(DiscordUser, user_id=interaction_user_id)
+ if author.pk != match.author.pk:
+ return Response(
+ {"message": "Only the author of the match can recreate the match"},
+ status=400,
+ )
+ new_match = Match.objects.create_match(
+ type=match.type,
+ team1=match.team1,
+ team2=match.team2,
+ author=match.author,
+ guild=match.guild,
+ server=match.server,
+ )
+ new_match.create_webhook_cvars(str(reverse_lazy("match-webhook", args=[new_match.pk], request=request)))
+ new_match_serializer = MatchSerializer(new_match, context={"request": request})
+ return Response(new_match_serializer.data, status=201)
+
+
def join_match(request: Request, pk: int) -> Response:
"""
- Join a match.
+ Join a player to a team.
Args:
-----
@@ -545,37 +673,38 @@ def join_match(request: Request, pk: int) -> Response:
Response: Response object.
"""
match: Match = get_object_or_404(Match, pk=pk)
- match_player_join_serializer = MatchPlayerJoin(data=request.data)
- if not match_player_join_serializer.is_valid():
- return Response(match_player_join_serializer.errors, status=400)
+ serializer = MatchPlayerJoin(data=request.data, context={"match": match})
+ serializer.is_valid(raise_exception=True)
+ interaction_user_id = serializer.validated_data.get("interaction_user_id")
+ team = serializer.validated_data.get("team")
- discord_user_id = match_player_join_serializer.validated_data.get("interaction_user_id")
- player = get_object_or_404(Player, discord_user__user_id=discord_user_id)
- if match.team1.players.filter(pk=player.id).exists():
+ discord_user = DiscordUser.objects.get(user_id=interaction_user_id)
+
+ player = Player.objects.get(discord_user=discord_user)
+ if team == "team1" and match.team1.players.filter(discord_user=discord_user).exists():
return Response(
- {"message": f"Player {player.steam_user.username} is already in team 1"},
+ {"message": f"DiscordUser @<{interaction_user_id}> is already in team1"},
status=400,
)
- if match.team2.players.filter(pk=player.id).exists():
+ if team == "team2" and match.team2.players.filter(discord_user=discord_user).exists():
return Response(
- {"message": f"Player {player.steam_user.username} is already in team 2"},
+ {"message": f"DiscordUser @<{interaction_user_id}> is already in team2"},
status=400,
)
- if match.team1.players.count() < match.team2.players.count():
- match.team1.players.add(player)
- else:
- match.team2.players.add(player)
- match.save()
+ match.add_player_to_match(player, team)
+ if match.team1.players.count() + match.team2.players.count() == match.config.max_players:
+ match.start_match()
match_serializer = MatchSerializer(match, context={"request": request})
return Response(match_serializer.data, status=200)
-def recreate_match(request, pk: int) -> Response:
+def leave_match(request: Request, pk: int) -> Response:
"""
- Create a new match with the same teams.
+ Leave a match.
Args:
-----
+ request (Request): Request object.
pk (int): Match ID.
Returns:
@@ -583,23 +712,59 @@ def recreate_match(request, pk: int) -> Response:
Response: Response object.
"""
match: Match = get_object_or_404(Match, pk=pk)
- serializer = InteractionUserSerializer(data=request.data)
+ serializer = MatchPlayerLeave(data=request.data, context={"match": match})
serializer.is_valid(raise_exception=True)
interaction_user_id = serializer.validated_data.get("interaction_user_id")
- author = get_object_or_404(DiscordUser, user_id=interaction_user_id)
- if author.pk != match.author.pk:
- return Response(
- {"message": "Only the author of the match can recreate the match"},
- status=400,
- )
- new_match = Match.objects.create_match(
- type=match.type,
- team1=match.team1,
- team2=match.team2,
- author=match.author,
- guild=match.guild,
- server=match.server,
- )
- new_match.create_webhook_cvars(str(reverse_lazy("match-webhook", args=[new_match.pk], request=request)))
- new_match_serializer = MatchSerializer(new_match, context={"request": request})
- return Response(new_match_serializer.data, status=201)
+ discord_user = DiscordUser.objects.get(user_id=interaction_user_id)
+ player = Player.objects.get(discord_user=discord_user)
+ match.remove_player_from_match(player)
+ match_serializer = MatchSerializer(match, context={"request": request})
+ return Response(match_serializer.data, status=200)
+
+
+def select_captain(request: Request, pk: int) -> Response:
+ """
+ Select a capitan for a team.
+
+ Args:
+ -----
+ request (Request): Request object.
+ pk (int): Match ID.
+
+ Returns:
+ --------
+ Response: Response object.
+ """
+ match: Match = get_object_or_404(Match, pk=pk)
+
+ serializer = MatchSelectCaptain(data=request.data, context={"match": match})
+ serializer.is_valid(raise_exception=True)
+
+ interaction_user_id = serializer.validated_data.get("interaction_user_id")
+ team = serializer.validated_data.get("team")
+ player = Player.objects.get(discord_user__user_id=interaction_user_id)
+ if team == "team1":
+ if not match.team1.players.filter(discord_user__user_id=interaction_user_id).exists():
+ return Response(
+ {"message": f"DiscordUser @<{interaction_user_id}> is not in team1"},
+ status=400,
+ )
+ if match.team1.leader:
+ return Response(
+ {"message": f"Team1 already has a captain"},
+ status=400,
+ )
+ if team == "team2":
+ if not match.team2.players.filter(discord_user__user_id=interaction_user_id).exists():
+ return Response(
+ {"message": f"DiscordUser @<{interaction_user_id}> is not in team2"},
+ status=400,
+ )
+ if match.team2.leader:
+ return Response(
+ {"message": f"Team2 already has a captain"},
+ status=400,
+ )
+ match.set_team_captain(player, team)
+ match_serializer = MatchSerializer(match, context={"request": request})
+ return Response(match_serializer.data, status=200)
diff --git a/src/matches/validators.py b/src/matches/validators.py
new file mode 100644
index 0000000..dde378c
--- /dev/null
+++ b/src/matches/validators.py
@@ -0,0 +1,105 @@
+from rest_framework import serializers
+
+from matches.models import MatchStatus, Map, MatchType
+from players.models import DiscordUser
+
+
+class ValidDiscordUser(object):
+
+ def __call__(self, value):
+ if not DiscordUser.objects.filter(user_id=value).exists():
+ raise serializers.ValidationError(f"DiscordUser @<{value}> does not exist")
+
+
+class DiscordUserIsInMatch(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if not match.team1.players.filter(discord_user__user_id=value).exists() and not match.team2.players.filter(
+ discord_user__user_id=value).exists():
+ raise serializers.ValidationError(f"DiscordUser @<{value}> is not in a match")
+
+
+class DiscordUserCanJoinMatch(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if match.status != MatchStatus.CREATED:
+ raise serializers.ValidationError("Match is not in CREATED status")
+
+ # Allow author to join match
+ if match.author.user_id == value:
+ return
+ # if match.team1.players.filter(discord_user__user_id=value).exists() or match.team2.players.filter(
+ # discord_user__user_id=value).exists():
+ # raise serializers.ValidationError(f"DiscordUser @<{value}> is already in a match")
+
+ if (match.team1.players.count() + match.team2.players.count()) >= match.config.max_players:
+ raise serializers.ValidationError("Match is full")
+
+ if match.author.user_id == value and match.config.shuffle_teams:
+ raise serializers.ValidationError(f"DiscordUser @<{value}> is author of the match and cannot join")
+
+
+class TeamCanBeJoined(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+
+ if value == "team1" and match.team1.players.count() >= match.config.max_players // 2:
+ raise serializers.ValidationError("Team1 is full")
+ if value == "team2" and match.team2.players.count() >= match.config.max_players // 2:
+ raise serializers.ValidationError("Team2 is full")
+
+
+class DiscordUserCanLeaveMatch(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if match.status != MatchStatus.CREATED:
+ raise serializers.ValidationError("Match is not in CREATED status")
+
+ if match.author.user_id == value:
+ raise serializers.ValidationError(f"DiscordUser @<{value}> is author of the match and cannot leave")
+
+
+class ValidMap(object):
+ requires_context = True
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if not Map.objects.filter(tag=value).exists():
+ raise serializers.ValidationError(f"Map {value} does not exist")
+ if not match.config.map_pool.maps.filter(tag=value).exists():
+ raise serializers.ValidationError(f"Map {value} is not in the map pool")
+
+
+class MapCanBeBanned(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if match.status != MatchStatus.MAP_VETO:
+ raise serializers.ValidationError("Match is not in MAP_VETO status")
+
+ if match.map_bans.filter(map__tag=value).exists():
+ raise serializers.ValidationError(f"Map {value} is already banned")
+
+ if match.map_picks.filter(map__tag=value).exists():
+ raise serializers.ValidationError(f"Map {value} is already picked")
+
+
+
+
+class DiscordUserIsCaptain(object):
+ requires_context = True
+
+ def __call__(self, value, serializer_field):
+ match = serializer_field.context.get("match")
+ if match.team1.leader.user_id != value and match.team2.leader.user_id != value:
+ raise serializers.ValidationError(f"DiscordUser @<{value}> is not a captain")
+
+
diff --git a/src/matches/views.py b/src/matches/views.py
index 306c9c1..670b708 100644
--- a/src/matches/views.py
+++ b/src/matches/views.py
@@ -18,7 +18,8 @@
MatchConfigSerializer,
MatchMapSelectedSerializer,
MatchSerializer, CreateMatchSerializer, MatchBanMapSerializer, MatchPickMapSerializer, MatchBanMapResultSerializer,
- MatchPickMapResultSerializer, InteractionUserSerializer, MatchUpdateSerializer,
+ MatchPickMapResultSerializer, InteractionUserSerializer, MatchUpdateSerializer, MatchPlayerJoin, MatchPlayerLeave,
+ MatchSelectCaptain,
)
from matches.utils import (
ban_map,
@@ -28,7 +29,7 @@
pick_map,
process_webhook,
recreate_match,
- shuffle_teams,
+ shuffle_teams, leave_match, select_captain,
)
from players.models import Team, DiscordUser
from servers.models import Server
@@ -156,13 +157,30 @@ def picks(self, request, pk):
return Response(serializer.data)
@extend_schema(
- request=InteractionUserSerializer,
+ request=MatchPlayerJoin,
responses={200: MatchSerializer}
)
@action(detail=True, methods=["POST"])
def join(self, request, pk):
return join_match(request, pk)
+ @extend_schema(
+ request=MatchPlayerLeave,
+ responses={200: MatchSerializer}
+ )
+ @action(detail=True, methods=["POST"])
+ def leave(self, request, pk):
+ return leave_match(request, pk)
+
+
+ @extend_schema(
+ request=MatchSelectCaptain,
+ responses={200: MatchSerializer}
+ )
+ @action(detail=True, methods=["POST"])
+ def captain(self, request, pk):
+ return select_captain(request, pk)
+
@extend_schema(
responses={200: MatchConfigSerializer}
)
diff --git a/src/players/migrations/0006_alter_team_leader.py b/src/players/migrations/0006_alter_team_leader.py
new file mode 100644
index 0000000..b616c37
--- /dev/null
+++ b/src/players/migrations/0006_alter_team_leader.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.6 on 2024-05-10 15:15
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('players', '0005_alter_player_steam_user'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='team',
+ name='leader',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='team_leader', to='players.player'),
+ ),
+ ]
diff --git a/src/players/models.py b/src/players/models.py
index 78930ee..d84e196 100644
--- a/src/players/models.py
+++ b/src/players/models.py
@@ -50,6 +50,7 @@ class Team(models.Model):
on_delete=models.CASCADE,
related_name="team_leader",
null=True,
+ blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
diff --git a/src/servers/admin.py b/src/servers/admin.py
index 6cfd0da..02edaf8 100644
--- a/src/servers/admin.py
+++ b/src/servers/admin.py
@@ -4,4 +4,10 @@
# Register your models here.
-admin.site.register(Server)
\ No newline at end of file
+class AdminServer(admin.ModelAdmin):
+ readonly_fields = ('id', 'created_at', 'updated_at')
+
+
+
+
+admin.site.register(Server, AdminServer)
\ No newline at end of file