Compare commits

..

13 Commits

Author SHA1 Message Date
6c11be1460 test
Some checks failed
Test Build / test-build (push) Has been cancelled
2025-08-14 19:22:04 +03:00
0a17b31c06 added scheduler v2
All checks were successful
Test Build / test-build (push) Successful in 25s
2025-07-31 03:50:08 +03:00
4f66edbb21 fix index route
All checks were successful
Test Build / test-build (push) Successful in 3s
2025-07-30 19:22:10 +03:00
38f35d1915 fix old config format
All checks were successful
Test Build / test-build (push) Successful in 2s
2025-07-30 19:17:16 +03:00
ca8e70781c fix empty base on 1st start
All checks were successful
Test Build / test-build (push) Successful in 1s
2025-07-30 18:55:23 +03:00
4ebe15522f fix dotenv
All checks were successful
Test Build / test-build (push) Successful in 2s
2025-07-30 18:32:06 +03:00
0f1c749b33 Scheduler v1
All checks were successful
Test Build / test-build (push) Successful in 23s
2025-07-30 18:28:55 +03:00
8e757afe39 fix testing v3
All checks were successful
Test Build / test-build (push) Successful in 3s
2025-07-29 19:52:18 +03:00
5100c5d17c v1
All checks were successful
Test Build / test-build (push) Successful in 3s
2025-07-29 19:43:11 +03:00
81d33bebef test fix v1 2025-07-29 19:42:55 +03:00
3500d433ea fix reqs
All checks were successful
Test Build / test-build (push) Successful in 21s
2025-07-26 06:00:54 +03:00
c713b47d58 reqs fix 2025-07-26 06:00:09 +03:00
ddd0ffbcb0 vv
All checks were successful
Test Build / test-build (push) Successful in 3s
2025-07-26 05:56:11 +03:00
13 changed files with 1187 additions and 514 deletions

View File

@@ -24,10 +24,20 @@ jobs:
- name: Run new container
run: |
PORT=5005
CONTAINER_ID=$(docker ps --format '{{.ID}} {{.Ports}}' | grep ":$PORT->" | awk '{print $1}')
if [ -n "$CONTAINER_ID" ]; then
echo "Stopping container using port $PORT..."
docker stop "$CONTAINER_ID"
docker rm "$CONTAINER_ID"
fi
docker run -d \
--name olaper \
--restart always \
-p 5005:5005 \
-p ${PORT}:5005 \
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
-e ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }} \
-v mholaper_data:/app/data \
olaper:latest

View File

