← Назад к курсу
Полное руководство по разработке веб-приложений на Flask
1. Введение в Flask
Что такое Flask?
Flask — это микрофреймворк для веб-разработки на Python, который предоставляет только базовые возможности, оставляя выбор дополнительных компонентов разработчику.
Основные принципы:
- Минимализм — только необходимый функционал
- Расширяемость через extensions
- Простота и гибкость
Установка Flask
# Создание виртуального окружения python -m venv venv # Активация (Windows) venv\Scripts\activate # Активация (Linux/Mac) source venv/bin/activate # Установка Flask pip install flask # Для разработки также полезно установить pip install python-dotenv flask-sqlalchemy flask-wtf flask-login flask-migrate # Проверка установки python -c "import flask; print(flask.__version__)"
2. Создание первого приложения
Минимальное приложение
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return 'Привет, Flask!'
@app.route('/hello/<name>')
def hello(name):
return f'Привет, {name}!'
if __name__ == '__main__':
app.run(debug=True)
Структура проекта Flask
myproject/ ├── app.py # Основной файл приложения ├── config.py # Конфигурация ├── requirements.txt # Зависимости ├── .env # Переменные окружения ├── .gitignore ├── instance/ # Папка instance (данные БД) ├── app/ # Пакет приложения │ ├── __init__.py # Инициализация приложения │ ├── models.py # Модели данных │ ├── views.py # Представления (роуты) │ ├── forms.py # Формы │ ├── templates/ # Шаблоны Jinja2 │ │ ├── base.html │ │ └── ... │ ├── static/ # Статические файлы │ │ ├── css/ │ │ ├── js/ │ │ └── images/ │ └── auth/ # Модуль аутентификации │ ├── __init__.py │ └── routes.py └── migrations/ # Миграции базы данных
Фабрика приложений (Application Factory)
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
import os
# Инициализация расширений
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
def create_app(config_class='config.Config'):
"""Фабрика для создания экземпляра приложения"""
app = Flask(__name__)
# Загрузка конфигурации
app.config.from_object(config_class)
# Инициализация расширений с приложением
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
# Настройка LoginManager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Пожалуйста, войдите для доступа к этой странице.'
login_manager.login_message_category = 'info'
# Регистрация Blueprints
from app.auth import bp as auth_bp
from app.main import bp as main_bp
from app.blog import bp as blog_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(main_bp)
app.register_blueprint(blog_bp, url_prefix='/blog')
# Создание таблиц БД
with app.app_context():
db.create_all()
return app
3. Конфигурация приложения
Конфигурационные классы
# config.py
import os
from dotenv import load_dotenv
load_dotenv() # Загрузка переменных из .env
class Config:
"""Базовая конфигурация"""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Почта
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.gmail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
# Пагинация
POSTS_PER_PAGE = 10
COMMENTS_PER_PAGE = 20
class DevelopmentConfig(Config):
"""Конфигурация для разработки"""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(os.path.dirname(__file__), 'instance/dev.db')
class TestingConfig(Config):
"""Конфигурация для тестирования"""
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""Конфигурация для продакшена"""
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(os.path.dirname(__file__), 'instance/prod.db')
# Безопасность
SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_HTTPONLY = True
# Словарь конфигураций
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
Файл .env
# .env SECRET_KEY=your-secret-key-here-change-in-production DATABASE_URL=postgresql://username:password@localhost/dbname DEV_DATABASE_URL=sqlite:///instance/dev.db MAIL_USERNAME=your-email@gmail.com MAIL_PASSWORD=your-email-password
4. Модели данных (SQLAlchemy)
Определение моделей
# app/models.py
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login_manager
class User(UserMixin, db.Model):
"""Модель пользователя"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True, nullable=False)
email = db.Column(db.String(120), unique=True, index=True, nullable=False)
password_hash = db.Column(db.String(256))
is_active = db.Column(db.Boolean, default=True)
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Связи
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
"""Хеширование пароля"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Проверка пароля"""
return check_password_hash(self.password_hash, password)
@property
def is_authenticated(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return str(self.id)
@login_manager.user_loader
def load_user(user_id):
"""Загрузка пользователя для Flask-Login"""
return User.query.get(int(user_id))
class Post(db.Model):
"""Модель поста блога"""
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
slug = db.Column(db.String(250), unique=True, nullable=False)
is_published = db.Column(db.Boolean, default=False)
views = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Внешние ключи
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
# Связи
comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
tags = db.relationship('Tag', secondary='post_tags', backref=db.backref('posts', lazy='dynamic'))
def __repr__(self):
return f'<Post {self.title}>'
def increment_views(self):
"""Увеличение счетчика просмотров"""
self.views += 1
db.session.commit()
class Category(db.Model):
"""Модель категории"""
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
slug = db.Column(db.String(120), unique=True, nullable=False)
description = db.Column(db.Text)
# Связи
posts = db.relationship('Post', backref='category', lazy='dynamic')
def __repr__(self):
return f'<Category {self.name}>'
class Tag(db.Model):
"""Модель тега"""
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
slug = db.Column(db.String(70), unique=True, nullable=False)
def __repr__(self):
return f'<Tag {self.name}>'
# Таблица многие-ко-многим для постов и тегов
post_tags = db.Table('post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('posts.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
)
class Comment(db.Model):
"""Модель комментария"""
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
is_approved = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Внешние ключи
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=True)
# Связи для древовидных комментариев
replies = db.relationship('Comment',
backref=db.backref('parent', remote_side=[id]),
lazy='dynamic')
def __repr__(self):
return f'<Comment {self.id}>'
5. Маршрутизация (Routing)
Базовые маршруты
# app/main/routes.py
from flask import render_template, request, flash, redirect, url_for, abort, jsonify
from flask_login import login_required, current_user
from app import db
from app.main import bp
from app.models import Post, Category, Comment
from app.main.forms import CommentForm
from sqlalchemy import or_
import markdown
@bp.route('/')
@bp.route('/index')
def index():
"""Главная страница"""
page = request.args.get('page', 1, type=int)
# Получаем посты с пагинацией
posts = Post.query.filter_by(is_published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=10, error_out=False)
# Получаем популярные посты
popular_posts = Post.query.filter_by(is_published=True)\
.order_by(Post.views.desc())\
.limit(5).all()
# Получаем все категории
categories = Category.query.all()
return render_template('index.html',
posts=posts,
popular_posts=popular_posts,
categories=categories)
@bp.route('/about')
def about():
"""Страница "О сайте" """
return render_template('about.html')
@bp.route('/contact', methods=['GET', 'POST'])
def contact():
"""Страница контактов"""
if request.method == 'POST':
# Обработка формы контактов
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# Здесь можно отправить email
flash('Сообщение отправлено! Мы ответим вам в ближайшее время.', 'success')
return redirect(url_for('main.contact'))
return render_template('contact.html')
@bp.route('/search')
def search():
"""Поиск по сайту"""
query = request.args.get('q', '')
page = request.args.get('page', 1, type=int)
if query:
# Поиск по заголовку и содержанию
posts = Post.query.filter(
or_(
Post.title.ilike(f'%{query}%'),
Post.content.ilike(f'%{query}%')
),
Post.is_published == True
).order_by(Post.created_at.desc())\
.paginate(page=page, per_page=10, error_out=False)
else:
posts = []
return render_template('search.html',
posts=posts,
query=query)
@bp.route('/post/<slug>', methods=['GET', 'POST'])
def post_detail(slug):
"""Детальная страница поста"""
post = Post.query.filter_by(slug=slug, is_published=True).first_or_404()
# Увеличиваем счетчик просмотров
post.increment_views()
# Форма комментария
form = CommentForm()
if form.validate_on_submit():
if not current_user.is_authenticated:
flash('Для добавления комментария необходимо войти.', 'warning')
return redirect(url_for('auth.login'))
comment = Comment(
content=form.content.data,
user_id=current_user.id,
post_id=post.id,
is_approved=True # Для зарегистрированных пользователей
)
db.session.add(comment)
db.session.commit()
flash('Комментарий добавлен!', 'success')
return redirect(url_for('main.post_detail', slug=slug))
# Получаем комментарии
comments = Comment.query.filter_by(post_id=post.id, is_approved=True)\
.order_by(Comment.created_at.desc()).all()
# Конвертируем markdown в HTML
html_content = markdown.markdown(post.content)
return render_template('post_detail.html',
post=post,
html_content=html_content,
form=form,
comments=comments)
@bp.route('/category/<slug>')
def category_posts(slug):
"""Посты по категории"""
category = Category.query.filter_by(slug=slug).first_or_404()
page = request.args.get('page', 1, type=int)
posts = Post.query.filter_by(category_id=category.id, is_published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=10, error_out=False)
return render_template('category.html',
category=category,
posts=posts)
# API эндпоинты
@bp.route('/api/posts')
def api_posts():
"""API для получения списка постов"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
posts = Post.query.filter_by(is_published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=per_page)
return jsonify({
'posts': [{
'id': post.id,
'title': post.title,
'slug': post.slug,
'excerpt': post.content[:200] + '...' if len(post.content) > 200 else post.content,
'created_at': post.created_at.isoformat(),
'author': post.author.username,
'views': post.views
} for post in posts.items],
'total': posts.total,
'pages': posts.pages,
'current_page': posts.page
})
@bp.errorhandler(404)
def not_found_error(error):
"""Обработка ошибки 404"""
return render_template('errors/404.html'), 404
@bp.errorhandler(500)
def internal_error(error):
"""Обработка ошибки 500"""
db.session.rollback()
return render_template('errors/500.html'), 500
Blueprints (Модули приложения)
# app/main/__init__.py
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
# app/auth/__init__.py
from flask import Blueprint
bp = Blueprint('auth', __name__, template_folder='templates')
from app.auth import routes
# app/blog/__init__.py
from flask import Blueprint
bp = Blueprint('blog', __name__, template_folder='templates')
from app.blog import routes
6. Формы (WTForms)
Создание форм
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User
class LoginForm(FlaskForm):
"""Форма входа"""
username = StringField('Имя пользователя',
validators=[DataRequired()])
password = PasswordField('Пароль',
validators=[DataRequired()])
remember_me = BooleanField('Запомнить меня')
submit = SubmitField('Войти')
class RegistrationForm(FlaskForm):
"""Форма регистрации"""
username = StringField('Имя пользователя',
validators=[DataRequired(),
Length(min=3, max=64)])
email = StringField('Email',
validators=[DataRequired(), Email()])
password = PasswordField('Пароль',
validators=[DataRequired(),
Length(min=6)])
password2 = PasswordField('Повторите пароль',
validators=[DataRequired(),
EqualTo('password')])
submit = SubmitField('Зарегистрироваться')
def validate_username(self, username):
"""Проверка уникальности имени пользователя"""
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Это имя пользователя уже занято.')
def validate_email(self, email):
"""Проверка уникальности email"""
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Этот email уже зарегистрирован.')
class PostForm(FlaskForm):
"""Форма создания/редактирования поста"""
title = StringField('Заголовок',
validators=[DataRequired(),
Length(min=3, max=200)])
content = TextAreaField('Содержание',
validators=[DataRequired()],
render_kw={"rows": 10})
category = SelectField('Категория',
coerce=int,
validators=[DataRequired()])
tags = StringField('Теги (через запятую)')
is_published = BooleanField('Опубликовать')
submit = SubmitField('Сохранить')
class CommentForm(FlaskForm):
"""Форма комментария"""
content = TextAreaField('Комментарий',
validators=[DataRequired(),
Length(min=3, max=1000)],
render_kw={"rows": 4,
"placeholder": "Введите ваш комментарий..."})
submit = SubmitField('Отправить')
class ProfileForm(FlaskForm):
"""Форма редактирования профиля"""
username = StringField('Имя пользователя',
validators=[DataRequired(),
Length(min=3, max=64)])
email = StringField('Email',
validators=[DataRequired(), Email()])
bio = TextAreaField('О себе',
render_kw={"rows": 4})
submit = SubmitField('Обновить')
class ContactForm(FlaskForm):
"""Форма обратной связи"""
name = StringField('Ваше имя',
validators=[DataRequired()])
email = StringField('Email',
validators=[DataRequired(), Email()])
message = TextAreaField('Сообщение',
validators=[DataRequired(),
Length(min=10, max=2000)],
render_kw={"rows": 5})
submit = SubmitField('Отправить')
7. Шаблоны (Jinja2)
Базовый шаблон
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Мой Flask Блог{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Иконки Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<!-- Собственные стили -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<!-- Навигация -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<i class="bi bi-journal-text"></i> Flask Blog
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">
<i class="bi bi-house"></i> Главная
</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('blog.create_post') }}">
<i class="bi bi-plus-circle"></i> Новый пост
</a>
</li>
{% endif %}
<!-- Категории -->
{% set categories = [] %}
{% for category in g.categories %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.category_posts', slug=category.slug) }}">
{{ category.name }}
</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.about') }}">
<i class="bi bi-info-circle"></i> О сайте
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.contact') }}">
<i class="bi bi-envelope"></i> Контакты
</a>
</li>
</ul>
<!-- Поиск -->
<form class="d-flex me-3" action="{{ url_for('main.search') }}" method="get">
<input class="form-control me-2" type="search" name="q" placeholder="Поиск..."
value="{{ request.args.get('q', '') }}">
<button class="btn btn-outline-light" type="submit">
<i class="bi bi-search"></i>
</button>
</form>
<!-- Аутентификация -->
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ url_for('auth.profile') }}">
<i class="bi bi-person"></i> Профиль
</a>
</li>
{% if current_user.is_admin %}
<li>
<a class="dropdown-item" href="{{ url_for('admin.index') }}">
<i class="bi bi-gear"></i> Админка
</a>
</li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right"></i> Выйти
</a>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">
<i class="bi bi-box-arrow-in-right"></i> Войти
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">
<i class="bi bi-person-plus"></i> Регистрация
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash сообщения -->
<div class="container mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Основной контент -->
<main class="container py-4">
{% block content %}{% endblock %}
</main>
<!-- Футер -->
<footer class="bg-dark text-white py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-4">
<h5>Flask Blog</h5>
<p>Простой блог на Flask для демонстрации возможностей фреймворка.</p>
</div>
<div class="col-md-4">
<h5>Навигация</h5>
<ul class="list-unstyled">
<li><a href="{{ url_for('main.index') }}" class="text-white-50">Главная</a></li>
<li><a href="{{ url_for('main.about') }}" class="text-white-50">О сайте</a></li>
<li><a href="{{ url_for('main.contact') }}" class="text-white-50">Контакты</a></li>
</ul>
</div>
<div class="col-md-4">
<h5>Контакты</h5>
<p><i class="bi bi-envelope"></i> info@flaskblog.com</p>
<p>© {{ current_year }} Flask Blog. Все права защищены.</p>
</div>
</div>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Собственные скрипты -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
Шаблон списка постов
<!-- app/templates/index.html -->
{% extends "base.html" %}
{% block title %}Главная - Flask Blog{% endblock %}
{% block content %}
<div class="row">
<!-- Основной контент -->
<div class="col-lg-8">
<h1 class="mb-4">Последние посты</h1>
{% for post in posts.items %}
<article class="card mb-4 shadow-sm">
{% if post.image_url %}
<img src="{{ post.image_url }}" class="card-img-top" alt="{{ post.title }}">
{% endif %}
<div class="card-body">
<h2 class="card-title">
<a href="{{ url_for('main.post_detail', slug=post.slug) }}"
class="text-decoration-none text-dark">
{{ post.title }}
</a>
</h2>
<div class="text-muted mb-3">
<small>
<i class="bi bi-calendar"></i> {{ post.created_at.strftime('%d.%m.%Y %H:%M') }} |
<i class="bi bi-person"></i> {{ post.author.username }} |
<i class="bi bi-eye"></i> {{ post.views }} |
<i class="bi bi-chat"></i> {{ post.comments.count() }}
{% if post.category %}
| <a href="{{ url_for('main.category_posts', slug=post.category.slug) }}"
class="text-decoration-none">
<i class="bi bi-folder"></i> {{ post.category.name }}
</a>
{% endif %}
</small>
</div>
<p class="card-text">
{{ post.content|striptags|truncate(300) }}
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('main.post_detail', slug=post.slug) }}"
class="btn btn-primary">
Читать далее <i class="bi bi-arrow-right"></i>
</a>
<div>
{% for tag in post.tags %}
<span class="badge bg-secondary me-1">{{ tag.name }}</span>
{% endfor %}
</div>
</div>
</div>
</article>
{% else %}
<div class="alert alert-info">
Постов пока нет. Будьте первым, кто напишет пост!
</div>
{% endfor %}
<!-- Пагинация -->
{% if posts.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=posts.prev_num) }}">
<i class="bi bi-chevron-left"></i> Назад
</a>
</li>
{% endif %}
{% for page_num in posts.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page_num %}
{% if page_num == posts.page %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=page_num) }}">
{{ page_num }}
</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.index', page=posts.next_num) }}">
Вперед <i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
<!-- Сайдбар -->
<div class="col-lg-4">
<!-- Категории -->
<div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-folder"></i> Категории</h5>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
{% for category in categories %}
<li class="mb-2">
<a href="{{ url_for('main.category_posts', slug=category.slug) }}"
class="text-decoration-none">
<i class="bi bi-folder2"></i> {{ category.name }}
<span class="badge bg-secondary float-end">{{ category.posts.count() }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Популярные посты -->
<div class="card mb-4 shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-fire"></i> Популярные посты</h5>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
{% for post in popular_posts %}
<li class="mb-2">
<a href="{{ url_for('main.post_detail', slug=post.slug) }}"
class="text-decoration-none">
{{ post.title|truncate(40) }}
<span class="text-muted float-end">
<i class="bi bi-eye"></i> {{ post.views }}
</span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Теги -->
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-tags"></i> Теги</h5>
</div>
<div class="card-body">
{% for tag in tags %}
<a href="{{ url_for('main.tag_posts', slug=tag.slug) }}"
class="badge bg-light text-dark text-decoration-none me-1 mb-1">
{{ tag.name }}
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
Шаблон формы
<!-- app/templates/auth/login.html -->
{% extends "base.html" %}
{% block title %}Вход - Flask Blog{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="bi bi-box-arrow-in-right"></i> Вход в систему</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.login') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
{% for error in form.username.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% for error in form.password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
<hr class="my-4">
<div class="text-center">
<p class="mb-2">Еще нет аккаунта?</p>
<a href="{{ url_for('auth.register') }}" class="btn btn-outline-primary">
Зарегистрироваться
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
8. Аутентификация (Flask-Login)
Реализация аутентификации
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user, login_required
from werkzeug.urls import url_parse
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, ProfileForm, ResetPasswordRequestForm, ResetPasswordForm
from app.models import User
from app.auth.email import send_password_reset_email
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""Страница входа"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Неверное имя пользователя или пароль', 'danger')
return redirect(url_for('auth.login'))
if not user.is_active:
flash('Аккаунт деактивирован', 'warning')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
# Перенаправление на следующую страницу
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', form=form)
@bp.route('/logout')
@login_required
def logout():
"""Выход из системы"""
logout_user()
flash('Вы успешно вышли из системы', 'success')
return redirect(url_for('main.index'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
"""Страница регистрации"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Поздравляем! Вы успешно зарегистрировались.', 'success')
login_user(user)
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)
@bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
"""Страница профиля"""
form = ProfileForm()
if form.validate_on_submit():
current_user.username = form.username.data
current_user.email = form.email.data
current_user.bio = form.bio.data
db.session.commit()
flash('Профиль обновлен', 'success')
return redirect(url_for('auth.profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.email.data = current_user.email
form.bio.data = current_user.bio
return render_template('auth/profile.html', form=form)
@bp.route('/reset-password-request', methods=['GET', 'POST'])
def reset_password_request():
"""Запрос сброса пароля"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
flash('Инструкции по сбросу пароля отправлены на ваш email', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html', form=form)
@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
def reset_password(token):
"""Сброс пароля по токену"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
flash('Неверный или просроченный токен', 'danger')
return redirect(url_for('auth.reset_password_request'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('Пароль успешно изменен', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
# Декоратор для проверки администратора
def admin_required(f):
"""Декоратор для проверки прав администратора"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
abort(403)
return f(*args, **kwargs)
return decorated_function
9. Административная панель
Flask-Admin
# app/admin.py
from flask import redirect, url_for, request
from flask_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from flask_login import current_user
from app import db
from app.models import User, Post, Category, Comment, Tag
class AdminModelView(ModelView):
"""Базовый класс для административных представлений"""
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for('auth.login', next=request.url))
class UserAdminView(AdminModelView):
"""Административное представление для пользователей"""
column_list = ['username', 'email', 'is_active', 'is_admin', 'created_at']
column_searchable_list = ['username', 'email']
column_filters = ['is_active', 'is_admin', 'created_at']
form_columns = ['username', 'email', 'is_active', 'is_admin']
def on_model_change(self, form, model, is_created):
if 'password' in form:
if form.password.data:
model.set_password(form.password.data)
class PostAdminView(AdminModelView):
"""Административное представление для постов"""
column_list = ['title', 'author', 'category', 'is_published', 'views', 'created_at']
column_searchable_list = ['title', 'content']
column_filters = ['is_published', 'created_at', 'category']
form_columns = ['title', 'content', 'author', 'category', 'tags', 'is_published']
form_widget_args = {
'content': {
'rows': 20
}
}
class MyAdminIndexView(AdminIndexView):
"""Кастомное представление главной страницы админки"""
@expose('/')
def index(self):
if not current_user.is_authenticated or not current_user.is_admin:
return redirect(url_for('auth.login'))
stats = {
'users': User.query.count(),
'posts': Post.query.count(),
'categories': Category.query.count(),
'comments': Comment.query.count()
}
return self.render('admin/index.html', stats=stats)
# Инициализация Flask-Admin
admin = Admin(name='Flask Blog Admin',
template_mode='bootstrap4',
index_view=MyAdminIndexView())
def init_admin(app):
"""Инициализация административной панели"""
admin.init_app(app)
# Добавление представлений
admin.add_view(UserAdminView(User, db.session, name='Пользователи'))
admin.add_view(PostAdminView(Post, db.session, name='Посты'))
admin.add_view(AdminModelView(Category, db.session, name='Категории'))
admin.add_view(AdminModelView(Comment, db.session, name='Комментарии'))
admin.add_view(AdminModelView(Tag, db.session, name='Теги'))
10. Миграции базы данных (Flask-Migrate)
Настройка миграций
# Инициализация миграций flask db init # Создание миграции flask db migrate -m "Initial migration" # Применение миграций flask db upgrade # Откат миграции flask db downgrade # Просмотр истории миграций flask db history # Создание ревизии вручную flask db revision -m "Add new column"
Пример миграции
# migrations/versions/xxxx_initial_migration.py
"""Initial migration
Revision ID: xxxx
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
# Создание таблицы users
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=64), nullable=False),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('password_hash', sa.String(length=256), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# Создание таблицы categories
op.create_table('categories',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('slug', sa.String(length=120), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('slug')
)
def downgrade():
op.drop_table('categories')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
11. REST API с Flask-RESTful
Создание API
# app/api/__init__.py
from flask import Blueprint
from flask_restful import Api
bp = Blueprint('api', __name__, url_prefix='/api')
api = Api(bp)
from app.api import posts, users, auth
# app/api/posts.py
from flask_restful import Resource, reqparse, abort
from flask_login import login_required, current_user
from app import db
from app.models import Post, User
parser = reqparse.RequestParser()
parser.add_argument('title', type=str, required=True, help='Title is required')
parser.add_argument('content', type=str, required=True, help='Content is required')
parser.add_argument('category_id', type=int)
parser.add_argument('is_published', type=bool, default=False)
class PostListResource(Resource):
"""Ресурс для работы со списком постов"""
def get(self):
"""Получить все посты"""
posts = Post.query.filter_by(is_published=True).all()
return {
'posts': [{
'id': post.id,
'title': post.title,
'slug': post.slug,
'content': post.content[:200] + '...',
'author': post.author.username,
'created_at': post.created_at.isoformat(),
'views': post.views
} for post in posts]
}, 200
@login_required
def post(self):
"""Создать новый пост"""
args = parser.parse_args()
post = Post(
title=args['title'],
content=args['content'],
author=current_user,
is_published=args['is_published']
)
if args.get('category_id'):
post.category_id = args['category_id']
db.session.add(post)
db.session.commit()
return {
'message': 'Post created successfully',
'post_id': post.id
}, 201
class PostResource(Resource):
"""Ресурс для работы с конкретным постом"""
def get(self, post_id):
"""Получить пост по ID"""
post = Post.query.get_or_404(post_id)
if not post.is_published and (not current_user.is_authenticated or post.author != current_user):
abort(403, message='You do not have permission to view this post')
# Увеличиваем счетчик просмотров
post.views += 1
db.session.commit()
return {
'post': {
'id': post.id,
'title': post.title,
'content': post.content,
'author': post.author.username,
'category': post.category.name if post.category else None,
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat(),
'views': post.views,
'is_published': post.is_published
}
}, 200
@login_required
def put(self, post_id):
"""Обновить пост"""
post = Post.query.get_or_404(post_id)
# Проверка прав
if post.author != current_user and not current_user.is_admin:
abort(403, message='You do not have permission to edit this post')
args = parser.parse_args()
post.title = args['title']
post.content = args['content']
post.is_published = args['is_published']
if args.get('category_id'):
post.category_id = args['category_id']
db.session.commit()
return {
'message': 'Post updated successfully'
}, 200
@login_required
def delete(self, post_id):
"""Удалить пост"""
post = Post.query.get_or_404(post_id)
# Проверка прав
if post.author != current_user and not current_user.is_admin:
abort(403, message='You do not have permission to delete this post')
db.session.delete(post)
db.session.commit()
return {
'message': 'Post deleted successfully'
}, 200
# Регистрация ресурсов
from app.api import posts
api.add_resource(posts.PostListResource, '/posts')
api.add_resource(posts.PostResource, '/posts/<int:post_id>')
12. Тестирование
Модульные тесты
# tests/test_basics.py
import unittest
from flask import current_app
from app import create_app, db
class BasicsTestCase(unittest.TestCase):
def setUp(self):
"""Настройка тестового окружения"""
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
self.client = self.app.test_client()
def tearDown(self):
"""Очистка тестового окружения"""
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_app_exists(self):
"""Тест: приложение существует"""
self.assertFalse(current_app is None)
def test_app_is_testing(self):
"""Тест: приложение в режиме тестирования"""
self.assertTrue(current_app.config['TESTING'])
def test_home_page(self):
"""Тест: главная страница работает"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Flask Blog', response.data)
# tests/test_models.py
from app.models import User, Post
class UserModelTestCase(unittest.TestCase):
def test_password_hashing(self):
"""Тест: хеширование пароля"""
u = User(username='testuser')
u.set_password('testpass')
self.assertTrue(u.check_password('testpass'))
self.assertFalse(u.check_password('wrongpass'))
def test_user_representation(self):
"""Тест: строковое представление пользователя"""
u = User(username='testuser')
self.assertEqual(str(u), '<User testuser>')
# tests/test_auth.py
class AuthTestCase(unittest.TestCase):
def test_registration(self):
"""Тест: регистрация пользователя"""
response = self.client.post('/auth/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'testpass123',
'password2': 'testpass123'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn(b'Поздравляем!', response.data)
# Проверка, что пользователь создан в БД
user = User.query.filter_by(username='testuser').first()
self.assertIsNotNone(user)
self.assertEqual(user.email, 'test@example.com')
def test_login(self):
"""Тест: вход пользователя"""
# Сначала создаем пользователя
u = User(username='testuser', email='test@example.com')
u.set_password('testpass')
db.session.add(u)
db.session.commit()
# Пытаемся войти
response = self.client.post('/auth/login', data={
'username': 'testuser',
'password': 'testpass'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn(b'Главная', response.data)
Запуск тестов
# Установка тестовых зависимостей pip install pytest coverage # Запуск всех тестов pytest # Запуск с покрытием кода coverage run -m pytest coverage report coverage html # Генерация HTML отчета # Запуск конкретного теста pytest tests/test_auth.py::AuthTestCase::test_login # Запуск с подробным выводом pytest -v
13. Оптимизация и лучшие практики
Кэширование
# app/__init__.py
from flask_caching import Cache
cache = Cache(config={'CACHE_TYPE': 'redis', 'CACHE_REDIS_URL': 'redis://localhost:6379/0'})
def create_app(config_class='config.Config'):
app = Flask(__name__)
# ...
cache.init_app(app)
return app
# Использование кэширования в представлениях
@app.route('/')
@cache.cached(timeout=300) # Кэшировать на 5 минут
def index():
posts = Post.query.filter_by(is_published=True).order_by(Post.created_at.desc()).limit(10).all()
return render_template('index.html', posts=posts)
# Кэширование фрагментов шаблонов
{% cache 300, 'recent_posts' %}
<!-- Кэшируемый контент -->
{% endcache %}
Логирование
# app/__init__.py
import logging
from logging.handlers import RotatingFileHandler
import os
def create_app(config_class='config.Config'):
app = Flask(__name__)
# Настройка логирования
if not app.debug and not app.testing:
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/flaskblog.log',
maxBytes=10240,
backupCount=10)
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('FlaskBlog startup')
return app
Контекстные процессоры
# app/main/__init__.py
from flask import g
from app.models import Category
@bp.app_context_processor
def inject_categories():
"""Добавление категорий во все шаблоны"""
return dict(
categories=Category.query.all(),
current_year=datetime.now().year
)
@bp.before_app_request
def before_request():
"""Выполняется перед каждым запросом"""
g.user = current_user
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
14. Деплой приложения
requirements.txt
# Основные зависимости Flask==2.3.3 Werkzeug==2.3.7 Jinja2==3.1.2 # База данных Flask-SQLAlchemy==3.0.5 Flask-Migrate==4.0.5 psycopg2-binary==2.9.7 SQLAlchemy==2.0.19 # Аутентификация и формы Flask-Login==0.6.2 Flask-WTF==1.1.1 WTForms==3.0.1 email-validator==2.0.0 # Административная панель Flask-Admin==1.6.1 # REST API Flask-RESTful==0.3.10 # Дополнительные утилиты python-dotenv==1.0.0 Markdown==3.4.4 bleach==6.0.0 # Для продакшена gunicorn==21.2.0 whitenoise==6.5.0 # Для разработки pytest==7.4.2 coverage==7.3.2
gunicorn_config.py
# gunicorn_config.py bind = "0.0.0.0:8000" workers = 4 worker_class = "sync" threads = 2 timeout = 120 keepalive = 5 max_requests = 1000 max_requests_jitter = 50
Dockerfile
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Установка системных зависимостей
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Копирование зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование приложения
COPY . .
# Создание директории для логов
RUN mkdir -p logs
# Запуск приложения
CMD ["gunicorn", "--config", "gunicorn_config.py", "app:create_app()"]
docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- DATABASE_URL=postgresql://user:password@db/flaskblog
- SECRET_KEY=your-secret-key
depends_on:
- db
- redis
volumes:
- ./logs:/app/logs
- ./static:/app/static
- ./instance:/app/instance
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=flaskblog
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./static:/static:ro
depends_on:
- web
volumes:
postgres_data:
Deploy на Heroku
# Создание Procfile echo "web: gunicorn app:create_app()" > Procfile # Создание runtime.txt echo "python-3.11.5" > runtime.txt # Развертывание heroku create your-app-name heroku addons:create heroku-postgresql:hobby-dev heroku addons:create heroku-redis:hobby-dev heroku config:set SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))") git push heroku main heroku open
15. Полный пример: Приложение To-Do List
Модель
# app/todo/models.py
from datetime import datetime
from app import db
class Todo(db.Model):
__tablename__ = 'todos'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
completed = db.Column(db.Boolean, default=False)
priority = db.Column(db.Integer, default=1) # 1-высокий, 2-средний, 3-низкий
due_date = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Внешний ключ
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
def __repr__(self):
return f'<Todo {self.title}>'
def to_dict(self):
"""Конвертация в словарь для API"""
return {
'id': self.id,
'title': self.title,
'description': self.description,
'completed': self.completed,
'priority': self.priority,
'due_date': self.due_date.isoformat() if self.due_date else None,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat()
}
API для To-Do
# app/todo/api.py
from flask_restful import Resource, reqparse
from flask_login import login_required, current_user
from app import db
from app.todo.models import Todo
todo_parser = reqparse.RequestParser()
todo_parser.add_argument('title', type=str, required=True, help='Title is required')
todo_parser.add_argument('description', type=str)
todo_parser.add_argument('completed', type=bool)
todo_parser.add_argument('priority', type=int, choices=[1, 2, 3])
todo_parser.add_argument('due_date', type=str)
class TodoListResource(Resource):
@login_required
def get(self):
"""Получить все задачи пользователя"""
todos = Todo.query.filter_by(user_id=current_user.id)\
.order_by(Todo.priority, Todo.due_date)\
.all()
return {
'todos': [todo.to_dict() for todo in todos]
}, 200
@login_required
def post(self):
"""Создать новую задачу"""
args = todo_parser.parse_args()
todo = Todo(
title=args['title'],
description=args.get('description'),
completed=args.get('completed', False),
priority=args.get('priority', 2),
user_id=current_user.id
)
if args.get('due_date'):
todo.due_date = datetime.fromisoformat(args['due_date'])
db.session.add(todo)
db.session.commit()
return {
'message': 'Todo created successfully',
'todo': todo.to_dict()
}, 201
class TodoResource(Resource):
@login_required
def get(self, todo_id):
"""Получить задачу по ID"""
todo = Todo.query.filter_by(id=todo_id, user_id=current_user.id).first_or_404()
return {'todo': todo.to_dict()}, 200
@login_required
def put(self, todo_id):
"""Обновить задачу"""
todo = Todo.query.filter_by(id=todo_id, user_id=current_user.id).first_or_404()
args = todo_parser.parse_args()
todo.title = args['title']
todo.description = args.get('description', todo.description)
todo.completed = args.get('completed', todo.completed)
todo.priority = args.get('priority', todo.priority)
if args.get('due_date'):
todo.due_date = datetime.fromisoformat(args['due_date'])
db.session.commit()
return {
'message': 'Todo updated successfully',
'todo': todo.to_dict()
}, 200
@login_required
def delete(self, todo_id):
"""Удалить задачу"""
todo = Todo.query.filter_by(id=todo_id, user_id=current_user.id).first_or_404()
db.session.delete(todo)
db.session.commit()
return {'message': 'Todo deleted successfully'}, 200
# Регистрация в API
api.add_resource(TodoListResource, '/todos')
api.add_resource(TodoResource, '/todos/<int:todo_id>')
Заключение
Flask предоставляет минималистичный, но мощный подход к веб-разработке. Ключевые преимущества:
- Гибкость — выбирайте только нужные компоненты
- Простота — быстрое начало разработки
- Расширяемость — богатая экосистема расширений
- Контроль — полный контроль над структурой приложения
Рекомендации для дальнейшего изучения:
- Официальная документация Flask — https://flask.palletsprojects.com/
- Flask Mega-Tutorial от Miguel Grinberg
- Flask Web Development (книга)
- Django vs Flask — выбор в зависимости от проекта
- Microservices с Flask — для масштабируемых приложений
- Asyncio с Flask — для высоконагруженных приложений
Лучшие практики:
- Используйте Application Factory
- Разделяйте приложение на Blueprints
- Всегда используйте виртуальные окружения
- Храните секреты в переменных окружения
- Пишите тесты для критического функционала
- Используйте миграции для изменения БД
- Оптимизируйте запросы к базе данных
- Настройте логирование для продакшена
Удачи в разработке на Flask!