diff --git a/wger/manager/dataclasses.py b/wger/manager/dataclasses.py index f43b73ad3..f75100bc5 100644 --- a/wger/manager/dataclasses.py +++ b/wger/manager/dataclasses.py @@ -22,7 +22,10 @@ dataclass, field, ) -from decimal import Decimal +from decimal import ( + ROUND_DOWN, + Decimal, +) from typing import ( Any, List, @@ -84,9 +87,6 @@ def text_repr(self) -> str: This converts the values to something readable like "10 × 100 kg @ 2.00RiR" """ - def round_value(x: int | float, base: float = 5) -> Decimal: - return normalize_decimal(Decimal(base * round(Decimal(x) / Decimal(base)))) - out = [] if self.sets and self.sets > 1: @@ -217,3 +217,23 @@ class RoutineLogData: volume: GroupedLogData = field(default_factory=GroupedLogData) intensity: GroupedLogData = field(default_factory=GroupedLogData) sets: GroupedLogData = field(default_factory=GroupedLogData) + + +def round_value( + x: int | float | Decimal | None, + base: int | float | Decimal | None = None, +) -> Decimal | None: + """ + Rounds a value to the nearest base + + If the base is None, the value will be returned as a Decimal object. + """ + if x is None: + return x + + # If the result is an integer, remove the decimal part + result = Decimal(x) if base is None else Decimal(base * round(Decimal(x) / Decimal(base))) + if result == result.to_integral_value(): + result = result.quantize(1, ROUND_DOWN) + + return result diff --git a/wger/manager/migrations/0018_flexible_routines.py b/wger/manager/migrations/0018_flexible_routines.py index 554b312fb..1db250e1a 100644 --- a/wger/manager/migrations/0018_flexible_routines.py +++ b/wger/manager/migrations/0018_flexible_routines.py @@ -93,7 +93,7 @@ class Migration(migrations.Migration): field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='sessions', + related_name='logs', to='manager.workoutsession', verbose_name='Session', ), @@ -194,7 +194,12 @@ class Migration(migrations.Migration): ), ( 'repetition_rounding', - models.DecimalField(decimal_places=2, default=1, max_digits=4), + models.DecimalField( + decimal_places=2, + default=None, + max_digits=4, + null=True, + ), ), ( 'slot', @@ -217,8 +222,9 @@ class Migration(migrations.Migration): 'weight_rounding', models.DecimalField( decimal_places=2, - default=1.25, + default=None, max_digits=4, + null=True, ), ), ( @@ -355,6 +361,7 @@ class Migration(migrations.Migration): field=models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='sessions', to='manager.routine', ), ), diff --git a/wger/manager/models/slot_entry.py b/wger/manager/models/slot_entry.py index daa87ff27..2a1c8de62 100644 --- a/wger/manager/models/slot_entry.py +++ b/wger/manager/models/slot_entry.py @@ -30,7 +30,10 @@ WeightUnit, ) from wger.exercises.models import Exercise -from wger.manager.dataclasses import SetConfigData +from wger.manager.dataclasses import ( + SetConfigData, + round_value, +) from wger.manager.models.abstract_config import ( AbstractChangeConfig, OperationChoices, @@ -77,7 +80,8 @@ class SlotEntry(models.Model): repetition_rounding = models.DecimalField( decimal_places=2, max_digits=4, - default=1, + default=None, + null=True, ) """ The amount by which the repetitions will be rounded @@ -98,7 +102,8 @@ class SlotEntry(models.Model): weight_rounding = models.DecimalField( decimal_places=2, max_digits=4, - default=1.25, + default=None, + null=True, ) """ The amount by which the weight will be rounded @@ -276,22 +281,26 @@ def get_config(self, iteration: int) -> SetConfigData: slot_entry_id=self.id, exercise=self.exercise.id, sets=sets if sets is not None else 1, - max_sets=max_sets, - weight=weight, - max_weight=max_weight if max_weight and weight and max_weight > weight else None, + max_sets=round_value(max_sets, 1), + weight=round_value(weight, self.weight_rounding), + max_weight=round_value(max_weight, self.weight_rounding) + if max_weight and weight and max_weight > weight + else None, weight_rounding=self.weight_rounding if weight is not None else None, # TODO: decide on whether to return None or always the unit # weight_unit=self.weight_unit.pk if weight is not None else None, weight_unit=self.weight_unit.pk, - reps=reps, - max_reps=max_reps if max_reps and reps and max_reps > reps else None, + reps=round_value(reps, self.repetition_rounding), + max_reps=round_value(max_reps, self.repetition_rounding) + if max_reps and reps and max_reps > reps + else None, reps_rounding=self.repetition_rounding if reps is not None else None, reps_unit=self.repetition_unit.pk, # TODO: decide on whether to return None or always the unit # reps_unit=self.repetition_unit.pk if reps is not None else None, rir=self.get_rir(iteration), - rest=rest, - max_rest=max_rest if max_rest and rest and max_rest > rest else None, + rest=round_value(rest, 1), + max_rest=round_value(max_rest, 1) if max_rest and rest and max_rest > rest else None, type=str(self.type), comment=self.comment, ) diff --git a/wger/manager/tests/test_dataclasses_helper.py b/wger/manager/tests/test_dataclasses_helper.py new file mode 100644 index 000000000..0006f3d11 --- /dev/null +++ b/wger/manager/tests/test_dataclasses_helper.py @@ -0,0 +1,40 @@ +# This file is part of wger Workout Manager. +# +# wger Workout Manager is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# wger Workout Manager is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License + +# Standard Library +from decimal import Decimal + +# Django +from django.test import SimpleTestCase + +# wger +from wger.manager.dataclasses import round_value + + +class RoundValueTestCase(SimpleTestCase): + """ + Test that the rounding helper works as expected + """ + + def test_round_value(self): + self.assertEqual(round_value(5.1, 5), 5) + + def test_round_value2(self): + self.assertEqual(round_value(Decimal('7'), 1.25), Decimal('7.5')) + + def test_round_value_no_base(self): + self.assertEqual(round_value(Decimal('1.33')), Decimal('1.33')) + + def test_round_no_value(self): + self.assertEqual(round_value(None), None) diff --git a/wger/manager/tests/test_slot_entry.py b/wger/manager/tests/test_slot_entry.py index a831365d0..fcdca8045 100644 --- a/wger/manager/tests/test_slot_entry.py +++ b/wger/manager/tests/test_slot_entry.py @@ -15,6 +15,9 @@ # Standard Library from decimal import Decimal +# Django +from django.test import SimpleTestCase + # wger from wger.core.tests.base_testcase import WgerTestCase from wger.manager.dataclasses import SetConfigData @@ -176,11 +179,11 @@ def test_weight_config_with_logs(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=4, - weight=80, - weight_rounding=Decimal('2.5'), - reps=5, + weight=Decimal(80), + weight_rounding=Decimal(2.5), + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -191,11 +194,11 @@ def test_weight_config_with_logs(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=4, - weight=80, - weight_rounding=Decimal('2.5'), - reps=5, + weight=Decimal(80), + weight_rounding=Decimal(2.5), + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -206,11 +209,11 @@ def test_weight_config_with_logs(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=4, - weight=80, + weight=Decimal(80), weight_rounding=Decimal('2.5'), - reps=5, + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -223,9 +226,9 @@ def test_weight_config_with_logs(self): sets=4, weight=Decimal(82.5), weight_rounding=Decimal('2.5'), - reps=5, + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -236,11 +239,11 @@ def test_weight_config_with_logs(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=4, - weight=42, + weight=Decimal('42.5'), weight_rounding=Decimal('2.5'), - reps=5, + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -251,11 +254,11 @@ def test_weight_config_with_logs(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=4, - weight=42, + weight=Decimal(42.5), weight_rounding=Decimal('2.5'), - reps=5, + reps=Decimal(4), reps_rounding=2, - rir=2, + rir=Decimal(2), rest=120, ), ) @@ -313,11 +316,11 @@ def test_weight_config_with_logs_and_range(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=1, - weight=80, - max_weight=100, + weight=Decimal(80), + max_weight=Decimal(100), weight_rounding=Decimal('2.5'), - reps=5, - max_reps=6, + reps=Decimal(4), + max_reps=Decimal(6), reps_rounding=2, rir=None, rest=None, @@ -330,11 +333,11 @@ def test_weight_config_with_logs_and_range(self): slot_entry_id=self.slot_entry.pk, exercise=1, sets=1, - weight=80, - max_weight=100, + weight=Decimal(80), + max_weight=Decimal(100), weight_rounding=Decimal('2.5'), - reps=5, - max_reps=6, + reps=Decimal(4), + max_reps=Decimal(6), reps_rounding=2, rir=None, rest=None, @@ -397,6 +400,8 @@ def test_empty_configs(self): ), ) + +class SlotEntryDuplicateConfigTestCase(SimpleTestCase): def test_duplicate_configs(self): configs = [ WeightConfig(