@@ -39,6 +39,7 @@ jobs:
docker run -d \
--name olaper_test \
-p 5050:5005 \
-v /home/master/olaper-debug/data:/opt/olaper/data \
-e SECRET_KEY=${{ secrets.SECRET_KEY }} \
-e ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }} \
olaper:test

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@
/.env
/.idea
cred.json
*.db
*.db
*/*.log

64
app.py
View File

@@ -1,12 +1,13 @@
import os
from flask import Flask, session, request
from sqlalchemy import inspect
from dotenv import load_dotenv
# 1. Загрузка переменных окружения - в самом верху
load_dotenv()
# 2. Импорт расширений из центрального файла
from extensions import db, migrate, login_manager, babel
from extensions import scheduler, db, migrate, login_manager, babel
from models import init_encryption
# 3. Фабрика приложений
@@ -16,6 +17,34 @@ def create_app():
"""
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')
@@ -49,16 +78,47 @@ def create_app():
migrate.init_app(app, db)
login_manager.init_app(app)
babel.init_app(app, locale_selector=get_locale)
scheduler.init_app(app)
init_encryption(app)
# --- Регистрация блюпринтов ---
from routes import main_bp
from routes import main_bp, execute_olap_export
app.register_blueprint(main_bp)
login_manager.login_view = 'main.login'
login_manager.login_message = "Пожалуйста, войдите, чтобы получить доступ к этой странице."
login_manager.login_message_category = "info"
with app.app_context():
if inspect(db.engine).has_table('user_config'):
from models import User, UserConfig
from utils import _parse_cron_string
all_configs = UserConfig.query.all()
for config in all_configs:
user_id = config.user_id
mappings = config.mappings
for sheet_title, params in mappings.items():
if isinstance(params, dict):
cron_schedule = params.get('schedule_cron')
if cron_schedule:
job_id = f"user_{user_id}_sheet_{sheet_title}"
try:
if not scheduler.get_job(job_id):
scheduler.add_job(
id=job_id,
func=execute_olap_export,
trigger='cron',
args=[app, user_id, sheet_title],
**_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()
# --- Регистрация команд CLI ---
from models import User, UserConfig
@app.cli.command('init-db')

View File

@@ -1,3 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.i18n

224
data/olaper.log Normal file
View 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]

View File

@@ -1,11 +1,13 @@
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_babel import Babel
# Создаем экземпляры расширений здесь, без привязки к приложению.
# Теперь любой модуль может безопасно импортировать их отсюда.
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
babel = Babel()
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_babel import Babel
from flask_apscheduler import APScheduler
# Создаем экземпляры расширений здесь, без привязки к приложению.
# Теперь любой модуль может безопасно импортировать их отсюда.
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
babel = Babel()
scheduler = APScheduler()

98
prompt
View File

@@ -1,49 +1,49 @@
Тема: Доработка и рефакторинг Flask-приложения "MyHoreca OLAP-to-GoogleSheets"
1. Обзор Проекта
Выступаешь в роли опытного Python/Flask-разработчика. Тебе предоставляется код существующего веб-приложения "MyHoreca OLAP-to-GoogleSheets". Основная задача приложения — предоставить пользователям веб-интерфейс для автоматической выгрузки OLAP-отчетов с сервера RMS (iiko/Syrve) в Google Таблицы.
Стек технологий:
Backend: Flask, Flask-SQLAlchemy, Flask-Login, Flask-Migrate
Работа с API: requests (для RMS), gspread (для Google Sheets)
Безопасность: werkzeug.security (хэширование паролей), cryptography (шифрование паролей RMS)
База данных: SQLite
Frontend: Jinja2, стандартный HTML/CSS/JS.
Текущий функционал:
Приложение уже реализует полный цикл работы для одного пользователя:
Регистрация и авторизация.
Настройка подключения к RMS API (хост, логин, пароль).
Получение и сохранение списка OLAP-отчетов (пресетов) для пользователя.
Настройка подключения к Google Sheets (загрузка credentials.json, указание URL таблицы).
Получение и сохранение списка листов из Google Таблицы.
Сопоставление (маппинг) отчетов RMS с листами Google Таблицы.
Отрисовка отчета за выбранный период: приложение получает данные из RMS, очищает соответствующий лист и записывает новые данные.
Предоставленные файлы:
app.py (основная логика Flask)
models.py (модели SQLAlchemy)
google_sheets.py (модуль для работы с Google Sheets API)
request_module.py (модуль для работы с RMS API)
utils.py (вспомогательные функции)
README.md (документация)
HTML-шаблоны (index.html, login.html, register.html)
2. Ключевые Задачи для Разработки
Задача 1: Отладка, Рефакторинг и Русификация Комментариев
Отладка отрисовки: Внимательно проанализировать функцию render_olap в app.py и связанные с ней модули (google_sheets.py, utils.py). Выявить и исправить "нюансы" и потенциальные ошибки при обработке данных отчета и записи их в таблицу. Уделить особое внимание обработке пустых отчетов, ошибок API и корректному информированию пользователя.
Чистка кода: Провести рефакторинг кода. Удалить неиспользуемые переменные, устаревшие комментарии и "мусор". Улучшить читаемость и структуру, особенно в app.py.
Русификация комментариев: Перевести все комментарии в коде на русский язык для соответствия стандартам проекта. Пояснения должны описывать текущий, работающий функционал.
Задача 2: Интернационализация (i18n) и Перевод Интерфейса
Внедрение i18n: Интегрировать Flask-Babel для поддержки многоязычности.
Механизм выбора языка:
На странице логина (login.html) добавить возможность выбора языка (Русский/Английский).
Выбор пользователя должен сохраняться (например, в сессии или в профиле пользователя в БД).
В основном шаблоне (index.html), рядом с кнопкой "Logout", добавить переключатель языка в виде флагов (🇷🇺/🇬🇧).
Перевод интерфейса:
Обернуть все текстовые строки в шаблонах Jinja2 и сообщения flash() в app.py в функцию перевода.
Создать файлы перевода (.po, .mo) и выполнить полный перевод всего видимого пользователю интерфейса на русский язык. Русский язык должен стать основным.
Задача 3: Улучшение Среды Разработки для Windows
Поддержка .env: Интегрировать библиотеку python-dotenv для управления переменными окружения.
Конфигурация: Модифицировать app.py и models.py так, чтобы они могли считывать конфигурационные переменные (SECRET_KEY, ENCRYPTION_KEY, DATABASE_URL и др.) из файла .env в корне проекта.
Документация: Дополнить README.md инструкциями по созданию и использованию файла .env для локальной разработки, особенно на Windows.
3. Правила Взаимодействия
Язык общения: Всегда общайся на русском языке.
Формат кода: Присылай изменения в коде точечно, указывая файл и участок кода, который нужно изменить. Не присылай полные файлы без необходимости.
Бизнес-логика: Никогда не придумывай бизнес-логику самостоятельно. Если для реализации функционала требуются данные (например, конкретные ключи API, пути, названия), всегда уточняй их у меня.
Качество кода: Пиши чистый, поддерживаемый код, готовый к дальнейшему расширению функционала.
Тема: Доработка и рефакторинг Flask-приложения "MyHoreca OLAP-to-GoogleSheets"
1. Обзор Проекта
Выступаешь в роли опытного Python/Flask-разработчика. Тебе предоставляется код существующего веб-приложения "MyHoreca OLAP-to-GoogleSheets". Основная задача приложения — предоставить пользователям веб-интерфейс для автоматической выгрузки OLAP-отчетов с сервера RMS (iiko/Syrve) в Google Таблицы.
Стек технологий:
Backend: Flask, Flask-SQLAlchemy, Flask-Login, Flask-Migrate
Работа с API: requests (для RMS), gspread (для Google Sheets)
Безопасность: werkzeug.security (хэширование паролей), cryptography (шифрование паролей RMS)
База данных: SQLite
Frontend: Jinja2, стандартный HTML/CSS/JS.
Текущий функционал:
Приложение уже реализует полный цикл работы для одного пользователя:
Регистрация и авторизация.
Настройка подключения к RMS API (хост, логин, пароль).
Получение и сохранение списка OLAP-отчетов (пресетов) для пользователя.
Настройка подключения к Google Sheets (загрузка credentials.json, указание URL таблицы).
Получение и сохранение списка листов из Google Таблицы.
Сопоставление (маппинг) отчетов RMS с листами Google Таблицы.
Отрисовка отчета за выбранный период: приложение получает данные из RMS, очищает соответствующий лист и записывает новые данные.
Предоставленные файлы:
app.py (основная логика Flask)
models.py (модели SQLAlchemy)
google_sheets.py (модуль для работы с Google Sheets API)
request_module.py (модуль для работы с RMS API)
utils.py (вспомогательные функции)
README.md (документация)
HTML-шаблоны (index.html, login.html, register.html)
2. Ключевые Задачи для Разработки
Задача 1: Отладка, Рефакторинг и Русификация Комментариев
Отладка отрисовки: Внимательно проанализировать функцию render_olap в app.py и связанные с ней модули (google_sheets.py, utils.py). Выявить и исправить "нюансы" и потенциальные ошибки при обработке данных отчета и записи их в таблицу. Уделить особое внимание обработке пустых отчетов, ошибок API и корректному информированию пользователя.
Чистка кода: Провести рефакторинг кода. Удалить неиспользуемые переменные, устаревшие комментарии и "мусор". Улучшить читаемость и структуру, особенно в app.py.
Русификация комментариев: Перевести все комментарии в коде на русский язык для соответствия стандартам проекта. Пояснения должны описывать текущий, работающий функционал.
Задача 2: Интернационализация (i18n) и Перевод Интерфейса
Внедрение i18n: Интегрировать Flask-Babel для поддержки многоязычности.
Механизм выбора языка:
На странице логина (login.html) добавить возможность выбора языка (Русский/Английский).
Выбор пользователя должен сохраняться (например, в сессии или в профиле пользователя в БД).
В основном шаблоне (index.html), рядом с кнопкой "Logout", добавить переключатель языка в виде флагов (🇷🇺/🇬🇧).
Перевод интерфейса:
Обернуть все текстовые строки в шаблонах Jinja2 и сообщения flash() в app.py в функцию перевода.
Создать файлы перевода (.po, .mo) и выполнить полный перевод всего видимого пользователю интерфейса на русский язык. Русский язык должен стать основным.
Задача 3: Улучшение Среды Разработки для Windows
Поддержка .env: Интегрировать библиотеку python-dotenv для управления переменными окружения.
Конфигурация: Модифицировать app.py и models.py так, чтобы они могли считывать конфигурационные переменные (SECRET_KEY, ENCRYPTION_KEY, DATABASE_URL и др.) из файла .env в корне проекта.
Документация: Дополнить README.md инструкциями по созданию и использованию файла .env для локальной разработки, особенно на Windows.
3. Правила Взаимодействия
Язык общения: Всегда общайся на русском языке.
Формат кода: Присылай изменения в коде точечно, указывая файл и участок кода, который нужно изменить. Не присылай полные файлы без необходимости.
Бизнес-логика: Никогда не придумывай бизнес-логику самостоятельно. Если для реализации функционала требуются данные (например, конкретные ключи API, пути, названия), всегда уточняй их у меня.
Качество кода: Пиши чистый, поддерживаемый код, готовый к дальнейшему расширению функционала.

Binary file not shown.

1003
routes.py

File diff suppressed because it is too large Load Diff

View File

@@ -257,4 +257,32 @@ small {
margin-right: 5px; /* Отступ справа от чекбокса до текста */
vertical-align: middle; /* Выравнивание по вертикали */
box-shadow: none; /* Убираем тень, если есть */
}
.cron-constructor {
border: 1px solid #e0e0e0;
padding: 15px;
margin-top: 15px;
border-radius: 5px;
background-color: #fdfdfd;
}
.cron-constructor h4 {
margin-top: 0;
}
.cron-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.cron-row select {
flex-grow: 1;
min-width: 60px;
}
#cron-output {
background-color: #e9ecef;
font-family: monospace;
}

