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:
igor_nadein 2026-02-24 18:04:05 +03:00
parent 64c0abf548
commit 01b35c3b83
23 changed files with 2 additions and 373523 deletions

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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
- ✨ Система уведомлений
- ✨ Управление заявками

View File

@ -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
}

View File

@ -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

View File

@ -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" %}

View File

@ -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>

View File

@ -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>

View File

@ -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")

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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
**Ревью**: Требуется

File diff suppressed because it is too large Load Diff

View File

@ -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] │
└─────────────────────────────────────────────────────────────────┘

View File

@ -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
View File

@ -1,6 +0,0 @@
{
"name": "HiRo",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}