init commit

This commit is contained in:
2025-11-24 07:02:33 +09:00
commit 7bf003e70d
488 changed files with 51130 additions and 0 deletions

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Django Settings
SECRET_KEY=your-secret-key-here-change-this-in-production
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1,your-domain.com
CSRF_TRUSTED_ORIGINS=http://localhost:8000,https://your-domain.com
# PostgreSQL Database
POSTGRES_DB=smartsoltech_db
POSTGRES_USER=smartsoltech_user
POSTGRES_PASSWORD=your-strong-password-here
POSTGRES_HOST=postgres_db
# PgAdmin
PGADMIN_DEFAULT_EMAIL=admin@smartsoltech.kr
PGADMIN_DEFAULT_PASSWORD=your-pgadmin-password
# Zabbix Agent
ZBX_SERVER_HOST=your-zabbix-server-ip
# Telegram Bot (настраивается через админку Django)
# TELEGRAM_BOT_TOKEN=your-bot-token-from-botfather
# TELEGRAM_BOT_NAME=your_bot_name
# Email Settings (настраивается через админку Django)
# SMTP_SERVER=smtp.gmail.com
# SMTP_PORT=587
# SENDER_EMAIL=your-email@gmail.com
# EMAIL_PASSWORD=your-app-password
# USE_TLS=True

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.env
__pycache__
.venv
.history
static/qr_codes

48
Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
# Dockerfile for Django Application
# Base image
FROM python:3.10-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
netcat-traditional \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --upgrade pip && pip install -r requirements.txt
# Copy the project files to the container
COPY . /app/
# Replace the template file inside the container
RUN if [ -f patch/fieldset.html ]; then \
cp patch/fieldset.html /usr/local/lib/python3.10/site-packages/jazzmin/templates/admin/includes/fieldset.html; \
fi
# Make wait-for-it script executable
RUN chmod +x wait-for-it.sh
# Create directories for static and media files
RUN mkdir -p /app/smartsoltech/static /app/smartsoltech/media
# Collect static files (with error handling)
RUN python smartsoltech/manage.py collectstatic --noinput || echo "Collectstatic will run after database is ready"
# Expose the port for the Django application
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000').read()" || exit 1
# Start the Django server with wait for database
CMD ["sh", "-c", "./wait-for-it.sh postgres_db:5432 -- python smartsoltech/manage.py migrate && python smartsoltech/manage.py runserver 0.0.0.0:8000"]

View File

@@ -0,0 +1,60 @@
# QR-код механизм для подачи заявок через Telegram бота
## ✅ Что добавлено:
### 1. Модальное окно с QR-кодом
- **Файл**: `smartsoltech/web/templates/web/services_modern.html`
- **Что добавлено**:
- Секция QR-кода в модальном окне заявки
- JavaScript для обработки формы и генерации QR-кода
- Автоматический сброс формы при закрытии модального окна
### 2. Backend функциональность
- **Существующий механизм**: View `generate_qr_code` в `smartsoltech/web/views.py`
- **Что работает**:
- Создание клиента и заявки на услугу
- Генерация уникального токена для заявки
- Создание QR-кода с ссылкой на Telegram бота
- Сохранение QR-кода в папку static/qr_codes/
### 3. Telegram бот интеграция
- **Файл**: `smartsoltech/comunication/telegram_bot.py`
- **Что работает**:
- Обработка команды `/start request_{id}_token_{token}`
- Подтверждение заявки и связывание с chat_id пользователя
- Создание пользователя Django из данных Telegram
- Отправка подтверждающего сообщения
## 🔄 Workflow (Рабочий процесс):
1. **Пользователь** заполняет форму в модальном окне на странице услуг
2. **JavaScript** отправляет POST запрос на `/service/generate_qr_code/{service_id}/`
3. **Django** создает:
- Client (клиента)
- ServiceRequest (заявку на услугу)
- QR-код с ссылкой на Telegram бота
4. **Модальное окно** показывает QR-код и ссылку "Открыть в Telegram"
5. **Пользователь** сканирует QR-код или переходит по ссылке
6. **Telegram бот** получает команду `/start` с параметрами заявки
7. **Бот** подтверждает заявку, связывает с chat_id и отправляет подтверждение
## 🧪 Тестирование:
Откройте файл `test_qr_functionality.html` в браузере для подробных инструкций по тестированию.
Быстрый тест:
1. Перейдите на http://localhost:8000/services/
2. Нажмите "Заказать услугу" под любой услугой
3. Заполните форму и отправьте
4. Должен появиться QR-код
5. Перейдите по ссылке в Telegram и нажмите "Start"
## 📁 Измененные файлы:
1. `smartsoltech/web/templates/web/services_modern.html` - добавлен QR-код в модальное окно
2. Использован существующий механизм в `smartsoltech/web/views.py` - `generate_qr_code`
3. Использован существующий Telegram бот в `smartsoltech/comunication/telegram_bot.py`
## 🎯 Результат:
Теперь пользователи могут подавать заявки на услуги через современное модальное окно, которое генерирует QR-код для подтверждения заявки через Telegram бота. Весь процесс автоматизирован и интегрирован с существующей системой.

3
bin/migrate.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Run migrations
docker exec ${django_app} python3 smartsoltech/manage.py migrate

