Руководство по слагам Django

В этом руководстве мы добавим слаги на сайт Django. Как отмечается в официальной документации : "Slug - это газетный термин. Slug - это короткое обозначение чего-либо, содержащее только буквы, цифры, подчеркивания или дефисы. Обычно они используются в URL-адресах."

Чтобы привести конкретный пример, предположим, что у вас есть сайт газеты (такой, какой мы создадим в этом учебнике). Для статьи под названием "Hello World", URL будет example.com/hello-world при условии, что сайт называется example.com.

Несмотря на свою повсеместную распространенность, слаги могут быть несколько сложными для реализации с первого раза, по крайней мере, по моему опыту. Поэтому мы будем реализовывать все с нуля, чтобы вы могли увидеть, как все части подходят друг другу. Если вам уже удобно реализовывать ListView и DetailView, вы можете сразу перейти к разделу Slug.

Полный исходный код можно найти на Github.

Установить

Для начала давайте перейдем в каталог для нашего кода. Она может быть размещена в любом месте вашего компьютера, но если вы работаете на Mac, то легко найти ее в каталоге Desktop.

$ cd ~/Desktop
$ mkdir newspaper && cd newspaper

Чтобы отдать дань уважения Django, который зародился в газете, мы создадим базовый Newspaper веб-сайт с помощью Articles. Если вам нужна помощь в установке Python, Django и всего остального ( смотрите здесь подробные инструкции).

В командной строке введите следующие команды для установки последней версии с помощью Pipenv, создайте проект под названием config, настройте начальную базу данных с помощью migrate, а затем запустите локальный веб-сервер с помощью runserver.

$ pipenv install django==3.0
$ pipenv shell
(newspaper) $ django-admin startproject config .
(newspaper) $ python manage.py migrate
(newspaper) $ python manage.py runserver

Не забудьте поставить точку . в конце команды startproject! Это необязательный шаг, который позволит избежать создания Django дополнительной директории в противном случае.

Перейдите по адресу http://127.0.0.1:8000/ в вашем веб-браузере, чтобы увидеть страницу приветствия Django, которая подтверждает, что все настроено правильно.

Django welcome page

Приложение для статей

Поскольку основное внимание в этом учебнике уделяется слагам, я просто дам команды и код для подключения этого приложения Articles. Полные объяснения можно найти в моей книге Django для начинающих!

Для начала создадим приложение под названием articles. Остановите локальный сервер командой Control+c и используйте команду startapp для создания этого нового приложения.

(newspaper) $ python manage.py startapp articles

Затем обновите INSTALLED_APPS внутри нашего config/settings.py файла, чтобы уведомить Django о приложении.

# config/settings.py
INSTALLED_APPS = [
    ...
    'articles.apps.ArticlesConfig', # new
]

Модель статьи

Создайте модель базы данных, которая будет иметь title и body. Мы также установим __str__ и get_absolute_url, что является лучшей практикой Django.

# articles/models.py
from django.db import models
from django.urls import reverse

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', args=[str(self.id)])

Теперь создайте файл миграций для этого изменения, затем добавьте его в нашу базу данных через migrate.

(newspaper) $ python manage.py makemigrations articles
(newspaper) $ python manage.py migrate

Django Admin

Админка Django - это удобный способ играть с моделями, поэтому мы будем использовать ее. Но сначала создайте учетную запись суперпользователя.

(newspaper) $ python manage.py createsuperuser

А затем обновите articles/admin.py для отображения нашего приложения в админке.

# articles/admin.py
from django.contrib import admin

from .models import Article

class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title', 'body',)

admin.site.register(Article, ArticleAdmin)

Снова запустите сервер с помощью python manage.py runserver и перейдите в админку по адресу http://127.0.0.1:8000/admin. Войдите в систему под учетной записью суперпользователя.

Admin Homepage

Нажмите на "+ Добавить" рядом с разделом Articles и добавьте запись.

Admin Article

URLs

В дополнение к модели нам в конечном итоге понадобятся URL, представление и шаблон для отображения страницы статьи. Мне нравится переходить к URL следующим после моделей, хотя порядок не имеет значения: нам нужны все четыре, прежде чем мы сможем отобразить одну страницу. Первым шагом будет добавление приложения articles в наш файл config/urls.py на уровне проекта.

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('articles.urls')), # new
]

Далее мы должны создать файл уровня приложения urls.py.

$ touch articles/urls.py

У нас будет ListView для списка всех статей и DetailView для отдельных статей.

