🏠 Домашнее задание — Summary session 3

🎯 Расширение Community Pulse: категории ⏱️ ~90 мин

Это ДЗ из разбора лекции Summary session 3. На лекции разбирались задачи по расширению проекта Community Pulse поддержкой категорий вопросов. Задачи ниже взяты из этого разбора.

Часть 1 — Расширение модели: добавить категории

Расширить функциональность существующего API для поддержки категорий вопросов.

1

Задача 1. Создание модели Category

Создайте новую модель Category с использованием Flask-SQLAlchemy в модуле app/models/.

Модель должна содержать поля:

  • id: первичный ключ, целое число, авто-инкремент
  • name: строка (макс. 100 символов), не должна быть пустой

Модель Question должна быть обновлена: добавить поле category_id как внешний ключ на categories.id.

2

Задача 2. Миграция базы данных

Создайте новую миграцию для добавления таблицы категорий и обновления таблицы вопросов с использованием Flask-Migrate:

flask db migrate -m "Add category model"
flask db upgrade

Часть 2 — Обновление схем и эндпоинтов

3

Задача 3. Обновление схем Pydantic

В файле app/schemas/question.py:

  • Добавьте схему CategoryBase для сериализации и валидации данных категории
  • Обновите QuestionCreate: добавьте необязательное поле category_id
  • Обновите QuestionResponse: добавьте поле category_id (может быть None)
4

Задача 4. Эндпоинты для категорий

Создайте новый Blueprint categories_bp в файле app/routers/categories.py.

Реализуйте эндпоинты:

  • POST /categories — создание новой категории
  • GET /categories — получение списка всех категорий
  • PUT /categories/<id> — обновление категории по ID
  • DELETE /categories/<id> — удаление категории по ID
5

Задача 5. Обновление эндпоинтов вопросов

Обновите существующие эндпоинты:

  • GET /questions — возвращать вопросы с информацией о категориях
  • POST /questions — поддерживать указание category_id при создании

Подготовка окружения

A

Клонирование / создание ветки

# PowerShell
# Если Community Pulse уже в репозитории:
git checkout -b lesson/12-categories

# Создать venv и установить зависимости
python -m venv venv
venv\Scripts\activate
pip install flask flask-sqlalchemy flask-migrate pydantic email-validator

# Проверить установку
python -c "import flask; print(flask.__version__)"
B

Структура файлов после выполнения ДЗ

/community_pulse/
|-- app/
|   |-- __init__.py
|   |-- routers/
|   |   |-- questions.py
|   |   |-- response.py
|   |   |-- categories.py     # новый файл
|   |-- models/
|   |   |-- questions.py      # обновлён (добавлен Category, ForeignKey)
|   |   |-- response.py
|   |-- schemas/
|       |-- question.py       # обновлён (CategoryBase, category_id)
|       |-- response.py
|-- config.py
|-- run.py
|-- migrations/               # создан flask db init

Пошаговое решение

Показать решение: Задача 1 — Модель Category
# app/models/questions.py (обновлённый)
from app.models import db

class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    questions = db.relationship('Question', backref='category', lazy=True)

    def __repr__(self):
        return f'<Category {self.name}>'

class Question(db.Model):
    __tablename__ = 'questions'
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(255), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
    responses = db.relationship('Response', backref='question', lazy=True)

    def __repr__(self):
        return f'<Question {self.id}: {self.text[:40]}>'
Показать решение: Задача 3 — Схемы Pydantic
# app/schemas/question.py (обновлённый)
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict

class CategoryBase(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str

class QuestionCreate(BaseModel):
    text: str = Field(..., min_length=12, description="Текст вопроса")
    category_id: Optional[int] = Field(None, description="ID категории (опционально)")

class QuestionResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    text: str
    category_id: Optional[int] = None

class MessageResponse(BaseModel):
    message: str
Показать решение: Задача 4 — Blueprint категорий
# app/routers/categories.py
from flask import Blueprint, request, jsonify
from pydantic import ValidationError
from app.models import db
from app.models.questions import Category
from app.schemas.question import CategoryBase

categories_bp = Blueprint('categories', __name__, url_prefix='/categories')

@categories_bp.route('/', methods=['GET'])
def get_categories():
    categories = Category.query.all()
    return jsonify([CategoryBase.model_validate(c).model_dump() for c in categories]), 200

@categories_bp.route('/', methods=['POST'])
def create_category():
    data = request.get_json()
    if not data or 'name' not in data:
        return jsonify({'error': 'Поле name обязательно'}), 400
    category = Category(name=data['name'])
    db.session.add(category)
    db.session.commit()
    return jsonify(CategoryBase.model_validate(category).model_dump()), 201

@categories_bp.route('/<int:id>', methods=['PUT'])
def update_category(id):
    category = db.session.get(Category, id)
    if category is None:
        return jsonify({'message': 'Категория не найдена'}), 404
    data = request.get_json()
    if 'name' in data:
        category.name = data['name']
        db.session.commit()
    return jsonify(CategoryBase.model_validate(category).model_dump()), 200

@categories_bp.route('/<int:id>', methods=['DELETE'])
def delete_category(id):
    category = db.session.get(Category, id)
    if category is None:
        return jsonify({'message': 'Категория не найдена'}), 404
    db.session.delete(category)
    db.session.commit()
    return jsonify({'message': f'Категория {id} удалена'}), 200
Показать решение: регистрация нового Blueprint
# app/__init__.py (обновлённый)
from flask import Flask
from flask_migrate import Migrate
from app.models import db
from app.routers.questions import questions_bp
from app.routers.response import response_bp
from app.routers.categories import categories_bp  # новый

def create_app(config=None):
    app = Flask(__name__)
    if config:
        app.config.from_object(config)
    db.init_app(app)
    Migrate(app, db)
    app.register_blueprint(questions_bp)
    app.register_blueprint(response_bp)
    app.register_blueprint(categories_bp)  # зарегистрировать!
    return app

Проверка в VS Code

C

Запуск и тестирование

# Запустить приложение
python run.py

# Тест через PowerShell (Invoke-RestMethod)
Invoke-RestMethod -Uri "http://localhost:5000/categories/" -Method POST `
  -ContentType "application/json" `
  -Body '{"name": "Наука"}'

Invoke-RestMethod -Uri "http://localhost:5000/categories/" -Method GET

Invoke-RestMethod -Uri "http://localhost:5000/questions/" -Method POST `
  -ContentType "application/json" `
  -Body '{"text": "Поддерживаете ли вы развитие науки?", "category_id": 1}'
D

Конфигурация отладки (.vscode/launch.json)

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Community Pulse",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/run.py",
      "console": "integratedTerminal",
      "env": {
        "FLASK_ENV": "development",
        "FLASK_DEBUG": "1"
      }
    }
  ]
}

Запустите через F5. Поставьте точку останова на db.session.commit() при создании категории и проверьте состояние объекта в панели переменных.

Связь с разделами теории

Критерии выполнения