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 с отображением всех медиа
294 lines
12 KiB
Python
294 lines
12 KiB
Python
from django.contrib import admin
|
|
from .models import (
|
|
Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest,
|
|
HeroBanner, ContactInfo, Team, Career,
|
|
ProjectMedia
|
|
)
|
|
from .forms import ProjectForm
|
|
|
|
@admin.register(ContactInfo)
|
|
class ContactInfoAdmin(admin.ModelAdmin):
|
|
list_display = ('company_name', 'email', 'phone', 'is_active')
|
|
list_filter = ('is_active',)
|
|
search_fields = ('company_name', 'email', 'phone')
|
|
fields = ('company_name', 'email', 'phone', 'telegram', 'address', 'working_hours',
|
|
'description', 'call_to_action', 'subtitle', 'is_active')
|
|
|
|
@admin.register(HeroBanner)
|
|
class HeroBannerAdmin(admin.ModelAdmin):
|
|
list_display = ('title', 'is_active', 'order', 'created_at')
|
|
list_filter = ('is_active', 'created_at')
|
|
search_fields = ('title', 'subtitle')
|
|
fields = ('title', 'subtitle', 'description', 'image', 'video', 'video_poster',
|
|
'button_text', 'button_link', 'is_active', 'order')
|
|
list_editable = ('is_active', 'order')
|
|
|
|
@admin.register(Service)
|
|
class ServiceAdmin(admin.ModelAdmin):
|
|
list_display = ('name', 'category', 'price', 'has_video')
|
|
search_fields = ('name', 'category')
|
|
fields = ('name', 'description', 'price', 'category', 'image', 'video', 'video_poster')
|
|
|
|
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')
|
|
search_fields = ('first_name', 'last_name', 'email')
|
|
|
|
@admin.register(Order)
|
|
class OrderAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'service', 'client', 'client__email', 'client__phone_number', 'status')
|
|
list_filter = ('status','client', 'order_date')
|
|
search_fields = ('client__first_name', 'service__name','status','client', 'order_date')
|
|
|
|
@admin.register(Review)
|
|
class ReviewAdmin(admin.ModelAdmin):
|
|
list_display = ('client', 'service', 'rating', 'review_date', 'has_video')
|
|
list_filter = ('rating',)
|
|
search_fields = ('client__first_name', 'service__name')
|
|
fields = ('client', 'service', 'project', 'rating', 'comment', 'image', 'video', 'video_poster')
|
|
|
|
def has_video(self, obj):
|
|
return bool(obj.video)
|
|
has_video.boolean = True
|
|
has_video.short_description = 'Есть видео'
|
|
|
|
@admin.register(BlogPost)
|
|
class BlogPostAdmin(admin.ModelAdmin):
|
|
list_display = ('title', 'published_date', 'has_video')
|
|
search_fields = ('title',)
|
|
fields = ('title', 'content', 'image', 'video', 'video_poster')
|
|
|
|
def has_video(self, obj):
|
|
return bool(obj.video)
|
|
has_video.boolean = True
|
|
has_video.short_description = 'Есть видео'
|
|
|
|
@admin.register(Category)
|
|
class CategoryAdmin(admin.ModelAdmin):
|
|
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):
|
|
list_display = ('service','token', 'client', 'created_at')
|
|
search_fields = ('service','token', 'client')
|
|
list_filter = ('service','token','client')
|
|
|
|
|
|
@admin.register(Team)
|
|
class TeamAdmin(admin.ModelAdmin):
|
|
list_display = ('first_name', 'last_name', 'position', 'department', 'is_active', 'display_order')
|
|
list_filter = ('department', 'is_active', 'show_on_about')
|
|
search_fields = ('first_name', 'last_name', 'position', 'skills')
|
|
list_editable = ('display_order', 'is_active')
|
|
|
|
fieldsets = (
|
|
('Основная информация', {
|
|
'fields': ('first_name', 'last_name', 'position', 'department')
|
|
}),
|
|
('Контактные данные', {
|
|
'fields': ('email', 'phone', 'photo'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Профессиональная информация', {
|
|
'fields': ('bio', 'skills', 'experience_years')
|
|
}),
|
|
('Социальные сети', {
|
|
'fields': ('linkedin', 'github', 'telegram'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Настройки отображения', {
|
|
'fields': ('is_active', 'show_on_about', 'display_order')
|
|
}),
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
return super().get_queryset(request).order_by('display_order', 'last_name')
|
|
|
|
|
|
@admin.register(Career)
|
|
class CareerAdmin(admin.ModelAdmin):
|
|
list_display = ('title', 'department', 'experience_level', 'employment_type', 'status', 'is_featured', 'created_at')
|
|
list_filter = ('status', 'employment_type', 'experience_level', 'department', 'is_featured')
|
|
search_fields = ('title', 'department', 'description', 'required_skills')
|
|
list_editable = ('status', 'is_featured')
|
|
|
|
fieldsets = (
|
|
('Основная информация', {
|
|
'fields': ('title', 'department', 'location', 'employment_type', 'experience_level')
|
|
}),
|
|
('Описание вакансии', {
|
|
'fields': ('description', 'responsibilities', 'requirements', 'benefits')
|
|
}),
|
|
('Зарплата', {
|
|
'fields': ('salary_min', 'salary_max', 'salary_currency'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Навыки', {
|
|
'fields': ('required_skills', 'preferred_skills'),
|
|
}),
|
|
('Контактная информация', {
|
|
'fields': ('contact_email', 'contact_person'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Статус и метаданные', {
|
|
'fields': ('status', 'is_featured', 'application_deadline', 'published_at')
|
|
}),
|
|
)
|
|
|
|
readonly_fields = ('created_at', 'updated_at')
|
|
|
|
def get_queryset(self, request):
|
|
return super().get_queryset(request).order_by('-is_featured', '-created_at')
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
if obj.status == 'active' and not obj.published_at:
|
|
from django.utils import timezone
|
|
obj.published_at = timezone.now()
|
|
super().save_model(request, obj, form, change)
|
|
|
|
actions = ['mark_as_active', 'mark_as_paused', 'mark_as_closed']
|
|
|
|
def mark_as_active(self, request, queryset):
|
|
from django.utils import timezone
|
|
updated = queryset.update(status='active')
|
|
queryset.filter(published_at__isnull=True).update(published_at=timezone.now())
|
|
self.message_user(request, f'{updated} вакансий отмечены как активные.')
|
|
mark_as_active.short_description = "Отметить как активные"
|
|
|
|
def mark_as_paused(self, request, queryset):
|
|
updated = queryset.update(status='paused')
|
|
self.message_user(request, f'{updated} вакансий приостановлены.')
|
|
mark_as_paused.short_description = "Приостановить"
|
|
|
|
def mark_as_closed(self, request, queryset):
|
|
updated = queryset.update(status='closed')
|
|
self.message_user(request, f'{updated} вакансий закрыты.')
|
|
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',)
|
|
}),
|
|
)
|