CRUD проект
Что строим
Приложение «Заметки» — классический учебный 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
Сопоставьте операции CRUD с маршрутами: расставьте маршруты в порядке C, R (список), R (детально), U, D.
-
notes/new/— создание -
notes/<int:pk>/edit/— редактирование -
notes/<int:pk>/— просмотр -
notes/<int:pk>/delete/— удаление -
notes/— список
Напишите view note_delete(request, pk): на GET показывает страницу подтверждения
notes/note_confirm_delete.html с переменной note, на POST удаляет заметку и перенаправляет на notes:list.
def note_delete(request, pk):
note = get_object_or_404(Note, pk=pk)
if request.method == "POST":
note.delete()
return redirect("notes:list")
return render(request, "notes/note_confirm_delete.html", {"note": note})
Почему удаление нельзя делать по обычной ссылке (GET-запросом)?
Напишите view note_edit(request, pk) на основе NoteForm (ModelForm):
форма заполняется существующей заметкой, при успешной отправке сохраняется и перенаправляет на страницу заметки.
Шаблон — notes/note_form.html.
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()
return redirect(note)
return render(request, "notes/note_form.html", {"form": form})
Какие утверждения о паттерне Post/Redirect/Get верны?
Соберите приложение «Заметки» по шагам урока, создайте, отредактируйте и удалите заметку через интерфейс. Нажмите, когда всё заработает.
Войдите, чтобы отмечать пройденные уроки.