View File

@@ -147,7 +147,7 @@
<select name="sheet_{{ sheet.id }}">
<option value="">-- {{ _('Not set') }} --</option>
{% for preset in presets %}
<option value="{{ preset['id'] }}" {% if mappings.get(sheet.title) == preset['id'] %}selected{% endif %}>
<option value="{{ preset['id'] }}" {% if mappings.get(sheet.title, {}).get('report_id') == preset['id'] %}selected{% endif %}>
{{ preset['name'] }} ({{ preset['id'] }})
</option>
{% endfor %}
@@ -196,25 +196,26 @@
</tr>
</thead>
<tbody>
{% for sheet in sheets %}
{% set report_id = mappings.get(sheet.title) %}
{% if report_id %}
{% for sheet_title, mapping_info in mappings.items() %}
{% if mapping_info and mapping_info.get('report_id') %}
{% set report_id = mapping_info.get('report_id') %}
{% set matching_presets = presets | selectattr('id', 'equalto', report_id) | list %}
{% set preset_name = _('ID: ') + report_id %}
<!-- Отображаем строку только если для этого report_id найден соответствующий пресет -->
{% if matching_presets %}
{% set preset = matching_presets[0] %}
{% set preset_name = preset.get('name', _('Unnamed Preset')) %}
{% endif %}
<tr>
<td>{{ sheet.title }}</td>
<td>{{ preset_name }}</td>
<td>
<button type="submit" name="render_{{ sheet.title }}">
{{ _('Render to sheet') }}
</button>
</td>
</tr>
<tr>
<td>{{ sheet_title }}</td>
<td>{{ preset_name }}</td>
<td>
<button type="submit" name="render_{{ sheet_title }}">
{{ _('Render to sheet') }}
</button>
</td>
</tr>
{% endif %}
{% endif %}
{% endfor %}
</tbody>
@@ -226,6 +227,71 @@
{% endif %}
</div>
<button type="button" class="collapsible" {% if not mappings or mappings|length == 0 %}disabled title="{{ _('Configure Mappings first') }}"{% endif %}>
5. {{ _('Scheduling Automatic Reports') }}
</button>
<div class="content">
<h3>{{ _('Schedule Settings') }}</h3>
<p>
{% trans %}Here you can set up a CRON schedule for automatic report generation.
The report will be generated for the specified period relative to the execution time.{% endtrans %}
</p>
<div class="cron-constructor">
<h4>{{ _('Cron Schedule Builder') }}</h4>
<p><small>{% trans %}Use this tool to build a cron string, then copy it to the desired field below.{% endtrans %}</small></p>
<div class="cron-row">
<select id="cron-minute"><option value="*">*</option>{% for i in range(60) %}<option value="{{i}}">{{'%02d'|format(i)}}</option>{% endfor %}</select>
<select id="cron-hour"><option value="*">*</option>{% for i in range(24) %}<option value="{{i}}">{{'%02d'|format(i)}}</option>{% endfor %}</select>
<select id="cron-day"><option value="*">*</option>{% for i in range(1, 32) %}<option value="{{i}}">{{i}}</option>{% endfor %}</select>
<select id="cron-month"><option value="*">*</option>{% for i in range(1, 13) %}<option value="{{i}}">{{i}}</option>{% endfor %}</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>
<label for="cron-output">{{ _('Generated Cron String:') }}</label>
<input type="text" id="cron-output">
<p id="cron-human-readable"></p>
</div>
<hr>
<form action="{{ url_for('.save_schedule') }}" method="post">
<table>
<thead>
<tr>
<th>{{ _('Worksheet') }}</th>
<th>{{ _('Schedule (Cron)') }}</th>
<th>{{ _('Report Period') }}</th>
<th>{{ _('Action') }}</th>
</tr>
</thead>
<tbody>
{% for sheet_title, params in mappings.items() %}
<tr>
<td>{{ sheet_title }}</td>
<td>
<input type="text" name="cron-{{ sheet_title }}" value="{{ params.get('schedule_cron', '') }}" placeholder="e.g., 0 2 * * 1">
</td>
<td>
{% set current_period = params.get('schedule_period', '') %}
<select name="period-{{ sheet_title }}" class="period-select" data-target="custom-days-{{ sheet_title }}">
<option value="">-- {{ _('Not Scheduled') }} --</option>
<option value="previous_week" {% if current_period == 'previous_week' %}selected{% endif %}>{{ _('Previous Week') }}</option>
<option value="last_7_days" {% if current_period == 'last_7_days' %}selected{% endif %}>{{ _('Last 7 Days') }}</option>
<option value="previous_month" {% if current_period == 'previous_month' %}selected{% endif %}>{{ _('Previous Month') }}</option>
<option value="current_month" {% if current_period == 'current_month' %}selected{% endif %}>{{ _('Current Month (to yesterday)') }}</option>
<option value="last_N_days" {% if current_period and current_period.startswith('last_') and current_period.endswith('_days') %}selected{% endif %}>{{ _('Last N Days') }}</option>
</select>
<div id="custom-days-{{ sheet_title }}" class="custom-days-input" style="display: none; margin-top: 5px;">
<input type="number" name="custom_days-{{ sheet_title }}" min="1" placeholder="N" style="width: 60px;" value="{% if current_period and current_period.startswith('last_') %}{{ current_period.split('_')[1] }}{% endif %}">
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit">{{ _('Save Schedule') }}</button>
</form>
</div>
</div> <!-- End Container -->
<script>
@@ -251,6 +317,85 @@
}
});
}
document.addEventListener('DOMContentLoaded', function() {
// --- Cron конструктор и переводчик ---
const cronInputs = ['cron-minute', 'cron-hour', 'cron-day', 'cron-month', 'cron-day-of-week'];
const cronOutput = document.getElementById('cron-output');
const humanReadableOutput = document.getElementById('cron-human-readable');
// Функция для перевода cron-строки
async function translateCronString(cronStr) {
if (!humanReadableOutput) return;
try {
const response = await fetch("{{ url_for('.translate_cron') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ cron_string: cronStr })
});
if (!response.ok) {
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>
{% 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>

View File

@@ -1,7 +1,8 @@
import json
import logging
from jinja2 import Template
from datetime import datetime
from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta
# Настройка логирования
logger = logging.getLogger(__name__)
@@ -138,4 +139,64 @@ def get_dates(start_date, end_date):
logger.error(f"Дата начала '{start_date}' не может быть позже даты окончания '{end_date}'.")
raise ValueError("Дата начала не может быть позже даты окончания.")
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):
"""
Вычисляет начальную и конечную даты на основе строкового ключа.
Возвращает кортеж (start_date_str, end_date_str) в формате YYYY-MM-DD.
"""
if not period_key:
raise ValueError("Period key cannot be empty.")
today = date.today()
yesterday = today - timedelta(days=1)
date_format = "%Y-%m-%d"
# За прошлую неделю (с пн по вс)
if period_key == 'previous_week':
start_of_last_week = today - timedelta(days=today.weekday() + 7)
end_of_last_week = start_of_last_week + timedelta(days=6)
return start_of_last_week.strftime(date_format), end_of_last_week.strftime(date_format)
# За последние 7 дней (не включая сегодня)
if period_key == 'last_7_days':
start_date = today - timedelta(days=7)
return start_date.strftime(date_format), yesterday.strftime(date_format)
# За прошлый месяц
if period_key == 'previous_month':
last_month = today - relativedelta(months=1)
start_date = last_month.replace(day=1)
end_date = start_date + relativedelta(months=1) - timedelta(days=1)
return start_date.strftime(date_format), end_date.strftime(date_format)
# За текущий месяц (до вчера)
if period_key == 'current_month':
start_date = today.replace(day=1)
return start_date.strftime(date_format), yesterday.strftime(date_format)
# Динамический ключ "За последние N дней"
if period_key.startswith('last_') and period_key.endswith('_days'):
try:
days = int(period_key.split('_')[1])
if days <= 0:
raise ValueError("Number of days must be positive.")
start_date = today - timedelta(days=days)
return start_date.strftime(date_format), yesterday.strftime(date_format)
except (ValueError, IndexError):
raise ValueError(f"Invalid dynamic period key: {period_key}")
raise ValueError(f"Unknown period key: {period_key}")