Обратите внимание, что мы ссылаемся на два представления, которые еще не созданы: ArticleListView и ArticleDetailView. Мы добавим их в следующем разделе.

# articles/urls.py
from django.urls import path

from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    path('<int:pk>', ArticleDetailView.as_view(), name='article_detail'),
    path('', ArticleListView.as_view(), name='article_list'),
]

Виды

Для каждого представления мы указываем связанную модель и соответствующий еще не созданный шаблон.

# articles/views.py
from django.views.generic import ListView, DetailView

from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'article_list.html'


class ArticleDetailView(DetailView):
    model = Article
    template_name = 'article_detail.html'

Шаблоны

Наконец, мы подошли к последнему шагу: шаблоны. По умолчанию Django будет искать внутри каждого приложения каталог templates. В нашем случае это будет структура articles/templates/template.html.

Введите Control+c в командной строке и создайте новый templates каталог.

(newspaper) $ mkdir articles/templates

Теперь добавьте два новых шаблона.

(newspaper) $ touch articles/templates/article_list.html
(newspaper) $ touch articles/templates/article_detail.html

Для нашей страницы списка мы переходим по циклу object_list, который обеспечивается ListView. И мы добавляем a href с помощью метода get_absolute_url, добавленного к модели.

<!-- article_list.html -->
<h1>Articles</h1>
{% for article in object_list %}
  <ul>
    <li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></li>
  </ul>
{% endfor %}

Представление деталей выводит наши два поля - title и body - используя object по умолчанию, предоставляемое DetailView. Вы можете и, вероятно, должны переименовать оба поля object_list в ListView и object в DetailView, чтобы они были более описательными.

<!-- article_detail.html -->
<div>
  <h2>{{ object.title }</h2>
  <p>{{ object.body }}</p>
</div>

Убедитесь, что сервер работает--python manage.py runserver-- и просмотрите обе наши страницы в своем веб-браузере.

Список всех статей доступен на сайте http://127.0.0.1:8000/.

ListView

А детальный вид для нашей отдельной статьи находится по адресу http://127.0.0.1:8000/1.

DetailView

Слаги

Наконец, мы переходим к слагам. В конечном итоге мы хотим, чтобы название нашей статьи было отражено в URL. Другими словами, "День из жизни" должен иметь URL http://127.0.0.1:8000/a-day-in-the-life.

Необходимо выполнить всего два шага: обновить наш файл articles/models.py и articles/urls.py. Готовы? Поехали...

В нашей модели мы можем добавить встроенное в Django SlugField. Но мы должны также - и это та часть, которая обычно ставит людей в тупик - обновить get_absolute_url также. Именно здесь мы передаем значение, используемое в нашем URL. В настоящее время он передает id для статьи args, так что 1 для нашей первой статьи. Нам нужно изменить это на аргумент ключевого слова, kwargs, для нашей slug.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField() # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', kwargs={'slug': self.slug}) # new

Далее давайте добавим файл миграции, поскольку мы обновили нашу модель.

(newspaper) $ python manage.py makemigrations articles
You are trying to add a non-nullable field 'slug' to article without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

Ак! Что это?! Оказывается, у нас уже есть данные в нашей базе данных, наша единственная Статья, поэтому мы не можем просто так взять и добавить новое поле. Django услужливо подсказывает нам, что мы должны либо сделать одноразовое поле по умолчанию null, либо добавить его самостоятельно. Хммм.

По этой самой причине, как правило, хорошим советом является всегда добавлять новые поля либо с null=True, либо со значением default.

Давайте пока воспользуемся простым подходом и установим null=True. Итак, введите 2 в командной строке. Затем добавьте это в наше поле slug.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=True) # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', kwargs={'slug': self.slug})

Попробуйте создать файл миграций снова, и он будет работать.

(newspaper) $ python manage.py makemigrations articles

Продолжайте и migrate базу данных, чтобы применить изменения.

(newspaper) $ python manage.py migrate

Но если подумать, то мы создали значение null для нашего slug. Нам нужно зайти в админку, чтобы установить его правильно. Запустите локальный сервер, python manage.py runserver, и перейдите на страницу Article в админке. Поле Slug будет пустым.

Empty Slug

Вручную добавьте нужное значение a-day-in-the-life и нажмите "Сохранить"

Add Slug

Ок, последний шаг - обновить articles/urls.py так, чтобы отображать аргумент ключевого слова slug в самом URL. К счастью, это означает лишь замену <int:pk> на <slug:slug>.

# articles/urls.py
from django.urls import path

