Compare commits
5 Commits
f22849878b
...
dd99fdffe8
| Author | SHA1 | Date | |
|---|---|---|---|
| dd99fdffe8 | |||
|
|
223fea5369 | ||
|
|
df0e27696c | ||
|
|
cf0d48c800 | ||
|
|
443d547f9d |
@ -60,15 +60,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Скрипт для модального окна -->
|
|
||||||
<script src="{% static 'programmer/js/recall.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Модальное окно для увеличения изображений -->
|
<!-- Модальное окно для увеличения изображений -->
|
||||||
<div id="imageModal" class="modal">
|
<div id="imageModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modalTitle">Просмотр изображения</h3>
|
<h3 id="modalTitle">Просмотр изображения</h3>
|
||||||
<button class="modal-close" onclick="closeModal()">×</button>
|
<button class="modal-close" onclick="ImageModal.close()">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<img class="modal-image" id="modalImage" alt="">
|
<img class="modal-image" id="modalImage" alt="">
|
||||||
@ -76,6 +73,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Скрипт для модального окна -->
|
||||||
|
<script src="{% static 'programmer/js/recall.js' %}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@ -90,7 +90,7 @@
|
|||||||
img.style.cursor = 'pointer';
|
img.style.cursor = 'pointer';
|
||||||
img.addEventListener('click', function() {
|
img.addEventListener('click', function() {
|
||||||
// Вызываем глобальную функцию openModal из recall.js
|
// Вызываем глобальную функцию openModal из recall.js
|
||||||
openModal(this.src, this.alt || 'Изображение из статьи');
|
ImageModal.open(this.src, this.alt || 'Изображение из статьи');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,8 @@ from django.utils import timezone
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.contrib import messages
|
from django.db import models
|
||||||
|
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||||
|
|
||||||
from .models import Home, Solution, Competence, Recall, CallbackRequest, PageView
|
from .models import Home, Solution, Competence, Recall, CallbackRequest, PageView
|
||||||
|
|
||||||
@ -11,38 +12,134 @@ from .models import Home, Solution, Competence, Recall, CallbackRequest, PageVie
|
|||||||
# ===== BASE ADMIN CLASSES =====
|
# ===== BASE ADMIN CLASSES =====
|
||||||
|
|
||||||
class BaseContentAdmin(admin.ModelAdmin):
|
class BaseContentAdmin(admin.ModelAdmin):
|
||||||
"""Общий базовый класс для контентных моделей."""
|
"""
|
||||||
|
Базовый класс для контентных моделей.
|
||||||
|
CKEditor, actions публикации, date_hierarchy.
|
||||||
|
"""
|
||||||
list_display_links = ('id', 'title')
|
list_display_links = ('id', 'title')
|
||||||
list_editable = ('is_published',)
|
list_editable = ('is_published',)
|
||||||
list_filter = ('time_create', 'is_published')
|
list_filter = ('time_create', 'is_published')
|
||||||
search_fields = ('title',)
|
search_fields = ('title',)
|
||||||
|
date_hierarchy = 'time_create'
|
||||||
|
actions = ['make_published', 'make_unpublished']
|
||||||
|
readonly_fields = ('time_create', 'time_update')
|
||||||
|
|
||||||
|
formfield_overrides = {
|
||||||
|
models.TextField: {'widget': CKEditor5Widget(config_name='default')},
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_published(self, request, queryset):
|
||||||
|
updated = queryset.update(is_published=True)
|
||||||
|
self.message_user(request, f'{updated} объектов опубликовано.')
|
||||||
|
make_published.short_description = 'Опубликовать выбранные'
|
||||||
|
|
||||||
|
def make_unpublished(self, request, queryset):
|
||||||
|
updated = queryset.update(is_published=False)
|
||||||
|
self.message_user(request, f'{updated} объектов снято с публикации.')
|
||||||
|
make_unpublished.short_description = 'Снять с публикации'
|
||||||
|
|
||||||
|
|
||||||
# ===== MODEL ADMINS =====
|
# ===== MODEL ADMINS =====
|
||||||
|
|
||||||
@admin.register(Home)
|
@admin.register(Home)
|
||||||
class HomeAdmin(BaseContentAdmin):
|
class HomeAdmin(BaseContentAdmin):
|
||||||
|
# Поля: title, content, home_image, time_create, time_update, is_published
|
||||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
search_fields = ('title', 'content')
|
search_fields = ('title', 'content')
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('title', 'is_published'),
|
||||||
|
}),
|
||||||
|
('Содержание', {
|
||||||
|
'fields': ('content', 'home_image'),
|
||||||
|
'description': 'Основной текст главной страницы.',
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'fields': ('time_create', 'time_update'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Solution)
|
@admin.register(Solution)
|
||||||
class SolutionAdmin(BaseContentAdmin):
|
class SolutionAdmin(BaseContentAdmin):
|
||||||
|
# Поля: title, description, implementation, closing, time_create, time_update, is_published
|
||||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
search_fields = ('title', 'description', 'implementation')
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
|
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('title', 'slug', 'is_published'),
|
||||||
|
}),
|
||||||
|
('Содержание', {
|
||||||
|
'fields': ('description', 'implementation', 'closing'),
|
||||||
|
'description': 'Описание, реализация и заключение по проекту.',
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'fields': ('time_create', 'time_update'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Competence)
|
@admin.register(Competence)
|
||||||
class CompetenceAdmin(BaseContentAdmin):
|
class CompetenceAdmin(BaseContentAdmin):
|
||||||
|
# Поля: title, content, photo, time_create, time_update, is_published
|
||||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
search_fields = ('title', 'content')
|
search_fields = ('title', 'content')
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('title', 'is_published'),
|
||||||
|
}),
|
||||||
|
('Содержание', {
|
||||||
|
'fields': ('content', 'photo'),
|
||||||
|
'description': 'Описание компетенции и фото.',
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'fields': ('time_create', 'time_update'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Recall)
|
@admin.register(Recall)
|
||||||
class RecallAdmin(BaseContentAdmin):
|
class RecallAdmin(BaseContentAdmin):
|
||||||
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
# Поля: title, content, scan, time_create, time_update, is_published
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan_preview', 'is_published')
|
||||||
search_fields = ('title', 'content')
|
search_fields = ('title', 'content')
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('title', 'is_published'),
|
||||||
|
}),
|
||||||
|
('Содержание', {
|
||||||
|
'fields': ('content', 'scan', 'scan_preview'),
|
||||||
|
'description': 'Текст отзыва и скан документа.',
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'fields': ('time_create', 'time_update'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
readonly_fields = ('time_create', 'time_update', 'scan_preview')
|
||||||
|
|
||||||
|
def scan_preview(self, obj):
|
||||||
|
if obj.scan:
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" style="max-height:200px; max-width:400px; '
|
||||||
|
'border-radius:4px; border:1px solid #ddd;" />',
|
||||||
|
obj.scan.url
|
||||||
|
)
|
||||||
|
return '—'
|
||||||
|
scan_preview.short_description = 'Превью скана'
|
||||||
|
|
||||||
|
|
||||||
|
# ===== CALLBACK =====
|
||||||
|
|
||||||
@admin.register(CallbackRequest)
|
@admin.register(CallbackRequest)
|
||||||
class CallbackAdmin(admin.ModelAdmin):
|
class CallbackAdmin(admin.ModelAdmin):
|
||||||
@ -54,16 +151,12 @@ class CallbackAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ('time_create',)
|
readonly_fields = ('time_create',)
|
||||||
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
|
||||||
|
|
||||||
# ── badge helper ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def new_badge(self, obj):
|
def new_badge(self, obj):
|
||||||
if not obj.is_read:
|
if not obj.is_read:
|
||||||
return format_html('<span style="color:red;font-weight:bold;">🆕 НОВАЯ</span>')
|
return format_html('<span style="color:red;font-weight:bold;">🆕 НОВАЯ</span>')
|
||||||
return ''
|
return ''
|
||||||
new_badge.short_description = 'Статус'
|
new_badge.short_description = 'Статус'
|
||||||
|
|
||||||
# ── bulk actions ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def mark_as_read(self, request, queryset):
|
def mark_as_read(self, request, queryset):
|
||||||
updated = queryset.update(is_read=True)
|
updated = queryset.update(is_read=True)
|
||||||
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
||||||
@ -85,8 +178,6 @@ class CallbackAdmin(admin.ModelAdmin):
|
|||||||
self.message_user(request, f'Уведомления отправлены для {count} заявок')
|
self.message_user(request, f'Уведомления отправлены для {count} заявок')
|
||||||
resend_notification.short_description = 'Переотправить email уведомления'
|
resend_notification.short_description = 'Переотправить email уведомления'
|
||||||
|
|
||||||
# ── custom URL + view for stats ───────────────────────────────────────────
|
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
custom_urls = [
|
custom_urls = [
|
||||||
@ -117,11 +208,8 @@ class CallbackAdmin(admin.ModelAdmin):
|
|||||||
}
|
}
|
||||||
return render(request, 'admin/callback_stats.html', context)
|
return render(request, 'admin/callback_stats.html', context)
|
||||||
|
|
||||||
# ── FIX: removed get_queryset message_user spam ───────────────────────────
|
|
||||||
# Previously, a warning banner was shown on EVERY request to the changelist,
|
|
||||||
# including background requests. Removed entirely — the new_badge column
|
|
||||||
# and base_site.html header notification already surface unread counts.
|
|
||||||
|
|
||||||
|
# ===== PAGE VIEWS =====
|
||||||
|
|
||||||
@admin.register(PageView)
|
@admin.register(PageView)
|
||||||
class PageViewAdmin(admin.ModelAdmin):
|
class PageViewAdmin(admin.ModelAdmin):
|
||||||
@ -134,7 +222,6 @@ class PageViewAdmin(admin.ModelAdmin):
|
|||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).order_by('-timestamp')
|
return super().get_queryset(request).order_by('-timestamp')
|
||||||
|
|
||||||
# PageViews should never be created or edited manually
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,7 @@ class ProfileForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class RegistrationForm(UserCreationForm):
|
class RegistrationForm(UserCreationForm):
|
||||||
|
captcha = TurnstileField(label='', theme='light', size='normal')
|
||||||
# Поля пользователя
|
# Поля пользователя
|
||||||
first_name = forms.CharField(
|
first_name = forms.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -7,11 +9,13 @@ from django.dispatch import receiver
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from .utils.email_notifications import send_callback_notification
|
from .utils.email_notifications import send_callback_notification
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Recall(models.Model):
|
class Recall(models.Model):
|
||||||
title = models.CharField(max_length=255, verbose_name='Организация')
|
title = models.CharField(max_length=255, verbose_name='Организация')
|
||||||
content = models.TextField(blank=True, verbose_name='Отзыв')
|
content = models.TextField(blank=True, verbose_name='Отзыв')
|
||||||
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото')
|
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
@ -40,7 +44,7 @@ class Recall(models.Model):
|
|||||||
class Competence(models.Model):
|
class Competence(models.Model):
|
||||||
title = models.CharField(max_length=255, verbose_name='Программист')
|
title = models.CharField(max_length=255, verbose_name='Программист')
|
||||||
content = models.TextField(blank=True, verbose_name='Компетенция')
|
content = models.TextField(blank=True, verbose_name='Компетенция')
|
||||||
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото')
|
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
@ -65,12 +69,24 @@ class Solution(models.Model):
|
|||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
|
slug = models.SlugField(
|
||||||
|
max_length=255,
|
||||||
|
unique=False, # временно не уникальное
|
||||||
|
db_index=True,
|
||||||
|
verbose_name='URL-идентификатор',
|
||||||
|
blank=True,
|
||||||
|
null=True, # разрешаем NULL
|
||||||
|
)
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.title)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('solution_detail', kwargs={'pk': self.pk})
|
return reverse('solution_detail', kwargs={'slug': self.slug})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Проекты'
|
verbose_name = 'Проекты'
|
||||||
@ -78,18 +94,17 @@ class Solution(models.Model):
|
|||||||
ordering = ['time_create', 'title']
|
ordering = ['time_create', 'title']
|
||||||
|
|
||||||
def get_seo_title(self):
|
def get_seo_title(self):
|
||||||
"""Генерирует SEO-заголовок для проекта"""
|
|
||||||
return f"Проект: {self.title} | Автоматизация 1С"
|
return f"Проект: {self.title} | Автоматизация 1С"
|
||||||
|
|
||||||
def get_seo_description(self):
|
def get_seo_description(self):
|
||||||
"""Генерирует SEO-описание для проекта"""
|
# Используем closing если есть, иначе description
|
||||||
if self.description:
|
source = self.closing or self.description
|
||||||
clean_desc = self.description[:160].replace('\n', ' ').strip()
|
if source:
|
||||||
return f"Проект автоматизации: {self.title}. {clean_desc}..."
|
clean = source[:160].replace('\n', ' ').strip()
|
||||||
|
return f"Проект автоматизации: {self.title}. {clean}..."
|
||||||
return f"Реализация проекта {self.title} - программист 1С Николай Сердюк"
|
return f"Реализация проекта {self.title} - программист 1С Николай Сердюк"
|
||||||
|
|
||||||
def get_meta_keywords(self):
|
def get_meta_keywords(self):
|
||||||
"""Автоматические ключевые слова для проекта"""
|
|
||||||
base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"]
|
base_keywords = ["проект 1С", "автоматизация 1С", "внедрение 1С"]
|
||||||
title_words = self.title.lower().split()
|
title_words = self.title.lower().split()
|
||||||
return base_keywords + title_words
|
return base_keywords + title_words
|
||||||
@ -98,7 +113,7 @@ class Solution(models.Model):
|
|||||||
class Home(models.Model):
|
class Home(models.Model):
|
||||||
title = models.CharField(max_length=255, verbose_name='Наименование')
|
title = models.CharField(max_length=255, verbose_name='Наименование')
|
||||||
content = models.TextField(blank=True, verbose_name='Статья')
|
content = models.TextField(blank=True, verbose_name='Статья')
|
||||||
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото')
|
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", blank=True, null=True, verbose_name='Фото')
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||||
@ -118,8 +133,8 @@ class Home(models.Model):
|
|||||||
class CallbackRequest(models.Model):
|
class CallbackRequest(models.Model):
|
||||||
name = models.CharField(max_length=100, verbose_name='Имя')
|
name = models.CharField(max_length=100, verbose_name='Имя')
|
||||||
phone = models.CharField(max_length=20, verbose_name='Телефон')
|
phone = models.CharField(max_length=20, verbose_name='Телефон')
|
||||||
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным
|
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта')
|
||||||
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
|
question = models.TextField(blank=True, verbose_name='Ваш вопрос')
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
||||||
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
|
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
|
||||||
@ -134,16 +149,18 @@ class CallbackRequest(models.Model):
|
|||||||
ordering = ['-time_create']
|
ordering = ['-time_create']
|
||||||
|
|
||||||
|
|
||||||
# Сигнал для отправки уведомления при создании заявки
|
|
||||||
@receiver(post_save, sender=CallbackRequest)
|
@receiver(post_save, sender=CallbackRequest)
|
||||||
def send_callback_email_notification(sender, instance, created, **kwargs):
|
def send_callback_email_notification(sender, instance, created, **kwargs):
|
||||||
if created and not instance.notification_sent:
|
if created and not instance.notification_sent:
|
||||||
# Отправляем email уведомление
|
try:
|
||||||
success = send_callback_notification(instance)
|
success = send_callback_notification(instance)
|
||||||
if success:
|
if success:
|
||||||
instance.notification_sent = True
|
|
||||||
# Сохраняем без повторного вызова сигнала
|
|
||||||
sender.objects.filter(pk=instance.pk).update(notification_sent=True)
|
sender.objects.filter(pk=instance.pk).update(notification_sent=True)
|
||||||
|
except Exception:
|
||||||
|
# Логируем ошибку, но не прерываем сохранение объекта
|
||||||
|
logger.exception(
|
||||||
|
'Не удалось отправить email-уведомление для заявки pk=%s', instance.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PageView(models.Model):
|
class PageView(models.Model):
|
||||||
@ -190,7 +207,6 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def create_or_save_user_profile(sender, instance, created, **kwargs):
|
def create_or_save_user_profile(sender, instance, created, **kwargs):
|
||||||
# Получаем или создаём профиль, затем сохраняем
|
|
||||||
if created:
|
if created:
|
||||||
Profile.objects.get_or_create(user=instance)
|
Profile.objects.get_or_create(user=instance)
|
||||||
|
|
||||||
@ -200,5 +216,4 @@ def create_or_save_user_profile(sender, instance, created, **kwargs):
|
|||||||
@receiver([post_save, post_delete], sender=Competence)
|
@receiver([post_save, post_delete], sender=Competence)
|
||||||
@receiver([post_save, post_delete], sender=Recall)
|
@receiver([post_save, post_delete], sender=Recall)
|
||||||
def clear_sitemap_cache(sender, **kwargs):
|
def clear_sitemap_cache(sender, **kwargs):
|
||||||
"""Очищаем кэш sitemap при изменении контента"""
|
|
||||||
cache.delete('sitemap_cache')
|
cache.delete('sitemap_cache')
|
||||||
@ -1,10 +1,11 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from .models import PageView, Visitor
|
from .models import PageView, Visitor
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from typing import Type
|
logger = logging.getLogger(__name__)
|
||||||
from django.db.models import Model, QuerySet
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_ip(request: HttpRequest) -> str:
|
def get_client_ip(request: HttpRequest) -> str:
|
||||||
@ -22,13 +23,11 @@ def should_track_request(request: HttpRequest) -> bool:
|
|||||||
client_ip = get_client_ip(request)
|
client_ip = get_client_ip(request)
|
||||||
path = request.path
|
path = request.path
|
||||||
|
|
||||||
# Игнорируемые пути
|
|
||||||
ignored_paths = [
|
ignored_paths = [
|
||||||
'/static/', '/admin/', '/index.php', '/status.php',
|
'/static/', '/admin/', '/index.php', '/status.php',
|
||||||
'/cron', '/remote.php', '/ocs', '/apps/', '/custom_apps/',
|
'/cron', '/remote.php', '/ocs', '/apps/', '/custom_apps/',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Игнорируемые IP (Docker сети)
|
|
||||||
docker_ips = [
|
docker_ips = [
|
||||||
'192.168.64.1', '192.168.65.1',
|
'192.168.64.1', '192.168.65.1',
|
||||||
'172.17.0.1', '172.18.0.1', '172.19.0.1',
|
'172.17.0.1', '172.18.0.1', '172.19.0.1',
|
||||||
@ -48,7 +47,6 @@ def track_page_view(request: HttpRequest) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Сохраняем просмотр страницы
|
|
||||||
PageView.objects.create(
|
PageView.objects.create(
|
||||||
url=request.path,
|
url=request.path,
|
||||||
ip_address=get_client_ip(request),
|
ip_address=get_client_ip(request),
|
||||||
@ -56,13 +54,12 @@ def track_page_view(request: HttpRequest) -> None:
|
|||||||
referer=request.META.get('HTTP_REFERER', '')[:500],
|
referer=request.META.get('HTTP_REFERER', '')[:500],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Обновляем статистику посетителя
|
|
||||||
ip = get_client_ip(request)
|
ip = get_client_ip(request)
|
||||||
visitor, created = Visitor.objects.get_or_create(
|
visitor, created = Visitor.objects.get_or_create(
|
||||||
ip_address=ip,
|
ip_address=ip,
|
||||||
defaults={
|
defaults={
|
||||||
'first_visit': timezone.now(),
|
'first_visit': timezone.now(),
|
||||||
'last_visit': timezone.now()
|
'last_visit': timezone.now(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,26 +68,14 @@ def track_page_view(request: HttpRequest) -> None:
|
|||||||
visitor.visit_count += 1
|
visitor.visit_count += 1
|
||||||
visitor.save()
|
visitor.save()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# В продакшене лучше использовать логирование
|
logger.exception('Ошибка при трекинге просмотра страницы: %s', request.path)
|
||||||
print(f"Error tracking page view: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_published_queryset(model_class, order_by: str = '-time_create'):
|
def get_published_queryset(model_class, order_by: str = '-time_create'):
|
||||||
"""
|
"""
|
||||||
Возвращает QuerySet опубликованных записей для модели.
|
Возвращает QuerySet опубликованных записей для модели.
|
||||||
|
|
||||||
Args:
|
|
||||||
model_class: Класс модели Django
|
|
||||||
order_by: Поле для сортировки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
QuerySet или пустой список, если поле is_published отсутствует
|
|
||||||
"""
|
"""
|
||||||
if hasattr(model_class, 'is_published'):
|
if hasattr(model_class, 'is_published'):
|
||||||
return model_class.objects.filter(is_published=True).order_by(order_by)
|
return model_class.objects.filter(is_published=True).order_by(order_by)
|
||||||
return model_class.objects.none()
|
return model_class.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
194
programmer/static/programmer/css/solution-cards.css
Normal file
194
programmer/static/programmer/css/solution-cards.css
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/* ===== PROJECTS GRID ===== */
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== PROJECT CARD ===== */
|
||||||
|
.project-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Оранжевая полоска сверху — единственный декоративный слой */
|
||||||
|
.project-card__accent {
|
||||||
|
height: 3px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__body {
|
||||||
|
padding: 1.5rem 1.5rem 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__desc {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.65;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__footer {
|
||||||
|
padding: 1rem 1.5rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: gap 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__link:hover {
|
||||||
|
color: var(--primary-dark);
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MOBILE ===== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.projects-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобиле убираем hover-подъём — он не работает на touch */
|
||||||
|
.project-card:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Вместо этого — активное состояние при нажатии */
|
||||||
|
.project-card:active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__body {
|
||||||
|
padding: 1.25rem 1.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__footer {
|
||||||
|
padding: 0.75rem 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card__title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.projects-grid {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== СТИЛИ ДЛЯ СТРАНИЦЫ ДЕТАЛЬНОГО РЕШЕНИЯ ===== */
|
||||||
|
|
||||||
|
.solution-section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solution-section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-left: 5px solid var(--primary);
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Вложенные заголовки */
|
||||||
|
.section-content h2,
|
||||||
|
.section-content h3 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content h2 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
border-bottom: 1px dashed var(--border-light);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content ul,
|
||||||
|
.section-content ol {
|
||||||
|
margin: 1rem 0 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптация для мобильных */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.solution-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
.section-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,25 @@
|
|||||||
{% autoescape off %}
|
{% for solution in posts %}
|
||||||
{% for post in posts %}
|
<article class="project-card">
|
||||||
{% include 'programmer/includes/project_card.html' %}
|
<div class="project-card__accent"></div>
|
||||||
|
|
||||||
|
<div class="project-card__body">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<a href="{{ solution.get_absolute_url }}">{{ solution.title }}</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{% if solution.description %}
|
||||||
|
<p class="project-card__desc">{{ solution.description|striptags|truncatewords:25 }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-card__footer">
|
||||||
|
<a href="{{ solution.get_absolute_url }}" class="project-card__link">
|
||||||
|
Подробнее
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endautoescape %}
|
|
||||||
@ -30,6 +30,8 @@
|
|||||||
{% bootstrap_field form.specialization %}
|
{% bootstrap_field form.specialization %}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Зарегистрироваться</button>
|
<button type="submit" class="btn btn-primary" style="width: 100%;">Зарегистрироваться</button>
|
||||||
|
<!-- {{ form.as_p }} -->
|
||||||
|
{{ form.captcha }}
|
||||||
</form>
|
</form>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
|
|||||||
@ -1,29 +1,21 @@
|
|||||||
{% extends 'programmer/base.html' %}
|
{% extends 'programmer/base.html' %}
|
||||||
{% load django_bootstrap5 %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load seo_tags %}
|
{% load seo_tags %}
|
||||||
|
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link rel="stylesheet" href="{% static 'programmer/css/solution-accordion.css' %}">
|
<link rel="stylesheet" href="{% static 'programmer/css/solution-cards.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
|
||||||
<h1>{{ solution.title }}</h1>
|
|
||||||
<p>{{ solution.description }}</p>
|
|
||||||
<section>{{ solution.implementation|safe }}</section>
|
|
||||||
<footer>{{ solution.closing|safe }}</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Проекты автоматизации 1С</h1>
|
<h1 class="page-title">Проекты автоматизации 1С</h1>
|
||||||
<p class="page-subtitle">Реализованные решения и кейсы по автоматизации бизнес-процессов</p>
|
<p class="page-subtitle">Реализованные решения и кейсы по автоматизации бизнес-процессов</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="improved-list" id="projects-container">
|
<div class="projects-grid" id="projects-container">
|
||||||
{% include 'programmer/includes/project_cards.html' %}
|
{% include 'programmer/includes/project_cards.html' %}
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
{% if not posts %}
|
{% if not posts %}
|
||||||
<div class="content-card text-center">
|
<div class="content-card text-center">
|
||||||
@ -32,15 +24,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Индикатор загрузки -->
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<div id="loading-spinner" style="display: none; text-align: center; margin: 20px 0;">
|
<div id="loading-spinner" style="display: none; text-align: center; margin: 2rem 0;">
|
||||||
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div>
|
<div class="spinner-border text-primary" role="status">
|
||||||
<p>Загрузка проектов...</p>
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 0.5rem; color: var(--text-secondary);">Загрузка проектов...</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Данные пагинации -->
|
|
||||||
<script>
|
<script>
|
||||||
window.currentPage = {{ page_obj.number }};
|
window.currentPage = {{ page_obj.number }};
|
||||||
window.totalPages = {{ paginator.num_pages }};
|
window.totalPages = {{ paginator.num_pages }};
|
||||||
@ -49,7 +41,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{% static 'programmer/js/solution-accordion.js' %}"></script>
|
|
||||||
<script src="{% static 'programmer/js/infinite_scroll.js' %}"></script>
|
<script src="{% static 'programmer/js/infinite_scroll.js' %}"></script>
|
||||||
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
|
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
109
programmer/templates/programmer/solution_detail.html
Normal file
109
programmer/templates/programmer/solution_detail.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{% extends 'programmer/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load seo_tags %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'programmer/css/solution-cards.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
{# Хлебные крошки #}
|
||||||
|
{% if breadcrumbs %}
|
||||||
|
<nav class="breadcrumbs" aria-label="Навигация">
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
{% if crumb.url_name %}
|
||||||
|
<a href="{% url crumb.url_name %}" class="breadcrumb-link">{{ crumb.title }}</a>
|
||||||
|
<span class="breadcrumb-separator">/</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="breadcrumb-current">{{ crumb.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Заголовок страницы #}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">{{ solution.title }}</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
Опубликовано: {{ solution.time_create|date:"d.m.Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Основное содержимое #}
|
||||||
|
<article class="content-card">
|
||||||
|
{# Секция: Описание задачи #}
|
||||||
|
<section class="solution-section">
|
||||||
|
<h2 class="section-title">📋 Описание задачи</h2>
|
||||||
|
<div class="section-content">
|
||||||
|
{{ solution.description|safe }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{# Секция: Описание решения #}
|
||||||
|
<section class="solution-section">
|
||||||
|
<h2 class="section-title">🔧 Описание решения</h2>
|
||||||
|
<div class="section-content">
|
||||||
|
{{ solution.implementation|safe }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{# Секция: Результат #}
|
||||||
|
<section class="solution-section">
|
||||||
|
<h2 class="section-title">✅ Результат</h2>
|
||||||
|
<div class="section-content">
|
||||||
|
{{ solution.closing|safe }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{# Кнопка возврата #}
|
||||||
|
<div style="margin-top: 2rem; text-align: center;">
|
||||||
|
<a href="{% url 'solution' %}" class="btn btn-outline">
|
||||||
|
← Вернуться к списку проектов
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function toggleAccordion(header) {
|
||||||
|
const content = header.nextElementSibling;
|
||||||
|
const icon = header.querySelector('.accordion-icon');
|
||||||
|
|
||||||
|
// Закрываем все другие открытые элементы в этом аккордеоне (опционально)
|
||||||
|
const accordion = header.closest('.solution-accordion');
|
||||||
|
if (accordion) {
|
||||||
|
accordion.querySelectorAll('.accordion-header').forEach(otherHeader => {
|
||||||
|
if (otherHeader !== header) {
|
||||||
|
const otherContent = otherHeader.nextElementSibling;
|
||||||
|
const otherIcon = otherHeader.querySelector('.accordion-icon');
|
||||||
|
otherContent.classList.remove('active');
|
||||||
|
otherHeader.classList.remove('active');
|
||||||
|
if (otherIcon) otherIcon.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переключаем текущий элемент
|
||||||
|
content.classList.toggle('active');
|
||||||
|
header.classList.toggle('active');
|
||||||
|
|
||||||
|
if (content.classList.contains('active')) {
|
||||||
|
icon.style.transform = 'rotate(180deg)';
|
||||||
|
} else {
|
||||||
|
icon.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация: первый элемент открыт по умолчанию
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const firstHeader = document.querySelector('.accordion-header');
|
||||||
|
if (firstHeader) {
|
||||||
|
toggleAccordion(firstHeader);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'programmer/js/floating-button.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
@ -14,7 +14,7 @@ urlpatterns = [
|
|||||||
path('', views.HomePageView.as_view(), name='home'),
|
path('', views.HomePageView.as_view(), name='home'),
|
||||||
path('about/', views.AboutPageView.as_view(), name='about'),
|
path('about/', views.AboutPageView.as_view(), name='about'),
|
||||||
path('solutions/', views.SolutionListView.as_view(), name='solution'),
|
path('solutions/', views.SolutionListView.as_view(), name='solution'),
|
||||||
path('solutions/<int:pk>/', SolutionDetailView.as_view(), name='solution_detail'),
|
path('solutions/<slug:slug>/', SolutionDetailView.as_view(), name='solution_detail'),
|
||||||
# path('competence/', ability, name='ability'),
|
# path('competence/', ability, name='ability'),
|
||||||
# path('competence/<int:pk>/', CompetenceDetailView.as_view(), name='competence_detail'),
|
# path('competence/<int:pk>/', CompetenceDetailView.as_view(), name='competence_detail'),
|
||||||
path('recall/', views.RecallListView.as_view(), name='recall'),
|
path('recall/', views.RecallListView.as_view(), name='recall'),
|
||||||
|
|||||||
@ -229,6 +229,10 @@ class RegisterView(PageViewTrackingMixin, MenuContextMixin, SuccessMessageMixin,
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = kwargs.pop('request', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ProfileEditView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin, UpdateView):
|
class ProfileEditView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, BreadcrumbMixin, UpdateView):
|
||||||
model = Profile
|
model = Profile
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user