Как делать11 октября 2026 г.9 мин чтения
Backend-разработкаШаг 8 из 13

Django: модели, админка и представления

Коротко

Django — Python-фреймворк, в котором ORM, миграции базы, панель администратора, формы с защитой от CSRF, шаблоны и тестовый раннер идут в комплекте. Модель описывается классом Python, makemigrations и migrate превращают её в таблицу, а одна строка @admin.register даёт рабочий интерфейс для редактирования данных. В уроке собран каталог услуг с публичной страницей, формой заявки, JSON-вьюхой и семью тестами на Django 6.1.

Пошаговый урокУровень: НачальныйDjangoПрактика: ~1,5–3 ч

После материала вы сможете

  • создать проект и приложение Django, подключить приложение в INSTALLED_APPS и объяснить разницу между ними
  • описать модели с полями, choices и ForeignKey, создать и применить миграции командами makemigrations и migrate
  • зарегистрировать модели в админке и настроить list_display, list_filter, search_fields и prepopulated_fields
  • отдать страницы через представления и шаблоны, принять форму с валидацией и CSRF и покрыть всё тестами manage.py test

Проверено на

Python 3.12Django 6.1SQLite 3.49

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01«Батарейки в комплекте»: что даёт Django
  2. 02Проект и приложение
  3. 03Модели и миграции
  4. 04Админка: регистрация и настройка списка
  5. 05Формы и валидация
  6. 06URL, представления и шаблоны
  7. 07Тесты: manage.py test
  8. 08Частые ошибки, production и упражнения

«Батарейки в комплекте»: что даёт Django

Общая роль бэкенда — принимать запросы, работать с базой и отдавать ответы — разобрана в материале «Что такое бэкенд». Django решает эту задачу с готовыми компонентами под типичные нужды сайта. Если FastAPI — конструктор для API, где базу, админку и авторизацию подбирают отдельно, то Django предлагает целостный набор, где эти части уже связаны между собой.

Таблицы в базе

Что есть в Django
модели и ORM, миграции
Где в уроке
Service, Inquiry, makemigrations

Редактирование данных

Что есть в Django
админка django.contrib.admin
Где в уроке
ServiceAdmin со списком, фильтрами и поиском

Пользователи и вход

Что есть в Django
django.contrib.auth
Где в уроке
суперпользователь для админки

Формы

Что есть в Django
ModelForm, валидаторы, CSRF-защита
Где в уроке
форма заявки на странице услуги

Страницы

Что есть в Django
URL-маршруты, представления, шаблоны
Где в уроке
список и карточка услуги

Тесты

Что есть в Django
TestCase, тестовый клиент, отдельная тестовая база
Где в уроке
семь тестов через manage.py test

Учебный проект — каталог услуг компании. Менеджер добавляет услуги в админке и решает, какие опубликовать. Посетитель видит список опубликованных услуг, открывает карточку и оставляет заявку на звонок. Заявки попадают в ту же админку. Для фронтенда или мобильного приложения есть JSON-эндпоинт со списком услуг.

Проект и приложение

Окружение создаётся так же, как в любом Python-проекте: виртуальное окружение, затем установка пакета. Документация Django разделяет два понятия. Проект — это настройки и набор приложений конкретного сайта. Приложение — модуль, который делает одну вещь: блог, каталог, опросы. В проекте может быть несколько приложений, а одно приложение можно подключить к разным проектам.

terminal
$ python -m venv .venv
$ source .venv/bin/activate          # Windows PowerShell: .venv\Scripts\Activate.ps1
$ python -m pip install "Django==6.1.2"
$ python -m django --version
6.1.2
$ django-admin startproject config services_site
$ cd services_site
$ python manage.py startapp catalog
Окружение, установка и создание проекта. Вывод версии — реальный

Команда startproject config services_site создаёт папку services_site с файлом manage.py и пакетом настроек config, а startapp catalog — приложение. Имя config для пакета настроек — договорённость, а не требование. Документация предупреждает только, что проект нельзя называть как встроенные модули, например django или test.

  • manage.py — утилита команд проекта
  • config/settings.py — настройки: приложения, база, шаблоны, часовой пояс
  • config/urls.py — корневые маршруты; asgi.py и wsgi.py — точки входа для серверов
  • catalog/models.py, admin.py, views.py, tests.py — модели, админка, представления и тесты приложения
  • catalog/migrations/ — файлы миграций, пока пустая папка с __init__.py

