Облако тегов через связь ManyToMany и принцип DRY в Django

👁 96 просмотров
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (Пока оценок нет)
Загрузка...

Теория

Настало время добавить к постам tags(теги), которые бы сгруппировали наши посты по определенным тематикам. Плюс ко всему сделаем, чтобы все связанные посты могли выходить списком при нажатии на тег и чтобы все теги тоже выходили списком при нажатии на ссылку.

Т.е., для этого нам понадобиться добавить еще одну таблицу в blog/models.py и выполнить еще раз процесс миграции, чтобы изменения локальных моделей обновились в базе данных.

Практика

Файл models.py

Откроем blog/models.py и добавим новую таблицу Tag . Выделенные строки соответствуют местам изменения кода

from django.db import models
from django.shortcuts import reverse

# Create your models here.
class Post(models.Model):
    title = models.CharField(max_length=150, db_index = True)
    slug = models.SlugField(max_length=150, unique = True)
    body = models.TextField(blank=True, db_index = True)
    date_pub = models.DateTimeField(auto_now_add = True)
    tags = models.ManyToManyField('Tag', blank=True, related_name = 'posts')
    
    def get_absolute_url(self):
        return reverse('post_detail_url', kwargs={'slug': self.slug})
    
    def __str__(self):
        return '{}'.format(self.title)

class Tag(models.Model):
    title = models.CharField(max_length=50)
    slug = models.SlugField(max_length=50, unique = True)
    
    def get_absolute_url(self):
        return reverse('tag_detail_url', kwargs = {'slug': self.slug})
    def __str__(self):
        return '{}'.format(self.title)
        

После обновления файла моделей нужно их отразить в  реальной БД, поэтому выполняем команды миграции в консоли

python3 manage.py makemigrations
python3 manage.py migrate

 

После выполнения миграции можно поэксперементировать в консоли. Для этого заходим в shell python, через команду

python3 manage.py shell

И потренеруемся с добавлением и связкой тегов к постам

