Добавил статистику на сайт
This commit is contained in:
parent
cde6762f9e
commit
1360bc18e1
Binary file not shown.
@ -50,6 +50,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'programmer.middleware.PageViewMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'OneCprogsite.urls'
|
ROOT_URLCONF = 'OneCprogsite.urls'
|
||||||
|
|||||||
Binary file not shown.
BIN
OneCprogsite/programmer/__pycache__/middleware.cpython-310.pyc
Normal file
BIN
OneCprogsite/programmer/__pycache__/middleware.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -43,6 +43,17 @@ class CallbackAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ('name', 'phone', 'email')
|
search_fields = ('name', 'phone', 'email')
|
||||||
readonly_fields = ('time_create',)
|
readonly_fields = ('time_create',)
|
||||||
|
|
||||||
|
|
||||||
|
@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(Competence, ProgrammerAdmin)
|
||||||
admin.site.register(Recall, RecallAdmin)
|
admin.site.register(Recall, RecallAdmin)
|
||||||
admin.site.register(Solution, SolutionAdmin)
|
admin.site.register(Solution, SolutionAdmin)
|
||||||
|
|||||||
54
OneCprogsite/programmer/middleware.py
Normal file
54
OneCprogsite/programmer/middleware.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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')
|
||||||
|
return ip
|
||||||
41
OneCprogsite/programmer/migrations/0011_visitor_pageview.py
Normal file
41
OneCprogsite/programmer/migrations/0011_visitor_pageview.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-11-12 11:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0010_alter_callbackrequest_email_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Visitor',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ip_address', models.GenericIPAddressField()),
|
||||||
|
('first_visit', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('last_visit', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('visit_count', models.IntegerField(default=1)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'indexes': [models.Index(fields=['ip_address'], name='programmer__ip_addr_2c6dca_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PageView',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('url', models.CharField(max_length=500)),
|
||||||
|
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('ip_address', models.GenericIPAddressField()),
|
||||||
|
('user_agent', models.TextField(blank=True)),
|
||||||
|
('referer', models.CharField(blank=True, max_length=500)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'indexes': [models.Index(fields=['url', 'timestamp'], name='programmer__url_9a41b2_idx'), models.Index(fields=['timestamp'], name='programmer__timesta_070072_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class Recall(models.Model):
|
class Recall(models.Model):
|
||||||
@ -98,3 +99,29 @@ class CallbackRequest(models.Model):
|
|||||||
verbose_name = 'Заявка на звонок'
|
verbose_name = 'Заявка на звонок'
|
||||||
verbose_name_plural = 'Заявки на звонок'
|
verbose_name_plural = 'Заявки на звонок'
|
||||||
ordering = ['-time_create']
|
ordering = ['-time_create']
|
||||||
|
|
||||||
|
|
||||||
|
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']),
|
||||||
|
]
|
||||||
|
|||||||
79
OneCprogsite/programmer/templates/admin/base.html
Normal file
79
OneCprogsite/programmer/templates/admin/base.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!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>
|
||||||
|
</html>
|
||||||
15
OneCprogsite/programmer/templates/admin/base_site.html
Normal file
15
OneCprogsite/programmer/templates/admin/base_site.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% 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 %}
|
||||||
|
<a href="{% url 'statistics' %}">📊 Статистика</a> /
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
100
OneCprogsite/programmer/templates/admin/statistics.html
Normal file
100
OneCprogsite/programmer/templates/admin/statistics.html
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{% extends 'admin/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Статистика посещений{% endblock %}
|
||||||
|
{% block page_title %}Статистика посещений{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
{% endblock %}
|
||||||
@ -12,6 +12,7 @@ urlpatterns = [
|
|||||||
path('recall/', recall, name='recall'),
|
path('recall/', recall, name='recall'),
|
||||||
path('post/<int:post_id>/', show_post, name='post'),
|
path('post/<int:post_id>/', show_post, name='post'),
|
||||||
path('callback/', callback_request, name='callback'),
|
path('callback/', callback_request, name='callback'),
|
||||||
|
path('admin/statistics/', statistics_view, name='statistics'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
from django.http import HttpResponse, HttpResponseNotFound
|
from django.http import HttpResponse, HttpResponseNotFound
|
||||||
from django.shortcuts import render
|
|
||||||
from .models import *
|
from .models import *
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import CallbackRequest # Импортируем из models, а не forms
|
from .models import CallbackRequest # Импортируем из models, а не forms
|
||||||
from .forms import CallbackForm
|
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
|
||||||
|
|
||||||
|
|
||||||
menu = [
|
menu = [
|
||||||
@ -89,4 +93,58 @@ def callback_request(request):
|
|||||||
return redirect('home')
|
return redirect('home')
|
||||||
|
|
||||||
# Если GET запрос, просто показываем главную страницу
|
# Если GET запрос, просто показываем главную страницу
|
||||||
return redirect('home')
|
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]
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'admin/statistics.html', context)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user