diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e74d61 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.venv +__pycache__ +*.pyc +*.pyo +.git +.gitignore +*.sqlite3 +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6fe7f15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Run migrations, load data and start server +CMD ["sh", "-c", "python manage.py migrate && python manage.py load_static_data && python manage.py runserver 0.0.0.0:8000"] diff --git a/api/__pycache__/models.cpython-312.pyc b/api/__pycache__/models.cpython-312.pyc index 3d686e1..249e1c7 100644 Binary files a/api/__pycache__/models.cpython-312.pyc and b/api/__pycache__/models.cpython-312.pyc differ diff --git a/api/__pycache__/urls.cpython-312.pyc b/api/__pycache__/urls.cpython-312.pyc index 305d821..8acf2d6 100644 Binary files a/api/__pycache__/urls.cpython-312.pyc and b/api/__pycache__/urls.cpython-312.pyc differ diff --git a/api/__pycache__/views.cpython-312.pyc b/api/__pycache__/views.cpython-312.pyc index 8c3d127..65ed18f 100644 Binary files a/api/__pycache__/views.cpython-312.pyc and b/api/__pycache__/views.cpython-312.pyc differ diff --git a/api/migrations/0004_buildofday.py b/api/migrations/0004_buildofday.py new file mode 100644 index 0000000..0c30389 --- /dev/null +++ b/api/migrations/0004_buildofday.py @@ -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')), + ], + ), + ] diff --git a/api/migrations/__pycache__/0004_buildofday.cpython-312.pyc b/api/migrations/__pycache__/0004_buildofday.cpython-312.pyc new file mode 100644 index 0000000..d84ff32 Binary files /dev/null and b/api/migrations/__pycache__/0004_buildofday.cpython-312.pyc differ diff --git a/api/models.py b/api/models.py index f53ddb2..0087c3b 100644 --- a/api/models.py +++ b/api/models.py @@ -35,3 +35,14 @@ class Aspect(models.Model): 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}" diff --git a/api/urls.py b/api/urls.py index af62307..21133cc 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import HeroesListView, RandomizeBuildView +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"), ] diff --git a/api/views.py b/api/views.py index 10a246d..b8f46e3 100644 --- a/api/views.py +++ b/api/views.py @@ -1,8 +1,10 @@ +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, Hero, Item +from api.models import Aspect, BuildOfDay, Hero, Item from api.serializers import ( HeroSerializer, ItemSerializer, @@ -101,3 +103,62 @@ class RandomizeBuildView(APIView): 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 diff --git a/config/settings.py b/config/settings.py index ca637a2..eef9ade 100644 --- a/config/settings.py +++ b/config/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-a#7v1um&b88$k)2nm7hvxe__o2c(gz=t5%)1)*oaij6u%+i((= # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition @@ -76,10 +76,15 @@ WSGI_APPLICATION = 'config.wsgi.application' # Database # https://docs.djangoproject.com/en/6.0/ref/settings/#databases +import os + +DATA_DIR = BASE_DIR / 'data' +DATA_DIR.mkdir(exist_ok=True) + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'NAME': DATA_DIR / 'db.sqlite3', } } @@ -121,8 +126,4 @@ USE_TZ = True STATIC_URL = 'static/' # CORS settings -CORS_ALLOWED_ORIGINS = [ - "http://localhost:5173", - "http://127.0.0.1:5173", - -] +CORS_ALLOW_ALL_ORIGINS = True diff --git a/db.sqlite3 b/db.sqlite3 index 99605f5..fc07fb6 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9d02c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +django>=6.0,<7.0 +djangorestframework>=3.16,<4.0 +django-cors-headers>=4.9,<5.0