60
deploy.sh Normal file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Production deployment script for SmartSolTech
set -e
echo "🚀 Starting SmartSolTech deployment..."
# Check if .env file exists
if [ ! -f .env ]; then
echo "⚠️ .env file not found!"
echo "Creating .env from .env.example..."
cp .env.example .env
echo "📝 Please edit .env file with your production values before continuing."
exit 1
fi
# Stop existing containers
echo "🛑 Stopping existing containers..."
docker-compose down
# Build images
echo "🏗️ Building Docker images..."
docker-compose build --no-cache
# Start services
echo "▶️ Starting services..."
docker-compose up -d postgres_db
# Wait for database to be ready
echo "⏳ Waiting for database to be ready..."
sleep 10
# Run migrations
echo "🔄 Running database migrations..."
docker-compose run --rm web python smartsoltech/manage.py migrate
# Collect static files
echo "📦 Collecting static files..."
docker-compose run --rm web python smartsoltech/manage.py collectstatic --noinput
# Create superuser (optional, commented out for security)
# echo "👤 Creating superuser..."
# docker-compose run --rm web python smartsoltech/manage.py createsuperuser
# Start all services
echo "🎬 Starting all services..."
docker-compose up -d
# Show running containers
echo "✅ Deployment complete! Running containers:"
docker-compose ps
echo ""
echo "📋 Service URLs:"
echo " Django App: http://localhost:8000"
echo " PgAdmin: http://localhost:8080"
echo ""
echo "📝 To view logs: docker-compose logs -f"
echo "🛑 To stop: docker-compose down"

107
docker-compose.yml Normal file
View File

@@ -0,0 +1,107 @@
version: '3.8'
services:
postgres_db:
image: postgres:17
container_name: postgres_db
env_file: .env
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
networks:
- web_db_network
- pgadmin_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin
env_file: .env
depends_on:
postgres_db:
condition: service_healthy
ports:
- "8080:80"
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
networks:
- pgadmin_network
volumes:
- pgadmin:/var/lib/pgadmin
restart: unless-stopped
zabbix-agent:
image: zabbix/zabbix-agent:latest
container_name: zabbix_agent
env_file: .env
environment:
ZBX_SERVER_HOST: ${ZBX_SERVER_HOST}
volumes:
- /proc:/host/proc
- /sys:/host/sys
- /etc:/host/etc
privileged: true
networks:
- zabbix_network
web:
build: .
container_name: django_app
env_file: .env
restart: unless-stopped
volumes:
- .:/app
- static_volume:/app/smartsoltech/staticfiles
- media_volume:/app/smartsoltech/media
ports:
- "8000:8000"
depends_on:
postgres_db:
condition: service_healthy
networks:
- web_db_network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000').read()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
bot:
build: .
container_name: telegram_bot
command: sh -c "./wait-for-it.sh postgres_db:5432 -- python smartsoltech/manage.py start_telegram_bot"
restart: unless-stopped
volumes:
- .:/app
env_file:
- .env
depends_on:
web:
condition: service_healthy
networks:
- web_db_network
volumes:
pgdata:
pgadmin:
static_volume:
media_volume:
networks:
web_db_network:
pgadmin_network:
zabbix_network:

48
endpoint_test.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
BASE_URL="http://localhost:8002/auth"
EMAIL="testuser@example.com"
PASSWORD="secret123"
echo "1⃣ Регистрация пользователя..."
curl -s -X POST "$BASE_URL/register" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"password\": \"$PASSWORD\"}" | tee response_register.json
echo -e "\n"
USER_ID=$(jq .id response_register.json)
echo "2⃣ Аутентификация..."
curl -s -X POST "$BASE_URL/login" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"password\": \"$PASSWORD\"}" | tee response_login.json
echo -e "\n"
TOKEN=$(jq -r .access_token response_login.json)
echo "🔐 Получен токен: $TOKEN"
AUTH_HEADER="Authorization: Bearer $TOKEN"
echo "3⃣ Получение текущего пользователя (/me)..."
curl -s -X GET "$BASE_URL/me" -H "$AUTH_HEADER" | tee response_me.json
echo -e "\n"
echo "4⃣ Получение списка всех пользователей..."
curl -s -X GET "$BASE_URL/users" | tee response_users.json
echo -e "\n"
echo "5⃣ Получение пользователя по ID ($USER_ID)..."
curl -s -X GET "$BASE_URL/users/$USER_ID" | tee response_user.json
echo -e "\n"
echo "6⃣ Обновление пользователя..."
curl -s -X PUT "$BASE_URL/users/$USER_ID" \
-H "Content-Type: application/json" \
-d "{\"email\": \"updated_$EMAIL\", \"role\": \"admin\"}" | tee response_update.json
echo -e "\n"
echo "7⃣ Удаление пользователя..."
curl -s -X DELETE "$BASE_URL/users/$USER_ID" | tee response_delete.json
echo -e "\n"
echo "✅ Тест завершён."

File diff suppressed because one or more lines are too long

1
frontend/assets/css/styles.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

