Убрал комментарии слияния
This commit is contained in:
parent
decb9471bd
commit
1219966c84
5
.gitignore
vendored
5
.gitignore
vendored
@ -22,3 +22,8 @@ ENV/
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
*.log
|
||||
*.pot
|
||||
*.py[co]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.contrib import admin
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
@ -168,174 +167,3 @@ admin.site.register(Competence, ProgrammerAdmin)
|
||||
admin.site.register(Recall, RecallAdmin)
|
||||
admin.site.register(Solution, SolutionAdmin)
|
||||
admin.site.register(Home, HomeAdmin)
|
||||
=======
|
||||
from django.contrib import admin
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.urls import path
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib import messages
|
||||
from .models import *
|
||||
|
||||
|
||||
class ProgrammerAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
|
||||
class RecallAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'description', 'implementation')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
|
||||
class HomeAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
|
||||
@admin.register(CallbackRequest)
|
||||
class CallbackAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge')
|
||||
list_display_links = ('name', 'phone')
|
||||
list_editable = ('is_processed', 'is_read')
|
||||
list_filter = ('time_create', 'is_processed', 'is_read')
|
||||
search_fields = ('name', 'phone', 'email')
|
||||
readonly_fields = ('time_create',)
|
||||
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed']
|
||||
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed', 'resend_notification']
|
||||
|
||||
def resend_notification(self, request, queryset):
|
||||
from .utils.email_notifications import send_callback_notification
|
||||
count = 0
|
||||
for callback in queryset:
|
||||
success = send_callback_notification(callback)
|
||||
if success:
|
||||
count += 1
|
||||
self.message_user(request, f'Уведомления отправлены для {count} заявок')
|
||||
|
||||
resend_notification.short_description = "Переотправить email уведомления"
|
||||
|
||||
def new_badge(self, obj):
|
||||
if not obj.is_read:
|
||||
return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>')
|
||||
return ""
|
||||
|
||||
new_badge.short_description = 'Статус'
|
||||
|
||||
def get_queryset(self, request):
|
||||
# Показываем количество непрочитанных в заголовке
|
||||
unread_count = CallbackRequest.objects.filter(is_read=False).count()
|
||||
if unread_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
f'У вас {unread_count} непрочитанных заявок!',
|
||||
messages.WARNING
|
||||
)
|
||||
return super().get_queryset(request)
|
||||
|
||||
def mark_as_read(self, request, queryset):
|
||||
updated = queryset.update(is_read=True)
|
||||
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
||||
|
||||
mark_as_read.short_description = "Отметить как прочитанные"
|
||||
|
||||
def mark_as_unread(self, request, queryset):
|
||||
updated = queryset.update(is_read=False)
|
||||
self.message_user(request, f'{updated} заявок отмечены как непрочитанные')
|
||||
|
||||
mark_as_unread.short_description = "Отметить как непрочитанные"
|
||||
|
||||
def mark_as_processed(self, request, queryset):
|
||||
updated = queryset.update(is_processed=True)
|
||||
self.message_user(request, f'{updated} заявок отмечены как обработанные')
|
||||
|
||||
mark_as_processed.short_description = "Отметить как обработанные"
|
||||
|
||||
# Добавляем кастомное представление для статистики
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def callback_stats(self, request):
|
||||
today = timezone.now().date()
|
||||
week_ago = today - timezone.timedelta(days=7)
|
||||
|
||||
stats = {
|
||||
'total': CallbackRequest.objects.count(),
|
||||
'today': CallbackRequest.objects.filter(time_create__date=today).count(),
|
||||
'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(),
|
||||
'unread': CallbackRequest.objects.filter(is_read=False).count(),
|
||||
'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(),
|
||||
}
|
||||
|
||||
context = {
|
||||
**self.admin_site.each_context(request),
|
||||
'title': 'Статистика заявок',
|
||||
'stats': stats,
|
||||
}
|
||||
return render(request, 'admin/callback_stats.html', context)
|
||||
|
||||
class ProgrammerAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
class RecallAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'description', 'implementation')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
class HomeAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||
list_display_links = ('id', 'title')
|
||||
search_fields = ('title', 'content')
|
||||
list_editable = ('is_published',)
|
||||
list_filter = ('time_create', 'is_published')
|
||||
|
||||
@admin.register(PageView)
|
||||
class PageViewAdmin(admin.ModelAdmin):
|
||||
list_display = ['url', 'timestamp', 'ip_address']
|
||||
list_filter = ['timestamp', 'url']
|
||||
search_fields = ['url', 'ip_address']
|
||||
date_hierarchy = 'timestamp'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).order_by('-timestamp')
|
||||
|
||||
admin.site.register(Competence, ProgrammerAdmin)
|
||||
admin.site.register(Recall, RecallAdmin)
|
||||
admin.site.register(Solution, SolutionAdmin)
|
||||
admin.site.register(Home, HomeAdmin)
|
||||
>>>>>>> master
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -6,12 +5,3 @@ class ProgrammerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'programmer'
|
||||
verbose_name = 'Программисты'
|
||||
=======
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProgrammerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'programmer'
|
||||
verbose_name = 'Программисты'
|
||||
>>>>>>> master
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from .views import menu
|
||||
from django.conf import settings
|
||||
|
||||
@ -10,17 +9,4 @@ def contact_info(request):
|
||||
return {
|
||||
'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'),
|
||||
'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'),
|
||||
=======
|
||||
from .views import menu
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def menu_processor(request):
|
||||
return {'menu': menu}
|
||||
|
||||
def contact_info(request):
|
||||
return {
|
||||
'CONTACT_EMAIL': getattr(settings, 'CONTACT_EMAIL', 'it@nserdyuk.ru'),
|
||||
'CONTACT_PHONE': getattr(settings, 'CONTACT_PHONE', '+7 (960) 469-40-88'),
|
||||
>>>>>>> master
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django import forms
|
||||
from .models import CallbackRequest
|
||||
|
||||
@ -31,38 +30,4 @@ class CallbackForm(forms.ModelForm):
|
||||
'phone': 'Телефон',
|
||||
'email': 'Электронная почта',
|
||||
'question': 'Ваш вопрос'
|
||||
=======
|
||||
from django import forms
|
||||
from .models import CallbackRequest
|
||||
|
||||
|
||||
class CallbackForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CallbackRequest
|
||||
fields = ['name', 'phone', 'email', 'question']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-input',
|
||||
'placeholder': 'Ваше имя'
|
||||
}),
|
||||
'phone': forms.TextInput(attrs={
|
||||
'class': 'form-input',
|
||||
'placeholder': '+7 (___) ___-__-__'
|
||||
}),
|
||||
'email': forms.EmailInput(attrs={
|
||||
'class': 'form-input',
|
||||
'placeholder': 'your@email.com'
|
||||
}),
|
||||
'question': forms.Textarea(attrs={
|
||||
'class': 'form-textarea',
|
||||
'placeholder': 'Опишите ваш вопрос или задачу...',
|
||||
'rows': 4
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'name': 'Имя',
|
||||
'phone': 'Телефон',
|
||||
'email': 'Электронная почта',
|
||||
'question': 'Ваш вопрос'
|
||||
>>>>>>> master
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from .models import PageView, Visitor
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
@ -52,59 +51,4 @@ class PageViewMiddleware:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
=======
|
||||
from .models import PageView, Visitor
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class PageViewMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# Игнорируем статические файлы и админку
|
||||
if not request.path.startswith('/static/') and not request.path.startswith('/admin/'):
|
||||
self.track_page_view(request)
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
def track_page_view(self, request):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Сохраняем просмотр страницы
|
||||
PageView.objects.create(
|
||||
url=request.path,
|
||||
ip_address=self.get_client_ip(request),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', ''),
|
||||
referer=request.META.get('HTTP_REFERER', '')
|
||||
)
|
||||
|
||||
# Обновляем статистику посетителя
|
||||
ip = self.get_client_ip(request)
|
||||
visitor, created = Visitor.objects.get_or_create(
|
||||
ip_address=ip,
|
||||
defaults={
|
||||
'first_visit': timezone.now(),
|
||||
'last_visit': timezone.now()
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
visitor.last_visit = timezone.now()
|
||||
visitor.visit_count += 1
|
||||
visitor.save()
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку, но не прерываем выполнение
|
||||
print(f"Error tracking page view: {e}")
|
||||
|
||||
def get_client_ip(self, request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
>>>>>>> master
|
||||
return ip
|
||||
@ -1,159 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.core.cache import cache
|
||||
from .utils.email_notifications import send_callback_notification
|
||||
|
||||
|
||||
class Recall(models.Model):
|
||||
title = models.CharField(max_length=255, verbose_name='Организация')
|
||||
content = models.TextField(blank=True, verbose_name='Отзыв')
|
||||
scan = models.ImageField(upload_to="scan/%Y/%m/%d/", verbose_name='Фото')
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('post', kwargs={'post_id': self.pk})
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Отзыв'
|
||||
verbose_name_plural = 'Отзывы'
|
||||
ordering = ['time_create', 'title']
|
||||
|
||||
|
||||
class Competence(models.Model):
|
||||
title = models.CharField(max_length=255, verbose_name='Программист')
|
||||
content = models.TextField(blank=True, verbose_name='Компетенция')
|
||||
photo = models.ImageField(upload_to="photos/%Y/%m/%d/", verbose_name='Фото')
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('post', kwargs={'post_id': self.pk})
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Компетенция'
|
||||
verbose_name_plural = 'Компетенции'
|
||||
ordering = ['time_create', 'title']
|
||||
|
||||
|
||||
class Solution(models.Model):
|
||||
title = models.CharField(max_length=255, verbose_name='Наименование')
|
||||
description = models.TextField(blank=True, verbose_name='Описание')
|
||||
implementation = models.TextField(blank=True, verbose_name='Реализация')
|
||||
closing = models.TextField(blank=True, verbose_name='Заключение')
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('post', kwargs={'post_id': self.pk})
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Проекты'
|
||||
verbose_name_plural = 'Проекты'
|
||||
ordering = ['time_create', 'title']
|
||||
|
||||
|
||||
class Home(models.Model):
|
||||
title = models.CharField(max_length=255, verbose_name='Наименование')
|
||||
content = models.TextField(blank=True, verbose_name='Статья')
|
||||
home_image = models.ImageField(upload_to="home_image/%Y/%m/%d/", verbose_name='Фото')
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||
time_update = models.DateTimeField(auto_now=True, verbose_name='Дата изменения')
|
||||
is_published = models.BooleanField(default=True, verbose_name='Опубликован')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('post', kwargs={'post_id': self.pk})
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Главная страница'
|
||||
verbose_name_plural = 'Главная страница'
|
||||
ordering = ['time_create', 'title']
|
||||
|
||||
|
||||
class CallbackRequest(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name='Имя')
|
||||
phone = models.CharField(max_length=20, verbose_name='Телефон')
|
||||
email = models.EmailField(blank=True, null=True, verbose_name='Электронная почта') # Сделать необязательным
|
||||
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
||||
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
|
||||
notification_sent = models.BooleanField(default=False, verbose_name='Уведомление отправлено')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.phone}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Заявка на звонок'
|
||||
verbose_name_plural = 'Заявки на звонок'
|
||||
ordering = ['-time_create']
|
||||
|
||||
|
||||
# Сигнал для отправки уведомления при создании заявки
|
||||
@receiver(post_save, sender=CallbackRequest)
|
||||
def send_callback_email_notification(sender, instance, created, **kwargs):
|
||||
if created and not instance.notification_sent:
|
||||
# Отправляем email уведомление
|
||||
success = send_callback_notification(instance)
|
||||
if success:
|
||||
instance.notification_sent = True
|
||||
# Сохраняем без повторного вызова сигнала
|
||||
sender.objects.filter(pk=instance.pk).update(notification_sent=True)
|
||||
|
||||
|
||||
class PageView(models.Model):
|
||||
url = models.CharField(max_length=500)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
ip_address = models.GenericIPAddressField()
|
||||
user_agent = models.TextField(blank=True)
|
||||
referer = models.CharField(max_length=500, blank=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['url', 'timestamp']),
|
||||
models.Index(fields=['timestamp']),
|
||||
]
|
||||
|
||||
|
||||
class Visitor(models.Model):
|
||||
ip_address = models.GenericIPAddressField()
|
||||
first_visit = models.DateTimeField(default=timezone.now)
|
||||
last_visit = models.DateTimeField(default=timezone.now)
|
||||
visit_count = models.IntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['ip_address']),
|
||||
]
|
||||
|
||||
|
||||
@receiver([post_save, post_delete], sender=Home)
|
||||
@receiver([post_save, post_delete], sender=Solution)
|
||||
@receiver([post_save, post_delete], sender=Competence)
|
||||
@receiver([post_save, post_delete], sender=Recall)
|
||||
def clear_sitemap_cache(sender, **kwargs):
|
||||
"""Очищаем кэш sitemap при изменении контента"""
|
||||
cache.delete('sitemap_cache')
|
||||
=======
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@ -334,4 +178,3 @@ class Visitor(models.Model):
|
||||
def clear_sitemap_cache(sender, **kwargs):
|
||||
"""Очищаем кэш sitemap при изменении контента"""
|
||||
cache.delete('sitemap_cache')
|
||||
>>>>>>> master
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
from .models import Home, Solution, Competence, Recall
|
||||
@ -62,69 +61,4 @@ sitemaps = {
|
||||
'solutions': SolutionSitemap,
|
||||
'competence': CompetenceSitemap,
|
||||
'recall': RecallSitemap,
|
||||
=======
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
from .models import Home, Solution, Competence, Recall
|
||||
|
||||
class StaticViewSitemap(Sitemap):
|
||||
priority = 1.0
|
||||
changefreq = 'monthly'
|
||||
|
||||
def items(self):
|
||||
return ['home', 'about', 'solution', 'ability', 'recall']
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
|
||||
class HomeSitemap(Sitemap):
|
||||
changefreq = 'weekly'
|
||||
priority = 1.0
|
||||
|
||||
def items(self):
|
||||
return Home.objects.filter(is_published=True)
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.time_update
|
||||
|
||||
# УБИРАЕМ метод location - используем default
|
||||
# Django автоматически сгенерирует правильные URL
|
||||
|
||||
class SolutionSitemap(Sitemap):
|
||||
changefreq = 'weekly'
|
||||
priority = 0.9
|
||||
|
||||
def items(self):
|
||||
return Solution.objects.filter(is_published=True)
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.time_update
|
||||
|
||||
class CompetenceSitemap(Sitemap):
|
||||
changefreq = 'monthly'
|
||||
priority = 0.8
|
||||
|
||||
def items(self):
|
||||
return Competence.objects.filter(is_published=True)
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.time_update
|
||||
|
||||
class RecallSitemap(Sitemap):
|
||||
changefreq = 'monthly'
|
||||
priority = 0.7
|
||||
|
||||
def items(self):
|
||||
return Recall.objects.filter(is_published=True)
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.time_update
|
||||
|
||||
# Упрощаем sitemaps - убираем HomeSitemap если он дублирует главную
|
||||
sitemaps = {
|
||||
'static': StaticViewSitemap,
|
||||
'solutions': SolutionSitemap,
|
||||
'competence': CompetenceSitemap,
|
||||
'recall': RecallSitemap,
|
||||
>>>>>>> master
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
@ -77,84 +76,4 @@
|
||||
|
||||
{% bootstrap_javascript %}
|
||||
</body>
|
||||
=======
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Админ-панель - Статистика{% endblock %}</title>
|
||||
{% load django_bootstrap5 %}
|
||||
{% bootstrap_css %}
|
||||
<style>
|
||||
body {
|
||||
background: #f8f9fa;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.admin-header {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin: 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-header">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1>{% block page_title %}Админ-панель{% endblock %}</h1>
|
||||
<div>
|
||||
<a href="{% url 'home' %}" class="btn btn-outline-light btn-sm">На сайт</a>
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-outline-light btn-sm">Django Admin</a>
|
||||
<a href="{% url 'admin:logout' %}" class="btn btn-outline-light btn-sm">Выйти</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% bootstrap_messages %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% bootstrap_javascript %}
|
||||
</body>
|
||||
>>>>>>> master
|
||||
</html>
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
@ -24,31 +23,4 @@
|
||||
<a href="{% url 'callback_stats' %}">📊 Статистика заявок</a> /
|
||||
<a href="{% url 'statistics' %}">📈 Посещения</a> /
|
||||
{{ block.super }}
|
||||
=======
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav-global %}
|
||||
{% endblock %}
|
||||
|
||||
{% block userlinks %}
|
||||
{% load programmer_tags %}
|
||||
|
||||
<!-- Уведомление о новых заявках -->
|
||||
{% get_unread_callbacks as unread_callbacks %}
|
||||
{% if unread_callbacks %}
|
||||
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" style="color: #dc3545; font-weight: bold;">
|
||||
🚨 {{ unread_callbacks }} новых заявок
|
||||
</a> /
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'callback_stats' %}">📊 Статистика заявок</a> /
|
||||
<a href="{% url 'statistics' %}">📈 Посещения</a> /
|
||||
{{ block.super }}
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block title %}Статистика заявок{% endblock %}
|
||||
@ -43,50 +42,4 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
=======
|
||||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block title %}Статистика заявок{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="module">
|
||||
<h1>📊 Статистика заявок на обратный звонок</h1>
|
||||
|
||||
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;">
|
||||
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||
<h3>📋 Всего заявок</h3>
|
||||
<p style="font-size: 2rem; font-weight: bold; color: #007bff; margin: 0;">{{ stats.total }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||
<h3>📅 Сегодня</h3>
|
||||
<p style="font-size: 2rem; font-weight: bold; color: #28a745; margin: 0;">{{ stats.today }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||
<h3>📈 За неделю</h3>
|
||||
<p style="font-size: 2rem; font-weight: bold; color: #17a2b8; margin: 0;">{{ stats.week }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||
<h3>🆕 Непрочитанные</h3>
|
||||
<p style="font-size: 2rem; font-weight: bold; color: #dc3545; margin: 0;">{{ stats.unread }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||
<h3>⏳ В обработке</h3>
|
||||
<p style="font-size: 2rem; font-weight: bold; color: #ffc107; margin: 0;">{{ stats.unprocessed }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="button" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">
|
||||
📋 Перейти к списку заявок
|
||||
</a>
|
||||
<a href="{% url 'admin:index' %}" class="button" style="background: #6c757d; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin-left: 10px;">
|
||||
🏠 На главную админки
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'admin/base.html' %}
|
||||
{% load programmer_tags %}
|
||||
|
||||
@ -129,136 +128,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
=======
|
||||
{% extends 'admin/base.html' %}
|
||||
{% load programmer_tags %}
|
||||
|
||||
{% block title %}Статистика посещений{% endblock %}
|
||||
{% block page_title %}Статистика посещений{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Уведомления о заявках -->
|
||||
{% get_unread_callbacks as unread_callbacks %}
|
||||
{% get_today_callbacks as today_callbacks %}
|
||||
{% if unread_callbacks > 0 %}
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
<h4>🚨 Внимание!</h4>
|
||||
<p>
|
||||
У вас <strong>{{ unread_callbacks }}</strong> непрочитанных заявок на обратный звонок
|
||||
{% if today_callbacks > 0 %}
|
||||
({{ today_callbacks }} из них сегодня)
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="btn btn-warning btn-sm">
|
||||
📋 Перейти к заявкам
|
||||
</a>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>📊 Просмотров сегодня</h3>
|
||||
<p class="stat-number">{{ today_views }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>📈 Просмотров за неделю</h3>
|
||||
<p class="stat-number">{{ weekly_views }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>👥 Уникальных посетителей</h3>
|
||||
<p class="stat-number">{{ unique_visitors }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>🕒 Всего просмотров</h3>
|
||||
<p class="stat-number">{{ total_views }}</p>
|
||||
</div>
|
||||
<!-- Добавляем статистику по заявкам -->
|
||||
<div class="stat-card">
|
||||
<h3>📞 Заявок сегодня</h3>
|
||||
<p class="stat-number">{{ today_callbacks }}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>📋 Всего заявок</h3>
|
||||
<p class="stat-number">{% get_unread_callbacks %}/{{ total_callbacks }}</p>
|
||||
<small>(непрочитанные/всего)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Остальной код статистики остается без изменений -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔥 Популярные страницы (за неделю)</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Страница</th>
|
||||
<th>Просмотров</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for page in popular_pages %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ page.url }}</code>
|
||||
{% if page.url == '/' %}
|
||||
<span class="badge bg-primary">Главная</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><strong>{{ page.views }}</strong></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Нет данных за выбранный период</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Последние посещения</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Время</th>
|
||||
<th>Страница</th>
|
||||
<th>IP-адрес</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for view in recent_views %}
|
||||
<tr>
|
||||
<td>{{ view.timestamp|date:"d.m.Y H:i" }}</td>
|
||||
<td><code>{{ view.url }}</code></td>
|
||||
<td><small>{{ view.ip_address }}</small></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">Нет данных</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- templates/emails/callback_notification.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -55,62 +54,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
=======
|
||||
<!-- templates/emails/callback_notification.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #FF6B00 0%, #0055A5 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; }
|
||||
.content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; }
|
||||
.alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }
|
||||
.info-box { background: white; padding: 15px; border-radius: 5px; margin: 10px 0; border-left: 4px solid #007bff; }
|
||||
.btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }
|
||||
.footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚨 Новая заявка на сайте</h1>
|
||||
<p>Требуется ваше внимание!</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="alert">
|
||||
<strong>⚠️ Срочно!</strong> Пользователь оставил заявку на обратный звонок.
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>📋 Информация о заявке:</h3>
|
||||
<p><strong>👤 Имя:</strong> {{ callback.name }}</p>
|
||||
<p><strong>📞 Телефон:</strong> {{ callback.phone }}</p>
|
||||
{% if callback.email %}
|
||||
<p><strong>📧 Email:</strong> {{ callback.email }}</p>
|
||||
{% endif %}
|
||||
{% if callback.question %}
|
||||
<p><strong>❓ Вопрос:</strong><br>{{ callback.question }}</p>
|
||||
{% endif %}
|
||||
<p><strong>🕒 Время отправки:</strong> {{ callback.time_create|date:"d.m.Y H:i" }}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn">
|
||||
📋 Перейти к заявкам в админке
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p><em>Не забудьте отметить заявку как обработанную после связи с клиентом!</em></p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Это автоматическое уведомление от системы сайта.</p>
|
||||
<p>Если вы получили это письмо по ошибке, пожалуйста, свяжитесь с администратором.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
>>>>>>> master
|
||||
</html>
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- templates/emails/daily_summary.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -58,65 +57,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
=======
|
||||
<!-- templates/emails/daily_summary.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0; }
|
||||
.content { background: #f9f9f9; padding: 20px; border-radius: 0 0 10px 10px; }
|
||||
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; }
|
||||
.stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.stat-number { font-size: 2rem; font-weight: bold; margin: 0; }
|
||||
.alert { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }
|
||||
.btn { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }
|
||||
.footer { text-align: center; margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📊 Ежедневная сводка</h1>
|
||||
<p>Статистика заявок за {{ yesterday|date:"d.m.Y" }}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>📅 Вчерашние заявки</h3>
|
||||
<p class="stat-number" style="color: #28a745;">{{ yesterday_callbacks }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>⏳ Ожидают обработки</h3>
|
||||
<p class="stat-number" style="color: #ffc107;">{{ unprocessed_callbacks }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if unprocessed_callbacks > 0 %}
|
||||
<div class="alert">
|
||||
<strong>⚠️ Внимание!</strong> У вас есть {{ unprocessed_callbacks }} необработанных заявок.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<a href="http://{{ site_url }}/admin/programmer/callbackrequest/" class="btn">
|
||||
📋 Управление заявками
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p><em>Не забудьте обработать все pending заявки!</em></p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Ежедневная автоматическая сводка от системы сайта.</p>
|
||||
<p>Дата формирования: {{ today|date:"d.m.Y H:i" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
>>>>>>> master
|
||||
</html>
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
@ -152,159 +151,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{title}}</h1>
|
||||
<p class="page-subtitle">Профессиональный программист 1С с более чем 10-летним опытом</p>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<div class="about-header">
|
||||
<h2>Николай Сердюк</h2>
|
||||
<p class="subtitle">Разработчик 1С</p>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>🚀 Опыт работы</h3>
|
||||
<p class="card-subtitle">Более 10 лет успешной работы в разработке и сопровождении систем на платформе 1С</p>
|
||||
|
||||
<div class="experience-item mt-3">
|
||||
<h4>Основные направления:</h4>
|
||||
<div class="skills-grid">
|
||||
<div class="skill-category">
|
||||
<h4>💻 Разработка</h4>
|
||||
<ul>
|
||||
<li>Разработка и доработка конфигураций 1С</li>
|
||||
<li>Создание внешних обработок и отчетов</li>
|
||||
<li>Кастомизация под бизнес-процессы</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<h4>🔗 Интеграция</h4>
|
||||
<ul>
|
||||
<li>Интеграция 1С с веб-сервисами</li>
|
||||
<li>Связь с сайтами и мобильными приложениями</li>
|
||||
<li>API и веб-сервисы</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<h4>⚡ Оптимизация</h4>
|
||||
<ul>
|
||||
<li>Оптимизация бизнес-процессов</li>
|
||||
<li>Ускорение работы баз данных</li>
|
||||
<li>Автоматизация рутинных операций</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>🛠 Технологии и навыки</h3>
|
||||
<div class="skills-grid">
|
||||
<div class="skill-category">
|
||||
<h4>🎯 1С Разработка</h4>
|
||||
<ul>
|
||||
<li>1С:Предприятие 8.3</li>
|
||||
<li>Управление торговлей</li>
|
||||
<li>Бухгалтерия предприятия</li>
|
||||
<li>Зарплата и управление персоналом</li>
|
||||
<li>Внешние обработки и отчеты</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<h4>🔧 Дополнительные технологии</h4>
|
||||
<ul>
|
||||
<li>SQL и оптимизация запросов</li>
|
||||
<li>Веб-сервисы и API</li>
|
||||
<li>XML, JSON, REST</li>
|
||||
<li>Системное администрирование</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>📈 Проекты и достижения</h3>
|
||||
<p class="card-subtitle">Успешно реализовал более 50 проектов различной сложности</p>
|
||||
|
||||
<div class="skills-grid mt-3">
|
||||
<div class="skill-category">
|
||||
<h4>🏆 Ключевые проекты</h4>
|
||||
<ul>
|
||||
<li>Автоматизация учетных систем для предприятий</li>
|
||||
<li>Интеграция 1С с сайтами и мобильными приложениями</li>
|
||||
<li>Разработка кастомизированных отчетов и дашбордов</li>
|
||||
<li>Оптимизация производительности баз данных</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-section">
|
||||
<h3>📞 Контакты</h3>
|
||||
<div class="contacts">
|
||||
<div class="skills-grid">
|
||||
<div class="skill-category">
|
||||
<h4>📧 Электронная почта</h4>
|
||||
<p><strong>{{ CONTACT_EMAIL }}</strong></p>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<h4>📱 Телефон</h4>
|
||||
<p><strong>{{ CONTACT_PHONE }}</strong></p>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<h4>💬 Telegram</h4>
|
||||
<p><strong><a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="display: inline-flex; padding: 0.5rem 1rem;">@odinesina_prog</a></strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'solution' %}" class="btn btn-primary">📂 Посмотреть мои проекты</a>
|
||||
<a href="{% url 'recall' %}" class="btn btn-secondary">⭐ Отзывы клиентов</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Service",
|
||||
"serviceType": "1С программирование",
|
||||
"provider": {
|
||||
"@type": "Person",
|
||||
"name": "Николай Сердюк"
|
||||
},
|
||||
"areaServed": "Россия",
|
||||
"hasOfferCatalog": {
|
||||
"@type": "OfferCatalog",
|
||||
"name": "Услуги программиста 1С",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "Разработка конфигураций 1С"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "Интеграция 1С с веб-сервисами"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,559 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% load static %}
|
||||
{% load programmer_tags %}
|
||||
{% load django_bootstrap5 %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<title>{{title}} - Программист 1С</title>
|
||||
<!-- Основные мета-теги -->
|
||||
<meta name="description" content="{% block meta_description %}Профессиональный программист 1С с более чем 10-летним опытом. Разработка, интеграция и оптимизация систем 1С.{% endblock %}">
|
||||
<meta name="keywords" content="{% block meta_keywords %}программист 1С, разработка 1С, интеграция 1С, оптимизация 1С, 1С предприятие{% endblock %}">
|
||||
<meta name="author" content="Николай Сердюк">
|
||||
|
||||
<!-- Open Graph для соцсетей -->
|
||||
<meta property="og:title" content="{{title}} - Программист 1С">
|
||||
<meta property="og:description" content="Профессиональный программист 1С с более чем 10-летним опытом">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{% static 'programmer/images/og-image.jpg' %}">
|
||||
<meta property="og:site_name" content="Программист 1С - Николай Сердюк">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{title}} - Программист 1С">
|
||||
<meta name="twitter:description" content="Профессиональный программист 1С с более чем 10-летним опытом">
|
||||
<meta name="twitter:image" content="{% static 'programmer/images/og-image.jpg' %}">
|
||||
|
||||
<!-- Дополнительные SEO-теги -->
|
||||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
|
||||
<link rel="canonical" href="{{ request.build_absolute_uri }}">
|
||||
{% bootstrap_css %}
|
||||
|
||||
<!-- Основной CSS файл (темная тема по умолчанию) -->
|
||||
<link type="text/css" href="{% static 'programmer/css/styles_w.css' %}" rel="stylesheet" id="theme-css" />
|
||||
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<link rel="shortcut icon" href="{% static 'programmer/images/main.ico' %}" type="image/x-icon">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* Временные стили для тумблера и мобильного меню */
|
||||
.theme-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-toggle-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle-label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background: #222738;
|
||||
border: 2px solid #2D3447;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.theme-toggle-slider {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #FF6B00;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
left: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.theme-toggle-checkbox:checked + .theme-toggle-label .theme-toggle-slider {
|
||||
transform: translateX(30px);
|
||||
background: #0055A5;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.theme-icon.sun {
|
||||
left: 8px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theme-icon.moon {
|
||||
right: 8px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.sun {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-toggle-checkbox:checked + .theme-toggle-label .theme-icon.moon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Мобильное меню */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-btn:hover {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.mobile-menu-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: var(--bg-card);
|
||||
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3);
|
||||
transition: right 0.3s ease;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-menu.active {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.mobile-menu-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-close:hover {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.mobile-nav-menu {
|
||||
list-style: none;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mobile-nav-link:hover,
|
||||
.mobile-nav-link.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mobile-nav-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* ===== MOBILE RESPONSIVE STYLES ===== */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.125rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.125rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.modern-card {
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.competence-item {
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.competence-scan-container {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.recall-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.recall-meta {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-contacts p {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin: 5% auto;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.modern-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.competence-scan-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Дополнительные CSS файлы для конкретных страниц -->
|
||||
{% endblock %}
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-X3W9YSQHRM"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-X3W9YSQHRM');
|
||||
</script>
|
||||
|
||||
<!-- Яндекс.Метрика -->
|
||||
<script type="text/javascript">
|
||||
(function(m,e,t,r,i,k,a){
|
||||
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=105278924', 'ym');
|
||||
|
||||
ym(105278924, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", accurateTrackBounce:true, trackLinks:true});
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/105278924" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"name": "Николай Сердюк",
|
||||
"jobTitle": "Программист 1С",
|
||||
"description": "Профессиональный программист 1С с более чем 10-летним опытом",
|
||||
"url": "https://nikdizell.ru",
|
||||
"email": "{{ CONTACT_EMAIL }}",
|
||||
"telephone": "{{ CONTACT_PHONE }}",
|
||||
"knowsAbout": [
|
||||
"1С:Предприятие 8.3",
|
||||
"Управление торговлей",
|
||||
"Бухгалтерия предприятия",
|
||||
"Зарплата и управление персоналом",
|
||||
"SQL",
|
||||
"Веб-сервисы",
|
||||
"API интеграция"
|
||||
],
|
||||
"hasOccupation": {
|
||||
"@type": "Occupation",
|
||||
"name": "Программист 1С",
|
||||
"description": "Разработка и сопровождение систем на платформе 1С",
|
||||
"occupationLocation": "Россия"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
{% block mainmenu %}
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<nav class="nav">
|
||||
<a href="{% url 'home' %}" class="logo">
|
||||
<img src="{% static 'programmer/images/main.ico' %}" alt="Logo" class="logo-img">
|
||||
<span class="logo-text">Программист 1С</span>
|
||||
</a>
|
||||
|
||||
<!-- Десктопное меню -->
|
||||
<ul class="nav-menu">
|
||||
{% for m in menu %}
|
||||
<li class="nav-item">
|
||||
<a href="{% url m.url_name %}" class="nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
|
||||
{{m.title}}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="nav-actions">
|
||||
<a href="https://t.me/odinesina_prog" target="_blank" class="telegram-btn">
|
||||
<span class="telegram-icon">
|
||||
<img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Theme Toggle Switch -->
|
||||
<div class="theme-switcher">
|
||||
<input type="checkbox" id="theme-toggle" class="theme-toggle-checkbox" checked>
|
||||
<label for="theme-toggle" class="theme-toggle-label">
|
||||
<span class="theme-toggle-slider"></span>
|
||||
<span class="theme-icon sun">☀️</span>
|
||||
<span class="theme-icon moon">🌙</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка мобильного меню -->
|
||||
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Мобильное меню -->
|
||||
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
|
||||
<div class="mobile-menu" id="mobileMenu">
|
||||
<div class="mobile-menu-header">
|
||||
<h3>Меню</h3>
|
||||
<button class="mobile-menu-close" id="mobileMenuClose">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="mobile-nav-menu">
|
||||
{% for m in menu %}
|
||||
<li class="mobile-nav-item">
|
||||
<a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
|
||||
{{m.title}}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="mobile-nav-actions">
|
||||
<a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="width: 100%; text-align: center;">
|
||||
<span class="telegram-icon">
|
||||
<img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">
|
||||
</span>
|
||||
</a>
|
||||
<div class="theme-switcher" style="justify-content: center;">
|
||||
<input type="checkbox" id="mobile-theme-toggle" class="theme-toggle-checkbox" checked>
|
||||
<label for="mobile-theme-toggle" class="theme-toggle-label">
|
||||
<span class="theme-toggle-slider"></span>
|
||||
<span class="theme-icon sun">☀️</span>
|
||||
<span class="theme-icon moon">🌙</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock mainmenu %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main">
|
||||
<div class="container">
|
||||
<section class="content">
|
||||
<!-- Breadcrumbs -->
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumbs">
|
||||
<a href="{% url 'home' %}" class="breadcrumb-link">Главная</a>
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span class="breadcrumb-current">{{title}}</span>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Messages -->
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="page-content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-info">
|
||||
<h3>Николай Сердюк</h3>
|
||||
<p>Программист 1С</p>
|
||||
</div>
|
||||
<div class="footer-contacts">
|
||||
<p>📧 {{ CONTACT_EMAIL }}</p>
|
||||
<p>📱 {{ CONTACT_PHONE }}</p>
|
||||
</div>
|
||||
<div class="footer-copyright">
|
||||
<p>© 2025 ИП Сердюк Николай Александрович. Все права защищены.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% bootstrap_javascript %}
|
||||
<script src="{% static 'programmer/js/theme-switcher.js' %}"></script>
|
||||
<script src="{% static 'programmer/js/mobile-menu.js' %}"></script>
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Дополнительные JS файлы для конкретных страниц -->
|
||||
{% endblock %}
|
||||
</body>
|
||||
=======
|
||||
{% load static %}
|
||||
{% load programmer_tags %}
|
||||
{% load django_bootstrap5 %}
|
||||
@ -1131,5 +575,4 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
>>>>>>> master
|
||||
</html>
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
@ -6,13 +5,4 @@
|
||||
|
||||
|
||||
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,76 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'programmer/css/competence.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Компетенции</h1>
|
||||
<p class="page-subtitle">Профессиональные навыки и опыт</p>
|
||||
</div>
|
||||
|
||||
<div class="competence-grid">
|
||||
{% for p in posts %}
|
||||
<div class="modern-card fade-in">
|
||||
<div class="competence-item">
|
||||
{% if p.photo %}
|
||||
<div class="competence-scan-wrapper">
|
||||
<div class="competence-scan-container">
|
||||
<img src="{{ p.photo.url }}"
|
||||
alt="{{ p.title }}"
|
||||
class="competence-scan"
|
||||
onclick="openCompetenceModal('{{ p.photo.url }}', '{{ p.title }}')">
|
||||
<div class="scan-hint">
|
||||
<span class="scan-zoom-icon">🔍</span>
|
||||
<span class="scan-text">Нажмите для увеличения</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="competence-content">
|
||||
<h2 class="competence-title">{{ p.title }}</h2>
|
||||
<div class="competence-description">
|
||||
{{ p.content|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not posts %}
|
||||
<div class="modern-card text-center fade-in">
|
||||
<h3>📚 Информация о компетенциях</h3>
|
||||
<p class="card-subtitle">Раздел находится в разработке</p>
|
||||
<div class="card-actions justify-center">
|
||||
<a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a>
|
||||
<a href="{% url 'about' %}" class="btn btn-secondary">Обо мне</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Модальное окно для увеличенного просмотра -->
|
||||
<div id="competenceModal" class="modal competence-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="competenceModalTitle">Компетенция</h3>
|
||||
<button class="modal-close" onclick="closeCompetenceModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img class="modal-image" id="competenceModalImage" alt="Увеличенное изображение компетенции">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'programmer/js/competence.js' %}"></script>
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
@ -142,5 +69,4 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'programmer/js/competence.js' %}"></script>
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
/* TEAM */
|
||||
Developer: Николай Сердюк
|
||||
Site: https://nikdizell.ru
|
||||
@ -11,18 +10,4 @@ Bootstrap
|
||||
/* SITE */
|
||||
Last update: 2025
|
||||
Language: Russian
|
||||
=======
|
||||
/* TEAM */
|
||||
Developer: Николай Сердюк
|
||||
Site: https://nikdizell.ru
|
||||
Email: {{ CONTACT_EMAIL }}
|
||||
|
||||
/* THANKS */
|
||||
Django Framework
|
||||
Bootstrap
|
||||
|
||||
/* SITE */
|
||||
Last update: 2025
|
||||
Language: Russian
|
||||
>>>>>>> master
|
||||
Doctype: HTML5
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
@ -101,108 +100,4 @@ document.addEventListener('keydown', function(event) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hero-section fade-in">
|
||||
<h1 class="hero-title">🚀 Добро пожаловать!</h1>
|
||||
<p class="hero-subtitle">Я профессиональный программист 1С с опытом создания эффективных бизнес-решений</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
{% autoescape off %}
|
||||
{% for p in posts %}
|
||||
<div class="modern-card fade-in {% cycle '' 'secondary' %}">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">{{p.title}}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
{{p.content}}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно формы -->
|
||||
<div id="callbackModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>📞 Заявка на консультацию</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="{% url 'callback' %}" id="callbackForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_name">Имя *</label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_phone">Телефон *</label>
|
||||
{{ form.phone }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_email">Электронная почта</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_question">Ваш вопрос</label>
|
||||
{{ form.question }}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">
|
||||
📨 Отправить заявку
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not posts %}
|
||||
<div class="modern-card text-center fade-in">
|
||||
<h3>🚀 Контент скоро появится</h3>
|
||||
<p class="card-subtitle">Мы готовим для вас интересные материалы и кейсы</p>
|
||||
<div class="card-actions justify-center">
|
||||
<button onclick="openModal()" class="btn btn-primary">🎯 Получить консультацию</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function openModal() {
|
||||
document.getElementById('callbackModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('callbackModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Закрытие модального окна при клике вне его
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('callbackModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Закрытие по ESC
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,153 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Отзывы</h1>
|
||||
<p class="page-subtitle">Мнения клиентов и партнеров</p>
|
||||
</div>
|
||||
|
||||
<div class="recall-grid">
|
||||
{% for p in posts %}
|
||||
<div class="modern-card fade-in">
|
||||
<div class="recall-item">
|
||||
<div class="recall-header">
|
||||
<div class="recall-info">
|
||||
<h2 class="recall-title">{{ p.title }}</h2>
|
||||
{% if p.time_create %}
|
||||
<!-- <div class="recall-meta">-->
|
||||
<!-- <span class="recall-date">{{ p.time_create|date:"d.m.Y" }}</span>-->
|
||||
<!-- </div>-->
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recall-content">
|
||||
{% if p.scan %}
|
||||
<div class="recall-scan-wrapper">
|
||||
<div class="recall-scan-container">
|
||||
<img src="{{ p.scan.url }}"
|
||||
alt="Отзыв от {{ p.title }}"
|
||||
class="recall-scan"
|
||||
onclick="openModal('{{ p.scan.url }}', '{{ p.title }}')">
|
||||
<div class="scan-hint">
|
||||
<span class="scan-zoom-icon">🔍</span>
|
||||
<span class="scan-text">Нажмите для увеличения</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="recall-text">
|
||||
{{ p.content|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if not posts %}
|
||||
<div class="modern-card text-center fade-in">
|
||||
<h3>💬 Отзывы клиентов</h3>
|
||||
<p class="card-subtitle">Здесь будут отображаться отзывы от довольных клиентов</p>
|
||||
<div class="card-actions justify-center">
|
||||
<a href="{% url 'solution' %}" class="btn btn-primary">Посмотреть проекты</a>
|
||||
<a href="{% url 'about' %}" class="btn btn-secondary">Связаться со мной</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Модальное окно для увеличенного просмотра -->
|
||||
<div id="imageModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Отзыв</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img class="modal-image" id="modalImage">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModal(imageUrl, title) {
|
||||
console.log('Opening modal with:', imageUrl);
|
||||
const modal = document.getElementById('imageModal');
|
||||
const modalImg = document.getElementById('modalImage');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
if (modal && modalImg) {
|
||||
modal.style.display = "block";
|
||||
modalImg.src = imageUrl;
|
||||
if (title && modalTitle) {
|
||||
modalTitle.textContent = title;
|
||||
}
|
||||
|
||||
// Добавляем класс для анимации
|
||||
setTimeout(() => {
|
||||
modal.classList.add('active');
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('imageModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.style.display = "none";
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Закрытие модального окна при клике вне изображения
|
||||
document.addEventListener('click', function(event) {
|
||||
const modal = document.getElementById('imageModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Закрытие по ESC
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Адаптация размера изображения в модальном окне
|
||||
window.addEventListener('resize', function() {
|
||||
const modalImg = document.getElementById('modalImage');
|
||||
if (modalImg && modalImg.src) {
|
||||
adjustModalImageSize();
|
||||
}
|
||||
});
|
||||
|
||||
function adjustModalImageSize() {
|
||||
const modalImg = document.getElementById('modalImage');
|
||||
const modalContent = document.querySelector('.modal-content');
|
||||
|
||||
if (modalImg && modalContent) {
|
||||
const maxWidth = window.innerWidth * 0.9;
|
||||
const maxHeight = window.innerHeight * 0.8;
|
||||
|
||||
modalImg.style.maxWidth = `${maxWidth}px`;
|
||||
modalImg.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'programmer/js/recall.js' %}"></script>
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
@ -321,5 +171,4 @@ function adjustModalImageSize() {
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'programmer/js/recall.js' %}"></script>
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,22 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Основной сайт
|
||||
Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml
|
||||
|
||||
# Запрещаем служебные разделы
|
||||
Disallow: /admin/
|
||||
Disallow: /static/admin/
|
||||
Disallow: /callback/
|
||||
Disallow: /api/
|
||||
|
||||
# Разрешаем индексацию статических файлов
|
||||
Allow: /static/
|
||||
Allow: /media/
|
||||
|
||||
# Указываем главное зеркало
|
||||
=======
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
@ -35,5 +16,4 @@ Allow: /static/
|
||||
Allow: /media/
|
||||
|
||||
# Указываем главное зеркало
|
||||
>>>>>>> master
|
||||
Host: {{ request.scheme }}://{{ request.get_host }}
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
@ -10,17 +9,4 @@
|
||||
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
|
||||
</url>
|
||||
{% endfor %}
|
||||
=======
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
{% for url in urlset %}
|
||||
<url>
|
||||
<loc>{{ url.location }}</loc>
|
||||
{% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %}
|
||||
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
|
||||
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
|
||||
</url>
|
||||
{% endfor %}
|
||||
>>>>>>> master
|
||||
</urlset>
|
||||
@ -1,74 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'programmer/css/solution-accordion.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<div class="improved-list">
|
||||
{% autoescape off %}
|
||||
{% for p in posts %}
|
||||
<li class="fade-in">
|
||||
<div class="content-card">
|
||||
<h2>{{p.title}}</h2>
|
||||
|
||||
<div class="solution-accordion">
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||
<strong>📋 Описание задачи</strong>
|
||||
<span class="accordion-icon">▼</span>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
{{p.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||
<strong>🔧 Описание решения</strong>
|
||||
<span class="accordion-icon">▼</span>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
{{p.implementation}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header" onclick="toggleAccordion(this)">
|
||||
<strong>✅ Результат</strong>
|
||||
<span class="accordion-icon">▼</span>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
{{p.closing}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="article-panel">
|
||||
<p class="first">Опубликовано: {{p.time_create|date:"d.m.Y"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
|
||||
{% if not posts %}
|
||||
<div class="content-card text-center">
|
||||
<h3>Примеры решений скоро появятся</h3>
|
||||
<p>Мы готовим для вас интересные кейсы и решения</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Подключаем внешний скрипт -->
|
||||
<script src="{% static 'programmer/js/solution-accordion.js' %}"></script>
|
||||
|
||||
=======
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
@ -164,5 +93,4 @@
|
||||
<!-- Подключаем внешний скрипт -->
|
||||
<script src="{% static 'programmer/js/solution-accordion.js' %}"></script>
|
||||
|
||||
>>>>>>> master
|
||||
{% endblock %}
|
||||
@ -1,19 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django import template
|
||||
from programmer.models import *
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag(name='competence')
|
||||
def get_competence():
|
||||
return Competence.objects.all()
|
||||
|
||||
|
||||
@register.simple_tag(name='recall')
|
||||
def get_recall():
|
||||
return Recall.objects.all()
|
||||
=======
|
||||
from django import template
|
||||
from ..models import CallbackRequest
|
||||
|
||||
@ -28,4 +12,3 @@ def get_today_callbacks():
|
||||
from django.utils import timezone
|
||||
today = timezone.now().date()
|
||||
return CallbackRequest.objects.filter(time_create__date=today).count()
|
||||
>>>>>>> master
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
=======
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
>>>>>>> master
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
@ -25,32 +24,4 @@ urlpatterns = [
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
=======
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from .views import *
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from .sitemaps import sitemaps
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', index, name='home'),
|
||||
path('about/', about, name='about'),
|
||||
path('solutions/', solution, name='solution'),
|
||||
path('competence/', ability, name='ability'),
|
||||
path('recall/', recall, name='recall'),
|
||||
path('post/<int:post_id>/', show_post, name='post'),
|
||||
path('callback/', callback_request, name='callback'),
|
||||
path('admin/statistics/', statistics_view, name='statistics'),
|
||||
# Sitemap
|
||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
|
||||
name='django.contrib.sitemaps.views.sitemap'),
|
||||
path('robots.txt', robots_txt, name='robots'),
|
||||
]
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
>>>>>>> master
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
@ -1,258 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from .models import *
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from .models import CallbackRequest # Импортируем из models, а не forms
|
||||
from .forms import CallbackForm
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import PageView, Visitor
|
||||
from django.db.models import Count
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
|
||||
menu = [
|
||||
{'title': "Главная", 'url_name': 'home'},
|
||||
{'title': "Проекты", 'url_name': 'solution'},
|
||||
{'title': "Компетенции", 'url_name': 'ability'},
|
||||
{'title': "Отзывы", 'url_name': 'recall'},
|
||||
{'title': "Обо мне", 'url_name': 'about'}
|
||||
]
|
||||
|
||||
|
||||
# === ДОБАВЬТЕ ЭТИ ФУНКЦИИ ЗДЕСЬ ===
|
||||
|
||||
def get_client_ip(request):
|
||||
"""Получаем реальный IP клиента"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
|
||||
|
||||
def should_track_request(request):
|
||||
"""Определяем, нужно ли отслеживать запрос"""
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
path = request.path
|
||||
|
||||
# Игнорируемые пути (Nextcloud специфичные)
|
||||
nextcloud_paths = [
|
||||
'/index.php',
|
||||
'/status.php',
|
||||
'/cron',
|
||||
'/remote.php',
|
||||
'/ocs',
|
||||
'/apps/',
|
||||
'/custom_apps/',
|
||||
]
|
||||
|
||||
# Игнорируемые IP (Docker сети)
|
||||
docker_ips = [
|
||||
'192.168.64.1',
|
||||
'192.168.65.1',
|
||||
'172.17.0.1',
|
||||
'172.18.0.1',
|
||||
'172.19.0.1',
|
||||
]
|
||||
|
||||
# Игнорируем статические файлы и админку
|
||||
if path.startswith('/static/') or path.startswith('/admin/'):
|
||||
return False
|
||||
|
||||
# Не отслеживаем Nextcloud и Docker запросы
|
||||
if any(path.startswith(p) for p in nextcloud_paths):
|
||||
return False
|
||||
if client_ip in docker_ips:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def track_page_view(request):
|
||||
"""Основная функция отслеживания просмотров"""
|
||||
if not should_track_request(request):
|
||||
return
|
||||
|
||||
try:
|
||||
PageView.objects.create(
|
||||
url=request.path,
|
||||
ip_address=get_client_ip(request),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
|
||||
referer=request.META.get('HTTP_REFERER', '')[:500],
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error tracking page: {e}")
|
||||
|
||||
|
||||
def track_view(view_func):
|
||||
"""Декоратор для отслеживания просмотров страниц"""
|
||||
from functools import wraps
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
# Отслеживаем просмотр перед выполнением view
|
||||
track_page_view(request)
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
|
||||
@track_view
|
||||
def index(request):
|
||||
posts = Home.objects.filter(is_published=True)
|
||||
context = {
|
||||
'posts': posts,
|
||||
'menu': menu,
|
||||
'title': "Главная страница",
|
||||
'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, интеграция и оптимизация систем 1С. Закажите консультацию.",
|
||||
'meta_keywords': "программист 1С, разработка 1С, интеграция 1С, оптимизация 1С, 1С предприятие 8.3",
|
||||
'form': CallbackForm()
|
||||
}
|
||||
return render(request, 'programmer/index.html', context=context)
|
||||
|
||||
|
||||
@track_view
|
||||
def about(request):
|
||||
context = {
|
||||
'menu': menu,
|
||||
'title': "Обо мне - Программист 1С",
|
||||
'meta_description': "Николай Сердюк - профессиональный программист 1С с более чем 10-летним опытом. Разработка, интеграция, оптимизация бизнес-процессов.",
|
||||
'meta_keywords': "программист 1С Николай Сердюк, опыт работы 1С, компетенции 1С, проекты 1С"
|
||||
}
|
||||
return render(request, 'programmer/about.html', context=context)
|
||||
|
||||
|
||||
@track_view
|
||||
def solution(request):
|
||||
posts = Solution.objects.filter(is_published=True)
|
||||
context = {
|
||||
'posts': posts,
|
||||
'menu': menu,
|
||||
'title': "Проекты"
|
||||
}
|
||||
return render(request, 'programmer/solution.html', context=context)
|
||||
|
||||
|
||||
@track_view
|
||||
def ability(request):
|
||||
posts = Competence.objects.filter(is_published=True)
|
||||
context = {
|
||||
'posts': posts,
|
||||
'menu': menu,
|
||||
'title': "Компетенции"
|
||||
}
|
||||
return render(request, 'programmer/competence.html', context=context)
|
||||
|
||||
|
||||
@track_view
|
||||
def recall(request):
|
||||
posts = Recall.objects.filter(is_published=True)
|
||||
context = {
|
||||
'posts': posts,
|
||||
'menu': menu,
|
||||
'title': "Отзывы"
|
||||
}
|
||||
return render(request, 'programmer/recall.html', context=context)
|
||||
|
||||
|
||||
def show_post(request, post_id):
|
||||
return HttpResponse(f"Отображение № {post_id}")
|
||||
|
||||
|
||||
def pageNotFound(request, exception):
|
||||
return HttpResponseNotFound('<h1>Страница не найдена</h1>')
|
||||
|
||||
|
||||
def callback_request(request):
|
||||
if request.method == 'POST':
|
||||
form = CallbackForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Сохраняем заявку через форму
|
||||
form.save()
|
||||
messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.')
|
||||
return redirect('home')
|
||||
else:
|
||||
# Если форма невалидна, показываем ошибки
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}')
|
||||
return redirect('home')
|
||||
|
||||
# Если GET запрос, просто показываем главную страницу
|
||||
return redirect('home')
|
||||
|
||||
|
||||
def is_admin(user):
|
||||
return user.is_staff
|
||||
|
||||
|
||||
def is_staff(user):
|
||||
return user.is_staff
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff)
|
||||
def statistics_view(request):
|
||||
today = timezone.now().date()
|
||||
week_ago = today - timedelta(days=7)
|
||||
|
||||
# Статистика за сегодня
|
||||
today_views = PageView.objects.filter(
|
||||
timestamp__date=today
|
||||
).count()
|
||||
|
||||
# Статистика за неделю
|
||||
weekly_views = PageView.objects.filter(
|
||||
timestamp__date__gte=week_ago
|
||||
).count()
|
||||
|
||||
# Всего просмотров
|
||||
total_views = PageView.objects.count()
|
||||
|
||||
# Популярные страницы за неделю
|
||||
popular_pages = PageView.objects.filter(
|
||||
timestamp__date__gte=week_ago
|
||||
).values('url').annotate(
|
||||
views=Count('id')
|
||||
).order_by('-views')[:10]
|
||||
|
||||
# Уникальные посетители за неделю
|
||||
unique_visitors = Visitor.objects.filter(
|
||||
last_visit__date__gte=week_ago
|
||||
).count()
|
||||
|
||||
# Последние посещения
|
||||
recent_views = PageView.objects.select_related().order_by('-timestamp')[:20]
|
||||
|
||||
today = timezone.now().date()
|
||||
total_callbacks = CallbackRequest.objects.count()
|
||||
today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count()
|
||||
unread_callbacks = CallbackRequest.objects.filter(is_read=False).count()
|
||||
|
||||
context = {
|
||||
'today_views': today_views,
|
||||
'weekly_views': weekly_views,
|
||||
'total_views': total_views,
|
||||
'unique_visitors': unique_visitors,
|
||||
'popular_pages': popular_pages,
|
||||
'recent_views': recent_views,
|
||||
'total_callbacks': total_callbacks,
|
||||
'today_callbacks': today_callbacks,
|
||||
'unread_callbacks': unread_callbacks,
|
||||
}
|
||||
|
||||
return render(request, 'admin/statistics.html', context)
|
||||
|
||||
|
||||
@require_GET
|
||||
def robots_txt(request):
|
||||
return render(request, 'robots.txt', content_type='text/plain')
|
||||
=======
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from .models import *
|
||||
from django.shortcuts import render, redirect
|
||||
@ -512,4 +257,3 @@ def statistics_view(request):
|
||||
@require_GET
|
||||
def robots_txt(request):
|
||||
return render(request, 'robots.txt', content_type='text/plain')
|
||||
>>>>>>> master
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user