Приложение нужно подключить к проекту: добавить его класс конфигурации из catalog/apps.py в INSTALLED_APPS. Заодно задайте часовой пояс: при включённом USE_TZ (так по умолчанию) Django хранит время в UTC, а шаблоны показывают его в часовом поясе TIME_ZONE.

config/settings.py
INSTALLED_APPS = [
    'catalog.apps.CatalogConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# ...

TIME_ZONE = 'Asia/Almaty'
Фрагмент: что меняем в сгенерированных настройках
  • manage.py — точка входа для команд: миграции, сервер разработки, тесты, создание пользователей.
  • DATABASES по умолчанию указывает на SQLite-файл db.sqlite3 в корне проекта. SQLite входит в Python, ничего ставить не нужно.
  • SECRET_KEY в сгенерированном файле начинается с django-insecure-: это ключ только для разработки. О production-настройках — в конце урока.

Модели и миграции

Модель — класс-наследник django.db.models.Model, каждый атрибут которого — поле в таблице базы. Поле id Django добавляет сам. С версии 6.0 по умолчанию это BigAutoField, 64-битный автоинкремент. Имя таблицы складывается из имени приложения и модели: catalog_service.

catalog/models.py
from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse

phone_validator = RegexValidator(
    regex=r"^\+?\d{10,15}$",
    message="Enter 10–15 digits, optionally starting with +, e.g. +77011234567.",
)


class Service(models.Model):
    class Category(models.TextChoices):
        WEB = "web", "Websites"
        MOBILE = "mobile", "Mobile apps"
        AUTOMATION = "automation", "Automation"

    title = models.CharField(max_length=120)
    slug = models.SlugField(unique=True)
    category = models.CharField(max_length=20, choices=Category.choices)
    summary = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    is_published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["title"]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("catalog:service_detail", args=[self.slug])


class Inquiry(models.Model):
    service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name="inquiries")
    name = models.CharField(max_length=80)
    phone = models.CharField(max_length=16, validators=[phone_validator])
    message = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]
        verbose_name_plural = "inquiries"

    def __str__(self):
        return f"{self.name} — {self.service}"
Две модели: услуга и заявка на неё
  • SlugField(unique=True) — короткий латинский идентификатор для URL (corporate-website), уникальный на уровне базы.
  • TextChoices задаёт допустимые категории: в базе хранится значение (web), в формах и админке показывается подпись (Websites), а в шаблоне её отдаёт метод get_category_display().
  • ForeignKey(..., on_delete=models.CASCADE) связывает заявку с услугой: удалите услугу — удалятся и её заявки. related_name="inquiries" даёт обратный доступ: service.inquiries.all().
  • validators=[phone_validator] — проверка на уровне модели. Её выполнят и ModelForm, и админка.
  • __str__ — строковое представление объекта. Документация подчёркивает, что его используют по всей админке, поэтому определять его стоит всегда.
  • get_absolute_url() возвращает адрес карточки. Его используют шаблоны и redirect().

Миграция — файл, в котором Django записывает изменения моделей. makemigrations создаёт такой файл, sqlmigrate показывает SQL, который он выполнит, а migrate применяет к базе всё, что ещё не применено. Какие миграции уже выполнены, Django хранит в служебной таблице django_migrations.

terminal
$ python manage.py makemigrations catalog
Migrations for 'catalog':
  catalog/migrations/0001_initial.py
    + Create model Service
    + Create model Inquiry

