2025-12-16 22:14:33 +03:00
|
|
|
from datetime import date
|
|
|
|
|
|
2025-12-11 18:15:56 +03:00
|
|
|
from rest_framework import status
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.views import APIView
|
|
|
|
|
|
2025-12-16 22:14:33 +03:00
|
|
|
from api.models import Aspect, BuildOfDay, Hero, Item
|
2025-12-11 18:15:56 +03:00
|
|
|
from api.serializers import (
|
|
|
|
|
HeroSerializer,
|
|
|
|
|
ItemSerializer,
|
|
|
|
|
RandomizeBuildRequestSerializer,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from .data import generate_skill_build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HeroesListView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
GET: Return all available heroes for selection.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
heroes = Hero.objects.all().order_by("name")
|
|
|
|
|
return Response(HeroSerializer(heroes, many=True).data, status=status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RandomizeBuildView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
POST: Generate a random Dota 2 build with hero, items, and optionally skills/aspects.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def get_serializer(self, *args, **kwargs):
|
|
|
|
|
return RandomizeBuildRequestSerializer(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def post(self, request):
|
|
|
|
|
serializer = RandomizeBuildRequestSerializer(data=request.data)
|
|
|
|
|
if not serializer.is_valid():
|
|
|
|
|
return Response(
|
|
|
|
|
{"message": self._format_errors(serializer.errors)},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
validated_data = serializer.validated_data
|
|
|
|
|
include_skills = validated_data["includeSkills"]
|
|
|
|
|
include_aspect = validated_data["includeAspect"]
|
|
|
|
|
items_count = validated_data["itemsCount"]
|
|
|
|
|
hero_id = validated_data.get("heroId")
|
|
|
|
|
|
|
|
|
|
hero_count = Hero.objects.count()
|
|
|
|
|
item_count = Item.objects.count()
|
|
|
|
|
|
|
|
|
|
if hero_count == 0:
|
|
|
|
|
return Response(
|
|
|
|
|
{"message": "No heroes available. Load data first."},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if item_count < items_count:
|
|
|
|
|
return Response(
|
|
|
|
|
{"message": f"Not enough items available. Requested {items_count}, found {item_count}."},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if hero_id:
|
|
|
|
|
hero_obj = Hero.objects.filter(id=hero_id).first()
|
|
|
|
|
if not hero_obj:
|
|
|
|
|
return Response(
|
|
|
|
|
{"message": f"Hero with id {hero_id} not found."},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
hero_obj = Hero.objects.order_by("?").first()
|
|
|
|
|
item_objs = Item.objects.order_by("?")[:items_count]
|
|
|
|
|
|
|
|
|
|
response_data = {
|
|
|
|
|
"hero": HeroSerializer(hero_obj).data,
|
|
|
|
|
"items": ItemSerializer(item_objs, many=True).data,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if include_skills:
|
|
|
|
|
response_data["skillBuild"] = generate_skill_build()
|
|
|
|
|
|
|
|
|
|
if include_aspect:
|
|
|
|
|
hero_aspects = Aspect.objects.filter(hero=hero_obj)
|
|
|
|
|
if hero_aspects.exists():
|
|
|
|
|
aspect_obj = hero_aspects.order_by("?").first()
|
|
|
|
|
response_data["aspect"] = aspect_obj.name
|
|
|
|
|
|
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
def _format_errors(self, errors: dict) -> str:
|
|
|
|
|
"""Format serializer errors into a readable message."""
|
|
|
|
|
messages = []
|
|
|
|
|
for field, field_errors in errors.items():
|
|
|
|
|
for error in field_errors:
|
|
|
|
|
if field == "itemsCount" and "greater than or equal to" in str(error):
|
|
|
|
|
messages.append("itemsCount must be at least 1.")
|
|
|
|
|
elif "valid" in str(error).lower() or "required" in str(error).lower():
|
|
|
|
|
if field in ("includeSkills", "includeAspect"):
|
|
|
|
|
messages.append("includeSkills and includeAspect must be boolean.")
|
|
|
|
|
elif field == "itemsCount":
|
|
|
|
|
messages.append("itemsCount must be a number.")
|
|
|
|
|
else:
|
|
|
|
|
messages.append(f"{field}: {error}")
|
|
|
|
|
return messages[0] if messages else "Invalid request data."
|
2025-12-16 22:14:33 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class BuildOfDayView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
GET: Return the build of the day. Generates one if it doesn't exist for today.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
ITEMS_COUNT = 6
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
today = date.today()
|
|
|
|
|
|
|
|
|
|
build = BuildOfDay.objects.filter(date=today).first()
|
|
|
|
|
if not build:
|
|
|
|
|
build = self._generate_build_of_day(today)
|
|
|
|
|
if build is None:
|
|
|
|
|
return Response(
|
|
|
|
|
{"message": "Unable to generate build. Load data first."},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response_data = {
|
|
|
|
|
"date": build.date.isoformat(),
|
|
|
|
|
"hero": HeroSerializer(build.hero).data,
|
|
|
|
|
"items": ItemSerializer(build.items.all(), many=True).data,
|
|
|
|
|
"skillBuild": build.skill_build,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if build.aspect:
|
|
|
|
|
response_data["aspect"] = build.aspect
|
|
|
|
|
|
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
def _generate_build_of_day(self, today: date) -> BuildOfDay | None:
|
|
|
|
|
"""Generate and save a new build of the day."""
|
|
|
|
|
hero_count = Hero.objects.count()
|
|
|
|
|
item_count = Item.objects.count()
|
|
|
|
|
|
|
|
|
|
if hero_count == 0 or item_count < self.ITEMS_COUNT:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
hero_obj = Hero.objects.order_by("?").first()
|
|
|
|
|
item_objs = list(Item.objects.order_by("?")[: self.ITEMS_COUNT])
|
|
|
|
|
|
|
|
|
|
aspect_name = None
|
|
|
|
|
hero_aspects = Aspect.objects.filter(hero=hero_obj)
|
|
|
|
|
if hero_aspects.exists():
|
|
|
|
|
aspect_obj = hero_aspects.order_by("?").first()
|
|
|
|
|
aspect_name = aspect_obj.name
|
|
|
|
|
|
|
|
|
|
build = BuildOfDay.objects.create(
|
|
|
|
|
date=today,
|
|
|
|
|
hero=hero_obj,
|
|
|
|
|
skill_build=generate_skill_build(),
|
|
|
|
|
aspect=aspect_name,
|
|
|
|
|
)
|
|
|
|
|
build.items.set(item_objs)
|
|
|
|
|
|
|
|
|
|
return build
|