Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
132 changes: 131 additions & 1 deletion pets/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin

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


@admin.register(Pet)
Expand Down Expand Up @@ -194,6 +194,118 @@ class PetEventSettingAdmin(admin.ModelAdmin):
ordering = ("pet__name", "event_type__name")


@admin.register(CareRule)
class CareRuleAdmin(admin.ModelAdmin):
"""Настройки отображения правил регулярного ухода."""

list_display = (
"id",
"pet",
"event_type",
"interval_value",
"interval_unit",
"calculate_from",
"start_date",
"is_active",
"created_by",
"updated_at",
)
list_filter = (
"event_type",
"interval_unit",
"calculate_from",
"is_active",
"start_date",
"created_at",
)
search_fields = (
"pet__name",
"pet__species_name",
"pet__owner__email",
"event_type__slug",
"event_type__name",
"created_by__email",
)
autocomplete_fields = ("pet", "event_type", "created_by")
readonly_fields = ("created_at", "updated_at")
list_select_related = ("pet", "event_type", "created_by", "pet__owner")
ordering = ("pet__name", "event_type__name", "start_date")


@admin.register(PlannedEvent)
class PlannedEventAdmin(admin.ModelAdmin):
"""Настройки отображения плановых событий ухода."""

list_display = (
"id",
"pet",
"event_type",
"scheduled_at",
"status",
"care_rule",
"actual_event",
"created_by",
"updated_at",
)
list_filter = (
"event_type",
"status",
"scheduled_at",
"created_at",
)
search_fields = (
"pet__name",
"pet__species_name",
"pet__owner__email",
"event_type__slug",
"event_type__name",
"title",
"comment",
"created_by__email",
)
autocomplete_fields = ("pet", "event_type", "care_rule", "actual_event", "created_by")
readonly_fields = ("created_at", "updated_at")
list_select_related = ("pet", "event_type", "care_rule", "actual_event", "created_by", "pet__owner")
ordering = ("scheduled_at",)


@admin.register(PetCondition)
class PetConditionAdmin(admin.ModelAdmin):
"""Настройки отображения состояний питомцев."""

list_display = (
"id",
"pet",
"condition_type",
"started_at",
"ended_at",
"is_active",
"source_event",
"end_event",
"created_by",
"updated_at",
)
list_filter = (
"condition_type",
"is_active",
"started_at",
"ended_at",
"created_at",
)
search_fields = (
"pet__name",
"pet__species_name",
"pet__owner__email",
"title",
"comment",
"created_by__email",
)
autocomplete_fields = ("pet", "source_event", "end_event", "created_by")
readonly_fields = ("created_at", "updated_at")
list_select_related = ("pet", "source_event", "end_event", "created_by", "pet__owner")
ordering = ("-started_at",)


@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
"""Настройки отображения модели события в админке."""
Expand All @@ -204,6 +316,7 @@ class EventAdmin(admin.ModelAdmin):
"event_type",
"event_type_ref",
"event_datetime",
"happened_at",
"owner",
"repeat_after_days",
"no_handling_days",
Expand All @@ -213,6 +326,7 @@ class EventAdmin(admin.ModelAdmin):
"event_type",
"event_type_ref",
"event_datetime",
"happened_at",
"created_at",
)
search_fields = (
Expand Down Expand Up @@ -240,6 +354,7 @@ class EventAdmin(admin.ModelAdmin):
"event_type_ref",
"title",
"event_datetime",
"happened_at",
"comment",
)
},
Expand All @@ -260,3 +375,18 @@ class EventAdmin(admin.ModelAdmin):
{"fields": ("created_at",)},
),
)

