Compare commits
5 Commits
4ebe15522f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c11be1460 | |||
| 0a17b31c06 | |||
| 4f66edbb21 | |||
| 38f35d1915 | |||
| ca8e70781c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
|||||||
/.env
|
/.env
|
||||||
/.idea
|
/.idea
|
||||||
cred.json
|
cred.json
|
||||||
*.db
|
*.db
|
||||||
|
*/*.log
|
||||||
85
app.py
85
app.py
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from flask import Flask, session, request
|
from flask import Flask, session, request
|
||||||
|
from sqlalchemy import inspect
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# 1. Загрузка переменных окружения - в самом верху
|
# 1. Загрузка переменных окружения - в самом верху
|
||||||
@@ -16,6 +17,34 @@ def create_app():
|
|||||||
"""
|
"""
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
if not app.debug:
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
# Создаем папку для логов, если ее нет
|
||||||
|
log_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
|
||||||
|
log_file = os.path.join(log_dir, 'olaper.log')
|
||||||
|
|
||||||
|
# Создаем обработчик, который пишет логи в файл с ротацией
|
||||||
|
# 10 МБ на файл, храним 5 старых файлов
|
||||||
|
file_handler = RotatingFileHandler(log_file, maxBytes=1024*1024*10, backupCount=5, encoding='utf-8')
|
||||||
|
|
||||||
|
# Устанавливаем формат сообщений
|
||||||
|
file_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Устанавливаем уровень логирования
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Добавляем обработчик к логгеру приложения
|
||||||
|
app.logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.INFO)
|
||||||
|
app.logger.info('Application startup')
|
||||||
|
|
||||||
# --- Конфигурация приложения ---
|
# --- Конфигурация приложения ---
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-dev')
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-dev')
|
||||||
|
|
||||||
@@ -62,26 +91,32 @@ def create_app():
|
|||||||
login_manager.login_message_category = "info"
|
login_manager.login_message_category = "info"
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
from models import User, UserConfig
|
if inspect(db.engine).has_table('user_config'):
|
||||||
all_configs = UserConfig.query.all()
|
from models import User, UserConfig
|
||||||
for config in all_configs:
|
from utils import _parse_cron_string
|
||||||
user_id = config.user_id
|
all_configs = UserConfig.query.all()
|
||||||
mappings = config.mappings
|
for config in all_configs:
|
||||||
for sheer_title, params in mappings.items():
|
user_id = config.user_id
|
||||||
cron_schedule = params.get('schedule_cron')
|
mappings = config.mappings
|
||||||
if cron_schedule:
|
for sheet_title, params in mappings.items():
|
||||||
job_id = f"user_{user_id}_sheet_{sheer_title}"
|
if isinstance(params, dict):
|
||||||
try:
|
cron_schedule = params.get('schedule_cron')
|
||||||
scheduler.add_job(
|
if cron_schedule:
|
||||||
id=job_id,
|
job_id = f"user_{user_id}_sheet_{sheet_title}"
|
||||||
func=execute_olap_export,
|
try:
|
||||||
trigger='cron',
|
if not scheduler.get_job(job_id):
|
||||||
args=[user_id, sheer_title],
|
scheduler.add_job(
|
||||||
**_parse_cron_string(cron_schedule)
|
id=job_id,
|
||||||
)
|
func=execute_olap_export,
|
||||||
app.logger.info(f"Job {job_id} loaded on startup.")
|
trigger='cron',
|
||||||
except Exception as e:
|
args=[app, user_id, sheet_title],
|
||||||
app.logger.error(f"Failed to load job {job_id}: {e}")
|
**_parse_cron_string(cron_schedule)
|
||||||
|
)
|
||||||
|
app.logger.info(f"Scheduled job loaded on startup: {job_id} with schedule '{cron_schedule}'")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Failed to load job {job_id} with schedule '{cron_schedule}': {e}")
|
||||||
|
else:
|
||||||
|
app.logger.warning("Database tables not found. Skipping job loading on startup. Run 'flask init-db' to create the tables.")
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
# --- Регистрация команд CLI ---
|
# --- Регистрация команд CLI ---
|
||||||
@@ -98,16 +133,6 @@ def create_app():
|
|||||||
# --- Точка входа для запуска ---
|
# --- Точка входа для запуска ---
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
# --- Вспомогательная функция для парсинга cron ---
|
|
||||||
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)}
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Для прямого запуска через `python app.py` (удобно для отладки)
|
# Для прямого запуска через `python app.py` (удобно для отладки)
|
||||||
app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 5005)))
|
app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 5005)))
|
||||||
224
data/olaper.log
Normal file
224
data/olaper.log
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
2025-07-31 02:19:43,501 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:19:48,374 ERROR: Exception on / [GET] [in C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py:875]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 1511, in wsgi_app
|
||||||
|
response = self.full_dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 919, in full_dispatch_request
|
||||||
|
rv = self.handle_user_exception(e)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 902, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask_login\utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
|
||||||
|
File "c:\safe\repos\olaper\routes.py", line 145, in index
|
||||||
|
return render_template(
|
||||||
|
'index.html',
|
||||||
|
...<5 lines>...
|
||||||
|
client_email=config.google_client_email
|
||||||
|
)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\templating.py", line 149, in render_template
|
||||||
|
template = app.jinja_env.get_or_select_template(template_name_or_list)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 1087, in get_or_select_template
|
||||||
|
return self.get_template(template_name_or_list, parent, globals)
|
||||||
|
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 1016, in get_template
|
||||||
|
return self._load_template(name, globals)
|
||||||
|
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 975, in _load_template
|
||||||
|
template = self.loader.load(self, name, self.make_globals(globals))
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\loaders.py", line 138, in load
|
||||||
|
code = environment.compile(source, name, filename)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 771, in compile
|
||||||
|
self.handle_exception(source=source_hint)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 942, in handle_exception
|
||||||
|
raise rewrite_traceback_stack(source=source)
|
||||||
|
File "c:\safe\repos\olaper\templates\index.html", line 289, in template
|
||||||
|
<a href="{{ url_for('.run_job_now', sheet_title=sheet_title) }}" class="button-link" onclick="return confirm('{{ _('Run the scheduled task for sheet \\%(sheet)s\\' now?', sheet=sheet_title) }}')">
|
||||||
|
|
||||||
|
jinja2.exceptions.TemplateSyntaxError: expected token ',', got 'now'
|
||||||
|
2025-07-31 02:21:56,465 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:23:21,790 INFO: Added/updated job: user_1_sheet_<74><5F><EFBFBD><EFBFBD>1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\routes.py:531]
|
||||||
|
2025-07-31 02:23:37,761 INFO: Removed existing job: user_1_sheet_<74><5F><EFBFBD><EFBFBD>1 [in c:\safe\repos\olaper\routes.py:517]
|
||||||
|
2025-07-31 02:23:37,761 INFO: Added/updated job: user_1_sheet_<74><5F><EFBFBD><EFBFBD>1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\routes.py:531]
|
||||||
|
2025-07-31 02:25:41,679 INFO: Added/updated job: user_2_sheet_<74><5F><EFBFBD><EFBFBD>1 with schedule '26 2 * * *' [in c:\safe\repos\olaper\routes.py:531]
|
||||||
|
2025-07-31 02:36:53,978 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:36:54,452 ERROR: Failed to load job user_1_sheet_<74><5F><EFBFBD><EFBFBD>1: name '_parse_cron_string' is not defined [in c:\safe\repos\olaper\app.py:116]
|
||||||
|
2025-07-31 02:36:54,453 ERROR: Failed to load job user_2_sheet_<74><5F><EFBFBD><EFBFBD>1: name '_parse_cron_string' is not defined [in c:\safe\repos\olaper\app.py:116]
|
||||||
|
2025-07-31 02:37:28,388 INFO: Added/updated job: user_2_sheet_<74><5F><EFBFBD><EFBFBD>1 with schedule '26 2 * * *' [in c:\safe\repos\olaper\routes.py:530]
|
||||||
|
2025-07-31 02:37:37,377 INFO: Removed existing job: user_2_sheet_<74><5F><EFBFBD><EFBFBD>1 [in c:\safe\repos\olaper\routes.py:516]
|
||||||
|
2025-07-31 02:37:37,377 INFO: Added/updated job: user_2_sheet_<74><5F><EFBFBD><EFBFBD>1 with schedule '38 2 * * *' [in c:\safe\repos\olaper\routes.py:530]
|
||||||
|
2025-07-31 02:38:00,003 INFO: Executing scheduled job for user 2, sheet '<27><><EFBFBD><EFBFBD>1', period 'last_10_days' (2025-07-21 to 2025-07-30) [in c:\safe\repos\olaper\routes.py:367]
|
||||||
|
2025-07-31 02:38:03,135 INFO: Successfully wrote 713 rows to sheet '<27><><EFBFBD><EFBFBD>1' for user 2. [in c:\safe\repos\olaper\routes.py:447]
|
||||||
|
2025-07-31 02:46:17,382 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:46:17,849 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 02:46:17,850 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '38 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 02:46:47,525 INFO: Removed existing job: user_2_sheet_Лист1 [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 02:46:47,526 INFO: Added/updated job: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\routes.py:521]
|
||||||
|
2025-07-31 02:50:00,006 INFO: Executing scheduled job for user 2, sheet 'Лист1', period 'last_15_days' (2025-07-16 to 2025-07-30) [in c:\safe\repos\olaper\routes.py:358]
|
||||||
|
2025-07-31 02:50:02,254 INFO: Successfully wrote 805 rows to sheet 'Лист1' for user 2. [in c:\safe\repos\olaper\routes.py:438]
|
||||||
|
2025-07-31 02:53:15,576 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:53:21,166 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:55:25,567 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:55:26,057 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 02:55:26,058 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 02:55:30,020 ERROR: Exception on / [GET] [in C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py:875]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 1511, in wsgi_app
|
||||||
|
response = self.full_dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 919, in full_dispatch_request
|
||||||
|
rv = self.handle_user_exception(e)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 902, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask_login\utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
|
||||||
|
File "c:\safe\repos\olaper\routes.py", line 137, in index
|
||||||
|
return render_template(
|
||||||
|
'index.html',
|
||||||
|
...<5 lines>...
|
||||||
|
client_email=config.google_client_email
|
||||||
|
)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\templating.py", line 150, in render_template
|
||||||
|
return _render(app, template, context)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\templating.py", line 131, in _render
|
||||||
|
rv = template.render(context)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 1295, in render
|
||||||
|
self.environment.handle_exception()
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 942, in handle_exception
|
||||||
|
raise rewrite_traceback_stack(source=source)
|
||||||
|
File "c:\safe\repos\olaper\templates\index.html", line 337, in top-level template code
|
||||||
|
// 'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\utils.py", line 92, in from_obj
|
||||||
|
if hasattr(obj, "jinja_pass_arg"):
|
||||||
|
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
jinja2.exceptions.UndefinedError: 'csrf_token' is undefined
|
||||||
|
2025-07-31 02:57:14,133 ERROR: Exception on / [GET] [in C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py:875]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 1511, in wsgi_app
|
||||||
|
response = self.full_dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 919, in full_dispatch_request
|
||||||
|
rv = self.handle_user_exception(e)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request
|
||||||
|
rv = self.dispatch_request()
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\app.py", line 902, in dispatch_request
|
||||||
|
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask_login\utils.py", line 290, in decorated_view
|
||||||
|
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
|
||||||
|
File "c:\safe\repos\olaper\routes.py", line 137, in index
|
||||||
|
return render_template(
|
||||||
|
'index.html',
|
||||||
|
...<5 lines>...
|
||||||
|
client_email=config.google_client_email
|
||||||
|
)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\templating.py", line 150, in render_template
|
||||||
|
return _render(app, template, context)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\flask\templating.py", line 131, in _render
|
||||||
|
rv = template.render(context)
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 1295, in render
|
||||||
|
self.environment.handle_exception()
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\environment.py", line 942, in handle_exception
|
||||||
|
raise rewrite_traceback_stack(source=source)
|
||||||
|
File "c:\safe\repos\olaper\templates\index.html", line 337, in top-level template code
|
||||||
|
body: JSON.stringify({ cron_string: cronStr })
|
||||||
|
^^^^^^^
|
||||||
|
File "C:\safe\repos\olaper\.venv\Lib\site-packages\jinja2\utils.py", line 92, in from_obj
|
||||||
|
if hasattr(obj, "jinja_pass_arg"):
|
||||||
|
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
jinja2.exceptions.UndefinedError: 'csrf_token' is undefined
|
||||||
|
2025-07-31 02:57:19,964 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 02:57:20,425 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 02:57:20,426 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:03:30,227 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:03:31,054 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:03:31,055 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:09:12,340 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:09:12,803 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:09:12,803 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:09:24,741 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:09:25,200 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:09:25,201 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:10:47,743 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:10:48,576 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:10:48,576 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:14:10,324 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:14:11,169 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:14:11,170 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:14:17,931 WARNING: Cron descriptor failed for string '20 18 * * *': get_description() got an unexpected keyword argument 'casing_type' [in c:\safe\repos\olaper\routes.py:506]
|
||||||
|
2025-07-31 03:16:10,376 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:16:10,839 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:16:10,840 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:16:16,952 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:16:17,401 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:16:17,402 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:16:27,363 WARNING: Cron descriptor failed for string '20 18 * * *': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:17:27,908 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:17:28,360 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:17:28,361 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:17:31,939 WARNING: Cron descriptor failed for string '20 18 * * *': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:18:08,943 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:18:09,771 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:18:09,772 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:18:13,243 WARNING: Cron descriptor failed for string '20 18 * * *': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:18:23,676 WARNING: Cron descriptor failed for string '23 18 * * *': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:20:00,630 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:20:01,474 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:20:01,474 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:23:38,184 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:23:39,029 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:23:39,029 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:23:45,347 WARNING: Cron descriptor failed for string '23 18 * * *': Unknown Locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:24:01,010 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:24:01,847 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:24:01,848 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:24:08,733 WARNING: Cron descriptor failed for string '23 18 * * *': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:505]
|
||||||
|
2025-07-31 03:26:26,201 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:26:27,048 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:26:27,049 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:27:34,340 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:27:34,799 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:27:34,799 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:27:50,717 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:27:51,179 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:27:51,179 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:29:27,053 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:29:27,894 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:29:27,894 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:32:46,325 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:32:46,788 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:32:46,788 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:32:54,423 WARNING: Cron descriptor failed for string '4 18 3 * *' with locale 'ru_RU': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:33:12,448 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:33:12,894 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:33:12,894 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:33:29,971 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:33:30,418 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:33:30,418 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:33:34,105 WARNING: Cron descriptor failed for string '4 18 3 * *' with locale 'ru_RU': Unknown locale configuration argument [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:34:54,314 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:34:54,783 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:34:54,784 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:45:44,279 INFO: Application startup [in c:\safe\repos\olaper\app.py:46]
|
||||||
|
2025-07-31 03:45:44,776 INFO: Scheduled job loaded on startup: user_1_sheet_Лист1 with schedule '25 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:45:44,776 INFO: Scheduled job loaded on startup: user_2_sheet_Лист1 with schedule '50 2 * * *' [in c:\safe\repos\olaper\app.py:115]
|
||||||
|
2025-07-31 03:45:57,114 WARNING: Cron descriptor failed for string '4 -18 3 7 5' with locale 'ru_RU': [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:45:59,074 WARNING: Cron descriptor failed for string '4 \18 3 7 5' with locale 'ru_RU': [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:46:00,538 WARNING: Cron descriptor failed for string '4 /18 3 7 5' with locale 'ru_RU': [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:46:08,186 WARNING: Cron descriptor failed for string '4 2/? 3 7 5' with locale 'ru_RU': [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:46:09,962 WARNING: Cron descriptor failed for string '4 2/ 3 7 5' with locale 'ru_RU': [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:46:18,499 WARNING: Cron descriptor failed for string '4 2/18 7 5' with locale 'ru_RU': Error: Expression only has 4 parts. At least 5 part are required. [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:46:22,058 WARNING: Cron descriptor failed for string '4 2/18 7 5' with locale 'ru_RU': Error: Expression only has 4 parts. At least 5 part are required. [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:48:10,579 WARNING: Cron descriptor failed for string '4 2/18 7 5' with locale 'ru_RU': Error: Expression only has 4 parts. At least 5 part are required. [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:48:14,027 WARNING: Cron descriptor failed for string '4 2/18 7 5' with locale 'ru_RU': Error: Expression only has 4 parts. At least 5 part are required. [in c:\safe\repos\olaper\routes.py:507]
|
||||||
|
2025-07-31 03:48:16,964 WARNING: Cron descriptor failed for string '4 2/18 7 5 ' with locale 'ru_RU': Error: Expression only has 4 parts. At least 5 part are required. [in c:\safe\repos\olaper\routes.py:507]
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
101
routes.py
101
routes.py
@@ -3,12 +3,12 @@ import os
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from flask import (
|
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_login import login_user, login_required, logout_user, current_user
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
|
from cron_descriptor import ExpressionDescriptor, CasingTypeEnum, Options
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
import gspread
|
|
||||||
|
|
||||||
# --- ИМПОРТ РАСШИРЕНИЙ И МОДЕЛЕЙ ---
|
# --- ИМПОРТ РАСШИРЕНИЙ И МОДЕЛЕЙ ---
|
||||||
# Импортируем экземпляры расширений, созданные в app.py
|
# Импортируем экземпляры расширений, созданные в app.py
|
||||||
@@ -17,7 +17,7 @@ from extensions import db, login_manager, scheduler
|
|||||||
from models import User, UserConfig
|
from models import User, UserConfig
|
||||||
from google_sheets import GoogleSheets
|
from google_sheets import GoogleSheets
|
||||||
from request_module import ReqModule
|
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)
|
os.makedirs(user_dir, exist_ok=True)
|
||||||
return os.path.join(user_dir, secure_filename(filename))
|
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>')
|
@main_bp.route('/language/<language>')
|
||||||
@@ -131,13 +123,24 @@ def logout():
|
|||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
config = g.user_config
|
config = g.user_config
|
||||||
|
clean_mappings = {}
|
||||||
|
if config.mappings:
|
||||||
|
for key, value in config.mappings.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
clean_mappings[key] = value
|
||||||
|
else:
|
||||||
|
clean_mappings[key] = {
|
||||||
|
'report_id': value,
|
||||||
|
'schedule_cron': None,
|
||||||
|
'schedule_period': None
|
||||||
|
}
|
||||||
return render_template(
|
return render_template(
|
||||||
'index.html',
|
'index.html',
|
||||||
rms_config=config.get_rms_dict(),
|
rms_config=config.get_rms_dict(),
|
||||||
google_config=config.get_google_dict(),
|
google_config=config.get_google_dict(),
|
||||||
presets=config.presets,
|
presets=config.presets,
|
||||||
sheets=config.sheets,
|
sheets=config.sheets,
|
||||||
mappings=config.mappings,
|
mappings=clean_mappings,
|
||||||
client_email=config.google_client_email
|
client_email=config.google_client_email
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -281,22 +284,38 @@ def mapping_set():
|
|||||||
config = g.user_config
|
config = g.user_config
|
||||||
try:
|
try:
|
||||||
new_mappings = {}
|
new_mappings = {}
|
||||||
# Сохраняем существующие настройки расписания при обновлении отчетов
|
|
||||||
current_mappings = config.mappings or {}
|
current_mappings = config.mappings or {}
|
||||||
|
|
||||||
|
# Итерируемся по всем листам, доступным пользователю в Google Sheets
|
||||||
for sheet in config.sheets:
|
for sheet in config.sheets:
|
||||||
|
# Получаем ID отчета, который пользователь выбрал для этого листа в форме
|
||||||
report_key = f"sheet_{sheet['id']}"
|
report_key = f"sheet_{sheet['id']}"
|
||||||
selected_report_id = request.form.get(report_key)
|
selected_report_id = request.form.get(report_key)
|
||||||
|
|
||||||
|
# Если пользователь выбрал отчет (а не оставил поле пустым)
|
||||||
if selected_report_id:
|
if selected_report_id:
|
||||||
# Получаем существующие данные расписания для этого листа
|
# Инициализируем переменные для расписания
|
||||||
existing_schedule = current_mappings.get(sheet['title'], {})
|
schedule_cron = None
|
||||||
new_mappings[sheet['title']] = {
|
schedule_period = None
|
||||||
|
|
||||||
|
# Ищем текущие настройки для этого конкретного листа в базе данных
|
||||||
|
current_sheet_mapping = current_mappings.get(sheet.get('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,
|
'report_id': selected_report_id,
|
||||||
'schedule_cron': existing_schedule.get('schedule_cron'),
|
'schedule_cron': schedule_cron,
|
||||||
'schedule_period': existing_schedule.get('schedule_period')
|
'schedule_period': schedule_period
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Полностью заменяем старые сопоставления на новые
|
||||||
config.mappings = new_mappings
|
config.mappings = new_mappings
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -308,12 +327,11 @@ def mapping_set():
|
|||||||
return redirect(url_for('.index'))
|
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-отчета. Может вызываться как из эндпоинта, так и из планировщика.
|
Основная логика выгрузки OLAP-отчета. Может вызываться как из эндпоинта, так и из планировщика.
|
||||||
Если start_date_str и end_date_str не переданы, вычисляет их на основе расписания.
|
Если start_date_str и end_date_str не переданы, вычисляет их на основе расписания.
|
||||||
"""
|
"""
|
||||||
app = current_app._get_current_object()
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
user = db.session.get(User, user_id)
|
user = db.session.get(User, user_id)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -445,7 +463,7 @@ def render_olap():
|
|||||||
|
|
||||||
try:
|
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')
|
flash(_('Report generation task for sheet "%(sheet)s" has been started. The data will appear shortly.', sheet=sheet_title), 'success')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error')
|
flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error')
|
||||||
@@ -453,6 +471,42 @@ def render_olap():
|
|||||||
|
|
||||||
return redirect(url_for('.index'))
|
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'])
|
@main_bp.route('/save_schedule', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -462,6 +516,9 @@ def save_schedule():
|
|||||||
updated_mappings = config.mappings or {}
|
updated_mappings = config.mappings or {}
|
||||||
|
|
||||||
for sheet_title, params in updated_mappings.items():
|
for sheet_title, params in updated_mappings.items():
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
cron_value = request.form.get(f"cron-{sheet_title}", "").strip()
|
cron_value = request.form.get(f"cron-{sheet_title}", "").strip()
|
||||||
period_value = request.form.get(f"period-{sheet_title}", "").strip()
|
period_value = request.form.get(f"period-{sheet_title}", "").strip()
|
||||||
|
|
||||||
@@ -494,7 +551,7 @@ def save_schedule():
|
|||||||
id=job_id,
|
id=job_id,
|
||||||
func=execute_olap_export,
|
func=execute_olap_export,
|
||||||
trigger='cron',
|
trigger='cron',
|
||||||
args=[current_user.id, sheet_title],
|
args=[current_app._get_current_object(), current_user.id, sheet_title],
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
**cron_params
|
**cron_params
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -196,26 +196,26 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for sheet in sheets %}
|
{% for sheet_title, mapping_info in mappings.items() %}
|
||||||
{% set mappings = mappings.get(sheet.title) %}
|
{% if mapping_info and mapping_info.get('report_id') %}
|
||||||
{% if mapping_info and mapping_info.get('report_id') %}
|
{% set report_id = mapping_info.get('report_id') %}
|
||||||
{% set report_id = mapping_info.get('report_id') %}
|
{% set matching_presets = presets | selectattr('id', 'equalto', report_id) | list %}
|
||||||
{% set matching_presets = presets | selectattr('id', 'equalto', report_id) | list %}
|
|
||||||
{% set preset_name = _('ID: ') + report_id %}
|
<!-- Отображаем строку только если для этого report_id найден соответствующий пресет -->
|
||||||
{% if matching_presets %}
|
{% if matching_presets %}
|
||||||
{% set preset = matching_presets[0] %}
|
{% set preset = matching_presets[0] %}
|
||||||
{% set preset_name = preset.get('name', _('Unnamed Preset')) %}
|
{% set preset_name = preset.get('name', _('Unnamed Preset')) %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ sheet.title }}</td>
|
<td>{{ sheet_title }}</td>
|
||||||
<td>{{ preset_name }}</td>
|
<td>{{ preset_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="submit" name="render_{{ sheet.title }}">
|
<button type="submit" name="render_{{ sheet_title }}">
|
||||||
{{ _('Render to sheet') }}
|
{{ _('Render to sheet') }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
<select id="cron-day-of-week"><option value="*">*</option><option value="1">{{_('Mon')}}</option><option value="2">{{_('Tue')}}</option><option value="3">{{_('Wed')}}</option><option value="4">{{_('Thu')}}</option><option value="5">{{_('Fri')}}</option><option value="6">{{_('Sat')}}</option><option value="0">{{_('Sun')}}</option></select>
|
<select id="cron-day-of-week"><option value="*">*</option><option value="1">{{_('Mon')}}</option><option value="2">{{_('Tue')}}</option><option value="3">{{_('Wed')}}</option><option value="4">{{_('Thu')}}</option><option value="5">{{_('Fri')}}</option><option value="6">{{_('Sat')}}</option><option value="0">{{_('Sun')}}</option></select>
|
||||||
</div>
|
</div>
|
||||||
<label for="cron-output">{{ _('Generated Cron String:') }}</label>
|
<label for="cron-output">{{ _('Generated Cron String:') }}</label>
|
||||||
<input type="text" id="cron-output" readonly>
|
<input type="text" id="cron-output">
|
||||||
<p id="cron-human-readable"></p>
|
<p id="cron-human-readable"></p>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@@ -260,6 +260,7 @@
|
|||||||
<th>{{ _('Worksheet') }}</th>
|
<th>{{ _('Worksheet') }}</th>
|
||||||
<th>{{ _('Schedule (Cron)') }}</th>
|
<th>{{ _('Schedule (Cron)') }}</th>
|
||||||
<th>{{ _('Report Period') }}</th>
|
<th>{{ _('Report Period') }}</th>
|
||||||
|
<th>{{ _('Action') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -318,39 +319,83 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Cron конструктор
|
// --- Cron конструктор и переводчик ---
|
||||||
const cronInputs = ['cron-minute', 'cron-hour', 'cron-day', 'cron-month', 'cron-day-of-week'];
|
const cronInputs = ['cron-minute', 'cron-hour', 'cron-day', 'cron-month', 'cron-day-of-week'];
|
||||||
const cronOutput = document.getElementById('cron-output');
|
const cronOutput = document.getElementById('cron-output');
|
||||||
|
const humanReadableOutput = document.getElementById('cron-human-readable');
|
||||||
function updateCronString() {
|
|
||||||
if (!cronOutput) return;
|
// Функция для перевода cron-строки
|
||||||
const values = cronInputs.map(id => document.getElementById(id).value);
|
async function translateCronString(cronStr) {
|
||||||
cronOutput.value = values.join(' ');
|
if (!humanReadableOutput) return;
|
||||||
}
|
|
||||||
|
|
||||||
cronInputs.forEach(id => {
|
try {
|
||||||
const el = document.getElementById(id);
|
const response = await fetch("{{ url_for('.translate_cron') }}", {
|
||||||
if(el) el.addEventListener('change', updateCronString);
|
method: 'POST',
|
||||||
});
|
headers: {
|
||||||
updateCronString(); // Initial call
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
// Управление видимостью поля "N дней"
|
body: JSON.stringify({ cron_string: cronStr })
|
||||||
document.querySelectorAll('.period-select').forEach(select => {
|
});
|
||||||
const targetId = select.dataset.target;
|
|
||||||
const targetDiv = document.getElementById(targetId);
|
|
||||||
|
|
||||||
function toggleCustomInput() {
|
|
||||||
if (select.value === 'last_N_days') {
|
|
||||||
targetDiv.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
targetDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select.addEventListener('change', toggleCustomInput);
|
if (!response.ok) {
|
||||||
toggleCustomInput(); // Initial call on page load
|
humanReadableOutput.textContent = 'Error communicating with server.';
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
humanReadableOutput.textContent = data.description;
|
||||||
|
humanReadableOutput.style.color = data.description.startsWith('Invalid') ? 'red' : '#555';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error translating cron string:', error);
|
||||||
|
humanReadableOutput.textContent = 'Translation error.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для обновления строки (из селектов) и запуска перевода
|
||||||
|
function updateCronStringFromSelects() {
|
||||||
|
if (!cronOutput) return;
|
||||||
|
const values = cronInputs.map(id => document.getElementById(id).value);
|
||||||
|
const newCronString = values.join(' ');
|
||||||
|
cronOutput.value = newCronString;
|
||||||
|
|
||||||
|
// Вызываем перевод
|
||||||
|
translateCronString(newCronString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Слушаем изменения в выпадающих списках конструктора
|
||||||
|
cronInputs.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener('change', updateCronStringFromSelects);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cronOutput) {
|
||||||
|
cronOutput.addEventListener('input', function() {
|
||||||
|
translateCronString(this.value); // Переводим то, что введено вручную
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Первоначальный вызов при загрузке страницы (для начальной строки)
|
||||||
|
updateCronStringFromSelects();
|
||||||
|
|
||||||
|
// --- Управление видимостью поля "N дней" ---
|
||||||
|
document.querySelectorAll('.period-select').forEach(select => {
|
||||||
|
const targetId = select.dataset.target;
|
||||||
|
const targetDiv = document.getElementById(targetId);
|
||||||
|
|
||||||
|
function toggleCustomInput() {
|
||||||
|
if (!targetDiv) return;
|
||||||
|
if (select.value === 'last_N_days') {
|
||||||
|
targetDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
targetDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', toggleCustomInput);
|
||||||
|
toggleCustomInput();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p style="text-align: center; margin-top: 50px;">{{ _('Please,') }} <a href="{{ url_for('.login') }}">{{ _('login') }}</a> {{ _('or') }} <a href="{{ url_for('.register') }}">{{ _('register') }}</a></p>
|
<p style="text-align: center; margin-top: 50px;">{{ _('Please,') }} <a href="{{ url_for('.login') }}">{{ _('login') }}</a> {{ _('or') }} <a href="{{ url_for('.register') }}">{{ _('register') }}</a></p>
|
||||||
|
|||||||
12
utils.py
12
utils.py
@@ -141,6 +141,18 @@ def get_dates(start_date, end_date):
|
|||||||
|
|
||||||
return start_date, end_date
|
return start_date, end_date
|
||||||
|
|
||||||
|
def _parse_cron_string(cron_str):
|
||||||
|
"""Парсит строку cron в словарь для APScheduler."""
|
||||||
|
if not isinstance(cron_str, str):
|
||||||
|
raise TypeError("cron_str must be a string.")
|
||||||
|
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)}
|
||||||
|
|
||||||
|
|
||||||
def calculate_period_dates(period_key):
|
def calculate_period_dates(period_key):
|
||||||
"""
|
"""
|
||||||
Вычисляет начальную и конечную даты на основе строкового ключа.
|
Вычисляет начальную и конечную даты на основе строкового ключа.
|
||||||
|
|||||||
Reference in New Issue
Block a user