👥 Добавлено управление персоналом и карьерой

 Новые функции:
- 🧑‍💻 Team модель для управления сотрудниками
  • Полная информация о персонале (имя, должность, отдел)
  • Фотографии и контактные данные
  • Социальные сети (LinkedIn, GitHub, Telegram)
  • Навыки и опыт работы
  • Гибкие настройки отображения

- 💼 Career модель для вакансий
  • Детальное описание позиций
  • Требования и обязанности
  • Зарплатные вилки
  • Типы занятости и уровни опыта
  • Статусы вакансий и дедлайны

🔧 Админ-панель:
- Удобные интерфейсы для HR-менеджмента
- Группировка полей и фильтрация
- Быстрые действия для массовых операций
- Сортировка по приоритету

📊 База данных:
- Миграция 0013_career_team.py
- Оптимизированные индексы и связи
This commit is contained in:
2025-11-25 15:44:57 +09:00
parent c1616ac542
commit ec01a2ae10
3 changed files with 340 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo
from .models import Service, Project, Client, Order, Review, BlogPost, Category, ServiceRequest, HeroBanner, ContactInfo, Team, Career
from .forms import ProjectForm
@admin.register(ContactInfo)
@@ -87,4 +87,96 @@ class CategoryAdmin(admin.ModelAdmin):
class ServiceRequestAdmin(admin.ModelAdmin):
list_display = ('service','token', 'client', 'created_at')
search_fields = ('service','token', 'client')
list_filter = ('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 = "Закрыть"

View File

@@ -0,0 +1,75 @@
# Generated by Django 5.1.1 on 2025-11-25 06:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('web', '0012_contactinfo'),
]
operations = [
migrations.CreateModel(
name='Career',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='Название вакансии')),
('department', models.CharField(max_length=100, verbose_name='Отдел')),
('location', models.CharField(default='Кванджу, Южная Корея', max_length=200, verbose_name='Местоположение')),
('employment_type', models.CharField(choices=[('full_time', 'Полная занятость'), ('part_time', 'Частичная занятость'), ('contract', 'Контракт'), ('internship', 'Стажировка'), ('remote', 'Удаленная работа'), ('freelance', 'Фриланс')], default='full_time', max_length=20, verbose_name='Тип занятости')),
('experience_level', models.CharField(choices=[('junior', 'Junior (0-1 год)'), ('middle', 'Middle (2-4 года)'), ('senior', 'Senior (5+ лет)'), ('lead', 'Team Lead'), ('intern', 'Стажер')], default='middle', max_length=20, verbose_name='Уровень опыта')),
('description', models.TextField(verbose_name='Описание вакансии')),
('responsibilities', models.TextField(verbose_name='Обязанности')),
('requirements', models.TextField(verbose_name='Требования')),
('benefits', models.TextField(blank=True, verbose_name='Преимущества и условия')),
('salary_min', models.PositiveIntegerField(blank=True, null=True, verbose_name='Зарплата от (₩)')),
('salary_max', models.PositiveIntegerField(blank=True, null=True, verbose_name='Зарплата до (₩)')),
('salary_currency', models.CharField(default='KRW', max_length=10, verbose_name='Валюта')),
('required_skills', models.TextField(help_text='Разделите навыки запятыми', verbose_name='Обязательные навыки')),
('preferred_skills', models.TextField(blank=True, help_text='Разделите навыки запятыми', verbose_name='Желательные навыки')),
('status', models.CharField(choices=[('active', 'Активная'), ('paused', 'Приостановлена'), ('closed', 'Закрыта'), ('draft', 'Черновик')], default='active', max_length=20, verbose_name='Статус')),
('is_featured', models.BooleanField(default=False, verbose_name='Рекомендуемая вакансия')),
('application_deadline', models.DateField(blank=True, null=True, verbose_name='Дедлайн подачи заявок')),
('contact_email', models.EmailField(default='hr@smartsoltech.kr', max_length=254, verbose_name='Email для связи')),
('contact_person', models.CharField(blank=True, max_length=200, 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='Дата публикации')),
],
options={
'verbose_name': 'Вакансия',
'verbose_name_plural': 'Карьера',
'ordering': ['-is_featured', '-published_at', '-created_at'],
},
),
migrations.CreateModel(
name='Team',
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='Должность')),
('department', models.CharField(blank=True, max_length=100, verbose_name='Отдел')),
('bio', models.TextField(blank=True, verbose_name='Биография/Описание')),
('photo', models.ImageField(blank=True, null=True, upload_to='static/img/team/', verbose_name='Фотография')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, verbose_name='Телефон')),
('linkedin', models.URLField(blank=True, verbose_name='LinkedIn')),
('github', models.URLField(blank=True, verbose_name='GitHub')),
('telegram', models.CharField(blank=True, max_length=100, verbose_name='Telegram')),
('skills', models.TextField(blank=True, help_text='Разделите навыки запятыми', verbose_name='Навыки')),
('experience_years', models.PositiveIntegerField(default=0, verbose_name='Лет опыта')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('display_order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
('show_on_about', models.BooleanField(default=True, verbose_name='Показывать на странице О нас')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Сотрудник',
'verbose_name_plural': 'Команда',
'ordering': ['display_order', 'last_name', 'first_name'],
},
),
]

View File

