Enable more ruff rules

This commit is contained in:
Gabriel Augendre 2023-02-28 12:34:45 +01:00
parent bcabc15054
commit 16d7ff5d20
38 changed files with 394 additions and 215 deletions

View file

@ -36,50 +36,12 @@ python_files = [
[tool.ruff] [tool.ruff]
src = ["src"] src = ["src"]
target-version = "py311" target-version = "py311"
select = [ select = ["ALL"]
"F", # pyflakes
"E", "W", # pycodestyle
"C90", # mccabe
"I", # isort
"N", # pep8-naming
"D", # pydocstyle
"S", # flake8-bandit
"FBT", # flake8-boolean-trap
"B", # flake8-bugbear
"A", # flake8-builtins
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"T10", # flake8-debugger
"EXE", # flake8-executable
"ISC", # flake8-implicit-str-concat
"ICN", # flake8-import-conventions
"G", # flake8-logging-format
"INP", # flake8-no-pep420
"PIE", # flake8-pie
"T20", # flake8-print
"PT", # flake8-pytest-style
"RET", # flake8-return
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"ERA", # eradicate
"PD", # pandas-vet
"PGH", # pygrep-hooks
"PL", # pylint
"TRY", # tryceratops
"RUF", # ruff-specific rules
]
unfixable = ["T20", "RUF001", "RUF002", "RUF003"] unfixable = ["T20", "RUF001", "RUF002", "RUF003"]
ignore = [ ignore = [
"UP", # pyupgrade
"YTT", # flake8-2020
"ANN", # flake8-annotations "ANN", # flake8-annotations
"BLE", # flake8-blind-except "BLE", # flake8-blind-except
"COM", # flake8-commas
"EM", # flake8-errmsg
"Q", # flake8-quotes
"TCH", # flake8-type-checking / TODO: revisit later ? "TCH", # flake8-type-checking / TODO: revisit later ?
"E501", # long lines "E501", # long lines

View file

@ -92,12 +92,14 @@ class RaceAdmin(admin.ModelAdmin):
class CharacterAdminForm(ModelForm): class CharacterAdminForm(ModelForm):
class Meta: class Meta:
model = models.Character model = models.Character
exclude = () exclude = () # noqa: DJ006
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["capabilities"].queryset = models.Capability.objects.select_related( self.fields["capabilities"].queryset = models.Capability.objects.select_related(
"path", "path__race", "path__profile" "path",
"path__race",
"path__profile",
) )
self.fields[ self.fields[
"racial_capability" "racial_capability"
@ -127,7 +129,7 @@ class CharacterAdmin(admin.ModelAdmin):
"level", "level",
"race", "race",
"private", "private",
] ],
}, },
), ),
("Apparence", {"fields": ["gender", "age", "height", "weight"]}), ("Apparence", {"fields": ["gender", "age", "height", "weight"]}),
@ -141,7 +143,7 @@ class CharacterAdmin(admin.ModelAdmin):
("value_intelligence", "modifier_intelligence"), ("value_intelligence", "modifier_intelligence"),
("value_wisdom", "modifier_wisdom"), ("value_wisdom", "modifier_wisdom"),
("value_charisma", "modifier_charisma"), ("value_charisma", "modifier_charisma"),
] ],
}, },
), ),
( (
@ -153,7 +155,7 @@ class CharacterAdmin(admin.ModelAdmin):
"attack_range", "attack_range",
"attack_magic", "attack_magic",
"states", "states",
] ],
}, },
), ),
("Vitalité", {"fields": [("health_max", "health_remaining")]}), ("Vitalité", {"fields": [("health_max", "health_remaining")]}),
@ -165,7 +167,7 @@ class CharacterAdmin(admin.ModelAdmin):
"weapons", "weapons",
"equipment", "equipment",
("money_pp", "money_po", "money_pa", "money_pc"), ("money_pp", "money_po", "money_pa", "money_pc"),
] ],
}, },
), ),
("Race", {"fields": ["racial_capability"]}), ("Race", {"fields": ["racial_capability"]}),

View file

@ -18,10 +18,12 @@ class AddPathForm(forms.Form):
empty_label="----- Voies liées au personnage -----", empty_label="----- Voies liées au personnage -----",
) )
other_path = forms.ModelChoiceField( other_path = forms.ModelChoiceField(
Path.objects.none(), required=False, empty_label="----- Autres voies -----" Path.objects.none(),
required=False,
empty_label="----- Autres voies -----",
) )
def __init__(self, character: Character, *args, **kwargs): def __init__(self, character: Character, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
paths = {cap.path_id for cap in character.capabilities.all()} paths = {cap.path_id for cap in character.capabilities.all()}
paths = ( paths = (
@ -30,12 +32,12 @@ class AddPathForm(forms.Form):
.select_related("profile", "race") .select_related("profile", "race")
) )
character_paths = paths.filter( character_paths = paths.filter(
Q(profile=character.profile) | Q(race=character.race) Q(profile=character.profile) | Q(race=character.race),
) )
self.fields["character_path"].queryset = character_paths self.fields["character_path"].queryset = character_paths
self.fields["character_path"].widget.attrs["class"] = "form-select" self.fields["character_path"].widget.attrs["class"] = "form-select"
self.fields["other_path"].queryset = paths.exclude( self.fields["other_path"].queryset = paths.exclude(
pk__in={path.pk for path in character_paths} pk__in={path.pk for path in character_paths},
) )
self.fields["other_path"].widget.attrs["class"] = "form-select" self.fields["other_path"].widget.attrs["class"] = "form-select"
@ -43,12 +45,13 @@ class AddPathForm(forms.Form):
cleaned_data = super().clean() cleaned_data = super().clean()
values = [cleaned_data.get("character_path"), cleaned_data.get("other_path")] values = [cleaned_data.get("character_path"), cleaned_data.get("other_path")]
if len(list(filter(None, values))) != 1: if len(list(filter(None, values))) != 1:
raise ValidationError("Vous devez sélectionner une seule valeur.") msg = "Vous devez sélectionner une seule valeur."
raise ValidationError(msg)
return cleaned_data return cleaned_data
class CharacterCreateForm(forms.ModelForm): class CharacterCreateForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields[ self.fields[
"racial_capability" "racial_capability"

View file

@ -17,7 +17,8 @@ class Command(BaseCommand):
while len(cards) < expected_capability_count: while len(cards) < expected_capability_count:
self.selenium.find_element(By.TAG_NAME, "body").send_keys(Keys.END) self.selenium.find_element(By.TAG_NAME, "body").send_keys(Keys.END)
cards = self.selenium.find_elements( cards = self.selenium.find_elements(
By.CSS_SELECTOR, ".col-md-4.col-sm-6.col-12.mb-4.views-row" By.CSS_SELECTOR,
".col-md-4.col-sm-6.col-12.mb-4.views-row",
) )
for card in cards: for card in cards:
try: try:
@ -63,11 +64,11 @@ class Command(BaseCommand):
}, },
) )
self.stdout.write( self.stdout.write(
self.style.SUCCESS(f"Created/updated cap {capability}") self.style.SUCCESS(f"Created/updated cap {capability}"),
) )
except Exception as e: except Exception as e:
self.stdout.write( self.stdout.write(
self.style.ERROR(f"Couldn't create/update cap {name}: {e}") self.style.ERROR(f"Couldn't create/update cap {name}: {e}"),
) )
def get_paths(self, card: WebElement, name: str) -> list[Path]: def get_paths(self, card: WebElement, name: str) -> list[Path]:
@ -78,7 +79,7 @@ class Command(BaseCommand):
paths.append(Path.objects.get(name__iexact=path_name)) paths.append(Path.objects.get(name__iexact=path_name))
except Exception: except Exception:
self.stdout.write( self.stdout.write(
self.style.WARNING(f"Couldn't find path in card for cap '{name}'.") self.style.WARNING(f"Couldn't find path in card for cap '{name}'."),
) )
return [] return []
return paths return paths

View file

