feat(core): add core module with mixins, services, and background jobs
- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc. - Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin - Add Base ViewSets with bulk operations - Add BackgroundJob model for Celery task tracking - Add BaseAppCommand for management commands - Add permissions, pagination, filters, cache, logging - Migrate tests to factory_boy + faker - Add CHANGELOG.md - 297 tests passing
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## 0) Язык и стиль общения (СТРОГО)
|
||||
- ИИ-агент **ВСЕГДА отвечает на русском языке**
|
||||
@@ -264,5 +261,162 @@ systemctl restart apache2
|
||||
- объясняет, почему оно не подходит
|
||||
- запрашивает разрешение в чате
|
||||
|
||||
## 15) Структура проекта
|
||||
-
|
||||
## 15) Структура проекта и миксины (ОБЯЗАТЕЛЬНО)
|
||||
|
||||
### 15.0 Правило Core-First (КРИТИЧНО)
|
||||
**ПЕРЕД созданием любого нового компонента** агент ОБЯЗАН проверить модуль `apps.core`:
|
||||
|
||||
```
|
||||
src/apps/core/
|
||||
├── mixins.py # Model mixins (TimestampMixin, SoftDeleteMixin, etc.)
|
||||
├── services.py # BaseService, BackgroundJobService
|
||||
├── views.py # Health checks, BackgroundJob API
|
||||
├── viewsets.py # BaseViewSet, ReadOnlyViewSet
|
||||
├── exceptions.py # APIError, NotFoundError, ValidationError
|
||||
├── permissions.py # IsOwner, IsAdminOrReadOnly, etc.
|
||||
├── pagination.py # CursorPagination
|
||||
├── filters.py # BaseFilterSet
|
||||
├── cache.py # cache_result, invalidate_cache
|
||||
├── tasks.py # BaseTask для Celery
|
||||
├── logging.py # StructuredLogger
|
||||
├── middleware.py # RequestIDMiddleware
|
||||
├── signals.py # SignalDispatcher
|
||||
├── responses.py # APIResponse wrapper
|
||||
├── openapi.py # api_docs decorator
|
||||
└── management/commands/base.py # BaseAppCommand
|
||||
```
|
||||
|
||||
**Порядок действий:**
|
||||
1. Проверить `apps.core` на наличие нужного базового класса/миксина
|
||||
2. Наследоваться от существующего, а не создавать с нуля
|
||||
3. Если нужного нет — обсудить добавление в core
|
||||
|
||||
❌ **ЗАПРЕЩЕНО:** создавать дублирующую функциональность в app-модулях
|
||||
|
||||
---
|
||||
|
||||
### 15.1 Model Mixins
|
||||
При создании моделей **ОБЯЗАТЕЛЬНО** использовать миксины из `apps.core.mixins`:
|
||||
|
||||
| Миксин | Когда использовать | Поля |
|
||||
|--------|-------------------|------|
|
||||
| `TimestampMixin` | **ВСЕГДА** для любой модели | `created_at`, `updated_at` |
|
||||
| `UUIDPrimaryKeyMixin` | Когда нужен UUID вместо int ID | `id` (UUID) |
|
||||
| `SoftDeleteMixin` | Когда нельзя физически удалять | `is_deleted`, `deleted_at` |
|
||||
| `AuditMixin` | Когда нужно знать кто создал/изменил | `created_by`, `updated_by` |
|
||||
| `OrderableMixin` | Для сортируемых списков | `order` |
|
||||
| `StatusMixin` | Для моделей со статусами | `status` |
|
||||
| `SlugMixin` | Для URL-friendly идентификаторов | `slug` |
|
||||
|
||||
**Пример правильного использования:**
|
||||
```python
|
||||
from apps.core.mixins import TimestampMixin, SoftDeleteMixin, AuditMixin
|
||||
|
||||
class Document(TimestampMixin, SoftDeleteMixin, AuditMixin, models.Model):
|
||||
"""Документ с историей и мягким удалением."""
|
||||
title = models.CharField(max_length=200)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
```
|
||||
|
||||
**Порядок наследования миксинов:**
|
||||
1. `UUIDPrimaryKeyMixin` (если нужен)
|
||||
2. `TimestampMixin`
|
||||
3. `SoftDeleteMixin` (если нужен)
|
||||
4. `AuditMixin` (если нужен)
|
||||
5. `OrderableMixin` / `StatusMixin` / `SlugMixin`
|
||||
6. `models.Model` (последним)
|
||||
|
||||
---
|
||||
|
||||
### 15.2 Management Commands
|
||||
Все management commands наследуются от `BaseAppCommand`:
|
||||
|
||||
```python
|
||||
from apps.core.management.commands.base import BaseAppCommand
|
||||
|
||||
class Command(BaseAppCommand):
|
||||
help = 'Описание команды'
|
||||
use_transaction = True # Обернуть в транзакцию
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser) # Добавляет --dry-run, --silent
|
||||
parser.add_argument('--my-arg', type=str)
|
||||
|
||||
def execute_command(self, *args, **options):
|
||||
items = MyModel.objects.all()
|
||||
|
||||
for item in self.progress_iter(items, desc="Обработка"):
|
||||
if not self.dry_run:
|
||||
self.process(item)
|
||||
|
||||
return "Обработано успешно"
|
||||
```
|
||||
|
||||
**Возможности BaseAppCommand:**
|
||||
- `--dry-run` — тестовый запуск без изменений
|
||||
- `--silent` — минимальный вывод
|
||||
- `self.progress_iter()` — прогресс-бар
|
||||
- `self.timed_operation()` — измерение времени
|
||||
- `self.confirm()` — подтверждение
|
||||
- `self.log_info/success/warning/error()` — логирование
|
||||
|
||||
---
|
||||
|
||||
### 15.3 Background Jobs (Celery)
|
||||
Для отслеживания статуса фоновых задач использовать `BackgroundJob`:
|
||||
|
||||
```python
|
||||
# В сервисе при запуске задачи
|
||||
from apps.core.services import BackgroundJobService
|
||||
|
||||
job = BackgroundJobService.create_job(
|
||||
task_id=task.id,
|
||||
task_name="apps.myapp.tasks.process_data",
|
||||
user_id=request.user.id,
|
||||
)
|
||||
|
||||
# В Celery таске
|
||||
from apps.core.models import BackgroundJob
|
||||
|
||||
@shared_task(bind=True)
|
||||
def my_task(self, data):
|
||||
job = BackgroundJob.objects.get(task_id=self.request.id)
|
||||
job.mark_started()
|
||||
|
||||
for i, item in enumerate(items):
|
||||
process(item)
|
||||
job.update_progress(i * 100 // len(items), "Обработка...")
|
||||
|
||||
job.complete(result={"processed": len(items)})
|
||||
```
|
||||
|
||||
**API эндпоинты:**
|
||||
- `GET /api/v1/jobs/` — список задач пользователя
|
||||
- `GET /api/v1/jobs/{task_id}/` — статус конкретной задачи
|
||||
|
||||
---
|
||||
|
||||
### 15.4 Factories (тестирование)
|
||||
Все фабрики используют `factory_boy` + `faker`:
|
||||
|
||||
```python
|
||||
import factory
|
||||
from faker import Faker
|
||||
|
||||
fake = Faker("ru_RU")
|
||||
|
||||
class MyModelFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = MyModel
|
||||
|
||||
name = factory.LazyAttribute(lambda _: fake.word())
|
||||
email = factory.LazyAttribute(lambda _: fake.unique.email())
|
||||
```
|
||||
|
||||
**Правила:**
|
||||
- Никакого хардкода в тестах (`"test@example.com"` → `fake.email()`)
|
||||
- Использовать `fake.unique.*` для уникальных полей
|
||||
- Локаль: `Faker("ru_RU")` для русских данных
|
||||
|
||||
|
||||
Reference in New Issue
Block a user