diff --git a/game/api/__init__.py b/game/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/api/v1/__init__.py b/game/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/api/v1/serializers.py b/game/api/v1/serializers.py new file mode 100644 index 0000000..a54c81e --- /dev/null +++ b/game/api/v1/serializers.py @@ -0,0 +1,94 @@ +from abc import ABC + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from game.models import Hero, Player, HeroInDeck, Deck + + +class CreateHeroSerializer(serializers.ModelSerializer): + class Meta: + model = Hero + fields = ( + "type", + "idle_img", + "attack_img", + "die_img", + "health", + "attack", + "speed", + ) + + +class GetHeroSerializer(serializers.ModelSerializer): + class Meta: + model = Hero + fields = ( + "added", + "type", + "idle_img", + "attack_img", + "die_img", + "health", + "attack", + "speed", + ) + + +class ListHeroSerializer(serializers.ModelSerializer): + class Meta: + model = Hero + fields = ( + "uuid", + "type", + "idle_img", + "attack_img", + "die_img", + "health", + "attack", + "speed", + ) + + +class CreatePlayerSerializer(serializers.ModelSerializer): + class Meta: + model = Player + fields = ("ton_wallet", "name") + + +class DeckCreateSerializer(serializers.ModelSerializer): + hero_ids = serializers.ListSerializer( + child=serializers.UUIDField(), min_length=16, max_length=16 + ) + + class Meta: + model = Deck + fields = ("hero_ids",) + + def validate_hero_ids(self, value): + for x in value: + if not (hero := Hero.objects.filter(uuid=x)): + raise ValidationError(f"Hero with uuid {x} doesn't exist") + + if deck := HeroInDeck.objects.filter(hero=hero.first()): + raise ValidationError( + f"Hero with uuid {x} is already in deck with id {deck.first().deck.id}" + ) + return value + + def create(self, validated_data): + deck = Deck.objects.create(player=self.context["request"].user) + for x in validated_data["hero_ids"]: + HeroInDeck.objects.create(hero_id=x, deck=deck) + return deck + + +class GetDeckSerializer(serializers.ModelSerializer): + heroes = ListHeroSerializer(many=True) + + class Meta: + model = Deck + fields = ("player_uuid", "heroes") + + def get_heroes(self, val): + print(val) diff --git a/game/views.py b/game/api/v1/views.py similarity index 56% rename from game/views.py rename to game/api/v1/views.py index 826b844..e684555 100644 --- a/game/views.py +++ b/game/api/v1/views.py @@ -1,16 +1,29 @@ from rest_framework import status from rest_framework.generics import GenericAPIView, UpdateAPIView -from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, ListModelMixin +from rest_framework.mixins import ( + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + DestroyModelMixin, + UpdateModelMixin, +) from rest_framework.response import Response from game.authentication import PlayerAuthentication from game.models import Hero -from game.serializers import CreateHeroSerializer, GetHeroSerializer, CreatePlayerView +from game.api.v1.serializers import ( + CreateHeroSerializer, + GetHeroSerializer, + CreatePlayerSerializer, + ListHeroSerializer, + DeckCreateSerializer, + GetDeckSerializer, +) from game.services.jwt import sign_jwt -class CreateHeroView(GenericAPIView, CreateModelMixin, ListModelMixin): +class ListCreateHeroView(GenericAPIView, CreateModelMixin, ListModelMixin): authentication_classes = (PlayerAuthentication,) def perform_create(self, serializer): @@ -27,7 +40,7 @@ class CreateHeroView(GenericAPIView, CreateModelMixin, ListModelMixin): def get_serializer_class(self): if self.request.method == "GET": - return GetHeroSerializer + return ListHeroSerializer else: return CreateHeroSerializer @@ -52,7 +65,7 @@ class RetrieveHeroView(RetrieveModelMixin, UpdateAPIView, GenericAPIView): class PlayerCreateView(GenericAPIView, CreateModelMixin): - serializer_class = CreatePlayerView + serializer_class = CreatePlayerSerializer def perform_create(self, serializer): return serializer.save() @@ -65,4 +78,35 @@ class PlayerCreateView(GenericAPIView, CreateModelMixin): # TODO: add JTI to refresh token access_jwt = sign_jwt({"id": instance.id, "type": "access"}, t_life=3600) refresh_jwt = sign_jwt({"id": instance.id, "type": "refresh"}) - return Response({"access_token": access_jwt, "refresh_token": refresh_jwt}, status=status.HTTP_201_CREATED) + return Response( + {"access_token": access_jwt, "refresh_token": refresh_jwt}, + status=status.HTTP_201_CREATED, + ) + + +class DeckCreateView(GenericAPIView, CreateModelMixin): + serializer_class = DeckCreateSerializer + authentication_classes = (PlayerAuthentication,) + + def perform_create(self, serializer): + return serializer.save(player=self.request.user) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + heroes_list = ListHeroSerializer(instance.get_heroes(), many=True) + return Response(heroes_list.data, status=status.HTTP_201_CREATED) + + +class RetireUpdateDeleteDeckView( + RetrieveHeroView, DestroyModelMixin, UpdateModelMixin, GenericAPIView +): + lookup_field = "pk" + + def get_serializer_class(self): + if self.request.method == "GET": + return GetDeckSerializer + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) diff --git a/game/authentication.py b/game/authentication.py index 2ddf9ec..026177f 100644 --- a/game/authentication.py +++ b/game/authentication.py @@ -7,7 +7,9 @@ from .services.jwt import read_jwt class PlayerAuthentication(authentication.BaseAuthentication): def authenticate(self, request): - if "Authorization" not in request.headers or not (token := request.headers["Authorization"]): + if "Authorization" not in request.headers or not ( + token := request.headers["Authorization"] + ): raise exceptions.AuthenticationFailed("No credentials provided.") t = read_jwt(token) diff --git a/game/migrations/0001_initial.py b/game/migrations/0001_initial.py new file mode 100644 index 0000000..af633c5 --- /dev/null +++ b/game/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.0.5 on 2022-06-04 14:16 + +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Hero', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('added', models.DateTimeField(auto_now_add=True)), + ('type', models.CharField(choices=[('WIZARD', 'wizard'), ('ARCHER', 'archer'), ('WARRIOR', 'warrior')], max_length=7)), + ('idle_img', models.ImageField(upload_to='uploads/idle')), + ('attack_img', models.ImageField(upload_to='uploads/attack')), + ('die_img', models.ImageField(upload_to='uploads/die')), + ('health', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])), + ('speed', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])), + ], + options={ + 'verbose_name': 'hero', + 'verbose_name_plural': 'heroes', + 'ordering': ['-added'], + }, + ), + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ton_wallet', models.CharField(max_length=50, verbose_name='TON wallet')), + ('name', models.CharField(blank=True, max_length=100)), + ('added', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'player', + 'verbose_name_plural': 'players', + 'ordering': ['-added'], + }, + ), + migrations.AddIndex( + model_name='player', + index=models.Index(fields=['ton_wallet'], name='game_player_ton_wal_47dd93_idx'), + ), + migrations.AddIndex( + model_name='hero', + index=models.Index(fields=['uuid'], name='game_hero_uuid_ada5d9_idx'), + ), + ] diff --git a/game/models.py b/game/models.py index 3abe6b2..3d30375 100644 --- a/game/models.py +++ b/game/models.py @@ -41,7 +41,10 @@ class Player(models.Model): hero.player = self hero.type = t - with open("/home/sanspie/Projects/chess_rpg_backend/media/dummy.jpg", "rb+") as file: + # TODO: create image pool to generate heroes (awaiting for designer) + with open( + "/home/sanspie/Projects/chess_rpg_backend/media/dummy.jpg", "rb+" + ) as file: hero.idle_img = File(file, name="dummy.jpg") hero.attack_img = File(file, name="dummy.jpg") hero.die_img = File(file, name="dummy.jpg") @@ -58,6 +61,8 @@ class Player(models.Model): class Meta: indexes = [models.Index(fields=["ton_wallet"])] ordering = ["-created"] + + db_table = "player" verbose_name = "player" verbose_name_plural = "players" @@ -65,7 +70,9 @@ class Player(models.Model): class Hero(models.Model): """Model to store heroes and their stats, connected to player""" - uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + uuid = models.UUIDField( + default=uuid.uuid4, editable=False, unique=True, primary_key=True + ) player = models.ForeignKey( Player, on_delete=models.CASCADE, @@ -94,5 +101,47 @@ class Hero(models.Model): class Meta: indexes = [models.Index(fields=["uuid"])] ordering = ["-added"] + + db_table = "hero" verbose_name = "hero" verbose_name_plural = "heroes" + + +class Deck(models.Model): + player = models.ForeignKey( + Player, + on_delete=models.CASCADE, + related_name="decks", + related_query_name="deck", + ) + + def __str__(self): + return f"{self.player.name} deck" + + def get_heroes(self): + return [x.hero for x in HeroInDeck.objects.filter(deck=self)] + + class Meta: + db_table = "deck" + verbose_name = "deck" + verbose_name_plural = "decks" + + +class HeroInDeck(models.Model): + deck = models.ForeignKey( + Deck, + on_delete=models.CASCADE, + related_name="hero_in_deck", + related_query_name="heroes", + ) + hero = models.OneToOneField( + Hero, + on_delete=models.CASCADE, + related_name="hero_in_deck", + related_query_name="decks", + ) + + class Meta: + db_table = "hero_in_deck" + verbose_name = "Hero in deck" + verbose_name_plural = "Heroes in decks" diff --git a/game/serializers.py b/game/serializers.py deleted file mode 100644 index 1f629e4..0000000 --- a/game/serializers.py +++ /dev/null @@ -1,38 +0,0 @@ -from rest_framework import serializers - -from game.models import Hero, Player - - -class CreateHeroSerializer(serializers.ModelSerializer): - class Meta: - model = Hero - fields = ( - "type", - "idle_img", - "attack_img", - "die_img", - "health", - "attack", - "speed", - ) - - -class GetHeroSerializer(serializers.ModelSerializer): - class Meta: - model = Hero - fields = ( - "added", - "type", - "idle_img", - "attack_img", - "die_img", - "health", - "attack", - "speed", - ) - - -class CreatePlayerView(serializers.ModelSerializer): - class Meta: - model = Player - fields = ("ton_wallet", "name") diff --git a/game/urls.py b/game/urls.py index f868f61..8185009 100644 --- a/game/urls.py +++ b/game/urls.py @@ -1,9 +1,19 @@ from django.urls import path -from game.views import CreateHeroView, RetrieveHeroView, PlayerCreateView +from game.api.v1.views import ( + ListCreateHeroView, + RetrieveHeroView, + PlayerCreateView, + DeckCreateView, + RetireUpdateDeleteDeckView, +) urlpatterns = [ - path("hero/", CreateHeroView.as_view(), name="hero_api_create"), - path("hero/", RetrieveHeroView.as_view(), name="hero_api_retrieve"), - path("player/", PlayerCreateView.as_view(), name="player_create_api"), + path("v1/hero/", ListCreateHeroView.as_view(), name="hero_api_create"), + path("v1/hero/", RetrieveHeroView.as_view(), name="hero_api_retrieve"), + path("v1/player/", PlayerCreateView.as_view(), name="player_create_api"), + path("v1/deck/", DeckCreateView.as_view(), name="deck_create_api"), + path( + "v1/deck/", RetireUpdateDeleteDeckView.as_view(), name="deck_retire_api" + ), ]