@ -22,10 +22,12 @@ class Command(BaseCommand):
def import_row(self, url: str, state_row: WebElement) -> None: def import_row(self, url: str, state_row: WebElement) -> None:
name = state_row.find_element(By.CLASS_NAME, "views-field-name").text.strip() name = state_row.find_element(By.CLASS_NAME, "views-field-name").text.strip()
description = state_row.find_element( description = state_row.find_element(
By.CLASS_NAME, "views-field-description__value" By.CLASS_NAME,
"views-field-description__value",
).text.strip() ).text.strip()
icon_url = state_row.find_element( icon_url = state_row.find_element(
By.CSS_SELECTOR, ".views-field-field-svg-icon img" By.CSS_SELECTOR,
".views-field-field-svg-icon img",
).get_attribute("src") ).get_attribute("src")
state, _ = HarmfulState.objects.update_or_create( state, _ = HarmfulState.objects.update_or_create(
name=name, name=name,

View file

@ -16,7 +16,8 @@ class Command(BaseCommand):
while len(anchors) < expected_path_count: while len(anchors) < expected_path_count:
self.selenium.find_element(By.TAG_NAME, "body").send_keys(Keys.END) self.selenium.find_element(By.TAG_NAME, "body").send_keys(Keys.END)
anchors = self.selenium.find_elements( anchors = self.selenium.find_elements(
By.CSS_SELECTOR, ".card-body .card-title a" By.CSS_SELECTOR,
".card-body .card-title a",
) )
urls = [anchor.get_attribute("href") for anchor in anchors] urls = [anchor.get_attribute("href") for anchor in anchors]
for url in urls: for url in urls:
@ -57,7 +58,8 @@ class Command(BaseCommand):
try: try:
category = ( category = (
self.selenium.find_element( self.selenium.find_element(
By.CSS_SELECTOR, ".field--name-type .field__item" By.CSS_SELECTOR,
".field--name-type .field__item",
) )
.text.lower() .text.lower()
.strip() .strip()
@ -65,8 +67,8 @@ class Command(BaseCommand):
except Exception: except Exception:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f"Couldn't find category for {name}. Defaulting to profile." f"Couldn't find category for {name}. Defaulting to profile.",
) ),
) )
return Path.Category.PROFILE return Path.Category.PROFILE
@ -79,7 +81,8 @@ class Command(BaseCommand):
def get_profile(self, name: str) -> Profile | None: def get_profile(self, name: str) -> Profile | None:
try: try:
profile_name = self.selenium.find_element( profile_name = self.selenium.find_element(
By.CSS_SELECTOR, ".field--name-type + strong + a" By.CSS_SELECTOR,
".field--name-type + strong + a",
).text ).text
except Exception: except Exception:
self.stdout.write(self.style.WARNING(f"Couldn't find profile for {name}")) self.stdout.write(self.style.WARNING(f"Couldn't find profile for {name}"))
@ -99,7 +102,8 @@ class Command(BaseCommand):
def get_notes(self) -> str: def get_notes(self) -> str:
try: try:
return self.selenium.find_element( return self.selenium.find_element(
By.CSS_SELECTOR, ".mt-3 > .field--name-description" By.CSS_SELECTOR,
".mt-3 > .field--name-description",
).text.strip() ).text.strip()
except Exception: except Exception:
return "" return ""

View file

@ -41,19 +41,20 @@ class Command(BaseCommand):
def get_dice(self, name: str) -> Dice: def get_dice(self, name: str) -> Dice:
dice = self.selenium.find_element(By.CSS_SELECTOR, ".dice + div").text.split( dice = self.selenium.find_element(By.CSS_SELECTOR, ".dice + div").text.split(
"D" "D",
) )
number_of_dice, dice_value = int(dice[0]), int(dice[1]) number_of_dice, dice_value = int(dice[0]), int(dice[1])
if number_of_dice != 1: if number_of_dice != 1:
self.stdout.write( self.stdout.write(
self.style.WARNING(f"Multiple dice for {name}: {number_of_dice}") self.style.WARNING(f"Multiple dice for {name}: {number_of_dice}"),
) )
return Dice(dice_value) return Dice(dice_value)
def get_magical_strength(self) -> Profile.MagicalStrength: def get_magical_strength(self) -> Profile.MagicalStrength:
try: try:
magical_strength = self.selenium.find_element( magical_strength = self.selenium.find_element(
By.CSS_SELECTOR, ".field--name-magic-attack-modifier .field__item" By.CSS_SELECTOR,
".field--name-magic-attack-modifier .field__item",
).text ).text
magical_strength = Profile.MagicalStrength(magical_strength) magical_strength = Profile.MagicalStrength(magical_strength)
except Exception: except Exception:

View file

@ -27,7 +27,8 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(f"Created/updated race {race}")) self.stdout.write(self.style.SUCCESS(f"Created/updated race {race}"))
racial_cap = self.selenium.find_element( racial_cap = self.selenium.find_element(
By.CSS_SELECTOR, ".field--name-abilities" By.CSS_SELECTOR,
".field--name-abilities",
) )
racial_name = ( racial_name = (
racial_cap.find_element(By.TAG_NAME, "strong") racial_cap.find_element(By.TAG_NAME, "strong")

View file

@ -32,13 +32,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
( (
@ -47,7 +49,7 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(1), django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5), django.core.validators.MaxValueValidator(5),
] ],
), ),
), ),
("limited", models.BooleanField(blank=True)), ("limited", models.BooleanField(blank=True)),
@ -74,13 +76,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
( (
@ -106,7 +110,7 @@ class Migration(migrations.Migration):
(10, "D10"), (10, "D10"),
(12, "D12"), (12, "D12"),
(20, "D20"), (20, "D20"),
] ],
), ),
), ),
], ],
@ -130,13 +134,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
], ],
@ -160,13 +166,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
("damage", models.CharField(blank=True, max_length=50)), ("damage", models.CharField(blank=True, max_length=50)),
@ -192,20 +200,23 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
("description", models.TextField()), ("description", models.TextField()),
( (
"race", "race",
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="character.race" on_delete=django.db.models.deletion.CASCADE,
to="character.race",
), ),
), ),
], ],
@ -229,13 +240,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
( (
@ -355,7 +368,8 @@ class Migration(migrations.Migration):
model_name="capability", model_name="capability",
name="path", name="path",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="character.path" on_delete=django.db.models.deletion.CASCADE,
to="character.path",
), ),
), ),
migrations.AddConstraint( migrations.AddConstraint(
@ -369,7 +383,9 @@ class Migration(migrations.Migration):
migrations.AddConstraint( migrations.AddConstraint(
model_name="capability", model_name="capability",
constraint=models.UniqueConstraint( constraint=models.UniqueConstraint(
models.F("path"), models.F("rank"), name="unique_path_rank" models.F("path"),
models.F("rank"),
name="unique_path_rank",
), ),
), ),
] ]

View file

@ -17,7 +17,9 @@ class Migration(migrations.Migration):
migrations.AddConstraint( migrations.AddConstraint(
model_name="racialcapability", model_name="racialcapability",
constraint=models.UniqueConstraint( constraint=models.UniqueConstraint(
models.F("name"), models.F("race"), name="unique_name_race" models.F("name"),
models.F("race"),
name="unique_name_race",
), ),
), ),
] ]

View file

