Слаги (slug) в URL-адресах. Метод get_absolute_url()

Курс по Django: https://stepik.org/a/183363

Архив проекта: 22_sitewomen.zip

На этом занятии мы сделаем отображение отдельных статей по их слагу (slug). Если кто не знает, то slug – это уникальный фрагмент URL-адреса, ассоциированный с конкретной записью и, обычно, состоит из набора маленьких латинских букв, цифр, символов подчеркивания и дефиса. Например, статья «Основные методы строк» на сайте https://proproprogs.ru доступна по следующему адресу:

Здесь slug – это последние символы, по которым и выбирается данная страница из БД. Использование слагов – рекомендуемая практика в веб-программировании. Такие страницы лучше ранжируются поисковыми системами и понятнее конечному пользователю.

Давайте вначале сделаем отображение статей по их идентификатору, а затем, заменим адрес на слаг. У нас уже есть функция-заглушка show_post() в файле women/views.py. Мы ее перепишем, следующим образом:

def show_post(request, post_id):
    post = get_object_or_404(Women, pk=post_id)
 
    data = {
        'title': post.title,
        'menu': menu,
        'post': post,
        'cat_selected': 1,
    }
 
    return render(request, 'women/post.html', context=data)

Здесь функция get_object_or_404 выбирает одну запись из таблицы Women, которая имеет идентификатор, равный post_id, либо генерирует исключение 404, если запись не была найдена. Это аналог следующего кода:

try:
    post = Women.objects.get(pk=post_id)
except Women.DoesNotExist:
    raise Http404("Page Not Found")

Но прописывать постоянно такие строчки не очень удобно, поэтому в Django для таких случаев заготовлена специальная функция get_object_or_404().

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

{% extends 'base.html' %}
 
{% block content %}
<h1>{{post.title}}</h1>
 
{% if post.photo %}
<p ><img class="img-article-left" src="{{post.photo.url}}"></p>
{% endif %}
 
{{post.content|linebreaks}}
{% endblock %}

Здесь все достаточно очевидно. Вначале отображаем заголовок h1, затем, фотографию статьи, если она есть, ну и потом уже содержимое самой статьи.

Если теперь перейти по ссылке, то увидим статью, взятую по указанному индексу:

http://127.0.0.1:8000/post/1/

Если же указать неверный адрес, то получим исключение 404. Повторю еще раз, исключения в таком развернутом виде отображаются только в режиме отладки сайта. При эксплуатации с константой DEBUG = False вместо исключения отображается заготовленная страница 404.

Добавление слага

Следующим шагом сделаем отображение статей по их слагу. Но откуда нам его взять? Для этого в модели Women необходимо прописать еще одно поле, которое так и назовем – slug:

class Women(models.Model):
    title = models.CharField(max_length=255, verbose_name="Заголовок")
    slug = models.SlugField(max_length=255, unique=True, db_index=True, verbose_name="URL")
...

Я его определил после поля title, указав уникальным и индексируемым. Однако если сейчас попытаться создать миграцию для внесения этих изменений в структуру таблицы women:

python manage.py makemigrations

то увидим предупреждение, что поле не может быть пустым (так как у нас есть записи в таблице). Чтобы таблица была сформирована, временно пропишу еще два параметра blank=True и default='', а unique=True уберу:

slug = models.SlugField(max_length=255, db_index=True, blank=True, default='')

Снова выполним команду:

python manage.py makemigrations

и видим, что теперь никаких ошибок нет и был создан еще один файл миграций со значением 2.

Применим эти миграции для добавления нового поля в таблицу women:

python manage.py migrate

Видим, что в таблице было успешно создано новое поле slug и размещено в самом конце.

Давайте теперь его заполним уникальными значениями. Для этого я перейду в терминал:

python manage.py shell_plus

(модели здесь импортируются автоматически) и выполню следующие команды:

for w in Women.objects.all():
     w.slug = 'slug-'+str(w.pk)
     w.save()

Теперь у каждой записи свой слаг в виде строки «slug-<идентификатор>».

Вернемся к определению модели. Изменим поле slug следующим образом:

slug = models.SlugField(max_length=255, db_index=True, unique=True)

Создадим миграцию для внесения изменений в таблицу:

python manage.py makemigrations

и применим ее:

python manage.py migrate

Теперь поле slug у нас должно быть уникальным, причем это условие уровня базы данных, то есть СУБД дополнительно будет проверять на уникальность поля slug каждой добавляемой или изменяемой записи.

