diff --git a/data/v2/build.py b/data/v2/build.py index 62ad3be42..c0e845477 100644 --- a/data/v2/build.py +++ b/data/v2/build.py @@ -2281,6 +2281,17 @@ def csv_record_to_objects(info): build_generic((PokemonStat,), "pokemon_stats.csv", csv_record_to_objects) + def csv_record_to_objects(info): + yield PokemonStatPast( + pokemon_id=int(info[0]), + generation_id=int(info[1]), + stat_id=int(info[2]), + base_stat=int(info[3]), + effort=int(info[4]), + ) + + build_generic((PokemonStatPast,), "pokemon_stats_past.csv", csv_record_to_objects) + def csv_record_to_objects(info): yield PokemonType( pokemon_id=int(info[0]), type_id=int(info[1]), slot=int(info[2]) diff --git a/data/v2/csv/pokemon_stats.csv b/data/v2/csv/pokemon_stats.csv index 83107fc33..404f43c20 100644 --- a/data/v2/csv/pokemon_stats.csv +++ b/data/v2/csv/pokemon_stats.csv @@ -5390,11 +5390,11 @@ pokemon_id,stat_id,base_stat,effort 899,1,103,0 899,2,105,1 899,3,72,0 -899,4,105,0 +899,4,105,1 899,5,75,0 899,6,65,0 900,1,70,0 -900,2,135,2 +900,2,135,3 900,3,95,0 900,4,45,0 900,5,70,0 @@ -5405,20 +5405,20 @@ pokemon_id,stat_id,base_stat,effort 901,4,45,0 901,5,80,0 901,6,50,0 -902,1,120,2 +902,1,120,3 902,2,112,0 902,3,65,0 902,4,80,0 902,5,75,0 902,6,78,0 903,1,80,0 -903,2,130,1 +903,2,130,2 903,3,60,0 903,4,40,0 903,5,80,0 -903,6,120,1 +903,6,120,0 904,1,85,0 -904,2,115,1 +904,2,115,2 904,3,95,0 904,4,65,0 904,5,65,0 @@ -7631,16 +7631,16 @@ pokemon_id,stat_id,base_stat,effort 10247,4,80,0 10247,5,55,0 10247,6,98,2 -10248,1,120,2 +10248,1,120,3 10248,2,92,0 10248,3,65,0 10248,4,100,0 10248,5,75,0 10248,6,78,0 10249,1,74,0 -10249,2,115,3 +10249,2,115,0 10249,3,110,0 -10249,4,135,0 +10249,4,135,3 10249,5,100,0 10249,6,46,0 10250,1,75,0 diff --git a/data/v2/csv/pokemon_stats_past.csv b/data/v2/csv/pokemon_stats_past.csv new file mode 100644 index 000000000..6674f66f5 --- /dev/null +++ b/data/v2/csv/pokemon_stats_past.csv @@ -0,0 +1,243 @@ +pokemon_id,generation_id,stat_id,base_stat,effort +1,1,9,65,0 +2,1,9,80,0 +3,1,9,100,0 +4,1,9,50,0 +5,1,9,65,0 +6,1,9,85,0 +7,1,9,50,0 +8,1,9,65,0 +9,1,9,85,0 +10,1,9,20,0 +11,1,9,25,0 +12,1,9,80,0 +13,1,9,20,0 +14,1,9,25,0 +15,1,9,45,0 +16,1,9,35,0 +17,1,9,50,0 +18,1,9,70,0 +19,1,9,25,0 +20,1,9,50,0 +21,1,9,31,0 +22,1,9,61,0 +23,1,9,40,0 +24,1,9,65,0 +25,1,9,50,0 +26,1,9,90,0 +27,1,9,30,0 +28,1,9,55,0 +29,1,9,40,0 +30,1,9,55,0 +31,1,9,75,0 +32,1,9,40,0 +33,1,9,55,0 +34,1,9,75,0 +35,1,9,60,0 +36,1,9,85,0 +37,1,9,65,0 +38,1,9,100,0 +39,1,9,25,0 +40,1,9,50,0 +41,1,9,40,0 +42,1,9,75,0 +43,1,9,75,0 +44,1,9,85,0 +45,1,9,100,0 +46,1,9,55,0 +47,1,9,80,0 +48,1,9,40,0 +49,1,9,90,0 +50,1,9,45,0 +51,1,9,70,0 +52,1,9,40,0 +53,1,9,65,0 +54,1,9,50,0 +55,1,9,80,0 +56,1,9,35,0 +57,1,9,60,0 +58,1,9,50,0 +59,1,9,80,0 +60,1,9,40,0 +61,1,9,50,0 +62,1,9,70,0 +63,1,9,105,0 +64,1,9,120,0 +65,1,9,135,0 +66,1,9,35,0 +67,1,9,50,0 +68,1,9,65,0 +69,1,9,70,0 +70,1,9,85,0 +71,1,9,100,0 +72,1,9,100,0 +73,1,9,120,0 +74,1,9,30,0 +75,1,9,45,0 +76,1,9,55,0 +77,1,9,65,0 +78,1,9,80,0 +79,1,9,40,0 +80,1,9,80,0 +81,1,9,95,0 +82,1,9,120,0 +83,1,9,58,0 +84,1,9,35,0 +85,1,9,60,0 +86,1,9,70,0 +87,1,9,95,0 +88,1,9,40,0 +89,1,9,65,0 +90,1,9,45,0 +91,1,9,85,0 +92,1,9,100,0 +93,1,9,115,0 +94,1,9,130,0 +95,1,9,30,0 +96,1,9,90,0 +97,1,9,115,0 +98,1,9,25,0 +99,1,9,50,0 +100,1,9,55,0 +101,1,9,80,0 +102,1,9,60,0 +103,1,9,125,0 +104,1,9,40,0 +105,1,9,50,0 +106,1,9,35,0 +107,1,9,35,0 +108,1,9,60,0 +109,1,9,60,0 +110,1,9,85,0 +111,1,9,30,0 +112,1,9,45,0 +113,1,9,105,0 +114,1,9,100,0 +115,1,9,40,0 +116,1,9,70,0 +117,1,9,95,0 +118,1,9,50,0 +119,1,9,80,0 +120,1,9,70,0 +121,1,9,100,0 +122,1,9,100,0 +123,1,9,55,0 +124,1,9,95,0 +125,1,9,85,0 +126,1,9,85,0 +127,1,9,55,0 +128,1,9,70,0 +129,1,9,20,0 +130,1,9,100,0 +131,1,9,95,0 +132,1,9,48,0 +133,1,9,65,0 +134,1,9,110,0 +135,1,9,110,0 +136,1,9,110,0 +137,1,9,75,0 +138,1,9,90,0 +139,1,9,115,0 +140,1,9,45,0 +141,1,9,70,0 +142,1,9,60,0 +143,1,9,65,0 +144,1,9,125,0 +145,1,9,125,0 +146,1,9,125,0 +147,1,9,50,0 +148,1,9,70,0 +149,1,9,100,0 +150,1,9,154,0 +151,1,9,100,0 +193,3,6,95,2 +200,3,4,85,1 +242,3,1,255,2 +315,3,4,100,1 +355,3,3,90,1 +356,3,5,130,2 +12,5,4,80,2 +15,5,2,80,2 +18,5,6,91,3 +25,5,2,30,0 +25,5,4,40,0 +26,5,5,100,0 +31,5,2,82,0 +34,5,2,92,3 +36,5,3,85,0 +40,5,4,75,0 +45,5,4,100,3 +62,5,2,85,0 +65,5,4,85,3 +71,5,5,60,0 +76,5,2,110,0 +181,5,2,75,0 +182,5,2,85,0 +184,5,4,50,0 +189,5,5,85,0 +267,5,4,90,3 +295,5,4,63,0 +398,5,5,50,0 +407,5,3,55,0 +508,5,1,100,0 +521,5,2,105,3 +526,5,4,70,0 +537,5,2,85,0 +542,5,5,70,0 +545,5,2,90,0 +553,5,3,70,0 +24,6,1,85,0 +51,6,1,80,0 +10037,6,4,95,3 +83,6,2,65,1 +85,6,6,100,0 +101,6,5,140,0 +103,6,5,65,0 +164,6,4,76,0 +168,6,5,60,0 +211,6,3,75,0 +219,6,1,50,0 +219,6,4,80,0 +222,6,1,55,0 +222,6,3,85,1 +222,6,5,85,1 +226,6,1,65,0 +277,6,4,50,0 +279,6,4,85,0 +284,6,4,80,1 +284,6,6,60,0 +301,6,5,70,0 +313,6,2,55,0 +313,6,4,75,0 +314,6,2,55,0 +314,6,4,75,0 +337,6,1,70,0 +338,6,1,70,0 +358,6,2,70,0 +358,6,4,80,1 +527,6,1,55,0 +558,6,2,95,0 +614,6,1,110,0 +615,6,2,30,0 +199,7,5,110,3 +681,7,3,150,2 +681,7,5,150,1 +10026,7,2,150,2 +10026,7,4,150,1 +488,8,3,120,0 +488,8,5,130,3 +888,8,2,130,0 +10188,8,2,170,0 +889,8,2,130,0 +10189,8,2,130,0 +10189,8,3,145,0 +10189,8,5,145,0 +899,8,4,105,0 +900,8,2,135,2 +902,8,1,120,2 +10248,8,1,120,2 +903,8,2,130,1 +903,8,6,120,1 +904,8,2,115,1 +10249,8,2,115,3 +10249,8,4,135,0 diff --git a/data/v2/csv/stats.csv b/data/v2/csv/stats.csv index 56a771d09..a2104dd9d 100644 --- a/data/v2/csv/stats.csv +++ b/data/v2/csv/stats.csv @@ -7,3 +7,4 @@ id,damage_class_id,identifier,is_battle_only,game_index 6,,speed,0,4 7,,accuracy,1, 8,,evasion,1, +9,3,special,0,7 diff --git a/pokemon_v2/migrations/0025_pokemonstatpast.py b/pokemon_v2/migrations/0025_pokemonstatpast.py new file mode 100644 index 000000000..6eec3d545 --- /dev/null +++ b/pokemon_v2/migrations/0025_pokemonstatpast.py @@ -0,0 +1,63 @@ +# Generated by Django 5.2.10 on 2026-01-28 14:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pokemon_v2", "0024_django5"), + ] + + operations = [ + migrations.CreateModel( + name="PokemonStatPast", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("base_stat", models.IntegerField()), + ("effort", models.IntegerField()), + ( + "generation", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s", + to="pokemon_v2.generation", + ), + ), + ( + "pokemon", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s", + to="pokemon_v2.pokemon", + ), + ), + ( + "stat", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s", + to="pokemon_v2.stat", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/pokemon_v2/models.py b/pokemon_v2/models.py index 8a2e23fe4..5fd7c2ae5 100644 --- a/pokemon_v2/models.py +++ b/pokemon_v2/models.py @@ -1816,6 +1816,12 @@ class PokemonStat(HasPokemon, HasStat): effort = models.IntegerField() +class PokemonStatPast(HasPokemon, HasStat, HasGeneration): + base_stat = models.IntegerField() + + effort = models.IntegerField() + + class PokemonType(HasPokemon, HasType): slot = models.IntegerField() diff --git a/pokemon_v2/serializers.py b/pokemon_v2/serializers.py index 6863de286..636cdb20f 100644 --- a/pokemon_v2/serializers.py +++ b/pokemon_v2/serializers.py @@ -4318,6 +4318,15 @@ class Meta: fields = ("base_stat", "effort", "stat") +class PokemonStatPastSerializer(serializers.ModelSerializer): + generation = GenerationSummarySerializer() + stat = StatSummarySerializer() + + class Meta: + model = PokemonStatPast + fields = ("base_stat", "effort", "generation", "stat") + + ######################### # POKEMON SERIALIZERS # ######################### @@ -4340,6 +4349,7 @@ class PokemonDetailSerializer(serializers.ModelSerializer): moves = serializers.SerializerMethodField("get_pokemon_moves") species = PokemonSpeciesSummarySerializer(source="pokemon_species") stats = PokemonStatSerializer(many=True, read_only=True, source="pokemonstat") + past_stats = serializers.SerializerMethodField("get_past_pokemon_stats") types = serializers.SerializerMethodField("get_pokemon_types") past_types = serializers.SerializerMethodField("get_past_pokemon_types") forms = PokemonFormSummarySerializer( @@ -4371,6 +4381,7 @@ class Meta: "sprites", "cries", "stats", + "past_stats", "types", "past_types", ) @@ -5083,6 +5094,39 @@ def get_past_pokemon_abilities(self, obj): return final_data + def get_past_pokemon_stats(self, obj): + pokemon_past_stat_objects = PokemonStatPast.objects.filter(pokemon=obj) + pokemon_past_stats = PokemonStatPastSerializer( + pokemon_past_stat_objects, many=True, context=self.context + ).data + + # post-process to the form we want + current_generation = "" + past_obj = {} + final_data = [] + for pokemon_past_stat in pokemon_past_stats: + generation = pokemon_past_stat["generation"]["name"] + if generation != current_generation: + current_generation = generation + past_obj = {} + + # create past stats object for this generation + past_obj["generation"] = pokemon_past_stat["generation"] + del pokemon_past_stat["generation"] + + # create stats array + past_obj["stats"] = [pokemon_past_stat] + + # add to past stats array + final_data.append(past_obj) + + else: + # add to existing array for this generation + del pokemon_past_stat["generation"] + past_obj["stats"].append(pokemon_past_stat) + + return final_data + # { # "slot": 1, # "type": { diff --git a/pokemon_v2/tests.py b/pokemon_v2/tests.py index c0bc8f0a9..6bed79fcd 100644 --- a/pokemon_v2/tests.py +++ b/pokemon_v2/tests.py @@ -1656,6 +1656,21 @@ def setup_pokemon_stat_data(cls, pokemon, base_stat=10, effort=10): return pokemon_stat + @classmethod + def setup_pokemon_past_stat_data(cls, pokemon, generation, base_stat=10, effort=10): + stat = cls.setup_stat_data(name="stt for pkmn") + + pokemon_stat_past = PokemonStatPast( + pokemon=pokemon, + generation=generation, + stat=stat, + base_stat=base_stat, + effort=effort, + ) + pokemon_stat_past.save() + + return pokemon_stat_past + @classmethod def setup_pokemon_type_data(cls, pokemon, type=None, slot=1): type = type or cls.setup_type_data(name="tp for pkmn") @@ -4904,6 +4919,9 @@ def test_pokemon_api(self): pokemon=pokemon, generation=generation ) pokemon_stat = self.setup_pokemon_stat_data(pokemon=pokemon) + pokemon_past_stat = self.setup_pokemon_past_stat_data( + pokemon=pokemon, generation=generation + ) pokemon_type = self.setup_pokemon_type_data(pokemon=pokemon) pokemon_past_type = self.setup_pokemon_past_type_data( pokemon=pokemon, generation=generation @@ -5029,6 +5047,29 @@ def test_pokemon_api(self): response.data["stats"][0]["stat"]["url"], "{}{}/stat/{}/".format(TEST_HOST, API_V2, pokemon_stat.stat.pk), ) + # past stat params + past_stats_obj = response.data["past_stats"][0] + self.assertEqual( + past_stats_obj["generation"]["name"], + pokemon_past_stat.generation.name, + ) + self.assertEqual( + past_stats_obj["generation"]["url"], + "{}{}/generation/{}/".format( + TEST_HOST, API_V2, pokemon_past_stat.generation.pk + ), + ) + + past_stats_stats_obj = past_stats_obj["stats"][0] + self.assertEqual(past_stats_stats_obj["base_stat"], pokemon_past_stat.base_stat) + self.assertEqual(past_stats_stats_obj["effort"], pokemon_past_stat.effort) + self.assertEqual( + past_stats_stats_obj["stat"]["name"], pokemon_past_stat.stat.name + ) + self.assertEqual( + past_stats_stats_obj["stat"]["url"], + "{}{}/stat/{}/".format(TEST_HOST, API_V2, pokemon_past_stat.stat.pk), + ) # type params self.assertEqual(response.data["types"][0]["slot"], pokemon_type.slot) self.assertEqual( @@ -5719,6 +5760,9 @@ def test_case_insensitive_api(self): pokemon=pokemon, generation=generation ) pokemon_stat = self.setup_pokemon_stat_data(pokemon=pokemon) + pokemon_past_stat = self.setup_pokemon_past_stat_data( + pokemon=pokemon, generation=generation + ) pokemon_type = self.setup_pokemon_type_data(pokemon=pokemon) pokemon_past_type = self.setup_pokemon_past_type_data( pokemon=pokemon, generation=generation