from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    path('<slug:slug>', ArticleDetailView.as_view(), name='article_detail'), # new
    path('', ArticleListView.as_view(), name='article_list'),
]

И мы закончили! Перейдите на страницу просмотра списка по адресу http://127.0.0.1:8000/ и нажмите на ссылку для нашей статьи.

Slug URL

И вот он с нашим slug в качестве URL! Прекрасно.

Unique и Null

Далее, действительно ли мы хотим разрешить значение null для slug? Скорее всего, нет, так как это сломает наш сайт! Еще одно соображение: что произойдет, если будут одинаковые slug? Как это будет разрешено? Короткий ответ: не очень хорошо.

Поэтому давайте изменим наше поле slug так, чтобы null не допускалось, а уникальные значения были обязательны.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=False, unique=True) # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', kwargs={'slug': self.slug})

Сделайте миграцию/переход. Нужна пустота для прошлой записи.

(newspaper) $ python manage.py makemigrations articles
You are trying to change the nullable field 'slug' on article to non-nullable without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in models.py
Select an option:

Выбираем 2, поскольку мы можем сами вручную обработать существующий ряд, и фактически уже сделали это. Затем migrate в базу данных.

(newspaper) $ python manage.py migrate

Предварительно заполненные поля

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

Обновите articles/admin.py следующим образом:

# articles/admin.py
from django.contrib import admin

from .models import Article

class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title', 'body',)
    prepopulated_fields = {'slug': ('title',)} # new

admin.site.register(Article, ArticleAdmin)

Теперь перейдите в админку и добавьте новую статью. Вы заметите, что по мере ввода текста в поле Title поле Slug автоматически заполняется. Довольно аккуратно!

Сигналы, хуки жизненного цикла, сохранение и формы/сериализаторы

В реальном мире маловероятно просто предоставить пользователю доступ администратора. Можно, но в больших масштабах это определенно не лучшая идея. И даже в небольших масштабах большинство нетехнических пользователей найдут веб-интерфейс более привлекательным

Итак... как автоматически заполнить поле slug при создании новой статьи. Оказывается, в Django для этого есть встроенный инструмент под названием slugify!

Но как использовать slugify? На практике часто можно увидеть, как это делается с помощью Signal. Но я бы возразил - как и коллега по Django Карлтон Гибсон - что это не лучшее использование сигналов, потому что здесь мы знаем и отправителя, и получателя. Здесь нет никакой тайны. Мы подробно обсуждаем правильное использование сигналов в нашем эпизоде Django Chat Podcast, посвященном этой теме.

Альтернативой сигналам является использование перехватчиков жизненного цикла с помощью чего-то вроде пакета django-lifecycle. Lifecycle hooks - это альтернатива сигналам, которая обеспечивает схожую функциональность с меньшей непрямолинейностью.

Другим распространенным способом реализации этого является переопределение метода Article модели save. Это тоже "работает", но не является лучшим решением. Вот один из способов сделать это

# articles/models.py
from django.db import models
from django.template.defaultfilters import slugify # new
from django.urls import reverse

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=False, unique=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs): # new
        if not self.slug:
            self.slug = slugify(self.title)
        return super().save(*args, **kwargs)

Лучшим решением, на мой взгляд, является создание slug в самой форме. Это можно сделать, переопределив метод clean формы так, чтобы cleaned_data содержал slug, или можно использовать JavaScript для автоматического заполнения поля, как это сделано в админке Django. Если вы используете API, тот же подход можно применить к сериализатору.

Это решение не полагается на пользовательский сигнал и обрабатывает slug до того, как он коснется базы данных. По словам Карлтона Гибсона, который предложил мне этот подход, win-win-win.

Слова предостережения

Небольшое предостережение по поводу использования slugs на большом сайте. Несмотря на то, что статья должна быть unique, при использовании прорезей почти неизбежны конфликты имен. Однако слаги, похоже, обладают хорошими SEO-свойствами. Хорошим компромиссом является сочетание slug с UUID или именем пользователя. Таким образом, конечный URL будет {{ uuid }}/{{ slug }} или {{ username }}/{{ slug }}. Если вы посмотрите на Github, например, исходный код этого руководства находится по адресу https://github.com/wsvincent/django-slug-tutorial, что является шаблоном имя пользователя + slug. Хотя вы могли бы также использовать id + slug, использование идентификаторов часто является проблемой безопасности и, по моему мнению, его следует избегать для производственных веб-сайтов.

Следующие шаги

Если вы хотите сравнить свой код с официальным исходным кодом, его можно найти на Github.

Вернуться на верх