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:
20
src/config/api_v1_urls.py
Normal file
20
src/config/api_v1_urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
API v1 URL configuration.
|
||||
|
||||
All API endpoints are versioned under /api/v1/
|
||||
"""
|
||||
|
||||
from apps.core.views import BackgroundJobListView, BackgroundJobStatusView
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api_v1"
|
||||
|
||||
jobs_urlpatterns = [
|
||||
path("", BackgroundJobListView.as_view(), name="job-list"),
|
||||
path("<str:task_id>/", BackgroundJobStatusView.as_view(), name="job-status"),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.user.urls")),
|
||||
path("jobs/", include((jobs_urlpatterns, "jobs"))),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
import sys
|
||||
|
||||
from django.test.runner import DiscoverRunner
|
||||
|
||||
|
||||
class CustomTestRunner(DiscoverRunner):
|
||||
"""Custom test runner that avoids ipdb import issues"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Отключаем использование ipdb
|
||||
import os
|
||||
|
||||
os.environ["PYTHONBREAKPOINT"] = "pdb.set_trace"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def run_tests(self, test_labels, extra_tests=None, **kwargs):
|
||||
# Проверяем, что ipdb не будет импортирован
|
||||
# Создаем mock-модуль вместо None
|
||||
mock_ipdb = type("MockModule", (), {"__getattr__": lambda s, n: None})()
|
||||
sys.modules["ipdb"] = mock_ipdb
|
||||
|
||||
try:
|
||||
return super().run_tests(test_labels, extra_tests, **kwargs)
|
||||
finally:
|
||||
# Восстанавливаем модуль если был
|
||||
if "ipdb" in sys.modules:
|
||||
del sys.modules["ipdb"]
|
||||
@@ -11,6 +11,9 @@ from decouple import Config, RepositoryEnv
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# Application version
|
||||
APP_VERSION = "1.0.0"
|
||||
|
||||
# Load environment variables
|
||||
ENV_FILE = BASE_DIR / ".env"
|
||||
if ENV_FILE.exists():
|
||||
@@ -50,15 +53,18 @@ INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
# Third-party apps
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"corsheaders",
|
||||
"django_celery_beat",
|
||||
"django_celery_results",
|
||||
"drf_yasg",
|
||||
# Local apps
|
||||
"apps.core",
|
||||
"apps.user",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"apps.core.middleware.RequestIDMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
@@ -104,6 +110,17 @@ DATABASES = {
|
||||
},
|
||||
}
|
||||
|
||||
# Cache configuration
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": get_env("REDIS_URL", "redis://localhost:6379/0"),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
@@ -152,12 +169,27 @@ REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"DEFAULT_FILTER_BACKENDS": [
|
||||
"django_filters.rest_framework.DjangoFilterBackend",
|
||||
"rest_framework.filters.SearchFilter",
|
||||
"rest_framework.filters.OrderingFilter",
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "apps.core.pagination.StandardPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
"rest_framework.renderers.BrowsableAPIRenderer",
|
||||
],
|
||||
"EXCEPTION_HANDLER": "apps.core.exception_handler.custom_exception_handler",
|
||||
# Rate limiting
|
||||
"DEFAULT_THROTTLE_CLASSES": [
|
||||
"rest_framework.throttling.AnonRateThrottle",
|
||||
"rest_framework.throttling.UserRateThrottle",
|
||||
],
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"anon": "100/hour",
|
||||
"user": "1000/hour",
|
||||
},
|
||||
}
|
||||
|
||||
# JWT settings
|
||||
@@ -237,6 +269,3 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Test runner configuration
|
||||
TEST_RUNNER = "config.custom_test_runner.CustomTestRunner"
|
||||
|
||||
105
src/config/settings/test.py
Normal file
105
src/config/settings/test.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from .base import *
|
||||
|
||||
# Test settings
|
||||
SECRET_KEY = "django-insecure-test-key-only-for-testing" # noqa: S105
|
||||
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0", "testserver"] # noqa: S104
|
||||
|
||||
# Use in-memory SQLite database for faster tests
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
"TEST": {
|
||||
"NAME": ":memory:",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Disable migrations for faster tests
|
||||
class DisableMigrations:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def __getitem__(self, item):
|
||||
return None
|
||||
|
||||
|
||||
MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
# Cache configuration for tests (use local memory)
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
}
|
||||
}
|
||||
|
||||
# Email backend for tests
|
||||
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
# Celery Configuration for Tests (use eager execution)
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
CELERY_TASK_EAGER_PROPAGATES = True
|
||||
CELERY_BROKER_URL = "memory://"
|
||||
CELERY_RESULT_BACKEND = "cache+memory://"
|
||||
|
||||
# Password hashers - use fast hasher for tests
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
|
||||
# Disable logging during tests
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"null": {
|
||||
"class": "logging.NullHandler",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["null"],
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["null"],
|
||||
"propagate": False,
|
||||
},
|
||||
"django.request": {
|
||||
"handlers": ["null"],
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Media files for tests
|
||||
MEDIA_ROOT = "/tmp/test_media" # noqa: S108
|
||||
|
||||
# Static files for tests
|
||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||
|
||||
# Disable CSRF for API tests and disable throttling
|
||||
REST_FRAMEWORK = {
|
||||
**globals().get("REST_FRAMEWORK", {}),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
],
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "json",
|
||||
# Disable throttling for tests
|
||||
"DEFAULT_THROTTLE_CLASSES": [],
|
||||
"DEFAULT_THROTTLE_RATES": {},
|
||||
}
|
||||
|
||||
# JWT settings for tests
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
**globals().get("SIMPLE_JWT", {}),
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
"ROTATE_REFRESH_TOKENS": True,
|
||||
}
|
||||
@@ -27,16 +27,15 @@ schema_view = get_schema_view(
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/users/", include("apps.user.urls")),
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
# Swagger documentation
|
||||
path(
|
||||
"swagger/",
|
||||
"",
|
||||
schema_view.with_ui("swagger", cache_timeout=0),
|
||||
name="schema-swagger-ui",
|
||||
),
|
||||
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("health/", include("apps.core.urls")),
|
||||
path("api/v1/", include("config.api_v1_urls", namespace="api_v1")),
|
||||
path("auth/", include("rest_framework.urls")),
|
||||
]
|
||||
|
||||
# Serve media files in development
|
||||
|
||||
Reference in New Issue
Block a user