Добавлена система проектов с автоматическим ресайзом изображений и адаптивным дизайном
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
- Удалена старая система портфолио (PortfolioCategory, PortfolioItem) - Расширена модель Project: slug, categories (M2M), thumbnail, media files, meta fields - Объединены категории: ProjectCategory удалена, используется общая Category - Автоматический ресайз thumbnail до 600x400px с умным кропом по центру - Создан /projects/ - страница списка проектов с фильтрацией по категориям - Создан /project/<pk>/ - детальная страница проекта с галереей Swiper - Адаптивный дизайн: 3 карточки в ряд (десктоп), 2 (планшет), 1 (мобильный) - Параллакс-эффект на изображениях при наведении - Lazy loading для оптимизации загрузки - Фильтры категорий в виде пилюль как на странице услуг - Компактные карточки с фиксированной шириной - Кликабельные проекты в service_detail с отображением всех медиа
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-25 23:21
|
||||
|
||||
import ckeditor_uploader.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0013_career_team'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PortfolioCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('icon', models.CharField(blank=True, help_text='Класс иконки (например: fa-code)', max_length=50, verbose_name='Иконка')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Категория портфолио',
|
||||
'verbose_name_plural': 'Категории портфолио',
|
||||
'ordering': ['order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpost',
|
||||
name='content',
|
||||
field=ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NewsPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='Заголовок')),
|
||||
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
|
||||
('excerpt', models.TextField(max_length=300, verbose_name='Краткое описание')),
|
||||
('content', ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Содержание')),
|
||||
('featured_image', models.ImageField(upload_to='news/', verbose_name='Главное изображение')),
|
||||
('tags', models.CharField(blank=True, help_text='Разделите запятыми', max_length=200, verbose_name='Теги')),
|
||||
('is_published', models.BooleanField(default=False, verbose_name='Опубликовано')),
|
||||
('is_featured', models.BooleanField(default=False, verbose_name='Избранная новость')),
|
||||
('views_count', models.PositiveIntegerField(default=0, verbose_name='Просмотры')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата публикации')),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='web.category', verbose_name='Категория')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Новость',
|
||||
'verbose_name_plural': 'Новости',
|
||||
'ordering': ['-published_at', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PortfolioItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='Название проекта')),
|
||||
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL')),
|
||||
('short_description', models.TextField(max_length=300, verbose_name='Краткое описание')),
|
||||
('description', ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Полное описание')),
|
||||
('thumbnail', models.ImageField(upload_to='portfolio/thumbnails/', verbose_name='Превью изображение')),
|
||||
('client', models.CharField(blank=True, max_length=200, verbose_name='Клиент')),
|
||||
('project_url', models.URLField(blank=True, verbose_name='Ссылка на проект')),
|
||||
('github_url', models.URLField(blank=True, verbose_name='GitHub репозиторий')),
|
||||
('technologies', models.TextField(help_text='Разделите запятыми', verbose_name='Технологии')),
|
||||
('duration', models.CharField(blank=True, max_length=100, verbose_name='Длительность')),
|
||||
('team_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер команды')),
|
||||
('status', models.CharField(choices=[('draft', 'Черновик'), ('published', 'Опубликовано'), ('featured', 'Избранное')], default='draft', max_length=20, verbose_name='Статус')),
|
||||
('completion_date', models.DateField(blank=True, null=True, verbose_name='Дата завершения')),
|
||||
('meta_title', models.CharField(blank=True, max_length=200, verbose_name='SEO заголовок')),
|
||||
('meta_description', models.TextField(blank=True, max_length=300, verbose_name='SEO описание')),
|
||||
('views_count', models.PositiveIntegerField(default=0, verbose_name='Просмотры')),
|
||||
('likes_count', models.PositiveIntegerField(default=0, verbose_name='Лайки')),
|
||||
('is_featured', models.BooleanField(default=False, verbose_name='Избранный проект')),
|
||||
('display_order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('published_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата публикации')),
|
||||
('categories', models.ManyToManyField(related_name='portfolio_items', to='web.portfoliocategory', verbose_name='Категории')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Проект портфолио',
|
||||
'verbose_name_plural': 'Портфолио',
|
||||
'ordering': ['-is_featured', '-display_order', '-published_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PortfolioMedia',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('media_type', models.CharField(choices=[('image', 'Изображение'), ('video', 'Видео'), ('embed', 'Встроенное видео (YouTube, Vimeo)')], default='image', max_length=10, verbose_name='Тип медиа')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='portfolio/gallery/', verbose_name='Изображение')),
|
||||
('video', models.FileField(blank=True, null=True, upload_to='portfolio/videos/', verbose_name='Видео файл')),
|
||||
('video_poster', models.ImageField(blank=True, null=True, upload_to='portfolio/posters/', verbose_name='Превью видео')),
|
||||
('embed_url', models.URLField(blank=True, verbose_name='URL видео (YouTube, Vimeo)')),
|
||||
('caption', models.CharField(blank=True, max_length=200, verbose_name='Подпись')),
|
||||
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alt текст')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
||||
('portfolio_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_files', to='web.portfolioitem', verbose_name='Проект')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Медиа файл портфолио',
|
||||
'verbose_name_plural': 'Медиа файлы портфолио',
|
||||
'ordering': ['order', 'uploaded_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,217 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-26 00:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import tinymce.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0014_portfoliocategory_alter_blogpost_content_newspost_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProjectCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('icon', models.CharField(blank=True, help_text='Класс иконки (например: fa-code)', max_length=50, verbose_name='Иконка')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Категория проекта',
|
||||
'verbose_name_plural': 'Категории проектов',
|
||||
'ordering': ['order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='portfolioitem',
|
||||
name='categories',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='portfoliomedia',
|
||||
name='portfolio_item',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='project',
|
||||
options={'ordering': ['-is_featured', '-display_order', '-completion_date'], 'verbose_name': 'Проект', 'verbose_name_plural': 'Проекты'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='display_order',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Порядок отображения'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='duration',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='Длительность'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='github_url',
|
||||
field=models.URLField(blank=True, verbose_name='GitHub репозиторий'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='is_featured',
|
||||
field=models.BooleanField(default=False, verbose_name='Избранный проект'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='likes_count',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Количество лайков'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_description',
|
||||
field=models.TextField(blank=True, max_length=300, verbose_name='SEO описание'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_keywords',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Ключевые слова'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='meta_title',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='SEO заголовок'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='project_url',
|
||||
field=models.URLField(blank=True, verbose_name='Ссылка на проект'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='short_description',
|
||||
field=models.TextField(default='Описание проекта', max_length=300, verbose_name='Краткое описание'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='team_size',
|
||||
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Размер команды'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='technologies',
|
||||
field=models.TextField(blank=True, help_text='Разделите запятыми', verbose_name='Технологии'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='thumbnail',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='static/img/project/thumbnails/', verbose_name='Миниатюра'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='views_count',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Количество просмотров'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpost',
|
||||
name='content',
|
||||
field=tinymce.models.HTMLField(verbose_name='Содержание'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='web.category', verbose_name='Категория (старая)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='client',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='web.client', verbose_name='Клиент'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='completion_date',
|
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата завершения'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='description',
|
||||
field=tinymce.models.HTMLField(verbose_name='Полное описание'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='static/img/project/', verbose_name='Главное изображение'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, verbose_name='Название проекта'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='order',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project', to='web.order', verbose_name='Заказ'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='service',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='web.service', verbose_name='Услуга'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_progress', 'В процессе'), ('completed', 'Завершен'), ('archived', 'В архиве')], default='in_progress', max_length=50, verbose_name='Статус'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(blank=True, related_name='projects', to='web.projectcategory', verbose_name='Категории'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectMedia',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('media_type', models.CharField(choices=[('image', 'Изображение'), ('video', 'Видео'), ('embed_video', 'Встроенное видео (YouTube, Vimeo)')], default='image', max_length=20, verbose_name='Тип медиа')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='static/img/project/gallery/', verbose_name='Изображение')),
|
||||
('video', models.FileField(blank=True, null=True, upload_to='static/video/project/gallery/', verbose_name='Видео файл')),
|
||||
('video_poster', models.ImageField(blank=True, null=True, upload_to='static/img/project/gallery/posters/', verbose_name='Превью видео')),
|
||||
('embed_code', models.TextField(blank=True, help_text='Вставьте iframe код от YouTube или Vimeo', verbose_name='Код встраивания (iframe)')),
|
||||
('caption', models.CharField(blank=True, max_length=200, verbose_name='Подпись')),
|
||||
('alt_text', models.CharField(blank=True, max_length=200, verbose_name='Alt текст')),
|
||||
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_items', to='web.project', verbose_name='Проект')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Медиа файл проекта',
|
||||
'verbose_name_plural': 'Медиа файлы проектов',
|
||||
'ordering': ['order', 'uploaded_at'],
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='NewsPost',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioCategory',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioItem',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PortfolioMedia',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 5.1.1 on 2025-11-26 00:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0015_projectcategory_remove_portfolioitem_categories_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='ProjectCategory',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': ['order', 'name'], 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='icon',
|
||||
field=models.CharField(blank=True, help_text='Класс FontAwesome (например: fa-code)', max_length=50, verbose_name='Иконка'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='Активна'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Порядок'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, max_length=100, null=True, unique=True, verbose_name='URL'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(blank=True, related_name='projects', to='web.category', verbose_name='Категории'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectmedia',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_files', to='web.project', verbose_name='Проект'),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user