📖 Теория: REST CRUD и объект request
⚡ Кратко
- GET /questions —
select(Question)+scalars().all()→ jsonify - POST /questions —
request.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)
- Импорт модели — класс
Questionдаёт доступ к таблице через ORM. - Запрос к БД —
select(Question)формирует SQL;scalars().all()возвращает список объектов. - Преобразование данных — list comprehension собирает список словарей для JSON-сериализации.
- Возврат —
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
- request.get_json() — извлекает тело запроса как Python-словарь. Безопаснее
request.json: не бросает исключение если Content-Type не application/json. - Валидация — проверяем наличие ключа
'text'. При ошибке возвращаем 400 Bad Request. - Создание объекта —
Question(text=...)создаёт экземпляр, пока не сохранённый в БД. - Сохранение —
add()ставит объект в очередь;commit()фиксирует транзакцию и присваиваетid. - Ответ 201 Created — стандартный статус при успешном создании ресурса.
Тестирование в Postman: метод POST, URL http://localhost:5000/questions, тело raw JSON: {"text": "Согласны ли вы с этим?"}.
3. Объект request
Объект request — глобальный контекстный объект Flask, который автоматически создаётся при каждом HTTP-запросе и содержит всю информацию о нём.
| Атрибут / метод | Описание | Пример использования |
|---|---|---|
request.args |
Параметры строки запроса (после ?) |
GET /items?page=2 → request.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
- Проверяем наличие и корректность всех обязательных полей.
- Создаём
Response— факт ответа — и добавляем в сессию. - Проверяем наличие записи
Statisticдля вопроса. Если нет — создаём с нулевыми счётчиками. - Инкрементируем нужный счётчик (
agree_countилиdisagree_count). - Единственный
commit()в конце сохраняет обе записи атомарно — либо оба объекта, либо ни один.
7. Резюме: полная карта эндпоинтов Community Pulse
| Метод | URL | Функция | Статус успеха |
|---|---|---|---|
| GET | /questions/ | get_questions | 200 |
| GET | /questions/<id> | get_question | 200 / 404 |
| POST | /questions/ | create_question | 201 / 400 |
| PUT | /questions/<id> | update_question | 200 / 400 / 404 |
| DELETE | /questions/<id> | delete_question | 200 / 404 |
| GET | /responses/ | get_responses | 200 |
| POST | /responses/ | add_response | 201 / 400 / 404 |