def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Уточняет подписи переходных полей даты события в админке."""
if db_field.name == "event_datetime":
kwargs["label"] = "Дата и время события (старое поле)"
kwargs["help_text"] = "Старое рабочее поле. Пока должно совпадать с фактическим временем события."

if db_field.name == "happened_at":
kwargs["label"] = "Фактическая дата и время события"
kwargs["help_text"] = (
"Новое переходное поле для факта события. "
"Отдельное время завершения будет добавляться позже как новое поле."
)

return super().formfield_for_dbfield(db_field, request, **kwargs)
22 changes: 19 additions & 3 deletions pets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

# ---------------- PET ----------------

REPEAT_AFTER_DAYS_HELP_TEXT = (
"Событие будет повторяться через указанный интервал; "
"правило ухода сохранится отдельно от этого события."
)


class PetForm(forms.ModelForm):
"""Форма для создания и редактирования питомца."""
Expand Down Expand Up @@ -237,6 +242,16 @@ def clean_comment(self):
"""Удаляет лишние пробелы из начала и конца комментария"""
return self.cleaned_data.get("comment", "").strip()

def clean(self):
"""Синхронизирует переходную дату факта со старым полем формы."""
cleaned_data = super().clean()
event_datetime = cleaned_data.get("event_datetime")

if event_datetime:
self.instance.happened_at = event_datetime

return cleaned_data


# ---------------- SPECIFIC FORMS ----------------

Expand All @@ -255,7 +270,7 @@ class Meta(BaseEventForm.Meta):
]
help_texts = {
"no_handling_days": "Сколько дней нельзя брать питомца после кормления",
"repeat_after_days": "Когда нужно следующее кормление",
"repeat_after_days": REPEAT_AFTER_DAYS_HELP_TEXT,
}


Expand Down Expand Up @@ -287,7 +302,7 @@ class Meta(BaseEventForm.Meta):
"comment",
]
help_texts = {
"repeat_after_days": "Через сколько дней нужна следующая уборка",
"repeat_after_days": REPEAT_AFTER_DAYS_HELP_TEXT,
}


Expand All @@ -307,6 +322,7 @@ class Meta(BaseEventForm.Meta):
help_texts = {
"weight_grams": "Можно указать только вес или только длину",
"length_cm": "Можно указать только длину или только вес",
"repeat_after_days": REPEAT_AFTER_DAYS_HELP_TEXT,
}


Expand All @@ -324,6 +340,6 @@ class Meta(BaseEventForm.Meta):
"comment",
]
help_texts = {
"repeat_after_days": "Если поле не нужно — можно оставить пустым.",
"repeat_after_days": REPEAT_AFTER_DAYS_HELP_TEXT,
"no_handling_days": "Если поле не нужно — можно оставить пустым.",
}
39 changes: 39 additions & 0 deletions pets/management/commands/backfill_care_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.core.management.base import BaseCommand, CommandParser

from pets.services import backfill_care_rules_from_repeat_events


class Command(BaseCommand):
"""Создаёт правила ухода из старых повторяющихся событий."""

help = "Готовит CareRule из старых событий с repeat_after_days."

def add_arguments(self, parser: CommandParser) -> None:
"""Добавляет флаг применения найденных правил."""
parser.add_argument(
"--apply",
action="store_true",
help="Создать найденные правила CareRule.",
)

def handle(self, *args, **options) -> None:
"""Запускает подготовку правил ухода и выводит краткую сводку."""
result = backfill_care_rules_from_repeat_events(apply_changes=options["apply"])
mode = "правила созданы" if result.applied else "предпросмотр без записи"

self.stdout.write(self.style.SUCCESS(f"Подготовка CareRule завершена: {mode}."))
self.stdout.write(f"Событий с повторяемостью проверено: {result.repeat_events_checked}")

if result.applied:
self.stdout.write(f"Правил создано: {result.created_rules_count}")
else:
self.stdout.write(f"Правил будет создано: {result.candidate_rules_count}")

self.stdout.write(f"Пропущено из-за существующих правил: {result.skipped_existing_rules_count}")
self.stdout.write(f"Пропущено без типа события: {result.skipped_missing_event_type_count}")
self.stdout.write(f"Пропущено с неповторяемым типом события: {result.skipped_non_recurring_event_type_count}")

if not result.applied and result.candidate_rules_count:
self.stdout.write(
self.style.WARNING("Найдены правила для создания. Запустите команду с --apply, чтобы сохранить их.")
)
38 changes: 38 additions & 0 deletions pets/management/commands/backfill_pet_conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand, CommandParser

from pets.services import backfill_pet_conditions_from_events


class Command(BaseCommand):
"""Создаёт активные состояния питомцев из старых событий."""

help = "Готовит PetCondition из старых событий с активными no_handling_days."

def add_arguments(self, parser: CommandParser) -> None:
"""Добавляет флаг применения найденных состояний."""
parser.add_argument(
"--apply",
action="store_true",
help="Создать найденные PetCondition.",
)

def handle(self, *args, **options) -> None:
"""Запускает подготовку состояний питомцев и выводит краткую сводку."""
result = backfill_pet_conditions_from_events(apply_changes=options["apply"])
mode = "состояния созданы" if result.applied else "предпросмотр без записи"

self.stdout.write(self.style.SUCCESS(f"Подготовка PetCondition завершена: {mode}."))
self.stdout.write(f"Событий с ограничениями проверено: {result.events_checked}")

if result.applied:
self.stdout.write(f"Состояний создано: {result.created_conditions_count}")
else:
self.stdout.write(f"Состояний будет создано: {result.candidate_conditions_count}")

self.stdout.write(f"Пропущено из-за существующих состояний: {result.skipped_existing_conditions_count}")
self.stdout.write(f"Пропущено из-за истёкших ограничений: {result.skipped_expired_conditions_count}")

if not result.applied and result.candidate_conditions_count:
self.stdout.write(
self.style.WARNING("Найдены состояния для создания. Запустите команду с --apply, чтобы сохранить их.")
)
41 changes: 41 additions & 0 deletions pets/management/commands/generate_planned_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand, CommandParser

from pets.services import generate_planned_events_from_care_rules


class Command(BaseCommand):
"""Создаёт ближайшие плановые задачи из правил ухода."""

help = "Готовит PlannedEvent из активных CareRule."

def add_arguments(self, parser: CommandParser) -> None:
"""Добавляет флаг применения найденных задач."""
parser.add_argument(
"--apply",
action="store_true",
help="Создать найденные PlannedEvent.",
)

def handle(self, *args, **options) -> None:
"""Запускает подготовку плановых задач и выводит краткую сводку."""
result = generate_planned_events_from_care_rules(apply_changes=options["apply"])
mode = "задачи созданы" if result.applied else "предпросмотр без записи"

self.stdout.write(self.style.SUCCESS(f"Подготовка PlannedEvent завершена: {mode}."))
self.stdout.write(f"Правил ухода проверено: {result.care_rules_checked}")

if result.applied:
self.stdout.write(f"Плановых задач создано: {result.created_tasks_count}")
else:
self.stdout.write(f"Плановых задач будет создано: {result.candidate_tasks_count}")

self.stdout.write(f"Пропущено из-за существующих плановых задач: {result.skipped_existing_tasks_count}")
self.stdout.write(f"Пропущено без даты планирования: {result.skipped_missing_datetime_count}")
self.stdout.write(f"Пропущено с непланируемым типом события: {result.skipped_non_plannable_event_type_count}")

if not result.applied and result.candidate_tasks_count:
self.stdout.write(
self.style.WARNING(
"Найдены плановые задачи для создания. Запустите команду с --apply, чтобы сохранить их."
)
)
9 changes: 9 additions & 0 deletions pets/management/commands/recalculate_pet_statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def handle(self, *args, **options) -> None:
self.stdout.write(f"Активных линек: {result.active_shedding_count}")
self.stdout.write(f"Ближайших повторяющихся задач: {result.upcoming_tasks_count}")

if result.applied:
self.stdout.write(f"Правил ухода создано: {result.care_rules_created}")
self.stdout.write(f"Плановых задач создано: {result.planned_events_created}")
self.stdout.write(f"Состояний питомцев создано: {result.pet_conditions_created}")
else:
self.stdout.write(f"Правил ухода будет создано: {result.care_rules_to_create}")
self.stdout.write(f"Плановых задач будет создано: {result.planned_events_to_create}")
self.stdout.write(f"Состояний питомцев будет создано: {result.pet_conditions_to_create}")

if not result.applied and result.pets_with_measurement_changes:
self.stdout.write(
self.style.WARNING(
Expand Down
Loading
Loading