Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f5f6a4d
Добавлено выполнение плановых задач ухода из карточки питомца
newfatto Jul 30, 2026
38ef9a6
Добавлен диалог выполнения плановой задачи ухода
newfatto Jul 30, 2026
36687bc
Добавлен запрет контакта при выполнении планового кормления
newfatto Jul 30, 2026
e0094e0
Добавлено управление плановыми задачами и завершением линьки
newfatto Jul 31, 2026
a4b7be9
Синхронизировано ограничение контакта с состоянием питомца
newfatto Jul 31, 2026
d3f42ab
Добавлено выполнение планового измерения через форму события
newfatto Jul 31, 2026
3b3edfd
Добавлены пропуск и отмена плановых задач ухода
newfatto Jul 31, 2026
9152cd8
Добавлен старт линьки как активное состояние питомца
newfatto Jul 31, 2026
8bff6c6
Синхронизированы Telegram-напоминания с плановыми задачами
newfatto Jul 31, 2026
9d60331
Переведено отображение событий на дату факта
newfatto Jul 31, 2026
5a82eb1
Переведены расчёты событий на дату факта
newfatto Jul 31, 2026
b7cbb7e
Переведена админка событий на дату факта
newfatto Jul 31, 2026
235fb2c
Унифицирована таблица событий ухода
newfatto Jul 31, 2026
a64d4e3
Синхронизированы Telegram-задачи с датой факта события
newfatto Jul 31, 2026
4190b86
Уточнены подписи даты факта события
newfatto Jul 31, 2026
88f6d3a
Упорядочены импорты после стабилизации ухода
newfatto Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion legal/services.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import cast

from django.contrib.auth.base_user import AbstractBaseUser
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from .models import LegalDocumentVersion, UserLegalAcceptance

Expand Down
23 changes: 20 additions & 3 deletions pets/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib import admin
from django.db.models.functions import Coalesce

from .models import AnimalTypeEventRule, CareRule, Event, EventType, Pet, PetCondition, PetEventSetting, PlannedEvent

Expand Down Expand Up @@ -171,6 +172,8 @@ class PetEventSettingAdmin(admin.ModelAdmin):
"event_type",
"is_tracked",
"show_quick_action",
"default_no_handling_days",
"no_handling_during_shedding",
"source",
"created_at",
)
Expand Down Expand Up @@ -315,8 +318,8 @@ class EventAdmin(admin.ModelAdmin):
"pet",
"event_type",
"event_type_ref",
"event_datetime",
"happened_at",
"event_happened_at_display",
"ended_at",
"owner",
"repeat_after_days",
"no_handling_days",
Expand All @@ -341,7 +344,7 @@ class EventAdmin(admin.ModelAdmin):
autocomplete_fields = ("owner", "pet", "event_type_ref")
readonly_fields = ("created_at",)
list_select_related = ("owner", "pet", "event_type_ref")
ordering = ("-event_datetime",)
ordering = ()

fieldsets = (
(
Expand Down Expand Up @@ -376,6 +379,20 @@ class EventAdmin(admin.ModelAdmin):
),
)

def get_queryset(self, request):
"""Сортирует события в админке по новой дате факта с fallback на старое поле."""
return (
super()
.get_queryset(request)
.annotate(event_happened_at_sort=Coalesce("happened_at", "event_datetime"))
.order_by("-event_happened_at_sort", "-pk")
)

@admin.display(description="Дата факта", ordering="event_happened_at_sort")
def event_happened_at_display(self, obj: Event):
"""Показывает фактическое время события в переходный период."""
return obj.happened_at or obj.event_datetime