$ python manage.py sqlmigrate catalog 0001
BEGIN;
--
-- Create model Service
--
CREATE TABLE "catalog_service" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(120) NOT NULL, "slug" varchar(50) NOT NULL UNIQUE, "category" varchar(20) NOT NULL, "summary" varchar(255) NOT NULL, "description" text NOT NULL, "is_published" bool NOT NULL, "created_at" datetime NOT NULL);
--
-- Create model Inquiry
--
CREATE TABLE "catalog_inquiry" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(80) NOT NULL, "phone" varchar(16) NOT NULL, "message" text NOT NULL, "created_at" datetime NOT NULL, "service_id" bigint NOT NULL REFERENCES "catalog_service" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "catalog_inquiry_service_id_41ee89bc" ON "catalog_inquiry" ("service_id");
COMMIT;

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, catalog, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  …
  Applying catalog.0001_initial... OK
  Applying sessions.0001_initial... OK
Реальный вывод. Список применённых миграций сокращён на месте «…»: первый migrate создаёт и таблицы встроенных приложений — пользователей, сессий, админки
  1. Изменить модель

    Добавить, переименовать или удалить поле в models.py.

  2. makemigrations

    Django сравнивает модели с последней миграцией и создаёт новый файл в catalog/migrations/.

  3. migrate

    Применить изменения к базе. Та же команда на сервере приведёт production-базу к той же схеме.

Админка: регистрация и настройка списка

Админка Django строит интерфейс редактирования прямо по моделям: формы, списки, поиск, журнал изменений. Достаточно зарегистрировать модель. Класс ModelAdmin настраивает, как её показывать.

catalog/admin.py
from django.contrib import admin

from .models import Inquiry, Service


@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
    list_display = ["title", "category", "is_published", "created_at"]
    list_editable = ["is_published"]
    list_filter = ["category", "is_published"]
    search_fields = ["title", "summary"]
    prepopulated_fields = {"slug": ["title"]}


@admin.register(Inquiry)
class InquiryAdmin(admin.ModelAdmin):
    list_display = ["name", "phone", "service", "created_at"]
    list_filter = ["service"]
    search_fields = ["name", "phone"]
    readonly_fields = ["created_at"]
Регистрация моделей через декоратор и настройка списков

list_display

Что делает
колонки в списке объектов. Без неё — одна колонка со __str__

list_editable

Что делает
поля, которые правятся прямо в списке. Поле должно быть и в list_display

list_filter

Что делает
фильтры в боковой панели: по категории и статусу публикации

search_fields

Что делает
поле поиска над списком: ищет по перечисленным полям

prepopulated_fields

Что делает
заполняет slug из title прямо в браузере. После сохранения slug не меняется — чтобы не ломать URL

readonly_fields

Что делает
поля только для чтения. Они исключаются из формы редактирования
bash
python manage.py createsuperuser
python manage.py runserver
Создайте пользователя (команда спросит логин, email и пароль) и запустите сервер

Откройте http://127.0.0.1:8000/admin/, войдите и добавьте две-три услуги. Одну оставьте неопубликованной — она понадобится для проверки. Обратите внимание: поле slug заполняется само, пока вы набираете название, а статус публикации можно переключать прямо в списке.

Формы и валидация

ModelForm строит форму по модели: типы полей, обязательность, максимальная длина и валидаторы берутся из Inquiry. Список полей задаётся явно через fields. Документация настойчиво рекомендует именно так: fields = "__all__" или exclude легко открывают пользователю поля, которые он не должен менять, особенно когда в модель потом добавят новые.

catalog/forms.py
from django import forms

from .models import Inquiry


class InquiryForm(forms.ModelForm):
    class Meta:
        model = Inquiry
        fields = ["name", "phone", "message"]
Посетитель заполняет только имя, телефон и сообщение. Услугу и время ставит сервер
  1. GET — пустая форма

    Представление создаёт InquiryForm() и передаёт его в шаблон.

  2. POST — связанная форма

    InquiryForm(request.POST) получает данные. is_valid() запускает проверку формы, а затем проверку модели — включая phone_validator.

  3. Ошибка — та же страница

    Если данные неверны, страница рендерится снова: форма уже содержит введённые значения и тексты ошибок у полей.

  4. Успех — сохранение и редирект

    form.save(commit=False) создаёт объект без записи в базу, мы проставляем service и вызываем save(). Затем — редирект: документация советует всегда отвечать редиректом после успешного POST, чтобы кнопка «Назад» не отправила форму повторно.

