Initial commit
This commit is contained in:
0
dota-random-builds-back/api/__init__.py
Normal file
0
dota-random-builds-back/api/__init__.py
Normal file
3
dota-random-builds-back/api/admin.py
Normal file
3
dota-random-builds-back/api/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
dota-random-builds-back/api/apps.py
Normal file
5
dota-random-builds-back/api/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = 'api'
|
||||
51
dota-random-builds-back/api/data.py
Normal file
51
dota-random-builds-back/api/data.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import random
|
||||
from typing import Dict
|
||||
|
||||
|
||||
def generate_skill_build() -> Dict[int, str]:
|
||||
"""
|
||||
Generate a random skill build for levels 1-25 following Dota 2 rules:
|
||||
- Ultimate (r) can only be skilled at levels 6, 12, 18
|
||||
- Talents can only be skilled at levels 10, 15, 20, 25
|
||||
- Basic abilities (q, w, e) can be skilled at other levels
|
||||
- Each basic ability maxes at 4 points
|
||||
- Ultimate maxes at 3 points
|
||||
- One talent per tier (left OR right)
|
||||
"""
|
||||
skill_build: Dict[int, str] = {}
|
||||
|
||||
# Track ability points spent
|
||||
ability_points = {"q": 0, "w": 0, "e": 0, "r": 0}
|
||||
max_points = {"q": 4, "w": 4, "e": 4, "r": 3}
|
||||
|
||||
# Talent levels and which side to pick
|
||||
talent_levels = [10, 15, 20, 25]
|
||||
|
||||
# Ultimate levels
|
||||
ult_levels = [6, 12, 18]
|
||||
|
||||
for level in range(1, 26):
|
||||
if level in talent_levels:
|
||||
# Pick random talent side
|
||||
skill_build[level] = random.choice(["left_talent", "right_talent"])
|
||||
elif level in ult_levels:
|
||||
# Must skill ultimate if not maxed
|
||||
if ability_points["r"] < max_points["r"]:
|
||||
skill_build[level] = "r"
|
||||
ability_points["r"] += 1
|
||||
else:
|
||||
# Ultimate maxed, pick a basic ability
|
||||
available = [a for a in ["q", "w", "e"] if ability_points[a] < max_points[a]]
|
||||
if available:
|
||||
choice = random.choice(available)
|
||||
skill_build[level] = choice
|
||||
ability_points[choice] += 1
|
||||
else:
|
||||
# Regular level - pick a basic ability
|
||||
available = [a for a in ["q", "w", "e"] if ability_points[a] < max_points[a]]
|
||||
if available:
|
||||
choice = random.choice(available)
|
||||
skill_build[level] = choice
|
||||
ability_points[choice] += 1
|
||||
|
||||
return skill_build
|
||||
1
dota-random-builds-back/api/management/__init__.py
Normal file
1
dota-random-builds-back/api/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"" # Package marker
|
||||
@@ -0,0 +1 @@
|
||||
"" # Package marker
|
||||
@@ -0,0 +1,104 @@
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from api.models import Aspect, Hero, Item
|
||||
|
||||
# Add data directory to path for imports
|
||||
sys.path.insert(0, str(Path(settings.BASE_DIR) / "data"))
|
||||
from facets import HERO_FACETS
|
||||
|
||||
|
||||
def _strip_comments(text: str) -> str:
|
||||
"""Remove single-line and block comments from TypeScript data."""
|
||||
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
||||
lines = []
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("//"):
|
||||
continue
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _extract_objects(text: str, require_primary: bool = False):
|
||||
cleaned = _strip_comments(text)
|
||||
objects = []
|
||||
|
||||
for match in re.finditer(r"\{[^}]*\}", cleaned):
|
||||
chunk = match.group(0)
|
||||
entry = {}
|
||||
id_match = re.search(r"['\"]?id['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
|
||||
name_match = re.search(r"['\"]?name['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
|
||||
if not (id_match and name_match):
|
||||
continue
|
||||
entry["slug"] = id_match.group(2)
|
||||
entry["name"] = name_match.group(2)
|
||||
|
||||
if require_primary:
|
||||
primary_match = re.search(r"['\"]?primary['\"]?\s*:\s*(['\"])(.*?)\1", chunk)
|
||||
if not primary_match:
|
||||
continue
|
||||
entry["primary"] = primary_match.group(2)
|
||||
|
||||
if len(entry) == (3 if require_primary else 2):
|
||||
objects.append(entry)
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Load heroes, items and facets from data files into the database."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
base_dir = Path(settings.BASE_DIR)
|
||||
heroes_path = base_dir / "data" / "heroes.ts"
|
||||
items_path = base_dir / "data" / "items.ts"
|
||||
|
||||
if not heroes_path.exists() or not items_path.exists():
|
||||
raise CommandError("Missing data files in the ./data directory.")
|
||||
|
||||
heroes = _extract_objects(heroes_path.read_text(encoding="utf-8"), require_primary=True)
|
||||
items = _extract_objects(items_path.read_text(encoding="utf-8"), require_primary=False)
|
||||
|
||||
if not heroes:
|
||||
raise CommandError("Failed to parse heroes.ts")
|
||||
if not items:
|
||||
raise CommandError("Failed to parse items.ts")
|
||||
|
||||
hero_created = item_created = aspect_created = 0
|
||||
|
||||
for hero in heroes:
|
||||
hero_obj, created = Hero.objects.update_or_create(
|
||||
name=hero["name"],
|
||||
defaults={
|
||||
"primary": hero["primary"],
|
||||
},
|
||||
)
|
||||
hero_created += int(created)
|
||||
|
||||
# Load facets for this hero
|
||||
facets = HERO_FACETS.get(hero["name"], [])
|
||||
for facet_name in facets:
|
||||
_, facet_created = Aspect.objects.update_or_create(
|
||||
hero=hero_obj,
|
||||
name=facet_name,
|
||||
)
|
||||
aspect_created += int(facet_created)
|
||||
|
||||
for item in items:
|
||||
_, created = Item.objects.update_or_create(
|
||||
name=item["name"],
|
||||
)
|
||||
item_created += int(created)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Loaded {len(heroes)} heroes ({hero_created} new), "
|
||||
f"{len(items)} items ({item_created} new), "
|
||||
f"and {aspect_created} new facets."
|
||||
)
|
||||
)
|
||||
31
dota-random-builds-back/api/migrations/0001_initial.py
Normal file
31
dota-random-builds-back/api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 6.0 on 2025-12-09 03:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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')),
|
||||
('external_id', models.CharField(max_length=100, unique=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('primary', models.CharField(choices=[('Strength', 'Strength'), ('Agility', 'Agility'), ('Intelligence', 'Intelligence')], max_length=20)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('external_id', models.CharField(max_length=100, unique=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0 on 2025-12-09 03:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='hero',
|
||||
name='external_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='item',
|
||||
name='external_id',
|
||||
),
|
||||
]
|
||||
25
dota-random-builds-back/api/migrations/0003_aspect.py
Normal file
25
dota-random-builds-back/api/migrations/0003_aspect.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0 on 2025-12-09 18:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0002_remove_hero_external_id_remove_item_external_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Aspect',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aspects', to='api.hero')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('hero', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
25
dota-random-builds-back/api/migrations/0004_buildofday.py
Normal file
25
dota-random-builds-back/api/migrations/0004_buildofday.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 18:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0003_aspect'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BuildOfDay',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField(unique=True)),
|
||||
('skill_build', models.JSONField(default=dict)),
|
||||
('aspect', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('hero', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.hero')),
|
||||
('items', models.ManyToManyField(to='api.item')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
dota-random-builds-back/api/migrations/__init__.py
Normal file
0
dota-random-builds-back/api/migrations/__init__.py
Normal file
48
dota-random-builds-back/api/models.py
Normal file
48
dota-random-builds-back/api/models.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Hero(models.Model):
|
||||
PRIMARY_ATTR_STRENGTH = "Strength"
|
||||
PRIMARY_ATTR_AGILITY = "Agility"
|
||||
PRIMARY_ATTR_INTELLIGENCE = "Intelligence"
|
||||
|
||||
PRIMARY_CHOICES = (
|
||||
(PRIMARY_ATTR_STRENGTH, "Strength"),
|
||||
(PRIMARY_ATTR_AGILITY, "Agility"),
|
||||
(PRIMARY_ATTR_INTELLIGENCE, "Intelligence"),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=200)
|
||||
primary = models.CharField(max_length=20, choices=PRIMARY_CHOICES)
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - convenience only
|
||||
return f"{self.name} ({self.primary})"
|
||||
|
||||
|
||||
class Item(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - convenience only
|
||||
return self.name
|
||||
|
||||
|
||||
class Aspect(models.Model):
|
||||
hero = models.ForeignKey(Hero, on_delete=models.CASCADE, related_name="aspects")
|
||||
name = models.CharField(max_length=200)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["hero", "name"]
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - convenience only
|
||||
return f"{self.hero.name} - {self.name}"
|
||||
|
||||
|
||||
class BuildOfDay(models.Model):
|
||||
date = models.DateField(unique=True)
|
||||
hero = models.ForeignKey(Hero, on_delete=models.CASCADE)
|
||||
items = models.ManyToManyField(Item)
|
||||
skill_build = models.JSONField(default=dict)
|
||||
aspect = models.CharField(max_length=200, blank=True, null=True)
|
||||
|
||||
def __str__(self) -> str: # pragma: no cover - convenience only
|
||||
return f"Build of {self.date} - {self.hero.name}"
|
||||
38
dota-random-builds-back/api/serializers.py
Normal file
38
dota-random-builds-back/api/serializers.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.models import Hero, Item
|
||||
|
||||
|
||||
class HeroSerializer(serializers.ModelSerializer):
|
||||
primary = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Hero
|
||||
fields = ["id", "name", "primary"]
|
||||
|
||||
def get_primary(self, obj):
|
||||
return obj.primary.lower()
|
||||
|
||||
|
||||
class ItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ["id", "name"]
|
||||
|
||||
|
||||
class RandomizeBuildRequestSerializer(serializers.Serializer):
|
||||
includeSkills = serializers.BooleanField()
|
||||
includeAspect = serializers.BooleanField()
|
||||
itemsCount = serializers.IntegerField(min_value=1)
|
||||
heroId = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class RandomizeBuildResponseSerializer(serializers.Serializer):
|
||||
hero = HeroSerializer()
|
||||
items = ItemSerializer(many=True)
|
||||
skillBuild = serializers.DictField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
help_text="Map of level (1-25) to skill (q/w/e/r/left_talent/right_talent)"
|
||||
)
|
||||
aspect = serializers.CharField(required=False)
|
||||
69
dota-random-builds-back/api/tests.py
Normal file
69
dota-random-builds-back/api/tests.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from api.models import Hero, Item
|
||||
|
||||
|
||||
class RandomizeBuildAPITest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.hero = Hero.objects.create(name="Axe", primary="Strength")
|
||||
Item.objects.bulk_create(
|
||||
[
|
||||
Item(name="Blink Dagger"),
|
||||
Item(name="Black King Bar"),
|
||||
Item(name="Boots of Travel"),
|
||||
Item(name="Force Staff"),
|
||||
]
|
||||
)
|
||||
|
||||
def test_randomize_with_skills(self):
|
||||
response = self.client.post(
|
||||
"/api/randomize",
|
||||
{"includeSkills": True, "includeAspect": False, "itemsCount": 3},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
self.assertIn("hero", data)
|
||||
self.assertEqual(len(data.get("items", [])), 3)
|
||||
self.assertIn("skillBuild", data)
|
||||
self.assertTrue(data.get("skillBuilds"))
|
||||
self.assertNotIn("aspect", data)
|
||||
|
||||
def test_randomize_with_aspect_only(self):
|
||||
response = self.client.post(
|
||||
"/api/randomize",
|
||||
{"includeSkills": False, "includeAspect": True, "itemsCount": 2},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
self.assertIn("aspect", data)
|
||||
self.assertTrue(data.get("aspects"))
|
||||
self.assertNotIn("skillBuild", data)
|
||||
self.assertEqual(len(data.get("items", [])), 2)
|
||||
|
||||
def test_rejects_invalid_payload(self):
|
||||
response = self.client.post(
|
||||
"/api/randomize",
|
||||
{"includeSkills": "yes", "includeAspect": True, "itemsCount": 0},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("message", response.json())
|
||||
|
||||
|
||||
class LoadStaticDataCommandTest(TestCase):
|
||||
def test_loads_heroes_and_items(self):
|
||||
call_command("load_static_data")
|
||||
|
||||
self.assertGreater(Hero.objects.count(), 0)
|
||||
self.assertGreater(Item.objects.count(), 0)
|
||||
9
dota-random-builds-back/api/urls.py
Normal file
9
dota-random-builds-back/api/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import BuildOfDayView, HeroesListView, RandomizeBuildView
|
||||
|
||||
urlpatterns = [
|
||||
path("randomize", RandomizeBuildView.as_view(), name="randomize-build"),
|
||||
path("heroes", HeroesListView.as_view(), name="heroes-list"),
|
||||
path("build-of-day", BuildOfDayView.as_view(), name="build-of-day"),
|
||||
]
|
||||
164
dota-random-builds-back/api/views.py
Normal file
164
dota-random-builds-back/api/views.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from datetime import date
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from api.models import Aspect, BuildOfDay, Hero, Item
|
||||
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."
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user