def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Уточняет подписи переходных полей даты события в админке."""
if db_field.name == "event_datetime":
Expand Down
6 changes: 5 additions & 1 deletion pets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class Meta:
"pet": "Питомец",
"event_type": "Тип события",
"title": "Название",
"event_datetime": "Дата и время",
"event_datetime": "Дата",
"comment": "Комментарий",
"repeat_after_days": "Повторить через (дней)",
"no_handling_days": "Нельзя трогать (дней)",
Expand Down Expand Up @@ -285,6 +285,10 @@ class Meta(BaseEventForm.Meta):
"no_handling_days",
"comment",
]
labels = {
**BaseEventForm.Meta.labels,
"event_datetime": "Дата начала",
}
help_texts = {
"no_handling_days": "Сколько дней нельзя трогать",
}
Expand Down
24 changes: 24 additions & 0 deletions pets/migrations/0016_peteventsetting_comment_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.2.13 on 2026-07-30 17:57

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pets', '0015_event_happened_at'),
]

operations = [
migrations.AddField(
model_name='peteventsetting',
name='comment',
field=models.TextField(blank=True, verbose_name='Комментарий к настройке'),
),
migrations.AddField(
model_name='peteventsetting',
name='default_no_handling_days',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(365)], verbose_name='Запрет контакта по умолчанию (дней)'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.13 on 2026-07-30 18:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pets', '0016_peteventsetting_comment_and_more'),
]

operations = [
migrations.AddField(
model_name='peteventsetting',
name='no_handling_during_shedding',
field=models.BooleanField(default=False, verbose_name='Не трогать во время линьки'),
),
]
48 changes: 48 additions & 0 deletions pets/migrations/0018_peteventsetting_shedding_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.db import migrations


def apply_shedding_handling_defaults(apps, schema_editor):
AnimalTypeEventRule = apps.get_model("pets", "AnimalTypeEventRule")
EventType = apps.get_model("pets", "EventType")
PetEventSetting = apps.get_model("pets", "PetEventSetting")

shedding_type = EventType.objects.filter(slug="shedding").first()
if not shedding_type:
return

sensitive_animal_types = set(
AnimalTypeEventRule.objects.filter(
event_type=shedding_type,
is_active=True,
is_available=True,
shedding_policy__in=("handling_sensitive", "critical"),
).values_list("animal_type", flat=True)
)

if not sensitive_animal_types:
return

PetEventSetting.objects.filter(
event_type=shedding_type,
pet__animal_type__in=sensitive_animal_types,
).update(no_handling_during_shedding=True)


def clear_shedding_handling_defaults(apps, schema_editor):
EventType = apps.get_model("pets", "EventType")
PetEventSetting = apps.get_model("pets", "PetEventSetting")

shedding_type = EventType.objects.filter(slug="shedding").first()
if shedding_type:
PetEventSetting.objects.filter(event_type=shedding_type).update(no_handling_during_shedding=False)


class Migration(migrations.Migration):

dependencies = [
("pets", "0017_peteventsetting_no_handling_during_shedding"),
]

operations = [
migrations.RunPython(apply_shedding_handling_defaults, clear_shedding_handling_defaults),
]
18 changes: 18 additions & 0 deletions pets/migrations/0019_event_ended_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.13 on 2026-07-30 19:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pets', '0018_peteventsetting_shedding_defaults'),
]

operations = [
migrations.AddField(
model_name='event',
name='ended_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Когда закончилось'),
),
]
32 changes: 31 additions & 1 deletion pets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import timezone

Expand Down Expand Up @@ -371,6 +372,20 @@ class Source(models.TextChoices):
default=True,
verbose_name="Показывать быструю кнопку",
)
default_no_handling_days = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name="Запрет контакта по умолчанию (дней)",
validators=[MaxValueValidator(365)],
)
no_handling_during_shedding = models.BooleanField(
default=False,
verbose_name="Не трогать во время линьки",
)
comment = models.TextField(
blank=True,
verbose_name="Комментарий к настройке",
)
source = models.CharField(
max_length=20,
choices=Source.choices,
Expand Down Expand Up @@ -800,6 +815,12 @@ class EventType(models.TextChoices):
help_text="Переходное поле для будущей архитектуры событий. Старое поле event_datetime остаётся рабочим.",
)

ended_at = models.DateTimeField(
blank=True,
null=True,
verbose_name="Когда закончилось",
)

comment = models.TextField(
blank=True,
verbose_name="Комментарий",
Expand Down Expand Up @@ -877,6 +898,14 @@ def clean(self):
if self.event_datetime and self.event_datetime > timezone.now():
raise ValidationError({"event_datetime": "Дата и время события не могут быть в будущем."})

if self.ended_at:
if self.ended_at > timezone.now():
raise ValidationError({"ended_at": "Дата и время окончания события не могут быть в будущем."})

start_dt = self.happened_at or self.event_datetime
if start_dt and self.ended_at <= start_dt:
raise ValidationError({"ended_at": "Окончание события должно быть позже начала."})

def save(self, *args, **kwargs):
"""Сохраняет событие и обновляет параметры питомца, если это событие измерения."""

Expand Down Expand Up @@ -923,7 +952,8 @@ def get_latest_measurement_event_for_field(self, field_name: str) -> "Event | No
event_type=self.EventType.MEASUREMENT,
**{f"{field_name}__isnull": False},
)
.order_by("-event_datetime", "-pk")
.annotate(event_happened_at_sort=Coalesce("happened_at", "event_datetime"))
.order_by("-event_happened_at_sort", "-pk")
.first()
)

Expand Down
Loading
Loading