feat: добавлена модель TeamMember для управления командой через админку

This commit is contained in:
2025-11-24 09:23:45 +09:00
parent 3cea013a8e
commit ce7119e9e9
6 changed files with 294 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
Service, Project, Client, Order, Review, BlogPost, Service, Project, Client, Order, Review, BlogPost,
Category, ServiceRequest, AboutPage, FooterSettings Category, ServiceRequest, AboutPage, FooterSettings, TeamMember
) )
from .forms import ProjectForm from .forms import ProjectForm
@@ -150,3 +150,33 @@ class FooterSettingsAdmin(admin.ModelAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return not FooterSettings.objects.filter(is_active=True).exists() return not FooterSettings.objects.filter(is_active=True).exists()
@admin.register(TeamMember)
class TeamMemberAdmin(admin.ModelAdmin):
list_display = ('full_name', 'position', 'order', 'is_active', 'email', 'updated_at')
list_filter = ('is_active', 'position')
search_fields = ('first_name', 'last_name', 'position', 'email')
list_editable = ('order', 'is_active')
fieldsets = (
('Основная информация', {
'fields': ('first_name', 'last_name', 'position', 'photo')
}),
('О специалисте', {
'fields': ('bio', 'specialization')
}),
('Контакты', {
'fields': ('email', 'phone', 'telegram', 'linkedin', 'github'),
'classes': ('collapse',)
}),
('Настройки отображения', {
'fields': ('order', 'is_active')
}),
)
def full_name(self, obj):
return obj.full_name
full_name.short_description = 'ФИО'
full_name.admin_order_field = 'last_name'

View File

@@ -0,0 +1,65 @@
from django.core.management.base import BaseCommand
from web.models import TeamMember
class Command(BaseCommand):
help = 'Создает тестовых членов команды'
def handle(self, *args, **options):
# Проверяем, есть ли уже члены команды
if TeamMember.objects.exists():
self.stdout.write(self.style.WARNING('⚠️ Члены команды уже существуют'))
return
team_members = [
{
'first_name': 'Алексей',
'last_name': 'Чой',
'position': 'CEO & Founder',
'bio': 'Визионер и лидер команды с более чем 5-летним опытом в IT-индустрии. Специализируется на стратегическом планировании и управлении проектами.',
'specialization': 'Стратегическое планирование, управление проектами, бизнес-аналитика',
'email': 'alexey@smartsoltech.kr',
'telegram': 'alexey_choi',
'linkedin': 'https://linkedin.com/in/alexey-choi',
'github': 'https://github.com/alexeychoi',
'order': 1,
'is_active': True
},
{
'first_name': 'Анна',
'last_name': 'Ким',
'position': 'Lead Developer',
'bio': 'Опытный full-stack разработчик со страстью к созданию масштабируемых и эффективных веб-приложений. Эксперт в React, Django и cloud технологиях.',
'specialization': 'React, Django, Docker, PostgreSQL, AWS',
'email': 'anna@smartsoltech.kr',
'telegram': 'anna_kim_dev',
'linkedin': 'https://linkedin.com/in/anna-kim',
'github': 'https://github.com/annakim',
'order': 2,
'is_active': True
},
{
'first_name': 'Дмитрий',
'last_name': 'Пак',
'position': 'UI/UX Designer',
'bio': 'Креативный дизайнер, создающий интуитивные и привлекательные пользовательские интерфейсы. Специализируется на UX-исследованиях и современном веб-дизайне.',
'specialization': 'Figma, Adobe XD, Sketch, UI Design, UX Research',
'email': 'dmitry@smartsoltech.kr',
'telegram': 'dmitry_pak',
'linkedin': 'https://linkedin.com/in/dmitry-pak',
'order': 3,
'is_active': True
},
]
created_count = 0
for member_data in team_members:
member = TeamMember.objects.create(**member_data)
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'✅ Создан: {member.full_name} - {member.position}')
)
self.stdout.write(
self.style.SUCCESS(f'\n✨ Готово! Создано {created_count} членов команды')
)

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.1.1 on 2025-11-24 00:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0010_aboutpage_footersettings'),
]
operations = [
migrations.CreateModel(
name='TeamMember',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100, verbose_name='Имя')),
('last_name', models.CharField(max_length=100, verbose_name='Фамилия')),
('position', models.CharField(max_length=200, verbose_name='Должность')),
('photo', models.ImageField(blank=True, null=True, upload_to='static/img/team/', verbose_name='Фотография')),
('bio', models.TextField(blank=True, help_text='Краткое описание специалиста', verbose_name='Биография')),
('specialization', models.TextField(blank=True, help_text='Области экспертизы, навыки', verbose_name='Специализация')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=50, verbose_name='Телефон')),
('telegram', models.CharField(blank=True, max_length=100, verbose_name='Telegram')),
('linkedin', models.URLField(blank=True, verbose_name='LinkedIn')),
('github', models.URLField(blank=True, verbose_name='GitHub')),
('order', models.IntegerField(default=0, help_text='Чем меньше число, тем выше в списке', verbose_name='Порядок сортировки')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
],
options={
'verbose_name': 'Член команды',
'verbose_name_plural': 'Команда',
'ordering': ['order', 'last_name', 'first_name'],
},
),
]

View File

