diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24ff476..3f88db2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,4 +56,4 @@ jobs: - name: Run tests # For example, using `pytest` - run: uv run pytest test + run: uv run pytest diff --git a/README.rst b/README.rst index 934cae0..63ae779 100644 --- a/README.rst +++ b/README.rst @@ -27,3 +27,9 @@ ____________ 3. Run `python manage.py migrate` to create the enumeration models. + + +Development +____________ + +DJANGO_SETTINGS_MODULE=enumeration.tests.settings django-admin makemigrations \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 68e1ddb..076f7f2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = test.settings -pythonpath = . src \ No newline at end of file +DJANGO_SETTINGS_MODULE = enumeration.tests.settings +pythonpath = src \ No newline at end of file diff --git a/src/enumeration/migrations/0002_alter_counter_id_alter_gap_id_alter_sequence_id.py b/src/enumeration/migrations/0002_alter_counter_id_alter_gap_id_alter_sequence_id.py new file mode 100644 index 0000000..cf2a1e9 --- /dev/null +++ b/src/enumeration/migrations/0002_alter_counter_id_alter_gap_id_alter_sequence_id.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.17 on 2024-12-09 04:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("enumeration", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="counter", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="gap", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="sequence", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/src/enumeration/mixins.py b/src/enumeration/mixins.py new file mode 100644 index 0000000..19458ef --- /dev/null +++ b/src/enumeration/mixins.py @@ -0,0 +1,70 @@ +from django.db import models +from typing import Dict, TYPE_CHECKING +from enumeration.manager import get_number, format_number, ResetPeriod, truncate_date + +if TYPE_CHECKING: + from enumeration.models import Sequence + + +class EnumeratedModelMixin(models.Model): + sequence = models.ForeignKey( + "enumeration.Sequence", null=True, on_delete=models.PROTECT + ) + + number = models.CharField(max_length=255, null=True, blank=True, db_index=True) + position = models.PositiveIntegerField(null=True) + counter = models.ForeignKey( + "enumeration.Counter", null=True, on_delete=models.PROTECT + ) + + def get_sequence(self) -> "Sequence": + raise NotImplementedError + + def get_enumeration_context(self) -> Dict: + return {} + + def assign_number(self): + if self.number: + raise RuntimeError("Number present") + + if not self.sequence: + self.sequence = self.get_sequence() + + self.number, self.position, self.counter_id = get_number( + sequence=self.sequence, **self.get_enumeration_context() + ) + + def reassign_number(self): + assert self.sequence and self.position and self.counter + ctx = self.get_enumeration_context() + + # Sequecne never resets + if self.sequence.reset_period == ResetPeriod.NEVER: + # And correct counter + if self.counter.period is None: + self.number = format_number( + self.sequence.format, + position=self.position, + **ctx, + ) + return + # sequnce resets + else: + # and correct counter period + if self.counter.period == truncate_date( + self.sequence.reset_period, ctx["date"] + ): + self.number = format_number( + self.sequence.format, + position=self.position, + **ctx, + ) + return + + # get new number + self.number, self.position, self.counter_id = get_number( + sequence=self.sequence, **ctx + ) + + class Meta: + abstract = True diff --git a/src/enumeration/models.py b/src/enumeration/models.py index a16af68..c319694 100644 --- a/src/enumeration/models.py +++ b/src/enumeration/models.py @@ -1,6 +1,5 @@ from django.core.exceptions import ValidationError from django.db import models - from enumeration.const import ResetPeriod from enumeration.validators import validate_format @@ -43,12 +42,3 @@ class Gap(models.Model): position = models.PositiveIntegerField() unique_together = ("counter", "position") - - -# class EnumeratedDocumentMixin(models.Model): -# sequence = models.ForeignKey(Sequence) -# position = models.PositiveIntegerField(null=True) -# number = models.CharField(max_length=255, null=True, blank=True) -# -# class Meta: -# abstract = True diff --git a/test/__init__.py b/src/enumeration/tests/__init__.py similarity index 100% rename from test/__init__.py rename to src/enumeration/tests/__init__.py diff --git a/src/enumeration/tests/apps.py b/src/enumeration/tests/apps.py new file mode 100644 index 0000000..d20c499 --- /dev/null +++ b/src/enumeration/tests/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TestConfig(AppConfig): + name = "enumeration.tests" + label = "enumeration_tests" diff --git a/src/enumeration/tests/migrations/0001_initial.py b/src/enumeration/tests/migrations/0001_initial.py new file mode 100644 index 0000000..f82cc6a --- /dev/null +++ b/src/enumeration/tests/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.17 on 2024-12-09 04:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("enumeration", "0002_alter_counter_id_alter_gap_id_alter_sequence_id"), + ] + + operations = [ + migrations.CreateModel( + name="Document", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "number", + models.CharField( + blank=True, db_index=True, max_length=255, null=True + ), + ), + ("position", models.PositiveIntegerField(null=True)), + ("date", models.DateField()), + ( + "counter", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="enumeration.counter", + ), + ), + ( + "sequence", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="enumeration.sequence", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/enumeration/tests/migrations/__init__.py b/src/enumeration/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/enumeration/tests/models.py b/src/enumeration/tests/models.py new file mode 100644 index 0000000..4df7be4 --- /dev/null +++ b/src/enumeration/tests/models.py @@ -0,0 +1,9 @@ +from django.db import models +from enumeration.mixins import EnumeratedModelMixin + + +class Document(EnumeratedModelMixin): + date = models.DateField() + + def get_enumeration_context(self): + return {"date": self.date} diff --git a/test/settings.py b/src/enumeration/tests/settings.py similarity index 94% rename from test/settings.py rename to src/enumeration/tests/settings.py index 82d815d..eb7e444 100644 --- a/test/settings.py +++ b/src/enumeration/tests/settings.py @@ -1,5 +1,3 @@ -DATABASE_ENGINE = "sqlite3" - DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", @@ -17,6 +15,7 @@ # 'django.contrib.auth', # 'django.contrib.admin', "enumeration", + "enumeration.tests", ] SECRET_KEY = "enumeration" diff --git a/test/test_consume_gap.py b/src/enumeration/tests/test_consume_gap.py similarity index 100% rename from test/test_consume_gap.py rename to src/enumeration/tests/test_consume_gap.py diff --git a/test/test_format.py b/src/enumeration/tests/test_format.py similarity index 100% rename from test/test_format.py rename to src/enumeration/tests/test_format.py diff --git a/test/test_format_validator.py b/src/enumeration/tests/test_format_validator.py similarity index 100% rename from test/test_format_validator.py rename to src/enumeration/tests/test_format_validator.py diff --git a/test/test_get_next.py b/src/enumeration/tests/test_get_next.py similarity index 100% rename from test/test_get_next.py rename to src/enumeration/tests/test_get_next.py diff --git a/test/test_get_number.py b/src/enumeration/tests/test_get_number.py similarity index 100% rename from test/test_get_number.py rename to src/enumeration/tests/test_get_number.py diff --git a/test/test_increment.py b/src/enumeration/tests/test_increment.py similarity index 100% rename from test/test_increment.py rename to src/enumeration/tests/test_increment.py diff --git a/test/test_misc.py b/src/enumeration/tests/test_misc.py similarity index 100% rename from test/test_misc.py rename to src/enumeration/tests/test_misc.py diff --git a/src/enumeration/tests/test_model_mixin.py b/src/enumeration/tests/test_model_mixin.py new file mode 100644 index 0000000..4d51ed1 --- /dev/null +++ b/src/enumeration/tests/test_model_mixin.py @@ -0,0 +1,62 @@ +import pytest +from datetime import date +from enumeration.models import Sequence, ResetPeriod, Counter +from .models import Document + + +@pytest.fixture() +def sequence(db): + return Sequence.objects.create(format="YYMM###", reset_period=ResetPeriod.MONTHLY) + + +def test_assign_number(sequence): + d = Document(sequence=sequence, date=date(2024, 11, 5)) + d.assign_number() + assert d.number == "2411001" + assert d.position == 1 + assert d.counter == Counter.objects.get( + sequence=sequence, position=1, period=date(2024, 11, 1) + ) + + # can't assign + with pytest.raises(RuntimeError): + d.assign_number() + + +def test_reassing(sequence): + + i = Document(sequence=sequence, date=date(2024, 11, 5)) + i.assign_number() + + assert i.number == "2411001" + assert i.position == 1 + assert i.counter == Counter.objects.get( + sequence=sequence, position=1, period=date(2024, 11, 1) + ) + + # jump day in same month + i.date = date(2024, 11, 2) + i.reassign_number() + + assert i.number == "2411001" + assert i.position == 1 + assert i.counter == Counter.objects.get( + sequence=sequence, position=1, period=date(2024, 11, 1) + ) + + # move to next month + i.date = date(2024, 12, 2) + i.reassign_number() + assert i.number == "2412001" + assert i.position == 1 + assert i.counter == Counter.objects.get( + sequence=sequence, position=1, period=date(2024, 12, 1) + ) + + i = Document(sequence=sequence, date=date(2024, 12, 1)) + i.assign_number() + assert i.number == "2412002" + assert i.position == 2 + assert i.counter == Counter.objects.get( + sequence=sequence, position=2, period=date(2024, 12, 1) + )