удалил лишнего
This commit is contained in:
parent
5a6dc011dd
commit
a14a58cace
@ -1,207 +0,0 @@
|
||||
# 🔍 Команды для отладки календаря через консоль браузера
|
||||
|
||||
Откройте консоль браузера (F12 → Console) и выполните эти команды:
|
||||
|
||||
## 1️⃣ Проверить наличие интеграции
|
||||
|
||||
```javascript
|
||||
// Должно вернуть объект, а не undefined
|
||||
window.calendarIntegration
|
||||
```
|
||||
|
||||
**Ожидаемый результат:**
|
||||
```javascript
|
||||
{
|
||||
fetchEventsForVisibleCalendars: ƒ,
|
||||
getVisibleCalendarIds: ƒ,
|
||||
getCalendars: ƒ,
|
||||
setVisibleCalendars: ƒ,
|
||||
refresh: ƒ,
|
||||
instances: {manager: {...}, modal: {...}, widget: {...}}
|
||||
}
|
||||
```
|
||||
|
||||
**Если undefined** → интеграция не инициализировалась!
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ Проверить виджет календаря
|
||||
|
||||
```javascript
|
||||
window.calendarWidget
|
||||
```
|
||||
|
||||
**Должен вернуть объект FullCalendar**
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ Проверить список календарей
|
||||
|
||||
```javascript
|
||||
// Если интеграция работает:
|
||||
window.calendarIntegration.getCalendars()
|
||||
|
||||
// Должно вернуть массив календарей:
|
||||
// [{id: 1, title: "Personal", color: "#0d6efd", ...}, ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ Проверить видимые календари
|
||||
|
||||
```javascript
|
||||
window.calendarIntegration.getVisibleCalendarIds()
|
||||
|
||||
// Должно вернуть массив ID: [1, 2, 3]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ Вручную загрузить события
|
||||
|
||||
```javascript
|
||||
// Попробовать загрузить события для видимых календарей
|
||||
const start = new Date('2026-02-01');
|
||||
const end = new Date('2026-02-28');
|
||||
|
||||
window.calendarIntegration.fetchEventsForVisibleCalendars(start, end)
|
||||
.then(events => {
|
||||
console.log('✅ Events loaded:', events.length);
|
||||
console.table(events);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('❌ Error:', err);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6️⃣ Проверить, загружен ли скрипт интеграции
|
||||
|
||||
```javascript
|
||||
// Проверить, есть ли в DOM скрипт
|
||||
document.querySelector('script[src*="calendarWidgetIntegration"]')
|
||||
|
||||
// Должен вернуть <script> элемент, если файл подключен
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7️⃣ Вручную инициализировать интеграцию
|
||||
|
||||
Если интеграция не загрузилась автоматически, попробуйте вручную:
|
||||
|
||||
```javascript
|
||||
// 1. Импортировать модуль (если ES6 modules работают)
|
||||
import('/static/js/components/calendarWidgetIntegration.js')
|
||||
.then(module => {
|
||||
const { integrateCalendarManager } = module;
|
||||
const integration = integrateCalendarManager(window.calendarWidget);
|
||||
window.calendarIntegration = integration;
|
||||
console.log('✅ Integration manually initialized');
|
||||
})
|
||||
.catch(err => console.error('❌ Import failed:', err));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8️⃣ Перезагрузить события календаря
|
||||
|
||||
```javascript
|
||||
// После того как интеграция инициализирована
|
||||
if (window.calendarWidget && window.calendarWidget.refetchEvents) {
|
||||
window.calendarWidget.refetchEvents();
|
||||
console.log('🔄 Events refetched');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9️⃣ Проверить контейнер списка календарей
|
||||
|
||||
```javascript
|
||||
// Проверить, существует ли контейнер
|
||||
const container = document.getElementById('calendarListContainer');
|
||||
console.log('Container exists:', !!container);
|
||||
console.log('Container HTML:', container?.innerHTML.substring(0, 200));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔟 Проверить модальное окно
|
||||
|
||||
```javascript
|
||||
// Проверить, есть ли модальное окно
|
||||
const modal = document.getElementById('calendarManageModal');
|
||||
console.log('Modal exists:', !!modal);
|
||||
|
||||
// Попробовать открыть модальное окно
|
||||
if (window.calendarIntegration?.instances?.modal) {
|
||||
window.calendarIntegration.instances.modal.openForCreate();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Диагностика проблемы
|
||||
|
||||
Выполните все команды по порядку и скопируйте результаты. Это поможет понять, на каком этапе происходит сбой:
|
||||
|
||||
```javascript
|
||||
console.log('=== Calendar Integration Debug ===');
|
||||
console.log('1. calendarIntegration:', typeof window.calendarIntegration);
|
||||
console.log('2. calendarWidget:', typeof window.calendarWidget);
|
||||
console.log('3. Container:', !!document.getElementById('calendarListContainer'));
|
||||
console.log('4. Modal:', !!document.getElementById('calendarManageModal'));
|
||||
console.log('5. Create button:', !!document.querySelector('[data-action="create-calendar"]'));
|
||||
console.log('=================================');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Если интеграция undefined
|
||||
|
||||
Проверьте, загружается ли файл `calendar_scripts.html`:
|
||||
|
||||
```javascript
|
||||
// Найти все script теги с calendar
|
||||
Array.from(document.querySelectorAll('script[type="module"]'))
|
||||
.filter(s => s.textContent.includes('calendar'))
|
||||
.forEach(s => console.log('Found:', s.textContent.substring(0, 100)));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Полный отчёт о состоянии
|
||||
|
||||
Скопируйте и выполните весь блок:
|
||||
|
||||
```javascript
|
||||
(function debugCalendar() {
|
||||
const report = {
|
||||
integration: typeof window.calendarIntegration,
|
||||
widget: typeof window.calendarWidget,
|
||||
container: !!document.getElementById('calendarListContainer'),
|
||||
modal: !!document.getElementById('calendarManageModal'),
|
||||
createButton: !!document.querySelector('[data-action="create-calendar"]'),
|
||||
scripts: Array.from(document.querySelectorAll('script[type="module"]')).length,
|
||||
errors: []
|
||||
};
|
||||
|
||||
if (window.calendarIntegration) {
|
||||
try {
|
||||
report.calendars = window.calendarIntegration.getCalendars().length;
|
||||
report.visibleIds = window.calendarIntegration.getVisibleCalendarIds();
|
||||
} catch (e) {
|
||||
report.errors.push('Error accessing integration: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.table(report);
|
||||
return report;
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Выполните команды и покажите результаты! 🔍
|
||||
@ -1,60 +0,0 @@
|
||||
# 🔧 Исправление проблемы инициализации calendar integration
|
||||
|
||||
## Проблема
|
||||
`window.calendarIntegration` = undefined, но скрипт подключён.
|
||||
|
||||
## Быстрое решение - выполните в консоли:
|
||||
|
||||
```javascript
|
||||
// Проверка наличия скрипта
|
||||
const scripts = Array.from(document.querySelectorAll('script[type="module"]'));
|
||||
console.log('Module scripts found:', scripts.length);
|
||||
scripts.forEach((s, i) => {
|
||||
if (s.textContent.includes('calendar')) {
|
||||
console.log(`Script ${i}:`, s.textContent.substring(0, 200));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Если скрипт найден, но не работает - вручную загрузите:
|
||||
|
||||
```javascript
|
||||
(async function manualInit() {
|
||||
try {
|
||||
console.log('🔄 Manually loading calendar integration...');
|
||||
|
||||
// Импорт модуля
|
||||
const module = await import('/static/js/components/calendarWidgetIntegration.js');
|
||||
console.log('✅ Module loaded:', module);
|
||||
|
||||
// Инициализация
|
||||
const { integrateCalendarManager } = module;
|
||||
const integration = integrateCalendarManager(window.calendarWidget);
|
||||
|
||||
if (integration) {
|
||||
window.calendarIntegration = integration;
|
||||
console.log('✅ Integration initialized successfully!');
|
||||
console.log('Calendars:', integration.getCalendars().length);
|
||||
console.log('Visible:', integration.getVisibleCalendarIds());
|
||||
|
||||
// Перезагрузить события
|
||||
if (window.calendarWidget.refetchEvents) {
|
||||
window.calendarWidget.refetchEvents();
|
||||
console.log('🔄 Events refetched with new integration');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Manual init failed:', error);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
## После выполнения проверьте:
|
||||
|
||||
```javascript
|
||||
// Должно вернуть объект, а не undefined
|
||||
window.calendarIntegration
|
||||
|
||||
// Должно показать логи с новой системой
|
||||
window.calendarWidget.refetchEvents()
|
||||
```
|
||||
@ -1,761 +0,0 @@
|
||||
# Тестирование Прав Доступа Через Должности (Position Permissions)
|
||||
|
||||
## ✅ РЕШЕНИЕ НАЙДЕНО И ПРИМЕНЕНО
|
||||
|
||||
**Дата:** 8 декабря 2025
|
||||
**Статус:** ✅ Исправлено и протестировано
|
||||
|
||||
### Корневая Причина
|
||||
|
||||
В шаблоне `feed/feed_list.html` проверялись **несуществующие** права:
|
||||
```django
|
||||
{% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
|
||||
```
|
||||
|
||||
Эти права **никогда не создавались** в Django! Правильное право для создания постов:
|
||||
```django
|
||||
{% if user.is_staff or perms.feed.add_post %}
|
||||
```
|
||||
|
||||
### Диагностика Production
|
||||
|
||||
Запущен отладочный скрипт для пользователя `maze3290@gmail.com`:
|
||||
|
||||
```python
|
||||
User: maze3290@gmail.com
|
||||
Position: СММ специалист
|
||||
Position Groups: ['Дизайн СММ Реклама']
|
||||
Group Permissions: ['add_post', 'change_post', 'delete_post', 'view_post', ...]
|
||||
|
||||
User Permissions (via PositionRoleBackend):
|
||||
{'feed.add_post', 'feed.change_post', 'feed.delete_post', ...}
|
||||
|
||||
Has feed.publish_company_post: False # ❌ Несуществующее право!
|
||||
Has feed.add_post: True # ✅ Реальное право!
|
||||
```
|
||||
|
||||
**Вывод:** PositionRoleBackend работает правильно, проблема была в шаблоне!
|
||||
|
||||
### Исправление
|
||||
|
||||
**Файл:** `backend/templates/feed/feed_list.html` (строка 19)
|
||||
|
||||
```diff
|
||||
- {% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
|
||||
+ {% if user.is_staff or perms.feed.add_post %}
|
||||
```
|
||||
|
||||
### Коммиты
|
||||
|
||||
1. `769f2ed` - test: add comprehensive position permissions tests
|
||||
2. `66406d9` - fix: add is_active check to PositionRoleBackend
|
||||
3. `4a6b92f` - docs: add production debugging guide
|
||||
4. `6162d9f` - **fix: correct permission check in feed_list.html** ← РЕШЕНИЕ
|
||||
|
||||
---
|
||||
|
||||
## Описание Проблемы (Оригинал)
|
||||
|
||||
### Исходная Жалоба Пользователя
|
||||
> "Ты говорил, что пользователь с должностью получает все права которые есть у групп в этой должности, но по факту кнопка создания публикаций не появляется"
|
||||
|
||||
**Ожидаемое поведение:**
|
||||
- Пользователь имеет должность (Position)
|
||||
- Должность связана с группами (Position.groups)
|
||||
- Группы имеют права (Group.permissions)
|
||||
- Пользователь должен получить все права из групп своей должности
|
||||
|
||||
**Фактическое поведение:**
|
||||
- UI кнопки не появляются (например, "Создать публикацию")
|
||||
- Пользователи с должностями не могут выполнять действия, несмотря на наличие прав в группах
|
||||
|
||||
## Архитектура Системы Прав
|
||||
|
||||
### Цепочка Наследования
|
||||
```
|
||||
Position → Position.groups (ManyToMany) → Group.permissions (ManyToMany) → Permission
|
||||
↓
|
||||
Employee.position (ForeignKey)
|
||||
↓
|
||||
User (связан через OneToOne)
|
||||
```
|
||||
|
||||
### Реализация Backend
|
||||
|
||||
**Файл:** `backend/eusrr_backend/auth_backends.py`
|
||||
|
||||
```python
|
||||
class PositionRoleBackend(BaseBackend):
|
||||
"""
|
||||
Custom authentication backend that grants permissions based on
|
||||
Position.groups relationships.
|
||||
"""
|
||||
|
||||
def get_all_permissions(self, user_obj, obj=None):
|
||||
if not user_obj.is_active or user_obj.is_anonymous:
|
||||
return set()
|
||||
|
||||
# Получаем права из должности
|
||||
if hasattr(user_obj, 'employee') and user_obj.employee.position:
|
||||
pos = user_obj.employee.position
|
||||
return set(
|
||||
Permission.objects.filter(
|
||||
group__positions=pos
|
||||
).select_related('content_type').values_list(
|
||||
'content_type__app_label',
|
||||
'codename'
|
||||
)
|
||||
)
|
||||
|
||||
return set()
|
||||
```
|
||||
|
||||
**Конфигурация:** `settings.py` (строка 329)
|
||||
```python
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'eusrr_backend.auth_backends.LDAPAuthBackend',
|
||||
'eusrr_backend.auth_backends.LDAPGroupAuthBackend',
|
||||
'eusrr_backend.auth_backends.PositionRoleBackend', # ← Наш backend
|
||||
]
|
||||
```
|
||||
|
||||
### Модели
|
||||
|
||||
**Файл:** `backend/employees/models.py`
|
||||
|
||||
```python
|
||||
class Position(models.Model):
|
||||
"""Должность сотрудника"""
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
groups = models.ManyToManyField(
|
||||
Group,
|
||||
related_name='positions',
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Employee(models.Model):
|
||||
"""Сотрудник"""
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
position = models.ForeignKey(
|
||||
Position,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
```
|
||||
|
||||
## Тестовая Стратегия
|
||||
|
||||
### Цель Тестов
|
||||
Определить, является ли проблема:
|
||||
1. **Логической ошибкой** в `PositionRoleBackend` → Тесты упадут
|
||||
2. **Проблемой конфигурации** (кэширование, LDAP sync, deployment) → Тесты пройдут
|
||||
|
||||
### Структура Тестов
|
||||
|
||||
**Файл:** `backend/tests/employees/test_position_permissions.py`
|
||||
|
||||
#### Базовые Тесты (TestPositionPermissions)
|
||||
```python
|
||||
class TestPositionPermissions:
|
||||
"""Основная функциональность"""
|
||||
|
||||
✅ test_user_without_position_has_no_permissions
|
||||
Проверяет: Пользователь без должности = нет прав
|
||||
|
||||
❌ test_user_with_position_gets_permissions
|
||||
Проверяет: Position с Group → User получает права
|
||||
|
||||
❌ test_position_change_updates_permissions
|
||||
Проверяет: Смена должности → права обновляются
|
||||
|
||||
❌ test_remove_position_removes_permissions
|
||||
Проверяет: Удаление должности → права убираются
|
||||
|
||||
❌ test_multiple_users_same_position
|
||||
Проверяет: Несколько user с одной Position → одинаковые права
|
||||
|
||||
❌ test_staff_user_keeps_permissions
|
||||
Проверяет: Staff users сохраняют is_staff права
|
||||
|
||||
✅ test_position_group_permissions_are_additive
|
||||
Проверяет: Несколько групп → права складываются
|
||||
|
||||
❌ test_get_all_permissions_includes_position_perms
|
||||
Проверяет: get_all_permissions() возвращает права должности
|
||||
```
|
||||
|
||||
#### Граничные Случаи (TestPositionPermissionsEdgeCases)
|
||||
```python
|
||||
class TestPositionPermissionsEdgeCases:
|
||||
"""Edge cases и особые ситуации"""
|
||||
|
||||
✅ test_position_without_groups_grants_no_permissions
|
||||
Проверяет: Position без groups = нет прав
|
||||
|
||||
❌ test_inactive_user_has_no_permissions
|
||||
Проверяет: Неактивный user = нет прав (даже с Position)
|
||||
|
||||
❌ test_direct_user_permissions_and_position_permissions
|
||||
Проверяет: Прямые права user + права Position = аддитивны
|
||||
```
|
||||
|
||||
#### Интеграционные Тесты (TestPositionPermissionsIntegration)
|
||||
```python
|
||||
class TestPositionPermissionsIntegration:
|
||||
"""Интеграция с реальными моделями"""
|
||||
|
||||
❌ test_feed_publish_permission_via_position
|
||||
Проверяет: Право "feed.publish_company_post" работает
|
||||
|
||||
❌ test_document_add_permission_via_position
|
||||
Проверяет: Право "documents.add_document" работает
|
||||
```
|
||||
|
||||
### Текущие Результаты
|
||||
```
|
||||
============== test session starts ==============
|
||||
collected 13 items
|
||||
|
||||
✅ PASSED: 3 tests
|
||||
❌ ERRORS: 10 tests
|
||||
|
||||
Passing Tests:
|
||||
- test_user_without_position_has_no_permissions
|
||||
- test_position_group_permissions_are_additive
|
||||
- test_position_without_groups_grants_no_permissions
|
||||
|
||||
Failing Tests:
|
||||
- All tests with IntegrityError or fixture issues
|
||||
```
|
||||
|
||||
## Обнаруженные Проблемы в Тестах
|
||||
|
||||
### Проблема 1: IntegrityError на Permission Creation
|
||||
**Ошибка:**
|
||||
```
|
||||
IntegrityError: UNIQUE constraint failed: auth_permission.content_type_id, auth_permission.codename
|
||||
```
|
||||
|
||||
**Причина:**
|
||||
Использование `Permission.objects.get_or_create()` в фикстурах приводит к попыткам создать дубликаты.
|
||||
|
||||
**Решение:**
|
||||
Изменить паттерн создания:
|
||||
|
||||
```python
|
||||
# ❌ Старый код
|
||||
perm, _ = Permission.objects.get_or_create(
|
||||
codename='publish_company_post',
|
||||
content_type=feed_content_type,
|
||||
defaults={'name': 'Can publish company post'}
|
||||
)
|
||||
|
||||
# ✅ Новый код
|
||||
perm = Permission.objects.filter(
|
||||
codename='publish_company_post',
|
||||
content_type=feed_content_type
|
||||
).first()
|
||||
|
||||
if not perm:
|
||||
perm = Permission.objects.create(
|
||||
codename='publish_company_post',
|
||||
content_type=feed_content_type,
|
||||
name='Can publish company post'
|
||||
)
|
||||
```
|
||||
|
||||
**Статус:** Исправлено в 3 местах:
|
||||
- ✅ Фикстура `group_with_publish_perm` (строки 44-62)
|
||||
- ✅ Фикстура `group_with_document_perm` (строки 64-82)
|
||||
- ✅ Тест `test_position_group_permissions_are_additive` (строки 244-269)
|
||||
|
||||
### Проблема 2: Fixture Visibility Between Test Classes
|
||||
**Ошибка:**
|
||||
```
|
||||
fixture 'engineer_position' not found
|
||||
fixture 'feed_content_type' not found
|
||||
fixture 'documents_content_type' not found
|
||||
```
|
||||
|
||||
**Причина:**
|
||||
Фикстуры определены в `TestPositionPermissions` как методы класса, но дочерние классы `TestPositionPermissionsEdgeCases` и `TestPositionPermissionsIntegration` не имеют к ним доступа.
|
||||
|
||||
**Возможные решения:**
|
||||
1. Перенести фикстуры на уровень модуля (вне классов)
|
||||
2. Использовать `@pytest.fixture(scope="module")`
|
||||
3. Использовать наследование классов
|
||||
4. Создать фикстуры в `conftest.py`
|
||||
|
||||
**Статус:** ❌ Не исправлено
|
||||
|
||||
### Проблема 3: Permission Objects Already Exist in Database
|
||||
**Ошибка:**
|
||||
При запуске тестов Django уже имеет базовые права из миграций.
|
||||
|
||||
**Решение:**
|
||||
Использовать `.filter().first()` вместо `.create()` для всех Permission объектов.
|
||||
|
||||
**Статус:** ✅ Частично исправлено (нужна проверка всех 13 тестов)
|
||||
|
||||
## План Исправления
|
||||
|
||||
### Шаг 1: Рефакторинг Фикстур ✅ IN PROGRESS
|
||||
- [x] Заменить `get_or_create` на `filter().first()` + conditional create
|
||||
- [x] Применить в `group_with_publish_perm`
|
||||
- [x] Применить в `group_with_document_perm`
|
||||
- [x] Применить в `test_position_group_permissions_are_additive`
|
||||
- [ ] Перенести фикстуры на module level
|
||||
|
||||
### Шаг 2: Запуск Тестов
|
||||
```bash
|
||||
cd /c/Users/Игорь/Dev/EUSRR/backend
|
||||
pytest tests/employees/test_position_permissions.py -v --tb=short
|
||||
```
|
||||
|
||||
**Ожидаемый результат после исправлений:**
|
||||
- Все 13 тестов должны запуститься (не ERROR)
|
||||
- Проверим, сколько PASS vs FAIL
|
||||
|
||||
### Шаг 3: Анализ Результатов
|
||||
|
||||
#### Сценарий А: Все тесты PASS ✅
|
||||
**Вывод:** Логика `PositionRoleBackend` корректна
|
||||
|
||||
**Действия:**
|
||||
1. Проверить production конфигурацию:
|
||||
- `AUTHENTICATION_BACKENDS` order
|
||||
- Кэширование прав (Django cache)
|
||||
- LDAP синхронизация групп
|
||||
|
||||
2. Проверить данные в БД:
|
||||
```python
|
||||
# Django shell
|
||||
user = User.objects.get(username='...')
|
||||
print(user.employee.position)
|
||||
print(user.employee.position.groups.all())
|
||||
print(user.get_all_permissions()) # Должны быть права из Position
|
||||
```
|
||||
|
||||
3. Проверить middleware и template context
|
||||
|
||||
#### Сценарий Б: Некоторые тесты FAIL ❌
|
||||
**Вывод:** Есть проблема в логике `PositionRoleBackend`
|
||||
|
||||
**Действия:**
|
||||
1. Проанализировать failing assertions
|
||||
2. Исправить `get_all_permissions()` или `has_perm()`
|
||||
3. Убедиться что query `Permission.objects.filter(group__positions=pos)` корректен
|
||||
4. Проверить returns в формате "app_label.codename"
|
||||
|
||||
## Проверка UI Visibility
|
||||
|
||||
### Текущая Реализация
|
||||
|
||||
**Файл:** `backend/templates/feed/feed_list.html` (строка 19)
|
||||
```html
|
||||
{% if user.is_staff or perms.feed.publish_company_post or perms.feed.publish_department_post %}
|
||||
<button class="btn btn-primary">Создать публикацию</button>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Файл:** `backend/templates/documents/document_list.html` (строки 17-21)
|
||||
```html
|
||||
{% if user.is_staff or perms.documents.add_document %}
|
||||
<a href="{% url 'documents:document_create' %}" class="btn btn-primary">
|
||||
Добавить документ
|
||||
</a>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Логика Проверки в Templates
|
||||
|
||||
Django использует `perms.app_label.codename` в шаблонах:
|
||||
1. Вызывает `user.has_perm('app_label.codename')`
|
||||
2. Проходит по всем AUTHENTICATION_BACKENDS
|
||||
3. Если хотя бы один backend вернёт `True` → permission granted
|
||||
|
||||
**Требования:**
|
||||
- `PositionRoleBackend.has_perm()` должен вернуть `True`
|
||||
- `PositionRoleBackend.get_all_permissions()` должен включать это право
|
||||
- Формат: tuple `('app_label', 'codename')` или строка `'app_label.codename'`
|
||||
|
||||
## Отладочные Команды
|
||||
|
||||
### Django Shell Testing
|
||||
```python
|
||||
from django.contrib.auth.models import User, Permission, Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from employees.models import Employee, Position
|
||||
|
||||
# Получить пользователя
|
||||
user = User.objects.get(username='test_user')
|
||||
|
||||
# Проверить должность
|
||||
print(f"Position: {user.employee.position}")
|
||||
print(f"Position Groups: {user.employee.position.groups.all()}")
|
||||
|
||||
# Проверить права через backend
|
||||
from eusrr_backend.auth_backends import PositionRoleBackend
|
||||
backend = PositionRoleBackend()
|
||||
all_perms = backend.get_all_permissions(user)
|
||||
print(f"All Permissions: {all_perms}")
|
||||
|
||||
# Проверить has_perm
|
||||
has_perm = backend.has_perm(user, 'feed.publish_company_post')
|
||||
print(f"Has feed.publish_company_post: {has_perm}")
|
||||
|
||||
# Проверить через Django API
|
||||
has_perm_django = user.has_perm('feed.publish_company_post')
|
||||
print(f"User.has_perm('feed.publish_company_post'): {has_perm_django}")
|
||||
```
|
||||
|
||||
### SQL Query Debugging
|
||||
```python
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
perms = backend.get_all_permissions(user)
|
||||
|
||||
for q in queries:
|
||||
print(q['sql'])
|
||||
```
|
||||
|
||||
### LDAP Group Sync Check
|
||||
```python
|
||||
from employees.ldap.services import PositionService
|
||||
|
||||
pos = Position.objects.get(name='Engineer')
|
||||
dn = PositionService._ensure_position_group(pos)
|
||||
print(f"LDAP Group DN: {dn}")
|
||||
```
|
||||
|
||||
## Ожидаемая SQL Query
|
||||
|
||||
При вызове `get_all_permissions()` должен выполниться query:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT
|
||||
django_content_type.app_label,
|
||||
auth_permission.codename
|
||||
FROM auth_permission
|
||||
INNER JOIN django_content_type
|
||||
ON auth_permission.content_type_id = django_content_type.id
|
||||
INNER JOIN auth_group_permissions
|
||||
ON auth_permission.id = auth_group_permissions.permission_id
|
||||
INNER JOIN auth_group
|
||||
ON auth_group_permissions.group_id = auth_group.id
|
||||
INNER JOIN employees_position_groups
|
||||
ON auth_group.id = employees_position_groups.group_id
|
||||
WHERE employees_position_groups.position_id = ?
|
||||
```
|
||||
|
||||
## Контрольный Чеклист
|
||||
|
||||
### Pre-Test Checklist
|
||||
- [x] Создан файл `tests/employees/test_position_permissions.py`
|
||||
- [x] 13 тестов покрывают все сценарии
|
||||
- [x] Фикстуры используют `.filter().first()` для Permission
|
||||
- [ ] Фикстуры доступны во всех test классах
|
||||
- [ ] Тесты запускаются без ERROR
|
||||
|
||||
### Post-Test Checklist (если тесты проходят)
|
||||
- [ ] Проверить `user.get_all_permissions()` в production shell
|
||||
- [ ] Проверить `user.has_perm('feed.publish_company_post')` в production
|
||||
- [ ] Проверить что `Position.groups` содержит нужные группы
|
||||
- [ ] Проверить что `Group.permissions` содержит нужные права
|
||||
- [ ] Проверить порядок AUTHENTICATION_BACKENDS
|
||||
- [ ] Очистить Django permission cache
|
||||
- [ ] Перезапустить сервер
|
||||
|
||||
### Post-Test Checklist (если тесты не проходят)
|
||||
- [ ] Проанализировать failing assertions
|
||||
- [ ] Исправить логику в `PositionRoleBackend.get_all_permissions()`
|
||||
- [ ] Исправить логику в `PositionRoleBackend.has_perm()`
|
||||
- [ ] Убедиться что query возвращает правильный формат
|
||||
- [ ] Проверить related_name в моделях
|
||||
- [ ] Перезапустить тесты
|
||||
|
||||
## Следующие Шаги
|
||||
|
||||
1. **Сейчас:** Исправить fixture visibility (перенести на module level)
|
||||
2. **Потом:** Запустить pytest с verbose output
|
||||
3. **Далее:** Проанализировать PASS/FAIL результаты
|
||||
4. **Финал:** Исправить production или backend в зависимости от результатов
|
||||
|
||||
## Дополнительная Информация
|
||||
|
||||
### Связанные Файлы
|
||||
- `backend/eusrr_backend/auth_backends.py` - PositionRoleBackend
|
||||
- `backend/employees/models.py` - Position, Employee
|
||||
- `backend/templates/feed/feed_list.html` - UI для публикаций
|
||||
- `backend/templates/documents/document_list.html` - UI для документов
|
||||
- `backend/tests/employees/test_position_permissions.py` - Тесты
|
||||
|
||||
### Связанные Коммиты
|
||||
- `769f2ed` - test: add comprehensive position permissions tests
|
||||
|
||||
### Известные Ограничения
|
||||
- Права кэшируются Django (может требовать перезапуска)
|
||||
- LDAP синхронизация может задерживаться
|
||||
- Staff users всегда имеют все права (is_staff=True)
|
||||
|
||||
---
|
||||
|
||||
**Документ создан:** 2025-01-XX
|
||||
**Последнее обновление:** ✅ ВСЕ ТЕСТЫ ПРОШЛИ (13/13 PASSED)
|
||||
**Статус:** ✅ Готово к production тестированию
|
||||
|
||||
## ВАЖНО: Что Делать Дальше
|
||||
|
||||
### ✅ Что Мы Выяснили
|
||||
|
||||
**Тесты показали, что логика `PositionRoleBackend` работает ПРАВИЛЬНО!**
|
||||
|
||||
- ✅ Пользователи с должностями получают права из групп
|
||||
- ✅ Права обновляются при смене должности
|
||||
- ✅ Права удаляются при снятии должности
|
||||
- ✅ `has_perm()` и `get_all_permissions()` работают корректно
|
||||
- ✅ Добавлена проверка `is_active` (исправили баг)
|
||||
|
||||
### 🔍 Где Искать Проблему
|
||||
|
||||
Если кнопки **всё ещё не появляются** в production, проблема НЕ в коде, а в:
|
||||
|
||||
1. **Кэширование прав Django**
|
||||
- Django кэширует права в `_perm_cache` и `_user_perm_cache`
|
||||
- После изменения должности нужен перелогин или очистка кэша
|
||||
|
||||
2. **LDAP синхронизация групп**
|
||||
- Проверить что `Position.groups` содержит правильные группы
|
||||
- Проверить что `Group.permissions` содержит нужные права
|
||||
|
||||
3. **Порядок AUTHENTICATION_BACKENDS**
|
||||
- `PositionRoleBackend` должен быть в списке (сейчас на 4 месте ✅)
|
||||
- Возможно другие backend'ы блокируют его работу
|
||||
|
||||
### 📋 Чеклист Отладки Production
|
||||
|
||||
#### Шаг 1: Проверить Права в Django Shell
|
||||
|
||||
```python
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
|
||||
# Замените на реального пользователя
|
||||
user = User.objects.get(username='имя_пользователя')
|
||||
|
||||
print(f"1. Position: {user.position}")
|
||||
print(f"2. Position Groups: {user.position.groups.all() if user.position else 'NO POSITION'}")
|
||||
|
||||
if user.position:
|
||||
for group in user.position.groups.all():
|
||||
print(f" - Group: {group.name}")
|
||||
print(f" Permissions: {list(group.permissions.values_list('codename', flat=True))}")
|
||||
|
||||
print(f"\n3. User Permissions:")
|
||||
print(f" All Permissions: {user.get_all_permissions()}")
|
||||
print(f" Has feed.publish_company_post: {user.has_perm('feed.publish_company_post')}")
|
||||
print(f" Has documents.add_document: {user.has_perm('documents.add_document')}")
|
||||
```
|
||||
|
||||
**Ожидаемый результат:**
|
||||
- `Position` должен быть не `None`
|
||||
- `Position.groups` должны содержать группы
|
||||
- `Group.permissions` должны включать нужные права
|
||||
- `user.get_all_permissions()` должен возвращать права из должности
|
||||
- `user.has_perm('feed.publish_company_post')` должен быть `True`
|
||||
|
||||
#### Шаг 2: Проверить Базу Данных
|
||||
|
||||
```sql
|
||||
-- Проверить связь User → Employee → Position
|
||||
SELECT u.username, e.id, p.name
|
||||
FROM auth_user u
|
||||
JOIN employees_employee e ON e.user_id = u.id
|
||||
LEFT JOIN employees_position p ON e.position_id = p.id
|
||||
WHERE u.username = 'имя_пользователя';
|
||||
|
||||
-- Проверить связь Position → Groups
|
||||
SELECT p.name AS position, g.name AS group_name
|
||||
FROM employees_position p
|
||||
JOIN employees_position_groups pg ON pg.position_id = p.id
|
||||
JOIN auth_group g ON pg.group_id = g.id
|
||||
WHERE p.id = <position_id>;
|
||||
|
||||
-- Проверить связь Group → Permissions
|
||||
SELECT g.name AS group_name, p.codename
|
||||
FROM auth_group g
|
||||
JOIN auth_group_permissions gp ON gp.group_id = g.id
|
||||
JOIN auth_permission p ON gp.permission_id = p.id
|
||||
WHERE g.id IN (
|
||||
SELECT group_id FROM employees_position_groups
|
||||
WHERE position_id = <position_id>
|
||||
);
|
||||
```
|
||||
|
||||
#### Шаг 3: Очистить Кэш Прав
|
||||
|
||||
```python
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
|
||||
user = User.objects.get(username='имя_пользователя')
|
||||
|
||||
# Очистить кэши прав
|
||||
if hasattr(user, '_perm_cache'):
|
||||
delattr(user, '_perm_cache')
|
||||
if hasattr(user, '_user_perm_cache'):
|
||||
delattr(user, '_user_perm_cache')
|
||||
|
||||
# Или просто получить fresh instance
|
||||
user = User.objects.get(id=user.id)
|
||||
|
||||
# Проверить снова
|
||||
print(user.has_perm('feed.publish_company_post'))
|
||||
```
|
||||
|
||||
#### Шаг 4: Проверить Template Context
|
||||
|
||||
В шаблоне `feed/feed_list.html`:
|
||||
```django
|
||||
{# DEBUG: Покажите права пользователя #}
|
||||
{% if user.is_authenticated %}
|
||||
<p>User: {{ user.username }}</p>
|
||||
<p>Position: {{ user.position }}</p>
|
||||
<p>Has publish_company_post: {{ perms.feed.publish_company_post }}</p>
|
||||
<p>Has publish_department_post: {{ perms.feed.publish_department_post }}</p>
|
||||
<p>Is Staff: {{ user.is_staff }}</p>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### Шаг 5: Проверить Middleware
|
||||
|
||||
В `settings.py` убедитесь что есть:
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
...
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### Шаг 6: Перезапустить Сервер
|
||||
|
||||
```bash
|
||||
# Остановить
|
||||
Ctrl+C
|
||||
|
||||
# Очистить кэш (если используется Redis/Memcached)
|
||||
python manage.py shell
|
||||
>>> from django.core.cache import cache
|
||||
>>> cache.clear()
|
||||
>>> exit()
|
||||
|
||||
# Запустить снова
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
#### Шаг 7: Перелогинить Пользователя
|
||||
|
||||
- Попросите пользователя выйти из системы
|
||||
- Войти заново
|
||||
- Проверить видимость кнопок
|
||||
|
||||
### 🐛 Возможные Проблемы
|
||||
|
||||
#### Проблема 1: Position.groups пустые
|
||||
|
||||
**Симптом:** `user.position.groups.all()` возвращает пустой список
|
||||
|
||||
**Решение:**
|
||||
```python
|
||||
from employees.models import Position
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
# Найти или создать группу
|
||||
pub_group = Group.objects.get(name="publishers")
|
||||
|
||||
# Назначить группу должности
|
||||
position = Position.objects.get(name="Инженер")
|
||||
position.groups.add(pub_group)
|
||||
```
|
||||
|
||||
#### Проблема 2: Group.permissions пустые
|
||||
|
||||
**Симптом:** `group.permissions.all()` возвращает пустой список
|
||||
|
||||
**Решение:**
|
||||
```python
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Найти Content Type
|
||||
ct = ContentType.objects.get(app_label="feed", model="post")
|
||||
|
||||
# Найти или создать Permission
|
||||
perm = Permission.objects.filter(
|
||||
codename="publish_company_post",
|
||||
content_type=ct
|
||||
).first()
|
||||
|
||||
if not perm:
|
||||
perm = Permission.objects.create(
|
||||
codename="publish_company_post",
|
||||
name="Can publish company post",
|
||||
content_type=ct
|
||||
)
|
||||
|
||||
# Добавить в группу
|
||||
pub_group = Group.objects.get(name="publishers")
|
||||
pub_group.permissions.add(perm)
|
||||
```
|
||||
|
||||
#### Проблема 3: LDAP не синхронизирует группы
|
||||
|
||||
**Симптом:** В LDAP нет группы `POS_Инженер`
|
||||
|
||||
**Решение:**
|
||||
```python
|
||||
from employees.models import Position
|
||||
from employees.ldap.services import PositionService
|
||||
|
||||
position = Position.objects.get(name="Инженер")
|
||||
dn = PositionService._ensure_position_group(position)
|
||||
print(f"LDAP Group DN: {dn}")
|
||||
```
|
||||
|
||||
### <20> Итоговая Таблица Результатов Тестов
|
||||
|
||||
| Тест | Статус | Что проверяет |
|
||||
|------|--------|---------------|
|
||||
| test_user_without_position_has_no_permissions | ✅ PASS | Без должности = нет прав |
|
||||
| test_user_with_position_gets_permissions | ✅ PASS | С должностью = права есть |
|
||||
| test_position_change_updates_permissions | ✅ PASS | Смена должности обновляет права |
|
||||
| test_remove_position_removes_permissions | ✅ PASS | Удаление должности убирает права |
|
||||
| test_multiple_users_with_same_position | ✅ PASS | Несколько user одна должность |
|
||||
| test_superuser_has_all_permissions | ✅ PASS | Superuser имеет все права |
|
||||
| test_position_group_permissions_are_additive | ✅ PASS | Несколько групп складываются |
|
||||
| test_get_all_permissions_includes_position_perms | ✅ PASS | get_all_permissions() работает |
|
||||
| test_position_without_groups_grants_no_permissions | ✅ PASS | Должность без групп = нет прав |
|
||||
| test_inactive_user_has_no_permissions | ✅ PASS | Неактивный user = нет прав |
|
||||
| test_position_permissions_with_direct_user_permissions | ✅ PASS | Прямые + должность аддитивны |
|
||||
| test_feed_permissions_through_position | ✅ PASS | Реальные feed права работают |
|
||||
| test_documents_permissions_through_position | ✅ PASS | Реальные document права работают |
|
||||
|
||||
### 🎯 Рекомендации
|
||||
|
||||
1. **Запустить отладочный скрипт** (Шаг 1) чтобы увидеть текущее состояние
|
||||
2. **Проверить LDAP синхронизацию** группы для должности
|
||||
3. **Очистить кэш** и перелогинить пользователя
|
||||
4. **Если проблема остаётся:** Показать вывод отладочного скрипта
|
||||
|
||||
**Результат должен быть:** Кнопки появляются у пользователей с нужными должностями! 🎉
|
||||
|
||||
---
|
||||
|
||||
**Документ создан:** 2025-01-XX
|
||||
**Последнее обновление:** ✅ ВСЕ ТЕСТЫ ПРОШЛИ (13/13 PASSED)
|
||||
**Статус:** ✅ Готово к production тестированию
|
||||
@ -1,267 +0,0 @@
|
||||
============================= test session starts =============================
|
||||
platform win32 -- Python 3.12.10, pytest-8.4.1, pluggy-1.6.0 -- C:\Users\nadeini\Dev\EUSRR\.venv\Scripts\python.exe
|
||||
cachedir: .pytest_cache
|
||||
django: version: 5.2.4, settings: eusrr_backend.settings (from ini)
|
||||
rootdir: C:\Users\nadeini\Dev\EUSRR\backend
|
||||
configfile: pytest.ini
|
||||
plugins: anyio-4.12.0, django-4.11.1
|
||||
collecting ... collected 19 items
|
||||
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive FAILED [ 5%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[first_name] PASSED [ 10%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[last_name] PASSED [ 15%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[phone_number] PASSED [ 21%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[email] PASSED [ 26%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[password] PASSED [ 31%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[birth_date] PASSED [ 36%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_requires_at_least_one_contact PASSED [ 42%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_birth_date_must_be_valid_date PASSED [ 47%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone FAILED [ 52%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields FAILED [ 57%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user FAILED [ 63%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code FAILED [ 68%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid FAILED [ 73%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account FAILED [ 78%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify FAILED [ 84%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify FAILED [ 89%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify FAILED [ 94%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_wrong_password FAILED [100%]
|
||||
|
||||
================================== FAILURES ===================================
|
||||
_____________ test_register_success_sends_email_and_user_inactive _____________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601BABB30>
|
||||
|
||||
def test_register_success_sends_email_and_user_inactive(api):
|
||||
resp = register(api)
|
||||
> assert resp.status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:102: AssertionError
|
||||
---------------------------- Captured stderr setup ----------------------------
|
||||
Creating test database for alias 'default'...
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,115 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
_____________________ test_register_duplicate_email_phone _____________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C54B30>
|
||||
|
||||
def test_register_duplicate_email_phone(api):
|
||||
# 1) Ðåãèñòðèðóåì ïåðâîãî ïîëüçîâàòåëÿ (ïîêà íå ïîäòâåðæä¸í)
|
||||
payload = register_payload()
|
||||
> assert register(api, **payload).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601C54B30>, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...})
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:149: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,550 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
____________________ test_register_accepts_optional_fields ____________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C56780>
|
||||
|
||||
def test_register_accepts_optional_fields(api):
|
||||
resp = register(
|
||||
api,
|
||||
patronymic="Ïåòðîâè÷",
|
||||
gender=1,
|
||||
whatsapp="+79995550011", # ìîæíî óêàçàòü äðóãîé êîíòàêò
|
||||
telegram="", # òåëåãðàì ìîæíî íå óêàçûâàòü, ò.ê. whatsapp åñòü
|
||||
)
|
||||
> assert resp.status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:183: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,563 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '', 'patronymic': 'Ïåòðîâè÷', 'gender': 1, 'whatsapp': '+79995550011'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')]}
|
||||
__________________ test_verify_email_success_activates_user ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C54530>
|
||||
|
||||
def test_verify_email_success_activates_user(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601C54530>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:190: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,574 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
________________________ test_verify_email_wrong_code _________________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601DB01A0>
|
||||
|
||||
def test_verify_email_wrong_code(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601DB01A0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:202: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,586 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________ test_resend_email_sends_new_code_and_old_becomes_invalid ___________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601CCEA50>
|
||||
|
||||
def test_resend_email_sends_new_code_and_old_becomes_invalid(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601CCEA50>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:212: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,598 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
___________ test_verify_expired_more_than_5_minutes_deletes_account ___________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601CCC290>
|
||||
settings = <pytest_django.fixtures.SettingsWrapper object at 0x0000013601CCF2F0>
|
||||
|
||||
def test_verify_expired_more_than_5_minutes_deletes_account(api, settings):
|
||||
# Ðåãèñòðèðóåì
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601CCC290>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:239: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,610 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_email_denied_before_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x00000136007A18E0>
|
||||
|
||||
def test_login_by_email_denied_before_verify(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x00000136007A18E0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:258: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,622 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_email_allowed_after_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x00000136007A25D0>
|
||||
|
||||
def test_login_by_email_allowed_after_verify(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x00000136007A25D0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:265: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,634 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_phone_allowed_after_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601B55490>
|
||||
|
||||
def test_login_by_phone_allowed_after_verify(api):
|
||||
# Ðåãèñòðèðóåì ñ òåëåôîíîì è ïîäòâåðæäàåì
|
||||
payload = register_payload()
|
||||
> assert register(api, **payload).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601B55490>, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...})
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:278: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,647 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________________ test_login_wrong_password __________________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601B56E40>
|
||||
|
||||
def test_login_wrong_password(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601B56E40>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:290: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,659 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
-------------------------- Captured stderr teardown ---------------------------
|
||||
Destroying test database for alias 'default'...
|
||||
============================== warnings summary ===============================
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: tagMap is deprecated. Please use TAG_MAP instead.
|
||||
warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning)
|
||||
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: typeMap is deprecated. Please use TYPE_MAP instead.
|
||||
warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
=========================== short test summary info ===========================
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_wrong_password
|
||||
================== 11 failed, 8 passed, 2 warnings in 7.69s ===================
|
||||
@ -1,16 +0,0 @@
|
||||
from schedule.models import Calendar, Event
|
||||
|
||||
print("\n=== КАЛЕНДАРИ В БД ===")
|
||||
calendars = Calendar.objects.all()
|
||||
print(f"Всего календарей: {calendars.count()}")
|
||||
for cal in calendars:
|
||||
events_count = Event.objects.filter(calendar=cal).count()
|
||||
print(f" - ID: {cal.id}, Name: {cal.name}, Slug: {cal.slug}, События: {events_count}")
|
||||
|
||||
print("\n=== СОБЫТИЯ В БД ===")
|
||||
events = Event.objects.all()
|
||||
print(f"Всего событий: {events.count()}")
|
||||
for event in events[:10]: # Показываем первые 10
|
||||
print(f" - ID: {event.id}, Title: {event.title}, Start: {event.start}, Calendar: {event.calendar.name if event.calendar else 'None'}")
|
||||
if events.count() > 10:
|
||||
print(f" ... и ещё {events.count() - 10} событий")
|
||||
@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Создание тестовых календарей и событий для проверки UI"""
|
||||
|
||||
from schedule.models import Calendar, Event
|
||||
from employees.models import Employee
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
# Часовой пояс
|
||||
tz = pytz.timezone('Europe/Moscow')
|
||||
|
||||
# Получаем первого пользователя
|
||||
try:
|
||||
user = Employee.objects.first()
|
||||
if not user:
|
||||
print("❌ Нет пользователей в системе")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка получения пользователя: {e}")
|
||||
exit(1)
|
||||
|
||||
print(f"✅ Используем пользователя: {user.username}")
|
||||
|
||||
# Создаём календари
|
||||
calendars_data = [
|
||||
{"name": "Рабочий календарь", "slug": "work"},
|
||||
{"name": "Личные дела", "slug": "personal"},
|
||||
{"name": "Встречи", "slug": "meetings"},
|
||||
]
|
||||
|
||||
calendars = []
|
||||
for data in calendars_data:
|
||||
cal, created = Calendar.objects.get_or_create(
|
||||
slug=data["slug"],
|
||||
defaults={"name": data["name"]}
|
||||
)
|
||||
calendars.append(cal)
|
||||
status = "создан" if created else "уже существует"
|
||||
print(f"📅 Календарь '{cal.name}' ({data['slug']}): {status}")
|
||||
|
||||
# Создаём тестовые события
|
||||
now = datetime.now(tz)
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
events_data = [
|
||||
# Рабочий календарь
|
||||
{
|
||||
"calendar": calendars[0],
|
||||
"title": "Планёрка",
|
||||
"description": "Еженедельное совещание команды",
|
||||
"start": today.replace(hour=10, minute=0),
|
||||
"end": today.replace(hour=11, minute=0),
|
||||
"color_event": "#3b82f6",
|
||||
},
|
||||
{
|
||||
"calendar": calendars[0],
|
||||
"title": "Дедлайн проекта",
|
||||
"description": "Завершение первого этапа",
|
||||
"start": today + timedelta(days=3, hours=18),
|
||||
"end": today + timedelta(days=3, hours=19),
|
||||
"color_event": "#ef4444",
|
||||
},
|
||||
{
|
||||
"calendar": calendars[0],
|
||||
"title": "Код-ревью",
|
||||
"description": "Проверка pull requests",
|
||||
"start": today + timedelta(days=1, hours=15),
|
||||
"end": today + timedelta(days=1, hours=16),
|
||||
"color_event": "#8b5cf6",
|
||||
},
|
||||
|
||||
# Личные дела
|
||||
{
|
||||
"calendar": calendars[1],
|
||||
"title": "Тренировка в зале",
|
||||
"description": "",
|
||||
"start": today.replace(hour=19, minute=0),
|
||||
"end": today.replace(hour=20, minute=30),
|
||||
"color_event": "#10b981",
|
||||
},
|
||||
{
|
||||
"calendar": calendars[1],
|
||||
"title": "День рождения друга",
|
||||
"description": "Купить подарок",
|
||||
"start": today + timedelta(days=5, hours=18),
|
||||
"end": today + timedelta(days=5, hours=23),
|
||||
"color_event": "#f59e0b",
|
||||
},
|
||||
|
||||
# Встречи
|
||||
{
|
||||
"calendar": calendars[2],
|
||||
"title": "Встреча с клиентом",
|
||||
"description": "Обсуждение нового проекта",
|
||||
"start": today + timedelta(days=2, hours=14),
|
||||
"end": today + timedelta(days=2, hours=15, minutes=30),
|
||||
"color_event": "#06b6d4",
|
||||
},
|
||||
{
|
||||
"calendar": calendars[2],
|
||||
"title": "Собеседование кандидата",
|
||||
"description": "Senior Python Developer",
|
||||
"start": today + timedelta(days=4, hours=11),
|
||||
"end": today + timedelta(days=4, hours=12),
|
||||
"color_event": "#ec4899",
|
||||
},
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for event_data in events_data:
|
||||
event, created = Event.objects.get_or_create(
|
||||
calendar=event_data["calendar"],
|
||||
title=event_data["title"],
|
||||
start=event_data["start"],
|
||||
defaults={
|
||||
"description": event_data["description"],
|
||||
"end": event_data["end"],
|
||||
"color_event": event_data["color_event"],
|
||||
"creator": user,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
print(f"✅ Создано событие: {event.title} ({event_data['calendar'].name})")
|
||||
|
||||
print(f"\n🎉 Готово! Создано {created_count} новых событий")
|
||||
print(f"📊 Всего календарей: {Calendar.objects.count()}")
|
||||
print(f"📊 Всего событий: {Event.objects.count()}")
|
||||
Loading…
x
Reference in New Issue
Block a user