init commit

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

1
smartsoltech/.gitignore vendored Normal file
View File

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

View File

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
smartsoltech/manage.py Executable file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

View File

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

View File

@@ -0,0 +1,172 @@
"""
Django settings for smartsoltech project.
Generated by 'django-admin startproject' using Django 5.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
# Allowed hosts and CSRF trusted origins
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost').split(',')
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='').split(',')
print(f"ALLOWED_HOSTS: {ALLOWED_HOSTS}")
print(f"CSRF_TRUSTED_ORIGINS: {CSRF_TRUSTED_ORIGINS}")
# Application definition
INSTALLED_APPS = [
'jazzmin',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'web',
'comunication'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'smartsoltech.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'smartsoltech.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('POSTGRES_DB'),
'USER': os.getenv('POSTGRES_USER'),
'PASSWORD': os.getenv('POSTGRES_PASSWORD'),
'HOST': os.getenv('POSTGRES_HOST'), # Имя сервиса контейнера базы данных в docker-compose.yml
'PORT': 5432,
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGE_CODE = 'ru-ru'
USE_I18N = True
USE_L10N = True
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
# Настройки для статических файлов
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # Основная папка для статики
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # Папка для собранных статических файлов при деплое
# Настройки для медиа-файлов
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
## customization section
JAZZMIN_SETTINGS = {
"site_title": "Smartsoltech Admin",
"site_header": "Smartsoltech",
"welcome_sign": "Добро пожаловать в панель управления Smartsoltech",
"site_logo": "img/logo.svg",
"search_model": "web.Client",
"user_avatar": "web.Client.image",
"topmenu_links": [
{"name": "Главная", "url": "admin:index", "permissions": ["auth.view_user"]},
{"model": "web.Client"},
{"app": "web"},
],
"usermenu_links": [
{"name": "Поддержка", "url": "https://smartsoltech.kr/support", "new_window": True},
],
}

View File

@@ -0,0 +1,8 @@
# smartsoltech/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('web.urls')), # Включаем маршруты приложения web
]

View File

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

0
smartsoltech/static/.gitignore vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
@import url('https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css');

View File

@@ -0,0 +1,526 @@
/* Modern IT Company Design - SmartSolTech */
:root {
/* Light Theme Colors */
--primary-color: #6366f1;
--primary-dark: #4f46e5;
--secondary-color: #8b5cf6;
--accent-color: #06d6a0;
--text-dark: #1f2937;
--text-light: #6b7280;
--bg-light: #ffffff;
--bg-gray: #f9fafb;
--border-color: #e5e7eb;
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
--gradient-accent: linear-gradient(135deg, #06d6a0 0%, #34d399 100%);
/* Dark Theme Colors */
--dark-bg: #0f172a;
--dark-bg-secondary: #1e293b;
--dark-text: #f8fafc;
--dark-text-secondary: #cbd5e1;
--dark-border: #334155;
}
/* Dark theme */
[data-theme="dark"] {
--text-dark: var(--dark-text);
--text-light: var(--dark-text-secondary);
--bg-light: var(--dark-bg);
--bg-gray: var(--dark-bg-secondary);
--border-color: var(--dark-border);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
line-height: 1.6;
color: var(--text-dark);
background-color: var(--bg-light);
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
margin-bottom: 1rem;
}
h1 {
font-size: 3.75rem;
font-weight: 800;
background: var(--gradient-primary);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
h2 {
font-size: 2.5rem;
color: var(--text-dark);
}
h3 {
font-size: 1.875rem;
}
p {
font-size: 1.125rem;
line-height: 1.7;
color: var(--text-light);
margin-bottom: 1.5rem;
}
/* Buttons */
.btn-modern {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 12px;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.btn-primary-modern {
background: var(--gradient-primary);
color: white;
box-shadow: var(--shadow);
}
.btn-primary-modern:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(99, 102, 241, 0.4);
color: white;
}
.btn-secondary-modern {
background: var(--bg-light);
color: var(--text-dark);
border: 2px solid var(--border-color);
}
.btn-secondary-modern:hover {
background: var(--bg-gray);
border-color: var(--primary-color);
color: var(--primary-color);
}
/* Cards */
.card-modern {
background: var(--bg-light);
border-radius: 20px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
transition: all 0.3s ease;
overflow: hidden;
}
.card-modern:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.card-modern .card-body {
padding: 2rem;
}
/* Navigation */
.navbar-modern {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
transition: all 0.3s ease;
}
.navbar-modern.scrolled {
background: rgba(255, 255, 255, 0.98);
box-shadow: var(--shadow);
}
.navbar-brand-modern {
font-size: 1.5rem;
font-weight: 800;
background: var(--gradient-primary);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-decoration: none;
}
.nav-link-modern {
font-weight: 500;
color: var(--text-light);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
text-decoration: none;
}
.nav-link-modern:hover,
.nav-link-modern.active {
color: var(--primary-color);
background: rgba(99, 102, 241, 0.1);
}
/* Hero Section */
.hero-modern {
padding: 8rem 0 4rem;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
position: relative;
overflow: hidden;
}
.hero-modern::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="0.5" fill="%236366f1" opacity="0.1"/><circle cx="75" cy="75" r="0.3" fill="%238b5cf6" opacity="0.1"/><circle cx="75" cy="25" r="0.4" fill="%2306d6a0" opacity="0.1"/><circle cx="25" cy="75" r="0.6" fill="%236366f1" opacity="0.05"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.5;
}
/* Services Grid */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.service-card {
background: var(--bg-light);
border-radius: 20px;
padding: 2.5rem;
border: 1px solid var(--border-color);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.service-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--gradient-primary);
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.service-card:hover::before {
transform: translateX(0);
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.service-icon {
width: 60px;
height: 60px;
border-radius: 16px;
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
color: white;
font-size: 1.5rem;
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.6s ease-out forwards;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* Dark mode toggle */
.theme-toggle {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--gradient-primary);
border: none;
color: white;
font-size: 1.25rem;
cursor: pointer;
box-shadow: var(--shadow);
transition: all 0.3s ease;
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1);
}
/* Responsive Design */
@media (max-width: 768px) {
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
.hero-modern {
padding: 6rem 0 3rem;
}
.services-grid {
grid-template-columns: 1fr;
}
.service-card {
padding: 2rem;
}
}
/* Utility Classes */
.text-gradient {
background: var(--gradient-primary);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.bg-gradient {
background: var(--gradient-primary);
}
.border-gradient {
border: 2px solid transparent;
background: linear-gradient(var(--bg-light), var(--bg-light)) padding-box,
var(--gradient-primary) border-box;
}
.container-modern {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.section-padding {
padding: 6rem 0;
}
.mb-large {
margin-bottom: 4rem;
}
.text-center {
text-align: center;
}
/* Loading Animation */
#loading-screen {
transition: opacity 0.3s ease-out;
}
#loading-screen.hidden {
opacity: 0 !important;
pointer-events: none !important;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Success Checkmark Animation */
.success-checkmark {
width: 80px;
height: 80px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #4CAF50;
stroke-miterlimit: 10;
margin: 10px auto;
position: relative;
}
.success-checkmark.animate .icon-circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #4CAF50;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.success-checkmark.animate .icon-line {
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
.success-checkmark.animate .icon-line.line-tip {
animation-delay: 1.1s;
}
.success-checkmark.animate .icon-line.line-long {
animation-delay: 1.2s;
}
.success-checkmark .icon-circle {
width: 80px;
height: 80px;
position: absolute;
border-radius: 50%;
border: 2px solid #4CAF50;
background-color: rgba(76, 175, 80, 0.1);
}
.success-checkmark .icon-line {
height: 2px;
background-color: #4CAF50;
display: block;
border-radius: 2px;
position: absolute;
z-index: 10;
}
.success-checkmark .icon-line.line-tip {
top: 46px;
left: 14px;
width: 25px;
transform: rotate(45deg);
animation: icon-line-tip 0.75s;
}
.success-checkmark .icon-line.line-long {
top: 38px;
right: 8px;
width: 47px;
transform: rotate(-45deg);
animation: icon-line-long 0.75s;
}
.success-checkmark .icon-fix {
top: 8px;
width: 5px;
left: 26px;
z-index: 1;
height: 85px;
position: absolute;
transform: rotate(-45deg);
background-color: #fff;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes icon-line-tip {
0% {
width: 0;
left: 1px;
top: 19px;
}
54% {
width: 0;
left: 1px;
top: 19px;
}
70% {
width: 50px;
left: -8px;
top: 37px;
}
84% {
width: 17px;
left: 21px;
top: 48px;
}
100% {
width: 25px;
left: 14px;
top: 45px;
}
}
@keyframes icon-line-long {
0% {
width: 0;
right: 46px;
top: 54px;
}
65% {
width: 0;
right: 46px;
top: 54px;
}
84% {
width: 55px;
right: 0px;
top: 35px;
}
100% {
width: 47px;
right: 8px;
top: 38px;
}
}

View File

@@ -0,0 +1,27 @@
/* Добавляем стили для анимации появления модального окна */
#serviceModal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 600px;
transform: scale(0);
transition: transform 0.5s ease;
}
.modal.show .modal-content {
transform: scale(1);
}

View File

@@ -0,0 +1,230 @@
/* Font Face Definitions */
@font-face {
font-family: 'Kaushan Script';
src: url('{% static "fonts/Kaushan%20Script-3db770c46931b64b6e281dcb2a7d0586.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Kaushan Script';
src: url('{% static "fonts/Kaushan%20Script-ba85b3a237ebd28058cd515071c02485.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Montserrat';
src: url('{% static "fonts/Montserrat-99764e33f03a95926b938c6f56e08a4f.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
font-family: 'Montserrat';
src: url('{% static "fonts/Montserrat-cc6c968bcebf02819ee3afff266119d9.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: 'Montserrat';
src: url('{% static "fonts/Montserrat-f32ddc894c166fbd8bc0d500ff2aeb3d.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: 'Montserrat';
src: url('{% static "fonts/Montserrat-bcb799eba0c324e431d5daac3e5ea6bf.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Montserrat';
src: url('{% static "fonts/Montserrat-5cfb114945f0457bd5a940932bb8156d.woff2" %}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Utility Classes */
.fit-cover {
object-fit: cover;
}
/* Footer Styles */
footer {
background-color: #343a40;
color: #fff;
padding-top: 5rem;
padding-bottom: 4rem;
}
footer h5 {
margin-bottom: 1.5rem;
font-weight: 700;
}
footer a, footer p {
color: #fff;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
footer hr {
border-top: 1px solid #fff;
width: 100%;
margin: 2rem 0;
}
footer .list-inline-item a {
color: #fff;
font-size: 23px;
}
/* Button Hover Styles */
button.btn-primary:hover {
background: rgb(20, 87, 187);
}
/* Navigation Styles */
ul.main-nav {
list-style-type: none;
padding: 0;
font-size: 0;
max-width: 1000px;
margin: 0 auto;
}
ul.main-nav > li {
display: inline-block;
padding: 0;
}
ul.main-nav > li > a {
display: block;
padding: 20px 30px;
position: relative;
color: #fff;
font-size: 16px;
font-weight: 400;
box-sizing: border-box;
}
ul.main-nav > li:hover {
background-color: #f9f9f9;
}
ul.main-nav > li:hover > a {
color: #333;
font-weight: 400;
}
ul.main-nav > li ul.sub-menu-lists {
margin: 0;
padding: 0;
list-style-type: none;
display: block;
}
ul.main-nav > li ul.sub-menu-lists > li {
padding: 2px 0;
}
ul.main-nav > li ul.sub-menu-lists > li > a {
font-size: 14px;
}
/* Responsive Navigation */
@media only screen and (max-width: 768px) {
.ic.menu {
display: block;
}
ul.main-nav {
z-index: 2;
padding: 50px 0;
position: fixed;
right: 0;
top: 0;
width: 0;
background-color: #000;
height: 100%;
overflow: auto;
transition: width 0.6s, background 0.6s;
}
.ic.menu:focus ~ ul.main-nav {
width: 300px;
}
}
@media only screen and (min-width: 769px) {
.ic.menu {
display: none;
}
ul.main-nav {
display: block;
position: relative;
}
}
.fa-star {
font-size: 20px;
color: gray;
}
.checked {
color: gold;
}
.project-card, .review-card {
width: 100%;
max-width: 350px;
margin: auto;
font-size: 0.5rem; /* Make font size smaller */
}
.card-body p {
margin-bottom: 10px;
font-size: 0.85rem; /* Reduce text size in paragraphs */
}
.text-muted {
font-size: 0.8rem; /* Make muted text slightly smaller */
}
.carousel-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5); /* Затемнение */
z-index: 1;
}
.carousel-item img {
z-index: -1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,2 @@
<!-- Placeholder for favicon - this would be a binary file in real scenario -->
<!-- Create a simple favicon.ico file -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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