added scheduler v2
All checks were successful
Test Build / test-build (push) Successful in 25s

This commit is contained in:
2025-07-31 03:50:08 +03:00
parent 4f66edbb21
commit 0a17b31c06
6 changed files with 430 additions and 94 deletions

View File

@@ -3,12 +3,12 @@ import os
import json
import shutil
from flask import (
Blueprint, render_template, request, redirect, url_for, flash, g, session, current_app
Blueprint, render_template, request, redirect, url_for, flash, g, session, current_app, jsonify
)
from flask_login import login_user, login_required, logout_user, current_user
from flask_babel import _
from cron_descriptor import ExpressionDescriptor, CasingTypeEnum, Options
from werkzeug.utils import secure_filename
import gspread
# --- ИМПОРТ РАСШИРЕНИЙ И МОДЕЛЕЙ ---
# Импортируем экземпляры расширений, созданные в app.py
@@ -17,7 +17,7 @@ from extensions import db, login_manager, scheduler
from models import User, UserConfig
from google_sheets import GoogleSheets
from request_module import ReqModule
from utils import calculate_period_dates, get_dates, generate_template_from_preset, render_temp
from utils import calculate_period_dates, get_dates, generate_template_from_preset, render_temp, _parse_cron_string
# --- Создание блюпринта ---
@@ -59,14 +59,6 @@ def get_user_upload_path(filename=""):
os.makedirs(user_dir, exist_ok=True)
return os.path.join(user_dir, secure_filename(filename))
def _parse_cron_string(cron_str):
"""Парсит строку cron в словарь для APScheduler. Локальная копия для удобства."""
parts = cron_str.split()
if len(parts) != 5:
raise ValueError("Invalid cron string format. Expected 5 parts.")
keys = ['minute', 'hour', 'day', 'month', 'day_of_week']
return {keys[i]: part for i, part in enumerate(parts)}
# --- Маршруты ---
@main_bp.route('/language/<language>')
@@ -292,30 +284,38 @@ def mapping_set():
config = g.user_config
try:
new_mappings = {}
# Сохраняем существующие настройки расписания при обновлении отчетов
current_mappings = config.mappings or {}
# Итерируемся по всем листам, доступным пользователю в Google Sheets
for sheet in config.sheets:
# Получаем ID отчета, который пользователь выбрал для этого листа в форме
report_key = f"sheet_{sheet['id']}"
selected_report_id = request.form.get(report_key)
# Если пользователь выбрал отчет (а не оставил поле пустым)
if selected_report_id:
# Получаем существующие данные расписания для этого листа
existing_schedule = current_mappings.get(sheet['title'], {})
# Инициализируем переменные для расписания
schedule_cron = None
schedule_period = None
if isinstance(existing_mapping_value, dict):
schedule_cron = existing_mapping_value.get('schedule_cron')
schedule_period = existing_mapping_value.get('schedule_period')
# Ищем текущие настройки для этого конкретного листа в базе данных
current_sheet_mapping = current_mappings.get(sheet.get('title'))
# Сохраняем новые настройки расписания в новом словаре
new_mappings[sheet['title']] = {
# Если для этого листа уже есть настройки, и они в новом формате (словарь),
# то мы сохраняем его параметры расписания.
if isinstance(current_sheet_mapping, dict):
schedule_cron = current_sheet_mapping.get('schedule_cron')
schedule_period = current_sheet_mapping.get('schedule_period')
# Создаем новую запись сопоставления.
# Она будет содержать НОВЫЙ ID отчета и СТАРЫЕ (сохраненные) настройки расписания.
new_mappings[sheet.get('title')] = {
'report_id': selected_report_id,
'schedule_cron': schedule_cron,
'schedule_period': schedule_period
}
# Полностью заменяем старые сопоставления на новые
config.mappings = new_mappings
db.session.commit()
@@ -327,12 +327,11 @@ def mapping_set():
return redirect(url_for('.index'))
def execute_olap_export(user_id, sheet_title, start_date_str=None, end_date_str=None):
def execute_olap_export(app, user_id, sheet_title, start_date_str=None, end_date_str=None):
"""
Основная логика выгрузки OLAP-отчета. Может вызываться как из эндпоинта, так и из планировщика.
Если start_date_str и end_date_str не переданы, вычисляет их на основе расписания.
"""
app = current_app._get_current_object()
with app.app_context():
user = db.session.get(User, user_id)
if not user:
@@ -464,7 +463,7 @@ def render_olap():
try:
# Просто вызываем нашу новую универсальную функцию
execute_olap_export(current_user.id, sheet_title, start_date, end_date)
execute_olap_export(current_app._get_current_object(), current_user.id, sheet_title, start_date, end_date)
flash(_('Report generation task for sheet "%(sheet)s" has been started. The data will appear shortly.', sheet=sheet_title), 'success')
except Exception as e:
flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error')
@@ -472,6 +471,42 @@ def render_olap():
return redirect(url_for('.index'))
@main_bp.route('/translate_cron', methods=['POST'])
@login_required
def translate_cron():
"""
Принимает cron-строку и возвращает ее человекочитаемое описание.
"""
cron_string = request.json.get('cron_string')
if not cron_string:
return jsonify({'description': ''})
try:
# 1. Получаем и преобразуем код локали
app_locale = session.get('language', 'ru')
locale_map = { 'ru': 'ru_RU', 'en': 'en_US' }
cron_locale = locale_map.get(app_locale, 'en_US')
# 2. Создаем объект Options ТОЛЬКО для форматирования
options = Options()
options.locale_code = cron_locale
options.casing_type = CasingTypeEnum.Sentence
options.use_24hour_time_format = True
# 3. Создаем ExpressionDescriptor, передавая ему ЯВНО и опции, и локаль
descriptor = ExpressionDescriptor(
expression=cron_string,
options=options
)
# 4. Получаем описание
description = descriptor.get_description()
return jsonify({'description': description})
except Exception as e:
current_app.logger.warning(f"Cron descriptor failed for string '{cron_string}' with locale '{cron_locale}': {e}")
return jsonify({'description': str(_('Invalid cron format'))})
@main_bp.route('/save_schedule', methods=['POST'])
@login_required
@@ -516,7 +551,7 @@ def save_schedule():
id=job_id,
func=execute_olap_export,
trigger='cron',
args=[current_user.id, sheet_title],
args=[current_app._get_current_object(), current_user.id, sheet_title],
replace_existing=True,
**cron_params
)