Добавлена система проектов с автоматическим ресайзом изображений и адаптивным дизайном
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:
@@ -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',)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user