add: приложение blog (для страниц и постов)
This commit is contained in:
0
etpgrf_site/blog/__init__.py
Normal file
0
etpgrf_site/blog/__init__.py
Normal file
23
etpgrf_site/blog/admin.py
Normal file
23
etpgrf_site/blog/admin.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.contrib import admin
|
||||
from .models import Post
|
||||
|
||||
@admin.register(Post)
|
||||
class PostAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'post_type', 'is_published', 'published_at')
|
||||
list_filter = ('post_type', 'is_published', 'published_at')
|
||||
search_fields = ('title', 'content', 'slug')
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
date_hierarchy = 'published_at'
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('title', 'slug', 'post_type', 'is_published', 'published_at')
|
||||
}),
|
||||
('Контент', {
|
||||
'fields': ('image', 'excerpt', 'content')
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('seo_title', 'seo_description', 'seo_keywords'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
6
etpgrf_site/blog/apps.py
Normal file
6
etpgrf_site/blog/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
verbose_name = 'Блог и Страницы'
|
||||
37
etpgrf_site/blog/migrations/0001_initial.py
Normal file
37
etpgrf_site/blog/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 6.0.1 on 2026-01-25 09:04
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(help_text='Основной заголовок (H1). Обязательно для заполнения.', max_length=255, verbose_name='Заголовок')),
|
||||
('slug', models.SlugField(help_text='Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post', max_length=255, unique=True, verbose_name='URL (slug)')),
|
||||
('post_type', models.CharField(choices=[('B', 'Пост в блог'), ('P', 'Страница')], default='B', help_text='Страница доступна по адресу /slug/, Пост — по адресу /blog/slug/', max_length=1, verbose_name='Тип публикации')),
|
||||
('is_published', models.BooleanField(default=True, help_text='Снимите галочку, чтобы скрыть публикацию (черновик).', verbose_name='Опубликовано')),
|
||||
('published_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Дата, которая будет отображаться в блоге. Можно запланировать на будущее.', verbose_name='Дата публикации')),
|
||||
('content', models.TextField(help_text='Основной текст публикации. Поддерживает HTML.', verbose_name='Контент')),
|
||||
('excerpt', models.TextField(blank=True, help_text='Отображается в списке постов. Если оставить пустым, будет взято начало контента.', verbose_name='Краткое описание (тизер)')),
|
||||
('image', models.ImageField(blank=True, help_text='Изображение для превью в ленте и Open Graph (соцсети).', null=True, upload_to='blog/', verbose_name='Обложка')),
|
||||
('seo_title', models.CharField(blank=True, help_text='Заголовок для поисковиков (<title>). Если пусто, используется основной заголовок.', max_length=255, verbose_name='SEO Title')),
|
||||
('seo_description', models.TextField(blank=True, help_text='Описание для поисковиков (meta description). Рекомендуется 150-160 символов.', verbose_name='SEO Description')),
|
||||
('seo_keywords', models.CharField(blank=True, help_text='Ключевые слова через запятую (meta keywords). Сейчас почти не используется поисковиками.', max_length=255, verbose_name='SEO Keywords')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Публикация',
|
||||
'verbose_name_plural': 'Публикации',
|
||||
'ordering': ['-published_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
etpgrf_site/blog/migrations/__init__.py
Normal file
0
etpgrf_site/blog/migrations/__init__.py
Normal file
93
etpgrf_site/blog/models.py
Normal file
93
etpgrf_site/blog/models.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
|
||||
class PostType(models.TextChoices):
|
||||
BLOG = 'B', 'Пост в блог'
|
||||
PAGE = 'P', 'Страница'
|
||||
|
||||
class Post(models.Model):
|
||||
"""
|
||||
Модель для постов блога и статических страниц.
|
||||
"""
|
||||
title = models.CharField(
|
||||
verbose_name="Заголовок",
|
||||
max_length=255,
|
||||
help_text="Основной заголовок (H1). Обязательно для заполнения."
|
||||
)
|
||||
slug = models.SlugField(
|
||||
verbose_name="URL (slug)",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post"
|
||||
)
|
||||
|
||||
post_type = models.CharField(
|
||||
verbose_name="Тип публикации",
|
||||
max_length=1,
|
||||
choices=PostType.choices,
|
||||
default=PostType.BLOG,
|
||||
help_text="Страница доступна по адресу /slug/, Пост — по адресу /blog/slug/"
|
||||
)
|
||||
|
||||
is_published = models.BooleanField(
|
||||
verbose_name="Опубликовано",
|
||||
default=True,
|
||||
help_text="Снимите галочку, чтобы скрыть публикацию (черновик)."
|
||||
)
|
||||
published_at = models.DateTimeField(
|
||||
verbose_name="Дата публикации",
|
||||
default=timezone.now,
|
||||
help_text="Дата, которая будет отображаться в блоге. Можно запланировать на будущее."
|
||||
)
|
||||
|
||||
content = models.TextField(
|
||||
verbose_name="Контент",
|
||||
help_text="Основной текст публикации. Поддерживает HTML."
|
||||
)
|
||||
excerpt = models.TextField(
|
||||
verbose_name="Краткое описание (тизер)",
|
||||
blank=True,
|
||||
help_text="Отображается в списке постов. Если оставить пустым, будет взято начало контента."
|
||||
)
|
||||
|
||||
image = models.ImageField(
|
||||
verbose_name="Обложка",
|
||||
upload_to='blog/',
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Изображение для превью в ленте и Open Graph (соцсети)."
|
||||
)
|
||||
|
||||
# SEO
|
||||
seo_title = models.CharField(
|
||||
verbose_name="SEO Title",
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Заголовок для поисковиков (<tt><title></tt>). Если пусто, используется основной заголовок."
|
||||
)
|
||||
seo_description = models.TextField(
|
||||
verbose_name="SEO Description",
|
||||
blank=True,
|
||||
help_text="Описание для поисковиков (meta description). Рекомендуется 150-160 символов."
|
||||
)
|
||||
seo_keywords = models.CharField(
|
||||
verbose_name="SEO Keywords",
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Ключевые слова через запятую (meta keywords). Сейчас почти не используется поисковиками,"
|
||||
"но может пригодиться."
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Публикация"
|
||||
verbose_name_plural = "Публикации"
|
||||
ordering = ['-published_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.post_type == PostType.PAGE:
|
||||
return reverse('page_detail', kwargs={'slug': self.slug})
|
||||
return reverse('post_detail', kwargs={'slug': self.slug})
|
||||
Reference in New Issue
Block a user