@ -56,7 +56,9 @@ class Migration(migrations.Migration):
model_name="capability", model_name="capability",
name="limited", name="limited",
field=models.BooleanField( field=models.BooleanField(
blank=True, default=False, verbose_name="limitée" blank=True,
default=False,
verbose_name="limitée",
), ),
), ),
migrations.AlterField( migrations.AlterField(
@ -108,7 +110,9 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="capabilities", name="capabilities",
field=models.ManyToManyField( field=models.ManyToManyField(
blank=True, to="character.capability", verbose_name="capacités" blank=True,
to="character.capability",
verbose_name="capacités",
), ),
), ),
migrations.AlterField( migrations.AlterField(
@ -140,7 +144,7 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="health_remaining", name="health_remaining",
field=models.PositiveSmallIntegerField( field=models.PositiveSmallIntegerField(
verbose_name="points de vie restants" verbose_name="points de vie restants",
), ),
), ),
migrations.AlterField( migrations.AlterField(
@ -162,14 +166,15 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="luck_points_remaining", name="luck_points_remaining",
field=models.PositiveSmallIntegerField( field=models.PositiveSmallIntegerField(
verbose_name="points de chance restants" verbose_name="points de chance restants",
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="character", model_name="character",
name="mana_consumed", name="mana_consumed",
field=models.PositiveSmallIntegerField( field=models.PositiveSmallIntegerField(
default=0, verbose_name="mana utilisé" default=0,
verbose_name="mana utilisé",
), ),
), ),
migrations.AlterField( migrations.AlterField(
@ -261,7 +266,9 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="weapons", name="weapons",
field=models.ManyToManyField( field=models.ManyToManyField(
blank=True, to="character.weapon", verbose_name="armes" blank=True,
to="character.weapon",
verbose_name="armes",
), ),
), ),
migrations.AlterField( migrations.AlterField(

View file

@ -17,7 +17,8 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="mana_remaining", name="mana_remaining",
field=models.PositiveSmallIntegerField( field=models.PositiveSmallIntegerField(
default=0, verbose_name="mana restant" default=0,
verbose_name="mana restant",
), ),
), ),
] ]

View file

@ -13,7 +13,8 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="recovery_points_remaining", name="recovery_points_remaining",
field=models.PositiveSmallIntegerField( field=models.PositiveSmallIntegerField(
default=5, verbose_name="points de récupération restants" default=5,
verbose_name="points de récupération restants",
), ),
), ),
] ]

View file

@ -30,13 +30,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
("description", models.TextField()), ("description", models.TextField()),

View file

@ -13,7 +13,8 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="states", name="states",
field=models.ManyToManyField( field=models.ManyToManyField(
related_name="characters", to="character.harmfulstate" related_name="characters",
to="character.harmfulstate",
), ),
), ),
] ]

View file

@ -13,7 +13,9 @@ class Migration(migrations.Migration):
model_name="character", model_name="character",
name="states", name="states",
field=models.ManyToManyField( field=models.ManyToManyField(
blank=True, related_name="characters", to="character.harmfulstate" blank=True,
related_name="characters",
to="character.harmfulstate",
), ),
), ),
] ]

View file

@ -32,13 +32,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
( (

View file

@ -25,7 +25,7 @@ class Migration(migrations.Migration):
character.models.character.validate_image, character.models.character.validate_image,
*(), *(),
**{"megabytes_limit": 2}, **{"megabytes_limit": 2},
) ),
], ],
verbose_name="image de profil", verbose_name="image de profil",
), ),

View file

@ -32,7 +32,9 @@ class Path(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model):
CREATURE = "creature", "Créature" CREATURE = "creature", "Créature"
category = models.CharField( category = models.CharField(
max_length=20, choices=Category.choices, verbose_name="catégorie" max_length=20,
choices=Category.choices,
verbose_name="catégorie",
) )
notes = models.TextField(blank=True, verbose_name="notes") notes = models.TextField(blank=True, verbose_name="notes")
@ -93,13 +95,20 @@ class Capability(DocumentedModel, TimeStampedModel, models.Model):
related_name="capabilities", related_name="capabilities",
) )
rank = models.PositiveSmallIntegerField( rank = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)], verbose_name="rang" validators=[MinValueValidator(1), MaxValueValidator(5)],
verbose_name="rang",
) )
limited = models.BooleanField( limited = models.BooleanField(
blank=True, null=False, default=False, verbose_name="limitée" blank=True,
null=False,
default=False,
verbose_name="limitée",
) )
spell = models.BooleanField( spell = models.BooleanField(
blank=True, null=False, default=False, verbose_name="sort" blank=True,
null=False,
default=False,
verbose_name="sort",
) )
description = models.TextField(verbose_name="description") description = models.TextField(verbose_name="description")
@ -134,7 +143,9 @@ class RacialCapabilityManager(models.Manager):
class RacialCapability(DocumentedModel, TimeStampedModel, models.Model): class RacialCapability(DocumentedModel, TimeStampedModel, models.Model):
name = models.CharField(max_length=100, blank=False, null=False, verbose_name="nom") name = models.CharField(max_length=100, blank=False, null=False, verbose_name="nom")
race = models.ForeignKey( race = models.ForeignKey(
"character.Race", on_delete=models.CASCADE, verbose_name="race" "character.Race",
on_delete=models.CASCADE,
verbose_name="race",
) )
description = models.TextField(verbose_name="description") description = models.TextField(verbose_name="description")

View file

