From b892d9b4ab0a0284c9529f7a8c67c185c1243a04 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 3 Mar 2026 08:24:20 +0700 Subject: [PATCH 1/3] [catalogs] add sowing period --- backend/catalogs/admin.py | 7 +-- ...owing_period_plantvariety_sowing_period.py | 46 +++++++++++++++++++ backend/catalogs/models.py | 37 ++++++++++++++- 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 backend/catalogs/migrations/0005_planttype_sowing_period_plantvariety_sowing_period.py diff --git a/backend/catalogs/admin.py b/backend/catalogs/admin.py index 31d59a6..4fe389c 100644 --- a/backend/catalogs/admin.py +++ b/backend/catalogs/admin.py @@ -31,8 +31,8 @@ class PlantTypeOperationsInline(admin.TabularInline): @admin.register(PlantType) class PlantTypeAdmin(admin.ModelAdmin): - fields = ("name", "slug", "description", "planting_period", "duration_days") - list_display = ("name", "slug", "planting_period", "duration_days") + fields = ("name", "slug", "description", "sowing_period", "planting_period", "duration_days") + list_display = ("name", "slug", "sowing_period", "planting_period", "duration_days") ordering = ("name",) prepopulated_fields = {"slug": ("name",)} search_fields = ("name",) @@ -47,10 +47,11 @@ class PlantVarietyAdmin(admin.ModelAdmin): "name", "slug", "description", + "sowing_period", "planting_period", "duration_days", ) - list_display = ("full_name", "type", "planting_period", "duration_days") + list_display = ("full_name", "type", "sowing_period", "planting_period", "duration_days") list_filter = ("type",) ordering = ("type__name", "name") prepopulated_fields = {"slug": ("name",)} diff --git a/backend/catalogs/migrations/0005_planttype_sowing_period_plantvariety_sowing_period.py b/backend/catalogs/migrations/0005_planttype_sowing_period_plantvariety_sowing_period.py new file mode 100644 index 0000000..8e4fd6d --- /dev/null +++ b/backend/catalogs/migrations/0005_planttype_sowing_period_plantvariety_sowing_period.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.7 on 2026-03-03 01:22 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("catalogs", "0004_alter_planttype_planting_period_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="planttype", + name="sowing_period", + field=models.CharField( + blank=True, + max_length=16, + null=True, + validators=[ + django.core.validators.RegexValidator( + message="Неверный формат периода посадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII).", + regex="^(0[1-9]|[12][0-9]|3[01])\\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)-(0[1-9]|[12][0-9]|3[01])\\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$", + ) + ], + verbose_name="Период посадки", + ), + ), + migrations.AddField( + model_name="plantvariety", + name="sowing_period", + field=models.CharField( + blank=True, + max_length=16, + null=True, + validators=[ + django.core.validators.RegexValidator( + message="Неверный формат периода посадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII).", + regex="^(0[1-9]|[12][0-9]|3[01])\\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)-(0[1-9]|[12][0-9]|3[01])\\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$", + ) + ], + verbose_name="Период посадки", + ), + ), + ] diff --git a/backend/catalogs/models.py b/backend/catalogs/models.py index 92a2f0b..4e74518 100644 --- a/backend/catalogs/models.py +++ b/backend/catalogs/models.py @@ -3,6 +3,9 @@ from django.utils.translation import gettext_lazy as _ +PLANTING_PERIOD_REGEX = r"^(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)-(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$" + + class Step(models.TextChoices): SEED_PREPARATION = "10-seed_preparation", _("🌱🛠️ Подготовка семян") SOWING = "20-sowing", _("🌱👐 Посев") @@ -30,6 +33,21 @@ class PlantType(models.Model): name = models.CharField(verbose_name=_("Наименование"), max_length=128) description = models.TextField(verbose_name=_("Описание")) + sowing_period = models.CharField( + verbose_name=_("Период посадки"), + max_length=16, + null=True, + blank=True, + validators=[ + RegexValidator( + regex=PLANTING_PERIOD_REGEX, + message=_( + "Неверный формат периода посадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII)." + ), + ), + ], + ) + planting_period = models.CharField( verbose_name=_("Период высадки"), max_length=16, @@ -37,7 +55,7 @@ class PlantType(models.Model): blank=True, validators=[ RegexValidator( - regex=r"^(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)-(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$", + regex=PLANTING_PERIOD_REGEX, message=_( "Неверный формат периода высадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII)." ), @@ -69,6 +87,21 @@ class PlantVariety(models.Model): name = models.CharField(verbose_name=_("Наименование"), max_length=128) description = models.TextField(verbose_name=_("Описание")) + sowing_period = models.CharField( + verbose_name=_("Период посадки"), + max_length=16, + null=True, + blank=True, + validators=[ + RegexValidator( + regex=PLANTING_PERIOD_REGEX, + message=_( + "Неверный формат периода посадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII)." + ), + ), + ], + ) + planting_period = models.CharField( verbose_name=_("Период высадки"), max_length=16, @@ -76,7 +109,7 @@ class PlantVariety(models.Model): blank=True, validators=[ RegexValidator( - regex=r"^(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)-(0[1-9]|[12][0-9]|3[01])\.(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII)$", + regex=PLANTING_PERIOD_REGEX, message=_( "Неверный формат периода высадки. Используйте формат 'DD.MM-DD.MM', где MM - месяц в римской записи (I-XII)." ), From 6b61cb49b6132ef879e348d094db3db17fe1e7e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 3 Mar 2026 08:26:24 +0700 Subject: [PATCH 2/3] [diary] improve parsing of periods --- backend/bot/bot.py | 2 +- backend/diary/admin.py | 4 +++ backend/diary/models.py | 65 +++++++++++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/backend/bot/bot.py b/backend/bot/bot.py index e2155ca..fe5024a 100644 --- a/backend/bot/bot.py +++ b/backend/bot/bot.py @@ -350,6 +350,6 @@ async def planting(message: Message): answer += f" {_('Planned planting period')}: {plant.planting_period}\n" if not plants_found: - answer = _("You don't have any plants yet.") + answer = _("No plants are ready for transplanting right now.") await message.answer(str(answer)) diff --git a/backend/diary/admin.py b/backend/diary/admin.py index 99fdb54..69a4595 100644 --- a/backend/diary/admin.py +++ b/backend/diary/admin.py @@ -69,6 +69,10 @@ def type_name(self, obj): def variety_name(self, obj): return obj.variety + @admin.display(description=_("Период высадки")) + def planting_period(self, obj): + return obj.planting_period + @admin.display(description=_("Дата сбора")) def planned_harvest_date(self, obj): return obj.planned_harvest_date diff --git a/backend/diary/models.py b/backend/diary/models.py index 6af95b3..7de8c77 100644 --- a/backend/diary/models.py +++ b/backend/diary/models.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db import models from django.utils.functional import cached_property +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from timezone_field import TimeZoneField @@ -146,28 +147,15 @@ def save(self, *args, **kwargs): self.name = self.type.name if self.variety: self.name += f" {self.variety.name}" - self.name += f" ({date.today().strftime('%Y-%m-%d')})" + self.name += f" ({timezone.localdate().strftime('%Y-%m-%d')})" super().save(*args, **kwargs) - @property - def planting_period(self): - if self.variety and self.variety.planting_period: - return self.variety.planting_period - return self.type.planting_period - - @cached_property - def parsed_planting_period(self): - """ - Parses the planting_period string in 'DD.MM-DD.MM' format into a tuple of start and end dates. - - Returns: - tuple: A tuple containing the start and end dates as datetime.date objects, or None if parsing fails. - """ - period = self.planting_period + @staticmethod + def _parse_period(period: Optional[str], plant_id: Optional[int], period_name: str): if not period: return None - romanian_months = { + roman_months = { "I": 1, "II": 2, "III": 3, @@ -187,15 +175,48 @@ def parsed_planting_period(self): start_day, start_month = start_str.split(".") end_day, end_month = end_str.split(".") - today = date.today() - start_date = date(today.year, romanian_months[start_month], int(start_day)) - end_date = date(today.year, romanian_months[end_month], int(end_day)) + today = timezone.localdate() + start_day_i = int(start_day) + end_day_i = int(end_day) + start_month_i = roman_months[start_month] + end_month_i = roman_months[end_month] + + wraps_year = (end_month_i, end_day_i) < (start_month_i, start_day_i) + start_year = today.year + end_year = today.year + + if wraps_year: + if (today.month, today.day) <= (end_month_i, end_day_i): + # We are in Jan/Feb part of a wrapped period: active range started last year. + start_year -= 1 + else: + # We are in/approaching Nov-Dec side: end falls next year. + end_year += 1 + + start_date = date(start_year, start_month_i, start_day_i) + end_date = date(end_year, end_month_i, end_day_i) return start_date, end_date - except (ValueError, KeyError): - logger.warning("Failed to parse planting period for plant %s", self.id) + except (ValueError, KeyError) as exc: + logger.warning( + "Failed to parse %s for plant %s: value=%r error=%s", + period_name, + plant_id, + period, + exc, + ) return None + @property + def planting_period(self): + if self.variety and self.variety.planting_period: + return self.variety.planting_period + return self.type.planting_period + + @cached_property + def parsed_planting_period(self): + return self._parse_period(self.planting_period, self.id, "planting period") + def __str__(self): return f"{self.name}" From fd1b6604fe371a3de6d166fdb09b743810fa0f91 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 3 Mar 2026 08:26:46 +0700 Subject: [PATCH 3/3] [bot] improve transplanting message --- backend/bot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/bot/bot.py b/backend/bot/bot.py index fe5024a..48c4994 100644 --- a/backend/bot/bot.py +++ b/backend/bot/bot.py @@ -322,7 +322,7 @@ async def planting(message: Message): logger.warning("Received message without user information") return - answer = _("Your future plantings:") + "\n" + answer = _("Your plants ready for transplanting:") + "\n" plants_found = False now = timezone.localdate() @@ -342,12 +342,12 @@ async def planting(message: Message): continue start_date, end_date = parsed_period - if end_date < now: + if now < start_date or end_date < now: continue plants_found = True answer += f"\n🌱 {plant.name}\n" - answer += f" {_('Planned planting period')}: {plant.planting_period}\n" + answer += f" {_('Planned transplanting period')}: {plant.planting_period}\n" if not plants_found: answer = _("No plants are ready for transplanting right now.")