CRUD проект

⏱ 40 мин · 🧩 заданий: 6

Что строим

Приложение «Заметки» — классический учебный CRUD-проект:

Операция Адрес View Шаблон
Список /notes/ note_list note_list.html
Просмотр /notes/5/ note_detail note_detail.html
Создание /notes/new/ note_create note_form.html
Редактирование /notes/5/edit/ note_edit note_form.html
Удаление /notes/5/delete/ note_delete note_confirm_delete.html

Шаг 1. Приложение и модель

python manage.py startapp notes

Добавьте "notes" в INSTALLED_APPS. notes/models.py:

from django.db import models
from django.urls import reverse


class Note(models.Model):
    title = models.CharField("Заголовок", max_length=200)
    text = models.TextField("Текст", blank=True)
    is_important = models.BooleanField("Важная", default=False)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-is_important", "-updated"]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("notes:detail", args=[self.pk])
python manage.py makemigrations notes
python manage.py migrate

Шаг 2. Форма

notes/forms.py:

from django import forms

from .models import Note


class NoteForm(forms.ModelForm):
    class Meta:
        model = Note
        fields = ["title", "text", "is_important"]
        widgets = {"text": forms.Textarea(attrs={"rows": 6})}

Шаг 3. Представления

notes/views.py:

from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render

from .forms import NoteForm
from .models import Note


def note_list(request):
    notes = Note.objects.all()
    return render(request, "notes/note_list.html", {"notes": notes})


def note_detail(request, pk):
    note = get_object_or_404(Note, pk=pk)
    return render(request, "notes/note_detail.html", {"note": note})


def note_create(request):
    form = NoteForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        note = form.save()
        messages.success(request, "Заметка создана")
        return redirect(note)
    return render(request, "notes/note_form.html", {"form": form, "title": "Новая заметка"})


def note_edit(request, pk):
    note = get_object_or_404(Note, pk=pk)
    form = NoteForm(request.POST or None, instance=note)
    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, "Изменения сохранены")
        return redirect(note)
    return render(request, "notes/note_form.html", {"form": form, "title": "Редактирование"})


def note_delete(request, pk):
    note = get_object_or_404(Note, pk=pk)
    if request.method == "POST":
        note.delete()
        messages.success(request, "Заметка удалена")
        return redirect("notes:list")
    return render(request, "notes/note_confirm_delete.html", {"note": note})

Приём NoteForm(request.POST or None): на GET request.POST пуст, значит форма создаётся пустой и несвязанной; на POST — с данными. Так обработка укладывается в несколько строк.

Шаг 4. Маршруты

notes/urls.py:

from django.urls import path

from . import views

app_name = "notes"

urlpatterns = [
    path("", views.note_list, name="list"),
    path("new/", views.note_create, name="create"),
    path("<int:pk>/", views.note_detail, name="detail"),
    path("<int:pk>/edit/", views.note_edit, name="edit"),
    path("<int:pk>/delete/", views.note_delete, name="delete"),
]

В config/urls.py: path("notes/", include("notes.urls")).

Шаг 5. Шаблоны

templates/base.html — с выводом сообщений:

<!doctype html>
<html lang="ru">
<head><meta charset="utf-8"><title>{% block title %}Заметки{% endblock %}</title></head>
<body>
  <nav><a href="{% url 'notes:list' %}">Все заметки</a> | <a href="{% url 'notes:create' %}">+ Новая</a></nav>
  {% for message in messages %}<p class="message">{{ message }}</p>{% endfor %}
  {% block content %}{% endblock %}
</body>
</html>

notes/templates/notes/note_list.html:

{% extends "base.html" %}
{% block content %}
  <h1>Заметки</h1>
  <ul>
    {% for note in notes %}
      <li>
        {% if note.is_important %}⭐{% endif %}
        <a href="{{ note.get_absolute_url }}">{{ note.title }}</a>
        <small>{{ note.updated|date:"d.m.Y H:i" }}</small>
      </li>
    {% empty %}
      <li>Заметок пока нет. <a href="{% url 'notes:create' %}">Создать первую</a></li>
    {% endfor %}
  </ul>
{% endblock %}

