🏠 Домашнее задание 6: Категории вопросов
⚡ ДЗ 6 кратко
Цель: Расширить API — добавить поддержку категорий вопросов (Category).
- Схема
CategoryBaseв Pydantic - CRUD эндпоинты: POST/GET/PUT/DELETE /categories
- Обновить
QuestionCreateиQuestionResponse— добавитьcategory_id - GET /questions возвращает вопросы с информацией о категории
- POST /questions принимает
category
Задание из LMS (оригинал)
Цели задания: Расширить функциональность существующего API для поддержки категорий вопросов.
Задачи
-
Обновление схем Pydantic
- Добавьте новую схему
CategoryBaseвschemas/question.pyдля сериализации и валидации данных категории. - Обновите схему
QuestionCreateиQuestionResponseдля интеграции данных о категории.
- Добавьте новую схему
-
Разработка API эндпоинтов
- Создайте новые эндпоинты для создания, чтения, обновления и удаления категорий.
POST /categories— создание новой категории.GET /categories— получение списка всех категорий.PUT /categories/{id}— обновление категории по ID.DELETE /categories/{id}— удаление категории по ID.
-
Обновите существующие эндпоинты вопросов
GET /questionsдолжен возвращать вопросы с информацией о категориях.POST /questionsдолжен позволять указывать категорию при создании вопроса.
Подготовка окружения
1. Активировать виртуальное окружение
# Windows PowerShell
cd community_pulse
.venv\Scripts\activate
# Linux/macOS
source .venv/bin/activate
2. Проверить зависимости
pip install flask flask-sqlalchemy sqlalchemy pydantic flask-migrate
3. Структура проекта
community_pulse/
├── app/
│ ├── __init__.py # create_app()
│ ├── models/
│ │ ├── __init__.py
│ │ ├── questions.py # Question model
│ │ ├── category.py # NEW: Category model
│ │ ├── response.py # Response model
│ │ └── statistic.py # Statistic model
│ ├── routers/
│ │ ├── questions.py # UPDATE: поддержка category
│ │ ├── response.py # без изменений
│ │ └── categories.py # NEW: CRUD для Category
│ └── schemas/
│ └── question.py # UPDATE: CategoryBase, QuestionCreate, QuestionResponse
├── migrations/
├── .env
└── run.py
4. Git — зафиксировать начальное состояние
git add -A
git commit -m "chore: start homework 6 — add category support"
Пошаговое решение
Шаг 1: Модель Category
Создать app/models/category.py. Категория имеет название и опциональное описание.
# app/models/category.py
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, Text
from app.models import db
class Category(db.Model):
__tablename__ = 'categories'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# Обратная связь с Question
questions: Mapped[list['Question']] = relationship(
'Question', back_populates='category'
)
def __repr__(self) -> str:
return f'<Category {self.name}>'
Логика: Mapped[str | None] — nullable поле (SQLAlchemy 2.x). unique=True предотвращает дубликаты названий.
Шаг 2: Обновить модель Question
Добавить внешний ключ category_id и связь с Category.
# app/models/questions.py
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey
from app.models import db
class Question(db.Model):
__tablename__ = 'questions'
id: Mapped[int] = mapped_column(primary_key=True)
text: Mapped[str] = mapped_column(String(500), nullable=False)
category_id: Mapped[int | None] = mapped_column(
ForeignKey('categories.id'), nullable=True
)
# Связь с Category
category: Mapped['Category | None'] = relationship(
'Category', back_populates='questions'
)
def __repr__(self) -> str:
return f'<Question {self.id}: {self.text[:30]}>'
Логика: nullable=True — вопрос может быть без категории (опциональная связь).
Шаг 3: Миграция базы данных
# Первая миграция
flask db init # если ещё не инициализировано
# Генерация миграции
flask db migrate -m "add category model and foreign key to question"
# Применить миграцию
flask db upgrade
Проверить: в migrations/versions/ появился новый файл. Открыть и убедиться, что создаётся таблица categories и добавляется колонка category_id в questions.
Шаг 4: Pydantic-схемы
Обновить app/schemas/question.py:
# app/schemas/question.py
from pydantic import BaseModel, Field
from typing import Optional
class CategoryBase(BaseModel):
"""Схема для категории."""
id: int
name: str
description: Optional[str] = None
model_config = {'from_attributes': True}
class CategoryCreate(BaseModel):
"""Схема для создания категории."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
class QuestionCreate(BaseModel):
"""Схема для создания вопроса."""
text: str = Field(..., min_length=1, max_length=500)
category_id: Optional[int] = None
class QuestionResponse(BaseModel):
"""Схема ответа при возврате вопроса."""
id: int
text: str
category: Optional[CategoryBase] = None
model_config = {'from_attributes': True}
Логика:
CategoryBase— базовая схема для сериализации категории в ответах.model_config = {'from_attributes': True}— позволяет создавать схему из ORM-объекта.QuestionResponse.category— Optional[CategoryBase], чтобы вопрос мог быть без категории.
Шаг 5: Роутер categories.py
# app/routers/categories.py
from flask import Blueprint, request, jsonify
from sqlalchemy import select
from app.models import db
from app.models.category import Category
from app.schemas.question import CategoryCreate
categories_bp = Blueprint('categories', __name__, url_prefix='/categories')
@categories_bp.route('/', methods=['GET'])
def get_categories():
"""Получение списка всех категорий."""
categories = db.session.execute(select(Category)).scalars().all()
result = [{'id': c.id, 'name': c.name, 'description': c.description}
for c in categories]
return jsonify(result), 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
# Валидация через Pydantic
try:
schema = CategoryCreate(**data)
except Exception as e:
return jsonify({'error': str(e)}), 422
# Проверка уникальности
existing = db.session.execute(
select(Category).filter_by(name=schema.name)
).scalar_one_or_none()
if existing:
return jsonify({'error': f'Категория "{schema.name}" уже существует'}), 409
category = Category(name=schema.name, description=schema.description)
db.session.add(category)
db.session.commit()
return jsonify({'id': category.id, 'name': category.name}), 201
@categories_bp.route('/<int:id>', methods=['PUT'])
def update_category(id):
"""Обновление категории по ID."""
category = db.session.get(Category, id)
if category is None:
return jsonify({'message': 'Категория не найдена'}), 404
data = request.get_json()
if not data:
return jsonify({'message': 'Нет данных'}), 400
if 'name' in data:
category.name = data['name']
if 'description' in data:
category.description = data['description']
db.session.commit()
return jsonify({'id': category.id, 'name': category.name}), 200
@categories_bp.route('/<int:id>', methods=['DELETE'])
def delete_category(id):
"""Удаление категории по 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
Шаг 6: Обновить questions.py — поддержка category
# app/routers/questions.py (обновлённые эндпоинты)
@questions_bp.route('/', methods=['GET'])
def get_questions():
"""Получение списка вопросов с информацией о категории."""
questions = db.session.execute(select(Question)).scalars().all()
result = []
for q in questions:
q_data = {'id': q.id, 'text': q.text, 'category': None}
if q.category:
q_data['category'] = {
'id': q.category.id,
'name': q.category.name,
'description': q.category.description
}
result.append(q_data)
return jsonify(result)
@questions_bp.route('/', methods=['POST'])
def create_question():
"""Создание нового вопроса с опциональной категорией."""
data = request.get_json()
if not data or 'text' not in data:
return jsonify({'error': 'No question text provided'}), 400
question = Question(
text=data['text'],
category_id=data.get('category_id') # опциональный внешний ключ
)
db.session.add(question)
db.session.commit()
return jsonify({'message': 'Вопрос создан', 'id': question.id}), 201
Шаг 7: Зарегистрировать categories_bp в create_app()
# app/__init__.py
from app.routers.categories import categories_bp
app.register_blueprint(categories_bp)
Проверка в VS Code
Терминал — запуск приложения
# Активировать venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux/macOS
# Запустить
flask run --debug
launch.json для отладки (F5)
{
"version": "0.2.0",
"configurations": [
{
"name": "Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"args": ["run", "--debug"],
"env": {
"FLASK_APP": "run.py",
"FLASK_ENV": "development"
},
"jinja": true,
"justMyCode": true
}
]
}
Точки останова
Для проверки логики add / commit:
- Открыть
app/routers/categories.py - Кликнуть слева от строки
db.session.commit()вcreate_category— красный кружок - F5 → Postman: POST /categories с
{"name": "Тест"} - VS Code остановится на commit — в панели Debug можно проверить значение
category.id(будет None до commit) - F10 (step over) — после commit
category.idполучает значение из БД
Тестирование в Postman
# 1. Создать категорию
POST http://localhost:5000/categories/
Body: {"name": "Транспорт", "description": "Вопросы о транспорте"}
Expected: 201 {"id": 1, "name": "Транспорт"}
# 2. Получить все категории
GET http://localhost:5000/categories/
Expected: 200 [{"id": 1, "name": "Транспорт", ...}]
# 3. Создать вопрос с категорией
POST http://localhost:5000/questions/
Body: {"text": "Нужны ли выделенные полосы для автобусов?", "category_id": 1}
Expected: 201 {"message": "Вопрос создан", "id": 1}
# 4. Получить вопросы с категорией
GET http://localhost:5000/questions/
Expected: 200 [{"id": 1, "text": "...", "category": {"id": 1, "name": "Транспорт"}}]
# 5. Обновить категорию
PUT http://localhost:5000/categories/1
Body: {"name": "Городской транспорт"}
Expected: 200
# 6. Удалить категорию
DELETE http://localhost:5000/categories/1
Expected: 200
Связь с разделами курса
- Теория — паттерн CRUD с db.session.get/add/commit, статус-коды 201/400/404/409
- Примеры — полный код questions.py и response.py как эталон структуры
- Старый vs Новый — почему используем select/scalars вместо Model.query.all()
- Урок 09: Теория — Application Factory, create_app, register_blueprint
- Урок 03: Pydantic — BaseModel, Field, model_config, from_attributes