refactor: introduce app factory + blueprints (auth, core, admin users, invite); add utils and wsgi entrypoint; keep legacy routes for now

This commit is contained in:
2025-09-04 14:25:39 +03:00
parent d54e12123b
commit a3d4fbb31d
9 changed files with 566 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from werkzeug.security import generate_password_hash
from app.models import User
from app.utils.auth import admin_required
bp = Blueprint('admin_users', __name__)
@bp.route('/admin/users', methods=['GET', 'POST'], endpoint='manage_users')
@admin_required
def manage_users():
if request.method == 'POST':
username = request.form['username'].strip()
email = request.form['email'].strip()
full_name = request.form.get('full_name', '').strip()
password = request.form['password']
is_admin_flag = request.form.get('is_admin') == 'on'
if not username, or not email or not password:
flash('Заполните обязательные поля', 'danger')
return redirect(url_for('manage_users'))
if User.select().where((User.username == username) | (User.email == email)).exists():
flash('Пользователь с таким логином или email уже существует', 'danger')
return redirect(url_for('manage_users'))
User.create(
username=username,
email=email,
full_name=full_name or None,
password_hash=generate_password_hash(password),
is_admin=is_admin_flag,
)
flash('Пользователь создан', 'success')
return redirect(url_for('manage_users'))
users = User.select().order_by(User.id)
return render_template('admin/users.html', users=users, title='Пользователи')
@bp.route('/admin/users/<int:user_id>/reset_password', methods=['POST'], endpoint='admin_reset_password')
@admin_required
def admin_reset_password(user_id):
user = User.get_or_none(User.id == user_id)
if not user:
flash('Пользователь не найден', 'danger')
return redirect(url_for('manage_users'))
new_password = request.form.get('new_password')
if not new_password:
flash('Укажите новый пароль', 'danger')
return redirect(url_for('manage_users'))
user.password_hash = generate_password_hash(new_password)
user.save()
flash('Пароль обновлён', 'success')
return redirect(url_for('manage_users'))
@bp.route('/admin/users/<int:user_id>/toggle_admin', methods=['POST'], endpoint='admin_toggle_admin')
@admin_required
def admin_toggle_admin(user_id):
from flask_login import current_user
if current_user.id == user_id:
flash('Нельзя менять свои собственные права', 'warning')
return redirect(url_for('manage_users'))
user = User.get_or_none(User.id == user_id)
if not user:
flash('Пользователь не найден', 'danger')
return redirect(url_for('manage_users'))
user.is_admin = not user.is_admin
user.save()
flash('Права обновлены', 'success')
return redirect(url_for('manage_users'))
@bp.route('/admin/users/<int:user_id>/delete', methods=['POST'], endpoint='admin_delete_user')
@admin_required
def admin_delete_user(user_id):
from flask_login import current_user
if current_user.id == user_id:
flash('Нельзя удалить самого себя', 'warning')
return redirect(url_for('manage_users'))
user = User.get_or_none(User.id == user_id)
if not user:
flash('Пользователь не найден', 'danger')
return redirect(url_for('manage_users'))
try:
user.delete_instance(recursive=True)
flash('Пользователь удалён', 'success')
except Exception:
flash('Не удалось удалить пользователя', 'danger')
return redirect(url_for('manage_users'))

29
app/blueprints/auth.py Normal file
View File

@@ -0,0 +1,29 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user
from werkzeug.security import check_password_hash
from app.models import User
bp = Blueprint('auth', __name__)
@bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.get_or_none(User.username == username)
if user and check_password_hash(user.password_hash, password):
login_user(user)
return redirect(url_for('dashboard' if not user.is_admin else 'manage_surveys'))
else:
flash('Неверный логин или пароль', 'danger')
return render_template('login.html', title='Вход')
@bp.route('/logout', endpoint='logout')
def logout():
logout_user()
return redirect(url_for('choose_survey'))

54
app/blueprints/core.py Normal file
View File

