Добавлена система проектов с автоматическим ресайзом изображений и адаптивным дизайном
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:
2025-11-26 09:44:14 +09:00
parent 5bcf3e8198
commit e7d6d5262d
26 changed files with 3029 additions and 447 deletions

View File

@@ -1,5 +1,9 @@
from django.contrib import admin
from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo, Team, Career
from .models import (
Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest,
HeroBanner, ContactInfo, Team, Career,
ProjectMedia
)
from .forms import ProjectForm
@admin.register(ContactInfo)
@@ -30,20 +34,6 @@ class ServiceAdmin(admin.ModelAdmin):
has_video.boolean = True
has_video.short_description = 'Есть видео'
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
list_display = ('name', 'client','service', 'status', 'order', 'has_video')
list_filter = ('name', 'client','service', 'status', 'order')
search_fields = ('name', 'client','service', 'status', 'order', 'client__first_name', 'client__last_name')
fields = ('name', 'description', 'completion_date', 'client', 'service', 'order',
'category', 'image', 'video', 'video_poster', 'status')
def has_video(self, obj):
return bool(obj.video)
has_video.boolean = True
has_video.short_description = 'Есть видео'
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email', 'phone_number')
@@ -80,8 +70,28 @@ class BlogPostAdmin(admin.ModelAdmin):
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name','description')
search_fields = ('name',)
list_display = ('name', 'slug', 'order', 'is_active', 'services_count', 'projects_count')
list_filter = ('is_active',)
search_fields = ('name', 'description')
prepopulated_fields = {'slug': ('name',)}
list_editable = ('order', 'is_active')
ordering = ('order', 'name')
fieldsets = (
('Основная информация', {
'fields': ('name', 'slug', 'description', 'icon')
}),
('Настройки отображения', {
'fields': ('order', 'is_active')
}),
)
def services_count(self, obj):
return obj.services.count()
services_count.short_description = 'Услуг'
def projects_count(self, obj):
return obj.projects.count()
projects_count.short_description = 'Проектов'
@admin.register(ServiceRequest)
class ServiceRequestAdmin(admin.ModelAdmin):
@@ -179,4 +189,105 @@ class CareerAdmin(admin.ModelAdmin):
def mark_as_closed(self, request, queryset):
updated = queryset.update(status='closed')
self.message_user(request, f'{updated} вакансий закрыты.')
mark_as_closed.short_description = "Закрыть"
mark_as_closed.short_description = "Закрыть"
# ============================================
# ПРОЕКТЫ - АДМИНКИ
# ============================================
class ProjectMediaInline(admin.TabularInline):
"""Inline для медиа-файлов проекта"""
model = ProjectMedia
extra = 1
fields = ('media_type', 'image', 'video', 'video_poster', 'embed_code', 'caption', 'order')
ordering = ('order',)
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
"""Админка для проектов"""
list_display = ('name', 'status', 'is_featured', 'display_order', 'categories_display',
'views_count', 'likes_count', 'media_count', 'completion_date')
list_filter = ('status', 'is_featured', 'categories', 'completion_date')
search_fields = ('name', 'description', 'client__first_name', 'client__last_name', 'technologies')
filter_horizontal = ('categories',)
list_editable = ('is_featured', 'display_order', 'status')
ordering = ('-is_featured', '-display_order', '-completion_date')
date_hierarchy = 'completion_date'
inlines = [ProjectMediaInline]
fieldsets = (
('📋 Основная информация', {
'fields': ('name', 'categories', 'status', 'is_featured', 'display_order')
}),
('📝 Описание', {
'fields': ('short_description', 'description', 'image')
}),
('🏢 Детали проекта', {
'fields': ('client', 'service', 'order', 'category', 'project_url', 'github_url',
'technologies', 'duration', 'team_size', 'completion_date')
}),
('🎬 Видео', {
'fields': ('video', 'video_poster'),
'classes': ('collapse',)
}),
('🔍 SEO', {
'fields': ('meta_title', 'meta_description', 'meta_keywords'),
'classes': ('collapse',)
}),
('📊 Статистика', {
'fields': ('views_count', 'likes_count'),
'classes': ('collapse',)
}),
)
readonly_fields = ('views_count', 'likes_count')
def categories_display(self, obj):
return ', '.join([cat.name for cat in obj.categories.all()[:3]])
categories_display.short_description = 'Категории'
def media_count(self, obj):
return obj.media_files.count()
media_count.short_description = 'Медиа'
actions = ['mark_as_completed', 'mark_as_featured']
def mark_as_completed(self, request, queryset):
updated = queryset.update(status='completed')
self.message_user(request, f'{updated} проектов отмечены как завершённые.')
mark_as_completed.short_description = "Отметить как завершённые"
def mark_as_featured(self, request, queryset):
updated = queryset.update(is_featured=True)
self.message_user(request, f'{updated} проектов отмечены как избранные.')
mark_as_featured.short_description = "Отметить как избранные"
@admin.register(ProjectMedia)
class ProjectMediaAdmin(admin.ModelAdmin):
"""Админка для медиа-файлов проектов"""
list_display = ('id', 'project', 'media_type', 'caption', 'order', 'uploaded_at')
list_filter = ('media_type', 'uploaded_at')
search_fields = ('project__name', 'caption', 'alt_text')
list_editable = ('order',)
ordering = ('project', 'order', '-uploaded_at')
fieldsets = (
('Проект', {
'fields': ('project', 'media_type', 'order')
}),
('Изображение', {
'fields': ('image', 'alt_text'),
'classes': ('collapse',)
}),
('Видео', {
'fields': ('video', 'video_poster', 'embed_code'),
'classes': ('collapse',)
}),
('Описание', {
'fields': ('caption',)
}),
)