{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "648710b0",
   "metadata": {},
   "source": [
    "# 22-05 · Flask: маршруты, шаблоны и формы\n",
    "\n",
    "Практика к разделу [«Flask в Python»](../../site/chapters/glava-22/22-05-flask.html). Полный проект — `projects/flask/todo-app/app.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a21d2b22",
   "metadata": {},
   "source": [
    "## Про тестовый клиент в этом ноутбуке\n",
    "\n",
    "Обычно Flask-приложение запускают командой `python app.py`, и оно ждёт запросы браузера бесконечно (`app.run()`). В автоматически выполняемом ноутбуке нет браузера и нет смысла ждать вечно, поэтому мы используем `app.test_client()` — он отправляет настоящие HTTP-запросы (GET, POST) прямо внутри Python, без сервера и без браузера. Это стандартный способ Flask тестировать приложения."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "770dea0b",
   "metadata": {},
   "source": [
    "## Рабочий пример — собираем маленькое приложение"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e3145953",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.627000Z",
     "iopub.status.busy": "2026-08-14T20:59:45.626862Z",
     "iopub.status.idle": "2026-08-14T20:59:45.748594Z",
     "shell.execute_reply": "2026-08-14T20:59:45.748188Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Приложение и тестовый клиент готовы.\n"
     ]
    }
   ],
   "source": [
    "from flask import Flask, redirect, render_template_string, request, url_for\n",
    "\n",
    "app = Flask(__name__)\n",
    "zadachi = [\"Выучить основы Python\", \"Собрать сайт на Flask\"]\n",
    "\n",
    "SHABLON_GLAVNOJ = \"\"\"\n",
    "<h1>Мой список задач</h1>\n",
    "<ul>\n",
    "{% for zadacha in zadachi %}\n",
    "  <li>{{ zadacha }}</li>\n",
    "{% endfor %}\n",
    "</ul>\n",
    "\"\"\"\n",
    "\n",
    "SHABLON_PRIVET = \"<h1>Привет, {{ imya }}!</h1>\"\n",
    "\n",
    "\n",
    "@app.route(\"/\")\n",
    "def glavnaya():\n",
    "    return render_template_string(SHABLON_GLAVNOJ, zadachi=zadachi)\n",
    "\n",
    "\n",
    "@app.route(\"/privet/<imya>\")\n",
    "def privet(imya):\n",
    "    return render_template_string(SHABLON_PRIVET, imya=imya)\n",
    "\n",
    "\n",
    "@app.route(\"/dobavit\", methods=[\"POST\"])\n",
    "def dobavit():\n",
    "    novaya_zadacha = request.form.get(\"zadacha\", \"\").strip()\n",
    "    if novaya_zadacha:\n",
    "        zadachi.append(novaya_zadacha)\n",
    "    return redirect(url_for(\"glavnaya\"))\n",
    "\n",
    "\n",
    "client = app.test_client()\n",
    "print(\"Приложение и тестовый клиент готовы.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e85a2cce",
   "metadata": {},
   "source": [
    "## Эксперимент 1 — главная страница (GET /)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a5bc3d50",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.749975Z",
     "iopub.status.busy": "2026-08-14T20:59:45.749798Z",
     "iopub.status.idle": "2026-08-14T20:59:45.754505Z",
     "shell.execute_reply": "2026-08-14T20:59:45.753978Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Код ответа: 200\n",
      "\n",
      "<h1>Мой список задач</h1>\n",
      "<ul>\n",
      "\n",
      "  <li>Выучить основы Python</li>\n",
      "\n",
      "  <li>Собрать сайт на Flask</li>\n",
      "\n",
      "</ul>\n"
     ]
    }
   ],
   "source": [
    "otvet = client.get(\"/\")\n",
    "telo = otvet.get_data(as_text=True)\n",
    "\n",
    "print(\"Код ответа:\", otvet.status_code)\n",
    "print(telo)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9656c9df",
   "metadata": {},
   "source": [
    "## Проверка результата"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6d0530bd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.755974Z",
     "iopub.status.busy": "2026-08-14T20:59:45.755842Z",
     "iopub.status.idle": "2026-08-14T20:59:45.758427Z",
     "shell.execute_reply": "2026-08-14T20:59:45.758005Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Верно: главная страница отдаёт список задач с кодом 200.\n"
     ]
    }
   ],
   "source": [
    "assert otvet.status_code == 200\n",
    "assert \"Выучить основы Python\" in telo\n",
    "assert \"Собрать сайт на Flask\" in telo\n",
    "print(\"Верно: главная страница отдаёт список задач с кодом 200.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f114f28",
   "metadata": {},
   "source": [
    "## Эксперимент 2 — динамический маршрут /privet/<imya>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "10985333",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.759602Z",
     "iopub.status.busy": "2026-08-14T20:59:45.759390Z",
     "iopub.status.idle": "2026-08-14T20:59:45.764440Z",
     "shell.execute_reply": "2026-08-14T20:59:45.764031Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<h1>Привет, Ада!</h1>\n",
      "Верно: значение из адреса подставилось в шаблон.\n"
     ]
    }
   ],
   "source": [
    "otvet2 = client.get(\"/privet/Ада\")\n",
    "telo2 = otvet2.get_data(as_text=True)\n",
    "\n",
    "print(telo2)\n",
    "assert \"Привет, Ада!\" in telo2\n",
    "print(\"Верно: значение из адреса подставилось в шаблон.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7c65839",
   "metadata": {},
   "source": [
    "## Эксперимент 3 — отправка формы (POST /dobavit)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2c4e61e4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.765668Z",
     "iopub.status.busy": "2026-08-14T20:59:45.765550Z",
     "iopub.status.idle": "2026-08-14T20:59:45.769250Z",
     "shell.execute_reply": "2026-08-14T20:59:45.768761Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Код ответа: 302\n",
      "Задач было: 2 -> стало: 3\n",
      "Верно: POST-запрос добавил новую задачу и вернул редирект.\n"
     ]
    }
   ],
   "source": [
    "kolichestvo_do = len(zadachi)\n",
    "\n",
    "otvet3 = client.post(\"/dobavit\", data={\"zadacha\": \"Прочитать книгу\"})\n",
    "print(\"Код ответа:\", otvet3.status_code)   # 302 — редирект на главную\n",
    "print(\"Задач было:\", kolichestvo_do, \"-> стало:\", len(zadachi))\n",
    "\n",
    "assert otvet3.status_code == 302\n",
    "assert len(zadachi) == kolichestvo_do + 1\n",
    "assert \"Прочитать книгу\" in zadachi\n",
    "print(\"Верно: POST-запрос добавил новую задачу и вернул редирект.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45febc1d",
   "metadata": {},
   "source": [
    "## Задание ★★ Самостоятельная задача\n",
    "\n",
    "Отправьте форму с пустой задачей (`{\"zadacha\": \"   \"}`) и убедитесь, что список задач не изменился — как и в настоящем `app.py`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "e721cf07",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:45.770214Z",
     "iopub.status.busy": "2026-08-14T20:59:45.770091Z",
     "iopub.status.idle": "2026-08-14T20:59:45.773528Z",
     "shell.execute_reply": "2026-08-14T20:59:45.773009Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Верно: пустая (только пробелы) задача не была добавлена.\n"
     ]
    }
   ],
   "source": [
    "kolichestvo_do2 = len(zadachi)\n",
    "client.post(\"/dobavit\", data={\"zadacha\": \"   \"})\n",
    "\n",
    "assert len(zadachi) == kolichestvo_do2\n",
    "print(\"Верно: пустая (только пробелы) задача не была добавлена.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