1
frontend/assets/js/script.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!function(){"use strict";var e=document.querySelector("#mainNav");if(e){var o=e.querySelector(".navbar-collapse");if(o){var n=new bootstrap.Collapse(o,{toggle:!1}),t=o.querySelectorAll("a");for(var a of t)a.addEventListener("click",(function(e){n.hide()}))}var r=function(){(void 0!==window.pageYOffset?window.pageYOffset:(document.documentElement||document.body.parentNode||document.body).scrollTop)>100?e.classList.add("navbar-shrink"):e.classList.remove("navbar-shrink")};r(),document.addEventListener("scroll",r);var d=document.querySelectorAll(".portfolio-modal");for(var s of d)s.addEventListener("shown.bs.modal",(function(o){e.classList.add("d-none")})),s.addEventListener("hidden.bs.modal",(function(o){e.classList.remove("d-none")}))}}();

75
frontend/footer.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Home - Brand</title>
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.0/css/all.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="54">
<!-- Start: footer -->
<footer class="text-light bg-dark pt-5 pb-4">
<div class="container text-md-left">
<div class="row text-md-left">
<div class="col-md-3 col-lg-3 col-xl-3 mx-auto mt-3">
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Company Name</h5>
<p>Here you can use rows and columns to organize your footer content. Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
</div>
<div class="col-md-2 col-lg-2 col-xl-2 mx-auto mt-3">
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Products</h5>
<p><a href="#" class="text-light" style="text-decoration:none;">Product 1</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Product 2</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Product 3</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Product 4</a></p>
</div>
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mt-3">
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Useful Links</h5>
<p><a href="#" class="text-light" style="text-decoration:none;">Your Account</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Become an Affiliate</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Shipping Rates</a></p>
<p><a href="#" class="text-light" style="text-decoration:none;">Help</a></p>
</div>
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mt-3">
<h5 class="text-uppercase text-warning mb-4 font-weight-bold">Contact</h5>
<p><i class="fas fa-home mr-3"></i> 123 Street, City, State</p>
<p><i class="fas fa-envelope mr-3"></i> info@example.com</p>
<p><i class="fas fa-phone mr-3"></i> + 01 234 567 88</p>
<p><i class="fas fa-print mr-3"></i> + 01 234 567 89</p>
</div>
</div>
<hr class="mb-4">
<div class="row align-items-center">
<div class="col-md-7 col-lg-8">
<p class="text-md-left">© 2024 Company Name. All rights reserved.</p>
</div>
<div class="col-md-5 col-lg-4">
<div class="text-md-right">
<ul class="list-unstyled list-inline">
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-facebook"></i></a></li>
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-twitter"></i></a></li>
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-google-plus"></i></a></li>
<li class="list-inline-item"><a href="#" class="btn-floating btn-sm text-light" style="font-size:23px;"><i class="fab fa-linkedin-in"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</footer><!-- End: footer -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/script.min.js"></script>
</body>
</html>

41
frontend/header.html Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Home - Brand</title>
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="/assets/css/styles.min.css">
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="54">
<!-- Start: Navbar Right Links (Dark) -->
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"></path>
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
</svg></span><span>Brand</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-5">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link active" href="#">First Item</a></li>
<li class="nav-item"><a class="nav-link" href="#">Second Item</a></li>
<li class="nav-item"><a class="nav-link" href="#">Third Item</a></li>
</ul>
</div>
</div>
</nav><!-- End: Navbar Right Links (Dark) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/script.min.js"></script>
</body>
</html>

1
frontend/manifest.json Normal file
View File

@@ -0,0 +1 @@
{"short_name":"sst","name":"smartsoltech","icons":[{"src":"/assets/img/photo_2024-10-06_10-06-08.jpg","type":"image/jpeg","sizes":"1011x702"},{"src":"/assets/img/photo_2024-10-06_10-06-08.jpg","type":"image/jpeg","sizes":"1011x702"}],"start_url":"smartsoltech.kr","display":"fullscreen"}

82
frontend/services.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>SmartSoltech</title>
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="500x500" href="/assets/img/photo_2024-10-06_10-06-15.jpg" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="icon" type="image/jpeg" sizes="1011x702" href="/assets/img/photo_2024-10-06_10-06-08.jpg">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="/assets/css/styles.min.css">
</head>
<body>
<!-- Start: Articles Cards -->
<div class="container py-4 py-xl-5">
<div class="row mb-5">
<div class="col-md-8 col-xl-6 text-center mx-auto">
<h2>Heading</h2>
<p class="w-lg-50">Curae hendrerit donec commodo hendrerit egestas tempus, turpis facilisis nostra nunc. Vestibulum dui eget ultrices.</p>
</div>
</div>
<div class="row gy-4 row-cols-1 row-cols-md-2 row-cols-xl-3">
<div class="col">
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div class="card-body p-4">
<p class="text-primary card-text mb-0">Article</p>
<h4 class="card-title">Lorem libero donec</h4>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div>
<p class="fw-bold mb-0">John Smith</p>
<p class="text-muted mb-0">Erat netus</p>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div class="card-body p-4">
<p class="text-primary card-text mb-0">Article</p>
<h4 class="card-title">Lorem libero donec</h4>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div>
<p class="fw-bold mb-0">John Smith</p>
<p class="text-muted mb-0">Erat netus</p>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card"><img class="card-img-top w-100 d-block fit-cover" style="height: 200px;" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div class="card-body p-4">
<p class="text-primary card-text mb-0">Article</p>
<h4 class="card-title">Lorem libero donec</h4>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<div class="d-flex"><img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png">
<div>
<p class="fw-bold mb-0">John Smith</p>
<p class="text-muted mb-0">Erat netus</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- End: Articles Cards -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/script.min.js"></script>
</body>
</html>