Отлично, база данных готова и теперь можно сделать отображение статей по слагу. Для этого откроем файл women/urls.py и в списке urlpatterns изменим маршрут для постов на следующий:

path('post/<slug:post_slug>/', views.show_post, name='post'),

Затем, в файле women/views.py немного поменяем функцию представления show_post:

def show_post(request, post_slug):
    post = get_object_or_404(Women, slug=post_slug)
...

Теперь, если перейти по адресу:

http://127.0.0.1:8000/post/slug-1/

то увидим отображение первой статьи по слагу, а не идентификатору статьи, что несколько понятнее для человека. Конечно, на данный момент у нас слаг не очень понятный, поэтому я перейду в таблицу women и у всех статей вручную поменяю слаги на более удобочитаемые:

  • andzhelina-dzholi
  • margo-robbi
  • dzhuliya-roberts
  • ekaterina-guseva

Сохраняем изменения, переходим по адресу:

http://127.0.0.1:8000/post/andzhelina-dzholi/

и видим пост по Анджелине Джоли.

Изменение ссылок в шаблоне. Метод get_absolute_url()

Однако если мы сейчас перейдем на главную страницу сайта, то ссылки кнопки «Читать пост» у нас по-прежнему отображаются с идентификатором. Давайте тоже это поправим и сделаем ссылки со слагами. Для этого в модели Women (в файле women/models.py) будем формировать нужный нам URL-адрес по параметру slug с помощью метода get_absolute_url() следующим образом:

class Women(models.Model):
...
    def get_absolute_url(self):
        return reverse('post', kwargs={'post_slug': self.slug})
...

В шаблоне index.html вызовем этот метод для каждого объекта класса Women:

{% extends 'base.html' %}
 
{% block content %}
<ul class="list-articles">
         {% for p in posts %}
         {% if p.is_published %}
                            <li><h2>{{p.title}}</h2>
         {% autoescape off %}
         {{p.content|linebreaks|truncatewords:50}}
         {% endautoescape %}
                            <div class="clear"></div>
                            <p class="link-read-post"><a href="{{ p.get_absolute_url }}">Читать пост</a></p>
                            </li>
         {% endif %}
         {% endfor %}
</ul>
{% endblock %}

Вспоминаем, что p в цикле for как раз ссылается на объекты класса Women, следовательно, у которого теперь есть атрибут get_absolute_url. И, обратите внимание, при указании этого метода, мы не пишем в конце круглые скобки, т.к. он здесь самостоятельно не вызывается. Вызов сделает функция render при обработке этого шаблона.

Почему мы заменили тег url методом get_absolute_url? Представьте, что в будущем шаблон этой ссылки снова изменился для вывода постов по id. Тогда, при использовании тега url, нам пришлось бы менять эти ссылки в каждом шаблоне, заменяя self.slug на self.pk. В этом как раз неудобство и источник потенциальных ошибок. А благодаря определению нового метода get_absolute_url() нам достаточно изменить маршрут только в нем и это автоматически скажется на всех шаблонах, где используется его вызов.

Второй важный момент функции get_absolute_url() заключается в том, что согласно конвенции, модули Django используют этот метод в своей работе (если он определен в модели). Например, стандартная админ-панель обращается к этому методу для построения ссылок на каждую запись наших моделей. И в дальнейшем мы увидим как это работает.

В свою очередь тег {% url %} имеет смысл применять для построения ссылок не связанных с моделями или, для ссылок без параметров, используя только имена маршрутов.

Но вернемся непосредственно к нашему проекту и в функции представления index() укажем читать все посты из БД, у которых флаг is_published равен 1, и передавать их шаблон:

def index(request):
    posts = Women.objects.filter(is_published=1)
    data = {
        'title': 'Главная страница',
        'menu': menu,
        'posts': posts,
    }
 
    return render(request, 'women/index.html', context=data)

Обновляем главную страницу сайта и видим, что теперь посты доступны по слагу, а не идентификатору. Этот пример показывает как в Django легко и просто можно менять URL-адреса и вместо id использовать другие поля, в частности, слаг. При этом в шаблоне мы использовали метод get_absolute_url() модели Women для формирования корректного URL-адреса. Кроме того, Django автоматически защищает такие адреса от SQL-инъекций, когда злоумышленник пытается выполнить SQL-запрос, прописывая его в адресной строке браузера. Благодаря всем этим мелочам, которые берет на себя фреймворк, даже начинающий веб-мастер может конструировать вполне безопасные сайты с богатым функционалом.

Курс по Django: https://stepik.org/a/183363

Видео по теме