>>> from blog.models import *
>>> t = Tag.objects.create(title='Django', slug='django')
>>> t
<Tag: Django>
>>> Post.objects.values()
<QuerySet [{'id': 1, 'title': 'New post title 1', 'slug': 'new-post-1', 'body': 'Body text 1 ...', 'date_pub': datetime.datetime(2018, 11, 11, 18, 56, 18, 127958, tzinfo=<UTC>)}, {'id': 2, 'title': 'New post title 2', 'slug': 'new-post-2', 'body': 'Body text 2 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 8, 32, 411534, tzinfo=<UTC>)}, {'id': 3, 'title': 'New post title 3', 'slug': 'new-post-3', 'body': 'Body text 3 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 24, 47, 409418, tzinfo=<UTC>)}, {'id': 4, 'title': 'New post title 4', 'slug': 'new-post-4', 'body': 'Body text 4 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 25, 15, 533403, tzinfo=<UTC>)}, {'id': 5, 'title': 'New post title 5', 'slug': 'new-post-5', 'body': 'Body text 5 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 25, 30, 460405, tzinfo=<UTC>)}, {'id': 6, 'title': 'New post title 6', 'slug': 'new-post-6', 'body': 'Body text 6 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 25, 47, 309093, tzinfo=<UTC>)}, {'id': 7, 'title': 'New post title 7', 'slug': 'new-post-7', 'body': 'Body text 7 ...', 'date_pub': datetime.datetime(2018, 11, 11, 19, 26, 1, 493016, tzinfo=<UTC>)}]>
>>> p = Post.objects.get(slug='new-post-1')
>>> p.title
'New post title 1'
>>> p.title = 'Post width tag'
>>> p.save()
>>> p.tags.add(t)
>>> p.tags.all()
<QuerySet [<Tag: Django>]>

Опишем некоторые команды

  • from blog.models import * — импортируем все модели;
  • t = Tag.objects.create(title=’Django’, slug=’django’) — создаем новый эксземпляр(для таблицы это будет строка) сущности
  • Post.objects.values() — выводим все значения в модели Post;
  • p = Post.objects.get(slug=’new-post-1′) — ищем посты по соответствию;
  • p.title — выводим заголовок поста;
  • p.title = ‘Post width tag’ — задаем новый заголовок для поста;
  • p.save() — сохраняем экземпляр с новым изменениями и перезаписываем в БД, как строку;
  • p.tags.add(t) — добавляем новый экземпляр тега к посту;
  • p.tags.all() — выводим все теги экземпляра поста.

 

Файл urls.py

Открываем файл blog/urls.py и задаем 2 дополнительных маршрутизации для вывода постов по тегу и списка всех тегов и плюс к этому добавим к ним обработчики для видов

from django.urls import path
from .views import *

urlpatterns = [
    path('', posts_list, name='posts_list_url'),
    path('post/<str:slug>', post_detail, name='post_detail_url'),
    path('tags/', tags_list, name='tags_list_url'),
    path('tag/<str:slug>', tag_detail, name='tag_detail_url'),
]

В данном случае, мы добавили 2 шаблона URL

  • tags — выведет все существующие теги в базе;
  • tag/<str:slug> —  выведет все посты, прикрепленные к этому тегу.

 

Файл views.py

Открываем файл blog/views.py и пишем обработчики для шаблонов ссылок маршрутизации, которые выведут в шаблоны вида список тегов и список связанных постов

from django.shortcuts import render
from django.http import HttpResponse
from .models import Post, Tag
# Create your views here.
def posts_list(request):
    posts = Post.objects.all()
    return render(request, 'blog/index.html', context = {'posts' : posts})
    #return HttpResponse("<h1>Hello! Hello!</h1>")
    
def post_detail(request, slug):
    post = Post.objects.get(slug__iexact = slug)
    return render(request, 'blog/post_detail.html', context = {'post' : post})

def tags_list(request):
    tags = Tag.objects.all()
    return render(request, 'blog/tags_list.html', context={'tags': tags})

def tag_detail(request, slug):
    tag = Tag.objects.get(slug__iexact = slug)
    return render(request, 'blog/tag_detail.html', context={'tag': tag})
    

Как мы видим, нам потребуется создать еще 2 шаблона вида

  • tags_list.html — шаблон страницы списка всех тегов;
  • tag_detail.html — шаблон страницы всех постов с тем или иным конкретным тегом.

 

Принцип DRY в шаблонах Django

Как было в предыдущей части упомянуто, нам нужно создать еще 2 шаблона вида: tags_list.html и tag_detail.html.

Тут важно учитывать, что вывод постов по ссылке mysite.com/blog и по ссылке mysite.com/blog/tag/tagname идентичны и нам нужно для этого создать карточку для поста, чтобы не копипастить один и тот же код и для этого в шаблонах Django есть возможность подключать куски кода html друг в друга и это соответствует принципу DRY.

Don’t repeat yourselfDRY (рус. не повторяйся) — это принцип разработки программного обеспечения, нацеленный на снижение повторения информации различного рода, особенно в системах со множеством слоёв абстрагирования. Принцип DRY формулируется как: «Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы».

Т.е., помимо шаблонов tags_list.html и tag_detail.html мы еще создадим папку blog/templates/blog/includes, в которой будет карточка поста post_card.html, который будет подключен к файлу blog/templates/blog/index.html и к файлу blog/templates/blog/tag_detail.html

Файл blog/templates/blog/index.html

{% extends 'blog/base_blog.html'%}
{% block title %}
	Site Blog
{% endblock %}
{% block content %}
	<div>
		<div class="grid-x">
		    {% for post in posts %}
				{% include 'blog/includes/post_card.html'%}
			{% endfor %}
		</div>
	</div>
{% endblock %}

Файл blog/templates/blog/tags_list.html

{% extends 'blog/base_blog.html'%}
{% block title %}
	Site Blog - Tags list
{% endblock %}
{% block content %}
	<div>
		<div class="grid-x">
		    {% for tag in tags %}
			    <div class="cell small-12 medium-3 large-3">
				      <div class="card">
					        <div class="card-section">
					        	<h4>
						        	<a href="{{ tag.get_absolute_url }}" class="button">
						        		{{ tag.title }}
						        	</a>
					        	</h4>
					        </div>
				      </div>
			    </div>
			{% endfor %}
		</div>
	</div>
{% endblock %}

Файл blog/templates/blog/tag_detail.html

{% extends 'blog/base_blog.html'%}
{% block title %}
	Site Blog - Tag detail
{% endblock %}
{% block content %}
	<div>
		<h3>Posts with tag <b>{{tag.title|title}}</b>:</h3>
		<br/>
		<div class="grid-x">
		    {% for post in tag.posts.all %}
				{% include 'blog/includes/post_card.html'%}
			{% endfor %}
		</div>
	</div>
{% endblock %}

Файл blog/templates/blog/includes/post_card.html

 <div class="cell small-12 medium-3 large-3">
    <div class="card">
       <img src="https://foundation.zurb.com/sites/docs/assets/img/generic/rectangle-1.jpg">
       <div class="card-section">
       	<h4>{{ post.title }}</h4>
         	<p>{{ post.body | truncatewords:15 }}</p>
		<!-- <a href="{% url 'post_detail_url' slug=post.slug %}" class="button">Learn More</a> -->
		<a href="{{ post.get_absolute_url }}" class="button">Learn More</a>
		<h6>{{ post.date_pub }}</h6>
       </div>
       <div>
           Tags: 
	    {% for tag in post.tags.all %}
        	<a href="{{ tag.get_absolute_url }}">
        		{{ tag.title }}
        	</a>
        {% empty %}
            none
		{% endfor %}
       </div>
    </div>
 </div>

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

В итоге карточки постов должны получиться, как на скриншотах внизу

Карточки постов с тегом и без тега
Карточки постов с тегом и без тега

Список тегов должно получиться, как внизу

Страница списка тегов. Пока один тег
Страница списка тегов (пока в базе один тег)

Список постов по определенному тегу

Список постов по определенному тегу
Список постов по определенному тегу