Удалён режим 'до даты' для повторяемых событий, исправлен патч django-scheduler, фронт теперь только 'count' и 'forever'
This commit is contained in:
parent
ba6e4a3ef0
commit
d6a3135bae
@ -5,3 +5,10 @@ class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
verbose_name = "REST API layer"
|
||||
|
||||
def ready(self):
|
||||
"""Инициализация при загрузке приложения."""
|
||||
# Применяем патч для django-scheduler (исправление бага с byweekday)
|
||||
print("🔧 ApiConfig.ready() - применение патча django-scheduler")
|
||||
import schedule_patch
|
||||
schedule_patch.apply_patch()
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
# backend/calendar_app/apps.py
|
||||
from __future__ import annotations
|
||||
|
||||
from django.apps import AppConfig, apps as django_apps
|
||||
from django.apps import AppConfig
|
||||
from django.apps import apps as django_apps
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
|
||||
|
||||
@ -16,8 +17,9 @@ class CalendarAppConfig(AppConfig):
|
||||
Raises:
|
||||
LookupError: Если приложение 'employees' или модель 'Employee' не найдены.
|
||||
"""
|
||||
from calendar_app import signals # noqa: F401 (гарантирует импорт модулей с хендлерами)
|
||||
import calendar_app.notification_signals # noqa: F401
|
||||
from calendar_app import \
|
||||
signals # noqa: F401 (гарантирует импорт модулей с хендлерами)
|
||||
|
||||
try:
|
||||
Employee = django_apps.get_model("employees", "Employee")
|
||||
@ -36,3 +38,5 @@ class CalendarAppConfig(AppConfig):
|
||||
sender=Employee,
|
||||
dispatch_uid="calendar_app.employee_deleted_remove_birthday",
|
||||
)
|
||||
dispatch_uid = "calendar_app.employee_deleted_remove_birthday",
|
||||
|
||||
|
||||
48
backend/check_event_params.py
Normal file
48
backend/check_event_params.py
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
"""Проверка параметров события в базе данных."""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Setup Django
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eusrr_backend.settings")
|
||||
django.setup()
|
||||
|
||||
# ЯВНЫЙ импорт патча
|
||||
import schedule_patch # noqa
|
||||
|
||||
from schedule.models import Event, Rule
|
||||
|
||||
# Найдём событие по названию
|
||||
event = Event.objects.filter(title__icontains='ыфвыфыф').first()
|
||||
|
||||
if event:
|
||||
print(f"=" * 80)
|
||||
print(f"Event ID: {event.id}")
|
||||
print(f"Title: {event.title}")
|
||||
print(f"Start: {event.start} (weekday: {event.start.weekday()})")
|
||||
print(f"End recurring: {event.end_recurring_period}")
|
||||
print(f"=" * 80)
|
||||
|
||||
if event.rule:
|
||||
print(f"\nRule ID: {event.rule.id}")
|
||||
print(f"Frequency: {event.rule.frequency}")
|
||||
print(f"Params: {event.rule.params}")
|
||||
print(f"=" * 80)
|
||||
|
||||
# Проверим occurrences
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
start = timezone.make_aware(datetime(2026, 2, 1, 0, 0, 0))
|
||||
end = timezone.make_aware(datetime(2026, 2, 28, 23, 59, 59))
|
||||
occurrences = event.get_occurrences(start, end)
|
||||
|
||||
print(f"\nOccurrences в феврале 2026:")
|
||||
for occ in occurrences:
|
||||
print(f" - {occ.start.strftime('%Y-%m-%d %A')} {occ.start.strftime('%H:%M')}")
|
||||
print(f"=" * 80)
|
||||
else:
|
||||
print("НЕТ ПРАВИЛА!")
|
||||
else:
|
||||
print("Событие не найдено!")
|
||||
@ -19,7 +19,8 @@ def _split_env_list(value: str) -> list[str]:
|
||||
return [x.strip() for x in (value or "").split(",") if x.strip()]
|
||||
|
||||
|
||||
ALLOWED_HOSTS = _split_env_list(os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1"))
|
||||
ALLOWED_HOSTS = _split_env_list(
|
||||
os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1"))
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"daphne",
|
||||
@ -43,6 +44,7 @@ INSTALLED_APPS = [
|
||||
"api.apps.ApiConfig",
|
||||
"hikcentral.apps.HikcentralConfig",
|
||||
"schedule", # django-scheduler (проверенная библиотека для календаря)
|
||||
"calendar_app.apps.CalendarAppConfig", # Наше приложение календаря
|
||||
"documents.apps.DocumentsConfig",
|
||||
"requests_app.apps.RequestsAppConfig",
|
||||
"feed.apps.FeedConfig",
|
||||
@ -66,7 +68,8 @@ MIDDLEWARE = [
|
||||
"api.middleware.JWTRefreshMiddleware", # Автообновление JWT токенов
|
||||
"eusrr_backend.middleware.AuthRequiredMiddleware",
|
||||
"eusrr_backend.middleware.EmailVerificationMiddleware",
|
||||
"eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", # IP ограничение для регистрации
|
||||
# IP ограничение для регистрации
|
||||
"eusrr_backend.middleware.RegistrationIPRestrictionMiddleware",
|
||||
"eusrr_backend.middleware.CacheControlMiddleware", # Cache-Control headers
|
||||
]
|
||||
|
||||
@ -249,7 +252,8 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
||||
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "true").lower() == "true"
|
||||
# Если SSL включён (465), TLS должен быть False
|
||||
EMAIL_USE_TLS = (
|
||||
False if EMAIL_USE_SSL else (os.getenv("EMAIL_USE_TLS", "false").lower() == "true")
|
||||
False if EMAIL_USE_SSL else (
|
||||
os.getenv("EMAIL_USE_TLS", "false").lower() == "true")
|
||||
)
|
||||
DEFAULT_FROM_EMAIL = os.getenv(
|
||||
"DEFAULT_FROM_EMAIL", EMAIL_HOST_USER or "webmaster@localhost"
|
||||
@ -260,7 +264,8 @@ DEFAULT_FROM_EMAIL = os.getenv(
|
||||
# -----------------------------------------------------------------------------
|
||||
# Порог для определения массовой рассылки (количество получателей)
|
||||
# При массовой рассылке уведомления создаются быстро, а отправка идёт в фоне
|
||||
NOTIFICATION_BULK_THRESHOLD = int(os.getenv("NOTIFICATION_BULK_THRESHOLD", "10"))
|
||||
NOTIFICATION_BULK_THRESHOLD = int(
|
||||
os.getenv("NOTIFICATION_BULK_THRESHOLD", "10"))
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CHANNELS & CACHE
|
||||
@ -321,7 +326,8 @@ SIMPLE_JWT = {
|
||||
}
|
||||
|
||||
# Автообновление JWT токенов: за сколько минут до истечения обновлять
|
||||
JWT_REFRESH_THRESHOLD_MINUTES = int(os.getenv("JWT_REFRESH_THRESHOLD_MIN", "5"))
|
||||
JWT_REFRESH_THRESHOLD_MINUTES = int(
|
||||
os.getenv("JWT_REFRESH_THRESHOLD_MIN", "5"))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ -332,10 +338,12 @@ JWT_REFRESH_THRESHOLD_MINUTES = int(os.getenv("JWT_REFRESH_THRESHOLD_MIN", "5"))
|
||||
# -----------------------------------------------------------------------------
|
||||
# Порядок важен: первый успешный бэкенд останавливает цепочку
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"eusrr_backend.auth_backends.LDAP3Backend", # работает только если LDAP_ENABLED=True
|
||||
# работает только если LDAP_ENABLED=True
|
||||
"eusrr_backend.auth_backends.LDAP3Backend",
|
||||
"eusrr_backend.auth_backends.EmailOrPhoneBackend", # фоллбэк для режима без LDAP
|
||||
"eusrr_backend.auth_backends.SuperuserOnlyBackend", # экстренный доступ для админа
|
||||
"eusrr_backend.auth_backends.PositionRoleBackend", # расчёт прав на основе должностей
|
||||
# расчёт прав на основе должностей
|
||||
"eusrr_backend.auth_backends.PositionRoleBackend",
|
||||
"django.contrib.auth.backends.ModelBackend", # стандартный Django бэкенд
|
||||
]
|
||||
|
||||
@ -350,7 +358,8 @@ LDAP_BIND_DN = os.getenv("LDAP_BIND_DN", "")
|
||||
LDAP_BIND_PASSWORD = os.getenv("LDAP_BIND_PASSWORD", "")
|
||||
|
||||
# TLS/CA
|
||||
LDAP_CA_CERTS = os.getenv("LDAP_CA_CERTS", "") # путь к CA bundle/серту (если нужен)
|
||||
# путь к CA bundle/серту (если нужен)
|
||||
LDAP_CA_CERTS = os.getenv("LDAP_CA_CERTS", "")
|
||||
LDAP_TLS_REQUIRED = os.getenv("LDAP_TLS_REQUIRED", "true").lower() == "true"
|
||||
|
||||
# Где искать пользователей
|
||||
@ -387,7 +396,8 @@ LDAP_SYNC_GROUPS = os.getenv("LDAP_SYNC_GROUPS", "false").lower() == "true"
|
||||
LDAP_GROUP_ATTR = os.getenv("LDAP_GROUP_ATTR", "memberOf")
|
||||
# Пример: {"CN=HR,OU=Groups,DC=...,DC=...": "hr"}
|
||||
LDAP_GROUP_MAP = {}
|
||||
LDAP_GROUPS_EXCLUSIVE = os.getenv("LDAP_GROUPS_EXCLUSIVE", "false").lower() == "true"
|
||||
LDAP_GROUPS_EXCLUSIVE = os.getenv(
|
||||
"LDAP_GROUPS_EXCLUSIVE", "false").lower() == "true"
|
||||
|
||||
# WRITE-BACK
|
||||
LDAP_WRITE_ENABLED = os.getenv("LDAP_WRITE_ENABLED", "false").lower() == "true"
|
||||
@ -448,7 +458,6 @@ LDAP_REGISTRATION_CREATE = True
|
||||
LDAP_POSITIONS_BASE = os.getenv("LDAP_POSITIONS_BASE")
|
||||
|
||||
|
||||
|
||||
BRAND_NAME = os.getenv("BRAND_NAME", "HiRo")
|
||||
BRAND_LOGO = "img/logo.png"
|
||||
|
||||
@ -471,7 +480,8 @@ VAPID_ADMIN_EMAIL = os.getenv("VAPID_ADMIN_EMAIL", "robotail-info@yandex.ru")
|
||||
# CELERY CONFIGURATION
|
||||
# -----------------------------------------------------------------------------
|
||||
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
||||
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/1')
|
||||
CELERY_RESULT_BACKEND = os.getenv(
|
||||
'CELERY_RESULT_BACKEND', 'redis://localhost:6379/1')
|
||||
|
||||
# Сериализация
|
||||
CELERY_ACCEPT_CONTENT = ['json']
|
||||
|
||||
101
backend/schedule_patch.py
Normal file
101
backend/schedule_patch.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""
|
||||
Monkey patch для django-scheduler: исправление бага с byweekday.
|
||||
|
||||
ПРОБЛЕМА:
|
||||
---------
|
||||
Когда событие создается в день, который входит в byweekday, django-scheduler
|
||||
заменяет весь массив byweekday на только этот день.
|
||||
|
||||
ПРИМЕР БАГА:
|
||||
- Создаем событие в пятницу (weekday = 4)
|
||||
- Выбираем byweekday = [1, 4] (вторник и пятница)
|
||||
- django-scheduler видит: 4 in [1, 4] = True
|
||||
- Результат: event_params['byweekday'] = [4] вместо [1, 4]
|
||||
- События создаются только в пятницу, вторник игнорируется
|
||||
|
||||
КОД БАГА (schedule/models/events.py, строки 375-378):
|
||||
if sp == rule_params[param] or (
|
||||
hasattr(rule_params[param], "__iter__") and sp in rule_params[param]
|
||||
):
|
||||
event_params[param] = [sp] # ← БАГ!
|
||||
|
||||
РЕШЕНИЕ:
|
||||
--------
|
||||
Переопределяем Event._event_params() с исправленной логикой для byweekday.
|
||||
Используем ленивый импорт чтобы избежать проблем с порядком загрузки Django.
|
||||
"""
|
||||
|
||||
|
||||
def create_patched_method():
|
||||
"""Создаёт пропатченный метод с ленивым импортом констант."""
|
||||
# Ленивый импорт - только при создании метода
|
||||
from schedule.models.events import freq_dict_order, param_dict_order
|
||||
|
||||
def patched_event_params(self):
|
||||
"""
|
||||
Исправленная версия Event._event_params().
|
||||
|
||||
Для byweekday с несколькими днями НЕ заменяем массив, если день начала входит в него.
|
||||
"""
|
||||
freq_order = freq_dict_order[self.rule.frequency]
|
||||
rule_params = self.event_rule_params
|
||||
start_params = self.event_start_params
|
||||
event_params = {}
|
||||
|
||||
if len(rule_params) == 0:
|
||||
return event_params
|
||||
|
||||
for param in rule_params:
|
||||
if (
|
||||
param in param_dict_order
|
||||
and param_dict_order[param] > freq_order
|
||||
and param in start_params
|
||||
):
|
||||
sp = start_params[param]
|
||||
|
||||
# ИСПРАВЛЕНИЕ: Для byweekday не заменяем массив
|
||||
if param == 'byweekday' and hasattr(rule_params[param], "__iter__"):
|
||||
event_params[param] = rule_params[param]
|
||||
elif sp == rule_params[param] or (
|
||||
hasattr(rule_params[param], "__iter__") and sp in rule_params[param]
|
||||
):
|
||||
event_params[param] = [sp]
|
||||
else:
|
||||
event_params[param] = rule_params[param]
|
||||
else:
|
||||
event_params[param] = rule_params[param]
|
||||
|
||||
return event_params
|
||||
|
||||
return patched_event_params
|
||||
|
||||
|
||||
def apply_patch():
|
||||
"""Применяет патч к django-scheduler Event модели."""
|
||||
try:
|
||||
# Ленивый импорт - только при вызове apply_patch()
|
||||
from schedule.models import Event
|
||||
|
||||
print("=" * 80)
|
||||
print("✅ ПРИМЕНЕНИЕ ПАТЧА django-scheduler")
|
||||
|
||||
# Создаём и применяем пропатченный метод
|
||||
patched_method = create_patched_method()
|
||||
Event._event_params = patched_method
|
||||
|
||||
print(f" Патч применён: Event._event_params = {patched_method.__name__}")
|
||||
print("=" * 80)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print("=" * 80)
|
||||
print(f"❌ ОШИБКА применения патча: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("=" * 80)
|
||||
return False
|
||||
|
||||
|
||||
# НЕ вызываем apply_patch() здесь!
|
||||
# Патч будет применён через AppConfig.ready() в calendar_app/apps.py
|
||||
|
||||
@ -113,10 +113,7 @@
|
||||
<div class="ms-4 ps-2">
|
||||
<small class="text-muted d-block mb-2">Заканчивается:</small>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<input type="date" class="form-control form-control-sm" name="recurrence_until" id="recurrenceUntil" placeholder="Дата окончания">
|
||||
<small class="text-muted">До даты</small>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<input type="number" class="form-control form-control-sm" name="recurrence_count" id="recurrenceCount" min="1" placeholder="Кол-во">
|
||||
<small class="text-muted">После N раз</small>
|
||||
|
||||
50
backend/test_patch_import.py
Normal file
50
backend/test_patch_import.py
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
"""Проверка импорта и применения патча."""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Setup Django
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eusrr_backend.settings")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("1. ЗАПУСК DJANGO")
|
||||
print("=" * 80)
|
||||
django.setup()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("2. ИМПОРТ Event БЕЗ ПАТЧА")
|
||||
print("=" * 80)
|
||||
from schedule.models import Event
|
||||
print(f"Event._event_params: {Event._event_params}")
|
||||
print(f"Имя метода: {Event._event_params.__name__}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("3. ИМПОРТ ПАТЧА")
|
||||
print("=" * 80)
|
||||
import schedule_patch # noqa
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("4. ПРОВЕРКА Event ПОСЛЕ ПАТЧА")
|
||||
print("=" * 80)
|
||||
print(f"Event._event_params: {Event._event_params}")
|
||||
print(f"Имя метода: {Event._event_params.__name__}")
|
||||
|
||||
# Проверим на реальном событии
|
||||
print("\n" + "=" * 80)
|
||||
print("5. ТЕСТ НА РЕАЛЬНОМ СОБЫТИИ")
|
||||
print("=" * 80)
|
||||
event = Event.objects.filter(title__icontains='ыфвыфыф').first()
|
||||
if event:
|
||||
print(f"Event ID: {event.id}")
|
||||
result = event._event_params()
|
||||
print(f"_event_params() result: {result}")
|
||||
if 'byweekday' in result:
|
||||
print(f"byweekday = {result['byweekday']}")
|
||||
if len(result['byweekday']) > 1:
|
||||
print("✅ ПАТЧ РАБОТАЕТ - несколько дней сохранены!")
|
||||
else:
|
||||
print("❌ ПАТЧ НЕ РАБОТАЕТ - только один день!")
|
||||
else:
|
||||
print("Событие не найдено")
|
||||
Loading…
x
Reference in New Issue
Block a user