Для защиты от подделки межсайтовых запросов (CSRF) каждая POST-форма на свои же адреса должна содержать тег {% csrf_token %}. Middleware Django отклоняет POST без корректного токена с кодом 403. Мы проверили это на запущенном сервере: запрос без токена получил 403, а запрос с cookie и токеном со страницы — редирект 302, и заявка появилась в базе.

terminal
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST -d "name=Aigerim&phone=%2B77011234567" http://127.0.0.1:8000/services/corporate-website/
403
Реальная проверка: POST без CSRF-токена отклонён

URL, представления и шаблоны

Запрос проходит путь: корневой config/urls.py → include() передаёт остаток адреса в catalog/urls.py → функция-представление (view) получает request и возвращает ответ. app_name задаёт пространство имён: ссылки строятся по именам вида catalog:service_detail, а не по захардкоженным путям.

config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("services/", include("catalog.urls")),
]
Корневые маршруты: админка и каталог
catalog/urls.py
from django.urls import path

from . import views

app_name = "catalog"

urlpatterns = [
    path("", views.service_list, name="service_list"),
    path("api/services/", views.service_list_json, name="service_list_json"),
    path("<slug:slug>/", views.service_detail, name="service_detail"),
]
<slug:slug> — конвертер пути: захватывает часть URL и передаёт её в представление аргументом slug
catalog/views.py
from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render

from .forms import InquiryForm
from .models import Service


def service_list(request):
    services = Service.objects.filter(is_published=True)
    return render(request, "catalog/service_list.html", {"services": services})


def service_detail(request, slug):
    service = get_object_or_404(Service, slug=slug, is_published=True)
    if request.method == "POST":
        form = InquiryForm(request.POST)
        if form.is_valid():
            inquiry = form.save(commit=False)
            inquiry.service = service
            inquiry.save()
            messages.success(request, "Thank you! We will call you back.")
            return redirect(service)
    else:
        form = InquiryForm()
    return render(request, "catalog/service_detail.html", {"service": service, "form": form})


def service_list_json(request):
    services = Service.objects.filter(is_published=True).values("title", "slug", "category", "summary")
    return JsonResponse({"services": list(services)})
Три представления: список (HTML), карточка с формой (HTML) и список в JSON
  • render() загружает шаблон, подставляет контекст и возвращает HttpResponse.
  • get_object_or_404(Service, slug=slug, is_published=True) отдаёт 404, если услуги нет или она не опубликована. Черновик нельзя открыть, даже зная адрес.
  • redirect(service) вызывает get_absolute_url() модели. messages.success кладёт сообщение, которое шаблон покажет один раз после редиректа.
  • JsonResponse сериализует словарь в JSON с Content-Type: application/json. Список верхнего уровня он по умолчанию не принимает (safe=True), поэтому список услуг обёрнут в ключ services.

Шаблоны лежат в catalog/templates/catalog/. Вложенная папка с именем приложения — пространство имён: Django ищет шаблоны во всех приложениях и берёт первый подходящий, поэтому без неё одноимённые шаблоны разных приложений конфликтуют. Базовый шаблон задаёт каркас страницы, остальные наследуют его через {% extends %} и заполняют блоки.

catalog/templates/catalog/base.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{% block title %}Services{% endblock %}</title>
</head>
<body>
  {% for message in messages %}
    <p role="status">{{ message }}</p>
  {% endfor %}
  {% block content %}{% endblock %}
</body>
</html>
Каркас страницы и вывод одноразовых сообщений
catalog/templates/catalog/service_list.html
{% extends "catalog/base.html" %}

{% block content %}
  <h1>Our services</h1>
  <ul>
    {% for service in services %}
      <li>
        <a href="{{ service.get_absolute_url }}">{{ service.title }}</a>
        — {{ service.get_category_display }}: {{ service.summary }}
      </li>
    {% empty %}
      <li>No services published yet.</li>
    {% endfor %}
  </ul>
{% endblock %}
{% empty %} срабатывает, если список пуст
catalog/templates/catalog/service_detail.html
{% extends "catalog/base.html" %}

{% block title %}{{ service.title }}{% endblock %}

