Files
core/orm/notification.py
Untone 1b48675b92
Some checks failed
Deploy on push / deploy (push) Failing after 2m22s
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`

### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода

### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки

### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации

### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00

140 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from enum import Enum
from typing import Any
from sqlalchemy import JSON, DateTime, ForeignKey, Index, Integer, PrimaryKeyConstraint, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
# Импорт Author отложен для избежания циклических импортов
from orm.author import Author
from orm.base import BaseModel as Base
from utils.logger import root_logger as logger
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в запросах"""
return Author
class NotificationEntity(Enum):
"""
Перечисление сущностей для уведомлений.
Определяет типы объектов, к которым относятся уведомления.
"""
TOPIC = "topic"
COMMENT = "comment"
SHOUT = "shout"
AUTHOR = "author"
COMMUNITY = "community"
REACTION = "reaction"
@classmethod
def from_string(cls, value: str) -> "NotificationEntity":
"""
Создает экземпляр сущности уведомления из строки.
Args:
value (str): Строковое представление сущности.
Returns:
NotificationEntity: Экземпляр сущности уведомления.
"""
try:
return cls(value)
except ValueError:
logger.error(f"Неверная сущность уведомления: {value}")
raise ValueError("Неверная сущность уведомления") # noqa: B904
class NotificationAction(Enum):
"""
Перечисление действий для уведомлений.
Определяет типы событий, которые могут происходить с сущностями.
"""
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
MENTION = "mention"
REACT = "react"
FOLLOW = "follow"
INVITE = "invite"
@classmethod
def from_string(cls, value: str) -> "NotificationAction":
"""
Создает экземпляр действия уведомления из строки.
Args:
value (str): Строковое представление действия.
Returns:
NotificationAction: Экземпляр действия уведомления.
"""
try:
return cls(value)
except ValueError:
logger.error(f"Неверное действие уведомления: {value}")
raise ValueError("Неверное действие уведомления") # noqa: B904
# Оставляем для обратной совместимости
NotificationStatus = Enum("NotificationStatus", ["UNREAD", "READ", "ARCHIVED"])
NotificationKind = NotificationAction # Для совместимости со старым кодом
class NotificationSeen(Base):
__tablename__ = "notification_seen"
viewer: Mapped[int] = mapped_column(ForeignKey("author.id"))
notification: Mapped[int] = mapped_column(ForeignKey("notification.id"))
__table_args__ = (
PrimaryKeyConstraint(viewer, notification),
Index("idx_notification_seen_viewer", "viewer"),
Index("idx_notification_seen_notification", "notification"),
{"extend_existing": True},
)
class Notification(Base):
__tablename__ = "notification"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
entity: Mapped[str] = mapped_column(String, nullable=False)
action: Mapped[str] = mapped_column(String, nullable=False)
payload: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
status: Mapped[NotificationStatus] = mapped_column(default=NotificationStatus.UNREAD)
kind: Mapped[NotificationKind] = mapped_column(nullable=False)
seen = relationship("Author", secondary="notification_seen")
__table_args__ = (
Index("idx_notification_created_at", "created_at"),
Index("idx_notification_status", "status"),
Index("idx_notification_kind", "kind"),
{"extend_existing": True},
)
def set_entity(self, entity: NotificationEntity) -> None:
"""Устанавливает сущность уведомления."""
self.entity = entity.value
def get_entity(self) -> NotificationEntity:
"""Возвращает сущность уведомления."""
return NotificationEntity.from_string(self.entity)
def set_action(self, action: NotificationAction) -> None:
"""Устанавливает действие уведомления."""
self.action = action.value
def get_action(self) -> NotificationAction:
"""Возвращает действие уведомления."""
return NotificationAction.from_string(self.action)