From 4896d18db79b9c134e6e704c69c1569669d954d3 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 16 Dec 2025 22:14:33 +0300 Subject: [PATCH] v2 --- .dockerignore | 8 +++ Dockerfile | 13 ++++ api/__pycache__/models.cpython-312.pyc | Bin 2317 -> 3246 bytes api/__pycache__/urls.cpython-312.pyc | Bin 536 -> 652 bytes api/__pycache__/views.cpython-312.pyc | Bin 5398 -> 8270 bytes api/migrations/0004_buildofday.py | 25 +++++++ .../0004_buildofday.cpython-312.pyc | Bin 0 -> 1509 bytes api/models.py | 11 +++ api/urls.py | 3 +- api/views.py | 63 +++++++++++++++++- config/settings.py | 15 +++-- db.sqlite3 | Bin 176128 -> 200704 bytes requirements.txt | 3 + 13 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 api/migrations/0004_buildofday.py create mode 100644 api/migrations/__pycache__/0004_buildofday.cpython-312.pyc create mode 100644 requirements.txt 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 3d686e1b50ccec6fc15fb7cd5ea1554759263aef..249e1c7864238f6d42edb49bf2dd495cf2f5e562 100644 GIT binary patch delta 898 zcmZWnK}^$77=CYS*LK~8bTAnkFl*E`8U(Wtw-6G|L?dw=ARIWbl(jR3vgvDgn3wIq zfs28q2WE_s#Keq11mlHx^lD64T(YGnuU?Fac;V!G8zSmU`u+d^z3=_+>!0^jcoXz} z^m^R@uMe+pXb%o?O$dFshJs;Gd)B{Y6f4+wR*1O;@I~Nx3*HLEBZx zX^#u41W^zcL=41vq1%SdIx$sAd!MF(& zXnlb}=&&Z3Kw^pP{L_p{w!X-x;0bl*$Zw5_Xf8I52H5^yW)pz9G%Cql+=Uza6~}@ zWSoe&wYem|X;M>l1tkbMN`NMCg8;E$m%1Lvx#Db|U`Kk9(6nZ%D~8S8pP5dzX>41| zm^N=LYr39ZBqK&cMCp=^M{34I5nP}EfeQqPFVssQ_8Z9j4TND8wD{24Sd$Oc`BG>r z*5IS%WQz|IDqQEo)!EwI-oo}mgCEDdM_ik1dc?X%+=_knM9X(tg1B+HDTsAJti>9_ zNO`K|9ax)ddI##>ftr6md0;lY$@1iP3et(H{|MmF@k6YnB*x~HB{}ynKddYwYn=^6 w+ZO+}eaw!pu$+_{#Vm8LA%^mDYjLd$TY?JwU g)F)f>NOG9~l`{fyvBl y6}53>=JxMdCmK04!w@;s5{u diff --git a/api/__pycache__/urls.cpython-312.pyc b/api/__pycache__/urls.cpython-312.pyc index 305d8219cecab511444476afb3ccf8bc3fa1fd3a..8acf2d6ebe7e0c16da11477624f64954ea2b934b 100644 GIT binary patch delta 272 zcmbQi(!;8LnwOW00SJC9bIkN-WMFs<;=lkql<|4rM0I(gT-GR7Mg}H^RK^q*Ajz7= zGciP;nFUC-sl|bXIe{c|77t82f?f?}rf{xdTg?pNq%%jcS8{1`zXWkL8E>%^B$i~n z1Ui91llc~(Q)y;Sihr6*Vr5unYWc(yv4T}RNgy%Z{50K^#7aL+j>#H~%1lMdkqeJe3r2Y)K>yr8xCG{^$8t&k` S!s7mcorRyNk-JC$s0{!aKRQ$Z delta 173 zcmeBSox!4hnwOW00SK;?T4a6$(vLwL7+`}kK6gx1m*-<-U}8vROkn|%ELmI=_ef7H z7GY)vv(!1(u&ri>s7_~!VyooTYBJtpDM&2Ic*zLjFiqxTjAi!IWS?Bas62TI rW2uB7P#7dt>;@!0Ff%eT-er(^&R{y(gvo^G11mE>QzLf~KTr+;Q-mR` diff --git a/api/__pycache__/views.cpython-312.pyc b/api/__pycache__/views.cpython-312.pyc index 8c3d127d980c667e397775b8033bed8931b4cb6c..65ed18f2162e9815301fc31b3dab9df17143d91c 100644 GIT binary patch delta 3104 zcmZ`*T}&I<6}~g}jQ{_bF~)|Ffh2%|b+Tn4Kae!!hipTD?k3q*-Us9`(MW}rR@%)&H*7Oz>!>O^xC2te=i>1!KJV#Wb3Tb=B5qD&q zac9OAcV*mhHJluvZ32jFe1wpDq06IEa-679- zUa&v6^TNAaO;a+DSpU7x2a{0?-2%G2a)Ox%Nk& z2wz)$&8Th5Xu6)bs!_rGne#WWm20ln&QCdd7$?O9iz(VfaRJH<>?wWndLo@tsm=Vk ztA!KI-@Cf=xOwVAXhK6_DmRxks2lrYGL<$oS{0Hhsv9(dN*E0FB4BLP0ZGPs~Fcb&3t|(;#PTpdP4yw+4CWgE)#3K~3l{y*0-5%hZK8akKDh|u+j|U5J=RoSwWK)1 zUms65)O*|CHS2?|&0Qo5mT7`(azR)m;x$5bHJE4ukrb`(0&{X#GmuNHfp#m~`l?-G zQK`b3SYKs20$Y|2Tc+_GcQw`ap>@#;=9ntG8ug(ZZTDJL`w+}-$GKqL5o3q>Lw;}G zF;4Lx(}Mu>-Iue8D`^dmMY+0lfVH!YDHn1HRe_q6P#D=_6sylA+6Wl6QalSrY>EV+ zM-c{bN@i14fgLo(ipywBPhsygA$d}5oUE@k$?|A2mz;#Gpsb={4$yA{R2?Zj2Pu!ZzjU71R@OwY*BlVW49)nAuRyphvOj%r|^9 z_hVNzLkGG3XMo#e!!Iv8mz<`%AUyR)%KrAEzrE~_7X8uHxpjZE z&O8ndmxCk4;7BF1w;bsyMtYt(gs`*V+4K^3;O@}-LzRx6`{{e>!q}TDT}Ai4r@qjN zXDwNdoh-&qKK2c-+lOm)1%DKM8({p^%={J4m3wA{!O{?M* z)u4dauFQ9SjAaaJRGR|X z{DECmdv!_p5&D3q7_iU$%+r%!nO=RP+%s708T`C;up)(?N}=VUrJ=IaRg}6`rIPeo zS?ViFeI=>CVBe6NK9oOd`Jkm7jupePRlOAMTYqEZp;{goDGrRRhtHMdHw%t08zTkR zMrhCSt)*M#P;W8R`><<0)LRM-7TgtAsO)Mjx>_H*luGzOeut-W_-HYFv=kmJoUcUo ze{}GJgN4yuWuX#itOUXvO}(Y2SVd~xbdW&zb0P$s1p#R8@ZNR3=ep~A&$r@uY;XS( zs)L(x@c+CDu3_zDb*ff;S!u<8x%*Wxi86#V{=Hs+t+(E$ok;TS(SKPRg;U(-p|B8K zJ+n#R^DzFq{l)%~7uc_r0W=?vHdgKUC^b@WvY6xzRWH?aV=_tMtZ(G#3_Lnp2W-JNkaCAHRFt*ntULCq4$6s0a%njPARVb~dMaK@GAoz)7& z4j#Hh$P3Z2juAoNrAz$-L20*!qz;jE2#OG-Q|~iNX#?|l=lLI}B{s2TFXh*j>il#m?Pp3E;Zy8{r}=tGrzA$s z6I)#&cG8ib#jzA2rTWO565hH!-t6tPk@9i;(B{+dZ}#aL+k1g z6xKe=+mhadH4?tVm^I@-oPSBZTHYsoMl6(WI#3B^H*kCwrr50Oc_UTVvkeNp_?Kcu z=Qf%E`+=yE>N0b|@?0D6?4f2(SHQOd(*dY}XtTD0gu#XjQ&X4C-tKO5WO&q^9Gkd0 zc4aEm7I-nOEo1ATA{sGQE?25^0VMHTnRdOvfl!`yfd#N1lTc)+LuiPEfY|Jo%K`={ z05l62dL$Vfz-Wp&;EGP~h?WM?p}BKquWCCco|HdNbDHPFnReQ?W@q|iN_0^JYftp= zl%C`dvJ=V)tlP!EW*2B5zmaQE@IGLGFXj4@(T$!FHh(NP^7WiXukla050|jYxR{S3 zrnVv45r+_+h>HSZTHJB~lW1cNIE>hxlnyiz%{!MV z({%`HKIw(q4 sJ?AW#VlygUHZ1TXPASzlN!unlzDbU45o3cGTrYN{x5+rAqqXV&17OpF 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 0000000000000000000000000000000000000000..d84ff3294549efbe4a9406c3a9bd88b5c153fee5 GIT binary patch literal 1509 zcmZuxJ#5=X6y~2OiIyG7ajeL09knouQidh9snVrL>iDOqTPr|q7>GI$C*Mh?Opyvn z1y!aN$dEBxJ$5S!YjCG#>}a412?&sIOAB<$Ccx>ErF}<=wE_DG9KZMOgf}FmG@*9^Qf>w8fg(mTD5u3EUSP7N2uiB4R60 ziv+^=3Fxv>i}iRl_L{+}Do~}ZH*MtVmg!KrZDC?K2W%}GsR=lL!L#Tjh+Gpe-{fne zz!B-XSXjVpO>WIZG8b->TN|CxwO<;x@1gXD}U98)BVhe5c(nV zY!LbVYj`*Na~G#+%Kcl>SCIAD-n^Qn$(^WfP(%@RbUIZom+xxG>5v1LDi6E5fgd$C zP>%{a-rJ(Wmm8`?Q>g1&+JQ}wOE8`4*m@h;J?)6}Xwo6Jjtu=7p{b|Du3HY#Ow=Y+ z!a!DeDtAr&Nte(m=SVjUtMbq$xwVeHpua3EErvATiGBWN)6on{cT=^j$a z>>37@4vB3&OTeCs48i`BmZ?GK5tmTGwP;LtN!tk-463}n$TS5IU>lMfIx(pmQD1g`4yy3#w}zvEh^wTO$3L5J4NH`YrE@f z8=LgT4l;Xt7MmyES{*XEc)xJxum$cdDjiyFQi6o(?NZ0KT2M)+ge@1X+Q`IKdo{@U zb*|MqIyfppN=p~_xdaXINmwSUfl;s6>De;G38pvzX9Ux8ZY*=jJ7ZCplfVCZtN?zY z3=>O!Vri6l_pi(?FI#x|beJvp*}^O7Y}u=6o@o!Vj+b^vnXH#nkLQOu)z7J?bA#MH zFa7B#v*hK=Kgq*<+0U0>%?Hcqze|8~@U)lG*C4*eUOE*WE+ulO?_=Dj>**-tP3!+{Y9{0BO$ Bhphkr literal 0 HcmV?d00001 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 99605f5dbe9238f799f4c2b01d2a932fb0035698..fc07fb689ea9d8cad3cf5f4788789c740686cd1a 100644 GIT binary patch delta 1990 zcmb7_TWs4@7{`5LCr;d?C#7p$Sf`#ww6RH(+HT#;%2=~BQnOacF72S=k~)dQqV*D* zvayG zeEy&R_xsNIe7x}^wegno#^II;48wTscmFf>O5Z@+1QuAkX4OI#xTD|bhkwGIa?AV` z*i7rUl&`hBpS(u^q>!{Bc)0b^w+7lf@35Ut%n z^WC$giizW!I76W#IUcc7?@}*O(-bNxTaLmbj1>dEVQMWSKVJtCl9rGjLRAyAoJJa1 zz^iHd$EcLNAm`->_&{Sl3{8N3u%Fypi|-@sJwyN&G=TJ!qfdXdf-|tK zNNz{$&{BC)+9fZQKj4T7JfYaJ8fy zAu3-4fyJWQByiI<#@Xl?tHzU{L(L^Xm-?mzKDO}z8y)V_qes*a6Tn^hGzs1V>Rb|t z>gNgYYGqLdFIoXr2J?S+ZD!rt_k%3q?x0{4gTKP>;g|3ecne;K>o5x?7}c5SvCV^tuQx(Fafu|9kxC*t-?S2RLbTP!yB#)uqgHW;F!V z_a{Mn!ECb(6y}cqr;I*Sw-5HmNP3C|3R|nFA^&-KWm&HxnHKrMQ88fz_t!7`zEy9W zYlf)z#IDZ6UOI7%JV%Qnxa`eKi3Qo1tXg-@EY`P+F%vv79o~mg?ROPu&nmUIj_S=t z&{O|^BLrANmsao8uC!P$$&NjG|44W+ZQLB*2;;dVjgd6Vg3IeB_(m;z5ONi^kdpGU zM|)M|WfYCgpy=%MG@6Y@PS1w5{mWg*EG*PI+Jo})#r&QoG#L&}A%;15HWrPa3r+@k+W5Mmeri4TvkrzJ;=Ext!PL23L|hFt6pCOPgQ=o09btYl=}yAN_g)8 delta 436 zcmZozz|-)6Yl5_(0|NttHV`WTF%uBGPSi0Lc3{ve>tqG;82Nt#Nd|HL-=j1E;+PtD%Koy+)Neuk&_>(sC894G! zoFF<*qA8{4=xU-QyqWme(!o6H^n6!^!p*)Mim5wi-v?qv20K!Lk_ zn|1GBPhjR}JU*S>fl;1=$)9l&!vzN0jg2!IxBI6sGBIsG>C9*$W60dcAk4_h$YsO1 zlH)Fi5Zfy@5!PI$qs)=a2bd-?&S45=WaZ1{ea6eolgj;Cma(aLV&moM0cniWr>mqh z?wtNSjnQ`c-E>Cd>7Ozgr%d0Q$!N;L^_;pW@%cKcBygzm~s%KXGHDJOB2? t^NjNyG=LgGfDwo_k+2q!4U$ub(#pWlzQMmtfhhsVcn=If!DSPe6aWXKc#Z%7 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