Fix singleton admin: ensure Save button appears by checking count instead of filter
This commit is contained in:
@@ -74,6 +74,7 @@ TEMPLATES = [
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'web.context_processors.footer_settings', # Custom context processor
|
||||
'web.context_processors.site_settings', # Site settings (currency, etc.)
|
||||
'web.context_processors.contact_settings', # Contact settings
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ from .models import (
|
||||
Service, Project, Client, Order, Review, BlogPost,
|
||||
Category, ServiceRequest, AboutPage, FooterSettings, TeamMember,
|
||||
PortfolioItem, PrivacyPolicy, TermsOfUse, NewsArticle, CareerVacancy,
|
||||
SiteSettings, PortfolioImage
|
||||
SiteSettings, PortfolioImage, ContactSettings
|
||||
)
|
||||
from .forms import ProjectForm
|
||||
|
||||
@@ -163,7 +163,12 @@ class AboutPageAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return not AboutPage.objects.filter(is_active=True).exists()
|
||||
# Разрешить создание если нет ни одной записи
|
||||
return AboutPage.objects.count() == 0
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Запретить удаление
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(FooterSettings)
|
||||
@@ -209,7 +214,12 @@ class FooterSettingsAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return not FooterSettings.objects.filter(is_active=True).exists()
|
||||
# Разрешить создание если нет ни одной записи
|
||||
return FooterSettings.objects.count() == 0
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Запретить удаление единственной активной записи
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(TeamMember)
|
||||
@@ -259,3 +269,53 @@ class SiteSettingsAdmin(admin.ModelAdmin):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(ContactSettings)
|
||||
class ContactSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ('company_name', 'email', 'phone', 'telegram', 'updated_at')
|
||||
|
||||
fieldsets = (
|
||||
('📋 Основная информация', {
|
||||
'fields': ('company_name',),
|
||||
'description': 'Название компании'
|
||||
}),
|
||||
('📞 Основные контакты', {
|
||||
'fields': ('email', 'phone', 'telegram', 'whatsapp'),
|
||||
'description': 'Главные каналы связи с клиентами'
|
||||
}),
|
||||
('📍 Адрес и режим работы', {
|
||||
'fields': ('address', 'working_hours'),
|
||||
}),
|
||||
('🌐 Социальные сети', {
|
||||
'fields': (
|
||||
'telegram_url',
|
||||
'instagram_url',
|
||||
'linkedin_url',
|
||||
'facebook_url',
|
||||
'twitter_url',
|
||||
'youtube_url',
|
||||
'github_url'
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Ссылки на страницы в социальных сетях'
|
||||
}),
|
||||
('📧 Дополнительные контакты', {
|
||||
'fields': ('support_email', 'sales_email', 'emergency_phone'),
|
||||
'classes': ('collapse',),
|
||||
'description': 'Специализированные контакты (опционально)'
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Запретить создание новых записей (singleton)
|
||||
return not ContactSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Запретить удаление контактов
|
||||
return False
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('admin/css/forms.css',)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .models import FooterSettings, SiteSettings
|
||||
from .models import FooterSettings, SiteSettings, ContactSettings
|
||||
|
||||
|
||||
def footer_settings(request):
|
||||
@@ -20,3 +20,11 @@ def site_settings(request):
|
||||
'site_settings': settings,
|
||||
'currency_symbol': settings.currency_symbol,
|
||||
}
|
||||
|
||||
|
||||
def contact_settings(request):
|
||||
"""Context processor для контактных данных"""
|
||||
contacts = ContactSettings.get_contacts()
|
||||
return {
|
||||
'contact_settings': contacts,
|
||||
}
|
||||
|
||||
@@ -631,5 +631,138 @@ class SiteSettings(models.Model):
|
||||
return settings
|
||||
|
||||
|
||||
class ContactSettings(models.Model):
|
||||
"""Контактная информация компании"""
|
||||
|
||||
# Основные контакты
|
||||
company_name = models.CharField(
|
||||
max_length=200,
|
||||
default='SmartSolTech',
|
||||
verbose_name='Название компании'
|
||||
)
|
||||
|
||||
email = models.EmailField(
|
||||
default='info@smartsoltech.kr',
|
||||
verbose_name='Email',
|
||||
help_text='Основной email для связи'
|
||||
)
|
||||
|
||||
phone = models.CharField(
|
||||
max_length=50,
|
||||
default='+82-10-XXXX-XXXX',
|
||||
verbose_name='Телефон',
|
||||
help_text='Контактный телефон'
|
||||
)
|
||||
|
||||
telegram = models.CharField(
|
||||
max_length=100,
|
||||
default='@smartsoltech',
|
||||
verbose_name='Telegram',
|
||||
help_text='Telegram username (с @)'
|
||||
)
|
||||
|
||||
whatsapp = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name='WhatsApp',
|
||||
help_text='Номер WhatsApp (опционально)'
|
||||
)
|
||||
|
||||
# Адрес и время работы
|
||||
address = models.TextField(
|
||||
default='Seoul, South Korea',
|
||||
verbose_name='Адрес',
|
||||
help_text='Полный адрес офиса'
|
||||
)
|
||||
|
||||
working_hours = models.CharField(
|
||||
max_length=200,
|
||||
default='Пн-Пт: 9:00-18:00',
|
||||
verbose_name='Время работы',
|
||||
help_text='График работы'
|
||||
)
|
||||
|
||||
# Социальные сети
|
||||
telegram_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Telegram URL',
|
||||
help_text='Ссылка на Telegram канал/группу'
|
||||
)
|
||||
|
||||
instagram_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Instagram URL'
|
||||
)
|
||||
|
||||
linkedin_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='LinkedIn URL'
|
||||
)
|
||||
|
||||
facebook_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Facebook URL'
|
||||
)
|
||||
|
||||
twitter_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='Twitter URL'
|
||||
)
|
||||
|
||||
youtube_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='YouTube URL'
|
||||
)
|
||||
|
||||
github_url = models.URLField(
|
||||
blank=True,
|
||||
verbose_name='GitHub URL'
|
||||
)
|
||||
|
||||
# Дополнительная информация
|
||||
support_email = models.EmailField(
|
||||
blank=True,
|
||||
verbose_name='Email поддержки',
|
||||
help_text='Отдельный email для техподдержки (опционально)'
|
||||
)
|
||||
|
||||
sales_email = models.EmailField(
|
||||
blank=True,
|
||||
verbose_name='Email отдела продаж',
|
||||
help_text='Email для коммерческих запросов (опционально)'
|
||||
)
|
||||
|
||||
emergency_phone = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name='Экстренный телефон',
|
||||
help_text='Телефон для срочных вопросов (опционально)'
|
||||
)
|
||||
|
||||
# Метаданные
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name='Обновлено'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Контактные данные'
|
||||
verbose_name_plural = 'Контактные данные'
|
||||
|
||||
def __str__(self):
|
||||
return f'Контакты {self.company_name} (обновлено: {self.updated_at.strftime("%d.%m.%Y")})'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Singleton pattern - только одна запись
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_contacts(cls):
|
||||
"""Получить контактные данные (создать если не существует)"""
|
||||
contacts, created = cls.objects.get_or_create(pk=1)
|
||||
return contacts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -326,8 +326,8 @@
|
||||
<i class="fas fa-envelope fa-2x text-primary"></i>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-3">Email</h5>
|
||||
<a href="mailto:{{ about.contact_email }}" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ about.contact_email }}</p>
|
||||
<a href="mailto:{{ contact_settings.email }}" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ contact_settings.email }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,8 +341,8 @@
|
||||
<i class="fas fa-phone fa-2x text-success"></i>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-3">Телефон</h5>
|
||||
<a href="tel:{{ about.contact_phone }}" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ about.contact_phone }}</p>
|
||||
<a href="tel:{{ contact_settings.phone }}" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ contact_settings.phone }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,15 +356,15 @@
|
||||
<i class="fab fa-telegram-plane fa-2x text-info"></i>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-3">Telegram</h5>
|
||||
<a href="https://t.me/{{ about.contact_telegram|cut:'@' }}" target="_blank" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ about.contact_telegram }}</p>
|
||||
<a href="https://t.me/{{ contact_settings.telegram|cut:'@' }}" target="_blank" class="text-decoration-none">
|
||||
<p class="card-text text-muted mb-0">{{ contact_settings.telegram }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Card (optional) -->
|
||||
{% if about.contact_address %}
|
||||
<!-- Address Card -->
|
||||
{% if contact_settings.address %}
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||
<div class="card-body text-center p-4">
|
||||
@@ -372,7 +372,10 @@
|
||||
<i class="fas fa-map-marker-alt fa-2x text-warning"></i>
|
||||
</div>
|
||||
<h5 class="card-title fw-bold mb-3">Адрес</h5>
|
||||
<p class="card-text text-muted mb-0">{{ about.contact_address }}</p>
|
||||
<p class="card-text text-muted mb-0">{{ contact_settings.address }}</p>
|
||||
{% if contact_settings.working_hours %}
|
||||
<p class="card-text text-muted small mt-2">{{ contact_settings.working_hours }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user