Files
site/WESP_REL/app/models/__init__.py
T
2026-07-17 12:57:18 +03:00

290 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from sqlalchemy import event
from sqlalchemy.orm import Session
# Импортируем модели, чтобы метаданные SQLAlchemy были доступны при инициализации.
from .component import Component, Ingredient # noqa: F401
from .recipe import Recipe, UnloadingGroup, PeriodRecipe # noqa: F401
from .report import ( # noqa: F401
LoadingReport,
LoadingReportComponent,
ComponentLoadingTime,
UnloadingReport,
UnloadingReportGroup,
)
from .feed_alert import FeedAlert # noqa: F401
from .feed_quality_settings import FeedQualitySettings # noqa: F401
from .org_settings import OrgSettings # noqa: F401
from .daily_component_norm_adjustment import DailyComponentNormAdjustment # noqa: F401
from .daily_trip_skip import DailyTripSkip # noqa: F401
from .daily_ingredient_skip import DailyIngredientSkip # noqa: F401
from .daily_ingredient_replacement import DailyIngredientReplacement # noqa: F401
from .daily_unloading_group_skip import DailyUnloadingGroupSkip # noqa: F401
from .equipment import ( # noqa: F401
FeedMixer,
FeedDispenser,
FeedingLocation,
FeedingPeriod,
FeedingPoint,
Trip,
)
from .sklad import ComponentStock # noqa: F401
from .hardware_setting import HardwareSetting # noqa: F401
from .kiosk import KioskDevice, KioskPairToken # noqa: F401
from .user import WebUser # noqa: F401
from .auto_update_settings import AutoUpdateSettings # noqa: F401
from .zootech_notification import ZootechNotification # noqa: F401
from .sync import ( # noqa: F401
SyncMetadata,
SyncClient,
SyncClientDisplayName,
SyncEngineState,
SyncQueue,
SyncDelivery,
SyncConflict,
)
from .base import TimestampMixin, SoftDeleteMixin # noqa: F401
def _cascade_on_parent_soft_delete(mapper, connection, target) -> None: # type: ignore[override]
if not getattr(target, "is_deleted", False):
return
table = getattr(target, "__tablename__", "")
if not table:
return
from sqlalchemy.orm import object_session
from app.services.sync_cascade import CASCADE_CHILDREN, cascade_soft_delete
if table not in CASCADE_CHILDREN:
return
sess = object_session(target)
suppress_enqueue = bool(
sess is not None and sess.info.get(WESP_SUPPRESS_SYNC_ENQUEUE)
)
cascade_soft_delete(
table,
str(getattr(target, "id", "")),
deleted_by=str(getattr(target, "deleted_by", None) or f"cascade:{table}"),
enqueue=not suppress_enqueue,
defer_enqueue=True,
)
for _parent_table in (
"recipe",
"component",
"feed_dispenser",
"feeding_period",
"feed_mixer",
"loading_report",
"unloading_report",
):
_parent_model = {
"recipe": Recipe,
"component": Component,
"feed_dispenser": FeedDispenser,
"feeding_period": FeedingPeriod,
"feed_mixer": FeedMixer,
"loading_report": LoadingReport,
"unloading_report": UnloadingReport,
}[_parent_table]
event.listen(_parent_model, "after_update", _cascade_on_parent_soft_delete)
def _sync_record_id(target) -> str:
table = getattr(target, "__tablename__", "")
if table == "period_recipes":
return f"{target.period_id}:{target.recipe_id}"
return target.id
_SYNC_ENQUEUE_INFO_KEY = "_wesp_sync_enqueue_pending"
# session.info: применение pull на клиенте (sync_client.apply_batch) — не ставить задачи в sync_queue.
WESP_SUPPRESS_SYNC_ENQUEUE = "_wesp_suppress_sync_enqueue"
def _schedule_sync_enqueue_from_listener(target, action: str, priority: int) -> None:
"""Не вызывать enqueue_sync_queue_task из after_insert/after_update — это внутри flush."""
from sqlalchemy.orm import object_session
sess = object_session(target)
if sess is not None and sess.info.get(WESP_SUPPRESS_SYNC_ENQUEUE):
return
rid = _sync_record_id(target)
row = (target.__tablename__, str(rid), action, priority, None)
if sess is None:
from app.services.sync_manager import enqueue_sync_queue_task
tname, rec_id, act, pri, tid = row
enqueue_sync_queue_task(tname, rec_id, act, priority=pri, target_node_id=tid)
return
sess.info.setdefault(_SYNC_ENQUEUE_INFO_KEY, []).append(row)
@event.listens_for(Session, "after_flush")
def _flush_pending_sync_enqueues(session, flush_context) -> None: # type: ignore[override]
from app.services.sync_cascade import CASCADE_ENQUEUE_INFO_KEY
pending = session.info.pop(_SYNC_ENQUEUE_INFO_KEY, None)
cascade_pending = session.info.pop(CASCADE_ENQUEUE_INFO_KEY, None)
from app.services.sync_manager import enqueue_sync_queue_task
if cascade_pending:
merged_cascade: set[tuple[str, str]] = set()
for tname, rid in cascade_pending:
merged_cascade.add((tname, rid))
with session.no_autoflush:
for tname, rid in merged_cascade:
enqueue_sync_queue_task(tname, rid, "update", priority=1)
if not pending:
return
merged: dict[tuple[str, str, str, str | None], tuple] = {}
for table_name, record_id, action, priority, target_node_id in pending:
key = (table_name, record_id, action, target_node_id)
pri = int(priority)
prev = merged.get(key)
if prev is None or pri > prev[3]:
merged[key] = (table_name, record_id, action, pri, target_node_id)
with session.no_autoflush:
for tname, rec_id, act, pri, tid in merged.values():
enqueue_sync_queue_task(tname, rec_id, act, priority=pri, target_node_id=tid)
def _sync_task_after_insert(mapper, connection, target): # type: ignore[override]
"""Универсальная постановка в sync_queue после insert (паритет с legacy sync_models)."""
priority_map = {
"component": 2,
"recipe": 2,
"ingredient": 3,
"unloading_group": 4,
"loading_report": 4,
"loading_report_component": 4,
"component_loading_time": 4,
"unloading_report": 4,
"unloading_report_group": 4,
"feed_mixer": 3,
"feeding_location": 3,
"feeding_period": 3,
"feeding_point": 3,
"trip": 4,
"feed_dispenser": 3,
"period_recipes": 3,
"daily_trip_skip": 2,
"daily_ingredient_skip": 2,
"daily_unloading_group_skip": 2,
"daily_ingredient_replacement": 2,
"daily_component_norm_adjustment": 2,
}
priority = priority_map.get(target.__tablename__, 3)
_schedule_sync_enqueue_from_listener(target, "create", priority)
def _sync_task_after_update(mapper, connection, target): # type: ignore[override]
# soft_delete / каскад: update в очередь не ставим — delete или cascade_soft_delete уже поставили задачи
if getattr(target, "is_deleted", False):
return
priority_map = {
"component": 3,
"recipe": 3,
"ingredient": 4,
"unloading_group": 4,
"loading_report": 4,
"loading_report_component": 4,
"component_loading_time": 4,
"unloading_report": 4,
"unloading_report_group": 4,
"feed_mixer": 4,
"feeding_location": 4,
"feeding_period": 4,
"feeding_point": 4,
"trip": 4,
"feed_dispenser": 4,
"period_recipes": 4,
"daily_trip_skip": 3,
"daily_ingredient_skip": 3,
"daily_unloading_group_skip": 3,
"daily_ingredient_replacement": 3,
"daily_component_norm_adjustment": 3,
}
priority = priority_map.get(target.__tablename__, 4)
_schedule_sync_enqueue_from_listener(target, "update", priority)
_sync_models = (
Component,
Recipe,
Ingredient,
UnloadingGroup,
LoadingReport,
LoadingReportComponent,
ComponentLoadingTime,
UnloadingReport,
UnloadingReportGroup,
FeedMixer,
FeedingLocation,
FeedingPeriod,
FeedingPoint,
Trip,
FeedDispenser,
PeriodRecipe,
DailyTripSkip,
DailyIngredientSkip,
DailyUnloadingGroupSkip,
DailyIngredientReplacement,
DailyComponentNormAdjustment,
)
def _model_has_content_hash_column(cls) -> bool:
try:
return "content_hash" in cls.__table__.columns
except Exception:
return False
def _sync_content_hash_after_insert(mapper, connection, target) -> None: # type: ignore[override]
"""После INSERT строка уже содержит значения по умолчанию из БД — хеш совпадает с get_object_data."""
from sqlalchemy import and_, update
from app.services.sync_content_hash import compute_content_hash_for_object
if not hasattr(target, "content_hash"):
return
h = compute_content_hash_for_object(target)
if not h:
return
table = mapper.local_table
pk_cols = list(table.primary_key.columns)
stmt = update(table).values(content_hash=h)
if len(pk_cols) == 1:
pk = pk_cols[0]
stmt = stmt.where(pk == getattr(target, pk.key))
else:
stmt = stmt.where(and_(*(c == getattr(target, c.key) for c in pk_cols)))
connection.execute(stmt)
target.content_hash = h
for _sm in _sync_models:
if _model_has_content_hash_column(_sm):
event.listen(_sm, "after_insert", _sync_content_hash_after_insert)
for _sm in _sync_models:
event.listen(_sm, "after_insert", _sync_task_after_insert)
event.listen(_sm, "after_update", _sync_task_after_update)
@event.listens_for(Session, "before_flush")
def _wesp_recompute_content_hashes_before_flush(session, flush_context, instances) -> None: # type: ignore[override]
from app.services.sync_content_hash import refresh_content_hash_if_applicable
for obj in list(session.dirty):
if hasattr(obj, "content_hash"):
refresh_content_hash_if_applicable(obj)