@@ -0,0 +1,54 @@
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required, current_user
from app.models import SurveyInvite, UserSurvey, SurveyType, User
bp = Blueprint('core', __name__)
@bp.route('/', endpoint='home')
def home():
return redirect(url_for('choose_survey'))
@bp.route('/dashboard', endpoint='dashboard')
@login_required
def dashboard():
if current_user.is_admin:
invites = (SurveyInvite
.select()
.join(UserSurvey)
.order_by(SurveyInvite.sent_at.desc()))
else:
invites = (SurveyInvite
.select()
.join(UserSurvey)
.where(UserSurvey.user == current_user.id)
.order_by(SurveyInvite.sent_at.desc()))
return render_template('user/dashboard.html', invites=invites, title='Отчёты')
@bp.route('/quiz', endpoint='choose_survey')
@login_required
def choose_survey():
if current_user.is_admin:
surveys = SurveyType.select()
users = User.select().where(User.is_admin == False)
assignments = {
s.id: [us.user.id for us in UserSurvey.select().where(UserSurvey.survey_type == s)]
for s in surveys
}
else:
surveys = (SurveyType
.select()
.join(UserSurvey)
.where(UserSurvey.user == current_user.id)
.distinct())
users = []
assignments = {}
return render_template('choose_survey.html',
surveys=surveys,
users=users,
assignments=assignments,
title='Опросы')

90
app/blueprints/invite.py Normal file
View File

@@ -0,0 +1,90 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
import datetime
import json
from app.models import SurveyInvite, Product, Feature, Platform, PlatformFeature, UserSurvey, SurveyType
from app.utils.mail import send_result_email
bp = Blueprint('invite', __name__)
@bp.route('/invite/<uuid_str>', methods=['GET', 'POST'], endpoint='handle_invite')
def handle_invite(uuid_str):
invite = SurveyInvite.get_or_none(SurveyInvite.uuid == uuid_str)
if not invite:
return "Ссылка недействительна", 404
user_survey = invite.survey
survey_type = user_survey.survey_type
products = Product.select().where(Product.survey_type == survey_type)
if request.method == 'GET':
questions = {}
for product in products:
features = Feature.select().where(Feature.product == product)
questions[product.name] = {f.name: f.question_text for f in features}
return render_template("invite/form.html", invite=invite, survey=user_survey, questions=questions)
full_name = request.form.get('full_name')
phone = request.form.get('phone')
organization = request.form.get('organization')
answers = dict(request.form)
for k in ['full_name', 'phone', 'organization']:
answers.pop(k, None)
selected_keys = list(answers.keys())
platforms = Platform.select().where(Platform.survey_type == survey_type)
features = Feature.select().join(Product).where(
Feature.name.in_(selected_keys),
Product.survey_type == survey_type
)
comment = request.form.get('comment')
scores = {}
unsupported = {}
total = len(selected_keys)
for platform in platforms:
match = 0
unsup = []
for feature in features:
pf = PlatformFeature.get_or_none(platform=platform, feature=feature)
if pf and pf.supported:
match += 1
else:
unsup.append(feature.question_text)
scores[platform.name] = round((match / total) * 100) if total else 0
unsupported[platform.name] = unsup
SurveyResult = __import__('app.models', fromlist=['SurveyResult']).SurveyResult
SurveyResult.create(
invite=invite,
full_name=full_name,
phone=phone,
organization=organization,
answers=json.dumps(answers),
platform_scores=json.dumps(scores),
unsupported=json.dumps(unsupported),
submitted_at=datetime.datetime.now(),
comment=comment
)
invite.responded = True
invite.save()
send_result_email(
to_email=invite.survey.user.email,
from_email=invite.recipient_email,
survey_name=survey_type.name,
scores=scores,
unsupported=unsupported,
sender_name=invite.survey.user.full_name
)
if not invite.show_result:
flash("Спасибо! Ваши ответы отправлены.", "success")
return redirect("https://www.mont.ru/ru-ru")
return render_template("invite/result.html",
scores=scores,
unsupported_features=unsupported,
chart_labels=list(scores.keys()),
chart_data=list(scores.values()))