📖 Теория: REST CRUD и объект request

⚡ Кратко

  • GET /questionsselect(Question) + scalars().all() → jsonify
  • POST /questionsrequest.get_json() → проверка → Question() → add+commit → 201
  • PUT /questions/<id> — найти → обновить → commit → 200; 404 если нет; 400 если нет поля
  • DELETE /questions/<id> — найти → delete+commit → 200; 404 если нет
  • POST /responses — создать Response + обновить Statistic (agree/disagree) атомарно
  • request.get_json() безопаснее request.json — не бросает исключение при плохом Content-Type

1. Реализация GET запросов с ORM

Первый шаг — интеграция модели Question с эндпоинтом get_questions. Задача: извлечь все вопросы из базы данных и вернуть список в формате JSON.

Импорт модели и настройка Blueprint

# app/routers/questions.py
from flask import Blueprint, jsonify
from app.models.questions import Question
from app.models import db

questions_bp = Blueprint('questions', __name__, url_prefix='/questions')

@questions_bp.route('/', methods=['GET'])
def get_questions():
    """Получение списка всех вопросов."""
    # SQLAlchemy 2.x: используем select() + scalars()
    from sqlalchemy import select
    questions = db.session.execute(select(Question)).scalars().all()
    questions_data = [{'id': q.id, 'text': q.text} for q in questions]
    return jsonify(questions_data)
Пояснение шагов:
  1. Импорт модели — класс Question даёт доступ к таблице через ORM.
  2. Запрос к БДselect(Question) формирует SQL; scalars().all() возвращает список объектов.
  3. Преобразование данных — list comprehension собирает список словарей для JSON-сериализации.
  4. Возвратjsonify() устанавливает Content-Type: application/json и сериализует данные.

Для тестирования: Postman → GET http://localhost:5000/questions. Ожидаемый статус: 200 OK. Если база пустая — вернётся пустой массив [].

2. Реализация POST запроса

Эндпоинт create_question принимает JSON в теле запроса, валидирует данные, создаёт объект модели и сохраняет его в базу данных.

from flask import Blueprint, request, jsonify
from app.models.questions import Question
from app.models import db

@questions_bp.route('/', methods=['POST'])
def create_question():
    """Создание нового вопроса."""
    data = request.get_json()         # Читаем JSON из тела запроса
    if not data or 'text' not in data:
        return jsonify({'error': 'No question text provided'}), 400

    question = Question(text=data['text'])  # Создаём экземпляр модели
    db.session.add(question)               # Добавляем в сессию
    db.session.commit()                    # Фиксируем в БД

    return jsonify({'message': 'Вопрос создан', 'id': question.id}), 201
Пояснение:
  1. request.get_json() — извлекает тело запроса как Python-словарь. Безопаснее request.json: не бросает исключение если Content-Type не application/json.
  2. Валидация — проверяем наличие ключа 'text'. При ошибке возвращаем 400 Bad Request.
  3. Создание объектаQuestion(text=...) создаёт экземпляр, пока не сохранённый в БД.
  4. Сохранениеadd() ставит объект в очередь; commit() фиксирует транзакцию и присваивает id.
  5. Ответ 201 Created — стандартный статус при успешном создании ресурса.

Тестирование в Postman: метод POST, URL http://localhost:5000/questions, тело raw JSON: {"text": "Согласны ли вы с этим?"}.

3. Объект request

Объект request — глобальный контекстный объект Flask, который автоматически создаётся при каждом HTTP-запросе и содержит всю информацию о нём.

Атрибут / метод Описание Пример использования
request.args Параметры строки запроса (после ?) GET /items?page=2request.args.get('page')
request.form Данные из HTML-форм (POST, application/x-www-form-urlencoded) request.form.get('username')
request.data Сырые байты тела запроса Когда Content-Type не распознан
request.json / request.get_json() Тело запроса как JSON (dict). Метод безопаснее свойства data = request.get_json()
request.method HTTP-метод строкой: 'GET', 'POST', ... if request.method == 'POST':
request.headers HTTP-заголовки запроса request.headers.get('Authorization')
request.files Загруженные файлы (FileStorage) file = request.files.get('photo')

4. HTTP статус-коды

Статус-коды — числовые коды ответа сервера. Информируют клиента о результате обработки запроса.

Группа Смысл Ключевые коды
1xx Информационные 100 Continue, 101 Switching Protocols
2xx Успешно 200 OK, 201 Created, 204 No Content
3xx Перенаправление 301 Moved Permanently, 302 Found, 304 Not Modified
4xx Ошибка клиента 400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict
5xx Ошибка сервера 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

Статус-коды во Flask

Flask по умолчанию возвращает 200 для успешных GET и 200 для успешных функций. Статус задаётся явно вторым элементом кортежа:

from flask import jsonify, make_response, abort

# Способ 1 — кортеж (response, status)
return jsonify({'message': 'ok'}), 200

# Способ 2 — make_response с кастомными заголовками
response = make_response(jsonify({'message': 'created'}), 201)
response.headers['Location'] = f'/questions/{question.id}'
return response

# Способ 3 — abort() для стандартных ошибок
from flask import abort
@app.route('/items/<int:id>')
def get_item(id):
    item = db.session.get(Item, id)
    if not item:
        abort(404)
    return jsonify(item)

5. Реализация функций и эндпоинтов

get_question — получение по ID

@questions_bp.route('/<int:id>', methods=['GET'])
def get_question(id):
    """Получение деталей конкретного вопроса по его ID."""
    question = db.session.get(Question, id)   # SQLAlchemy 2.x
    if question is None:
        return jsonify({'message': 'Вопрос с таким ID не найден'}), 404
    return jsonify({'id': question.id, 'text': question.text}), 200

update_question — обновление по ID

@questions_bp.route('/<int:id>', methods=['PUT'])
def update_question(id):
    """Обновление конкретного вопроса по его ID."""
    question = db.session.get(Question, id)
    if question is None:
        return jsonify({'message': 'Вопрос с таким ID не найден'}), 404

    data = request.get_json()
    if not data or 'text' not in data:
        return jsonify({'message': 'Текст вопроса не предоставлен'}), 400

    question.text = data['text']
    db.session.commit()
    return jsonify({'message': f'Вопрос обновлён: {question.text}'}), 200

delete_question — удаление по ID

@questions_bp.route('/<int:id>', methods=['DELETE'])
def delete_question(id):
    """Удаление конкретного вопроса по его ID."""
    question = db.session.get(Question, id)
    if question is None:
        return jsonify({'message': 'Вопрос с таким ID не найден'}), 404

    db.session.delete(question)
    db.session.commit()
    return jsonify({'message': f'Вопрос с ID {id} удалён'}), 200

6. Эндпоинты responses — статистика ответов

get_responses — агрегированная статистика

# app/routers/response.py
from flask import Blueprint, request, jsonify
from app.models import db
from app.models.questions import Question
from app.models.statistic import Statistic
from app.models.response import Response
from sqlalchemy import select

response_bp = Blueprint('response', __name__, url_prefix='/responses')

@response_bp.route('/', methods=['GET'])
def get_responses():
    """Получение агрегированной статистики ответов."""
    statistics = db.session.execute(select(Statistic)).scalars().all()
    results = [
        {
            'question_id': stat.question_id,
            'agree_count': stat.agree_count,
            'disagree_count': stat.disagree_count
        }
        for stat in statistics
    ]
    return jsonify(results), 200

add_response — добавление ответа + обновление статистики

@response_bp.route('/', methods=['POST'])
def add_response():
    """Добавление нового ответа на вопрос с обновлением статистики."""
    data = request.get_json()
    if not data or 'question_id' not in data or 'is_agree' not in data:
        return jsonify({'message': 'Некорректные данные'}), 400

    question = db.session.get(Question, data['question_id'])
    if not question:
        return jsonify({'message': 'Вопрос не найден'}), 404

    # Создаём Response
    response = Response(
        question_id=question.id,
        is_agree=data['is_agree']
    )
    db.session.add(response)

    # Атомарное обновление статистики
    from sqlalchemy import select
    statistic = db.session.execute(
        select(Statistic).filter_by(question_id=question.id)
    ).scalar_one_or_none()

    if not statistic:
        statistic = Statistic(question_id=question.id, agree_count=0, disagree_count=0)
        db.session.add(statistic)

    if data['is_agree']:
        statistic.agree_count += 1
    else:
        statistic.disagree_count += 1

    db.session.commit()
    return jsonify({'message': f'Ответ на вопрос {question.id} добавлен'}), 201
Ключевые моменты add_response:
  • Проверяем наличие и корректность всех обязательных полей.
  • Создаём Response — факт ответа — и добавляем в сессию.
  • Проверяем наличие записи Statistic для вопроса. Если нет — создаём с нулевыми счётчиками.
  • Инкрементируем нужный счётчик (agree_count или disagree_count).
  • Единственный commit() в конце сохраняет обе записи атомарно — либо оба объекта, либо ни один.

7. Резюме: полная карта эндпоинтов Community Pulse

Метод URL Функция Статус успеха
GET/questions/get_questions200
GET/questions/<id>get_question200 / 404
POST/questions/create_question201 / 400
PUT/questions/<id>update_question200 / 400 / 404
DELETE/questions/<id>delete_question200 / 404
GET/responses/get_responses200
POST/responses/add_response201 / 400 / 404
← К оглавлению урока