@ -17,7 +17,12 @@ from character.models.equipment import Weapon
from common.models import DocumentedModel, UniquelyNamedModel from common.models import DocumentedModel, UniquelyNamedModel
class Profile(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model): class Profile( # noqa: DJ008
DocumentedModel,
UniquelyNamedModel,
TimeStampedModel,
models.Model,
):
class MagicalStrength(models.TextChoices): class MagicalStrength(models.TextChoices):
NONE = "NON", "Aucun" NONE = "NON", "Aucun"
INTELLIGENCE = "INT", "Intelligence" INTELLIGENCE = "INT", "Intelligence"
@ -36,10 +41,13 @@ class Profile(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Mode
verbose_name="force magique", verbose_name="force magique",
) )
life_dice = models.PositiveSmallIntegerField( life_dice = models.PositiveSmallIntegerField(
choices=Dice.choices, verbose_name="dé de vie" choices=Dice.choices,
verbose_name="dé de vie",
) )
mana_max_compute = models.PositiveSmallIntegerField( mana_max_compute = models.PositiveSmallIntegerField(
choices=ManaMax.choices, verbose_name="calcul mana max", default=ManaMax.NO_MANA choices=ManaMax.choices,
verbose_name="calcul mana max",
default=ManaMax.NO_MANA,
) )
notes = models.TextField(blank=True, verbose_name="notes") notes = models.TextField(blank=True, verbose_name="notes")
@ -48,13 +56,23 @@ class Profile(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Mode
verbose_name_plural = "Profils" verbose_name_plural = "Profils"
class Race(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model): class Race( # noqa: DJ008
DocumentedModel,
UniquelyNamedModel,
TimeStampedModel,
models.Model,
):
class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta): class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta):
verbose_name = "Race" verbose_name = "Race"
verbose_name_plural = "Races" verbose_name_plural = "Races"
class HarmfulState(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model): class HarmfulState( # noqa: DJ008
DocumentedModel,
UniquelyNamedModel,
TimeStampedModel,
models.Model,
):
description = models.TextField() description = models.TextField()
icon_url = models.URLField() icon_url = models.URLField()
@ -88,7 +106,7 @@ class CharacterQuerySet(models.QuerySet):
from party.models import Party from party.models import Party
return self.filter( return self.filter(
Q(player=user) | Q(parties__in=Party.objects.managed_by(user)) Q(player=user) | Q(parties__in=Party.objects.managed_by(user)),
) )
def mastered_by(self, user): def mastered_by(self, user):
@ -113,7 +131,7 @@ class CharacterQuerySet(models.QuerySet):
return self.filter( return self.filter(
Q(player=user) Q(player=user)
| Q(parties__in=Party.objects.related_to(user)) | Q(parties__in=Party.objects.related_to(user))
| Q(invites__in=Party.objects.related_to(user)) | Q(invites__in=Party.objects.related_to(user)),
).distinct() ).distinct()
@ -184,7 +202,10 @@ class Character(models.Model):
level = models.PositiveSmallIntegerField(verbose_name="niveau", default=1) level = models.PositiveSmallIntegerField(verbose_name="niveau", default=1)
gender = models.CharField( gender = models.CharField(
max_length=1, choices=Gender.choices, default=Gender.OTHER, verbose_name="genre" max_length=1,
choices=Gender.choices,
default=Gender.OTHER,
verbose_name="genre",
) )
age = models.PositiveSmallIntegerField(verbose_name="âge") age = models.PositiveSmallIntegerField(verbose_name="âge")
height = models.PositiveSmallIntegerField(verbose_name="taille") height = models.PositiveSmallIntegerField(verbose_name="taille")
@ -193,17 +214,17 @@ class Character(models.Model):
value_strength = models.PositiveSmallIntegerField(verbose_name="valeur force") value_strength = models.PositiveSmallIntegerField(verbose_name="valeur force")
value_dexterity = models.PositiveSmallIntegerField(verbose_name="valeur dextérité") value_dexterity = models.PositiveSmallIntegerField(verbose_name="valeur dextérité")
value_constitution = models.PositiveSmallIntegerField( value_constitution = models.PositiveSmallIntegerField(
verbose_name="valeur constitution" verbose_name="valeur constitution",
) )
value_intelligence = models.PositiveSmallIntegerField( value_intelligence = models.PositiveSmallIntegerField(
verbose_name="valeur intelligence" verbose_name="valeur intelligence",
) )
value_wisdom = models.PositiveSmallIntegerField(verbose_name="valeur sagesse") value_wisdom = models.PositiveSmallIntegerField(verbose_name="valeur sagesse")
value_charisma = models.PositiveSmallIntegerField(verbose_name="valeur charisme") value_charisma = models.PositiveSmallIntegerField(verbose_name="valeur charisme")
health_max = models.PositiveSmallIntegerField(verbose_name="points de vie max") health_max = models.PositiveSmallIntegerField(verbose_name="points de vie max")
health_remaining = models.PositiveSmallIntegerField( health_remaining = models.PositiveSmallIntegerField(
verbose_name="points de vie restants" verbose_name="points de vie restants",
) )
racial_capability = models.ForeignKey( racial_capability = models.ForeignKey(
@ -214,7 +235,9 @@ class Character(models.Model):
) )
weapons = models.ManyToManyField( weapons = models.ManyToManyField(
"character.Weapon", blank=True, verbose_name="armes" "character.Weapon",
blank=True,
verbose_name="armes",
) )
armor = models.PositiveSmallIntegerField(verbose_name="armure", default=0) armor = models.PositiveSmallIntegerField(verbose_name="armure", default=0)
@ -222,20 +245,24 @@ class Character(models.Model):
defense_misc = models.SmallIntegerField(verbose_name="divers défense", default=0) defense_misc = models.SmallIntegerField(verbose_name="divers défense", default=0)
initiative_misc = models.SmallIntegerField( initiative_misc = models.SmallIntegerField(
verbose_name="divers initiative", default=0 verbose_name="divers initiative",
default=0,
) )
capabilities = models.ManyToManyField( capabilities = models.ManyToManyField(
"character.Capability", blank=True, verbose_name="capacités" "character.Capability",
blank=True,
verbose_name="capacités",
) )
equipment = models.TextField(blank=True, verbose_name="équipement") equipment = models.TextField(blank=True, verbose_name="équipement")
luck_points_remaining = models.PositiveSmallIntegerField( luck_points_remaining = models.PositiveSmallIntegerField(
verbose_name="points de chance restants" verbose_name="points de chance restants",
) )
mana_remaining = models.PositiveSmallIntegerField( mana_remaining = models.PositiveSmallIntegerField(
default=0, verbose_name="mana restant" default=0,
verbose_name="mana restant",
) )
money_pp = models.PositiveSmallIntegerField(default=0, verbose_name="PP") money_pp = models.PositiveSmallIntegerField(default=0, verbose_name="PP")
@ -244,7 +271,8 @@ class Character(models.Model):
money_pc = models.PositiveSmallIntegerField(default=0, verbose_name="PC") money_pc = models.PositiveSmallIntegerField(default=0, verbose_name="PC")
recovery_points_remaining = models.PositiveSmallIntegerField( recovery_points_remaining = models.PositiveSmallIntegerField(
default=5, verbose_name="points de récupération restants" default=5,
verbose_name="points de récupération restants",
) )
notes = models.TextField(blank=True, verbose_name="notes", default=DEFAULT_NOTES) notes = models.TextField(blank=True, verbose_name="notes", default=DEFAULT_NOTES)
@ -267,8 +295,10 @@ class Character(models.Model):
verbose_name_plural = "Personnages" verbose_name_plural = "Personnages"
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
Lower("name"), "player", name="unique_character_player" Lower("name"),
) "player",
name="unique_character_player",
),
] ]
def __str__(self): def __str__(self):
@ -329,7 +359,8 @@ class Character(models.Model):
Profile.MagicalStrength.NONE: 0, Profile.MagicalStrength.NONE: 0,
} }
return modifier_map.get( return modifier_map.get(
Profile.MagicalStrength(self.profile.magical_strength), 0 Profile.MagicalStrength(self.profile.magical_strength),
0,
) )
@property @property
@ -403,8 +434,9 @@ class Character(models.Model):
for capability in path.capabilities.all(): for capability in path.capabilities.all():
capabilities_by_path[capability.path].append( capabilities_by_path[capability.path].append(
CharacterCapability( CharacterCapability(
capability, known=capability in character_capabilities capability,
) known=capability in character_capabilities,
),
) )
return dict( return dict(
@ -414,7 +446,7 @@ class Character(models.Model):
for path, capabilities in capabilities_by_path.items() for path, capabilities in capabilities_by_path.items()
), ),
key=lambda x: x[0].name, key=lambda x: x[0].name,
) ),
) )
def get_formatted_notes(self) -> str: def get_formatted_notes(self) -> str:
@ -427,7 +459,7 @@ class Character(models.Model):
def get_missing_states(self) -> Iterable[HarmfulState]: def get_missing_states(self) -> Iterable[HarmfulState]:
return HarmfulState.objects.exclude( return HarmfulState.objects.exclude(
pk__in=self.states.all().values_list("pk", flat=True) pk__in=self.states.all().values_list("pk", flat=True),
) )
def managed_by(self, user): def managed_by(self, user):

View file

@ -4,7 +4,12 @@ from django_extensions.db.models import TimeStampedModel
from common.models import DocumentedModel, UniquelyNamedModel from common.models import DocumentedModel, UniquelyNamedModel
class Weapon(UniquelyNamedModel, DocumentedModel, TimeStampedModel, models.Model): class Weapon( # noqa: DJ008
UniquelyNamedModel,
DocumentedModel,
TimeStampedModel,
models.Model,
):
class Category(models.TextChoices): class Category(models.TextChoices):
MELEE = "MEL", "corps à corps" MELEE = "MEL", "corps à corps"
RANGE = "RAN", "à distance" RANGE = "RAN", "à distance"
@ -13,7 +18,9 @@ class Weapon(UniquelyNamedModel, DocumentedModel, TimeStampedModel, models.Model
damage = models.CharField(max_length=50, blank=True, verbose_name="dégâts") damage = models.CharField(max_length=50, blank=True, verbose_name="dégâts")
special = models.TextField(blank=True, verbose_name="spécial") special = models.TextField(blank=True, verbose_name="spécial")
category = models.CharField( category = models.CharField(
max_length=3, choices=Category.choices, default=Category.NONE max_length=3,
choices=Category.choices,
default=Category.NONE,
) )
class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta): class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta):

View file

@ -44,7 +44,10 @@ def test_can_access_character_in_party(client):
notes = "Some notes" notes = "Some notes"
gm_notes = "Some GM notes" gm_notes = "Some GM notes"
friend_character = baker.make( friend_character = baker.make(
Character, player=friend, notes=notes, gm_notes=gm_notes Character,
player=friend,
notes=notes,
gm_notes=gm_notes,
) )
party = baker.make(Party) party = baker.make(Party)
party.characters.add(character) party.characters.add(character)

View file

@ -68,7 +68,10 @@ def test_attack_range(level, dexterity):
@given(armor=integers(), shield=integers(), dexterity=ability_values(), misc=integers()) @given(armor=integers(), shield=integers(), dexterity=ability_values(), misc=integers())
def test_defense(armor, shield, dexterity, misc): def test_defense(armor, shield, dexterity, misc):
char = Character( char = Character(
armor=armor, shield=shield, value_dexterity=dexterity, defense_misc=misc armor=armor,
shield=shield,
value_dexterity=dexterity,
defense_misc=misc,
) )
assert char.defense == 10 + armor + shield + modifier_test(dexterity) + misc assert char.defense == 10 + armor + shield + modifier_test(dexterity) + misc

View file