@@ -341,3 +341,66 @@ class FooterSettings(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class TeamMember(models.Model):
"""Модель для членов команды"""
# Basic Info
first_name = models.CharField(max_length=100, verbose_name='Имя')
last_name = models.CharField(max_length=100, verbose_name='Фамилия')
position = models.CharField(max_length=200, verbose_name='Должность')
# Photo
photo = models.ImageField(
upload_to='static/img/team/',
blank=True,
null=True,
verbose_name='Фотография'
)
# Bio
bio = models.TextField(
blank=True,
verbose_name='Биография',
help_text='Краткое описание специалиста'
)
# Skills/Specialization
specialization = models.TextField(
blank=True,
verbose_name='Специализация',
help_text='Области экспертизы, навыки'
)
# Social Links
email = models.EmailField(blank=True, verbose_name='Email')
phone = models.CharField(max_length=50, blank=True, verbose_name='Телефон')
telegram = models.CharField(max_length=100, blank=True, verbose_name='Telegram')
linkedin = models.URLField(blank=True, verbose_name='LinkedIn')
github = models.URLField(blank=True, verbose_name='GitHub')
# Order and Status
order = models.IntegerField(
default=0,
verbose_name='Порядок сортировки',
help_text='Чем меньше число, тем выше в списке'
)
is_active = models.BooleanField(default=True, verbose_name='Активен')
# Timestamps
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Создано')
updated_at = models.DateTimeField(auto_now=True, verbose_name='Обновлено')
class Meta:
verbose_name = 'Член команды'
verbose_name_plural = 'Команда'
ordering = ['order', 'last_name', 'first_name']
def __str__(self):
return f"{self.first_name} {self.last_name} - {self.position}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"

View File

@@ -0,0 +1,90 @@
<!-- Team Section -->
<section class="section-padding" id="team">
<div class="container-modern">
<div class="text-center mb-5">
<span class="badge bg-gradient text-white mb-3 px-3 py-2 rounded-pill">
{{ about.team_badge|default:"👥 Команда" }}
</span>
<h2 class="display-5 fw-bold mb-3">
{{ about.team_title|default:"Познакомьтесь с нашей командой"|safe }}
</h2>
<p class="lead text-muted max-width-600 mx-auto">
{{ about.team_description|default:"Талантливые профессионалы, которые воплощают ваши идеи в реальность" }}
</p>
</div>
{% if team_members %}
<div class="row g-4">
{% for member in team_members %}
<div class="col-lg-4 col-md-6" data-aos="fade-up">
<div class="card-modern text-center h-100">
<div class="position-relative">
{% if member.photo %}
<img src="{{ member.photo.url }}"
alt="{{ member.full_name }}"
class="team-avatar mx-auto mb-3 rounded-circle"
style="width: 120px; height: 120px; object-fit: cover;">
{% else %}
<div class="team-avatar mx-auto mb-3" style="width: 120px; height: 120px; background: var(--gradient-primary); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span class="text-white" style="font-size: 2.5rem; font-weight: 700;">
{{ member.first_name|first }}{{ member.last_name|first }}
</span>
</div>
{% endif %}
</div>
<div class="card-body">
<h5 class="mb-2">{{ member.full_name }}</h5>
<p class="text-primary mb-3">{{ member.position }}</p>
{% if member.bio %}
<p class="text-muted small mb-3">
{{ member.bio|truncatewords:30 }}
</p>
{% endif %}
{% if member.specialization %}
<div class="mb-3">
<small class="text-muted">
<i class="fas fa-code me-1"></i>
{{ member.specialization|truncatewords:10 }}
</small>
</div>
{% endif %}
<div class="d-flex justify-content-center gap-2">
{% if member.linkedin %}
<a href="{{ member.linkedin }}" target="_blank" class="btn btn-outline-primary btn-sm rounded-circle" style="width: 40px; height: 40px;">
<i class="fab fa-linkedin-in"></i>
</a>
{% endif %}
{% if member.github %}
<a href="{{ member.github }}" target="_blank" class="btn btn-outline-primary btn-sm rounded-circle" style="width: 40px; height: 40px;">
<i class="fab fa-github"></i>
</a>
{% endif %}
{% if member.telegram %}
<a href="https://t.me/{{ member.telegram }}" target="_blank" class="btn btn-outline-primary btn-sm rounded-circle" style="width: 40px; height: 40px;">
<i class="fab fa-telegram-plane"></i>
</a>
{% endif %}
{% if member.email %}
<a href="mailto:{{ member.email }}" class="btn btn-outline-primary btn-sm rounded-circle" style="width: 40px; height: 40px;">
<i class="fas fa-envelope"></i>
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<p class="text-muted">Информация о команде скоро появится</p>
</div>
{% endif %}
</div>
</section>

View File

@@ -1,5 +1,5 @@
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from .models import Service, Project, Client, BlogPost, Review, Order, ServiceRequest, Category, AboutPage, FooterSettings from .models import Service, Project, Client, BlogPost, Review, Order, ServiceRequest, Category, AboutPage, FooterSettings, TeamMember
from django.db.models import Avg from django.db.models import Avg
from comunication.models import TelegramSettings from comunication.models import TelegramSettings
import qrcode import qrcode
@@ -88,7 +88,11 @@ def services_view(request):
def about_view(request): def about_view(request):
about_page = AboutPage.objects.filter(is_active=True).first() about_page = AboutPage.objects.filter(is_active=True).first()
return render(request, 'web/about_modern.html', {'about': about_page}) team_members = TeamMember.objects.filter(is_active=True).order_by('order', 'last_name')
return render(request, 'web/about_modern.html', {
'about': about_page,
'team_members': team_members
})
def create_service_request(request, service_id): def create_service_request(request, service_id):
if request.method == 'POST': if request.method == 'POST':