{% block content %}
  <h1>{{ service.title }}</h1>
  <p>{{ service.summary }}</p>
  {{ service.description|linebreaks }}

  <h2>Request a call</h2>
  <form method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button type="submit">Send</button>
  </form>
  <p><a href="{% url 'catalog:service_list' %}">All services</a></p>
{% endblock %}
Карточка услуги и форма заявки с CSRF-токеном
terminal
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
October 11, 2026 - 17:20:59
Django version 6.1.2, using settings 'config.settings'
Starting WSGI development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
Реальный вывод runserver на Windows (на Linux и macOS подсказка — CONTROL-C)
terminal
$ curl -s http://127.0.0.1:8000/services/api/services/ | python -m json.tool
{
    "services": [
        {
            "title": "CRM integration",
            "slug": "crm-integration",
            "category": "automation",
            "summary": "Leads from the site go straight to the CRM"
        },
        {
            "title": "Corporate website",
            "slug": "corporate-website",
            "category": "web",
            "summary": "A multi-page site with a CMS"
        }
    ]
}
JSON-эндпоинт: черновик в ответ не попал, порядок — по ordering модели
  • /services/ показывает только опубликованные услуги с подписью категории
  • Адрес неопубликованной услуги отвечает 404
  • Форма с телефоном «call me» возвращает ошибку у поля, заявка не создаётся
  • После корректной заявки — редирект и сообщение «Thank you!», заявка видна в админке
  • /services/api/services/ отдаёт JSON с тем же набором услуг

Тесты: manage.py test

Тесты Django — классы-наследники django.test.TestCase. Каждый тест выполняется внутри транзакции, которая потом откатывается, поэтому тесты не влияют друг на друга. Для тестов создаётся отдельная пустая база: рабочие данные не затрагиваются. setUpTestData создаёт общие данные один раз на класс, а self.client — тестовый клиент, который отправляет запросы к представлениям без запуска сервера.

catalog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

from .models import Inquiry, Service


class CatalogTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.website = Service.objects.create(
            title="Corporate website",
            slug="corporate-website",
            category=Service.Category.WEB,
            summary="A multi-page site with a CMS",
            is_published=True,
        )
        cls.draft = Service.objects.create(
            title="Secret draft",
            slug="secret-draft",
            category=Service.Category.AUTOMATION,
            summary="Not ready yet",
        )

    def test_str_and_absolute_url(self):
        self.assertEqual(str(self.website), "Corporate website")
        self.assertEqual(self.website.get_absolute_url(), "/services/corporate-website/")

    def test_list_shows_only_published_services(self):
        response = self.client.get(reverse("catalog:service_list"))
        self.assertContains(response, "Corporate website")
        self.assertNotContains(response, "Secret draft")

    def test_draft_detail_returns_404(self):
        response = self.client.get(self.draft.get_absolute_url())
        self.assertEqual(response.status_code, 404)

    def test_valid_inquiry_is_saved_and_redirects(self):
        url = self.website.get_absolute_url()
        response = self.client.post(url, {"name": "Aigerim", "phone": "+77011234567"}, follow=True)
        self.assertRedirects(response, url)
        self.assertContains(response, "Thank you!")
        inquiry = Inquiry.objects.get()
        self.assertEqual(inquiry.service, self.website)

    def test_invalid_phone_shows_error_and_saves_nothing(self):
        response = self.client.post(self.website.get_absolute_url(), {"name": "Aigerim", "phone": "call me"})
        self.assertEqual(response.status_code, 200)
        self.assertFormError(response.context["form"], "phone", "Enter 10–15 digits, optionally starting with +, e.g. +77011234567.")
        self.assertEqual(Inquiry.objects.count(), 0)

    def test_json_endpoint_lists_published_services(self):
        response = self.client.get(reverse("catalog:service_list_json"))
        self.assertEqual(response["Content-Type"], "application/json")
        self.assertEqual(
            response.json(),
            {"services": [{"title": "Corporate website", "slug": "corporate-website", "category": "web", "summary": "A multi-page site with a CMS"}]},
        )

    def test_admin_changelist_opens_for_superuser(self):
        admin_user = get_user_model().objects.create_superuser("admin", "admin@example.com", "test-pass-123")
        self.client.force_login(admin_user)
        response = self.client.get(reverse("admin:catalog_service_changelist"))
        self.assertContains(response, "Corporate website")
