This commit is contained in:
2024-12-21 21:56:15 +09:00
parent 1e64a432ab
commit c535a51953
42 changed files with 1069 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

30
.docker/admin/Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM python:3.9-alpine
WORKDIR /app
# Устанавливаем временную директорию
ENV TMPDIR=/tmp/tempdir
RUN mkdir -p $TMPDIR && chmod 1777 $TMPDIR
# Устанавливаем системные зависимости для Alpine
RUN apk add --no-cache \
gcc \
musl-dev \
mariadb-dev \
netcat-openbsd \
net-tools \
iputils
# Копируем только requirements.txt для кэширования зависимостей
COPY .docker/admin/requirements.txt /app/requirements.txt
# Устанавливаем Python-зависимости
RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
# Копируем весь проект
COPY . /app
RUN chmod +x .docker/admin/entrypoint.sh
ENTRYPOINT [".docker/admin/entrypoint.sh"]
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

14
.docker/admin/entrypoint.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# Ожидание доступности базы данных
until nc -z -v -w30 $DB_HOST $DB_PORT; do
echo "Ожидание базы данных..."
sleep 1
done
# Выполняем миграции
python manage.py makemigrations --no-input
python manage.py migrate --no-input
# Запускаем приложение
exec "$@"

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django_extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

12
.docker/bot/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.10-slim
WORKDIR /app
# Копируем весь проект в контейнер
COPY ../../ /app
# Устанавливаем зависимости только для bot
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/bot/requirements.txt
# Команда запуска для бота
CMD ["python", "manage.py" ,"run_bot.py"]

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django_extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -0,0 +1,14 @@
FROM python:3.10-slim
WORKDIR /app
# Копируем весь проект в контейнер
COPY ../../ /app
RUN chmod +x .docker/scheduler/entrypoint.sh
ENTRYPOINT [".docker/scheduler/entrypoint.sh"]
# Устанавливаем зависимости только для scheduler
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/scheduler/requirements.txt
# Команда запуска для планировщика
CMD ["python", "manage.py", "run_scheduler"]

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Выполняем миграции
python manage.py migrate --no-input
# Запускаем приложение
exec "$@"

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django-extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.1.4 on 2024-12-19 12:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('antifroud', '0001_initial'),
('hotels', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='roomdiscrepancy',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
),
migrations.AddField(
model_name='synclog',
name='hotel',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
),
migrations.AddField(
model_name='violationlog',
name='hotel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
),
]

0
app_settings/__init__.py Normal file
View File

24
app_settings/admin.py Normal file
View File

@@ -0,0 +1,24 @@
from django.contrib import admin
from .models import LocalDatabase, GlobalHotelSettings, GlobalSystemSettings, TelegramSettings, EmailSettings
@admin.register(LocalDatabase)
class LocalDatabaseAdmin(admin.ModelAdmin):
list_display = ['name', 'host', 'port', 'user', 'database', 'is_active']
search_fields = ['name', 'host', 'user', 'database']
@admin.register(GlobalHotelSettings)
class GlobalHotelSettingsAdmin(admin.ModelAdmin):
list_display = ['check_in_time', 'check_out_time', 'global_timezone']
list_filter = ['global_timezone']
@admin.register(GlobalSystemSettings)
class GlobalSystemSettingsAdmin(admin.ModelAdmin):
list_display = ['system_name', 'system_version', 'server_timezone']
@admin.register(TelegramSettings)
class TelegramSettingsAdmin(admin.ModelAdmin):
list_display = ['bot_token', 'username']
@admin.register(EmailSettings)
class EmailSettingsAdmin(admin.ModelAdmin):
list_display = ['smtp_server', 'smtp_port', 'smtp_user', 'from_email']

View File

@@ -0,0 +1,50 @@
# settings.py
from .models import LocalDatabase
from decouple import config
from django.conf import settings
from .models import LocalDatabase
def load_database_settings():
# Загружаем настройки из базы данных
local_db_settings = LocalDatabase.objects.all()
for db in local_db_settings:
# Пример добавления дополнительной базы данных
settings.DATABASES[db.name] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': db.db_name,
'USER': db.username,
'PASSWORD': db.password,
'HOST': db.host,
'PORT': db.port,
}
# Вызов этой функции при старте проекта, например, в файле wsgi.py
load_database_settings()
# Чтение локальных баз данных
local_databases = LocalDatabase.objects.filter(is_active=True)
# Основная база данных
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT'),
},
}
# Добавление локальных баз данных
for db in local_databases:
DATABASES[db.name] = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': db.name,
'USER': db.user,
'PASSWORD': db.password,
'HOST': db.host,
'PORT': db.port,
}

14
app_settings/apps.py Normal file
View File

@@ -0,0 +1,14 @@
from django.apps import AppConfig, apps
class AppSettingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app_settings'
def ready(self):
# Проверяем, что приложения готовы
if not apps.ready:
return
try:
import app_settings.signals # Регистрация сигналов
except ImportError as e:
print(f"Ошибка импорта signals: {e}")

File diff suppressed because one or more lines are too long

View File

77
app_settings/models.py Normal file
View File

@@ -0,0 +1,77 @@
from django.db import models
import pytz
class LocalDatabase(models.Model):
name = models.CharField(max_length=255, verbose_name="Имя базы данных")
host = models.CharField(max_length=255, verbose_name="Хост базы данных", default="localhost")
port = models.IntegerField(default=5432, verbose_name="Порт базы данных")
user = models.CharField(max_length=255, verbose_name="Пользователь базы данных")
database = models.CharField(max_length=255, verbose_name="Название базы данных")
password = models.CharField(max_length=255, verbose_name="Пароль базы данных")
is_active = models.BooleanField(default=True, verbose_name="Активна ли база данных")
def __str__(self):
return self.name
class Meta:
verbose_name = "База данных"
verbose_name_plural = "Базы данных"
class TelegramSettings(models.Model):
bot_token = models.CharField(max_length=255, help_text="Токен вашего бота Telegram")
chat_id = models.CharField(max_length=255, help_text="ID чата для отправки сообщений")
username = models.CharField(max_length=255, help_text="Имя пользователя для бота", blank=True, null=True)
def __str__(self):
return f"Telegram Bot ({self.username})"
class Meta:
verbose_name = "Telegram"
verbose_name_plural = "Telegram"
class EmailSettings(models.Model):
smtp_server = models.CharField(max_length=255, help_text="SMTP сервер для отправки почты")
smtp_port = models.IntegerField(default=587, help_text="SMTP порт для почты")
smtp_user = models.CharField(max_length=255, help_text="Имя пользователя для SMTP")
smtp_password = models.CharField(max_length=255, help_text="Пароль для SMTP")
from_email = models.EmailField(help_text="Email для отправки сообщений")
class Meta:
verbose_name = "E-mail"
verbose_name_plural = "E-mails"
def __str__(self):
return f"Email Settings for {self.from_email}"
class GlobalHotelSettings(models.Model):
check_in_time = models.TimeField(help_text="Время заезда")
check_out_time = models.TimeField(help_text="Время выезда")
currency = models.CharField(max_length=3, help_text="Валюта")
global_timezone = models.CharField(
max_length=63,
choices=[(tz, tz) for tz in pytz.all_timezones],
default='UTC',
)
def __str__(self):
return "Настройки отеля"
class Meta:
verbose_name = "Настройки отеля"
verbose_name_plural = "Настройки отеля"
class GlobalSystemSettings(models.Model):
system_name = models.CharField(max_length=255, help_text="Название системы")
system_version = models.CharField(max_length=255, help_text="Версия системы")
server_timezone = models.CharField(
max_length=63,
choices=[(tz, tz) for tz in pytz.all_timezones],
default='UTC',
)
def __str__(self):
return "Настройки системы"
class Meta:
verbose_name = "Настройки системы"
verbose_name_plural = "Настройки системы"

9
app_settings/signals.py Normal file
View File

@@ -0,0 +1,9 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import GlobalSystemSettings
@receiver(post_save, sender=GlobalSystemSettings)
def update_system_settings(sender, instance, **kwargs):
# Безопасное использование сигнала
if instance:
print(f"Настройки системы обновлены: {instance.system_name}")

3
app_settings/tests.py Normal file
View File

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

11
app_settings/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from django.http import HttpResponse
app_name = 'settings'
def placeholder_view(request):
return HttpResponse("Placeholder for settings app.")
urlpatterns = [
path('', placeholder_view, name='settings_placeholder'),
]

3
app_settings/views.py Normal file
View File

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

73
docker-compose.yml Normal file
View File

@@ -0,0 +1,73 @@
version: '3.9'
services:
mysql:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
TMPDIR: /var/tmp
ports:
- "${DB_PORT}:3306"
volumes:
- mysql_data:/var/lib/mysql
- /var/tmp:/var/tmp
django-admin:
build:
context: .
dockerfile: .docker/admin/Dockerfile
container_name: django-admin
restart: on-failure
volumes:
- .:/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=touchh.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- LOG_LEVEL=${LOG_LEVEL}
depends_on:
- mysql
ports:
- "8000:8000"
command: python manage.py runserver 0.0.0.0:8000
bot:
build:
context: .
dockerfile: .docker/bot/Dockerfile
container_name: bot
restart: on-failure
volumes:
- .:/app
environment:
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- LOG_LEVEL=${LOG_LEVEL}
depends_on:
- mysql
scheduler:
build:
context: .
dockerfile: .docker/scheduler/Dockerfile
container_name: scheduler
restart: on-failure
volumes:
- .:/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- SCHEDULED_SYNC_LOG_LEVEL=${SCHEDULED_SYNC_LOG_LEVEL}
depends_on:
- mysql
volumes:
mysql_data:

65
req1.txt Normal file
View File

@@ -0,0 +1,65 @@
ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiosignal==1.3.2
anyio==4.7.0
APScheduler==3.11.0
asgiref==3.8.1
async-timeout==5.0.1
attrs==24.3.0
certifi==2024.12.14
cffi==1.17.1
charset-normalizer==3.4.0
cryptography==44.0.0
defusedxml==0.7.1
Django==5.1.4
django-environ==0.11.2
django-extensions==3.2.3
django-filter==24.3
django-health-check==3.18.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
exceptiongroup==1.2.2
fonttools==4.55.3
fpdf2==2.8.2
frozenlist==1.5.0
geoip2==4.8.1
git-filter-repo==2.47.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.2.0
openpyxl==3.1.5
pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0
propcache==0.2.1
psycopg==3.2.3
pycparser==2.22
PyMySQL==1.1.1
python-dateutil==2.9.0.post0
python-decouple==3.8
python-dotenv==1.0.1
python-telegram-bot==21.9
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rpds-py==0.22.3
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.3
typing_extensions==4.12.2
tzdata==2024.2
tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3

View File

@@ -0,0 +1,27 @@
import os
import django
import asyncio
from django.core.management.base import BaseCommand
from scheduler.tasks import setup_scheduler
class Command(BaseCommand):
help = "Запуск планировщика задач"
def handle(self, *args, **options):
# Устанавливаем Django окружение
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
django.setup()
# Основная асинхронная функция
async def start_scheduler():
scheduler = await setup_scheduler()
self.stdout.write(self.style.SUCCESS("Планировщик задач успешно запущен."))
try:
while True:
await asyncio.sleep(3600) # Бесконечный цикл для поддержания работы
except asyncio.CancelledError:
scheduler.shutdown()
# Запускаем планировщик в асинхронном режиме
asyncio.run(start_scheduler())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

95
scheduler/task_loader.py Normal file
View File

@@ -0,0 +1,95 @@
import os
import inspect
import importlib
import asyncio
import logging
from typing import List, Tuple
from pathspec import PathSpec
from apscheduler.schedulers.asyncio import AsyncIOScheduler
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def reload_tasks_periodically(scheduler: AsyncIOScheduler):
"""Перезагрузка задач из базы данных каждые 5 минут."""
async def reload():
from scheduler.tasks import load_tasks_to_scheduler
try:
await load_tasks_to_scheduler(scheduler)
logger.info("Задачи успешно перезагружены.")
except Exception as e:
logger.error(f"Ошибка перезагрузки задач: {e}")
scheduler.add_job(lambda: asyncio.run(reload()), "interval", minutes=5)
def load_gitignore_patterns(project_root: str) -> PathSpec:
"""
Загружает паттерны из файла .gitignore.
"""
gitignore_path = os.path.join(project_root, ".gitignore")
try:
if os.path.exists(gitignore_path):
with open(gitignore_path, "r", encoding="utf-8") as f:
patterns = f.readlines()
return PathSpec.from_lines("gitwildmatch", patterns)
except Exception as e:
logger.warning(f"Ошибка загрузки .gitignore: {e}")
return PathSpec.from_lines("gitwildmatch", [])
def get_project_functions() -> List[Tuple[str, str]]:
"""
Сканирует проект и возвращает список всех функций в формате (путь, имя функции),
исключая файлы и папки, указанные в .gitignore.
"""
functions = []
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Загружаем паттерны из .gitignore
gitignore_spec = load_gitignore_patterns(project_root)
for root, dirs, files in os.walk(project_root):
# Исключаем директории, указанные в .gitignore
dirs[:] = [d for d in dirs if not gitignore_spec.match_file(os.path.relpath(os.path.join(root, d), project_root))]
for file in files:
file_path = os.path.relpath(os.path.join(root, file), project_root)
if (
file.endswith(".py") and
not file.startswith("__") and
not gitignore_spec.match_file(file_path)
):
module_path = os.path.relpath(os.path.join(root, file), project_root)
module_name = module_path.replace(os.sep, ".").replace(".py", "")
try:
spec = importlib.util.find_spec(module_name)
if spec is not None:
module = importlib.import_module(module_name)
for name, func in inspect.getmembers(module, inspect.isfunction):
functions.append((f"{module_name}.{name}", name))
except Exception as e:
logger.error(f"Ошибка при обработке модуля {module_name}: {e}")
return functions
def execute_function(function_path: str):
"""
Выполняет функцию по указанному пути.
"""
try:
module_name, func_name = function_path.rsplit(".", 1)
spec = importlib.util.find_spec(module_name)
if spec is None:
raise ImportError(f"Модуль {module_name} не найден")
module = importlib.import_module(module_name)
if not hasattr(module, func_name):
raise AttributeError(f"Функция {func_name} отсутствует в модуле {module_name}")
func = getattr(module, func_name)
logger.info(f"Выполняется функция: {function_path}")
return func()
except Exception as e:
logger.error(f"Ошибка выполнения функции {function_path}: {e}")
return None

14
scheduler/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path
from django.http import HttpResponse
def placeholder_view(request):
"""
Заглушка для URL-адресов приложения scheduler.
"""
return HttpResponse("Это заглушка для приложения scheduler.")
app_name = "scheduler"
urlpatterns = [
path("", placeholder_view, name="scheduler_placeholder"),
]

View File

@@ -0,0 +1,43 @@
/*global gettext*/
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const fieldsets = document.querySelectorAll('fieldset.collapse');
for (const [i, elem] of fieldsets.entries()) {
// Don't hide if fields in this fieldset have errors
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
elem.classList.add('collapsed');
const h2 = elem.querySelector('h2');
const link = document.createElement('a');
link.id = 'fieldsetcollapser' + i;
link.className = 'collapse-toggle';
link.href = '#';
link.textContent = gettext('Show');
h2.appendChild(document.createTextNode(' ('));
h2.appendChild(link);
h2.appendChild(document.createTextNode(')'));
}
}
// Add toggle to hide/show anchor tag
const toggleFunc = function(ev) {
if (ev.target.matches('.collapse-toggle')) {
ev.preventDefault();
ev.stopPropagation();
const fieldset = ev.target.closest('fieldset');
if (fieldset.classList.contains('collapsed')) {
// Show
ev.target.textContent = gettext('Hide');
fieldset.classList.remove('collapsed');
} else {
// Hide
ev.target.textContent = gettext('Show');
fieldset.classList.add('collapsed');
}
}
};
document.querySelectorAll('fieldset.module').forEach(function(el) {
el.addEventListener('click', toggleFunc);
});
});
}