45
nginx.conf Normal file
View File

@@ -0,0 +1,45 @@
# Nginx configuration for SmartSolTech (optional, for production with Nginx)
upstream django_app {
server web:8000;
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
client_max_body_size 20M;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://django_app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_redirect off;
proxy_buffering off;
}
location /static/ {
alias /app/smartsoltech/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /app/smartsoltech/media/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Deny access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

59
patch/fieldset.html Normal file
View File

@@ -0,0 +1,59 @@
{% load jazzmin %}
{% if card %}
<div class="card {{ fieldset.classes|cut:"collapse" }}">
{% if card_header and fieldset.name %}
<div class="card-header">
<div class="card-title">
<strong>{{ fieldset.name }}</strong>{% if fieldset.description %} - <i>{{ fieldset.description }}</i>{% endif %}
</div>
</div>
{%elif fieldset.description %}
<div class="card-header">
<div class="card-title">
{{ fieldset.description }}
</div>
</div>
{%endif%}
<div class="p-5{% if fieldset.name %} card-body{% endif %}">
{% endif %}
{% for line in fieldset %}
<div class="form-group{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
<div class="row">
{% for field in line %}
<label class="{% if not line.fields|length == 1 and forloop.counter != 1 %}col-auto {% else %}col-sm-3 {% endif %}text-left" for="id_{{ field.field.name }}">
{{ field.field.label|capfirst }}
{% if field.field.field.required %}
<span class="text-red">* </span>
{% endif %}
</label>
<div class="{% if not line.fields|length == 1 %} col-auto fieldBox {% else %} col-sm-7 {% endif %}
{% if field.field.name %} field-{{ field.field.name }}{% endif %}
{% if not field.is_readonly and field.errors %} errors{% endif %}
{% if field.field.is_hidden %} hidden {% endif %}
{% if field.is_checkcard %} checkcard-row{% endif %}">
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
<div class="help-block red">
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
</div>
{% if field.field.help_text %}
<div class="help-block">{{ field.field.help_text|safe }}</div>
{% endif %}
<div class="help-block text-red">
{% if line.fields|length == 1 %}{{ line.errors }}{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if card %}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔄 Реальная проверка подтверждения заявки</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; line-height: 1.6; background: #f8f9fa; }
.container { max-width: 800px; margin: 0 auto; }
.card { background: white; padding: 20px; border-radius: 10px; margin: 15px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.improvement { border-left: 5px solid #28a745; }
.process { border-left: 5px solid #007bff; }
.technical { border-left: 5px solid #ffc107; }
.test { border-left: 5px solid #17a2b8; }
h1 { color: #333; text-align: center; }
h2 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 5px; }
h3 { color: #666; }
.emoji { font-size: 1.2em; }
.code { background: #f8f9fa; padding: 2px 6px; border-radius: 4px; font-family: monospace; }
.success { color: #28a745; font-weight: bold; }
.warning { color: #ffc107; font-weight: bold; }
.info { color: #007bff; font-weight: bold; }
ul li { margin: 8px 0; }
.test-btn { display: inline-block; padding: 12px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; margin: 10px 5px; }
.test-btn:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<h1><span class="emoji">🔄</span> Реальная проверка подтверждения заявки</h1>
<div class="card improvement">
<h2><span class="emoji"></span> Исправленная логика</h2>
<p class="success">Теперь система корректно ожидает подтверждения от пользователя через Telegram!</p>
<h3>Что было неправильно:</h3>
<ul>
<li>❌ Анимация успеха показывалась сразу после создания заявки</li>
<li>❌ Окно закрывалось автоматически через 6 секунд</li>
<li>Не учитывалось, что пользователь должен подтвердить в Telegram</li>
</ul>
<h3>Что исправлено:</h3>
<ul>
<li>✅ QR-код остается на экране до реального подтверждения</li>
<li>✅ Система проверяет статус <span class="code">is_verified</span> каждые 3 секунды</li>
<li>✅ Анимация успеха появляется только после подтверждения в Telegram</li>
<li>✅ Индикатор "Ожидаем подтверждения..." показывает статус</li>
</ul>
</div>
<div class="card process">
<h2><span class="emoji">🔄</span> Новый процесс (правильный)</h2>
<ol>
<li><strong>Пользователь заполняет форму</strong> → нажимает "Отправить заявку"</li>
<li><strong>Создается заявка</strong> с <span class="code">is_verified = False</span></li>
<li><strong>Показывается QR-код</strong> с индикатором ожидания</li>
<li class="info"><strong>Система начинает проверку</strong> статуса каждые 3 секунды</li>
<li><strong>Пользователь сканирует QR-код</strong> → переходит в Telegram</li>
<li><strong>Telegram бот обрабатывает команду</strong> → устанавливает <span class="code">is_verified = True</span></li>
<li class="success"><strong>Система обнаруживает подтверждение</strong> → показывает анимацию успеха</li>
<li><strong>Окно закрывается</strong> через 3 секунды после подтверждения</li>
</ol>
</div>
<div class="card technical">
<h2><span class="emoji">🛠️</span> Технические изменения</h2>
<h3>Новый API endpoint:</h3>
<ul>
<li><span class="code">GET /service/check_status/{request_id}/</span></li>
<li>Возвращает <span class="code">{"is_verified": boolean, "chat_id": string}</span></li>
<li>Используется для polling проверки статуса</li>
</ul>
<h3>JavaScript логика:</h3>
<ul>
<li><strong>Интервал проверки:</strong> каждые 3 секунды</li>
<li><strong>Очистка интервала:</strong> при закрытии модального окна или подтверждении</li>
<li><strong>Визуальная обратная связь:</strong> спиннер "Ожидаем подтверждения..."</li>
</ul>
<h3>Обновленные файлы:</h3>
<ul>
<li><span class="code">web/views.py</span> - добавлен <span class="code">check_request_status</span></li>
<li><span class="code">web/urls.py</span> - добавлен URL для проверки статуса</li>
<li><span class="code">services_modern.html</span> - обновлен JavaScript и HTML</li>
</ul>
</div>
<div class="card test">
<h2><span class="emoji">🧪</span> Тестирование</h2>
<a href="http://localhost:8000/services/" target="_blank" class="test-btn">
<span class="emoji">🛠️</span> Тестировать на странице услуг
</a>
<h3>Сценарий тестирования:</h3>
<ol>
<li><strong>Откройте страницу услуг</strong> и нажмите "Заказать услугу"</li>
<li><strong>Заполните форму</strong> и отправьте</li>
<li><strong>Убедитесь, что:</strong>
<ul>
<li>Появился QR-код с кнопкой "Открыть в Telegram"</li>
<li>Показывается "Ожидаем подтверждения в Telegram..."</li>
<li>QR-код остается на экране (не исчезает через 3 секунды)</li>
</ul>
</li>
<li><strong>Перейдите в Telegram</strong> по QR-коду или ссылке</li>
<li><strong>Нажмите "Start"</strong> в боте</li>
<li><strong>Вернитесь в браузер</strong> - в течение 3 секунд должна:
<ul>
<li>Появиться анимированная галочка</li>
<li>Показаться "Заявка подтверждена!"</li>
<li>Окно автоматически закроется</li>
</ul>
</li>
</ol>
<h3 class="warning">Что проверить дополнительно:</h3>
<ul>
<li>🔍 <strong>Без подтверждения:</strong> QR-код должен оставаться на экране бесконечно</li>
<li>🔍 <strong>Закрытие окна:</strong> Проверка статуса должна прекращаться</li>
<li>🔍 <strong>Повторное открытие:</strong> Форма должна быть сброшена</li>
</ul>
</div>
<div class="card improvement">
<h2><span class="emoji">🎯</span> Результат</h2>
<p class="success">Теперь система корректно работает с реальным подтверждением пользователя через Telegram!</p>
<p>Пользователь видит визуальную обратную связь на каждом этапе:</p>
<ul>
<li>📝 Заполнение формы</li>
<li>⏳ Отправка заявки</li>
<li>📱 QR-код для Telegram</li>
<li>🔄 Ожидание подтверждения</li>
<li>✅ Успешное подтверждение</li>
</ul>
<p class="info">Заявка остается в состоянии ожидания до тех пор, пока пользователь не подтвердит её в Telegram боте!</p>
</div>
</div>
</body>
</html>

19
requirements.txt Normal file
View File

@@ -0,0 +1,19 @@
anyio==4.6.0
asgiref==3.8.1
certifi==2024.8.30
Django==5.1.1
django-jazzmin==3.0.0
exceptiongroup==1.2.2
h11==0.14.0
httpcore==1.0.6
httpx==0.27.2
idna==3.10
pillow==10.4.0
psycopg2-binary==2.9.9
python-decouple==3.8
python-telegram-bot==21.6
qrcode==8.0
sniffio==1.3.1
sqlparse==0.5.1
typing_extensions==4.12.2
pyTelegramBotAPI

1
response_delete.json Normal file
View File

@@ -0,0 +1 @@
{"detail":"User 4 deleted"}

1
response_login.json Normal file
View File

@@ -0,0 +1 @@
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTI0OTc2NDV9.S_tquLFIPnyG6XlfwIw97hJv0l9oKpTcYw_XG0mDd6w","token_type":"bearer"}

1
response_me.json Normal file
View File

@@ -0,0 +1 @@
{"id":4,"email":"testuser@example.com","role":"user"}

1
response_register.json Normal file
View File

@@ -0,0 +1 @@
{"id":4,"email":"testuser@example.com","role":"user"}

1
response_update.json Normal file
View File

@@ -0,0 +1 @@
{"id":4,"email":"updated_testuser@example.com","role":"admin"}

1
response_user.json Normal file
View File

@@ -0,0 +1 @@
{"id":4,"email":"testuser@example.com","role":"user"}

1
response_users.json Normal file
View File

@@ -0,0 +1 @@
[{"id":1,"email":"user1@example.com","role":"user"},{"id":4,"email":"testuser@example.com","role":"user"}]

1
smartsoltech/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
static/qr-qr_codes

View File

View File

@@ -0,0 +1,16 @@
# communication/admin.py
from django.contrib import admin
from .models import EmailSettings, TelegramSettings, UserCommunication
@admin.register(EmailSettings)
class EmailSettingsAdmin(admin.ModelAdmin):
list_display = ('smtp_server', 'sender_email', 'use_tls', 'use_ssl')
@admin.register(TelegramSettings)
class TelegramSettingsAdmin(admin.ModelAdmin):
list_display = ('bot_name', 'bot_token', 'use_polling')
@admin.register(UserCommunication)
class UserCommunicationAdmin(admin.ModelAdmin):
list_display = ('client', 'email', 'phone', 'chat_id')

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ComunicationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'comunication'

View File

View File

@@ -0,0 +1,11 @@
# comunication/management/commands/start_telegram_bot.py
from django.core.management.base import BaseCommand
from comunication.telegram_bot import TelegramBot
class Command(BaseCommand):
help = 'Starts the Telegram bot'
def handle(self, *args, **kwargs):
bot = TelegramBot()
self.stdout.write('Starting Telegram bot polling...')
bot.start_bot_polling()

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.1.1 on 2024-10-08 12:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CommunicationMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[('email', 'Email'), ('telegram', 'Telegram')], max_length=50)),
('settings', models.JSONField()),
],
),
migrations.CreateModel(
name='UserCommunication',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('chat_id', models.CharField(blank=True, max_length=100, null=True)),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('phone', models.CharField(blank=True, max_length=20, null=True)),
('preferred_method', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='comunication.communicationmethod')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.1.1 on 2024-10-08 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comunication', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EmailSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('smtp_server', models.CharField(max_length=255)),
('smtp_port', models.PositiveIntegerField()),
('sender_email', models.EmailField(max_length=254)),
('password', models.CharField(max_length=255)),
('use_tls', models.BooleanField(default=True)),
('use_ssl', models.BooleanField(default=False)),
('display_name', models.CharField(blank=True, max_length=255, null=True)),
],
),
migrations.CreateModel(
name='TelegramSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bot_name', models.CharField(max_length=100)),
('bot_token', models.CharField(max_length=255)),
('webhook_url', models.URLField(blank=True, null=True)),
('use_polling', models.BooleanField(default=True)),
],
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.1 on 2024-10-08 12:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comunication', '0002_emailsettings_telegramsettings'),
]
operations = [
migrations.RemoveField(
model_name='usercommunication',
name='preferred_method',
),
migrations.RemoveField(
model_name='usercommunication',
name='user',
),
migrations.DeleteModel(
name='CommunicationMethod',
),
migrations.DeleteModel(
name='UserCommunication',
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.1 on 2024-10-13 00:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comunication', '0003_remove_usercommunication_preferred_method_and_more'),
]
operations = [
migrations.CreateModel(
name='UserCommunication',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('phone', models.CharField(blank=True, max_length=15)),
('chat_id', models.CharField(blank=True, max_length=50)),
],
),
]

View File

@@ -0,0 +1,47 @@
# Generated by Django 5.1.1 on 2024-10-13 04:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comunication', '0004_usercommunication'),
('web', '0005_alter_blogpost_options_alter_category_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='emailsettings',
options={'ordering': ['-display_name'], 'verbose_name': 'Параметры E-mail', 'verbose_name_plural': 'Параметры E-mail'},
),
migrations.AlterModelOptions(
name='telegramsettings',
options={'ordering': ['-bot_name'], 'verbose_name': 'Параметры Telegram бота', 'verbose_name_plural': 'Параметры Telegram ботов'},
),
migrations.AlterModelOptions(
name='usercommunication',
options={'ordering': ['-id'], 'verbose_name': 'Связь с клиентом', 'verbose_name_plural': 'Связи с клиентами'},
),
migrations.AddField(
model_name='usercommunication',
name='client',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='communications', to='web.client', verbose_name='Клиент'),
),
migrations.AlterField(
model_name='usercommunication',
name='chat_id',
field=models.CharField(blank=True, max_length=50, verbose_name='Telegram Chat ID'),
),
migrations.AlterField(
model_name='usercommunication',
name='email',
field=models.EmailField(max_length=254, verbose_name='Электронная почта'),
),
migrations.AlterField(
model_name='usercommunication',
name='phone',
field=models.CharField(blank=True, max_length=15, verbose_name='Телефон'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.1 on 2024-10-13 04:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comunication', '0005_alter_emailsettings_options_and_more'),
('web', '0005_alter_blogpost_options_alter_category_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='usercommunication',
name='client',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='communications', to='web.client', verbose_name='Клиент'),
),
]

View File

@@ -0,0 +1,50 @@
# communication/models.py
from django.db import models
from django.contrib.auth.models import User
from web.models import Client
class EmailSettings(models.Model):
smtp_server = models.CharField(max_length=255)
smtp_port = models.PositiveIntegerField()
sender_email = models.EmailField()
password = models.CharField(max_length=255)
use_tls = models.BooleanField(default=True)
use_ssl = models.BooleanField(default=False)
display_name = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return f"SMTP: {self.smtp_server}, Email: {self.sender_email}"
class Meta:
verbose_name = 'Параметры E-mail'
verbose_name_plural = 'Параметры E-mail'
ordering = ['-display_name']
class TelegramSettings(models.Model):
bot_name = models.CharField(max_length=100)
bot_token = models.CharField(max_length=255)
webhook_url = models.URLField(null=True, blank=True)
use_polling = models.BooleanField(default=True)
def __str__(self):
return f"Telegram Bot: {self.bot_name}"
class Meta:
verbose_name = 'Параметры Telegram бота'
verbose_name_plural = 'Параметры Telegram ботов'
ordering = ['-bot_name']
class UserCommunication(models.Model):
client = models.ForeignKey(
'web.Client', on_delete=models.CASCADE, related_name='communications', verbose_name='Клиент', null=True, blank=True
)
email = models.EmailField(verbose_name='Электронная почта')
phone = models.CharField(max_length=15, blank=True, verbose_name='Телефон')
chat_id = models.CharField(max_length=50, blank=True, verbose_name='Telegram Chat ID')
class Meta:
verbose_name = 'Связь с клиентом'
verbose_name_plural = 'Связи с клиентами'
ordering = ['-id']
def __str__(self):
if self.client:
return f"Связь с клиентом: {self.client.first_name} {self.client.last_name} ({self.email})"
return f"Связь без клиента ({self.email})"

View File

@@ -0,0 +1,232 @@
import logging
import json
import requests
import os
import base64
import re
from comunication.models import TelegramSettings
from web.models import ServiceRequest, Order, Project, Client, User
import telebot
from django.shortcuts import get_object_or_404
from django.utils.crypto import get_random_string
class TelegramBot:
def __init__(self):
# Получение настроек бота из базы данных
bot_settings = TelegramSettings.objects.first()
if bot_settings and bot_settings.bot_token:
TELEGRAM_BOT_TOKEN = bot_settings.bot_token.strip()
# Проверяем валидность токена
if self._validate_token(TELEGRAM_BOT_TOKEN):
self.bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN)
logging.info(f"[TelegramBot] Бот инициализирован с токеном для {bot_settings.bot_name}.")
else:
logging.error(f"[TelegramBot] Токен невалиден: {TELEGRAM_BOT_TOKEN[:10]}...")
raise Exception(f"Невалидный токен Telegram бота. Обновите токен в базе данных.")
else:
raise Exception("Telegram bot settings not found or token is empty")
def _validate_token(self, token):
"""Проверяет валидность токена через Telegram API"""
url = f"https://api.telegram.org/bot{token}/getMe"
try:
response = requests.get(url, timeout=10)
result = response.json()
if result.get('ok'):
bot_info = result.get('result', {})
logging.info(f"[TelegramBot] Токен валиден. Бот: @{bot_info.get('username', 'unknown')}")
return True
else:
logging.error(f"[TelegramBot] Ошибка Telegram API: {result.get('description', 'Unknown error')}")
return False
except requests.RequestException as e:
logging.error(f"[TelegramBot] Ошибка при проверке токена: {e}")
return False
def start_bot_polling(self):
logging.info("[TelegramBot] Бот начал работу в режиме polling.")
@self.bot.message_handler(commands=['start'])
def send_welcome(message):
# Проверяем, содержатся ли параметры в команде /start
match = re.match(r'/start request_(\d+)_token_(.*)', message.text)
logging.info(f"[TelegramBot] Получена команда /start: {message.text}")
if match:
self.handle_confirm_command(message, match)
elif message.text.strip() == '/start':
# Ответ на просто команду /start без параметров
self.bot.reply_to(message, "Здравствуйте! Пожалуйста, используйте команду /start с корректными параметрами для подтверждения регистрации.")
else:
self.bot.reply_to(message, "Здравствуйте! Пожалуйста, используйте команду /start с корректными параметрами для подтверждения регистрации.")
@self.bot.message_handler(func=lambda message: 'статус заявки' in message.text.lower())
def handle_service_request_status(message):
chat_id = message.chat.id
client = Client.objects.filter(chat_id=chat_id).first()
if client:
service_requests = ServiceRequest.objects.filter(client=client)
if service_requests.exists():
response = "Ваши заявки:\n"
for req in service_requests:
response += (
f"Номер заявки: {req.id}\n"
f"Услуга: {req.service.name}\n"
f"Дата создания: {req.created_at.strftime('%Y-%m-%d')}\n"
f"UID заявки: {req.token}\n"
f"Подтверждена: {'Да' if req.is_verified else 'Нет'}\n\n"
)
else:
response = "У вас нет активных заявок."
else:
response = "Клиент не найден. Пожалуйста, зарегистрируйтесь."
self.bot.reply_to(message, response)
@self.bot.message_handler(func=lambda message: 'статус заказа' in message.text.lower())
def handle_order_status(message):
chat_id = message.chat.id
client = Client.objects.filter(chat_id=chat_id).first()
if client:
orders = Order.objects.filter(client=client)
if orders.exists():
response = "Ваши заказы:\n"
for order in orders:
response += (
f"Номер заказа: {order.id}\n"
f"Услуга: {order.service.name}\n"
f"Статус: {order.get_status_display()}\n\n"
)
else:
response = "У вас нет активных заказов."
else:
response = "Клиент не найден. Пожалуйста, зарегистрируйтесь."
self.bot.reply_to(message, response)
@self.bot.message_handler(func=lambda message: 'статус проекта' in message.text.lower())
def handle_project_status(message):
chat_id = message.chat.id
client = Client.objects.filter(chat_id=chat_id).first()
if client:
projects = Project.objects.filter(order__client=client)
if projects.exists():
response = "Ваши проекты:\n"
for project in projects:
response += (
f"Номер проекта: {project.id}\n"
f"Название проекта: {project.name}\n"
f"Статус: {project.get_status_display()}\n"
f"Дата завершения: {project.completion_date.strftime('%Y-%m-%d') if project.completion_date else 'В процессе'}\n\n"
)
else:
response = "У вас нет активных проектов."
else:
response = "Клиент не найден. Пожалуйста, зарегистрируйтесь."
self.bot.reply_to(message, response)
# Запуск бота
try:
self.bot.polling(non_stop=True)
except Exception as e:
logging.error(f"[TelegramBot] Ошибка при запуске polling: {e}")
def handle_confirm_command(self, message, match=None):
chat_id = message.chat.id
logging.info(f"[TelegramBot] Получено сообщение для подтверждения: {message.text}")
if not match:
match = re.match(r'/start request_(\d+)_token_(.*)', message.text)
if match:
request_id = match.group(1)
encoded_token = match.group(2)
# Декодируем токен
try:
token = base64.urlsafe_b64decode(encoded_token + '==').decode('utf-8')
logging.info(f"[TelegramBot] Декодированный токен: {token}")
except Exception as e:
logging.error(f"[TelegramBot] Ошибка при декодировании токена: {e}")
self.bot.send_message(chat_id, "Ошибка: Некорректный токен. Пожалуйста, повторите попытку позже.")
return
# Получаем заявку
try:
service_request = ServiceRequest.objects.get(id=request_id, token=token)
logging.info(f"[TelegramBot] Заявка найдена: {service_request}")
except ServiceRequest.DoesNotExist:
logging.error(f"[TelegramBot] Заявка с id {request_id} и токеном {token} не найдена.")
self.bot.send_message(chat_id, "Ошибка: Неверная заявка или токен. Пожалуйста, проверьте ссылку.")
return
# Если заявка найдена, обновляем и подтверждаем клиента
if service_request:
service_request.chat_id = chat_id
service_request.is_verified = True # Обновляем статус на подтвержденный
service_request.save()
logging.info(f"[TelegramBot] Заявка {service_request.id} подтверждена и обновлена.")
# Обновляем или создаем клиента, связанного с заявкой
client = service_request.client
# Проверяем, существует ли связанный пользователь, если нет — создаем его
if not client.user:
user, created = User.objects.get_or_create(
email=client.email,
defaults={
'username': f"{client.email.split('@')[0]}_{get_random_string(5)}",
'first_name': message.from_user.first_name,
'last_name': message.from_user.last_name if message.from_user.last_name else ''
}
)
if not created:
# Если пользователь уже существовал, обновляем его данные
user.first_name = message.from_user.first_name
if message.from_user.last_name:
user.last_name = message.from_user.last_name
user.save()
logging.info(f"[TelegramBot] Обновлен пользователь {user.username} с данными из Телеграм.")
# Связываем клиента с пользователем
client.user = user
client.save()
else:
# Обновляем данные существующего пользователя
user = client.user
user.first_name = message.from_user.first_name
if message.from_user.last_name:
user.last_name = message.from_user.last_name
user.save()
logging.info(f"[TelegramBot] Пользователь {user.username} обновлен с данными из Телеграм.")
# Обновляем chat_id клиента
client.chat_id = chat_id
client.save()
logging.info(f"[TelegramBot] Клиент {client.id} обновлен с chat_id {chat_id}")
# Отправляем сообщение пользователю в Telegram с подтверждением и информацией о заявке
confirmation_message = (
f"Здравствуйте, {client.first_name}!\n\n"
f"Ваш аккаунт успешно подтвержден! 🎉\n\n"
f"Детали вашей заявки:\n"
f"Номер заявки: {service_request.id}\n"
f"Услуга: {service_request.service.name}\n"
f"Статус заявки: {'Подтверждена' if service_request.is_verified else 'Не подтверждена'}\n"
f"Дата создания: {service_request.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
f"Спасибо, что выбрали наши услуги! Если у вас возникнут вопросы, вы всегда можете обратиться к нам."
)
self.bot.send_message(chat_id, confirmation_message)
# Вместо дополнительного POST-запроса — сообщаем о подтверждении через сообщение
self.bot.send_message(chat_id, "Ваш аккаунт успешно подтвержден на сервере! Продолжайте на сайте.")
else:
self.bot.send_message(chat_id, "Ошибка: Неверная заявка или токен. Пожалуйста, проверьте ссылку.")
else:
response_message = "Ошибка: Некорректная команда. Пожалуйста, используйте ссылку, предоставленную на сайте для регистрации."
self.bot.send_message(chat_id, response_message)

View File

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

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
smartsoltech/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', 'smartsoltech.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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Some files were not shown because too many files have changed in this diff Show More