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:
2026-01-21 11:47:26 +01:00
parent 06b30fca02
commit f121445313
72 changed files with 9258 additions and 594 deletions

View File

@@ -0,0 +1,186 @@
"""Тесты для BulkOperationsMixin и QueryOptimizerMixin."""
from apps.core.models import BackgroundJob
from apps.core.services import (
BulkOperationsMixin,
QueryOptimizerMixin,
)
from django.test import TestCase
from faker import Faker
fake = Faker()
class BulkOperationsMixinTest(TestCase):
"""Тесты для BulkOperationsMixin."""
def test_mixin_has_bulk_create_chunked(self):
"""Проверка наличия метода bulk_create_chunked."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_create_chunked"))
def test_mixin_has_bulk_update_or_create(self):
"""Проверка наличия метода bulk_update_or_create."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_update_or_create"))
def test_mixin_has_bulk_delete(self):
"""Проверка наличия метода bulk_delete."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_delete"))
def test_mixin_has_bulk_update_fields(self):
"""Проверка наличия метода bulk_update_fields."""
self.assertTrue(hasattr(BulkOperationsMixin, "bulk_update_fields"))
class QueryOptimizerMixinTest(TestCase):
"""Тесты для QueryOptimizerMixin."""
def test_mixin_has_get_optimized_queryset(self):
"""Проверка наличия метода get_optimized_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_optimized_queryset"))
def test_mixin_has_apply_optimizations(self):
"""Проверка наличия метода apply_optimizations."""
self.assertTrue(hasattr(QueryOptimizerMixin, "apply_optimizations"))
def test_mixin_has_get_list_queryset(self):
"""Проверка наличия метода get_list_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_list_queryset"))
def test_mixin_has_get_detail_queryset(self):
"""Проверка наличия метода get_detail_queryset."""
self.assertTrue(hasattr(QueryOptimizerMixin, "get_detail_queryset"))
def test_mixin_has_with_counts(self):
"""Проверка наличия метода with_counts."""
self.assertTrue(hasattr(QueryOptimizerMixin, "with_counts"))
def test_mixin_has_with_exists(self):
"""Проверка наличия метода with_exists."""
self.assertTrue(hasattr(QueryOptimizerMixin, "with_exists"))
def test_mixin_default_attributes(self):
"""Проверка атрибутов по умолчанию."""
self.assertEqual(QueryOptimizerMixin.select_related, [])
self.assertEqual(QueryOptimizerMixin.prefetch_related, [])
self.assertEqual(QueryOptimizerMixin.default_only, [])
self.assertEqual(QueryOptimizerMixin.default_defer, [])
class BulkOperationsIntegrationTest(TestCase):
"""Интеграционные тесты для bulk операций с BackgroundJob."""
def test_bulk_create_chunked(self):
"""Тест массового создания чанками."""
# Создаём тестовый сервис с BulkOperationsMixin
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём 10 объектов чанками по 3
jobs = [
BackgroundJob(
task_id=f"bulk-chunk-{i}",
task_name="test.bulk.task",
)
for i in range(10)
]
count = TestService.bulk_create_chunked(jobs, chunk_size=3)
self.assertEqual(count, 10)
# Проверяем что все созданы
self.assertEqual(BackgroundJob.objects.filter(task_name="test.bulk.task").count(), 10)
def test_bulk_delete(self):
"""Тест массового удаления."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём несколько задач
jobs = []
for i in range(5):
job = BackgroundJob.objects.create(
task_id=f"bulk-delete-{i}",
task_name="test.delete.task",
)
jobs.append(job)
# Удаляем первые 3
ids_to_delete = [j.pk for j in jobs[:3]]
deleted = TestService.bulk_delete(ids_to_delete)
self.assertEqual(deleted, 3)
self.assertEqual(BackgroundJob.objects.filter(task_name="test.delete.task").count(), 2)
def test_bulk_update_fields(self):
"""Тест массового обновления полей."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём задачи
for i in range(5):
BackgroundJob.objects.create(
task_id=f"bulk-update-{i}",
task_name="test.update.task",
progress=0,
)
# Обновляем все задачи этого типа
updated = TestService.bulk_update_fields(
filters={"task_name": "test.update.task"},
updates={"progress": 50},
)
self.assertEqual(updated, 5)
# Проверяем что обновились
for job in BackgroundJob.objects.filter(task_name="test.update.task"):
self.assertEqual(job.progress, 50)
def test_bulk_update_or_create_creates(self):
"""Тест upsert - создание новых."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
items = [
{"task_id": "upsert-new-1", "task_name": "upsert.task", "progress": 10},
{"task_id": "upsert-new-2", "task_name": "upsert.task", "progress": 20},
]
created, updated = TestService.bulk_update_or_create(
items=items,
unique_fields=["task_id"],
update_fields=["task_name", "progress"],
)
self.assertEqual(created, 2)
self.assertEqual(updated, 0)
def test_bulk_update_or_create_updates(self):
"""Тест upsert - обновление существующих."""
class TestService(BulkOperationsMixin):
model = BackgroundJob
# Создаём существующую запись
BackgroundJob.objects.create(
task_id="upsert-existing",
task_name="old.task",
progress=0,
)
items = [
{"task_id": "upsert-existing", "task_name": "new.task", "progress": 100},
]
created, updated = TestService.bulk_update_or_create(
items=items,
unique_fields=["task_id"],
update_fields=["task_name", "progress"],
)
self.assertEqual(created, 0)
self.assertEqual(updated, 1)
# Проверяем обновление
job = BackgroundJob.objects.get(task_id="upsert-existing")
self.assertEqual(job.task_name, "new.task")
self.assertEqual(job.progress, 100)