note_detail.html:

{% extends "base.html" %}
{% block title %}{{ note.title }}{% endblock %}
{% block content %}
  <h1>{{ note.title }}</h1>
  {{ note.text|linebreaks }}
  <a href="{% url 'notes:edit' note.pk %}">Редактировать</a>
  <a href="{% url 'notes:delete' note.pk %}">Удалить</a>
{% endblock %}

note_form.html — один шаблон и для создания, и для редактирования:

{% extends "base.html" %}
{% block content %}
  <h1>{{ title }}</h1>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Сохранить</button>
  </form>
{% endblock %}

note_confirm_delete.html:

{% extends "base.html" %}
{% block content %}
  <h1>Удалить «{{ note.title }}»?</h1>
  <form method="post">
    {% csrf_token %}
    <button type="submit">Да, удалить</button>
    <a href="{{ note.get_absolute_url }}">Отмена</a>
  </form>
{% endblock %}

Удаление — только POST

Ссылка «Удалить» ведёт на страницу подтверждения, а само удаление выполняется отправкой формы. GET-запросы не должны менять данные: ссылку может «нажать» поисковый робот или функция предзагрузки браузера.

Шаг 6. Тесты

class NoteCrudTests(TestCase):
    def test_create(self):
        resp = self.client.post(reverse("notes:create"), {"title": "Купить хлеб", "text": ""})
        note = Note.objects.get()
        self.assertRedirects(resp, note.get_absolute_url())

    def test_delete_requires_post(self):
        note = Note.objects.create(title="X")
        self.client.get(reverse("notes:delete", args=[note.pk]))
        self.assertTrue(Note.objects.filter(pk=note.pk).exists())
        self.client.post(reverse("notes:delete", args=[note.pk]))
        self.assertFalse(Note.objects.filter(pk=note.pk).exists())

Что дальше

Пять почти одинаковых view — повод задуматься. В Django есть представления на основе классов (ListView, DetailView, CreateView, UpdateView, DeleteView), которые делают всё это в пару строк. Мы познакомимся с ними в проекте «Блог».

Итог

  • CRUD = список, просмотр, создание, редактирование, удаление.
  • Одна ModelForm и один шаблон обслуживают и создание, и редактирование (instance=).
  • Удаление — через страницу подтверждения и POST.
  • После успешной операции — messages + redirect.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Расставить по порядку

Сопоставьте операции CRUD с маршрутами: расставьте маршруты в порядке C, R (список), R (детально), U, D.

  1. notes/new/ — создание
  2. notes/<int:pk>/edit/ — редактирование
  3. notes/<int:pk>/ — просмотр
  4. notes/<int:pk>/delete/ — удаление
  5. notes/ — список
Задание 2 Написать код

Напишите view note_delete(request, pk): на GET показывает страницу подтверждения notes/note_confirm_delete.html с переменной note, на POST удаляет заметку и перенаправляет на notes:list.

Задание 3 Один вариант ответа

Почему удаление нельзя делать по обычной ссылке (GET-запросом)?

Задание 4 Написать код

Напишите view note_edit(request, pk) на основе NoteForm (ModelForm): форма заполняется существующей заметкой, при успешной отправке сохраняется и перенаправляет на страницу заметки. Шаблон — notes/note_form.html.

Задание 5 Несколько вариантов

Какие утверждения о паттерне Post/Redirect/Get верны?

Задание 6 Практика (выполнить у себя)

Соберите приложение «Заметки» по шагам урока, создайте, отредактируйте и удалите заметку через интерфейс. Нажмите, когда всё заработает.

Войдите, чтобы отмечать пройденные уроки.