init commit
29
.env.example
Normal 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
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
__pycache__
|
||||
.venv
|
||||
.history
|
||||
static/qr_codes
|
||||
48
Dockerfile
Normal 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"]
|
||||
60
QR_CODE_FEATURE_SUMMARY.md
Normal 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
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run migrations
|
||||
docker exec ${django_app} python3 smartsoltech/manage.py migrate
|
||||
60
deploy.sh
Normal 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
@@ -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
@@ -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 "✅ Тест завершён."
|
||||
5
frontend/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
1
frontend/assets/css/styles.min.css
vendored
Normal file
BIN
frontend/assets/img/about/1.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/assets/img/about/2.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/assets/img/about/3.jpg
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
frontend/assets/img/about/4.jpg
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
frontend/assets/img/clients/creative-market.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/assets/img/clients/designmodo.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/assets/img/clients/envato.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/assets/img/clients/themeforest.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/assets/img/header-bg.jpg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
frontend/assets/img/map-image.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
frontend/assets/img/photo_2024-10-06_10-06-08.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/assets/img/photo_2024-10-06_10-06-15.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/assets/img/photo_2024-10-06_10-06-18.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/assets/img/portfolio/1-full.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
frontend/assets/img/portfolio/1-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/assets/img/portfolio/2-full.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/assets/img/portfolio/2-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/assets/img/portfolio/3-full.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/assets/img/portfolio/3-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/assets/img/portfolio/4-full.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/assets/img/portfolio/4-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/assets/img/portfolio/5-full.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
frontend/assets/img/portfolio/5-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/assets/img/portfolio/6-full.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/assets/img/portfolio/6-thumbnail.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/assets/img/team/1.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/assets/img/team/2.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
frontend/assets/img/team/3.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
1
frontend/assets/js/script.min.js
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 %}
|
||||
146
real_confirmation_process.html
Normal 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
@@ -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
@@ -0,0 +1 @@
|
||||
{"detail":"User 4 deleted"}
|
||||
1
response_login.json
Normal file
@@ -0,0 +1 @@
|
||||
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0Iiwicm9sZSI6InVzZXIiLCJleHAiOjE3NTI0OTc2NDV9.S_tquLFIPnyG6XlfwIw97hJv0l9oKpTcYw_XG0mDd6w","token_type":"bearer"}
|
||||
1
response_me.json
Normal file
@@ -0,0 +1 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
1
response_register.json
Normal file
@@ -0,0 +1 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
1
response_update.json
Normal file
@@ -0,0 +1 @@
|
||||
{"id":4,"email":"updated_testuser@example.com","role":"admin"}
|
||||
1
response_user.json
Normal file
@@ -0,0 +1 @@
|
||||
{"id":4,"email":"testuser@example.com","role":"user"}
|
||||
1
response_users.json
Normal 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
@@ -0,0 +1 @@
|
||||
static/qr-qr_codes
|
||||
0
smartsoltech/comunication/__init__.py
Normal file
16
smartsoltech/comunication/admin.py
Normal 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')
|
||||
|
||||
6
smartsoltech/comunication/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ComunicationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'comunication'
|
||||
0
smartsoltech/comunication/management/.gitignore
vendored
Normal 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()
|
||||
36
smartsoltech/comunication/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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='Телефон'),
|
||||
),
|
||||
]
|
||||
@@ -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='Клиент'),
|
||||
),
|
||||
]
|
||||
0
smartsoltech/comunication/migrations/__init__.py
Normal file
50
smartsoltech/comunication/models.py
Normal 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})"
|
||||
232
smartsoltech/comunication/telegram_bot.py
Normal 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)
|
||||
|
||||
|
||||
3
smartsoltech/comunication/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
smartsoltech/comunication/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
22
smartsoltech/manage.py
Executable 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()
|
||||
|
After Width: | Height: | Size: 69 KiB |
BIN
smartsoltech/media/static/img/customer/1.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
smartsoltech/media/static/img/customer/JD-26-512.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
smartsoltech/media/static/img/customer/JD-26-512_SKOAvnB.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
smartsoltech/media/static/img/project/1.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
smartsoltech/media/static/img/project/11.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
smartsoltech/media/static/img/project/3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
smartsoltech/media/static/img/project/8.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 31 KiB |
BIN
smartsoltech/media/static/img/review/JD-26-512.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
smartsoltech/media/static/img/services/1.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
smartsoltech/media/static/img/services/11.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
smartsoltech/media/static/img/services/13.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
smartsoltech/media/static/img/services/13_qS7q13T.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
smartsoltech/media/static/img/services/1_1tCuJFo.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
smartsoltech/media/static/img/services/1_g7siDRC.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
smartsoltech/media/static/img/services/1_j0npR4p.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
smartsoltech/media/static/img/services/1_ym12Lh9.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
smartsoltech/media/static/img/services/2.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
smartsoltech/media/static/img/services/2_VlpsUXb.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
smartsoltech/media/static/img/services/3.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |