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',
|
'django.contrib.messages.context_processors.messages',
|
||||||
'web.context_processors.footer_settings', # Custom context processor
|
'web.context_processors.footer_settings', # Custom context processor
|
||||||
'web.context_processors.site_settings', # Site settings (currency, etc.)
|
'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,
|
Service, Project, Client, Order, Review, BlogPost,
|
||||||
Category, ServiceRequest, AboutPage, FooterSettings, TeamMember,
|
Category, ServiceRequest, AboutPage, FooterSettings, TeamMember,
|
||||||
PortfolioItem, PrivacyPolicy, TermsOfUse, NewsArticle, CareerVacancy,
|
PortfolioItem, PrivacyPolicy, TermsOfUse, NewsArticle, CareerVacancy,
|
||||||
SiteSettings, PortfolioImage
|
SiteSettings, PortfolioImage, ContactSettings
|
||||||
)
|
)
|
||||||
from .forms import ProjectForm
|
from .forms import ProjectForm
|
||||||
|
|
||||||
@@ -163,7 +163,12 @@ class AboutPageAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
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)
|
@admin.register(FooterSettings)
|
||||||
@@ -209,7 +214,12 @@ 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 FooterSettings.objects.count() == 0
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
# Запретить удаление единственной активной записи
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TeamMember)
|
@admin.register(TeamMember)
|
||||||
@@ -259,3 +269,53 @@ class SiteSettingsAdmin(admin.ModelAdmin):
|
|||||||
return False
|
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):
|
def footer_settings(request):
|
||||||
@@ -20,3 +20,11 @@ def site_settings(request):
|
|||||||
'site_settings': settings,
|
'site_settings': settings,
|
||||||
'currency_symbol': settings.currency_symbol,
|
'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
|
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>
|
<i class="fas fa-envelope fa-2x text-primary"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="card-title fw-bold mb-3">Email</h5>
|
<h5 class="card-title fw-bold mb-3">Email</h5>
|
||||||
<a href="mailto:{{ about.contact_email }}" class="text-decoration-none">
|
<a href="mailto:{{ contact_settings.email }}" class="text-decoration-none">
|
||||||
<p class="card-text text-muted mb-0">{{ about.contact_email }}</p>
|
<p class="card-text text-muted mb-0">{{ contact_settings.email }}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -341,8 +341,8 @@
|
|||||||
<i class="fas fa-phone fa-2x text-success"></i>
|
<i class="fas fa-phone fa-2x text-success"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="card-title fw-bold mb-3">Телефон</h5>
|
<h5 class="card-title fw-bold mb-3">Телефон</h5>
|
||||||
<a href="tel:{{ about.contact_phone }}" class="text-decoration-none">
|
<a href="tel:{{ contact_settings.phone }}" class="text-decoration-none">
|
||||||
<p class="card-text text-muted mb-0">{{ about.contact_phone }}</p>
|
<p class="card-text text-muted mb-0">{{ contact_settings.phone }}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -356,15 +356,15 @@
|
|||||||
<i class="fab fa-telegram-plane fa-2x text-info"></i>
|
<i class="fab fa-telegram-plane fa-2x text-info"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="card-title fw-bold mb-3">Telegram</h5>
|
<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">
|
<a href="https://t.me/{{ contact_settings.telegram|cut:'@' }}" target="_blank" class="text-decoration-none">
|
||||||
<p class="card-text text-muted mb-0">{{ about.contact_telegram }}</p>
|
<p class="card-text text-muted mb-0">{{ contact_settings.telegram }}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Address Card (optional) -->
|
<!-- Address Card -->
|
||||||
{% if about.contact_address %}
|
{% if contact_settings.address %}
|
||||||
<div class="col-lg-3 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<div class="card h-100 border-0 shadow-sm hover-lift">
|
<div class="card h-100 border-0 shadow-sm hover-lift">
|
||||||
<div class="card-body text-center p-4">
|
<div class="card-body text-center p-4">
|
||||||
@@ -372,7 +372,10 @@
|
|||||||
<i class="fas fa-map-marker-alt fa-2x text-warning"></i>
|
<i class="fas fa-map-marker-alt fa-2x text-warning"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="card-title fw-bold mb-3">Адрес</h5>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user