@ -118,7 +118,8 @@ def test_list_characters(selenium: WebDriver, live_server: LiveServer):
names = { names = {
name.text name.text
for name in selenium.find_elements( for name in selenium.find_elements(
By.CSS_SELECTOR, ".character.card .card-title" By.CSS_SELECTOR,
".character.card .card-title",
) )
} }
expected_names = {character.name for character in characters} expected_names = {character.name for character in characters}
@ -137,7 +138,8 @@ def test_delete_character(selenium: WebDriver, live_server: LiveServer):
assert Character.objects.count() == 2 assert Character.objects.count() == 2
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".character.card[data-id='{characters[0].pk}'] .delete" By.CSS_SELECTOR,
f".character.card[data-id='{characters[0].pk}'] .delete",
).click() ).click()
selenium.find_element(By.CSS_SELECTOR, "[type=submit]").click() selenium.find_element(By.CSS_SELECTOR, "[type=submit]").click()
@ -148,7 +150,9 @@ def test_delete_character(selenium: WebDriver, live_server: LiveServer):
@pytest.mark.django_db() @pytest.mark.django_db()
def test_reset_stats_view( def test_reset_stats_view(
selenium: WebDriver, live_server: LiveServer, initial_data: None selenium: WebDriver,
live_server: LiveServer,
initial_data: None,
): ):
username, password = "user", "some_password" username, password = "user", "some_password"
player = User.objects.create_user(username, password=password) player = User.objects.create_user(username, password=password)
@ -186,7 +190,10 @@ def create_hurt_character(player, profile):
def login( def login(
selenium: WebDriver, live_server: LiveServer, username: str, password: str selenium: WebDriver,
live_server: LiveServer,
username: str,
password: str,
) -> None: ) -> None:
selenium.get(live_server.url) selenium.get(live_server.url)
selenium.find_element(By.ID, "login").click() selenium.find_element(By.ID, "login").click()

View file

@ -10,7 +10,9 @@ urlpatterns = [
path("<int:pk>/change/", views.character_change, name="change"), path("<int:pk>/change/", views.character_change, name="change"),
path("<int:pk>/delete/", views.character_delete, name="delete"), path("<int:pk>/delete/", views.character_delete, name="delete"),
path( path(
"<int:pk>/health_change/", views.character_health_change, name="health_change" "<int:pk>/health_change/",
views.character_health_change,
name="health_change",
), ),
path("<int:pk>/mana_change/", views.character_mana_change, name="mana_change"), path("<int:pk>/mana_change/", views.character_mana_change, name="mana_change"),
path( path(
@ -87,7 +89,9 @@ urlpatterns = [
), ),
path("<int:pk>/add_path/", views.add_path, name="add_path"), path("<int:pk>/add_path/", views.add_path, name="add_path"),
path( path(
"<int:pk>/remove_state/<int:state_pk>/", views.remove_state, name="remove_state" "<int:pk>/remove_state/<int:state_pk>/",
views.remove_state,
name="remove_state",
), ),
path("<int:pk>/add_state/<int:state_pk>/", views.add_state, name="add_state"), path("<int:pk>/add_state/<int:state_pk>/", views.add_state, name="add_state"),
path("<int:pk>/reset_stats/", views.reset_stats, name="reset_stats"), path("<int:pk>/reset_stats/", views.reset_stats, name="reset_stats"),

View file

@ -14,7 +14,8 @@ from party.models import Party
def characters_list(request): def characters_list(request):
context = { context = {
"characters": Character.objects.owned_by(request.user).select_related( "characters": Character.objects.owned_by(request.user).select_related(
"race", "profile" "race",
"profile",
), ),
"all_states": HarmfulState.objects.all(), "all_states": HarmfulState.objects.all(),
} }
@ -99,7 +100,7 @@ def add_path(request, pk: int):
context = {"character": character} context = {"character": character}
if form.is_valid(): if form.is_valid():
path: Path = form.cleaned_data.get("character_path") or form.cleaned_data.get( path: Path = form.cleaned_data.get("character_path") or form.cleaned_data.get(
"other_path" "other_path",
) )
cap = path.get_next_capability(character) cap = path.get_next_capability(character)
character.capabilities.add(cap) character.capabilities.add(cap)
@ -117,7 +118,8 @@ def add_path(request, pk: int):
def character_health_change(request, pk: int): def character_health_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only( Character.objects.managed_by(request.user).only(
"health_max", "health_remaining" "health_max",
"health_remaining",
), ),
pk=pk, pk=pk,
) )
@ -150,7 +152,9 @@ def character_recovery_points_change(request, pk: int):
pk=pk, pk=pk,
) )
value = get_updated_value( value = get_updated_value(
request, character.recovery_points_remaining, character.recovery_points_max request,
character.recovery_points_remaining,
character.recovery_points_max,
) )
character.recovery_points_remaining = value character.recovery_points_remaining = value
character.save(update_fields=["recovery_points_remaining"]) character.save(update_fields=["recovery_points_remaining"])
@ -160,7 +164,8 @@ def character_recovery_points_change(request, pk: int):
@login_required @login_required
def character_defense_misc_change(request, pk: int): def character_defense_misc_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only("defense_misc"), pk=pk Character.objects.managed_by(request.user).only("defense_misc"),
pk=pk,
) )
value = get_updated_value(request, character.defense_misc, float("inf")) value = get_updated_value(request, character.defense_misc, float("inf"))
character.defense_misc = value character.defense_misc = value
@ -172,7 +177,8 @@ def character_defense_misc_change(request, pk: int):
@login_required @login_required
def character_shield_change(request, pk: int): def character_shield_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only("shield"), pk=pk Character.objects.managed_by(request.user).only("shield"),
pk=pk,
) )
value = get_updated_value(request, character.shield, float("inf")) value = get_updated_value(request, character.shield, float("inf"))
character.shield = value character.shield = value
@ -184,7 +190,8 @@ def character_shield_change(request, pk: int):
@login_required @login_required
def character_armor_change(request, pk: int): def character_armor_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only("armor"), pk=pk Character.objects.managed_by(request.user).only("armor"),
pk=pk,
) )
value = get_updated_value(request, character.armor, float("inf")) value = get_updated_value(request, character.armor, float("inf"))
character.armor = value character.armor = value
@ -196,7 +203,8 @@ def character_armor_change(request, pk: int):
@login_required @login_required
def character_initiative_misc_change(request, pk: int): def character_initiative_misc_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only("initiative_misc"), pk=pk Character.objects.managed_by(request.user).only("initiative_misc"),
pk=pk,
) )
value = get_updated_value(request, character.initiative_misc, float("inf")) value = get_updated_value(request, character.initiative_misc, float("inf"))
character.initiative_misc = value character.initiative_misc = value
@ -209,12 +217,15 @@ def character_initiative_misc_change(request, pk: int):
def character_luck_points_change(request, pk: int): def character_luck_points_change(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only( Character.objects.managed_by(request.user).only(
"luck_points_remaining", "value_charisma" "luck_points_remaining",
"value_charisma",
), ),
pk=pk, pk=pk,
) )
value = get_updated_value( value = get_updated_value(
request, character.luck_points_remaining, character.luck_points_max request,
character.luck_points_remaining,
character.luck_points_max,
) )
character.luck_points_remaining = value character.luck_points_remaining = value
character.save(update_fields=["luck_points_remaining"]) character.save(update_fields=["luck_points_remaining"])
@ -222,7 +233,9 @@ def character_luck_points_change(request, pk: int):
def get_updated_value( def get_updated_value(
request, remaining_value: int | float, max_value: int | float request,
remaining_value: int | float,
max_value: int | float,
) -> int: ) -> int:
form_value = request.GET.get("value") form_value = request.GET.get("value")
if form_value == "ko": if form_value == "ko":
@ -241,7 +254,10 @@ def get_updated_value(
def character_get_defense(request, pk: int): def character_get_defense(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only( Character.objects.managed_by(request.user).only(
"defense_misc", "armor", "shield", "value_dexterity" "defense_misc",
"armor",
"shield",
"value_dexterity",
), ),
pk=pk, pk=pk,
) )
@ -252,13 +268,16 @@ def character_get_defense(request, pk: int):
def character_get_health_bar(request, pk: int): def character_get_health_bar(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only( Character.objects.managed_by(request.user).only(
"health_max", "health_remaining" "health_max",
"health_remaining",
), ),
pk=pk, pk=pk,
) )
context = {"character": character} context = {"character": character}
return render( return render(
request, "character/snippets/character_details/health_bar.html", context request,
"character/snippets/character_details/health_bar.html",
context,
) )
@ -270,7 +289,9 @@ def character_get_mana_bar(request, pk: int):
) )
context = {"character": character} context = {"character": character}
return render( return render(
request, "character/snippets/character_details/mana_bar.html", context request,
"character/snippets/character_details/mana_bar.html",
context,
) )
@ -278,7 +299,8 @@ def character_get_mana_bar(request, pk: int):
def character_get_initiative(request, pk: int): def character_get_initiative(request, pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only( Character.objects.managed_by(request.user).only(
"initiative_misc", "value_dexterity" "initiative_misc",
"value_dexterity",
), ),
pk=pk, pk=pk,
) )
@ -299,7 +321,8 @@ def character_gm_notes_change(request, pk: int):
def character_equipment_change(request, pk: int): def character_equipment_change(request, pk: int):
field = "equipment" field = "equipment"
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only(field), pk=pk Character.objects.managed_by(request.user).only(field),
pk=pk,
) )
context = {"character": character} context = {"character": character}
if request.method == "GET": if request.method == "GET":
@ -331,7 +354,8 @@ def character_damage_reduction_change(request, pk: int):
def update_text_field(request, pk, field): def update_text_field(request, pk, field):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user).only(field), pk=pk Character.objects.managed_by(request.user).only(field),
pk=pk,
) )
context = {"character": character} context = {"character": character}
if request.method == "GET": if request.method == "GET":
@ -343,14 +367,17 @@ def update_text_field(request, pk, field):
setattr(character, field, request.POST.get(field)) setattr(character, field, request.POST.get(field))
character.save(update_fields=[field]) character.save(update_fields=[field])
return render( return render(
request, f"character/snippets/character_details/{field}_display.html", context request,
f"character/snippets/character_details/{field}_display.html",
context,
) )
@login_required @login_required
def add_next_in_path(request, character_pk: int, path_pk: int): def add_next_in_path(request, character_pk: int, path_pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user), pk=character_pk Character.objects.managed_by(request.user),
pk=character_pk,
) )
path = get_object_or_404(Path, pk=path_pk) path = get_object_or_404(Path, pk=path_pk)
capability = path.get_next_capability(character) capability = path.get_next_capability(character)
@ -369,10 +396,11 @@ def add_next_in_path(request, character_pk: int, path_pk: int):
@login_required @login_required
def remove_last_in_path(request, character_pk: int, path_pk: int): def remove_last_in_path(request, character_pk: int, path_pk: int):
character = get_object_or_404( character = get_object_or_404(
Character.objects.managed_by(request.user), pk=character_pk Character.objects.managed_by(request.user),
pk=character_pk,
) )
last_rank = max( last_rank = max(
character.capabilities.filter(path_id=path_pk).values_list("rank", flat=True) character.capabilities.filter(path_id=path_pk).values_list("rank", flat=True),
) )
cap = Capability.objects.get(path_id=path_pk, rank=last_rank) cap = Capability.objects.get(path_id=path_pk, rank=last_rank)
character.capabilities.remove(cap) character.capabilities.remove(cap)
@ -390,13 +418,16 @@ def remove_last_in_path(request, character_pk: int, path_pk: int):
@login_required @login_required
def remove_state(request, pk: int, state_pk: int): def remove_state(request, pk: int, state_pk: int):
character: Character = get_object_or_404( character: Character = get_object_or_404(
Character.objects.managed_by(request.user), pk=pk Character.objects.managed_by(request.user),
pk=pk,
) )
state = get_object_or_404(HarmfulState, pk=state_pk) state = get_object_or_404(HarmfulState, pk=state_pk)
character.states.remove(state) character.states.remove(state)
context = {"character": character, "all_states": HarmfulState.objects.all()} context = {"character": character, "all_states": HarmfulState.objects.all()}
response = render( response = render(
request, "character/snippets/character_details/states.html", context request,
"character/snippets/character_details/states.html",
context,
) )
return trigger_client_event(response, "refresh_tooltips", after="swap") return trigger_client_event(response, "refresh_tooltips", after="swap")
@ -404,13 +435,16 @@ def remove_state(request, pk: int, state_pk: int):
@login_required @login_required
def add_state(request, pk: int, state_pk: int): def add_state(request, pk: int, state_pk: int):
character: Character = get_object_or_404( character: Character = get_object_or_404(
Character.objects.managed_by(request.user), pk=pk Character.objects.managed_by(request.user),
pk=pk,
) )
state = get_object_or_404(HarmfulState, pk=state_pk) state = get_object_or_404(HarmfulState, pk=state_pk)
character.states.add(state) character.states.add(state)
context = {"character": character, "all_states": HarmfulState.objects.all()} context = {"character": character, "all_states": HarmfulState.objects.all()}
response = render( response = render(
request, "character/snippets/character_details/states.html", context request,
"character/snippets/character_details/states.html",
context,
) )
return trigger_client_event(response, "refresh_tooltips", after="swap") return trigger_client_event(response, "refresh_tooltips", after="swap")
@ -418,7 +452,8 @@ def add_state(request, pk: int, state_pk: int):
@login_required @login_required
def reset_stats(request, pk: int): def reset_stats(request, pk: int):
character: Character = get_object_or_404( character: Character = get_object_or_404(
Character.objects.managed_by(request.user), pk=pk Character.objects.managed_by(request.user),
pk=pk,
) )
context = {"character": character} context = {"character": character}
if request.method == "POST": if request.method == "POST":