@@ -212,3 +212,174 @@ class Review(models.Model):
def __str__(self):
return f"Отзыв от {self.client.first_name} {self.client.last_name} for {self.service.name}"
class Team(models.Model):
"""Модель для управления персоналом компании"""
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="Должность")
department = models.CharField(max_length=100, verbose_name="Отдел", blank=True)
bio = models.TextField(verbose_name="Биография/Описание", blank=True)
photo = models.ImageField(upload_to='static/img/team/', blank=True, null=True, verbose_name="Фотография")
email = models.EmailField(blank=True, verbose_name="Email")
phone = models.CharField(max_length=20, blank=True, verbose_name="Телефон")
# Социальные сети
linkedin = models.URLField(blank=True, verbose_name="LinkedIn")
github = models.URLField(blank=True, verbose_name="GitHub")
telegram = models.CharField(max_length=100, blank=True, verbose_name="Telegram")
# Навыки и технологии
skills = models.TextField(blank=True, verbose_name="Навыки", help_text="Разделите навыки запятыми")
experience_years = models.PositiveIntegerField(default=0, verbose_name="Лет опыта")
# Настройки отображения
is_active = models.BooleanField(default=True, verbose_name="Активен")
display_order = models.PositiveIntegerField(default=0, verbose_name="Порядок отображения")
show_on_about = models.BooleanField(default=True, verbose_name="Показывать на странице О нас")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Сотрудник'
verbose_name_plural = 'Команда'
ordering = ['display_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}"
@property
def skills_list(self):
"""Возвращает список навыков"""
if self.skills:
return [skill.strip() for skill in self.skills.split(',') if skill.strip()]
return []
class Career(models.Model):
"""Модель для управления вакансиями и карьерными возможностями"""
EMPLOYMENT_TYPE_CHOICES = [
('full_time', 'Полная занятость'),
('part_time', 'Частичная занятость'),
('contract', 'Контракт'),
('internship', 'Стажировка'),
('remote', 'Удаленная работа'),
('freelance', 'Фриланс'),
]
EXPERIENCE_LEVEL_CHOICES = [
('junior', 'Junior (0-1 год)'),
('middle', 'Middle (2-4 года)'),
('senior', 'Senior (5+ лет)'),
('lead', 'Team Lead'),
('intern', 'Стажер'),
]
STATUS_CHOICES = [
('active', 'Активная'),
('paused', 'Приостановлена'),
('closed', 'Закрыта'),
('draft', 'Черновик'),
]
title = models.CharField(max_length=200, verbose_name="Название вакансии")
department = models.CharField(max_length=100, verbose_name="Отдел")
location = models.CharField(max_length=200, default="Кванджу, Южная Корея", verbose_name="Местоположение")
employment_type = models.CharField(
max_length=20,
choices=EMPLOYMENT_TYPE_CHOICES,
default='full_time',
verbose_name="Тип занятости"
)
experience_level = models.CharField(
max_length=20,
choices=EXPERIENCE_LEVEL_CHOICES,
default='middle',
verbose_name="Уровень опыта"
)
# Описание вакансии
description = models.TextField(verbose_name="Описание вакансии")
responsibilities = models.TextField(verbose_name="Обязанности")
requirements = models.TextField(verbose_name="Требования")
benefits = models.TextField(blank=True, verbose_name="Преимущества и условия")
# Зарплатная вилка
salary_min = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата от (₩)")
salary_max = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата до (₩)")
salary_currency = models.CharField(max_length=10, default="KRW", verbose_name="Валюта")
# Необходимые навыки
required_skills = models.TextField(verbose_name="Обязательные навыки", help_text="Разделите навыки запятыми")
preferred_skills = models.TextField(blank=True, verbose_name="Желательные навыки", help_text="Разделите навыки запятыми")
# Статус и метаданные
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='active',
verbose_name="Статус"
)
is_featured = models.BooleanField(default=False, verbose_name="Рекомендуемая вакансия")
application_deadline = models.DateField(blank=True, null=True, verbose_name="Дедлайн подачи заявок")
# Контактная информация
contact_email = models.EmailField(default="hr@smartsoltech.kr", verbose_name="Email для связи")
contact_person = models.CharField(max_length=200, blank=True, verbose_name="Контактное лицо")
# Timestamps
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="Дата публикации")
class Meta:
verbose_name = 'Вакансия'
verbose_name_plural = 'Карьера'
ordering = ['-is_featured', '-published_at', '-created_at']
def __str__(self):
return f"{self.title} ({self.get_experience_level_display()})"
@property
def required_skills_list(self):
"""Возвращает список обязательных навыков"""
if self.required_skills:
return [skill.strip() for skill in self.required_skills.split(',') if skill.strip()]
return []
@property
def preferred_skills_list(self):
"""Возвращает список желательных навыков"""
if self.preferred_skills:
return [skill.strip() for skill in self.preferred_skills.split(',') if skill.strip()]
return []
@property
def salary_range(self):
"""Возвращает строку с зарплатной вилкой"""
if self.salary_min and self.salary_max:
return f"{self.salary_min:,} - ₩{self.salary_max:,}"
elif self.salary_min:
return f"от ₩{self.salary_min:,}"
elif self.salary_max:
return f"до ₩{self.salary_max:,}"
return "По договоренности"
def is_active_position(self):
"""Проверяет, активна ли вакансия"""
from django.utils import timezone
if self.status != 'active':
return False
if self.application_deadline and self.application_deadline < timezone.now().date():
return False
return True