first commit

This commit is contained in:
Maxim
2025-12-11 19:58:49 +03:00
commit 87f3d0a36c
18 changed files with 926 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(python3 -m django:*)",
"Bash(pip3 install:*)",
"Bash(python3:*)",
"Bash(source venv/bin/activate)",
"Bash(pip install:*)",
"Bash(django-admin startproject:*)",
"Bash(python manage.py:*)"
]
}
}

217
.gitignore vendored Normal file
View File

@@ -0,0 +1,217 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
404: Not Found

View File

16
eng_bot_metrics/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for eng_bot_metrics project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eng_bot_metrics.settings')
application = get_asgi_application()

118
eng_bot_metrics/settings.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Django settings for eng_bot_metrics project.
Generated by 'django-admin startproject' using Django 6.0.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-det_&7$u2z3q9v_29srpv@@i8lvr$mckq_z_pqltnj%brn)x%('
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'metrics',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'eng_bot_metrics.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'eng_bot_metrics.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'Europe/Moscow'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'

23
eng_bot_metrics/urls.py Normal file
View File

@@ -0,0 +1,23 @@
"""
URL configuration for eng_bot_metrics project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('metrics.urls')),
]

16
eng_bot_metrics/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for eng_bot_metrics project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eng_bot_metrics.settings')
application = get_wsgi_application()

22
manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eng_bot_metrics.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
metrics/__init__.py Normal file
View File

10
metrics/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import ClickEvent
@admin.register(ClickEvent)
class ClickEventAdmin(admin.ModelAdmin):
list_display = ['event_type', 'ip_address', 'created_at']
list_filter = ['event_type', 'created_at']
search_fields = ['ip_address']
readonly_fields = ['created_at']

5
metrics/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class MetricsConfig(AppConfig):
name = 'metrics'

View File

@@ -0,0 +1,30 @@
# Generated by Django 6.0 on 2025-12-11 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ClickEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event_type', models.CharField(choices=[('bot', 'Переход в бота'), ('channel', 'Переход в TG канал')], max_length=20)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('referrer', models.URLField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Клик',
'verbose_name_plural': 'Клики',
'ordering': ['-created_at'],
},
),
]

View File

22
metrics/models.py Normal file
View File

@@ -0,0 +1,22 @@
from django.db import models
class ClickEvent(models.Model):
EVENT_TYPES = [
('bot', 'Переход в бота'),
('channel', 'Переход в TG канал'),
]
event_type = models.CharField(max_length=20, choices=EVENT_TYPES)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
referrer = models.URLField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'Клик'
verbose_name_plural = 'Клики'
ordering = ['-created_at']
def __str__(self):
return f"{self.get_event_type_display()} - {self.created_at}"

View File

@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Метрики - English Bot</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card h3 {
color: #666;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: bold;
color: #333;
}
.stat-card.bot .value {
color: #667eea;
}
.stat-card.channel .value {
color: #764ba2;
}
.chart-container {
background: white;
border-radius: 16px;
padding: 24px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.chart-container h2 {
color: #333;
margin-bottom: 20px;
}
.events-table {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow-x: auto;
}
.events-table h2 {
color: #333;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #666;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
}
td {
color: #333;
}
.badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.badge-bot {
background: #e8eaff;
color: #667eea;
}
.badge-channel {
background: #f3e8ff;
color: #764ba2;
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
@media (max-width: 768px) {
h1 {
font-size: 1.8rem;
}
.stat-card .value {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>English Bot - Метрики</h1>
<div class="stats-grid">
<div class="stat-card bot">
<h3>Всего переходов в бота</h3>
<div class="value">{{ total_bot }}</div>
</div>
<div class="stat-card channel">
<h3>Всего переходов в канал</h3>
<div class="value">{{ total_channel }}</div>
</div>
<div class="stat-card bot">
<h3>В бота сегодня</h3>
<div class="value">{{ today_bot }}</div>
</div>
<div class="stat-card channel">
<h3>В канал сегодня</h3>
<div class="value">{{ today_channel }}</div>
</div>
<div class="stat-card bot">
<h3>В бота за неделю</h3>
<div class="value">{{ week_bot }}</div>
</div>
<div class="stat-card channel">
<h3>В канал за неделю</h3>
<div class="value">{{ week_channel }}</div>
</div>
</div>
<div class="chart-container">
<h2 class="section-title">Статистика за 30 дней</h2>
<canvas id="clicksChart" height="100"></canvas>
</div>
<div class="events-table">
<h2 class="section-title">Последние события</h2>
<table>
<thead>
<tr>
<th>Тип</th>
<th>IP адрес</th>
<th>Дата и время</th>
</tr>
</thead>
<tbody>
{% for event in recent_events %}
<tr>
<td>
<span class="badge {% if event.event_type == 'bot' %}badge-bot{% else %}badge-channel{% endif %}">
{{ event.get_event_type_display }}
</span>
</td>
<td>{{ event.ip_address|default:"-" }}</td>
<td>{{ event.created_at|date:"d.m.Y H:i:s" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" style="text-align: center; color: #999;">Нет данных</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
const botData = {{ bot_by_day|safe }};
const channelData = {{ channel_by_day|safe }};
// Создаем массив дат за последние 30 дней
const dates = [];
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
dates.push(date.toISOString().split('T')[0]);
}
// Создаем объекты для быстрого поиска
const botMap = {};
botData.forEach(item => botMap[item.date] = item.count);
const channelMap = {};
channelData.forEach(item => channelMap[item.date] = item.count);
// Заполняем данные для графика
const botCounts = dates.map(date => botMap[date] || 0);
const channelCounts = dates.map(date => channelMap[date] || 0);
const labels = dates.map(date => {
const d = new Date(date);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
});
const ctx = document.getElementById('clicksChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Переходы в бота',
data: botCounts,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Переходы в канал',
data: channelCounts,
borderColor: '#764ba2',
backgroundColor: 'rgba(118, 75, 162, 0.1)',
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
</script>
</body>
</html>

3
metrics/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
metrics/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = 'metrics'
urlpatterns = [
path('api/track/bot/', views.track_bot_click, name='track_bot'),
path('api/track/channel/', views.track_channel_click, name='track_channel'),
path('', views.dashboard, name='dashboard'),
]

115
metrics/views.py Normal file
View File

@@ -0,0 +1,115 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.db.models import Count
from django.db.models.functions import TruncDate
from django.utils import timezone
from datetime import timedelta
import json
from .models import ClickEvent
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR')
return ip
@csrf_exempt
@require_http_methods(["POST"])
def track_bot_click(request):
"""Трекинг перехода в бота"""
ClickEvent.objects.create(
event_type='bot',
ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
referrer=request.META.get('HTTP_REFERER'),
)
return JsonResponse({'status': 'ok', 'event': 'bot'})
@csrf_exempt
@require_http_methods(["POST"])
def track_channel_click(request):
"""Трекинг перехода в TG канал"""
ClickEvent.objects.create(
event_type='channel',
ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
referrer=request.META.get('HTTP_REFERER'),
)
return JsonResponse({'status': 'ok', 'event': 'channel'})
def dashboard(request):
"""Страница статистики"""
today = timezone.now().date()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
# Общая статистика
total_bot = ClickEvent.objects.filter(event_type='bot').count()
total_channel = ClickEvent.objects.filter(event_type='channel').count()
# Статистика за сегодня
today_bot = ClickEvent.objects.filter(
event_type='bot',
created_at__date=today
).count()
today_channel = ClickEvent.objects.filter(
event_type='channel',
created_at__date=today
).count()
# Статистика за неделю
week_bot = ClickEvent.objects.filter(
event_type='bot',
created_at__date__gte=week_ago
).count()
week_channel = ClickEvent.objects.filter(
event_type='channel',
created_at__date__gte=week_ago
).count()
# Данные для графика (последние 30 дней)
bot_by_day = ClickEvent.objects.filter(
event_type='bot',
created_at__date__gte=month_ago
).annotate(date=TruncDate('created_at')).values('date').annotate(
count=Count('id')
).order_by('date')
channel_by_day = ClickEvent.objects.filter(
event_type='channel',
created_at__date__gte=month_ago
).annotate(date=TruncDate('created_at')).values('date').annotate(
count=Count('id')
).order_by('date')
# Последние события
recent_events = ClickEvent.objects.all()[:20]
context = {
'total_bot': total_bot,
'total_channel': total_channel,
'today_bot': today_bot,
'today_channel': today_channel,
'week_bot': week_bot,
'week_channel': week_channel,
'bot_by_day': json.dumps([
{'date': item['date'].isoformat(), 'count': item['count']}
for item in bot_by_day
]),
'channel_by_day': json.dumps([
{'date': item['date'].isoformat(), 'count': item['count']}
for item in channel_by_day
]),
'recent_events': recent_events,
}
return render(request, 'metrics/dashboard.html', context)