361 lines
14 KiB
Python
361 lines
14 KiB
Python
# routes.py
|
||
import os
|
||
import json
|
||
import shutil
|
||
from flask import (
|
||
Blueprint, render_template, request, redirect, url_for, flash, g, session, current_app
|
||
)
|
||
from flask_login import login_user, login_required, logout_user, current_user
|
||
from flask_babel import _
|
||
from werkzeug.utils import secure_filename
|
||
import gspread
|
||
|
||
# --- ИМПОРТ РАСШИРЕНИЙ И МОДЕЛЕЙ ---
|
||
# Импортируем экземпляры расширений, созданные в app.py
|
||
from extensions import db, login_manager
|
||
# Импортируем наши классы и утилиты
|
||
from models import User, UserConfig
|
||
from google_sheets import GoogleSheets
|
||
from request_module import ReqModule
|
||
from utils import get_dates, generate_template_from_preset, render_temp
|
||
|
||
|
||
# --- Создание блюпринта ---
|
||
main_bp = Blueprint('main', __name__)
|
||
|
||
|
||
# --- Регистрация обработчиков для расширений ---
|
||
|
||
@login_manager.user_loader
|
||
def load_user(user_id):
|
||
"""Загружает пользователя из БД для управления сессией."""
|
||
return db.session.get(User, int(user_id))
|
||
|
||
|
||
@main_bp.before_app_request
|
||
def load_user_specific_data():
|
||
"""Загружает конфигурацию пользователя в глобальный объект `g` для текущего запроса."""
|
||
g.user_config = None
|
||
if current_user.is_authenticated:
|
||
g.user_config = get_user_config()
|
||
|
||
|
||
# --- Вспомогательные функции, специфичные для маршрутов ---
|
||
|
||
def get_user_config():
|
||
"""Получает конфиг для текущего пользователя, создавая его при необходимости."""
|
||
if not current_user.is_authenticated:
|
||
return None
|
||
config = UserConfig.query.filter_by(user_id=current_user.id).first()
|
||
if not config:
|
||
config = UserConfig(user_id=current_user.id)
|
||
db.session.add(config)
|
||
return config
|
||
|
||
def get_user_upload_path(filename=""):
|
||
"""Возвращает путь для загрузки файлов для текущего пользователя."""
|
||
if not current_user.is_authenticated:
|
||
return None
|
||
user_dir = os.path.join(current_app.config['DATA_DIR'], str(current_user.id))
|
||
os.makedirs(user_dir, exist_ok=True)
|
||
return os.path.join(user_dir, secure_filename(filename))
|
||
|
||
|
||
# --- Маршруты ---
|
||
|
||
@main_bp.route('/language/<language>')
|
||
def set_language(language=None):
|
||
session['language'] = language
|
||
return redirect(request.referrer or url_for('.index'))
|
||
|
||
@main_bp.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
if current_user.is_authenticated:
|
||
return redirect(url_for('.index'))
|
||
if request.method == 'POST':
|
||
username = request.form.get('username')
|
||
password = request.form.get('password')
|
||
user = User.query.filter_by(username=username).first()
|
||
if user is None or not user.check_password(password):
|
||
flash(_('Invalid username or password'), 'error')
|
||
return redirect(url_for('.login'))
|
||
login_user(user, remember=request.form.get('remember'))
|
||
flash(_('Login successful!'), 'success')
|
||
next_page = request.args.get('next')
|
||
return redirect(next_page or url_for('.index'))
|
||
return render_template('login.html')
|
||
|
||
@main_bp.route('/register', methods=['GET', 'POST'])
|
||
def register():
|
||
if current_user.is_authenticated:
|
||
return redirect(url_for('.index'))
|
||
if request.method == 'POST':
|
||
username = request.form.get('username')
|
||
password = request.form.get('password')
|
||
if not username or not password:
|
||
flash(_('Username and password are required.'), 'error')
|
||
return redirect(url_for('.register'))
|
||
if User.query.filter_by(username=username).first():
|
||
flash(_('Username already exists.'), 'error')
|
||
return redirect(url_for('.register'))
|
||
|
||
user = User(username=username)
|
||
user.set_password(password)
|
||
user.config = UserConfig()
|
||
db.session.add(user)
|
||
try:
|
||
db.session.commit()
|
||
flash(_('Registration successful! Please log in.'), 'success')
|
||
return redirect(url_for('.login'))
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(_('An error occurred during registration. Please try again.'), 'error')
|
||
return redirect(url_for('.register'))
|
||
|
||
return render_template('register.html')
|
||
|
||
@main_bp.route('/logout')
|
||
@login_required
|
||
def logout():
|
||
logout_user()
|
||
flash(_('You have been logged out.'), 'success')
|
||
return redirect(url_for('.index'))
|
||
|
||
@main_bp.route('/')
|
||
@login_required
|
||
def index():
|
||
config = g.user_config
|
||
return render_template(
|
||
'index.html',
|
||
rms_config=config.get_rms_dict(),
|
||
google_config=config.get_google_dict(),
|
||
presets=config.presets,
|
||
sheets=config.sheets,
|
||
mappings=config.mappings,
|
||
client_email=config.google_client_email
|
||
)
|
||
|
||
@main_bp.route('/configure_rms', methods=['POST'])
|
||
@login_required
|
||
def configure_rms():
|
||
config = g.user_config
|
||
try:
|
||
host = request.form.get('host', '').strip()
|
||
login = request.form.get('login', '').strip()
|
||
password = request.form.get('password', '')
|
||
|
||
if not config.rms_password and not password:
|
||
flash(_('Password is required for the first time.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
if not host or not login:
|
||
flash(_('Host and Login fields must be filled.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
effective_password = password if password else config.rms_password
|
||
|
||
req_module = ReqModule(host, login, effective_password)
|
||
if req_module.login():
|
||
presets_data = req_module.take_presets()
|
||
req_module.logout()
|
||
|
||
config.rms_host = host
|
||
config.rms_login = login
|
||
if password:
|
||
config.rms_password = password
|
||
config.presets = presets_data
|
||
|
||
db.session.commit()
|
||
flash(_('Successfully authorized on RMS server. Received %(num)s presets.', num=len(presets_data)), 'success')
|
||
else:
|
||
flash(_('Authorization error on RMS server. Check host, login or password.'), 'error')
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(_('Error configuring RMS: %(error)s', error=str(e)), 'error')
|
||
|
||
return redirect(url_for('.index'))
|
||
|
||
@main_bp.route('/upload_credentials', methods=['POST'])
|
||
@login_required
|
||
def upload_credentials():
|
||
config = g.user_config
|
||
if 'cred_file' not in request.files or request.files['cred_file'].filename == '':
|
||
flash(_('No file was selected.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
cred_file = request.files['cred_file']
|
||
filename = cred_file.filename
|
||
# Получаем путь для сохранения файла в папке пользователя
|
||
user_cred_path = get_user_upload_path(filename)
|
||
temp_path = None
|
||
|
||
try:
|
||
# Сначала сохраняем файл во временную директорию для проверки
|
||
temp_dir = os.path.join(current_app.config['DATA_DIR'], "temp")
|
||
os.makedirs(temp_dir, exist_ok=True)
|
||
temp_path = os.path.join(temp_dir, f"temp_{current_user.id}_{filename}")
|
||
cred_file.save(temp_path)
|
||
|
||
with open(temp_path, 'r', encoding='utf-8') as f:
|
||
cred_data = json.load(f)
|
||
client_email = cred_data.get('client_email')
|
||
|
||
if not client_email:
|
||
flash(_('Could not find client_email in the credentials file.'), 'error')
|
||
# Не забываем удалить временный файл при ошибке
|
||
if os.path.exists(temp_path):
|
||
os.remove(temp_path)
|
||
return redirect(url_for('.index'))
|
||
|
||
# Если все хорошо, перемещаем файл из временной папки в постоянную
|
||
shutil.move(temp_path, user_cred_path)
|
||
|
||
# Сохраняем путь к файлу и email в базу данных
|
||
config.google_cred_file_path = user_cred_path
|
||
config.google_client_email = client_email
|
||
config.sheets = [] # Сбрасываем список листов при смене credentials
|
||
|
||
db.session.commit()
|
||
flash(_('Credentials file successfully uploaded. Email: %(email)s', email=client_email), 'success')
|
||
|
||
except json.JSONDecodeError:
|
||
flash(_('Error: Uploaded file is not a valid JSON.'), 'error')
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(_('Error processing credentials: %(error)s', error=str(e)), 'error')
|
||
finally:
|
||
# Гарантированно удаляем временный файл, если он еще существует
|
||
if temp_path and os.path.exists(temp_path):
|
||
os.remove(temp_path)
|
||
|
||
return redirect(url_for('.index'))
|
||
|
||
@main_bp.route('/configure_google', methods=['POST'])
|
||
@login_required
|
||
def configure_google():
|
||
config = g.user_config
|
||
sheet_url = request.form.get('sheet_url', '').strip()
|
||
|
||
if not sheet_url:
|
||
flash(_('Sheet URL must be provided.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
config.google_sheet_url = sheet_url
|
||
|
||
cred_path = config.google_cred_file_path
|
||
if not cred_path or not os.path.isfile(cred_path):
|
||
flash(_('Please upload a valid credentials file first.'), 'warning')
|
||
config.sheets = []
|
||
db.session.commit()
|
||
return redirect(url_for('.index'))
|
||
|
||
try:
|
||
gs_client = GoogleSheets(cred_path, sheet_url)
|
||
sheets_data = gs_client.get_sheets()
|
||
config.sheets = sheets_data
|
||
|
||
db.session.commit()
|
||
flash(_('Successfully connected to Google Sheets. Found %(num)s sheets. Settings saved.', num=len(sheets_data)), 'success')
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
config.sheets = []
|
||
flash(_('Error connecting to Google Sheets: %(error)s. Check the URL and service account permissions.', error=str(e)), 'error')
|
||
try:
|
||
db.session.commit()
|
||
except Exception:
|
||
db.session.rollback()
|
||
|
||
return redirect(url_for('.index'))
|
||
|
||
@main_bp.route('/mapping_set', methods=['POST'])
|
||
@login_required
|
||
def mapping_set():
|
||
config = g.user_config
|
||
try:
|
||
new_mappings = {}
|
||
for sheet in config.sheets:
|
||
report_key = f"sheet_{sheet['id']}"
|
||
selected_report_id = request.form.get(report_key)
|
||
if selected_report_id:
|
||
new_mappings[sheet['title']] = selected_report_id
|
||
|
||
config.mappings = new_mappings
|
||
db.session.commit()
|
||
|
||
flash(_('Mappings updated successfully.'), 'success')
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
flash(_('Error updating mappings: %(error)s', error=str(e)), 'error')
|
||
|
||
return redirect(url_for('.index'))
|
||
|
||
@main_bp.route('/render_olap', methods=['POST'])
|
||
@login_required
|
||
def render_olap():
|
||
config = g.user_config
|
||
sheet_title = None
|
||
req_module = None
|
||
|
||
try:
|
||
from_date, to_date = get_dates(request.form.get('start_date'), request.form.get('end_date'))
|
||
sheet_title = next((key for key in request.form if key.startswith('render_')), '').replace('render_', '')
|
||
if not sheet_title:
|
||
flash(_('Error: Could not determine which sheet to render the report for.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
report_id = config.mappings.get(sheet_title)
|
||
if not report_id:
|
||
flash(_('Error: No report is assigned to sheet "%(sheet)s".', sheet=sheet_title), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
if not all([config.rms_host, config.rms_login, config.rms_password, config.google_cred_file_path, config.google_sheet_url]):
|
||
flash(_('Error: RMS or Google Sheets configuration is incomplete.'), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
preset = next((p for p in config.presets if p.get('id') == report_id), None)
|
||
if not preset:
|
||
flash(_('Error: Preset with ID "%(id)s" not found in saved configuration.', id=report_id), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
template = generate_template_from_preset(preset)
|
||
json_body = render_temp(template, {"from_date": from_date, "to_date": to_date})
|
||
|
||
req_module = ReqModule(config.rms_host, config.rms_login, config.rms_password)
|
||
gs_client = GoogleSheets(config.google_cred_file_path, config.google_sheet_url)
|
||
|
||
if req_module.login():
|
||
result = req_module.take_olap(json_body)
|
||
|
||
if 'data' not in result or not isinstance(result['data'], list):
|
||
flash(_('Error: Unexpected response format from RMS for report "%(name)s".', name=preset.get('name', report_id)), 'error')
|
||
return redirect(url_for('.index'))
|
||
|
||
data_to_insert = []
|
||
if result['data']:
|
||
headers = list(result['data'][0].keys())
|
||
data_to_insert.append(headers)
|
||
for item in result['data']:
|
||
data_to_insert.append([item.get(h) for h in headers])
|
||
|
||
gs_client.clear_and_write_data(sheet_title, data_to_insert)
|
||
|
||
if len(data_to_insert) > 1:
|
||
flash(_('Report "%(name)s" data successfully written to sheet "%(sheet)s".', name=preset.get('name', report_id), sheet=sheet_title), 'success')
|
||
else:
|
||
flash(_('Report "%(name)s" returned no data for the selected period. Sheet "%(sheet)s" has been cleared.', name=preset.get('name', report_id), sheet=sheet_title), 'warning')
|
||
else:
|
||
flash(_('Error authorizing on RMS server when trying to get a report.'), 'error')
|
||
|
||
except ValueError as ve:
|
||
flash(_('Data Error: %(error)s', error=str(ve)), 'error')
|
||
except gspread.exceptions.APIError as api_err:
|
||
flash(_('Google API Error accessing sheet "%(sheet)s". Check service account permissions.', sheet=sheet_title), 'error')
|
||
except Exception as e:
|
||
flash(_('An unexpected error occurred: %(error)s', error=str(e)), 'error')
|
||
finally:
|
||
if req_module and req_module.token:
|
||
req_module.logout()
|
||
|
||
return redirect(url_for('.index')) |