diff --git a/.idea/charasheet.iml b/.idea/charasheet.iml index 03d83bd..f73b0e7 100644 --- a/.idea/charasheet.iml +++ b/.idea/charasheet.iml @@ -16,7 +16,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7502240 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/character/__init__.py b/src/character/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/character/admin.py b/src/character/admin.py new file mode 100644 index 0000000..fa52175 --- /dev/null +++ b/src/character/admin.py @@ -0,0 +1,122 @@ +from django.contrib import admin + +from character import models + + +@admin.register(models.Capability) +class CapabilityAdmin(admin.ModelAdmin): + list_display = ["name", "path", "rank", "limited", "spell"] + list_filter = ["path", "path__profile", "path__race", "rank", "limited", "spell"] + search_fields = ["name", "description"] + + +@admin.register(models.Path) +class PathAdmin(admin.ModelAdmin): + list_display = ["name", "category", "related_to"] + list_filter = ["category"] + search_fields = ["name"] + fieldsets = [ + (None, {"fields": ["name"]}), + ("Related to", {"fields": ["category", ("profile", "race")]}), + ("Notes", {"fields": ["notes"]}), + ] + + def related_to(self, instance: models.Path) -> str: + category = models.Path.Category(instance.category) + if category == models.Path.Category.PROFILE: + return str(instance.profile) + elif category == models.Path.Category.RACE: + return str(instance.race) + else: + return "" + + +@admin.register(models.RacialCapability) +class RacialCapabilityAdmin(admin.ModelAdmin): + list_display = ["name", "race"] + list_filter = ["race"] + search_fields = ["name", "description"] + + +@admin.register(models.Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ["name", "life_dice", "magical_strength"] + list_filter = ["life_dice", "magical_strength"] + search_fields = ["name"] + + +class RacialCapabilityInline(admin.TabularInline): + model = models.RacialCapability + extra = 0 + + +@admin.register(models.Race) +class RaceAdmin(admin.ModelAdmin): + list_display = ["name"] + search_fields = ["name"] + inlines = [RacialCapabilityInline] + + +@admin.register(models.Character) +class CharacterAdmin(admin.ModelAdmin): + list_display = ["name", "player", "race", "profile", "level"] + list_filter = ["race", "profile"] + search_fields = ["name", "notes"] + + fieldsets = [ + ( + "Character", + {"fields": ["name", "player", "profile", "level", "race"]}, + ), + ("Appearance", {"fields": ["gender", "age", "height", "weight"]}), + ( + "Abilities", + { + "fields": [ + ("value_strength", "modifier_strength"), + ("value_dexterity", "modifier_dexterity"), + ("value_constitution", "modifier_constitution"), + ("value_intelligence", "modifier_intelligence"), + ("value_wisdom", "modifier_wisdom"), + ("value_charisma", "modifier_charisma"), + ] + }, + ), + ( + "Fight", + {"fields": ["initiative", "attack_melee", "attack_range", "attack_magic"]}, + ), + ("Health", {"fields": ["health_max", "health_remaining"]}), + ("Defense", {"fields": ["armor", "shield", "defense_misc", "defense"]}), + ("Weapons & equipment", {"fields": ["weapons", "equipment"]}), + ("Racial", {"fields": ["racial_capability"]}), + ("Capabilities", {"fields": ["capabilities"]}), + ("Luck", {"fields": ["luck_points_max", "luck_points_remaining"]}), + ("Mana", {"fields": ["mana_max", "mana_consumed", "mana_remaining"]}), + ("Notes", {"fields": ["notes"]}), + ] + readonly_fields = [ + "modifier_strength", + "modifier_dexterity", + "modifier_constitution", + "modifier_intelligence", + "modifier_wisdom", + "modifier_charisma", + "initiative", + "attack_melee", + "attack_range", + "attack_magic", + "defense", + "mana_max", + "mana_remaining", + ] + filter_horizontal = [ + "capabilities", + "weapons", + ] + + +@admin.register(models.Weapon) +class WeaponAdmin(admin.ModelAdmin): + list_display = ["name", "damage"] + search_fields = ["name", "special", "damage"] diff --git a/src/character/apps.py b/src/character/apps.py new file mode 100644 index 0000000..bebdfd5 --- /dev/null +++ b/src/character/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CharacterConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "character" diff --git a/src/character/migrations/0001_initial.py b/src/character/migrations/0001_initial.py new file mode 100644 index 0000000..b7995d9 --- /dev/null +++ b/src/character/migrations/0001_initial.py @@ -0,0 +1,376 @@ +# Generated by Django 4.1.2 on 2022-10-28 21:52 + +import django.core.validators +import django.db.models.deletion +import django.db.models.functions.text +import django_extensions.db.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Capability", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "rank", + models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(5), + ] + ), + ), + ("limited", models.BooleanField(blank=True)), + ("spell", models.BooleanField(blank=True)), + ("description", models.TextField()), + ], + options={ + "verbose_name_plural": "Capabilities", + }, + ), + migrations.CreateModel( + name="Profile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "magical_strength", + models.CharField( + choices=[ + ("NON", "None"), + ("INT", "Intelligence"), + ("WIS", "Wisdom"), + ("CHA", "Charisma"), + ], + default="NON", + max_length=3, + ), + ), + ( + "life_dice", + models.PositiveSmallIntegerField( + choices=[ + (4, "D4"), + (6, "D6"), + (8, "D8"), + (10, "D10"), + (12, "D12"), + (20, "D20"), + ] + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Race", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Weapon", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ("damage", models.CharField(blank=True, max_length=50)), + ("special", models.TextField(blank=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RacialCapability", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ("description", models.TextField()), + ( + "race", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="character.race" + ), + ), + ], + options={ + "verbose_name_plural": "Racial capabilities", + }, + ), + migrations.CreateModel( + name="Path", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "category", + models.CharField( + choices=[ + ("profile", "Profile"), + ("race", "Race"), + ("prestige", "Prestige"), + ], + max_length=20, + ), + ), + ("notes", models.TextField()), + ( + "profile", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="paths", + to="character.profile", + ), + ), + ( + "race", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="paths", + to="character.race", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Character", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("level", models.PositiveSmallIntegerField()), + ( + "gender", + models.CharField( + choices=[("M", "Male"), ("F", "Female"), ("O", "Other")], + default="O", + max_length=1, + ), + ), + ("age", models.PositiveSmallIntegerField()), + ("height", models.PositiveSmallIntegerField()), + ("weight", models.PositiveSmallIntegerField()), + ("value_strength", models.PositiveSmallIntegerField()), + ("value_dexterity", models.PositiveSmallIntegerField()), + ("value_constitution", models.PositiveSmallIntegerField()), + ("value_intelligence", models.PositiveSmallIntegerField()), + ("value_wisdom", models.PositiveSmallIntegerField()), + ("value_charisma", models.PositiveSmallIntegerField()), + ("health_max", models.PositiveSmallIntegerField()), + ("health_remaining", models.PositiveSmallIntegerField()), + ("armor", models.PositiveSmallIntegerField()), + ("shield", models.PositiveSmallIntegerField()), + ("defense_misc", models.SmallIntegerField()), + ("equipment", models.TextField()), + ("luck_points_max", models.PositiveSmallIntegerField()), + ("luck_points_remaining", models.PositiveSmallIntegerField()), + ("mana_max", models.PositiveSmallIntegerField()), + ("mana_remaining", models.PositiveSmallIntegerField()), + ("notes", models.TextField()), + ("capabilities", models.ManyToManyField(to="character.capability")), + ( + "player", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="characters", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="characters", + to="character.profile", + ), + ), + ( + "race", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="characters", + to="character.race", + ), + ), + ( + "racial_capability", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="characters", + to="character.racialcapability", + ), + ), + ("weapons", models.ManyToManyField(to="character.weapon")), + ], + ), + migrations.AddField( + model_name="capability", + name="path", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="character.path" + ), + ), + migrations.AddConstraint( + model_name="character", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + models.F("player"), + name="unique_character_player", + ), + ), + migrations.AddConstraint( + model_name="capability", + constraint=models.UniqueConstraint( + models.F("path"), models.F("rank"), name="unique_path_rank" + ), + ), + ] diff --git a/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py b/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py new file mode 100644 index 0000000..2d57df4 --- /dev/null +++ b/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.1.2 on 2022-10-28 21:55 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("character", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="character", + name="capabilities", + field=models.ManyToManyField(blank=True, to="character.capability"), + ), + migrations.AlterField( + model_name="character", + name="weapons", + field=models.ManyToManyField(blank=True, to="character.weapon"), + ), + migrations.AlterField( + model_name="path", + name="profile", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="paths", + to="character.profile", + ), + ), + migrations.AlterField( + model_name="path", + name="race", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="paths", + to="character.race", + ), + ), + ] diff --git a/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py b/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py new file mode 100644 index 0000000..804bab8 --- /dev/null +++ b/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.2 on 2022-10-28 21:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "character", + "0002_alter_character_capabilities_alter_character_weapons_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="character", + name="equipment", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="character", + name="notes", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="path", + name="notes", + field=models.TextField(blank=True), + ), + ] diff --git a/src/character/migrations/0004_remove_character_mana_max_and_more.py b/src/character/migrations/0004_remove_character_mana_max_and_more.py new file mode 100644 index 0000000..ba39866 --- /dev/null +++ b/src/character/migrations/0004_remove_character_mana_max_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.2 on 2022-10-28 21:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("character", "0003_alter_character_equipment_alter_character_notes_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="character", + name="mana_max", + ), + migrations.RemoveField( + model_name="character", + name="mana_remaining", + ), + migrations.AddField( + model_name="character", + name="mana_consumed", + field=models.PositiveSmallIntegerField(default=0), + preserve_default=False, + ), + ] diff --git a/src/character/migrations/__init__.py b/src/character/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/character/migrations/max_migration.txt b/src/character/migrations/max_migration.txt new file mode 100644 index 0000000..e063c8b --- /dev/null +++ b/src/character/migrations/max_migration.txt @@ -0,0 +1 @@ +0004_remove_character_mana_max_and_more diff --git a/src/character/models/__init__.py b/src/character/models/__init__.py new file mode 100644 index 0000000..2dde838 --- /dev/null +++ b/src/character/models/__init__.py @@ -0,0 +1,13 @@ +from .capabilities import Capability, Path, RacialCapability +from .character import Character, Profile, Race +from .equipment import Weapon + +__all__ = [ + "Capability", + "Path", + "RacialCapability", + "Character", + "Profile", + "Race", + "Weapon", +] diff --git a/src/character/models/capabilities.py b/src/character/models/capabilities.py new file mode 100644 index 0000000..5174beb --- /dev/null +++ b/src/character/models/capabilities.py @@ -0,0 +1,52 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django_extensions.db.models import TimeStampedModel + +from common.models import UniquelyNamedModel + + +class Path(UniquelyNamedModel, TimeStampedModel, models.Model): + profile = models.ForeignKey( + "character.Profile", + on_delete=models.CASCADE, + related_name="paths", + blank=True, + null=True, + ) + race = models.ForeignKey( + "character.Race", + on_delete=models.CASCADE, + related_name="paths", + blank=True, + null=True, + ) + + class Category(models.TextChoices): + PROFILE = "profile", "Profile" + RACE = "race", "Race" + PRESTIGE = "prestige", "Prestige" + + category = models.CharField(max_length=20, choices=Category.choices) + notes = models.TextField(blank=True) + + +class Capability(UniquelyNamedModel, TimeStampedModel, models.Model): + path = models.ForeignKey("character.Path", on_delete=models.CASCADE) + rank = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(5)] + ) + limited = models.BooleanField(blank=True, null=False) + spell = models.BooleanField(blank=True, null=False) + description = models.TextField() + + class Meta: + constraints = [models.UniqueConstraint("path", "rank", name="unique_path_rank")] + verbose_name_plural = "Capabilities" + + +class RacialCapability(UniquelyNamedModel, TimeStampedModel, models.Model): + race = models.ForeignKey("character.Race", on_delete=models.CASCADE) + description = models.TextField() + + class Meta: + verbose_name_plural = "Racial capabilities" diff --git a/src/character/models/character.py b/src/character/models/character.py new file mode 100644 index 0000000..0f2179d --- /dev/null +++ b/src/character/models/character.py @@ -0,0 +1,176 @@ +from django.db import models +from django.db.models.functions import Lower +from django_extensions.db.models import TimeStampedModel + +from character.models.dice import Dice +from common.models import UniquelyNamedModel + + +class Profile(UniquelyNamedModel, TimeStampedModel, models.Model): + class MagicalStrength(models.TextChoices): + NONE = "NON", "None" + INTELLIGENCE = "INT", "Intelligence" + WISDOM = "WIS", "Wisdom" + CHARISMA = "CHA", "Charisma" + + magical_strength = models.CharField( + max_length=3, choices=MagicalStrength.choices, default=MagicalStrength.NONE + ) + life_dice = models.PositiveSmallIntegerField(choices=Dice.choices) + + +class Race(UniquelyNamedModel, TimeStampedModel, models.Model): + pass + + +def modifier(value: int) -> int: + if 1 < value < 10: + value -= 1 + value -= 10 + return int(value / 2) + + +class CharacterManager(models.Manager): + def get_by_natural_key(self, name: str, player_id: int): + return self.get(name=name, player_id=player_id) + + +class Character(models.Model): + class Gender(models.TextChoices): + MALE = "M", "Male" + FEMALE = "F", "Female" + OTHER = "O", "Other" + + name = models.CharField(max_length=100) + player = models.ForeignKey( + "common.User", on_delete=models.CASCADE, related_name="characters" + ) + + race = models.ForeignKey( + "character.Race", + on_delete=models.PROTECT, + related_name="characters", + ) + profile = models.ForeignKey( + "character.Profile", + on_delete=models.PROTECT, + related_name="characters", + ) + level = models.PositiveSmallIntegerField() + + gender = models.CharField( + max_length=1, choices=Gender.choices, default=Gender.OTHER + ) + age = models.PositiveSmallIntegerField() + height = models.PositiveSmallIntegerField() + weight = models.PositiveSmallIntegerField() + + value_strength = models.PositiveSmallIntegerField() + value_dexterity = models.PositiveSmallIntegerField() + value_constitution = models.PositiveSmallIntegerField() + value_intelligence = models.PositiveSmallIntegerField() + value_wisdom = models.PositiveSmallIntegerField() + value_charisma = models.PositiveSmallIntegerField() + + health_max = models.PositiveSmallIntegerField() + health_remaining = models.PositiveSmallIntegerField() + + racial_capability = models.ForeignKey( + "character.RacialCapability", + on_delete=models.PROTECT, + related_name="characters", + ) + + weapons = models.ManyToManyField("character.Weapon", blank=True) + + armor = models.PositiveSmallIntegerField() + shield = models.PositiveSmallIntegerField() + defense_misc = models.SmallIntegerField() + + capabilities = models.ManyToManyField("character.Capability", blank=True) + + equipment = models.TextField(blank=True) + luck_points_max = models.PositiveSmallIntegerField() + luck_points_remaining = models.PositiveSmallIntegerField() + + mana_consumed = models.PositiveSmallIntegerField(default=0) + + notes = models.TextField(blank=True) + + objects = CharacterManager() + + class Meta: + constraints = [ + models.UniqueConstraint( + Lower("name"), "player", name="unique_character_player" + ) + ] + + def __str__(self): + return self.name + + @property + def natural_key(self): + return (self.name, self.player_id) + + @property + def modifier_strength(self) -> int: + return modifier(self.value_strength) + + @property + def modifier_dexterity(self) -> int: + return modifier(self.value_dexterity) + + @property + def modifier_constitution(self) -> int: + return modifier(self.value_constitution) + + @property + def modifier_intelligence(self) -> int: + return modifier(self.value_intelligence) + + @property + def modifier_wisdom(self) -> int: + return modifier(self.value_wisdom) + + @property + def modifier_charisma(self) -> int: + return modifier(self.value_charisma) + + @property + def initiative(self) -> int: + return self.value_dexterity + + @property + def attack_melee(self) -> int: + return self.level + self.modifier_strength + + @property + def attack_range(self) -> int: + return self.level + self.modifier_dexterity + + @property + def attack_magic(self) -> int: + modifier_map = { + Profile.MagicalStrength.INTELLIGENCE: self.modifier_intelligence, + Profile.MagicalStrength.WISDOM: self.modifier_wisdom, + Profile.MagicalStrength.CHARISMA: self.modifier_charisma, + } + + return self.level + modifier_map.get( + Profile.MagicalStrength(self.profile.magical_strength) + ) + + @property + def defense(self) -> int: + return ( + 10 + self.armor + self.shield + self.modifier_dexterity + self.defense_misc + ) + + @property + def mana_max(self) -> int: + return 2 * self.level + self.modifier_intelligence + + @property + def mana_remaining(self) -> int: + return self.mana_max - self.mana_consumed diff --git a/src/character/models/dice.py b/src/character/models/dice.py new file mode 100644 index 0000000..98bbe34 --- /dev/null +++ b/src/character/models/dice.py @@ -0,0 +1,10 @@ +from django.db import models + + +class Dice(models.IntegerChoices): + D4 = 4 + D6 = 6 + D8 = 8 + D10 = 10 + D12 = 12 + D20 = 20 diff --git a/src/character/models/equipment.py b/src/character/models/equipment.py new file mode 100644 index 0000000..c471597 --- /dev/null +++ b/src/character/models/equipment.py @@ -0,0 +1,9 @@ +from django.db import models +from django_extensions.db.models import TimeStampedModel + +from common.models import UniquelyNamedModel + + +class Weapon(UniquelyNamedModel, TimeStampedModel, models.Model): + damage = models.CharField(max_length=50, blank=True) + special = models.TextField(blank=True) diff --git a/src/character/tests/__init__.py b/src/character/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/character/tests/test_modifier.py b/src/character/tests/test_modifier.py new file mode 100644 index 0000000..26b575d --- /dev/null +++ b/src/character/tests/test_modifier.py @@ -0,0 +1,33 @@ +import pytest + +from character.models import modifier + + +@pytest.mark.parametrize( + "value,expected", + [ + (1, -4), + (2, -4), + (3, -4), + (4, -3), + (5, -3), + (6, -2), + (7, -2), + (8, -1), + (9, -1), + (10, 0), + (11, 0), + (12, 1), + (13, 1), + (14, 2), + (15, 2), + (16, 3), + (17, 3), + (18, 4), + (19, 4), + (20, 5), + (21, 5), + ], +) +def test_modifier_values(value, expected): + assert modifier(value) == expected diff --git a/src/character/views.py b/src/character/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/src/character/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/src/charasheet/settings.py b/src/charasheet/settings.py index 462a4be..75bc31d 100644 --- a/src/charasheet/settings.py +++ b/src/charasheet/settings.py @@ -60,6 +60,7 @@ if DEBUG_TOOLBAR: CUSTOM_APPS = [ "whitenoise.runserver_nostatic", # should be first "common", + "character", ] INSTALLED_APPS = CUSTOM_APPS + DJANGO_APPS + EXTERNAL_APPS diff --git a/src/common/models.py b/src/common/models.py index dab8651..dc615e5 100644 --- a/src/common/models.py +++ b/src/common/models.py @@ -1,7 +1,28 @@ from django.contrib.auth.models import AbstractUser +from django.db import models class User(AbstractUser): """Default custom user model for My Awesome Project.""" pass + + +class UniquelyNamedModelManager(models.Manager): + def get_by_natural_key(self, name: str): + return self.get(name=name) + + +class UniquelyNamedModel(models.Model): + name = models.CharField(max_length=100, blank=False, null=False, unique=True) + objects = UniquelyNamedModelManager() + + class Meta: + abstract = True + + def __str__(self): + return self.name + + @property + def natural_key(self): + return (self.name,)