Семь тестов: модель, список, 404, форма — успех и ошибка, JSON, админка
terminal
$ python manage.py test
Found 7 test(s).
Creating test database for alias 'default'...

System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.484s

OK
Destroying test database for alias 'default'...
Реальный вывод
  • assertContains и assertNotContains проверяют HTML ответа. Тест списка ловит самую неприятную ошибку каталога — опубликованный черновик.
  • follow=True проходит редирект, и assertRedirects проверяет всю цепочку. Без этого сообщение «Thank you!» было бы прочитано при проверке редиректа и не дошло бы до следующего запроса — на этом мы споткнулись при написании урока.
  • assertFormError(form, "phone", ...) проверяет конкретный текст ошибки у конкретного поля.
  • force_login входит пользователем без проверки пароля — так тест админки не зависит от формы входа.
  • Тестовый клиент по умолчанию отключает CSRF-проверки. Поэтому POST в тестах проходит без токена, а саму защиту мы проверили на запущенном сервере. Для теста CSRF есть Client(enforce_csrf_checks=True).

Частые ошибки, production и упражнения

no such table: catalog_service

Причина
миграции не созданы или не применены
Исправление
makemigrations, затем migrate

makemigrations пишет No changes detected

Причина
приложение не добавлено в INSTALLED_APPS
Исправление
добавить catalog.apps.CatalogConfig

TemplateDoesNotExist: catalog/service_list.html

Причина
шаблон не в catalog/templates/catalog/
Исправление
проверить вложенную папку с именем приложения

NoReverseMatch в шаблоне

Причина
имя маршрута без пространства имён или опечатка
Исправление
{% url 'catalog:service_list' %}, проверить app_name

403 при отправке формы

Причина
в форме нет {% csrf_token %}
Исправление
добавить тег внутрь <form method="post">

Модель в админке не появилась

Причина
нет регистрации в admin.py
Исправление
@admin.register(Model)

Дальше: как ORM превращается в SQL и как безопасно менять схему работающей базы — в уроке об ORM и миграциях. Если нужен только API без страниц и админки, сравните этот урок с FastAPI.

Как проверено3 · пройдено2 · частичносверено 11 октября 2026 г.
  • запущено, работает

    config/settings.py (фрагмент), catalog/models.py, admin.py, forms.py, views.py, urls.py, config/urls.py, 3 шаблона, tests.py

    python -m venv в scratchpad, Django 6.1.2, Python 3.12.10, SQLite 3.49.1 (Windows 11): manage.py check — no issues; makemigrations/sqlmigrate/migrate — вывод в статье реальный (миграции сокращены «…», разделители путей приведены к /); manage.py test — Ran 7 tests, OK. Код вставлен в статью скриптом из файлов проекта

  • запущено, работает

    runserver + curl

    /services/ 200 (только опубликованные), /services/secret-draft/ 404, /admin/ 302 на логин, JSON-эндпоинт 200 application/json; POST без CSRF-токена 403, с cookie и токеном 302 + сообщение «Thank you!», заявка в базе. createsuperuser проверен в режиме --noinput (DJANGO_SUPERUSER_PASSWORD), интерактивный режим не запускался

  • запущено, работает

    упражнения: Service.objects.all(); поле position + миграция 0002; JSON одной услуги

    ex1 — FAILED (failures=1) именно test_list_shows_only_published_services, вывод реальный (сокращён HTML); ex2 — makemigrations создал 0002_alter_service_options_service_position, migrate OK, 8 tests OK; ex3 — 8 tests OK

  • частично

    совместимость с Django 5.2 LTS

    тот же код приложения на Django 5.2.18: manage.py test — 7 tests OK; с настройками, сгенерированными 6.1, makemigrations --check показывает изменение типа id (AutoField по умолчанию в 5.2) и 2 предупреждения W042 — проект, созданный startproject из 5.2, отдельно не собирался

  • частично

    команды установки (shellsession)

    выполнены на Windows эквивалентом (.venv/Scripts/python …); активация source .venv/bin/activate на Linux/macOS не запускалась — синтаксис сверен с документацией venv

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.