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 |