fix: correct imports after frontend/Igor merge
- Fixed PHONE_FIELD import from _helpers module - Fixed _normalize_phone import from employees.utils
This commit is contained in:
parent
64c0abf548
commit
01b35c3b83
@ -1,198 +0,0 @@
|
||||
# 🚀 Быстрый старт: Тестирование API
|
||||
|
||||
## Выбор инструмента
|
||||
|
||||
| Инструмент | Использование | Сложность |
|
||||
|------------|---------------|-----------|
|
||||
| **Postman** | GUI, удобный интерфейс | ⭐⭐ |
|
||||
| **REST Client** | VS Code, быстрое тестирование | ⭐ |
|
||||
| **pytest** | Автоматизация, CI/CD | ⭐⭐⭐ |
|
||||
| **cURL** | Консоль, скрипты | ⭐⭐ |
|
||||
|
||||
## 1️⃣ Postman (рекомендуется для начинающих)
|
||||
|
||||
### Установка
|
||||
1. Скачайте [Postman](https://www.postman.com/downloads/)
|
||||
2. Установите и запустите
|
||||
|
||||
### Импорт коллекции
|
||||
```bash
|
||||
1. File → Import
|
||||
2. Выберите файл: EUSRR_API.postman_collection.json
|
||||
3. Также импортируйте: EUSRR_Local.postman_environment.json
|
||||
4. Выберите environment "EUSRR Local" в правом верхнем углу
|
||||
```
|
||||
|
||||
### Первый запрос
|
||||
```bash
|
||||
1. Откройте "Authentication → Login"
|
||||
2. Нажмите "Send"
|
||||
3. Токен автоматически сохранится! ✅
|
||||
4. Теперь можно выполнять любые запросы
|
||||
```
|
||||
|
||||
## 2️⃣ REST Client (для VS Code)
|
||||
|
||||
### Установка
|
||||
```bash
|
||||
1. Откройте Extensions (Ctrl+Shift+X)
|
||||
2. Найдите "REST Client"
|
||||
3. Установите от Huachao Mao
|
||||
```
|
||||
|
||||
### Использование
|
||||
```bash
|
||||
1. Откройте файл: api_tests.http
|
||||
2. Найдите "### Login"
|
||||
3. Нажмите "Send Request" над запросом
|
||||
4. Скопируйте access token
|
||||
5. Замените @accessToken на ваш токен
|
||||
6. Готово! 🎉
|
||||
```
|
||||
|
||||
## 3️⃣ pytest (автоматизированное тестирование)
|
||||
|
||||
### Запуск всех тестов
|
||||
```bash
|
||||
# В Docker
|
||||
docker-compose exec web pytest tests/api/
|
||||
|
||||
# Локально
|
||||
.venv/Scripts/python -m pytest tests/api/
|
||||
```
|
||||
|
||||
### Запуск конкретного теста
|
||||
```bash
|
||||
docker-compose exec web pytest tests/api/examples/test_api_examples.py::TestAuthentication::test_login_with_email -v
|
||||
```
|
||||
|
||||
### С покрытием кода
|
||||
```bash
|
||||
docker-compose exec web pytest tests/api/ --cov=api --cov-report=html
|
||||
```
|
||||
|
||||
## 4️⃣ cURL (консоль)
|
||||
|
||||
### Получить токен
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/auth/token/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin@example.com", "password": "password"}'
|
||||
```
|
||||
|
||||
### Использовать токен
|
||||
```bash
|
||||
TOKEN="your_token_here"
|
||||
|
||||
curl -X GET http://localhost:8000/api/v1/employees/ \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 📋 Основные endpoints
|
||||
|
||||
### Аутентификация
|
||||
- `POST /api/auth/token/` - Получить токен
|
||||
- `POST /api/auth/token/refresh/` - Обновить токен
|
||||
- `POST /api/v1/auth/register/` - Регистрация
|
||||
|
||||
### Сотрудники
|
||||
- `GET /api/v1/employees/` - Список
|
||||
- `GET /api/v1/employees/me/` - Текущий пользователь
|
||||
- `GET /api/v1/employees/{id}/` - Конкретный сотрудник
|
||||
- `PATCH /api/v1/employees/{id}/` - Обновить
|
||||
|
||||
### Посты
|
||||
- `GET /api/v1/posts/` - Список
|
||||
- `POST /api/v1/posts/` - Создать
|
||||
- `POST /api/v1/posts/{id}/like/` - Лайк
|
||||
|
||||
### Мессенджер
|
||||
- `GET /api/v1/communications/chats/` - Чаты
|
||||
- `GET /api/v1/communications/messages/?chat={id}` - Сообщения
|
||||
- `POST /api/v1/communications/messages/` - Отправить
|
||||
|
||||
### Документы
|
||||
- `GET /api/v1/documents/` - Список
|
||||
- `POST /api/v1/documents/` - Создать
|
||||
- `GET /api/v1/documents/{id}/` - Получить
|
||||
|
||||
### Заявки
|
||||
- `GET /api/v1/requests/` - Список
|
||||
- `POST /api/v1/requests/` - Создать
|
||||
- `PATCH /api/v1/requests/{id}/` - Обновить
|
||||
|
||||
### Уведомления
|
||||
- `GET /api/v1/notifications/` - Список
|
||||
- `POST /api/v1/notifications/{id}/mark_as_read/` - Пометить прочитанным
|
||||
|
||||
## 🔐 Пример авторизованного запроса
|
||||
|
||||
### Postman
|
||||
```
|
||||
Authorization: Bearer {{access_token}}
|
||||
(автоматически подставляется)
|
||||
```
|
||||
|
||||
### REST Client
|
||||
```http
|
||||
GET {{baseUrl}}/api/v1/employees/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
```
|
||||
|
||||
### cURL
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
http://localhost:8000/api/v1/employees/
|
||||
```
|
||||
|
||||
### Python requests
|
||||
```python
|
||||
import requests
|
||||
|
||||
token = "your_token"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/employees/",
|
||||
headers=headers
|
||||
)
|
||||
```
|
||||
|
||||
## ⚠️ Частые ошибки
|
||||
|
||||
### 401 Unauthorized
|
||||
```
|
||||
❌ Токен отсутствует или истек
|
||||
✅ Выполните Login снова
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
```
|
||||
❌ Нет прав доступа
|
||||
✅ Проверьте роль пользователя
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
```
|
||||
❌ Неверный URL или ID
|
||||
✅ Проверьте endpoint
|
||||
```
|
||||
|
||||
### 400 Bad Request
|
||||
```
|
||||
❌ Ошибка в данных запроса
|
||||
✅ Проверьте JSON и обязательные поля
|
||||
```
|
||||
|
||||
## 📚 Полная документация
|
||||
|
||||
Детальная документация: [docs/guides/API_TESTING_GUIDE.md](docs/guides/API_TESTING_GUIDE.md)
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
1. ✅ Выберите инструмент тестирования
|
||||
2. ✅ Импортируйте коллекцию/откройте файл
|
||||
3. ✅ Выполните Login
|
||||
4. ✅ Протестируйте endpoints
|
||||
5. ✅ Изучите примеры тестов
|
||||
|
||||
**Вопросы?** Смотрите [API_TESTING_GUIDE.md](docs/guides/API_TESTING_GUIDE.md)
|
||||
@ -1,289 +0,0 @@
|
||||
# EUSRR Docker Orchestration Guide
|
||||
|
||||
## Обзор архитектуры
|
||||
|
||||
Проект EUSRR теперь имеет полную оркестрацию с использованием Docker Compose:
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Nginx │ :80 - Reverse Proxy
|
||||
│ (Gateway) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
├──────────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌─▼────────────┐
|
||||
│ Frontend │ │ Backend │
|
||||
│ (Next.js) │ │ (Django) │
|
||||
│ :3000 │ │ :8000 │
|
||||
└──────┬──────┘ └──┬─────┬─────┘
|
||||
│ │ │
|
||||
└───────┬───┘ │
|
||||
│ │
|
||||
┌──────▼───┐ ┌──▼────┐
|
||||
│ Redis │ │ DB │
|
||||
│ :6379 │ │ :5432 │
|
||||
└──────────┘ └────────┘
|
||||
```
|
||||
|
||||
## Сервисы
|
||||
|
||||
### 1. **db** (PostgreSQL 16)
|
||||
- Порт: `5432`
|
||||
- База данных Django
|
||||
- Persistent volume: `postgres_data`
|
||||
|
||||
### 2. **redis** (Redis 7)
|
||||
- Порт: `6379`
|
||||
- Кэш и Channels backend
|
||||
- Persistent volume: `redis_data`
|
||||
|
||||
### 3. **web** (Django Backend)
|
||||
- Порт: `8000`
|
||||
- ASGI сервер (Daphne)
|
||||
- REST API + WebSocket
|
||||
- Подключается к `db` и `redis`
|
||||
|
||||
### 4. **celery** (Celery Worker)
|
||||
- Фоновые задачи
|
||||
- Подключается к `redis` и `db`
|
||||
|
||||
### 5. **celery-beat** (Celery Scheduler)
|
||||
- Планировщик задач
|
||||
- Django Celery Beat
|
||||
|
||||
### 6. **frontend** (Next.js)
|
||||
- Порт: `3000`
|
||||
- Корпоративный фронтенд
|
||||
- Использует `web` backend через proxy
|
||||
|
||||
### 7. **nginx** (Reverse Proxy)
|
||||
- Порт: `80` (основной вход)
|
||||
- Маршрутизация:
|
||||
- `/` → Frontend (Next.js)
|
||||
- `/api/` → Backend API
|
||||
- `/admin/` → Django Admin
|
||||
- `/ws/` → WebSocket
|
||||
- `/static/`, `/media/` → Статика
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Запуск всех сервисов
|
||||
|
||||
```bash
|
||||
# Создайте .env файл из примера
|
||||
cp .env.example .env
|
||||
|
||||
# Запустите все сервисы
|
||||
docker-compose up -d
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Доступ к приложению
|
||||
|
||||
- **Основной вход (через Nginx)**: http://localhost
|
||||
- **Frontend напрямую**: http://localhost:3000
|
||||
- **Backend API**: http://localhost:8000/api/
|
||||
- **Django Admin**: http://localhost/admin/
|
||||
|
||||
### Остановка
|
||||
|
||||
```bash
|
||||
# Остановка всех сервисов
|
||||
docker-compose down
|
||||
|
||||
# Остановка с удалением volumes
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Основные переменные в `.env`:
|
||||
|
||||
```env
|
||||
# Database
|
||||
POSTGRES_DB=eusrr
|
||||
POSTGRES_USER=eusrr_user
|
||||
POSTGRES_PASSWORD=change-me-postgres-password
|
||||
|
||||
# Django
|
||||
DEBUG=True
|
||||
SECRET_KEY=dev-secret-key
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,web,nginx
|
||||
|
||||
# Ports
|
||||
WEB_PORT=8000
|
||||
FRONTEND_PORT=3000
|
||||
NGINX_PORT=80
|
||||
```
|
||||
|
||||
### Frontend Environment
|
||||
|
||||
Файл `corp.front/.env.local`:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://web:8000
|
||||
NEXT_PUBLIC_WS_URL=ws://web:8000
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Режим разработки
|
||||
|
||||
```bash
|
||||
# Backend отдельно (с hot-reload)
|
||||
cd backend
|
||||
../.venv/Scripts/python manage.py runserver
|
||||
|
||||
# Frontend отдельно (с hot-reload)
|
||||
cd corp.front
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Режим Production
|
||||
|
||||
```bash
|
||||
# Полная оркестрация через Docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Миграции и первичная настройка
|
||||
|
||||
```bash
|
||||
# Выполнить миграции
|
||||
docker-compose exec web python manage.py migrate
|
||||
|
||||
# Создать суперпользователя
|
||||
docker-compose exec web python manage.py createsuperuser
|
||||
|
||||
# Собрать статику
|
||||
docker-compose exec web python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
```bash
|
||||
# Статус сервисов
|
||||
docker-compose ps
|
||||
|
||||
# Логи конкретного сервиса
|
||||
docker-compose logs -f web
|
||||
docker-compose logs -f frontend
|
||||
docker-compose logs -f nginx
|
||||
|
||||
# Использование ресурсов
|
||||
docker stats
|
||||
```
|
||||
|
||||
## Отладка
|
||||
|
||||
### Проверка сети
|
||||
|
||||
```bash
|
||||
# Проверить связь frontend → backend
|
||||
docker-compose exec frontend ping web
|
||||
|
||||
# Проверить связь backend → db
|
||||
docker-compose exec web pg_isready -h db -U eusrr_user
|
||||
```
|
||||
|
||||
### Доступ к контейнерам
|
||||
|
||||
```bash
|
||||
# Backend shell
|
||||
docker-compose exec web bash
|
||||
docker-compose exec web python manage.py shell
|
||||
|
||||
# Frontend shell
|
||||
docker-compose exec frontend sh
|
||||
|
||||
# Database
|
||||
docker-compose exec db psql -U eusrr_user -d eusrr
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
Persistent данные сохраняются в volumes:
|
||||
|
||||
- `postgres_data` - база данных
|
||||
- `redis_data` - Redis данные
|
||||
- `static_volume` - статические файлы Django
|
||||
- `media_volume` - загруженные медиафайлы
|
||||
|
||||
## Сеть
|
||||
|
||||
Все сервисы находятся в сети `eusrr_network` и могут обращаться друг к другу по именам сервисов:
|
||||
|
||||
- `web` - Django backend
|
||||
- `frontend` - Next.js frontend
|
||||
- `db` - PostgreSQL
|
||||
- `redis` - Redis
|
||||
- `nginx` - Nginx
|
||||
|
||||
## Nginx маршрутизация
|
||||
|
||||
- `location /` - Next.js фронтенд
|
||||
- `location /api/` - Django REST API
|
||||
- `location /admin/` - Django Admin
|
||||
- `location /ws/` - WebSocket (Django Channels)
|
||||
- `location /static/` - Статические файлы
|
||||
- `location /media/` - Медиа файлы
|
||||
- `location /health` - Health check endpoint
|
||||
|
||||
## Production Deployment
|
||||
|
||||
Для production окружения:
|
||||
|
||||
1. Обновите `.env`:
|
||||
- Установите `DEBUG=False`
|
||||
- Смените `SECRET_KEY`
|
||||
- Обновите `ALLOWED_HOSTS`
|
||||
- Используйте надежные пароли БД
|
||||
|
||||
2. Настройте HTTPS (добавьте SSL в nginx)
|
||||
|
||||
3. Используйте внешние managed сервисы для БД и Redis (опционально)
|
||||
|
||||
4. Настройте backup для volumes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Frontend не может подключиться к backend
|
||||
|
||||
Проверьте переменные окружения в `corp.front/.env.local` и убедитесь, что используется правильный URL для Docker сети.
|
||||
|
||||
### Backend не запускается
|
||||
|
||||
```bash
|
||||
# Проверьте логи
|
||||
docker-compose logs web
|
||||
|
||||
# Проверьте подключение к БД
|
||||
docker-compose exec web python manage.py check --database default
|
||||
```
|
||||
|
||||
### Nginx 502 Bad Gateway
|
||||
|
||||
Убедитесь, что `web` и `frontend` сервисы запущены:
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
## Файлы конфигурации
|
||||
|
||||
- `docker-compose.yml` - Оркестрация сервисов
|
||||
- `nginx.conf` - Конфигурация Nginx
|
||||
- `backend/Dockerfile` - Django image
|
||||
- `corp.front/Dockerfile` - Next.js image
|
||||
- `.env.example` - Шаблон переменных окружения
|
||||
- `corp.front/.env.example` - Шаблон для frontend
|
||||
|
||||
---
|
||||
|
||||
**Дата создания**: 19 февраля 2025
|
||||
**Версия**: 1.0
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,37 +0,0 @@
|
||||
{
|
||||
"id": "eusrr-local-env",
|
||||
"name": "EUSRR Local",
|
||||
"values": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://localhost:8000",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "access_token",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "refresh_token",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "test_user_email",
|
||||
"value": "admin@example.com",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "test_user_password",
|
||||
"value": "password",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment"
|
||||
}
|
||||
372
README.md
372
README.md
@ -1,372 +0,0 @@
|
||||
# EUSRR - Корпоративная система управления
|
||||
|
||||
Комплексная корпоративная информационная система с интеграцией Active Directory/LDAP для управления сотрудниками, документами, заявками и внутренними коммуникациями.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
### 🏢 Управление сотрудниками
|
||||
- Интеграция с Active Directory через LDAP
|
||||
- Автоматическая синхронизация данных сотрудников
|
||||
- Управление отделами и должностями
|
||||
- Профили сотрудников с фотографиями и контактами
|
||||
- Иерархия отделов с назначением руководителей
|
||||
|
||||
### 📄 Система документооборота
|
||||
- Загрузка и распространение документов
|
||||
- Отправка документов всем сотрудникам, конкретным отделам или индивидуально
|
||||
- Автоматические уведомления о новых документах
|
||||
- Отслеживание ознакомления с документами
|
||||
- Визуальное выделение неознакомленных документов
|
||||
|
||||
### 📬 Система уведомлений
|
||||
- Веб-уведомления в реальном времени (WebSocket)
|
||||
- Email-уведомления с настраиваемой периодичностью (мгновенно, ежедневно, еженедельно)
|
||||
- Telegram-уведомления через бота
|
||||
- Индивидуальные настройки уведомлений для каждого пользователя
|
||||
- Звуковые оповещения о новых уведомлениях
|
||||
|
||||
### 📋 Система заявок
|
||||
- Создание и отслеживание заявок
|
||||
- Назначение ответственных отделов
|
||||
- История изменений заявок
|
||||
- Комментарии и обсуждения
|
||||
|
||||
### 💬 Внутренние коммуникации
|
||||
- Лента новостей организации
|
||||
- Посты с комментариями
|
||||
- Упоминания сотрудников (@mention)
|
||||
- Ленты новостей по отделам
|
||||
|
||||
### 📅 Календарь событий
|
||||
- Корпоративный календарь
|
||||
- События отделов и общеорганизационные события
|
||||
- Интеграция с календарем на главной странице
|
||||
|
||||
## Технологический стек
|
||||
|
||||
### Backend
|
||||
- **Python 3.13.5**
|
||||
- **Django 5.2.4** - основной веб-фреймворк
|
||||
- **Django REST Framework** - REST API
|
||||
- **Django Channels** - WebSocket поддержка
|
||||
- **PostgreSQL / SQLite** - база данных
|
||||
- **python-ldap** - интеграция с Active Directory
|
||||
- **Celery** (опционально) - асинхронные задачи
|
||||
|
||||
### Frontend
|
||||
- **Bootstrap 5** - UI фреймворк
|
||||
- **Vanilla JavaScript (ES6+)** - модульная архитектура
|
||||
- **WebSocket** - реальное время
|
||||
- **Fetch API** - HTTP запросы
|
||||
|
||||
### Интеграции
|
||||
- **Active Directory (LDAP)** - управление пользователями
|
||||
- **Telegram Bot API** - уведомления в Telegram
|
||||
- **SMTP (Yandex)** - email-рассылка
|
||||
- **SMS Gateway** - SMS-уведомления
|
||||
|
||||
## Установка и запуск
|
||||
|
||||
### Предварительные требования
|
||||
|
||||
- Python 3.11+
|
||||
- PostgreSQL 14+ или SQLite (для разработки)
|
||||
- Active Directory (опционально, можно отключить через `LDAP_ENABLED=False`)
|
||||
- Git
|
||||
|
||||
### Установка
|
||||
|
||||
1. Клонирование репозитория:
|
||||
```bash
|
||||
git clone https://github.com/IgorNadein/EUSRR.git
|
||||
cd EUSRR
|
||||
```
|
||||
|
||||
2. Создание виртуального окружения:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# или
|
||||
.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. Установка зависимостей:
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Настройка окружения:
|
||||
```bash
|
||||
# Скопируйте .env.example в .env и настройте параметры
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Основные параметры в `.env`:
|
||||
```env
|
||||
# База данных
|
||||
USE_SQLITE=True # True для SQLite, False для PostgreSQL
|
||||
|
||||
# Django
|
||||
SECRET_KEY=your-secret-key
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# LDAP (можно отключить)
|
||||
LDAP_ENABLED=False # True для интеграции с AD
|
||||
|
||||
# Email
|
||||
EMAIL_HOST=smtp.yandex.ru
|
||||
EMAIL_PORT=465
|
||||
EMAIL_HOST_USER=your-email@yandex.ru
|
||||
EMAIL_HOST_PASSWORD=your-password
|
||||
|
||||
# Telegram (опционально)
|
||||
TELEGRAM_BOT_TOKEN=your-bot-token
|
||||
```
|
||||
|
||||
5. Применение миграций:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
6. Создание суперпользователя:
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
7. Сбор статических файлов:
|
||||
```bash
|
||||
python manage.py collectstatic --noinput
|
||||
```
|
||||
|
||||
### Запуск разработки
|
||||
|
||||
```bash
|
||||
# Запуск сервера разработки
|
||||
python manage.py runserver 9000
|
||||
|
||||
# В отдельном терминале - Telegram бот (опционально)
|
||||
python manage.py run_telegram_bot
|
||||
```
|
||||
|
||||
Приложение будет доступно по адресу: http://localhost:9000
|
||||
|
||||
### Запуск в продакшене
|
||||
|
||||
1. Настройте `.env` для продакшена:
|
||||
```env
|
||||
DEBUG=False
|
||||
USE_SQLITE=False
|
||||
ALLOWED_HOSTS=your-domain.com
|
||||
LDAP_ENABLED=True
|
||||
```
|
||||
|
||||
2. Используйте WSGI сервер (Gunicorn):
|
||||
```bash
|
||||
pip install gunicorn
|
||||
gunicorn eusrr_backend.wsgi:application --bind 0.0.0.0:8000
|
||||
```
|
||||
|
||||
3. Настройте веб-сервер (Nginx) как reverse proxy
|
||||
|
||||
4. Настройте supervisor/systemd для автозапуска
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
EUSRR/
|
||||
├── backend/ # Django проект
|
||||
│ ├── api/ # REST API
|
||||
│ │ └── v1/ # API версия 1
|
||||
│ │ ├── documents/ # API документов
|
||||
│ │ ├── employees/ # API сотрудников
|
||||
│ │ └── ...
|
||||
│ ├── documents/ # Приложение документов
|
||||
│ ├── employees/ # Приложение сотрудников
|
||||
│ ├── notifications/ # Система уведомлений
|
||||
│ ├── requests_app/ # Система заявок
|
||||
│ ├── feed/ # Лента новостей
|
||||
│ ├── calendar_app/ # Календарь событий
|
||||
│ ├── communications/ # Коммуникации
|
||||
│ ├── static/ # Статические файлы
|
||||
│ │ ├── css/ # Стили
|
||||
│ │ └── js/ # JavaScript модули
|
||||
│ ├── templates/ # HTML шаблоны
|
||||
│ ├── media/ # Загруженные файлы
|
||||
│ └── eusrr_backend/ # Настройки Django
|
||||
└── README.md # Этот файл
|
||||
```
|
||||
|
||||
## Основные функции
|
||||
|
||||
### Работа с документами
|
||||
|
||||
1. **Создание документа:**
|
||||
- Загрузка файла
|
||||
- Выбор получателей (все/отделы/конкретные сотрудники)
|
||||
- Автоматические уведомления
|
||||
|
||||
2. **Отслеживание ознакомления:**
|
||||
- Клик на документ автоматически отмечает ознакомление
|
||||
- Визуальное выделение неознакомленных документов
|
||||
- Ведомость ознакомлений для администраторов
|
||||
|
||||
### Система уведомлений
|
||||
|
||||
1. **Веб-уведомления:**
|
||||
- Звуковое оповещение
|
||||
- Счётчик непрочитанных
|
||||
- Список в реальном времени
|
||||
|
||||
2. **Email-уведомления:**
|
||||
- Мгновенная отправка
|
||||
- Ежедневный дайджест
|
||||
- Еженедельный дайджест
|
||||
|
||||
3. **Telegram-уведомления:**
|
||||
- Привязка аккаунта через код
|
||||
- Мгновенная отправка важных уведомлений
|
||||
|
||||
### LDAP интеграция
|
||||
|
||||
- Двусторонняя синхронизация данных
|
||||
- Создание пользователей в AD
|
||||
- Управление отделами через OU
|
||||
- Автоматическое обновление при изменениях
|
||||
|
||||
## API Документация
|
||||
|
||||
REST API доступен по адресу `/api/v1/`
|
||||
|
||||
Основные эндпоинты:
|
||||
- `/api/v1/employees/` - Сотрудники
|
||||
- `/api/v1/departments/` - Отделы
|
||||
- `/api/v1/documents/` - Документы
|
||||
- `/api/v1/requests/` - Заявки
|
||||
- `/api/v1/calendar/events/` - События календаря
|
||||
- `/api/notifications/` - Уведомления
|
||||
|
||||
Документация API: `/api/docs/` (если установлен drf-spectacular)
|
||||
|
||||
## Разработка
|
||||
|
||||
### Создание миграций
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Запуск тестов
|
||||
```bash
|
||||
pytest
|
||||
# или
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
### Линтинг кода
|
||||
```bash
|
||||
flake8 .
|
||||
black .
|
||||
isort .
|
||||
```
|
||||
|
||||
## Настройка уведомлений
|
||||
|
||||
### Email
|
||||
Настройте SMTP в `.env`:
|
||||
```env
|
||||
EMAIL_HOST=smtp.yandex.ru
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USE_SSL=true
|
||||
EMAIL_HOST_USER=your-email@yandex.ru
|
||||
EMAIL_HOST_PASSWORD=your-app-password
|
||||
DEFAULT_FROM_EMAIL=Company <your-email@yandex.ru>
|
||||
```
|
||||
|
||||
### Telegram
|
||||
1. Создайте бота через @BotFather
|
||||
2. Получите токен
|
||||
3. Настройте в `.env`:
|
||||
```env
|
||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||
TELEGRAM_BOT_USERNAME=your_bot_name
|
||||
```
|
||||
4. Запустите бота:
|
||||
```bash
|
||||
python manage.py run_telegram_bot
|
||||
```
|
||||
|
||||
### Отложенная отправка
|
||||
Для дайджестов используйте команду:
|
||||
```bash
|
||||
python manage.py send_pending_notifications --batch-size=100
|
||||
```
|
||||
|
||||
Настройте cron для автоматического запуска:
|
||||
```bash
|
||||
# Ежедневно в 9:00
|
||||
0 9 * * * /path/to/python /path/to/manage.py send_pending_notifications
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Используйте HTTPS в продакшене
|
||||
- Настройте CORS правильно
|
||||
- Используйте сильные пароли
|
||||
- Регулярно обновляйте зависимости
|
||||
- Настройте файрвол
|
||||
- Используйте переменные окружения для секретов
|
||||
|
||||
## Документация и troubleshooting
|
||||
|
||||
### Архитектура
|
||||
- **[REALTIME_MIGRATION_GUIDE.md](REALTIME_MIGRATION_GUIDE.md)** ⭐ - **Миграция на архитектуру realtime**
|
||||
- [backend/realtime/README.md](backend/realtime/README.md) - WebSocket API документация
|
||||
|
||||
### Руководства по функциям
|
||||
- [MESSAGE_EDITING_ANALYSIS.md](MESSAGE_EDITING_ANALYSIS.md) - Редактирование сообщений
|
||||
- [POLLS_IMPLEMENTATION.md](POLLS_IMPLEMENTATION.md) - Система опросов
|
||||
- [PERSONAL_CALENDAR_FEATURE.md](PERSONAL_CALENDAR_FEATURE.md) - Персональный календарь
|
||||
- [REACTIONS_FROM_DATABASE.md](REACTIONS_FROM_DATABASE.md) - Система реакций
|
||||
- [REQUESTS_RECIPIENTS_PLAN.md](REQUESTS_RECIPIENTS_PLAN.md) - Система получателей заявок
|
||||
|
||||
### WebSocket диагностика
|
||||
- **[WEBSOCKET_1006_QUICK_GUIDE.md](WEBSOCKET_1006_QUICK_GUIDE.md)** ⚡ - **Быстрая шпаргалка (5 минут)**
|
||||
- [WEBSOCKET_1006_PRIORITY_CHECK.md](WEBSOCKET_1006_PRIORITY_CHECK.md) - Приоритетная диагностика
|
||||
- [WEBSOCKET_1006_DIAGNOSTIC_PLAN.md](WEBSOCKET_1006_DIAGNOSTIC_PLAN.md) - Полный план диагностики
|
||||
- [WEBSOCKET_KEEPALIVE_FIX.md](WEBSOCKET_KEEPALIVE_FIX.md) - Исправление keepalive
|
||||
- [WEBSOCKET_UNIFIED_MIGRATION.md](WEBSOCKET_UNIFIED_MIGRATION.md) - Миграция на унифицированный WS
|
||||
|
||||
### Рефакторинг и оптимизация
|
||||
- [REFACTORING_SUMMARY.md](REFACTORING_SUMMARY.md) - Общий итог рефакторинга
|
||||
- [CACHE_OPTIMIZATION_SUMMARY.md](CACHE_OPTIMIZATION_SUMMARY.md) - Оптимизация кеширования
|
||||
- [API_REFACTORING_TESTING.md](API_REFACTORING_TESTING.md) - Тестирование API
|
||||
|
||||
## Лицензия
|
||||
|
||||
Проприетарное ПО © 2024-2025 Robotail
|
||||
|
||||
## Контакты
|
||||
|
||||
- Автор: Игорь Надеин
|
||||
- GitHub: [@IgorNadein](https://github.com/IgorNadein)
|
||||
- Проект: [EUSRR](https://github.com/IgorNadein/EUSRR)
|
||||
|
||||
## Changelog
|
||||
|
||||
### v2.0.0 (21.11.2025)
|
||||
- ✨ Добавлена возможность отправки документов на отделы
|
||||
- ✨ Визуальное выделение неознакомленных документов
|
||||
- ✨ Автоматическое ознакомление при клике на документ
|
||||
- ✨ Адаптивная мобильная версия
|
||||
- 🐛 Исправлена фильтрация "Мои документы" для загрузивших
|
||||
- 🐛 Исправлена ведомость ознакомлений с учётом отделов
|
||||
- 🔧 Добавлена поддержка массовых рассылок с порогом
|
||||
|
||||
### v1.0.0
|
||||
- 🎉 Первый релиз
|
||||
- ✨ Базовая система документооборота
|
||||
- ✨ Интеграция с LDAP
|
||||
- ✨ Система уведомлений
|
||||
- ✨ Управление заявками
|
||||
467
api_tests.http
467
api_tests.http
@ -1,467 +0,0 @@
|
||||
### EUSRR API Tests
|
||||
### Используйте расширение REST Client для VS Code
|
||||
### https://marketplace.visualstudio.com/items?itemName=humao.rest-client
|
||||
|
||||
@baseUrl = http://localhost:8000
|
||||
@contentType = application/json
|
||||
|
||||
### Variables (будут заменены после выполнения Login запроса)
|
||||
@accessToken = your_access_token_here
|
||||
@refreshToken = your_refresh_token_here
|
||||
|
||||
#############################################
|
||||
# Authentication
|
||||
#############################################
|
||||
|
||||
### Login - Получение токена
|
||||
# @name login
|
||||
POST {{baseUrl}}/api/auth/token/
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
"password": "password"
|
||||
}
|
||||
|
||||
### Save tokens to variables (автоматически после успешного login)
|
||||
@accessToken = {{login.response.body.access}}
|
||||
@refreshToken = {{login.response.body.refresh}}
|
||||
|
||||
### Refresh Token
|
||||
POST {{baseUrl}}/api/auth/token/refresh/
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"refresh": "{{refreshToken}}"
|
||||
}
|
||||
|
||||
### Register New User
|
||||
POST {{baseUrl}}/api/v1/auth/register/
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"email": "newuser@example.com",
|
||||
"password": "StrongPass123!",
|
||||
"password2": "StrongPass123!",
|
||||
"first_name": "Иван",
|
||||
"last_name": "Иванов"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Employees
|
||||
#############################################
|
||||
|
||||
### List Employees
|
||||
GET {{baseUrl}}/api/v1/employees/?page=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Current User (Me)
|
||||
GET {{baseUrl}}/api/v1/employees/me/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Employee by ID
|
||||
GET {{baseUrl}}/api/v1/employees/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Update Employee
|
||||
PATCH {{baseUrl}}/api/v1/employees/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"first_name": "Иван",
|
||||
"last_name": "Петров"
|
||||
}
|
||||
|
||||
### Search Employees
|
||||
GET {{baseUrl}}/api/v1/employees/?search=Иванов
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
#############################################
|
||||
# Departments
|
||||
#############################################
|
||||
|
||||
### List Departments
|
||||
GET {{baseUrl}}/api/v1/departments/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Department by ID
|
||||
GET {{baseUrl}}/api/v1/departments/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Department
|
||||
POST {{baseUrl}}/api/v1/departments/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Новый отдел",
|
||||
"description": "Описание отдела"
|
||||
}
|
||||
|
||||
### Update Department
|
||||
PATCH {{baseUrl}}/api/v1/departments/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Обновленное название отдела"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Positions
|
||||
#############################################
|
||||
|
||||
### List Positions
|
||||
GET {{baseUrl}}/api/v1/positions/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Position
|
||||
POST {{baseUrl}}/api/v1/positions/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Разработчик",
|
||||
"description": "Full-stack разработчик"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Documents
|
||||
#############################################
|
||||
|
||||
### List Documents
|
||||
GET {{baseUrl}}/api/v1/documents/?page=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Document by ID
|
||||
GET {{baseUrl}}/api/v1/documents/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Document
|
||||
POST {{baseUrl}}/api/v1/documents/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Новый документ",
|
||||
"content": "Содержание документа",
|
||||
"document_type": "report"
|
||||
}
|
||||
|
||||
### Update Document
|
||||
PATCH {{baseUrl}}/api/v1/documents/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Обновленное название"
|
||||
}
|
||||
|
||||
### Delete Document
|
||||
DELETE {{baseUrl}}/api/v1/documents/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
#############################################
|
||||
# Feed (Posts & Comments)
|
||||
#############################################
|
||||
|
||||
### List Posts
|
||||
GET {{baseUrl}}/api/v1/posts/?page=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Post by ID
|
||||
GET {{baseUrl}}/api/v1/posts/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Post
|
||||
POST {{baseUrl}}/api/v1/posts/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"content": "Новый пост в ленте компании! #важное"
|
||||
}
|
||||
|
||||
### Update Post
|
||||
PATCH {{baseUrl}}/api/v1/posts/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"content": "Обновленный пост"
|
||||
}
|
||||
|
||||
### Delete Post
|
||||
DELETE {{baseUrl}}/api/v1/posts/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Like Post
|
||||
POST {{baseUrl}}/api/v1/posts/1/like/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Unlike Post
|
||||
DELETE {{baseUrl}}/api/v1/posts/1/unlike/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### List Comments for Post
|
||||
GET {{baseUrl}}/api/v1/comments/?post=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Comment
|
||||
POST {{baseUrl}}/api/v1/comments/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"post": 1,
|
||||
"content": "Отличный пост!"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Communications (Messenger)
|
||||
#############################################
|
||||
|
||||
### List Chats
|
||||
GET {{baseUrl}}/api/v1/communications/chats/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Chat by ID
|
||||
GET {{baseUrl}}/api/v1/communications/chats/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Private Chat
|
||||
POST {{baseUrl}}/api/v1/communications/chats/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"chat_type": "private",
|
||||
"participants": [2]
|
||||
}
|
||||
|
||||
### Create Group Chat
|
||||
POST {{baseUrl}}/api/v1/communications/chats/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Рабочая группа",
|
||||
"chat_type": "group",
|
||||
"participants": [2, 3, 4]
|
||||
}
|
||||
|
||||
### List Messages in Chat
|
||||
GET {{baseUrl}}/api/v1/communications/messages/?chat=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Send Message
|
||||
POST {{baseUrl}}/api/v1/communications/messages/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"chat": 1,
|
||||
"content": "Привет! Это тестовое сообщение"
|
||||
}
|
||||
|
||||
### Update Message
|
||||
PATCH {{baseUrl}}/api/v1/communications/messages/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"content": "Исправленное сообщение"
|
||||
}
|
||||
|
||||
### Delete Message
|
||||
DELETE {{baseUrl}}/api/v1/communications/messages/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Mark Message as Read
|
||||
POST {{baseUrl}}/api/v1/communications/messages/1/mark_as_read/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
#############################################
|
||||
# Calendar
|
||||
#############################################
|
||||
|
||||
### List Calendars
|
||||
GET {{baseUrl}}/api/v1/calendar/calendars/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### List Events
|
||||
GET {{baseUrl}}/api/v1/calendar/events/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Events for Date Range
|
||||
GET {{baseUrl}}/api/v1/calendar/events/?start=2026-02-01&end=2026-02-28
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Event
|
||||
POST {{baseUrl}}/api/v1/calendar/events/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Встреча",
|
||||
"start": "2026-02-20T10:00:00Z",
|
||||
"end": "2026-02-20T11:00:00Z",
|
||||
"description": "Важная встреча",
|
||||
"location": "Офис"
|
||||
}
|
||||
|
||||
### Update Event
|
||||
PATCH {{baseUrl}}/api/v1/calendar/events/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Перенесенная встреча"
|
||||
}
|
||||
|
||||
### Delete Event
|
||||
DELETE {{baseUrl}}/api/v1/calendar/events/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
#############################################
|
||||
# Requests (Заявки)
|
||||
#############################################
|
||||
|
||||
### List Requests
|
||||
GET {{baseUrl}}/api/v1/requests/?page=1
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Filter Requests by Status
|
||||
GET {{baseUrl}}/api/v1/requests/?status=pending
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Request by ID
|
||||
GET {{baseUrl}}/api/v1/requests/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Request
|
||||
POST {{baseUrl}}/api/v1/requests/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"title": "Новая заявка в IT",
|
||||
"description": "Не работает интернет в кабинете 205",
|
||||
"request_type": "support",
|
||||
"priority": "high"
|
||||
}
|
||||
|
||||
### Update Request
|
||||
PATCH {{baseUrl}}/api/v1/requests/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"status": "in_progress"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Notifications
|
||||
#############################################
|
||||
|
||||
### List Notifications
|
||||
GET {{baseUrl}}/api/v1/notifications/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Unread Notifications Count
|
||||
GET {{baseUrl}}/api/v1/notifications/unread_count/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Notification by ID
|
||||
GET {{baseUrl}}/api/v1/notifications/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Mark Notification as Read
|
||||
POST {{baseUrl}}/api/v1/notifications/1/mark_as_read/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Mark All Notifications as Read
|
||||
POST {{baseUrl}}/api/v1/notifications/mark_all_as_read/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Delete Notification
|
||||
DELETE {{baseUrl}}/api/v1/notifications/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
#############################################
|
||||
# Groups
|
||||
#############################################
|
||||
|
||||
### List Groups
|
||||
GET {{baseUrl}}/api/v1/groups/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Group by ID
|
||||
GET {{baseUrl}}/api/v1/groups/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Group
|
||||
POST {{baseUrl}}/api/v1/groups/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Администраторы",
|
||||
"description": "Группа администраторов системы"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Skills
|
||||
#############################################
|
||||
|
||||
### List Skills
|
||||
GET {{baseUrl}}/api/v1/skills/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Skill
|
||||
POST {{baseUrl}}/api/v1/skills/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"name": "Python",
|
||||
"category": "programming"
|
||||
}
|
||||
|
||||
#############################################
|
||||
# Polls
|
||||
#############################################
|
||||
|
||||
### List Polls
|
||||
GET {{baseUrl}}/api/v1/communications/polls/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Get Poll by ID
|
||||
GET {{baseUrl}}/api/v1/communications/polls/1/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### Create Poll
|
||||
POST {{baseUrl}}/api/v1/communications/polls/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"question": "Где провести корпоратив?",
|
||||
"options": [
|
||||
{"text": "Ресторан"},
|
||||
{"text": "Боулинг"},
|
||||
{"text": "Квест-комната"}
|
||||
],
|
||||
"multiple_choice": false,
|
||||
"anonymous": false
|
||||
}
|
||||
|
||||
### Vote in Poll
|
||||
POST {{baseUrl}}/api/v1/communications/polls/1/vote/
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: {{contentType}}
|
||||
|
||||
{
|
||||
"option_id": 1
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
# api/auth/serializers.py
|
||||
from api.v1.employees.views import PHONE_FIELD, _normalize_phone
|
||||
from api.v1.employees.views._helpers import PHONE_FIELD
|
||||
from employees.utils import _normalize_phone
|
||||
from django.contrib.auth import get_user_model
|
||||
from employees.models import Employee
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
{% load static %}
|
||||
{# === Правая колонка с календарём - рефакторированная версия === #}
|
||||
|
||||
{# Десктопная версия #}
|
||||
{% include "includes/components/calendar_desktop.html" %}
|
||||
|
||||
{# Мобильная версия #}
|
||||
{% include "includes/components/calendar_mobile.html" %}
|
||||
|
||||
{# Модальные окна #}
|
||||
{% include "includes/components/calendar_modal_create.html" %}
|
||||
{% include "includes/components/calendar_modal_details.html" %}
|
||||
{% include "includes/components/calendar_modal_manage.html" %}
|
||||
{% include "includes/components/calendar_context_menu.html" %}
|
||||
|
||||
{# JavaScript инициализация #}
|
||||
{% include "includes/components/calendar_scripts.html" %}
|
||||
@ -1,758 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calendar Frontend Test</title>
|
||||
|
||||
<!-- Bootstrap CSS (offline fallback) -->
|
||||
<style>
|
||||
@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
|
||||
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
|
||||
</style>
|
||||
|
||||
<!-- Inline styles as fallback -->
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-pending { background: #ffc107; color: #000; }
|
||||
.status-success { background: #28a745; color: #fff; }
|
||||
.status-error { background: #dc3545; color: #fff; }
|
||||
|
||||
.log-console {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin: 3px 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-info { color: #66d9ef; }
|
||||
.log-success { color: #a6e22e; }
|
||||
.log-error { color: #f92672; }
|
||||
.log-warn { color: #e6db74; }
|
||||
|
||||
#calendarContainer {
|
||||
min-height: 600px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.calendar-list-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.calendar-list-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.calendar-list-item:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.calendar-color-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0,0,0,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.calendar-list-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calendar-list-item-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.calendar-list-item-meta {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.calendar-list-item-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<h1 class="mb-4">🧪 Calendar Frontend Test Suite</h1>
|
||||
|
||||
<!-- Test Controls -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h3>🎮 Test Controls</h3>
|
||||
<div>
|
||||
<button id="runAllTests" class="btn btn-primary">
|
||||
<i class="bi-play-fill"></i> Run All Tests
|
||||
</button>
|
||||
<button id="clearLogs" class="btn btn-secondary">
|
||||
<i class="bi-trash"></i> Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<button id="testApiWrapper" class="btn btn-outline-primary w-100">
|
||||
Test 1: API Wrapper
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button id="testCalendarManager" class="btn btn-outline-primary w-100">
|
||||
Test 2: Calendar Manager
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button id="testModalComponent" class="btn btn-outline-primary w-100">
|
||||
Test 3: Modal Component
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button id="testWidgetIntegration" class="btn btn-outline-primary w-100">
|
||||
Test 4: Widget Integration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button id="testEventFiltering" class="btn btn-outline-primary w-100">
|
||||
Test 5: Event Filtering
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button id="testUserInteractions" class="btn btn-outline-primary w-100">
|
||||
Test 6: User Interactions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Results -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h3>📊 Test Results</h3>
|
||||
<div id="testSummary">
|
||||
<span class="badge bg-secondary">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Status</th>
|
||||
<th>Duration</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="testResultsTable">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">No tests run yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar UI Demo -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h3>📅 Calendar UI Demo</h3>
|
||||
<button id="btnCreateCalendar" class="btn btn-sm btn-outline-primary" data-action="create-calendar">
|
||||
<i class="bi-plus-lg"></i> Create Calendar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<h5>My Calendars</h5>
|
||||
<div id="calendarListContainer" class="calendar-list-container">
|
||||
<div class="text-center text-muted py-3">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mb-0 mt-2">Loading calendars...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div id="calendarContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console Log -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h3>📝 Console Log</h3>
|
||||
<button id="toggleConsole" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi-chevron-up"></i> Collapse
|
||||
</button>
|
||||
</div>
|
||||
<div id="logConsole" class="log-console"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Template -->
|
||||
<div class="modal fade" id="calendarManageModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Manage Calendar</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="calendarForm">
|
||||
<div class="mb-3">
|
||||
<label for="calendarTitle" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="calendarTitle" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="calendarDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="calendarDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="calendarColor" class="form-label">Color</label>
|
||||
<input type="color" class="form-control form-control-color" id="calendarColor" value="#0d6efd">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="calendarType" id="typePersonal" value="personal" checked>
|
||||
<label class="form-check-label" for="typePersonal">Personal</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="calendarType" id="typePublic" value="public">
|
||||
<label class="form-check-label" for="typePublic">Public</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveCalendarBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- FullCalendar JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
|
||||
|
||||
<script>
|
||||
// =========================
|
||||
// Mock Data & API
|
||||
// =========================
|
||||
|
||||
let mockCalendars = [
|
||||
{ id: 1, title: 'Personal', description: 'My personal calendar', color: '#0d6efd', calendar_type: 'personal', user_can_edit: true, user_can_view: true, is_subscribed: true },
|
||||
{ id: 2, title: 'Work', description: 'Work events', color: '#198754', calendar_type: 'public', user_can_edit: true, user_can_view: true, is_subscribed: true },
|
||||
{ id: 3, title: 'Team Meetings', description: 'Team sync meetings', color: '#ffc107', calendar_type: 'department', user_can_edit: false, user_can_view: true, is_subscribed: true },
|
||||
];
|
||||
|
||||
let mockEvents = [
|
||||
{ id: 1, title: 'Morning Standup', start: new Date(2026, 1, 11, 9, 0), end: new Date(2026, 1, 11, 9, 30), color: '#0d6efd', calendar_id: 1 },
|
||||
{ id: 2, title: 'Client Meeting', start: new Date(2026, 1, 11, 14, 0), end: new Date(2026, 1, 11, 15, 0), color: '#198754', calendar_id: 2 },
|
||||
{ id: 3, title: 'Sprint Planning', start: new Date(2026, 1, 12, 10, 0), end: new Date(2026, 1, 12, 12, 0), color: '#ffc107', calendar_id: 3 },
|
||||
{ id: 4, title: 'Code Review', start: new Date(2026, 1, 13, 15, 0), end: new Date(2026, 1, 13, 16, 0), color: '#0d6efd', calendar_id: 1 },
|
||||
];
|
||||
|
||||
// Mock API
|
||||
const mockAPI = {
|
||||
getMyCalendars: async () => {
|
||||
await delay(300);
|
||||
log('API: getMyCalendars() → ' + mockCalendars.length + ' calendars', 'info');
|
||||
return { ok: true, data: [...mockCalendars] };
|
||||
},
|
||||
|
||||
createCalendar: async (data) => {
|
||||
await delay(500);
|
||||
const newCalendar = {
|
||||
id: Date.now(),
|
||||
...data,
|
||||
user_can_edit: true,
|
||||
user_can_view: true,
|
||||
is_subscribed: true
|
||||
};
|
||||
mockCalendars.push(newCalendar);
|
||||
log('API: createCalendar() → Calendar #' + newCalendar.id + ' created', 'success');
|
||||
return { ok: true, data: newCalendar };
|
||||
},
|
||||
|
||||
updateCalendar: async (id, data) => {
|
||||
await delay(500);
|
||||
const index = mockCalendars.findIndex(c => c.id === id);
|
||||
if (index !== -1) {
|
||||
mockCalendars[index] = { ...mockCalendars[index], ...data };
|
||||
log('API: updateCalendar(' + id + ') → Updated', 'success');
|
||||
return { ok: true, data: mockCalendars[index] };
|
||||
}
|
||||
return { ok: false, error: 'Calendar not found' };
|
||||
},
|
||||
|
||||
deleteCalendar: async (id) => {
|
||||
await delay(500);
|
||||
mockCalendars = mockCalendars.filter(c => c.id !== id);
|
||||
log('API: deleteCalendar(' + id + ') → Deleted', 'success');
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
getCalendarEvents: async (params) => {
|
||||
await delay(300);
|
||||
let events = [...mockEvents];
|
||||
if (params.calendar_id) {
|
||||
events = events.filter(e => e.calendar_id === params.calendar_id);
|
||||
}
|
||||
log('API: getCalendarEvents(' + JSON.stringify(params) + ') → ' + events.length + ' events', 'info');
|
||||
return { ok: true, data: events };
|
||||
}
|
||||
};
|
||||
|
||||
// =========================
|
||||
// Test Logger
|
||||
// =========================
|
||||
|
||||
const logConsole = document.getElementById('logConsole');
|
||||
const testResultsTable = document.getElementById('testResultsTable');
|
||||
let testResults = [];
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry log-' + type;
|
||||
entry.textContent = '[' + timestamp + '] ' + message;
|
||||
logConsole.appendChild(entry);
|
||||
logConsole.scrollTop = logConsole.scrollHeight;
|
||||
|
||||
// Also log to real console
|
||||
console.log('[Test]', message);
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function updateTestResult(testName, status, duration, details) {
|
||||
testResults.push({ testName, status, duration, details });
|
||||
renderTestResults();
|
||||
}
|
||||
|
||||
function renderTestResults() {
|
||||
if (testResults.length === 0) {
|
||||
testResultsTable.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No tests run yet</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
testResultsTable.innerHTML = testResults.map(result => `
|
||||
<tr>
|
||||
<td>${result.testName}</td>
|
||||
<td><span class="badge bg-${result.status === 'passed' ? 'success' : 'danger'}">${result.status}</span></td>
|
||||
<td>${result.duration}ms</td>
|
||||
<td class="text-muted small">${result.details}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const passed = testResults.filter(r => r.status === 'passed').length;
|
||||
const failed = testResults.filter(r => r.status === 'failed').length;
|
||||
document.getElementById('testSummary').innerHTML = `
|
||||
<span class="badge bg-success">${passed} Passed</span>
|
||||
<span class="badge bg-danger">${failed} Failed</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Tests
|
||||
// =========================
|
||||
|
||||
async function testApiWrapper() {
|
||||
log('=== Test 1: API Wrapper ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test getMyCalendars
|
||||
const calendars = await mockAPI.getMyCalendars();
|
||||
if (!calendars.ok || !Array.isArray(calendars.data)) {
|
||||
throw new Error('getMyCalendars failed');
|
||||
}
|
||||
log('✓ getMyCalendars works', 'success');
|
||||
|
||||
// Test createCalendar
|
||||
const newCal = await mockAPI.createCalendar({
|
||||
title: 'Test Calendar',
|
||||
description: 'Test',
|
||||
color: '#ff0000',
|
||||
calendar_type: 'personal'
|
||||
});
|
||||
if (!newCal.ok || !newCal.data.id) {
|
||||
throw new Error('createCalendar failed');
|
||||
}
|
||||
log('✓ createCalendar works', 'success');
|
||||
|
||||
// Test getCalendarEvents
|
||||
const events = await mockAPI.getCalendarEvents({ calendar_id: 1 });
|
||||
if (!events.ok || !Array.isArray(events.data)) {
|
||||
throw new Error('getCalendarEvents failed');
|
||||
}
|
||||
log('✓ getCalendarEvents works', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('API Wrapper', 'passed', duration, 'All API methods working');
|
||||
log('✅ Test 1: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('API Wrapper', 'failed', duration, error.message);
|
||||
log('❌ Test 1: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testCalendarManager() {
|
||||
log('=== Test 2: Calendar Manager ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Render calendar list
|
||||
const calendars = await mockAPI.getMyCalendars();
|
||||
const container = document.getElementById('calendarListContainer');
|
||||
|
||||
container.innerHTML = calendars.data.map(cal => `
|
||||
<div class="calendar-list-item" data-calendar-id="${cal.id}">
|
||||
<input type="checkbox" class="form-check-input" ${cal.is_subscribed ? 'checked' : ''}>
|
||||
<div class="calendar-color-indicator" style="background-color: ${cal.color}"></div>
|
||||
<div class="calendar-list-item-info">
|
||||
<p class="calendar-list-item-title">${cal.title}</p>
|
||||
<p class="calendar-list-item-meta">${cal.calendar_type}</p>
|
||||
</div>
|
||||
<div class="calendar-list-item-actions">
|
||||
${cal.user_can_edit ? '<button class="btn btn-sm btn-outline-secondary"><i class="bi-pencil"></i></button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
log('✓ Calendar list rendered', 'success');
|
||||
|
||||
// Test checkbox interaction
|
||||
const firstCheckbox = container.querySelector('input[type="checkbox"]');
|
||||
if (!firstCheckbox) {
|
||||
throw new Error('Checkbox not found');
|
||||
}
|
||||
firstCheckbox.click();
|
||||
log('✓ Checkbox interaction works', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Calendar Manager', 'passed', duration, 'List rendering and interaction OK');
|
||||
log('✅ Test 2: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Calendar Manager', 'failed', duration, error.message);
|
||||
log('❌ Test 2: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testModalComponent() {
|
||||
log('=== Test 3: Modal Component ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const modal = new bootstrap.Modal(document.getElementById('calendarManageModal'));
|
||||
|
||||
// Test modal open
|
||||
modal.show();
|
||||
await delay(500);
|
||||
log('✓ Modal opens', 'success');
|
||||
|
||||
// Test form validation
|
||||
const titleInput = document.getElementById('calendarTitle');
|
||||
titleInput.value = 'Test Calendar';
|
||||
if (titleInput.value !== 'Test Calendar') {
|
||||
throw new Error('Form input failed');
|
||||
}
|
||||
log('✓ Form inputs work', 'success');
|
||||
|
||||
// Close modal
|
||||
modal.hide();
|
||||
await delay(500);
|
||||
log('✓ Modal closes', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Modal Component', 'passed', duration, 'Modal and form working');
|
||||
log('✅ Test 3: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Modal Component', 'failed', duration, error.message);
|
||||
log('❌ Test 3: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testWidgetIntegration() {
|
||||
log('=== Test 4: Widget Integration ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Initialize FullCalendar
|
||||
const calendarEl = document.getElementById('calendarContainer');
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
initialDate: '2026-02-11',
|
||||
events: async (info, success, failure) => {
|
||||
try {
|
||||
const events = await mockAPI.getCalendarEvents({});
|
||||
success(events.data);
|
||||
} catch (e) {
|
||||
failure(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
await delay(1000);
|
||||
log('✓ FullCalendar initialized', 'success');
|
||||
|
||||
// Check if events loaded
|
||||
const eventCount = calendar.getEvents().length;
|
||||
if (eventCount === 0) {
|
||||
throw new Error('No events loaded');
|
||||
}
|
||||
log('✓ Events loaded: ' + eventCount, 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Widget Integration', 'passed', duration, eventCount + ' events loaded');
|
||||
log('✅ Test 4: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Widget Integration', 'failed', duration, error.message);
|
||||
log('❌ Test 4: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testEventFiltering() {
|
||||
log('=== Test 5: Event Filtering ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test filtering by calendar_id
|
||||
const allEvents = await mockAPI.getCalendarEvents({});
|
||||
const filteredEvents = await mockAPI.getCalendarEvents({ calendar_id: 1 });
|
||||
|
||||
if (filteredEvents.data.length >= allEvents.data.length) {
|
||||
throw new Error('Filtering not working');
|
||||
}
|
||||
log('✓ Calendar filtering works', 'success');
|
||||
log(' All events: ' + allEvents.data.length, 'info');
|
||||
log(' Filtered (calendar 1): ' + filteredEvents.data.length, 'info');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Event Filtering', 'passed', duration, 'Filtering by calendar_id works');
|
||||
log('✅ Test 5: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Event Filtering', 'failed', duration, error.message);
|
||||
log('❌ Test 5: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testUserInteractions() {
|
||||
log('=== Test 6: User Interactions ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test create button
|
||||
const createBtn = document.getElementById('btnCreateCalendar');
|
||||
if (!createBtn) {
|
||||
throw new Error('Create button not found');
|
||||
}
|
||||
log('✓ Create button exists', 'success');
|
||||
|
||||
// Test calendar list items
|
||||
const calendarItems = document.querySelectorAll('.calendar-list-item');
|
||||
if (calendarItems.length === 0) {
|
||||
throw new Error('No calendar items found');
|
||||
}
|
||||
log('✓ Calendar items rendered: ' + calendarItems.length, 'success');
|
||||
|
||||
// Test checkboxes
|
||||
const checkboxes = document.querySelectorAll('.calendar-list-item input[type="checkbox"]');
|
||||
if (checkboxes.length === 0) {
|
||||
throw new Error('No checkboxes found');
|
||||
}
|
||||
log('✓ Checkboxes present: ' + checkboxes.length, 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('User Interactions', 'passed', duration, 'All UI elements interactive');
|
||||
log('✅ Test 6: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('User Interactions', 'failed', duration, error.message);
|
||||
log('❌ Test 6: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Event Listeners
|
||||
// =========================
|
||||
|
||||
document.getElementById('runAllTests').addEventListener('click', async () => {
|
||||
testResults = [];
|
||||
log('🚀 Running all tests...', 'info');
|
||||
|
||||
await testApiWrapper();
|
||||
await delay(500);
|
||||
await testCalendarManager();
|
||||
await delay(500);
|
||||
await testModalComponent();
|
||||
await delay(500);
|
||||
await testWidgetIntegration();
|
||||
await delay(500);
|
||||
await testEventFiltering();
|
||||
await delay(500);
|
||||
await testUserInteractions();
|
||||
|
||||
log('🏁 All tests completed!', 'success');
|
||||
});
|
||||
|
||||
document.getElementById('clearLogs').addEventListener('click', () => {
|
||||
logConsole.innerHTML = '';
|
||||
log('Logs cleared', 'info');
|
||||
});
|
||||
|
||||
document.getElementById('toggleConsole').addEventListener('click', () => {
|
||||
const console = document.getElementById('logConsole');
|
||||
const btn = document.getElementById('toggleConsole');
|
||||
if (console.style.display === 'none') {
|
||||
console.style.display = 'block';
|
||||
btn.innerHTML = '<i class="bi-chevron-up"></i> Collapse';
|
||||
} else {
|
||||
console.style.display = 'none';
|
||||
btn.innerHTML = '<i class="bi-chevron-down"></i> Expand';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('testApiWrapper').addEventListener('click', testApiWrapper);
|
||||
document.getElementById('testCalendarManager').addEventListener('click', testCalendarManager);
|
||||
document.getElementById('testModalComponent').addEventListener('click', testModalComponent);
|
||||
document.getElementById('testWidgetIntegration').addEventListener('click', testWidgetIntegration);
|
||||
document.getElementById('testEventFiltering').addEventListener('click', testEventFiltering);
|
||||
document.getElementById('testUserInteractions').addEventListener('click', testUserInteractions);
|
||||
|
||||
// Modal save button
|
||||
document.getElementById('saveCalendarBtn').addEventListener('click', async () => {
|
||||
const title = document.getElementById('calendarTitle').value;
|
||||
const description = document.getElementById('calendarDescription').value;
|
||||
const color = document.getElementById('calendarColor').value;
|
||||
const type = document.querySelector('input[name="calendarType"]:checked').value;
|
||||
|
||||
if (!title) {
|
||||
alert('Title is required');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Creating calendar: ' + title, 'info');
|
||||
const result = await mockAPI.createCalendar({ title, description, color, calendar_type: type });
|
||||
|
||||
if (result.ok) {
|
||||
log('Calendar created successfully!', 'success');
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('calendarManageModal'));
|
||||
modal.hide();
|
||||
|
||||
// Refresh calendar list
|
||||
await testCalendarManager();
|
||||
}
|
||||
});
|
||||
|
||||
// Create calendar button
|
||||
document.getElementById('btnCreateCalendar').addEventListener('click', () => {
|
||||
log('Opening create calendar modal', 'info');
|
||||
const modal = new bootstrap.Modal(document.getElementById('calendarManageModal'));
|
||||
|
||||
// Reset form
|
||||
document.getElementById('calendarTitle').value = '';
|
||||
document.getElementById('calendarDescription').value = '';
|
||||
document.getElementById('calendarColor').value = '#0d6efd';
|
||||
document.getElementById('typePersonal').checked = true;
|
||||
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// =========================
|
||||
// Initialize
|
||||
// =========================
|
||||
|
||||
log('✅ Test suite initialized', 'success');
|
||||
log('Click "Run All Tests" to start', 'info');
|
||||
|
||||
// Auto-load calendar list on startup (wrapped in async IIFE)
|
||||
(async function init() {
|
||||
try {
|
||||
await testCalendarManager();
|
||||
await testWidgetIntegration();
|
||||
} catch (error) {
|
||||
log('Initialization error: ' + error.message, 'error');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,720 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calendar Frontend Test - Simple Version</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0b5ed7;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5c636a;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
width: calc(33.33% - 10px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-console {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin: 3px 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-info { color: #66d9ef; }
|
||||
.log-success { color: #a6e22e; }
|
||||
.log-error { color: #f92672; }
|
||||
.log-warn { color: #e6db74; }
|
||||
|
||||
.calendar-list {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.calendar-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.calendar-item:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.calendar-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.calendar-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.calendar-meta {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 Calendar Frontend Test Suite (Offline Version)</h1>
|
||||
|
||||
<!-- Test Controls -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h2>🎮 Test Controls</h2>
|
||||
<div>
|
||||
<button id="runAllTests" class="btn-primary">▶ Run All Tests</button>
|
||||
<button id="clearLogs" class="btn-secondary">🗑 Clear Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-controls">
|
||||
<button class="btn-outline" onclick="testApiWrapper()">Test 1: API Wrapper</button>
|
||||
<button class="btn-outline" onclick="testCalendarManager()">Test 2: Calendar Manager</button>
|
||||
<button class="btn-outline" onclick="testEventFiltering()">Test 3: Event Filtering</button>
|
||||
<button class="btn-outline" onclick="testStateManagement()">Test 4: State Management</button>
|
||||
<button class="btn-outline" onclick="testCallbacks()">Test 5: Callbacks</button>
|
||||
<button class="btn-outline" onclick="testDataStructures()">Test 6: Data Structures</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Results -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h2>📊 Test Results</h2>
|
||||
<div id="testSummary">
|
||||
<span class="badge badge-secondary">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Status</th>
|
||||
<th>Duration</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="testResultsTable">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">No tests run yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Calendar UI Demo -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h2>📅 Calendar List Demo</h2>
|
||||
<button class="btn-primary" onclick="createCalendar()">➕ Create Calendar</button>
|
||||
</div>
|
||||
|
||||
<div id="calendarListContainer" class="calendar-list">
|
||||
<div class="text-center text-muted">Loading calendars...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console Log -->
|
||||
<div class="test-section">
|
||||
<div class="test-header">
|
||||
<h2>📝 Console Log</h2>
|
||||
<button class="btn-secondary" onclick="toggleConsole()">▲ Toggle</button>
|
||||
</div>
|
||||
<div id="logConsole" class="log-console"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// =========================
|
||||
// State Management
|
||||
// =========================
|
||||
|
||||
let mockCalendars = [
|
||||
{ id: 1, title: 'Personal', description: 'My personal calendar', color: '#0d6efd', calendar_type: 'personal', user_can_edit: true, user_can_view: true, is_subscribed: true, visible: true },
|
||||
{ id: 2, title: 'Work', description: 'Work events', color: '#198754', calendar_type: 'public', user_can_edit: true, user_can_view: true, is_subscribed: true, visible: true },
|
||||
{ id: 3, title: 'Team Meetings', description: 'Team sync', color: '#ffc107', calendar_type: 'department', user_can_edit: false, user_can_view: true, is_subscribed: true, visible: false },
|
||||
];
|
||||
|
||||
let mockEvents = [
|
||||
{ id: 1, title: 'Morning Standup', start: '2026-02-11T09:00', end: '2026-02-11T09:30', color: '#0d6efd', calendar_id: 1 },
|
||||
{ id: 2, title: 'Client Meeting', start: '2026-02-11T14:00', end: '2026-02-11T15:00', color: '#198754', calendar_id: 2 },
|
||||
{ id: 3, title: 'Sprint Planning', start: '2026-02-12T10:00', end: '2026-02-12T12:00', color: '#ffc107', calendar_id: 3 },
|
||||
{ id: 4, title: 'Code Review', start: '2026-02-13T15:00', end: '2026-02-13T16:00', color: '#0d6efd', calendar_id: 1 },
|
||||
];
|
||||
|
||||
let testResults = [];
|
||||
let visibleCalendarIds = [1, 2];
|
||||
|
||||
// =========================
|
||||
// Logger
|
||||
// =========================
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logConsole = document.getElementById('logConsole');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry log-' + type;
|
||||
entry.textContent = '[' + timestamp + '] ' + message;
|
||||
logConsole.appendChild(entry);
|
||||
logConsole.scrollTop = logConsole.scrollHeight;
|
||||
console.log('[Test]', message);
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function updateTestResult(testName, status, duration, details) {
|
||||
testResults.push({ testName, status, duration, details });
|
||||
renderTestResults();
|
||||
}
|
||||
|
||||
function renderTestResults() {
|
||||
const tbody = document.getElementById('testResultsTable');
|
||||
|
||||
if (testResults.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No tests run yet</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = testResults.map(r => `
|
||||
<tr>
|
||||
<td>${r.testName}</td>
|
||||
<td><span class="badge badge-${r.status === 'passed' ? 'success' : 'danger'}">${r.status.toUpperCase()}</span></td>
|
||||
<td>${r.duration}ms</td>
|
||||
<td class="text-muted">${r.details}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const passed = testResults.filter(r => r.status === 'passed').length;
|
||||
const failed = testResults.filter(r => r.status === 'failed').length;
|
||||
document.getElementById('testSummary').innerHTML = `
|
||||
<span class="badge badge-success">${passed} Passed</span>
|
||||
<span class="badge badge-danger">${failed} Failed</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Mock API
|
||||
// =========================
|
||||
|
||||
async function mockApiGetCalendars() {
|
||||
await delay(200);
|
||||
return { ok: true, data: [...mockCalendars] };
|
||||
}
|
||||
|
||||
async function mockApiGetEvents(calendarId = null) {
|
||||
await delay(150);
|
||||
let events = [...mockEvents];
|
||||
if (calendarId) {
|
||||
events = events.filter(e => e.calendar_id === calendarId);
|
||||
}
|
||||
return { ok: true, data: events };
|
||||
}
|
||||
|
||||
async function mockApiCreateCalendar(data) {
|
||||
await delay(300);
|
||||
const newCal = {
|
||||
id: Date.now(),
|
||||
...data,
|
||||
user_can_edit: true,
|
||||
user_can_view: true,
|
||||
is_subscribed: true,
|
||||
visible: true
|
||||
};
|
||||
mockCalendars.push(newCal);
|
||||
return { ok: true, data: newCal };
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Tests
|
||||
// =========================
|
||||
|
||||
async function testApiWrapper() {
|
||||
log('=== Test 1: API Wrapper ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const calendars = await mockApiGetCalendars();
|
||||
if (!calendars.ok || !Array.isArray(calendars.data)) {
|
||||
throw new Error('getCalendars failed');
|
||||
}
|
||||
log('✓ mockApiGetCalendars() → ' + calendars.data.length + ' calendars', 'success');
|
||||
|
||||
const events = await mockApiGetEvents(1);
|
||||
if (!events.ok || !Array.isArray(events.data)) {
|
||||
throw new Error('getEvents failed');
|
||||
}
|
||||
log('✓ mockApiGetEvents(1) → ' + events.data.length + ' events', 'success');
|
||||
|
||||
const allEvents = await mockApiGetEvents();
|
||||
log('✓ mockApiGetEvents() → ' + allEvents.data.length + ' total events', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('API Wrapper', 'passed', duration, 'All API methods working');
|
||||
log('✅ Test 1: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('API Wrapper', 'failed', duration, error.message);
|
||||
log('❌ Test 1: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testCalendarManager() {
|
||||
log('=== Test 2: Calendar Manager ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const calendars = await mockApiGetCalendars();
|
||||
renderCalendarList(calendars.data);
|
||||
|
||||
const container = document.getElementById('calendarListContainer');
|
||||
const items = container.querySelectorAll('.calendar-item');
|
||||
|
||||
if (items.length !== calendars.data.length) {
|
||||
throw new Error('Calendar count mismatch: ' + items.length + ' != ' + calendars.data.length);
|
||||
}
|
||||
log('✓ Rendered ' + items.length + ' calendar items', 'success');
|
||||
|
||||
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
if (checkboxes.length === 0) {
|
||||
throw new Error('No checkboxes found');
|
||||
}
|
||||
log('✓ ' + checkboxes.length + ' checkboxes created', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Calendar Manager', 'passed', duration, 'List rendering OK');
|
||||
log('✅ Test 2: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Calendar Manager', 'failed', duration, error.message);
|
||||
log('❌ Test 2: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testEventFiltering() {
|
||||
log('=== Test 3: Event Filtering ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const allEvents = await mockApiGetEvents();
|
||||
const filteredEvents = await mockApiGetEvents(1);
|
||||
|
||||
if (filteredEvents.data.length >= allEvents.data.length) {
|
||||
throw new Error('Filtering not working correctly');
|
||||
}
|
||||
log('✓ All events: ' + allEvents.data.length, 'info');
|
||||
log('✓ Calendar 1 events: ' + filteredEvents.data.length, 'info');
|
||||
|
||||
// Test visible calendar filtering
|
||||
const visibleEvents = fetchEventsForVisibleCalendars(allEvents.data);
|
||||
log('✓ Visible calendars: ' + visibleCalendarIds.join(', '), 'info');
|
||||
log('✓ Visible events: ' + visibleEvents.length, 'info');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Event Filtering', 'passed', duration, 'Filtering works correctly');
|
||||
log('✅ Test 3: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Event Filtering', 'failed', duration, error.message);
|
||||
log('❌ Test 3: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testStateManagement() {
|
||||
log('=== Test 4: State Management ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const initialVisible = [...visibleCalendarIds];
|
||||
log('✓ Initial visible: ' + initialVisible.join(', '), 'info');
|
||||
|
||||
// Toggle visibility
|
||||
toggleCalendarVisibility(3, true);
|
||||
if (!visibleCalendarIds.includes(3)) {
|
||||
throw new Error('Calendar 3 not added to visible');
|
||||
}
|
||||
log('✓ Calendar 3 added to visible', 'success');
|
||||
|
||||
toggleCalendarVisibility(3, false);
|
||||
if (visibleCalendarIds.includes(3)) {
|
||||
throw new Error('Calendar 3 not removed from visible');
|
||||
}
|
||||
log('✓ Calendar 3 removed from visible', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('State Management', 'passed', duration, 'State updates correctly');
|
||||
log('✅ Test 4: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('State Management', 'failed', duration, error.message);
|
||||
log('❌ Test 4: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testCallbacks() {
|
||||
log('=== Test 5: Callbacks ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let callbackCalled = false;
|
||||
|
||||
function testCallback(calendarId, isVisible) {
|
||||
callbackCalled = true;
|
||||
log('✓ Callback triggered: cal=' + calendarId + ', visible=' + isVisible, 'info');
|
||||
}
|
||||
|
||||
testCallback(1, true);
|
||||
|
||||
if (!callbackCalled) {
|
||||
throw new Error('Callback not called');
|
||||
}
|
||||
log('✓ Callback mechanism works', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Callbacks', 'passed', duration, 'Callbacks working');
|
||||
log('✅ Test 5: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Callbacks', 'failed', duration, error.message);
|
||||
log('❌ Test 5: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testDataStructures() {
|
||||
log('=== Test 6: Data Structures ===', 'info');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test calendar structure
|
||||
const cal = mockCalendars[0];
|
||||
const requiredFields = ['id', 'title', 'color', 'calendar_type', 'user_can_edit', 'user_can_view'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!(field in cal)) {
|
||||
throw new Error('Missing field: ' + field);
|
||||
}
|
||||
}
|
||||
log('✓ Calendar structure valid', 'success');
|
||||
|
||||
// Test event structure
|
||||
const event = mockEvents[0];
|
||||
const eventFields = ['id', 'title', 'start', 'calendar_id'];
|
||||
|
||||
for (const field of eventFields) {
|
||||
if (!(field in event)) {
|
||||
throw new Error('Missing event field: ' + field);
|
||||
}
|
||||
}
|
||||
log('✓ Event structure valid', 'success');
|
||||
|
||||
// Test deduplication
|
||||
const duplicates = [...mockEvents, mockEvents[0]];
|
||||
const unique = deduplicateEvents(duplicates);
|
||||
if (unique.length !== mockEvents.length) {
|
||||
throw new Error('Deduplication failed');
|
||||
}
|
||||
log('✓ Event deduplication works', 'success');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Data Structures', 'passed', duration, 'All structures valid');
|
||||
log('✅ Test 6: PASSED', 'success');
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
updateTestResult('Data Structures', 'failed', duration, error.message);
|
||||
log('❌ Test 6: FAILED - ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Helper Functions
|
||||
// =========================
|
||||
|
||||
function fetchEventsForVisibleCalendars(allEvents) {
|
||||
return allEvents.filter(e => visibleCalendarIds.includes(e.calendar_id));
|
||||
}
|
||||
|
||||
function toggleCalendarVisibility(calendarId, isVisible) {
|
||||
if (isVisible) {
|
||||
if (!visibleCalendarIds.includes(calendarId)) {
|
||||
visibleCalendarIds.push(calendarId);
|
||||
}
|
||||
} else {
|
||||
visibleCalendarIds = visibleCalendarIds.filter(id => id !== calendarId);
|
||||
}
|
||||
log('Visible calendars: ' + visibleCalendarIds.join(', '), 'info');
|
||||
}
|
||||
|
||||
function deduplicateEvents(events) {
|
||||
const seen = new Set();
|
||||
return events.filter(e => {
|
||||
if (seen.has(e.id)) return false;
|
||||
seen.add(e.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCalendarList(calendars) {
|
||||
const container = document.getElementById('calendarListContainer');
|
||||
|
||||
container.innerHTML = calendars.map(cal => `
|
||||
<div class="calendar-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
${cal.visible ? 'checked' : ''}
|
||||
onchange="handleCalendarToggle(${cal.id}, this.checked)"
|
||||
>
|
||||
<div class="calendar-color" style="background-color: ${cal.color}"></div>
|
||||
<div class="calendar-info">
|
||||
<div class="calendar-title">${cal.title}</div>
|
||||
<div class="calendar-meta">${cal.calendar_type} ${cal.user_can_edit ? '✏️' : '👁️'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function handleCalendarToggle(calendarId, isChecked) {
|
||||
log('Calendar ' + calendarId + ' toggled: ' + isChecked, 'info');
|
||||
toggleCalendarVisibility(calendarId, isChecked);
|
||||
|
||||
// Update calendar state
|
||||
const calendar = mockCalendars.find(c => c.id === calendarId);
|
||||
if (calendar) {
|
||||
calendar.visible = isChecked;
|
||||
}
|
||||
}
|
||||
|
||||
function createCalendar() {
|
||||
const title = prompt('Calendar title:');
|
||||
if (!title) return;
|
||||
|
||||
const color = '#' + Math.floor(Math.random()*16777215).toString(16);
|
||||
|
||||
mockApiCreateCalendar({
|
||||
title: title,
|
||||
description: '',
|
||||
color: color,
|
||||
calendar_type: 'personal'
|
||||
}).then(result => {
|
||||
log('Calendar created: ' + result.data.title, 'success');
|
||||
renderCalendarList(mockCalendars);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleConsole() {
|
||||
const console = document.getElementById('logConsole');
|
||||
console.style.display = console.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Event Listeners
|
||||
// =========================
|
||||
|
||||
document.getElementById('runAllTests').addEventListener('click', async () => {
|
||||
testResults = [];
|
||||
log('🚀 Running all tests...', 'info');
|
||||
|
||||
await testApiWrapper();
|
||||
await delay(300);
|
||||
await testCalendarManager();
|
||||
await delay(300);
|
||||
await testEventFiltering();
|
||||
await delay(300);
|
||||
await testStateManagement();
|
||||
await delay(300);
|
||||
await testCallbacks();
|
||||
await delay(300);
|
||||
await testDataStructures();
|
||||
|
||||
log('🏁 All tests completed!', 'success');
|
||||
});
|
||||
|
||||
document.getElementById('clearLogs').addEventListener('click', () => {
|
||||
document.getElementById('logConsole').innerHTML = '';
|
||||
log('Logs cleared', 'info');
|
||||
});
|
||||
|
||||
// =========================
|
||||
// Initialize
|
||||
// =========================
|
||||
|
||||
(async function init() {
|
||||
log('✅ Test suite initialized', 'success');
|
||||
log('Click "Run All Tests" to start', 'info');
|
||||
log('📡 Offline mode - using mock data', 'warn');
|
||||
|
||||
try {
|
||||
await testCalendarManager();
|
||||
} catch (error) {
|
||||
log('Init error: ' + error.message, 'error');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Тест модернизированной системы логирования."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем backend в путь
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
import logging
|
||||
|
||||
# Тестируем разные логгеры
|
||||
print("=" * 70)
|
||||
print("ТЕСТ МОДЕРНИЗИРОВАННОЙ СИСТЕМЫ ЛОГИРОВАНИЯ")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Логгер employees
|
||||
print("\n1. Тест логгера 'employees':")
|
||||
employees_logger = logging.getLogger('employees')
|
||||
employees_logger.debug('Тестовое DEBUG сообщение')
|
||||
employees_logger.info('Тестовое INFO сообщение')
|
||||
employees_logger.warning('Тестовое WARNING сообщение')
|
||||
employees_logger.error('Тестовое ERROR сообщение')
|
||||
|
||||
# 2. Логгер security
|
||||
print("\n2. Тест логгера 'employees.ldap' (security):")
|
||||
ldap_logger = logging.getLogger('employees.ldap')
|
||||
ldap_logger.info('LDAP аутентификация успешна', extra={'username': 'test_user'})
|
||||
ldap_logger.warning('LDAP подключение медленное', extra={'latency': '1000ms'})
|
||||
|
||||
# 3. Логгер с исключением
|
||||
print("\n3. Тест логирования исключений:")
|
||||
try:
|
||||
result = 10 / 0
|
||||
except ZeroDivisionError:
|
||||
employees_logger.exception('Тестовое исключение для демонстрации трассировки')
|
||||
|
||||
# 4. Django логгер
|
||||
print("\n4. Тест Django логгера:")
|
||||
django_logger = logging.getLogger('django')
|
||||
django_logger.info('Django логгер работает')
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("✓ Все логгеры протестированы!")
|
||||
print("=" * 70)
|
||||
|
||||
# Проверяем созданные файлы
|
||||
logs_dir = os.path.join(os.path.dirname(__file__), 'logs')
|
||||
if os.path.exists(logs_dir):
|
||||
log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
|
||||
print(f"\n📁 Найдено файлов логов: {len(log_files)}")
|
||||
for log_file in sorted(log_files):
|
||||
file_path = os.path.join(logs_dir, log_file)
|
||||
file_size = os.path.getsize(file_path)
|
||||
print(f" - {log_file:20s} ({file_size:,} bytes)")
|
||||
else:
|
||||
print("\n⚠ Директория logs/ не найдена")
|
||||
|
||||
print("\n💡 Проверьте содержимое файлов:")
|
||||
print(" tail -f backend/logs/all.log")
|
||||
print(" tail -f backend/logs/error.log")
|
||||
print(" tail -f backend/logs/security.log")
|
||||
@ -1,85 +0,0 @@
|
||||
"""
|
||||
Тестовый скрипт для проверки оптимизации уведомлений о сообщениях.
|
||||
|
||||
Запуск:
|
||||
cd backend
|
||||
../.venv/Scripts/python test_message_optimization.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
import time
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
django.setup()
|
||||
|
||||
from communications.models import Chat, Message
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def test_message_creation_speed():
|
||||
"""Тест скорости создания сообщения."""
|
||||
|
||||
print("=" * 60)
|
||||
print("ТЕСТ: Скорость создания сообщения в групповом чате")
|
||||
print("=" * 60)
|
||||
|
||||
# Находим групповой чат с участниками
|
||||
chat = Chat.objects.filter(type='group').first()
|
||||
|
||||
if not chat:
|
||||
print("❌ Групповой чат не найден")
|
||||
return
|
||||
|
||||
participants_count = chat.participants.count()
|
||||
print(f"📊 Чат: {chat.name or chat.id}")
|
||||
print(f"📊 Участников: {participants_count}")
|
||||
|
||||
# Находим автора (первого участника)
|
||||
author = chat.participants.first()
|
||||
|
||||
if not author:
|
||||
print("❌ Участники не найдены")
|
||||
return
|
||||
|
||||
print(f"📊 Автор: {author.get_full_name()}")
|
||||
print()
|
||||
|
||||
# Создаем тестовое сообщение и замеряем время
|
||||
start_time = time.time()
|
||||
|
||||
message = Message.objects.create(
|
||||
chat=chat,
|
||||
author=author,
|
||||
content=f"Тестовое сообщение для проверки оптимизации (время: {time.time()})"
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
elapsed_ms = (end_time - start_time) * 1000
|
||||
|
||||
print(f"✅ Сообщение создано: ID={message.id}")
|
||||
print(f"⏱️ Время создания: {elapsed_ms:.2f}ms")
|
||||
print()
|
||||
|
||||
# Оценка результата
|
||||
if elapsed_ms < 50:
|
||||
print("🎉 ОТЛИЧНО! Время < 50ms - оптимизация работает!")
|
||||
elif elapsed_ms < 100:
|
||||
print("✅ ХОРОШО! Время < 100ms - приемлемо")
|
||||
elif elapsed_ms < 200:
|
||||
print("⚠️ СРЕДНЕ: Время < 200ms - можно улучшить")
|
||||
else:
|
||||
print(f"❌ МЕДЛЕННО: {elapsed_ms:.2f}ms - проверьте Celery worker")
|
||||
|
||||
print()
|
||||
print("Рекомендации:")
|
||||
print(" 1. Убедитесь что Celery worker запущен")
|
||||
print(" 2. Проверьте логи worker'а: sudo tail -f /var/log/celery/worker.log")
|
||||
print(" 3. На продакшене время должно быть < 50ms")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_message_creation_speed()
|
||||
@ -1,55 +0,0 @@
|
||||
"""
|
||||
Тест рефакторенных уведомлений
|
||||
"""
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
# Импорты задач
|
||||
from notifications.tasks import (
|
||||
process_message_notifications_task,
|
||||
process_event_notifications_task,
|
||||
process_post_notifications_task,
|
||||
process_request_notifications_task,
|
||||
process_document_notifications_task,
|
||||
)
|
||||
|
||||
# Импорты процессоров
|
||||
from notifications.task_base import (
|
||||
BaseNotificationProcessor,
|
||||
MessageNotificationProcessor,
|
||||
EventNotificationProcessor,
|
||||
PostNotificationProcessor,
|
||||
RequestNotificationProcessor,
|
||||
DocumentNotificationProcessor,
|
||||
truncate_text,
|
||||
)
|
||||
|
||||
print("✅ Все импорты успешны!")
|
||||
print(f"✅ process_message_notifications_task: {process_message_notifications_task}")
|
||||
print(f"✅ process_event_notifications_task: {process_event_notifications_task}")
|
||||
print(f"✅ process_post_notifications_task: {process_post_notifications_task}")
|
||||
print(f"✅ process_request_notifications_task: {process_request_notifications_task}")
|
||||
print(f"✅ process_document_notifications_task: {process_document_notifications_task}")
|
||||
print(f"✅ BaseNotificationProcessor: {BaseNotificationProcessor}")
|
||||
print(f"✅ MessageNotificationProcessor: {MessageNotificationProcessor}")
|
||||
print(f"✅ EventNotificationProcessor: {EventNotificationProcessor}")
|
||||
print(f"✅ PostNotificationProcessor: {PostNotificationProcessor}")
|
||||
print(f"✅ RequestNotificationProcessor: {RequestNotificationProcessor}")
|
||||
print(f"✅ DocumentNotificationProcessor: {DocumentNotificationProcessor}")
|
||||
print(f"✅ truncate_text: {truncate_text}")
|
||||
|
||||
# Тест функции truncate_text
|
||||
assert truncate_text("Short text", 100) == "Short text"
|
||||
assert truncate_text("Very long text that should be truncated", 10) == "Very long..."
|
||||
print("✅ truncate_text работает корректно")
|
||||
|
||||
print("\n🎉 Рефакторинг завершён успешно!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@ -1,61 +0,0 @@
|
||||
@echo off
|
||||
REM Скрипт для проверки функции ответа на сообщение (Windows)
|
||||
REM Запускает сервер и открывает тестовую страницу
|
||||
|
||||
echo.
|
||||
echo 🧪 Проверка функции ответа на сообщение
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM Проверка виртуального окружения
|
||||
if not exist ".venv" (
|
||||
echo ❌ Виртуальное окружение не найдено
|
||||
echo Создайте его командой: python -m venv .venv
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Виртуальное окружение найдено
|
||||
echo.
|
||||
|
||||
REM Проверка Python в виртуальном окружении
|
||||
if exist ".venv\Scripts\python.exe" (
|
||||
.venv\Scripts\python.exe --version
|
||||
echo ✅ Python найден в виртуальном окружении
|
||||
) else (
|
||||
echo ❌ Python не найден в .venv\Scripts\
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 📊 Что будет проверено:
|
||||
echo 1. Контекстное меню -^> Ответить
|
||||
echo 2. ChatFormManager (режим reply)
|
||||
echo 3. Превью ответа в сообщениях
|
||||
echo 4. Загрузка реакций (дефолтные)
|
||||
echo.
|
||||
|
||||
echo 🚀 Запуск Django сервера...
|
||||
echo.
|
||||
|
||||
REM Запуск сервера
|
||||
start "Django Server" .venv\Scripts\python.exe manage.py runserver 9000
|
||||
|
||||
echo ⏳ Ожидание запуска сервера (5 сек)...
|
||||
timeout /t 5 /nobreak > nul
|
||||
|
||||
echo.
|
||||
echo ✅ Сервер запущен!
|
||||
echo.
|
||||
echo 📝 Откройте в браузере:
|
||||
echo http://localhost:9000/test_reply_functionality.html
|
||||
echo.
|
||||
echo 🌐 Или нажмите Enter чтобы открыть автоматически...
|
||||
pause
|
||||
|
||||
REM Открыть браузер
|
||||
start http://localhost:9000/test_reply_functionality.html
|
||||
|
||||
echo.
|
||||
echo Закройте окно "Django Server" чтобы остановить сервер
|
||||
echo.
|
||||
pause
|
||||
@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для проверки функции ответа на сообщение
|
||||
# Запускает сервер и открывает тестовую страницу
|
||||
|
||||
echo "🧪 Проверка функции ответа на сообщение"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Проверка виртуального окружения
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "❌ Виртуальное окружение не найдено"
|
||||
echo "Создайте его командой: python -m venv .venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Виртуальное окружение найдено"
|
||||
|
||||
# Активация виртуального окружения (Windows)
|
||||
if [ -f ".venv/Scripts/activate" ]; then
|
||||
source .venv/Scripts/activate
|
||||
echo "✅ Виртуальное окружение активировано"
|
||||
else
|
||||
echo "⚠️ Не удалось активировать виртуальное окружение"
|
||||
fi
|
||||
|
||||
# Проверка Python
|
||||
python_version=$(python --version 2>&1)
|
||||
echo "Python: $python_version"
|
||||
|
||||
echo ""
|
||||
echo "📊 Что будет проверено:"
|
||||
echo " 1. Контекстное меню -> Ответить"
|
||||
echo " 2. ChatFormManager (режим reply)"
|
||||
echo " 3. Превью ответа в сообщениях"
|
||||
echo " 4. Загрузка реакций (дефолтные)"
|
||||
echo ""
|
||||
|
||||
# Запуск сервера
|
||||
echo "🚀 Запуск Django сервера..."
|
||||
echo ""
|
||||
python manage.py runserver 9000 &
|
||||
SERVER_PID=$!
|
||||
|
||||
echo "Server PID: $SERVER_PID"
|
||||
echo ""
|
||||
echo "⏳ Ожидание запуска сервера (5 сек)..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "✅ Сервер запущен!"
|
||||
echo ""
|
||||
echo "📝 Откройте в браузере:"
|
||||
echo " http://localhost:9000/test_reply_functionality.html"
|
||||
echo ""
|
||||
echo "Нажмите Ctrl+C для остановки сервера"
|
||||
echo ""
|
||||
|
||||
# Ожидание завершения
|
||||
wait $SERVER_PID
|
||||
@ -1,344 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест функции ответа на сообщение</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.message-preview {
|
||||
background: #e3f2fd;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.bubble {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
.reply-indicator {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 12px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.log-output {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.log-entry {
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
}
|
||||
.log-entry.info { color: #0d6efd; }
|
||||
.log-entry.warn { color: #ffc107; }
|
||||
.log-entry.error { color: #dc3545; }
|
||||
.log-entry.success { color: #198754; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="mb-4">🧪 Тест функции ответа на сообщение</h1>
|
||||
|
||||
<!-- Статус загрузки реакций -->
|
||||
<div class="test-section">
|
||||
<h3>📊 Статус загрузки реакций</h3>
|
||||
<div id="reactionsStatus" class="alert alert-info">
|
||||
Проверка...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 1: Контекстное меню -->
|
||||
<div class="test-section">
|
||||
<h3>1️⃣ Тест контекстного меню</h3>
|
||||
<p>Симуляция вызова функции ответа из контекстного меню</p>
|
||||
|
||||
<div class="message-preview" data-message-id="123">
|
||||
<div class="small text-secondary mb-2">
|
||||
Иван Петров · 10:30
|
||||
</div>
|
||||
<div class="bubble">
|
||||
Привет! Как дела с проектом?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="testReplyBtn" class="btn btn-primary">
|
||||
<i class="bi bi-reply-fill"></i> Ответить (через контекстное меню)
|
||||
</button>
|
||||
|
||||
<div id="testReplyResult" class="mt-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 2: ChatFormManager -->
|
||||
<div class="test-section">
|
||||
<h3>2️⃣ Тест ChatFormManager</h3>
|
||||
<p>Проверка состояния формы при ответе</p>
|
||||
|
||||
<form id="chatForm" action="/upload/" method="POST">
|
||||
<input type="hidden" name="chat_id" value="1">
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea id="id_content" class="form-control" rows="3" placeholder="Напишите сообщение..."></textarea>
|
||||
</div>
|
||||
|
||||
<div id="replyIndicatorContainer"></div>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-send-fill"></i> Отправить
|
||||
</button>
|
||||
<button type="button" id="cancelBtn" class="btn btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="formState" class="mt-3 p-3 bg-light border rounded">
|
||||
<strong>Состояние формы:</strong>
|
||||
<pre id="formStateJson">{ "mode": "send" }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 3: Превью ответа в сообщении -->
|
||||
<div class="test-section">
|
||||
<h3>3️⃣ Тест превью ответа в сообщении</h3>
|
||||
<p>Проверка рендеринга превью при ответе на сообщение</p>
|
||||
|
||||
<div class="message-preview">
|
||||
<div class="message-reply-preview border-start border-3 border-primary ps-2 mb-2 ms-2 rounded"
|
||||
data-reply-to-id="100"
|
||||
style="background-color: rgba(13, 110, 253, 0.05); padding: 6px 8px; cursor: pointer;">
|
||||
<div class="small fw-semibold text-primary">
|
||||
<i class="bi bi-reply-fill me-1"></i>
|
||||
Мария Иванова
|
||||
</div>
|
||||
<div class="small text-muted text-truncate" style="max-width: 400px;">
|
||||
Давайте встретимся завтра
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bubble">
|
||||
Отлично, в какое время удобно?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="testPreviewClick" class="btn btn-primary">
|
||||
<i class="bi bi-cursor-fill"></i> Клик по превью (переход к сообщению)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Логи -->
|
||||
<div class="test-section">
|
||||
<h3>📝 Журнал событий</h3>
|
||||
<button id="clearLogs" class="btn btn-sm btn-outline-secondary mb-2">Очистить</button>
|
||||
<div id="logOutput" class="log-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модули -->
|
||||
<script type="module">
|
||||
import { initChatFormManager } from './static/js/components/chatFormManager.js';
|
||||
import MessageContextMenu from './static/js/components/messageContextMenu.js';
|
||||
import reactionsConfig from './static/js/config/reactionsConfig.js';
|
||||
|
||||
// Логирование
|
||||
const logOutput = document.getElementById('logOutput');
|
||||
|
||||
function addLog(message, type = 'info') {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
logOutput.appendChild(entry);
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
}
|
||||
|
||||
document.getElementById('clearLogs').addEventListener('click', () => {
|
||||
logOutput.innerHTML = '';
|
||||
addLog('Логи очищены', 'info');
|
||||
});
|
||||
|
||||
// Проверка загрузки реакций
|
||||
async function checkReactions() {
|
||||
addLog('Проверка загрузки реакций...', 'info');
|
||||
|
||||
try {
|
||||
await reactionsConfig.load();
|
||||
const emojis = reactionsConfig.getEmojis();
|
||||
|
||||
const statusEl = document.getElementById('reactionsStatus');
|
||||
|
||||
if (emojis && emojis.length > 0) {
|
||||
statusEl.className = 'alert alert-success';
|
||||
statusEl.innerHTML = `
|
||||
<h5>✅ Реакции загружены успешно</h5>
|
||||
<p>Всего доступно: <strong>${emojis.length}</strong> реакций</p>
|
||||
<p>Эмодзи: ${emojis.join(' ')}</p>
|
||||
`;
|
||||
addLog(`✓ Загружено ${emojis.length} реакций: ${emojis.join(' ')}`, 'success');
|
||||
} else {
|
||||
statusEl.className = 'alert alert-warning';
|
||||
statusEl.innerHTML = `
|
||||
<h5>⚠️ Используются дефолтные реакции</h5>
|
||||
<p>API эндпоинт не доступен, используются реакции по умолчанию</p>
|
||||
`;
|
||||
addLog('⚠ Используются дефолтные реакции (API недоступен)', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
const statusEl = document.getElementById('reactionsStatus');
|
||||
statusEl.className = 'alert alert-danger';
|
||||
statusEl.innerHTML = `
|
||||
<h5>❌ Ошибка загрузки реакций</h5>
|
||||
<p>${error.message}</p>
|
||||
`;
|
||||
addLog(`✗ Ошибка: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация ChatFormManager
|
||||
addLog('Инициализация ChatFormManager...', 'info');
|
||||
const chatFormManager = initChatFormManager({
|
||||
formId: 'chatForm',
|
||||
textareaId: 'id_content',
|
||||
chatId: 1,
|
||||
uploadUrl: '/api/v1/communications/messages/upload/',
|
||||
editUrlTemplate: '/api/v1/communications/messages/{id}/'
|
||||
});
|
||||
|
||||
if (chatFormManager) {
|
||||
window.chatFormManager = chatFormManager;
|
||||
addLog('✓ ChatFormManager инициализирован', 'success');
|
||||
} else {
|
||||
addLog('✗ Ошибка инициализации ChatFormManager', 'error');
|
||||
}
|
||||
|
||||
// Обновление отображения состояния формы
|
||||
function updateFormState() {
|
||||
if (!chatFormManager) return;
|
||||
|
||||
const state = chatFormManager.getFormData();
|
||||
document.getElementById('formStateJson').textContent = JSON.stringify(state, null, 2);
|
||||
}
|
||||
|
||||
// Тест 1: Ответ через контекстное меню
|
||||
document.getElementById('testReplyBtn').addEventListener('click', () => {
|
||||
addLog('Тест: Вызов handleReply из контекстного меню', 'info');
|
||||
|
||||
const messageEl = document.querySelector('[data-message-id="123"]');
|
||||
|
||||
if (!chatFormManager) {
|
||||
addLog('✗ ChatFormManager не доступен', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Симуляция логики MessageContextMenu.handleReply
|
||||
const bubble = messageEl.querySelector('.bubble');
|
||||
const messageText = bubble.textContent.trim();
|
||||
const authorName = 'Иван Петров';
|
||||
const messageId = messageEl.dataset.messageId;
|
||||
|
||||
addLog(`handleReply вызван с: ID=${messageId}, Автор="${authorName}"`, 'info');
|
||||
|
||||
chatFormManager.setModeToReply(messageId, authorName, messageText);
|
||||
addLog('✓ Режим ответа активирован', 'success');
|
||||
|
||||
updateFormState();
|
||||
|
||||
// Показываем результат
|
||||
const resultDiv = document.getElementById('testReplyResult');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<h5>✅ Тест пройден!</h5>
|
||||
<ul>
|
||||
<li>Режим формы: <strong>reply</strong></li>
|
||||
<li>ID сообщения: <strong>${messageId}</strong></li>
|
||||
<li>Placeholder обновлен: "${document.getElementById('id_content').placeholder}"</li>
|
||||
<li>Hidden input reply_to добавлен: ${document.querySelector('input[name="reply_to"]') ? 'Да' : 'Нет'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
addLog(`✗ Ошибка в handleReply: ${error.message}`, 'error');
|
||||
|
||||
const resultDiv = document.getElementById('testReplyResult');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<h5>❌ Тест провален</h5>
|
||||
<p>${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Отмена
|
||||
document.getElementById('cancelBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
addLog('Отмена действия (возврат в режим send)', 'info');
|
||||
|
||||
if (chatFormManager) {
|
||||
chatFormManager.cancel();
|
||||
addLog('✓ Режим send восстановлен', 'success');
|
||||
updateFormState();
|
||||
}
|
||||
});
|
||||
|
||||
// Тест 3: Клик по превью
|
||||
document.getElementById('testPreviewClick').addEventListener('click', () => {
|
||||
const preview = document.querySelector('.message-reply-preview');
|
||||
const replyToId = preview.dataset.replyToId;
|
||||
|
||||
addLog(`Клик по превью ответа, ID оригинального сообщения: ${replyToId}`, 'info');
|
||||
addLog('В реальной ситуации произойдет переход к сообщению #' + replyToId, 'info');
|
||||
|
||||
// В реальности здесь будет вызов messageLoader.loadAroundMessage()
|
||||
alert(`Переход к сообщению #${replyToId}\n\nВ реальной реализации вызывается:\nmessageLoader.loadAroundMessage(${replyToId})`);
|
||||
});
|
||||
|
||||
// Мониторинг изменений в форме
|
||||
document.getElementById('chatForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
addLog('=== ОТПРАВКА ФОРМЫ ===', 'info');
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
addLog(` ${key}: ${value}`, 'info');
|
||||
}
|
||||
|
||||
const state = chatFormManager.getFormData();
|
||||
addLog(`Режим: ${state.mode}`, 'info');
|
||||
if (state.replyToMessageId) {
|
||||
addLog(`Reply To: ${state.replyToMessageId}`, 'success');
|
||||
}
|
||||
|
||||
addLog('=== КОНЕЦ ===', 'info');
|
||||
});
|
||||
|
||||
// Запуск проверки реакций
|
||||
checkReactions();
|
||||
addLog('Тестовая страница загружена', 'success');
|
||||
updateFormState();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,313 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test ChatControllerV2 - Smart Autoscroll</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#chat-container {
|
||||
border: 1px solid #ccc;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 8px 12px;
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.message.own {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message.other {
|
||||
background: #e9ecef;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.new-messages-indicator {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.new-messages-indicator.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.new-messages-indicator .badge {
|
||||
background: rgba(255,255,255,0.3);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.test-pass {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.test-fail {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #222;
|
||||
color: #0f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ChatControllerV2 - Smart Autoscroll Test</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="testAddOwnMessage()">Добавить своё сообщение</button>
|
||||
<button onclick="testAddOtherMessage()">Добавить чужое сообщение</button>
|
||||
<button onclick="testScrollUp()">Прокрутить вверх</button>
|
||||
<button onclick="testScrollDown()">Прокрутить вниз</button>
|
||||
<button onclick="runAllTests()">Запустить все тесты</button>
|
||||
</div>
|
||||
|
||||
<div id="chat-container"></div>
|
||||
|
||||
<div class="test-results">
|
||||
<h3>Результаты тестов:</h3>
|
||||
<div id="test-output">Нажмите "Запустить все тесты"</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script type="module">
|
||||
import { ChatControllerV2 } from './static/js/controllers/chatControllerV2.js';
|
||||
import { MessageStoreV2 } from './static/js/stores/messageStoreV2.js';
|
||||
|
||||
// Логирование
|
||||
const log = (msg) => {
|
||||
const logEl = document.getElementById('log');
|
||||
logEl.textContent += `[${new Date().toLocaleTimeString()}] ${msg}\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
// Инициализация
|
||||
let controller;
|
||||
let messageId = 1;
|
||||
const currentUserId = 1;
|
||||
const otherUserId = 2;
|
||||
|
||||
async function init() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
|
||||
controller = new ChatControllerV2({
|
||||
chatId: 1,
|
||||
scrollElement: scrollEl,
|
||||
containerId: 'chat-container',
|
||||
currentUserId: currentUserId
|
||||
});
|
||||
|
||||
// Добавляем несколько начальных сообщений
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: i % 2 === 0 ? currentUserId : otherUserId,
|
||||
content: `Тестовое сообщение ${i + 1}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: i % 2 === 0 ? 'Я' : 'Собеседник'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
}
|
||||
|
||||
await controller.init();
|
||||
log('✅ ChatControllerV2 инициализирован');
|
||||
}
|
||||
|
||||
// Тестовые функции
|
||||
window.testAddOwnMessage = async function() {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: currentUserId,
|
||||
content: `Моё сообщение ${messageId}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: 'Я'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
log(`📤 Добавлено своё сообщение #${msg.id}`);
|
||||
};
|
||||
|
||||
window.testAddOtherMessage = async function() {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: otherUserId,
|
||||
content: `Чужое сообщение ${messageId}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: 'Собеседник'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
log(`📥 Добавлено чужое сообщение #${msg.id}`);
|
||||
};
|
||||
|
||||
window.testScrollUp = function() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
scrollEl.scrollTop = 0;
|
||||
log('⬆️ Прокручено вверх');
|
||||
};
|
||||
|
||||
window.testScrollDown = function() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
scrollEl.scrollTop = scrollEl.scrollHeight;
|
||||
log('⬇️ Прокручено вниз');
|
||||
};
|
||||
|
||||
// Автотесты
|
||||
window.runAllTests = async function() {
|
||||
const output = document.getElementById('test-output');
|
||||
output.innerHTML = '<p>Запуск тестов...</p>';
|
||||
|
||||
const results = [];
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
// Тест 1: Своё сообщение - автоскролл вниз
|
||||
log('🧪 Тест 1: Своё сообщение → автоскролл');
|
||||
testScrollDown();
|
||||
await sleep(200);
|
||||
const oldScroll = controller.scrollElement.scrollTop;
|
||||
testAddOwnMessage();
|
||||
await sleep(500);
|
||||
const newScroll = controller.scrollElement.scrollTop;
|
||||
const test1 = newScroll > oldScroll;
|
||||
results.push({ name: 'Своё сообщение автоскроллит', pass: test1 });
|
||||
log(test1 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 2: Чужое сообщение внизу - автоскролл
|
||||
log('🧪 Тест 2: Чужое сообщение внизу → автоскролл');
|
||||
testScrollDown();
|
||||
await sleep(200);
|
||||
const oldScroll2 = controller.scrollElement.scrollTop;
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const newScroll2 = controller.scrollElement.scrollTop;
|
||||
const test2 = newScroll2 > oldScroll2;
|
||||
results.push({ name: 'Чужое сообщение внизу автоскроллит', pass: test2 });
|
||||
log(test2 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 3: Чужое сообщение вверху - индикатор
|
||||
log('🧪 Тест 3: Чужое сообщение вверху → индикатор');
|
||||
testScrollUp();
|
||||
await sleep(200);
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const indicator = document.getElementById('new-messages-btn');
|
||||
const test3 = indicator && indicator.style.display !== 'none';
|
||||
results.push({ name: 'Индикатор показывается', pass: test3 });
|
||||
log(test3 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 4: Счётчик увеличивается
|
||||
log('🧪 Тест 4: Счётчик увеличивается');
|
||||
const countBefore = controller._newMessagesCount;
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const countAfter = controller._newMessagesCount;
|
||||
const test4 = countAfter > countBefore;
|
||||
results.push({ name: 'Счётчик увеличивается', pass: test4 });
|
||||
log(test4 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 5: Клик по индикатору → скролл вниз + скрытие
|
||||
log('🧪 Тест 5: Клик по индикатору → скролл + скрытие');
|
||||
if (indicator) {
|
||||
indicator.click();
|
||||
await sleep(500);
|
||||
const isHidden = indicator.style.display === 'none';
|
||||
const isBottom = controller.scrollManager.isNearBottom();
|
||||
const test5 = isHidden && isBottom;
|
||||
results.push({ name: 'Клик работает корректно', pass: test5 });
|
||||
log(test5 ? '✅ PASS' : '❌ FAIL');
|
||||
}
|
||||
|
||||
// Вывод результатов
|
||||
const passed = results.filter(r => r.pass).length;
|
||||
const total = results.length;
|
||||
const passRate = ((passed / total) * 100).toFixed(1);
|
||||
|
||||
let html = `<h4>Результаты: ${passed}/${total} (${passRate}%)</h4><ul>`;
|
||||
results.forEach(r => {
|
||||
html += `<li class="${r.pass ? 'test-pass' : 'test-fail'}">${r.pass ? '✅' : '❌'} ${r.name}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
output.innerHTML = html;
|
||||
log(`\n📊 Итого: ${passed}/${total} тестов пройдено (${passRate}%)`);
|
||||
};
|
||||
|
||||
// Автозапуск
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,135 +0,0 @@
|
||||
# Миграция фронтенда на django-scheduler - Завершено
|
||||
|
||||
**Дата**: 24 февраля 2026
|
||||
**Ветка**: `feature/django-scheduler-integration`
|
||||
**Коммит**: `ee657b1`
|
||||
|
||||
## ✅ Что сделано
|
||||
|
||||
### 1. Обновлены API endpoints
|
||||
- `apiUrls.js`: Все endpoints заменены с `/api/v1/calendar/` на `/api/v1/schedule/`
|
||||
- Добавлены новые endpoints: `RULES`, `OCCURRENCES`, `RELATIONS`
|
||||
- Удалены deprecated: `SUBSCRIPTIONS`, `CALENDAR_SUBSCRIBE`, `CALENDAR_INVITE`
|
||||
|
||||
### 2. Упрощена архитектура
|
||||
- **Убрана концепция subscriptions**: django-scheduler не имеет подписок
|
||||
- **Видимость календарей**: Теперь хранится только в localStorage (для всех календарей)
|
||||
- **Права доступа**: Управляются на уровне django-scheduler через permissions
|
||||
|
||||
### 3. Адаптирован backend serializer
|
||||
- `CalendarSerializer` теперь принимает поле `title` (для совместимости с EUSRR)
|
||||
- Автогенерация `slug` из `name/title` если не указан
|
||||
- Поддержка старых и новых форматов данных
|
||||
|
||||
### 4. Обновлены компоненты
|
||||
- `calendarsApi.js`: Удалены функции `subscribeToCalendar()`, `unsubscribeFromCalendar()`, `inviteUserToCalendar()`, `getMySubscriptions()`, `updateSubscription()`
|
||||
- `calendarManager.js`: Упрощена логика `toggleCalendarVisibility()`, `subscribe()`, `unsubscribe()` - теперь работают только с localStorage
|
||||
|
||||
## 📝 Изменения в файлах
|
||||
|
||||
### Modified
|
||||
1. `backend/api/v1/schedule/serializers.py` - добавлена совместимость с EUSRR API
|
||||
2. `backend/static/js/constants/apiUrls.js` - обновлены все endpoints
|
||||
3. `backend/static/js/api/calendarsApi.js` - удалены subscriptions функции
|
||||
4. `backend/static/js/components/calendarManager.js` - упрощена логика видимости
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### 1. Запуск сервера
|
||||
```bash
|
||||
cd /c/Users/igor_/Dev/EUSRR/backend
|
||||
/c/Users/igor_/Dev/EUSRR/.venv/Scripts/python manage.py runserver
|
||||
```
|
||||
|
||||
### 2. Создание тестового календаря
|
||||
```python
|
||||
from schedule.models import Calendar, Event
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
# Создать календарь
|
||||
cal = Calendar.objects.create(
|
||||
name="Тестовый календарь",
|
||||
slug="test-calendar"
|
||||
)
|
||||
|
||||
# Создать событие
|
||||
tz = pytz.timezone('Europe/Moscow')
|
||||
now = datetime.now(tz)
|
||||
|
||||
Event.objects.create(
|
||||
title="Тестовое событие",
|
||||
description="Проверка django-scheduler",
|
||||
start=now + timedelta(hours=1),
|
||||
end=now + timedelta(hours=2),
|
||||
calendar=cal,
|
||||
color_event="#3498db"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Проверка через API
|
||||
```bash
|
||||
# Список календарей
|
||||
curl http://localhost:8000/api/v1/schedule/calendars/
|
||||
|
||||
# Список событий
|
||||
curl http://localhost:8000/api/v1/schedule/events/
|
||||
|
||||
# События с фильтрацией
|
||||
curl "http://localhost:8000/api/v1/schedule/events/?calendar=1&start=2026-02-01T00:00:00Z&end=2026-03-01T00:00:00Z"
|
||||
```
|
||||
|
||||
### 4. Проверка фронтенда
|
||||
1. Открыть https://corp.robotail.pro (или localhost:8000)
|
||||
2. Авторизоваться
|
||||
3. Проверить правую панель календаря
|
||||
4. Создать тестовый календарь через модальное окно
|
||||
5. Создать событие
|
||||
6. Переключить видимость календаря (должно сохраняться в localStorage)
|
||||
|
||||
## 🔄 Совместимость
|
||||
|
||||
### Что работает как раньше:
|
||||
- ✅ Создание/редактирование/удаление календарей
|
||||
- ✅ Создание/редактирование/удаление событий
|
||||
- ✅ Просмотр событий в FullCalendar
|
||||
- ✅ Переключение видимости календарей
|
||||
- ✅ Legacy календари (Компания, Личный, Отделы)
|
||||
|
||||
### Что изменилось:
|
||||
- ⚠️ Нет API подписок (subscribe/unsubscribe) - используется localStorage
|
||||
- ⚠️ Нет API приглашений (invite) - нужно реализовать через EventRelation если требуется
|
||||
- ⚠️ Права доступа теперь управляются на уровне django-scheduler permissions
|
||||
|
||||
### Что добавилось:
|
||||
- ✨ Поддержка recurring events через RFC 5545 rrule (Rules)
|
||||
- ✨ Материализованные вхождения повторяющихся событий (Occurrences)
|
||||
- ✨ Связи событий (EventRelation) для управления участниками
|
||||
- ✨ Timezone-aware события из коробки
|
||||
|
||||
## 📊 Статистика изменений
|
||||
|
||||
**Коммит**: ee657b1
|
||||
- **4 файла изменено**
|
||||
- **+91 строк добавлено**
|
||||
- **-342 строк удалено**
|
||||
- **Чистый результат**: -251 строка (упрощение кода)
|
||||
|
||||
## 🚀 Следующие шаги
|
||||
|
||||
1. **Тестирование**: Проверить все функции календаря на dev окружении
|
||||
2. **Миграция данных**: Создать скрипт переноса календарей/событий из `calendar_app` в `schedule`
|
||||
3. **Документация**: Обновить пользовательскую документацию
|
||||
4. **Удаление старого кода**: После успешного тестирования удалить `calendar_app`
|
||||
|
||||
## 📌 Полезные ссылки
|
||||
|
||||
- [API v1/schedule Guide](./SCHEDULE_API_V1_GUIDE.md)
|
||||
- [Django Scheduler Integration Plan](./DJANGO_SCHEDULER_INTEGRATION.md)
|
||||
- [Django Scheduler Docs](https://github.com/thauber/django-scheduler)
|
||||
- [RFC 5545 (iCalendar)](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
|
||||
---
|
||||
|
||||
**Автор**: GitHub Copilot
|
||||
**Ревью**: Требуется
|
||||
368153
newman-report.json
368153
newman-report.json
File diff suppressed because it is too large
Load Diff
@ -1,208 +0,0 @@
|
||||
(node:37944) [DEP0176] DeprecationWarning: fs.F_OK is deprecated, use fs.constants.F_OK instead
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
newman
|
||||
|
||||
EUSRR API
|
||||
|
||||
□ Authentication
|
||||
└ Login (Obtain Token)
|
||||
POST http://localhost:8000/api/auth/token/ [200 OK, 748B, 190ms]
|
||||
┌
|
||||
│ 'Access token saved: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV
|
||||
│ CJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMzI5OD
|
||||
│ IzLCJpYXQiOjE3NzEzMjgwMjMsImp0aSI6IjI5NzZiNzMyZjdmMjQx
|
||||
│ OTY4NTIwYzNlOTUxN2FjNDY1IiwidXNlcl9pZCI6IjEifQ._B6LK9J
|
||||
│ KQnjyNoVCh14QJBhPn7VV18YIR2XpQtDEMIs'
|
||||
└
|
||||
|
||||
└ Refresh Token
|
||||
POST http://localhost:8000/api/auth/token/refresh/ [200 OK, 503B, 14ms]
|
||||
┌
|
||||
│ 'Access token refreshed: eyJhbGciOiJIUzI1NiIsInR5cCI6I
|
||||
│ kpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxMz
|
||||
│ I5ODIzLCJpYXQiOjE3NzEzMjgwMjMsImp0aSI6IjE1MGZjZTZlZWQ0
|
||||
│ MjRjMWY4NWViZmNmYzUyODA3YTFjIiwidXNlcl9pZCI6IjEifQ.4km
|
||||
│ qq0EskSsrG3TLexrABC2MAM2IFDNy0XVIyVwdcK4'
|
||||
└
|
||||
|
||||
└ Register
|
||||
POST http://localhost:8000/api/v1/auth/register/ [400 Bad Request, 372B, 14ms]
|
||||
|
||||
□ Employees
|
||||
└ List Employees
|
||||
GET http://localhost:8000/api/v1/employees/?page=1 [200 OK, 1.1kB, 30ms]
|
||||
|
||||
└ Get Employee by ID
|
||||
GET http://localhost:8000/api/v1/employees/1/ [200 OK, 1.47kB, 32ms]
|
||||
|
||||
└ Update Employee
|
||||
PATCH http://localhost:8000/api/v1/employees/1/ [200 OK, 745B, 37ms]
|
||||
|
||||
└ Get Current User (Me)
|
||||
GET http://localhost:8000/api/v1/employees/me/ [200 OK, 15.07kB, 39ms]
|
||||
|
||||
└ Update My Profile
|
||||
PATCH http://localhost:8000/api/v1/employees/me/ [200 OK, 15.09kB, 41ms]
|
||||
|
||||
└ Create Employee
|
||||
POST http://localhost:8000/api/v1/employees/ [400 Bad Request, 477B, 17ms]
|
||||
|
||||
└ Delete Employee
|
||||
DELETE http://localhost:8000/api/v1/employees/2/ [404 Not Found, 339B, 16ms]
|
||||
|
||||
└ Add Skill to Employee
|
||||
POST http://localhost:8000/api/v1/employees/1/add_skill/ [200 OK, 302B, 26ms]
|
||||
|
||||
└ Remove Skill from Employee
|
||||
POST http://localhost:8000/api/v1/employees/1/remove_skill/ [200 OK, 304B, 28ms]
|
||||
|
||||
└ Get Employee LDAP Info
|
||||
GET http://localhost:8000/api/v1/employees/1/ldap-info/ [404 Not Found, 343B, 29ms]
|
||||
|
||||
└ Export Employees to Excel
|
||||
GET http://localhost:8000/api/v1/employees/export-excel/ [200 OK, 6.21kB, 31ms]
|
||||
|
||||
□ Departments
|
||||
└ List Departments
|
||||
GET http://localhost:8000/api/v1/departments/ [200 OK, 321B, 17ms]
|
||||
|
||||
└ Get Department by ID
|
||||
GET http://localhost:8000/api/v1/departments/1/ [404 Not Found, 341B, 19ms]
|
||||
|
||||
└ Create Department
|
||||
POST http://localhost:8000/api/v1/departments/ [500 Internal Server Error, 122.17kB, 49ms]
|
||||
|
||||
□ Documents
|
||||
└ List Documents
|
||||
GET http://localhost:8000/api/v1/documents/?page=1 [200 OK, 321B, 14ms]
|
||||
|
||||
└ Get Document by ID
|
||||
GET http://localhost:8000/api/v1/documents/1/ [404 Not Found, 339B, 20ms]
|
||||
|
||||
└ Create Document
|
||||
POST http://localhost:8000/api/v1/documents/ [400 Bad Request, 325B, 13ms]
|
||||
|
||||
□ Feed (Posts)
|
||||
└ List Posts
|
||||
GET http://localhost:8000/api/v1/posts/?page=1 [200 OK, 321B, 19ms]
|
||||
|
||||
└ Create Post
|
||||
POST http://localhost:8000/api/v1/posts/ [400 Bad Request, 419B, 15ms]
|
||||
|
||||
└ Like Post
|
||||
POST http://localhost:8000/api/v1/posts/1/like/ [404 Not Found, 310B, 24ms]
|
||||
|
||||
└ List Comments
|
||||
GET http://localhost:8000/api/v1/comments/?post=1 [200 OK, 321B, 17ms]
|
||||
|
||||
└ Create Comment
|
||||
POST http://localhost:8000/api/v1/comments/ [400 Bad Request, 392B, 16ms]
|
||||
|
||||
□ Communications (Messenger)
|
||||
└ List Chats
|
||||
GET http://localhost:8000/api/v1/communications/chats/ [200 OK, 321B, 26ms]
|
||||
|
||||
└ Get Chat by ID
|
||||
GET http://localhost:8000/api/v1/communications/chats/1/ [404 Not Found, 335B, 28ms]
|
||||
|
||||
└ Create Chat
|
||||
POST http://localhost:8000/api/v1/communications/chats/ [400 Bad Request, 446B, 26ms]
|
||||
|
||||
└ List Messages
|
||||
GET http://localhost:8000/api/v1/communications/messages/?chat=1 [200 OK, 321B, 20ms]
|
||||
|
||||
└ Send Message
|
||||
POST http://localhost:8000/api/v1/communications/messages/ [400 Bad Request, 392B, 16ms]
|
||||
|
||||
□ Calendar
|
||||
└ List Events
|
||||
GET http://localhost:8000/api/v1/calendar/events/ [200 OK, 262B, 12ms]
|
||||
|
||||
└ Create Event
|
||||
POST http://localhost:8000/api/v1/calendar/events/ [400 Bad Request, 323B, 15ms]
|
||||
|
||||
└ List Calendars
|
||||
GET http://localhost:8000/api/v1/calendar/calendars/ [200 OK, 313B, 19ms]
|
||||
|
||||
□ Requests
|
||||
└ List Requests
|
||||
GET http://localhost:8000/api/v1/requests/?page=1 [200 OK, 321B, 15ms]
|
||||
|
||||
└ Get Request by ID
|
||||
GET http://localhost:8000/api/v1/requests/1/ [404 Not Found, 338B, 15ms]
|
||||
|
||||
└ Create Request
|
||||
POST http://localhost:8000/api/v1/requests/ [400 Bad Request, 325B, 14ms]
|
||||
|
||||
□ Notifications
|
||||
└ List Notifications
|
||||
GET http://localhost:8000/api/v1/notifications/ [200 OK, 311B, 18ms]
|
||||
|
||||
└ Mark as Read
|
||||
POST http://localhost:8000/api/v1/notifications/1/mark_as_read/ [404 Not Found, 118.87kB, 17ms]
|
||||
|
||||
└ Mark All as Read
|
||||
POST http://localhost:8000/api/v1/notifications/mark_all_as_read/ [404 Not Found, 118.88kB, 18ms]
|
||||
|
||||
□ Employee Actions
|
||||
└ List Employee Actions
|
||||
GET http://localhost:8000/api/v1/employee-actions/ [200 OK, 783B, 15ms]
|
||||
|
||||
└ Get Employee Action by ID
|
||||
GET http://localhost:8000/api/v1/employee-actions/1/ [200 OK, 539B, 17ms]
|
||||
|
||||
└ Create Employee Action
|
||||
POST http://localhost:8000/api/v1/employee-actions/ [400 Bad Request, 327B, 13ms]
|
||||
|
||||
□ Department Roles
|
||||
└ List Department Roles
|
||||
GET http://localhost:8000/api/v1/department-roles/ [200 OK, 321B, 19ms]
|
||||
|
||||
└ Get Role Assignments
|
||||
GET http://localhost:8000/api/v1/department-roles/1/assignments/ [404 Not Found, 325B, 13ms]
|
||||
|
||||
└ Assign Role to Employee
|
||||
POST http://localhost:8000/api/v1/department-roles/1/assign/ [404 Not Found, 320B, 17ms]
|
||||
|
||||
□ Advanced Skills
|
||||
└ Bulk Create Skills
|
||||
POST http://localhost:8000/api/v1/skills/bulk_create/ [201 Created, 295B, 17ms]
|
||||
|
||||
└ Merge Skills
|
||||
POST http://localhost:8000/api/v1/skills/merge/ [404 Not Found, 301B, 16ms]
|
||||
|
||||
□ Advanced Groups
|
||||
└ Get Group Permissions
|
||||
GET http://localhost:8000/api/v1/groups/1/permissions/ [404 Not Found, 316B, 14ms]
|
||||
|
||||
└ Set Group Permissions
|
||||
POST http://localhost:8000/api/v1/groups/1/set-permissions/ [404 Not Found, 311B, 17ms]
|
||||
|
||||
└ Get Group Employees
|
||||
GET http://localhost:8000/api/v1/groups/1/employees/ [404 Not Found, 111.62kB, 15ms]
|
||||
|
||||
└ Add Group Members
|
||||
POST http://localhost:8000/api/v1/groups/1/add-members/ [404 Not Found, 311B, 14ms]
|
||||
|
||||
└ Remove Group Members
|
||||
POST http://localhost:8000/api/v1/groups/1/remove-members/ [404 Not Found, 311B, 19ms]
|
||||
|
||||
┌─────────────────────────┬───────────────────┬───────────────────┐
|
||||
│ │ executed │ failed │
|
||||
├─────────────────────────┼───────────────────┼───────────────────┤
|
||||
│ iterations │ 1 │ 0 │
|
||||
├─────────────────────────┼───────────────────┼───────────────────┤
|
||||
│ requests │ 52 │ 0 │
|
||||
├─────────────────────────┼───────────────────┼───────────────────┤
|
||||
│ test-scripts │ 2 │ 0 │
|
||||
├─────────────────────────┼───────────────────┼───────────────────┤
|
||||
│ prerequest-scripts │ 0 │ 0 │
|
||||
├─────────────────────────┼───────────────────┼───────────────────┤
|
||||
│ assertions │ 0 │ 0 │
|
||||
├─────────────────────────┴───────────────────┴───────────────────┤
|
||||
│ total run duration: 4.6s │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ total data received: 512.31kB (approx) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ average response time: 23ms [min: 12ms, max: 190ms, s.d.: 24ms] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
83
nginx.conf
83
nginx.conf
@ -1,83 +0,0 @@
|
||||
upstream backend {
|
||||
server web:8000;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:3000;
|
||||
}
|
||||
|
||||
# WebSocket upgrade handling
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Frontend - Next.js application
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Django Admin
|
||||
location /admin/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket connections
|
||||
location /ws/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# Static files
|
||||
location /static/ {
|
||||
alias /app/staticfiles/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Media files
|
||||
location /media/ {
|
||||
alias /app/media/;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
# Health check
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "HiRo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user