Создание абстрактного класса модели
В модуле system я создам папку models, с __init__.py и файлом abstract.py, где и будет наша наследуемая абстрактная модель с SEO полями.
Структура:
- modules
- blog
- system
- models
- __init__.py
- abstract.py
- migrations
- services
- models
Я добавлю следующее содержимое:
modules/blog/models/abstract.py
from django.db import models
class AbstractBaseMeta(models.Model):
"""
Мета теги для обработки SEO в моделях, чтобы продвигать статьи в поисковике
"""
meta_title = models.CharField(verbose_name='Мета-название', max_length=255, blank=True)
meta_description = models.CharField(verbose_name='Мета-описание', blank=True, max_length=300)
meta_keywords = models.CharField(verbose_name='Ключевые слова', max_length=255, blank=True)
class Meta:
abstract = True
Пояснения:
- Мы добавляем свойство abstract = True, для того, чтобы мы могли наследоваться от этого класса, но не создавать его в базе данных, а создавать лишь для конкретной модели.
Если вы пользуетесь структурой как у меня, то в файл __init__.py добавим импорты:
from modules.system.models.abstract import AbstractBaseMeta
__all__ = (
'AbstractBaseMeta',
)
Давайте теперь попробуем наследоваться в Article и Category от AbstractBaseModel?
modules/blog/models/articles.py
from modules.system.models import AbstractBaseMeta
class Article(AbstractBaseMeta):
title = models.CharField(verbose_name='Заголовок', max_length=255)
slug = models.SlugField(verbose_name='URL', max_length=255, blank=True)
#some fields
#some methods
modules/blog/models/categories.py
from modules.system.models import AbstractBaseMeta
class Category(MPTTModel, AbstractBaseMeta):
title = models.CharField(max_length=255, verbose_name='Название категории')
slug = models.SlugField(max_length=255, verbose_name='URL категории', blank=True)
Пояснения:
- В статьях вместо models.Model мы наследуемся от AbstractBaseMeta, ибо та в свою очередь уже наследуется от models.Model.
- В категориях мы добавляем уже к MPTTModel рядом AbstractBaseMeta, не нарушая целостность MPTT дерева.
Таким образом, абстрактные модели можно использовать не повторяя одни и те же поля для различных моделей.
Давайте создадим и применим миграции:
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> python manage.py makemigrations
Migrations for 'blog':
modules\blog\migrations\0003_article_meta_description_article_meta_keywords_and_more.py
- Add field meta_description to article
- Add field meta_keywords to article
- Add field meta_title to article
- Add field meta_description to category
- Add field meta_keywords to category
- Add field meta_title to category
- Alter field thumbnail on article
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0003_article_meta_description_article_meta_keywords_and_more... OK
Давайте взглянем на структуру таблиц статей и категорий в базе данных:
Статьи:

Категории:

Давайте ещё добавим в метод сохранения автоматическое их заполнение?
Но перед этим, мы добавим функцию получения повторяющихся слов для meta_keywords заполнения, найденную на просторах stackoverflow.
modules/system/services/utils.py
import collections
import io
import re
from django.utils.html import strip_tags
def get_meta_keywords(description):
"""
Генератор meta-keywords из описания статьи
"""
count_min = 3
count_length = 5
collection = collections.Counter()
content = io.StringIO(description)
keywords = []
for line in content.readlines():
collection.update(re.findall(r"[\w']+", strip_tags(line).lower()))
for word, count in collection.most_common():
if len(word) > (count_length - 1) and count > (count_min - 1):
keywords.append(word)
return ', '.join(map(str, keywords))
Примечание: этот код может вызывать ошибки, ибо наше поле ограничено 255 символами, а он может собрать слов очень много. Поэтому, лучше повысьте count_min до 7-8, также и count_length.
А теперь можно и работать с методом сохранения в статьях и категориях
modules/blog/models/articles.py
from django.template.defaultfilters import striptags, truncatewords_html
from modules.system.services.utils import ImageDirectorySave, unique_slugify, get_meta_keywords
class Article(AbstractBaseMeta):
title = models.CharField(verbose_name='Заголовок', max_length=255)
slug = models.SlugField(verbose_name='URL', max_length=255, blank=True)
category = TreeForeignKey('Category', on_delete=models.PROTECT, related_name='articles', verbose_name='Категория')
short_description = models.TextField(max_length=300, verbose_name='Краткое описание')
full_description = models.TextField(verbose_name='Описание')
#some fields
#some methods
def save(self, *args, **kwargs):
"""
Сохранение полей модели при их отсутствии заполнения
"""
if not self.slug:
self.slug = unique_slugify(self, self.title)
if not self.meta_title:
self.meta_title = self.title
if not self.meta_description:
self.meta_description = striptags(truncatewords_html(self.short_description, 300))
if not self.meta_keywords:
self.meta_keywords = get_meta_keywords(self.full_description)
super().save(*args, **kwargs)
Тоже самое для категорий:
modules/blog/models/categories.py
from django.db import models
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
from django.template.defaultfilters import striptags, truncatewords_html
from modules.system.models import AbstractBaseMeta
from modules.system.services.utils import unique_slugify, get_meta_keywords
class Category(MPTTModel, AbstractBaseMeta):
title = models.CharField(max_length=255, verbose_name='Название категории')
slug = models.SlugField(max_length=255, verbose_name='URL категории', blank=True)
description = models.TextField(verbose_name='Описание категории', max_length=300)
parent = TreeForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
db_index=True,
related_name='children',
verbose_name='Родительская категория'
)
class MPTTMeta:
"""
Сортировка по вложенности
"""
order_insertion_by = ('title',)
class Meta:
"""
Сортировка, название модели в админ панели, таблица в данными
"""
unique_together = 'parent', 'slug'
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
db_table = 'app_categories'
def save(self, *args, **kwargs):
"""
Сохранение полей модели при их отсутствии заполнения
"""
if not self.slug:
self.slug = unique_slugify(self, self.title)
if not self.meta_title:
self.meta_title = self.title
if not self.meta_description:
self.meta_description = striptags(truncatewords_html(self.description, 300))
if not self.meta_keywords:
self.meta_keywords = get_meta_keywords(self.description)
super().save(*args, **kwargs)
def __str__(self):
"""
Возвращение заголовка статьи
"""
return self.title
Пояснения:
- Если мы не заполнили мета-описание, то оно автоматически формируется из краткого содержания, обрезаются html элементы, а также содержание будет не более 300 символов.
- Если мы не заполним мета-заголовок, то он автоматически заполнится из названия материала.
- Если мы не заполним мета-ключевые-слова, то они сгенерируются из полного описания через нашу функцию.
А теперь давайте глянем на автоматизацию заполнения!

Отлично, после сохранения у нас все автоматически заполнилось! Но не забывайте, вы сами можете изменять эти поля как хотите. И в дальнейшем я покажу вам в уроках SEO применение данных полей на сайте!