View file

@ -128,7 +128,7 @@ DATABASES = {"default": env.db()}
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache", "BACKEND": "django.core.cache.backends.db.DatabaseCache",
} },
} }
SOLO_CACHE = "default" SOLO_CACHE = "default"
@ -244,7 +244,7 @@ APP = {
"date": "latest-date", "date": "latest-date",
"commit": "latest-commit", "commit": "latest-commit",
"describe": "latest-describe", "describe": "latest-describe",
} },
} }
try: try:
with Path("/app/git/build-date").open() as f: with Path("/app/git/build-date").open() as f:

View file

@ -28,7 +28,9 @@ class Migration(migrations.Migration):
( (
"last_login", "last_login",
models.DateTimeField( models.DateTimeField(
blank=True, null=True, verbose_name="last login" blank=True,
null=True,
verbose_name="last login",
), ),
), ),
( (
@ -43,13 +45,13 @@ class Migration(migrations.Migration):
"username", "username",
models.CharField( models.CharField(
error_messages={ error_messages={
"unique": "A user with that username already exists." "unique": "A user with that username already exists.",
}, },
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150, max_length=150,
unique=True, unique=True,
validators=[ validators=[
django.contrib.auth.validators.UnicodeUsernameValidator() django.contrib.auth.validators.UnicodeUsernameValidator(),
], ],
verbose_name="username", verbose_name="username",
), ),
@ -57,19 +59,25 @@ class Migration(migrations.Migration):
( (
"first_name", "first_name",
models.CharField( models.CharField(
blank=True, max_length=150, verbose_name="first name" blank=True,
max_length=150,
verbose_name="first name",
), ),
), ),
( (
"last_name", "last_name",
models.CharField( models.CharField(
blank=True, max_length=150, verbose_name="last name" blank=True,
max_length=150,
verbose_name="last name",
), ),
), ),
( (
"email", "email",
models.EmailField( models.EmailField(
blank=True, max_length=254, verbose_name="email address" blank=True,
max_length=254,
verbose_name="email address",
), ),
), ),
( (
@ -91,7 +99,8 @@ class Migration(migrations.Migration):
( (
"date_joined", "date_joined",
models.DateTimeField( models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined" default=django.utils.timezone.now,
verbose_name="date joined",
), ),
), ),
( (

View file

@ -13,7 +13,11 @@ class UniquelyNamedModelManager(models.Manager):
class UniquelyNamedModel(models.Model): class UniquelyNamedModel(models.Model):
name = models.CharField( name = models.CharField(
max_length=100, blank=False, null=False, unique=True, verbose_name="nom" max_length=100,
blank=False,
null=False,
unique=True,
verbose_name="nom",
) )
objects = UniquelyNamedModelManager() objects = UniquelyNamedModelManager()

View file

@ -9,11 +9,12 @@ def main():
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
raise ImportError( msg = (
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " "available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?" "forget to activate a virtual environment?"
) from exc )
raise ImportError(msg) from exc
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View file

@ -7,7 +7,7 @@ from party.models import BattleEffect, Party
class PartyForm(forms.ModelForm): class PartyForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
self.original_instance = kwargs.get("instance") self.original_instance = kwargs.get("instance")
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
qs = Character.objects.all() qs = Character.objects.all()
@ -16,9 +16,10 @@ class PartyForm(forms.ModelForm):
Q(private=False) Q(private=False)
| Q( | Q(
pk__in=self.original_instance.invited_characters.all().values_list( pk__in=self.original_instance.invited_characters.all().values_list(
"pk", flat=True "pk",
) flat=True,
) ),
),
) )
self.fields["invited_characters"].queryset = qs self.fields["invited_characters"].queryset = qs

View file

@ -34,13 +34,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
( (

View file

@ -28,13 +28,15 @@ class Migration(migrations.Migration):
( (
"created", "created",
django_extensions.db.fields.CreationDateTimeField( django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created" auto_now_add=True,
verbose_name="created",
), ),
), ),
( (
"modified", "modified",
django_extensions.db.fields.ModificationDateTimeField( django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified" auto_now=True,
verbose_name="modified",
), ),
), ),
("name", models.CharField(max_length=100, verbose_name="nom")), ("name", models.CharField(max_length=100, verbose_name="nom")),

View file

@ -17,14 +17,14 @@ class PartyQuerySet(models.QuerySet):
def played_or_mastered_by(self, user): def played_or_mastered_by(self, user):
return self.filter( return self.filter(
Q(game_master=user) Q(game_master=user)
| Q(characters__in=Character.objects.filter(player=user)) | Q(characters__in=Character.objects.filter(player=user)),
).distinct() ).distinct()
def related_to(self, user): def related_to(self, user):
return self.filter( return self.filter(
Q(game_master=user) Q(game_master=user)
| Q(characters__in=Character.objects.filter(player=user)) | Q(characters__in=Character.objects.filter(player=user))
| Q(invited_characters__in=Character.objects.filter(player=user)) | Q(invited_characters__in=Character.objects.filter(player=user)),
).distinct() ).distinct()
def invited_to(self, user): def invited_to(self, user):
@ -35,7 +35,7 @@ class PartyManager(UniquelyNamedModelManager):
pass pass
class Party(UniquelyNamedModel, TimeStampedModel, models.Model): class Party(UniquelyNamedModel, TimeStampedModel, models.Model): # noqa: DJ008
game_master = models.ForeignKey( game_master = models.ForeignKey(
"common.User", "common.User",
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -96,7 +96,10 @@ class BattleEffectManager(models.Manager):
class BattleEffect(TimeStampedModel, models.Model): class BattleEffect(TimeStampedModel, models.Model):
name = models.CharField(max_length=100, blank=False, null=False, verbose_name="nom") name = models.CharField(max_length=100, blank=False, null=False, verbose_name="nom")
target = models.CharField( target = models.CharField(
max_length=100, blank=False, null=False, verbose_name="cible" max_length=100,
blank=False,
null=False,
verbose_name="cible",
) )
description = models.TextField(blank=True, null=False, verbose_name="description") description = models.TextField(blank=True, null=False, verbose_name="description")
remaining_rounds = models.SmallIntegerField( remaining_rounds = models.SmallIntegerField(
@ -126,3 +129,6 @@ class BattleEffect(TimeStampedModel, models.Model):
if self.remaining_rounds >= max_display_percent or self.remaining_rounds < 0: if self.remaining_rounds >= max_display_percent or self.remaining_rounds < 0:
return 100 return 100
return self.remaining_rounds / max_display_percent * 100 return self.remaining_rounds / max_display_percent * 100
def __str__(self):
return self.name

View file

@ -28,7 +28,8 @@ def test_add_character_to_existing_group(selenium: WebDriver, live_server: LiveS
selenium.get(live_server.url + reverse("party:list")) selenium.get(live_server.url + reverse("party:list"))
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".party[data-id='{party.pk}'] .edit" By.CSS_SELECTOR,
f".party[data-id='{party.pk}'] .edit",
).click() ).click()
invited = Select(selenium.find_element(By.ID, "id_invited_characters")) invited = Select(selenium.find_element(By.ID, "id_invited_characters"))
invited.select_by_index(0) invited.select_by_index(0)
@ -41,7 +42,8 @@ def test_add_character_to_existing_group(selenium: WebDriver, live_server: LiveS
@pytest.mark.django_db() @pytest.mark.django_db()
def test_gm_observe_invited_character_in_group( def test_gm_observe_invited_character_in_group(
selenium: WebDriver, live_server: LiveServer selenium: WebDriver,
live_server: LiveServer,
): ):
username, password = "gm", "password" username, password = "gm", "password"
gm = User.objects.create_user(username, password=password) gm = User.objects.create_user(username, password=password)
@ -54,10 +56,12 @@ def test_gm_observe_invited_character_in_group(
selenium.get(live_server.url + reverse("party:list")) selenium.get(live_server.url + reverse("party:list"))
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".party[data-id='{party.pk}'] .access" By.CSS_SELECTOR,
f".party[data-id='{party.pk}'] .access",
).click() ).click()
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".character[data-id='{character.pk}'] .observe" By.CSS_SELECTOR,
f".character[data-id='{character.pk}'] .observe",
).click() ).click()
title = selenium.find_element(By.TAG_NAME, "h1").text.strip() title = selenium.find_element(By.TAG_NAME, "h1").text.strip()
assert title == character.name assert title == character.name
@ -65,7 +69,8 @@ def test_gm_observe_invited_character_in_group(
@pytest.mark.django_db() @pytest.mark.django_db()
def test_gm_observe_invited_character_in_two_groups( def test_gm_observe_invited_character_in_two_groups(
selenium: WebDriver, live_server: LiveServer selenium: WebDriver,
live_server: LiveServer,
): ):
username, password = "gm", "password" username, password = "gm", "password"
gm = User.objects.create_user(username, password=password) gm = User.objects.create_user(username, password=password)
@ -80,10 +85,12 @@ def test_gm_observe_invited_character_in_two_groups(
selenium.get(live_server.url + reverse("party:list")) selenium.get(live_server.url + reverse("party:list"))
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".party[data-id='{party.pk}'] .access" By.CSS_SELECTOR,
f".party[data-id='{party.pk}'] .access",
).click() ).click()
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f".character[data-id='{character.pk}'] .observe" By.CSS_SELECTOR,
f".character[data-id='{character.pk}'] .observe",
).click() ).click()
title = selenium.find_element(By.TAG_NAME, "h1").text.strip() title = selenium.find_element(By.TAG_NAME, "h1").text.strip()
assert title == character.name assert title == character.name
@ -91,7 +98,9 @@ def test_gm_observe_invited_character_in_two_groups(
@pytest.mark.django_db() @pytest.mark.django_db()
def test_reset_stats_view( def test_reset_stats_view(
selenium: WebDriver, live_server: LiveServer, initial_data: None selenium: WebDriver,
live_server: LiveServer,
initial_data: None,
): ):
user, password = "gm", "password" user, password = "gm", "password"
gm = User.objects.create_user(user, password=password) gm = User.objects.create_user(user, password=password)
@ -189,13 +198,22 @@ def test_gm_can_change_remaining_rounds(selenium: WebDriver, live_server: LiveSe
party=party, party=party,
) )
active_nearly_terminated = baker.make( active_nearly_terminated = baker.make(
BattleEffect, _quantity=3, remaining_rounds=1, party=party BattleEffect,
_quantity=3,
remaining_rounds=1,
party=party,
) )
terminated = baker.make( # noqa: F841 terminated = baker.make( # noqa: F841
BattleEffect, _quantity=5, remaining_rounds=0, party=party BattleEffect,
_quantity=5,
remaining_rounds=0,
party=party,
) )
permanent = baker.make( # noqa: F841 permanent = baker.make( # noqa: F841
BattleEffect, _quantity=2, remaining_rounds=-1, party=party BattleEffect,
_quantity=2,
remaining_rounds=-1,
party=party,
) )
not_party = baker.make(BattleEffect, _quantity=4, remaining_rounds=55) # noqa: F841 not_party = baker.make(BattleEffect, _quantity=4, remaining_rounds=55) # noqa: F841
beacon = active_nearly_terminated[0] beacon = active_nearly_terminated[0]
@ -246,7 +264,8 @@ def test_gm_can_change_remaining_rounds(selenium: WebDriver, live_server: LiveSe
@pytest.mark.django_db() @pytest.mark.django_db()
def test_gm_can_delete_any_existing_effect( def test_gm_can_delete_any_existing_effect(
selenium: WebDriver, live_server: LiveServer selenium: WebDriver,
live_server: LiveServer,
): ):
"""The GM of a group can delete any existing effect, running or terminated.""" """The GM of a group can delete any existing effect, running or terminated."""
user, password = "gm", "password" user, password = "gm", "password"
@ -258,7 +277,8 @@ def test_gm_can_delete_any_existing_effect(
go_to_party(selenium, live_server, party, user, password) go_to_party(selenium, live_server, party, user, password)
selenium.find_element( selenium.find_element(
By.CSS_SELECTOR, f'.effect[data-id="{effects[0].pk}"] .delete' By.CSS_SELECTOR,
f'.effect[data-id="{effects[0].pk}"] .delete',
).click() ).click()
assert BattleEffect.objects.count() == 1 assert BattleEffect.objects.count() == 1
@ -267,7 +287,8 @@ def test_gm_can_delete_any_existing_effect(
@pytest.mark.django_db() @pytest.mark.django_db()
def test_player_cant_change_existing_running_effect( def test_player_cant_change_existing_running_effect(
selenium: WebDriver, live_server: LiveServer selenium: WebDriver,
live_server: LiveServer,
): ):
"""Members of the group can only view existing running effects, no update.""" """Members of the group can only view existing running effects, no update."""
user, password = "player", "password" user, password = "player", "password"
@ -280,7 +301,8 @@ def test_player_cant_change_existing_running_effect(
go_to_party(selenium, live_server, party, user, password) go_to_party(selenium, live_server, party, user, password)
effect = effects[0] effect = effects[0]
effect_element = selenium.find_element( effect_element = selenium.find_element(
By.CSS_SELECTOR, f'.effect[data-id="{effect.pk}"]' By.CSS_SELECTOR,
f'.effect[data-id="{effect.pk}"]',
) )
assert effect.name in effect_element.text assert effect.name in effect_element.text
assert effect.target in effect_element.text assert effect.target in effect_element.text
@ -295,7 +317,11 @@ def test_player_cant_change_existing_running_effect(
def fill_effect( def fill_effect(
selenium: WebDriver, name: str, description: str, target: str, remaining_rounds: str selenium: WebDriver,
name: str,
description: str,
target: str,
remaining_rounds: str,
) -> None: ) -> None:
selenium.find_element(By.ID, "add-effect").click() selenium.find_element(By.ID, "add-effect").click()
selenium.find_element(By.ID, "id_name").send_keys(name) selenium.find_element(By.ID, "id_name").send_keys(name)
@ -308,7 +334,10 @@ def fill_effect(
def assert_effect_is_created( def assert_effect_is_created(
name: str, description: str, target: str, remaining_rounds: str name: str,
description: str,
target: str,
remaining_rounds: str,
) -> BattleEffect: ) -> BattleEffect:
assert BattleEffect.objects.count() == 1 assert BattleEffect.objects.count() == 1
effect = BattleEffect.objects.first() effect = BattleEffect.objects.first()
@ -320,7 +349,11 @@ def assert_effect_is_created(
def go_to_party( def go_to_party(
selenium: WebDriver, live_server: LiveServer, party: Party, user: str, password: str selenium: WebDriver,
live_server: LiveServer,
party: Party,
user: str,
password: str,
) -> None: ) -> None:
login(selenium, live_server, user, password) login(selenium, live_server, user, password)
url = reverse("party:details", kwargs={"pk": party.pk}) url = reverse("party:details", kwargs={"pk": party.pk})

View file

@ -17,10 +17,14 @@ urlpatterns = [
name="delete_effect", name="delete_effect",
), ),
path( path(
"<int:pk>/increase_rounds/", views.party_increase_rounds, name="increase_rounds" "<int:pk>/increase_rounds/",
views.party_increase_rounds,
name="increase_rounds",
), ),
path( path(
"<int:pk>/decrease_rounds/", views.party_decrease_rounds, name="decrease_rounds" "<int:pk>/decrease_rounds/",
views.party_decrease_rounds,
name="decrease_rounds",
), ),
path("<int:pk>/leave/<int:character_pk>/", views.party_leave, name="leave"), path("<int:pk>/leave/<int:character_pk>/", views.party_leave, name="leave"),
path("<int:pk>/join/<int:character_pk>/", views.party_join, name="join"), path("<int:pk>/join/<int:character_pk>/", views.party_join, name="join"),

View file

@ -139,7 +139,8 @@ def party_change(request, pk):
def party_leave(request, pk, character_pk): def party_leave(request, pk, character_pk):
party = get_object_or_404(Party.objects.played_by(request.user).distinct(), pk=pk) party = get_object_or_404(Party.objects.played_by(request.user).distinct(), pk=pk)
character = get_object_or_404( character = get_object_or_404(
Character.objects.owned_by(request.user), pk=character_pk Character.objects.owned_by(request.user),
pk=character_pk,
) )
context = {"party": party, "character": character} context = {"party": party, "character": character}
if request.method == "POST": if request.method == "POST":
@ -154,7 +155,8 @@ def party_leave(request, pk, character_pk):
def party_join(request, pk, character_pk): def party_join(request, pk, character_pk):
party = get_object_or_404(Party.objects.invited_to(request.user).distinct(), pk=pk) party = get_object_or_404(Party.objects.invited_to(request.user).distinct(), pk=pk)
character = get_object_or_404( character = get_object_or_404(
Character.objects.owned_by(request.user), pk=character_pk Character.objects.owned_by(request.user),
pk=character_pk,
) )
party.characters.add(character) party.characters.add(character)
party.invited_characters.remove(character) party.invited_characters.remove(character)
@ -167,7 +169,8 @@ def party_join(request, pk, character_pk):
def party_refuse(request, pk, character_pk): def party_refuse(request, pk, character_pk):
party = get_object_or_404(Party.objects.invited_to(request.user).distinct(), pk=pk) party = get_object_or_404(Party.objects.invited_to(request.user).distinct(), pk=pk)
character = get_object_or_404( character = get_object_or_404(
Character.objects.owned_by(request.user), pk=character_pk Character.objects.owned_by(request.user),
pk=character_pk,
) )
party.invited_characters.remove(character) party.invited_characters.remove(character)
messages.success(request, f"{character} a refusé l'invitation au groupe {party}.") messages.success(request, f"{character} a refusé l'invitation au groupe {party}.")

View file

@ -84,7 +84,9 @@ def download_db(ctx: Context):
) )
ctx.run("rm -rf src/media/", pty=True, echo=True) ctx.run("rm -rf src/media/", pty=True, echo=True)
ctx.run( ctx.run(
"scp -r ubuntu:/mnt/data/charasheet/media/ ./src/media", pty=True, echo=True "scp -r ubuntu:/mnt/data/charasheet/media/ ./src/media",
pty=True,
echo=True,
) )
with ctx.cd(SRC_DIR): with ctx.cd(SRC_DIR):
ctx.run("./manage.py changepassword gaugendre", pty=True, echo=True) ctx.run("./manage.py changepassword gaugendre", pty=True, echo=True)